如何为 Python Swigged C++ 对象创建和分配回调函数

Posted

技术标签:

【中文标题】如何为 Python Swigged C++ 对象创建和分配回调函数【英文标题】:How to create and assign a callback function for a Python Swigged C++ object 【发布时间】:2018-07-16 23:16:18 【问题描述】:

我有一个 swigged C++ 类(充当带有事件的插件),它带有指向可分配回调函数的指针,例如

typedef void (*PluginEvent)(void* data1, void* data2);

class PluginWithEvents :  public Plugin

    public:
        bool assignOnProgressEvent(PluginEvent pluginsProgress, void* userData1 = nullptr, void* userData2 = nullptr);
        void workProgressEvent(void* data1, void* data2);
    protected:
        PluginEvent mWorkProgressEvent;
        void* mWorkProgressData1;
        void* mWorkProgressData2;
;

及实现代码

void PluginWithEvents::workProgressEvent(void* data1, void* data2)

    if(mWorkProgressEvent)
    
        mWorkProgressEvent(data1, data2);
    


bool PluginWithEvents::assignOnProgressEvent(PluginEvent progress, void* userData1, void* userData2)

    mWorkProgressEvent = progress;
    mWorkProgressData1 = userData1;
    mWorkProgressData2 = userData2;
    return true;

问题是,在Python中使用这个类时,如何定义要传递给assignProgressEvent函数的回调函数?

以下 Python 代码给出错误:

NotImplementedError: Wrong number or type of arguments for overloaded 
function 'PluginWithEvents_assignOnProgressEvent'.

Possible C/C++ prototypes are:
PluginWithEvents::assignOnProgressEvent(dsl::PluginEvent,void *,void *)
PluginWithEvents::assignOnProgressEvent(dsl::PluginEvent,void *)
PluginWithEvents::assignOnProgressEvent(dsl::PluginEvent)

【问题讨论】:

您想将 Python 函数作为回调传递,还是通过 Python 接口传递 C++ 函数指针? 在 Python 中,如果可能的话,可以很好地传递一个自定义 Python 函数? 这是可能的,我已经完成了something similar before,但是映射到C++函数指针并不好玩,并且会涉及全局静态变量(这很容易导致链接和并发问题)。 感谢您的示例。我会仔细看看。看起来相当复杂。 :) 那些投反对票并投票关闭而不发表评论的人是最好的。 【参考方案1】:

我已经准备了一个我在 cmets 中提到的示例。它变得非常可怕,而且可能相当脆弱。有很多地方可以显着改进错误检查。

在接口文件中,我包含后端代码 (test.hpp),这或多或少是您的问题中的代码以及 Python 回调所需的工具。所有令人讨厌的细节都隐藏在python_callback.hpp中。

然后我声明一个 global 变量 callback 保存当前回调和一个函数 callback_caller 调用回调。您已经可以在此处注意到这种方法的一个缺点。任何时候最多只能有一个回调。因此,不要将多个回调传递给函数,也不要持有指向 callback 的引用或指针(副本可能没问题,但不能保证)。

剩下的就是将 Python 函数映射到 C++ 函数指针的类型映射。


test.i

%module example
%
#include <iostream>

#include "test.hpp"
#include "python_callback.hpp"

PythonCallback callback;

void callback_caller(void *, void *) 
    double pi = callback.call<double>("ABC", 3.14, 42);
    std::cout << "C++ reveived: " << pi << '\n';

%

%include <exception.i>
%exception 
  try 
    $action
   catch (std::exception const &e) 
    SWIG_exception(SWIG_RuntimeError, e.what());
  


%typemap(typecheck) PluginEvent 
    $1 = PyCallable_Check($input);


%typemap(in) PluginEvent 
    callback = PythonCallback($input);
    $1 = &callback_caller;


%include "test.hpp"

test.hpp

#pragma once

typedef void (*PluginEvent)(void *data1, void *data2);

class PluginWithEvents 
public:
    bool assignOnProgressEvent(PluginEvent pluginsProgress,
                               void *userData1 = nullptr,
                               void *userData2 = nullptr) 
        mWorkProgressEvent = pluginsProgress;
        mWorkProgressData1 = userData1;
        mWorkProgressData2 = userData2;
        return true;
    
    void workProgressEvent(void *data1, void *data2) 
        if (mWorkProgressEvent) 
            mWorkProgressEvent(data1, data2);
        
    

protected:
    PluginEvent mWorkProgressEvent = nullptr;
    void *mWorkProgressData1 = nullptr;
    void *mWorkProgressData2 = nullptr;
;

python_callback.hpp

这个文件非常不完整,因为所有不同的 Python 和 C++ 类型之间的映射都丢失了。我添加了一些映射(PyFloatdouble,PyInttoint, PyStringstd::string),为您提供了如何使用自己的映射扩展代码的蓝图。

#pragma once
#include <Python.h>

#include <stdexcept>
#include <string>
#include <type_traits>

namespace internal 

// Convert C++ type to Python (add your favourite overloads)

