根据Python官方文档,ctypes
是一个外部函数库,它提供了与C兼容的数据类型,允许调用DLL(Dynamic Link Libraries, 动态链接库)或共享库中的函数。换句话说,通过ctypes
库,我们能在Python程序中调用C/C++代码。
动态链接库是一个已编译的二进制文件,其在程序编译时并不会被链接到目标代码,而是在程序运行时才载入。Windows上的动态链接库为DLL(
.dll
),Linux上为SO(.so
)。
先给一个演示的Demo,然后再展开来讲,演示环境说明如下:
Platform: Ubuntu-20.04
gcc: 9.4.0
首先新建一个名为test.cpp
的源码文件,源码内容如下:
extern "C"{
void greet(char* name){
std::cout << "Hello " << name << std::endl;
}
int sumArray(int a [], int n){
int s = 0;
for (int i = 0; i < n;i++){
s += a[i];
}
return s;
}
double distance(double *x, double y[], int n){
double dis;
for (int i = 0; i < n;i++){
dis += pow((x[i] - y[i]), 2);
}
return sqrt(dis);
}
}
然后通过下述命令将其编译为动态链接库:
g++ -fPIC -shared test.cpp -o test.so
Python调用上述三个函数的示例如下:
from ctypes import *
# 加载
lib = CDLL("./test.so")
lib.greet(b"Tom")
# Hello Tom
int_5 = c_int * 5
arr = int_5(1, 3, 5, 7, 9)
print(lib.sumArray(arr, 5))
# 25
double_2 = c_double * 2
x = double_2(1, 3)
y = double_2(2, 4)
distance = lib.distance
distance.restype = c_double
print(distance(x, y, 2))
# 1.4142135623730951
从上述演示示例可以看出,对于C/C++源码仅需要将其编译为动态链接库即可,上述的示例命令为:
g++ -fPIC -shared test.cpp -o test.so
说明:
gcc
,若为C++
程序则使用g++
。-fPIC
表示位置独立。-shared
表示编译为动态库。另外,需要注意在上述示例代码中还使用了extern "C" {}
的用法,其作用是告知编译器按C的方式编译,编译后可以直接通过函数名调用。这在编译C++程序时是必须的,因为C++支持函数重载,导致编译后函数名会发生改变,使得不能通过函数名来对C++程序中的函数进行调用。
注意:extern
修饰代表本模块可以在外部使用,若不想暴露相应的接口,则可以使用static
修饰。
在Python中载入动态链接库的方式如下所示:
from ctypes import *
# 方式1
lib = CDLL("./test.so")
# 方式2
lib = cdll.LoadLibrary("./test.so")
载入完成后便可以通过.
来调用C/C++程序中的内容。
ctypes
中定义的与C兼容的基本数据类型完整版详见官网Fundamental data types。本文仅列举一些常用的:
ctypes类型 | C类型 | Python类型 |
---|---|---|
c_bool | _Bool | 布尔型 |
c_char | char | 单字符字节对象 |
c_int | int | 整型 |
c_float | float | 浮点型 |
c_double | double | 浮点型(python不区分单精度还是双精度) |
Python中通过ctypes
传递参数类型为字符串的形式如下:
# 形式一
lib.travel(b"I love Python!")
# 形式二:使用字符指针
m_str = c_char_p(b"I love Python!")
lib.travel(m_str)
# 形式三
m_str = "我爱Python!"
lib.travel(m_str.encode())
# 形式四:创建String Buffer
m_str = create_string_buffer(("我爱Python!").encode())
lib.travel(m_str)
其中traval
是C++中遍历打印字符串的一个函数,函数定义如下:
void travel(char * str){
for(int i = 0;str[i];i++){
std::cout << str[i];
}
std::cout << std::endl;
}
说明:
b
可以让字符串强制转为bytes
类型,但这种方式仅限于只包含ASCII字符的字符串。encoode()
方法。ctypes
提供了函数create_string_buffer()
来创建字符串缓冲区,其属于可修改的字符串传参方式。下面的示例便验证了create_string_buffer
能创建可修改的字符串参数。
C++中修改函数定义:
void increment(char * str){
for(int i = 0; str[i]; i++){
str[i] += 1;
}
}
Python中调用及结果如下:
m_str = create_string_buffer(b"abc")
lib.increment(m_str)
print(m_str.value)
# b'bcd'
指针可以通过ctypes
中的pointer(obj)
函数进行创建:
pi = c_float(3.14)
ptr = pointer(pi)
print(ptr.contents)
# c_float(3.140000104904175)
引用可以通过ctypes
中的byref(obj)
函数进行创建:
pi = c_float(3.14)
ptr = byref(pi)
注意:官网指明若只想向外部函数传递一个对象指针,使用引用更快。
下面给出一个交换两个元素值的例子:
C++部分源码为:
void swap(int *a, int &b){
int t = *a;
*a = b;
b = t;
}
Python部分源码为:
x = c_int(3)
y = c_int(4)
lib.swap(pointer(x), byref(y))
print(x.value, y.value)
# 4 3
创建数组类型的推荐方式是类型乘以一个正数,例如:
int_3 = c_int * 3
通过argtypes
属性可以指定函数的传参类型。示例如下:
print_str = lib.travel
print_str.argtypes = [c_char_p]
默认情况下都假定函数返回c_int
类型,但可以通过函数对象的restype
属性可以指定返回值的类型。在上述示例演示中便有一个现成的例子,其指定了返回值类型是c_double
。
distance.restype = c_double
完成本文参考了如下资料: