使用ctypes为C++包装python接口
使用ctype库
在被python调用时,C++函数的参数的默认值无效,若不传入参数,将进行随机初始化。
使用ctypes时,接口函数参数类型、返回值类型中不要使用 string 等C++中的特性。经历过很多由于使用了string类型导致的问题,应该和string内部的机制有关,改为char* 就好了。
1. 简单的例子
C++:exp.cpp
// #include <iostream> // using namespace std; extern "C" { int get_word(int b) { return b+1; } }
这里是用的C++风格的代码,并且使用了c++的头文件,需要用 extern "C" {}将需要共享到python的函数部分包住,否则调用时回找不到对应的函数或变量(undefined symbol)。 ——extern "C"的作用
编译语句:gcc -shared exper.cpp -fpic -o exper.so [-lstdc++]
* gcc命令各项的顺序很重要,不同顺序的结果都是不一样的
* 这里用gcc或g++都一样,因为代码是C++风格的;
* -fpic指定生成位置无关代码,适用于动态链接。注意,使用的链接库是有地址的,特别是当链接了一些自己生产的库文件时。
* 使用参数 -shared 而不是 -c;使用 -shared 则会生成一个可共享的编译结果,动态链接库,在python文件中可用,而-c的编译对象在python种加载时会报错“xxx.so: only ET_DYN and ET_EXEC can be loaded” ——参考stackoverflow
* 防止出现错误undefined symbol:若使用了C++头文件iostream,所以加上链接库选项 -lstdc++,若使用了其它头文件,也要链接相关度库,若未使用,则不必添加 ——gcc链接库的问题
Python:
import ctypes so = ctypes.CDLL('./exp.so') print(so.get_word(4))
脚本将在终端打印 5。
示例代码中的参数传递有两种方式,1)直接传对象,2)传递对象的指针,二者是等效的,但一般还是按正常的逻辑传参,不要随便多加指针、引用。
以上面的get_word函数为例:
import ctypes so = ctypes.CDLL('./exp.so') so.get_word(4) # integer obj # equal to so.get_word(pointer(c_int(4)) # euqal to so.get_word(byref(c_int(4))
如果想再给接口包一层,隐藏其它内容,可以添加一个新的文件interface.cpp,内容如下,之后python直接从interface.cpp生成的链接库里调函数即可。(没必要)
extern "C" int get_word(int);
* 此处extern "C"既指明了函数是C风格的,也指明了该函数是外部函数,告诉编译器到其它文件中去查找定义。
2. 复杂参数传递
ctypes库只对C语言类型有封装映射,如下,简单类型可以直接使用,ctypes自会进行映射。对Mat这样的复杂类型,必须另外实现。
1)简单类型 ——上面的“简单例子” 或 参考博客中的更严谨写法
2)string类型(char*)
// c++ char* get(char* str); // use C-style type: char*
# python str = bytes('string-content', 'utf-8') # according to previous table so.get.restype = c_char_p # c_char_p means char* resu = so.get(str).decode()
* POINTER(c_char) 和 c_char_p 的效果不一样,前者修饰的变量显示的类型为 LP_c_char 对象,后者就是对应char*,需要用 decode() 函数将byte 数据解码为字符串。 ——参考简书
* 或者用create_string_buffer这样的函数(参考链接)
3)数组
// c++ ... int get_max(int* nums, int size) { // pointer of array for (int i=0; i<size; i++) cout << nums[i] << endl; }
# python import ctypes nums = (ctypes.c_int*3)() # type: c_int, size: 3, default value: 0; list couldn't match with byref nums[0] = 3 so.get_max(nums, len(nums)) # or so.get_max(ctypes.byref(nums), len(nums)) # byref() gets the pointer of nums
* byref(ctype_obj):返回一个指向ctypes对象的轻量级指针(&ctype_obj),注意,这不是实际的指针,没有实际分配指针空间,相当于传递引用类型的参数;其中cypte_obj必须是ctype类型的对象
* list进行转换的例子——参考博客(也是转换为ctypes类型)
4)cv::Mat ——参考博客(c++ <=Mat=> python)
将二维的图像数据展平为一维,数据类型为uchar(Mat中使用的就是uchar),并记录h、w用于恢复源图像。
下面例子为python向C++函数传递Mat图像,test.jpg和exam.png的每个像素值都是相同的,无损。
// C++ ... void get_mat(uchar* data, int rows, int cols) { Mat res = Mat(Size(cols, row), CV_8UC3, Scalar(255, 255, 255)); res.data = data; imwrite("./exam.png", res); // .png is lossless }
# python
... img = cv2.imread('./test.jpg') height, width, _ = img.shape img_data = img.ctypes.data_as(ctypes.POINTER(ctypes.c_ubyte)) so.get_mat(img_data, height, width)
* 注意,C++代码中的当前路径是 执行Python脚本时所在的路径,和C++、Python脚本的路径无关;
* Pointer(type):返回一个指向type类型的指针类型,type应当是是ctypes中的类型;
pointer(obj):创建并返回一个指向实例对象 x 的指针;
* 只有对象才能用 obj.ctypes.data_as(...)这样的类型转换,此处img就是个对象,但是height不是;
===>>> 结合(3)、(4),可以传递指针数组(用二级指针指向该数组的头部)
// C++ void get_mats(int nums, uchar** data, int rows, int cols) { Mat res = Mat(Size(cols, row), CV_8UC3, Scalar(255, 255, 255)); for (int i=0; i<nums; i++){ res.data = data[i]; // every image would be saved, they share the same Mat object is ok imwrite(string("./exam").append(to_string(i)).append(".png"), res); // .png is lossless } }
# python nums =2 img, img1 = cv2.imread('./test1.jpg'), cv2.imread('./test2.jpg') height, width, _ = img.shape img_data, img_data1 = img.ctypes.data_as(ctypes.POINTER(ctypes.c_ubyte)), img1.ctypes.data_as(ctypes.POINTER(ctypes.c_ubyte)) img_arr = (ctypes.POINTER(ctypes.c_ubyte)*nums)() # array of uchar*, aka char** img_arr[0], img_arr[1] = img_data, img_data1 so.get_mats(nums, img_arr, height, width) # it's ok if use cytpes.byref(img_arr), it will get an pointer to char**, so
3)结构体
结构体通过结构体指针在 C++ 和 Python 之间传递,直接传递结构体也行,相关代码类似。注意属性不要有C++类型。 ——参考博客
// C++ struct Test { // string str; char* str; int sim = 0; }; extern "C" Test* get2(Test* t){ Test* p = new Test(); p->str = (char*)malloc(20); // allocate space before assignment strcpy(p->str, "No"); p->sim = t->sim; return p; }
* 当使用 char* str 时,在 C++ 中设置的变量默认值会传输的 Python 这边;
而使用 string str 时,变量 str 的值也可以正常传递,但其它值就没法正常传递了。
在Python中,定义继承自 ctypes.Structure 的类来映射C++中的结构体,建议两边的变量名相同,否则对应逻辑会很乱。
# Python class _Struct(ctypes.Structure): _fields_ = [('str', c_char_p), ('sim', c_int)] # feature: '_fields' so.get.restype = POINTER(_Struct) st = _Struct(' ', 3) resu = so.get(pointer(st)).contents # contents means the content that resu points to print(resu.str.decode(), resu.sim) # output is: No 3
* 注意是 contents,不是content
4)C++类
在C++代码种,我们不直接操作类和类对象,可以通过一个 全局的类对象变量 和包装后的 接口函数 实现操作,该类对象在python脚本运行的阶段内都是有效的。
// C++ class Video{ public: int num = 2; video() {}int get() { return num; } void set(int b=0) { num = a} } Video vi; // global variable extern "C" { // interfaces int get_num() { return vi.get(); } void set_num(int a) { vi.set(a); } }
# Python print(so.get_num()) # 2 so.set_num(1); print(so.get_num()) # 1