使用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

 

posted @ 2022-04-22 23:01  谷小雨  阅读(419)  评论(0编辑  收藏  举报