inline PyObject *arg_to_python(double x)  return PyFloat_FromDouble(x); 
inline PyObject *arg_to_python(int v)  return PyInt_FromLong(v); 
inline PyObject *arg_to_python(std::string const &s) 
    return PyString_FromStringAndSize(s.c_str(), s.size());


// Convert Python type to C++ (add your favourite specializations)

template <typename T>
struct return_from_python 
    static T convert(PyObject *);
;

template <>
void return_from_python<void>::convert(PyObject *) 

template <>
double return_from_python<double>::convert(PyObject *result) 
    if (!PyFloat_Check(result)) 
        throw std::invalid_argument("type is not PyFloat");
    
    return PyFloat_AsDouble(result);


template <>
int return_from_python<int>::convert(PyObject *result) 
    if (!PyInt_Check(result)) 
        throw std::invalid_argument("type is not PyInt");
    
    return PyInt_AsLong(result);


template <>
std::string return_from_python<std::string>::convert(PyObject *result) 
    char *buffer;
    Py_ssize_t len;
    if (PyString_AsStringAndSize(result, &buffer, &len) == -1) 
        throw std::invalid_argument("type is not PyString");
    
    return std::stringbuffer, static_cast<std::size_t>(len);


// Scope Guard

template <typename F>
struct ScopeGuard_impl 
    F f;
    ScopeGuard_impl(F f) : f(std::move(f)) 
    ~ScopeGuard_impl()  f(); 
;

template <typename F>
inline ScopeGuard_impl<F> ScopeGuard(F &&f) 
    return ScopeGuard_impl<F>std::forward<F>(f);


 // namespace internal

class PythonCallback 
    PyObject *callable = nullptr;

public:
    PythonCallback() = default;
    PythonCallback(PyObject *obj) : callable(obj)  Py_INCREF(obj); 
    ~PythonCallback()  Py_XDECREF(callable); 

    PythonCallback(PythonCallback const &other) : callable(other.callable) 
        Py_INCREF(callable);
    

    PythonCallback &operator=(PythonCallback other) noexcept 
        if (this != &other) 
            std::swap(this->callable, other.callable);
        
        return *this;
    

    // Function caller
    template <typename ReturnType, typename... Args>
    ReturnType call(Args const &... args) 
        using internal::arg_to_python;
        using internal::return_from_python;
        using internal::ScopeGuard;

        PyGILState_STATE gil = PyGILState_Ensure();
        auto gil_ = ScopeGuard([&]()  PyGILState_Release(gil); );

        PyObject *const result = PyObject_CallFunctionObjArgs(
            callable, arg_to_python(args)..., nullptr);
        auto result_ = ScopeGuard([&]()  Py_XDECREF(result); );

        if (result == nullptr) 
            throw std::runtime_error("Executing Python callback failed!");
        

        return return_from_python<ReturnType>::convert(result);
    
;

我们可以使用一个希望打印“Hello World!”的小脚本来测试上述设置。

test.py

from example import *

p = PluginWithEvents()

def callback(a, b, c):
    print("Hello World!")
    print(a,type(a))
    print(b,type(b))
    print(c,type(c))
    return 3.14

p.assignOnProgressEvent(callback)
p.workProgressEvent(None,None)

让我们试试吧

$ swig -c++ -python test.i
$ clang++ -Wall -Wextra -Wpedantic -std=c++11 -I /usr/include/python2.7/ -fPIC -shared test_wrap.cxx -o _example.so -lpython2.7
$ python test.py
Hello World!
('ABC', <type 'str'>)
(3.14, <type 'float'>)
(42L, <type 'long'>)
C++ reveived: 3.14

【讨论】:

谢谢亨利,太棒了!我需要一些时间来掌握模板的东西!如果我可以让它与我的旧编译器 bcc32 一起工作,我会使用它。在示例中,回调没有任何参数。我假设它可以接受参数,并且 return_from_python 就是为此? 如果一天左右没有其他答案提交,我会将此标记为答案! @TotteKarlsson 我用更简单的 Python 对象调用、更多类型映射和返回值展示更新了我的答案。 @TotteKarlsson 顺便说一句,这在 Python 3 中不起作用,因为 C-API 在 2 和 3 之间发生了很大变化。尤其是整数都是 PyLong 而不是 PyInt 和所有字符串是 unicode 而不是字节。 @TotteKarlsson 该代码也将不适用于 bcc32,因为它有no support for variadic templates and lambda expressions(并且没有初始化列表,但可以避免这些)。如果您可以使用 Clang 增强的 bcc32c,您将可以使用所有这些功能。

以上是关于如何为 Python Swigged C++ 对象创建和分配回调函数的主要内容,如果未能解决你的问题,请参考以下文章

如何为 Python 制作 C++ 库

更改 Python swigged C 函数的返回值类型

ImportError:在 python 中导入 swigged c++-class 时未定义的符号

如何为 python C++ 嵌入构建 boost 示例

在 C++ 中,如何为向量中的每个元素创建一个新对象?

如何使用 cython 将 python 类公开给 c++