0


SWIG-Python与C++交互(三)-回调函数

接上一期SWIG对复杂数据类型的处理,本期介绍通过SWIG将Python的回调函数正确地传递到C++代码,并由C++调用该回调函数。前面介绍过的一些SWIG基础内容,这里不再重复介绍。

往期推荐:
SWIG-Python与C++交互(二)-复杂数据类型(std::map, 自定义数据类型)
SWIG-Python与C++交互(一)-简单教程

本期主要介绍以下几种回调函数的使用场景:

  1. C风格的回调函数;
  2. 仿函数作为回调函数;
  3. C++多线程调用回调函数。

这三个使用场景,从前往后,由易到难,涉及的技术细节,也由少变多。

00 C风格的回调函数

  1. 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文件中。

  1. 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完成输入参数转换。

  1. 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 仿函数作为回调函数

  1. 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"));}};
  1. 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,
  1. 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锁。

  1. 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锁。

  1. 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锁后进行,否者可能会崩溃。

  1. 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回调函数,主要讲述了三种使用场景:

  1. C风格的回调函数:通过SWIG的typemap以及Python的ctypes工具类,完成Python回调函数传递到C++;C++调用回调函数没有任何特殊处理。
  2. 仿函数作为回调函数:通过SWIG的extend关键字,以及在.i文件编写仿函数代码,完成Python回调函数传递到C++;C++调用回调函数,也是借助仿函数类完成参数类型的转换。
  3. C++多线程调用回调函数:这个属于“仿函数作为回调函数”的一个特殊情况,在“仿函数作为回调函数”情况处理的基础上,加入对GIL锁的获取与释放即可,否则会有线程安全问题。

那么,在“仿函数作为回调函数”的例子里,为什么就不需要GIL锁呢?因为Python的默认线程,默认是情况下都是获取到GIL锁后运行的。

关注微信公众号“程序员小阳”,相互交流更多软件开发技术。

标签: python c++

本文转载自: https://blog.csdn.net/ProComing/article/details/136276226
版权归原作者 ProComing 所有, 如有侵权,请联系我们删除。

“SWIG-Python与C++交互(三)-回调函数”的评论:

还没有评论