接上一期SWIG对复杂数据类型的处理,本期介绍通过SWIG将Python的回调函数正确地传递到C++代码,并由C++调用该回调函数。前面介绍过的一些SWIG基础内容,这里不再重复介绍。
往期推荐:
SWIG-Python与C++交互(二)-复杂数据类型(std::map, 自定义数据类型)
SWIG-Python与C++交互(一)-简单教程
本期主要介绍以下几种回调函数的使用场景:
- C风格的回调函数;
- 仿函数作为回调函数;
- C++多线程调用回调函数。
这三个使用场景,从前往后,由易到难,涉及的技术细节,也由少变多。
00 C风格的回调函数
- data_processor.h文件
classDataProcessor{public:DataProcessor(){}~DataProcessor(){}//定义一个用C风格回调函数作为函数参数的接口intProcWithCallback(int data,int(*f)(int)){if(f ==NULL){
std::cout <<"f == NULL";return0;}++ data;
data =f(data);
std::cout <<"f(data)="<< data << std::endl;return data;}};
以上代码主要是定义一个C++接口,接口函数中包含一个C风格的函数指针,在C++接口内部会调用传入的函数指针。建议将接口定义写在.h文件,接口实现写在.cpp文件。这里为了方便展示,都写在了.h文件中。
- data_processor.i文件
%modulepy_data_processor
# 通过typemap转换函数指针
%typemap(in)intint(*)(int){
$1=(int(*)(int))PyInt_AsLong($input);}%{#include"data_processor.h"%}%include "data_processor.h"
.i文件中需要对函数指针进行转换,这里通过SWIG的typemap完成输入参数转换。
- py_main.py文件
# -*- coding: utf-8 -*-from data_processor import py_data_processor
import ctypes
#定义一个与C语言函数类型‘int(*) (int)’一致的python函数defsimple_func(i):print("simple_func i=%d"%(i))
i = i+1return i
defmain():
dp = py_data_processor.DataProcessor()# 定义一个与C语言函数类型‘int(*) (int)’一致的函数类型
py_callback_type = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int)# 将python函数类型转换为C类型函数指针
f = py_callback_type(simple_func)
f_ptr = ctypes.cast(f, ctypes.c_void_p).value
#调用接口,接口参数包含C类型函数指针
i = dp.ProcWithCallback(11, f_ptr)print("i=%d"%(i))returnif __name__ =="__main__":
main()
python接口调用前面定义的C++接口,其中C++接口的输入参数包含一个函数指针,这里通过ctypes.CFUNCTYPE和ctypes.cast接口,再配合.i文件中的typemap,完成将python函数转换为C风格的函数指针。
01 仿函数作为回调函数
- data_processor.h文件
#include<functional>#include<iostream>#include<string>classDataProcessor{public:// 定义一个std::function类型的函数对象类型StrFuncusing StrFunc = std::function<void(std::string)>;DataProcessor(){}~DataProcessor(){}//定义一个接口,接口参数包含StrFuncvoidProcWithCallbackString(StrFunc f){if(f ==nullptr){
std::cout <<"f == nullptr";return;}// 调用回调函数f(std::string("c++ std::string param"));}};
- data_processor.i文件
%modulepy_data_processor%{#include"data_processor.h"// 定义一个仿函数类型,// 用于将std::function类型与Python函数类型相互转换classPyCallBackString{
PyObject* func;
PyCallBackString&operator=(const PyCallBackString&);public:PyCallBackString(const PyCallBackString& o):func(o.func){// 拷贝构造函数,Python对象的计数+1Py_XINCREF(func);}PyCallBackString(PyObject* o):func(o){// 构造函数,Python对象的计数+1Py_XINCREF(func);// 通过断言,Check数据类型assert(PyCallable_Check(func));}~PyCallBackString(){// 析构函数,Python对象的计数-1Py_XDECREF(func);}// 重载运算符(),这个函数由C++调起。voidoperator()(std::string s){// 检查func对象的合法性if(!func || Py_None == func ||!PyCallable_Check(func)){return;}// 将std::string转换为PyObject_Call接口能接收的参数类型// 这里转换成了PyTuple类型,PyTuple内部包含一个string类型// 注意:PyObject_Call第二个参数需要PyTuple类型,不是string类型
PyObject* tuple =Py_BuildValue("(s)", s.c_str());
PyObject* result =PyObject_Call(func, tuple,0);// 减掉计数以防止内存泄漏Py_DECREF(tuple);Py_XDECREF(result);}};%}
# 通过SWIG的extend关键字,重载接口
%extend DataProcessor {// 此接口由Python调用,作用是将Python传递的PyObject类型数据,// 在这里是Python函数,转换为上面定义的PyCallBackString类型// SWIG会帮我们完成PyCallBackString类型到std::function的转换voidProcWithCallbackString(PyObject* callback){
$self->ProcWithCallbackString(PyCallBackString(callback));}}%include "data_processor.h"
在以上.i文件中,整体上是通过SWIG的extend关键字,重载c++接口函数,使得Python可以将回调函数传递给C++。传递过程中,需要借助一个仿函数PyCallBackString类对象。C++回调Python函数时,则通过PyCallBackString类对象完成回调函数的参数类型转换。
需要注意的是,所有的PyObject类型及其子类型,都维护一个计时器,当计数为0时会回收对象:
- 通过C++代码手动计数+1的函数为Py_XINCREF及Py_INCREF;
- 通过C++代码手动计数-1的函数为Py_XDECREF及Py_DECREF,
- py_main.py文件
# -*- coding: utf-8 -*-from data_processor import py_data_processor
#定义一个与StrFunc类型一致的python函数defstring_func(cpp_str):print("string_func:%s"%(cpp_str))defmain():
dp = py_data_processor.DataProcessor()#调用接口,接口参数包含与StrFunc类型对应的Python函数
dp.ProcWithCallbackString(string_func)returnif __name__ =="__main__":
main()
相比于C风格的回调函数,这里的py文件就简单很多了,py文件里没有任何关于回调函数类型转换的代码。类型转换的代码完全由.i文件和SWIG内部生成代码完成。
02 C++多线程调用回调函数
C++多线程调用Python回调函数,与前面部分相比,最大的区别是Python的GIL锁。在Python中默认情况下是没有C++所说的多线程机制,由于GIL锁的存在,在Python中底层只有一个线程运行。如果要在C++代码中运行多线程,必须了解GIL锁,并在必要的适合的时候加锁和释放锁。
简单理解,GIL锁是用来保护Python底层基础资源的互斥锁,如果需要多线程访问Python底层基础资源(比如调用Python的C++ API),就需要先获取到GIL锁。
- data_processor.h文件
#include<functional>#include<iostream>#include<string>#include<thread>structCppData{
std::string str;float f;};classDataProcessor{public:// 定义一个std::function类型的函数对象类型CppDataFuncusing CppDataFunc = std::function<void(CppData*)>;DataProcessor(){}~DataProcessor(){}//定义一个接口,接口参数包含CppDataFuncvoidProcWithCallbackMultipleThread(CppDataFunc f){//创建线程,异步调用回调函数
std::thread th(std::bind([f](){if(f ==nullptr){
std::cout <<"f == nullptr";return;}
CppData cd ={std::string("image_index"),3};f(&cd);}));
th.detach();}};
在C++代码中,通过创建新的线程,在新的线程里调用Python的回调函数。由于使用了多线程调用,所以需要在.i文件中处理GIL锁。
- data_processor.i文件
%modulepy_data_processor%{#include"data_processor.h"// 定义一个仿函数类型,// 用于将std::function类型与Python函数相互转换classPyCallBackMultipleThread{
PyObject* func;
PyCallBackMultipleThread&operator=(const PyCallBackMultipleThread&)public:PyCallBackMultipleThread(const PyCallBackMultipleThread& o):func(o.func){// 拷贝构造函数,Python对象的计数+1Py_XINCREF(func);}PyCallBackMultipleThread(PyObject* o)// 构造函数,Python对象的计数+1Py_XINCREF(func);// 通过断言,Check数据类型assert(PyCallable_Check(func));}~PyCallBackMultipleThread(){// 析构函数,Python对象的计数-1Py_XDECREF(func);}// 重载运算符(),这个函数由C++调起。voidoperator()(CppData* s){// 获取GIL,如果没有获取到,会阻塞当前线程,直到获取成功为止
PyGILState_STATE state =PyGILState_Ensure();if(!func || Py_None == func ||!PyCallable_Check(func)){// 释放GIL锁PyGILState_Release(state);return;}//将CppData类型转换为Python可以接收的参数类型。// 这里的SWIG_NewPointerObj函数是SWIG提供的一种// 可以将C++指针转换为PyObject指针的函数
PyObject* resultobj =SWIG_NewPointerObj(SWIG_as_voidptr(s),
SWIGTYPE_p_CppData,0);
PyObject* tuple =PyTuple_New(1);PyTuple_SetItem(tuple,0, resultobj);//调用回调函数
PyObject* result =PyObject_Call(func, tuple,0);// 引用计数-1,以释放对象Py_DECREF(tuple);Py_XDECREF(result);//释放GIL锁PyGILState_Release(state);}};%}
# 通过SWIG的extend关键字,重载接口
%extend DataProcessor {// 此接口由Python调用,作用是将Python传递的PyObject类型数据,// 转换为上面定义的PyCallBackMultipleThread类型// SWIG会帮我们完成PyCallBackMultipleThread类型到std::function的转换voidProcWithCallbackMultipleThread(PyObject* callback){
$self->ProcWithCallbackMultipleThread(PyCallBackMultipleThread(callback));}}%include "data_processor.h"
GIL是互斥锁,同一时刻,只能被一个线程获取到。所有涉及Python的API调用,都需要在获得GIL锁后进行,否者可能会崩溃。
- py_main.py文件
# -*- coding: utf-8 -*-from data_processor import py_data_processor
#定义一个与CppDataFunc类型一致的python函数defcppdata_func(cpp_data):print("cppdata_func: str=%s, f=%f"%(cpp_data.str, cpp_data.f))defmain():
dp = py_data_processor.DataProcessor()#调用接口,接口参数包含与CppDataFunc类型对应的Python函数
dp.ProcWithCallbackMultipleThread(cppdata_func)# 等待异步线程的处理
time.sleep(0.1)returnif __name__ =="__main__":
main()
通过以上代码,就完成了C++多线程调用Python回调函数。针对这一使用场景的处理,主要通过.i文件和SWIG内部代码完成C++与Python的数据类型转换。
04 总结
本文讲述了,如何通过SWIG,将Python的回调函数传递给C++,然后C++如何调用Python回调函数,主要讲述了三种使用场景:
- C风格的回调函数:通过SWIG的typemap以及Python的ctypes工具类,完成Python回调函数传递到C++;C++调用回调函数没有任何特殊处理。
- 仿函数作为回调函数:通过SWIG的extend关键字,以及在.i文件编写仿函数代码,完成Python回调函数传递到C++;C++调用回调函数,也是借助仿函数类完成参数类型的转换。
- C++多线程调用回调函数:这个属于“仿函数作为回调函数”的一个特殊情况,在“仿函数作为回调函数”情况处理的基础上,加入对GIL锁的获取与释放即可,否则会有线程安全问题。
那么,在“仿函数作为回调函数”的例子里,为什么就不需要GIL锁呢?因为Python的默认线程,默认是情况下都是获取到GIL锁后运行的。
关注微信公众号“程序员小阳”,相互交流更多软件开发技术。
版权归原作者 ProComing 所有, 如有侵权,请联系我们删除。