python调用C++函数


之前写毕业设计,有一个地方我觉得实现得不太合理,因为有一个地方涉及用C++对输入的图像进行推理,而我没有找到合适的C++库来加载图像,与项目的甲方问起,他们的意见是先用python做预处理,然后将张量写到文件中去,再由C++代码读取张量,进行推理后,再将推理后得到的张量写到文件中去,最后再用python做后处理。这样虽然简单,但是会带来大量的IO开销,我觉得很不合理。虽然项目交付了,但是写毕业论文的时候我还是觉得这个地方值得优化,就花了点时间折腾了一下。

python提供了ctypes来调用动态库中的函数,它提供了与C 兼容的数据类型,可使用该模块以纯Python 形式对这些库进行封装,因此python也被称为胶水语言。

无参数、返回值的C++函数调用

在这里写了一个简单的hello world函数,给python调用:

1
2
3
4
5
6
7
8
// print.cpp
#include <iostream>

extern "C"{
void print(){
std::cout << "hello, wolrd" << std::endl;
}
}

注意,在C++的函数如果要被ctypes调用需要使用extern "C",因为ctypes是基于C语言编写的,需要让C++函数的符号变成C符号,不然在python中调用的时候,无法找到对应的函数符号。

然后把上面的函数编译成so动态库:

1
g++ -o libprint.so -shared -fPIC print.cpp

接下来就编写python程序调用print函数:

1
2
3
4
5
// print.py
from ctypes import *

lib = CDLL("./libprint.so")
lib.print()

执行:

1
2
$ python3 print.py
hello, wolrd

这是最简单的格式,比较麻烦的是参数,特别是数组参数的处理。

有参数、返回值的C++函数调用

  • 简单类型参数及返回值

​ 在这里编写了一个简单的sum函数为例:

1
2
3
4
5
6
// sum.cpp
extern "C"{
int sum(int a, int b){
return a + b;
}
}

编译成动态库:

1
g++ -o libsum.so -shared -fPIC sum.cpp

然后用python调用:

1
2
3
4
5
from ctypes import *

lib = CDLL("./libsum.so")
ret = lib.sum(100, 200)
print(ret)

在这里因为参数和返回值都是基本类型int,所以较为简单。

  • 复杂参数或返回值

我的需求涉及到numpy数组的处理,而如果直接把numpy数组传递给函数,会出现类型不匹配的错误。阅读了文档后,我觉得可以把numpy转化为bytes数组的形式,然后以buffer的形式传递给C++函数,C++再做一次强制类型转换就可以得到数组了。注意需要匹配好数据类型:

C++代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// mul_array.cpp
#include <iostream>
#include <vector>
#include <cstring>

extern "C"{
void* mul_array(void* buffer, size_t size){
int64_t* test = reinterpret_cast<int64_t*>(buffer);
for(int i = 0;i < size;i++){
test[i] = test[i] * 2;
}
return buffer;
}
}

python代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from ast import Num
from ctypes import *
import numpy as np

lib = CDLL("./libmul_array.so")
array = np.array(np.arange(5), dtype=np.int64)
lib.mul_array.restype = c_void_p
print(array)
array_buffer = array.tobytes()
ret = lib.mul_array(array_buffer, array.size)
ret_buffer = string_at(ret, len(array_buffer))
ret_array = np.frombuffer(ret_buffer, dtype=array.dtype)
print(ret_array)

在处理返回值的时候,需要指定返回值类型为c_void_p,即void*。在C++程序执行完毕后,返回值ret为内存中的一段地址,然后用string_at来获取这段地址,再用numpy的frombuffer函数从bytes数组中获取numpy数组。在这个过程中,需要保证numpy数组的类型和C++强制转换的类型一致,不然计算出来的结果就会由于类型格式的不同出现错误。

mul_array.cpp编译成动态库后,执行python:

1
2
3
$ python3 mul_array.py
[0 1 2 3 4]
[0 2 4 6 8]

可以看到成功将数组传入,并成功返回了数组。

似乎也可以用其他更简单的方法来实现我的需求,不过因为我的需求已经完成,有空再来折腾。