我比较习惯使用c++,但是最近遇到需要读取数据后画图的问题,我是以对象的形式保存数据的,可以把数据存到文本文档中,然后再用python或matlab读取画图,但是这也太麻烦了,不想这么干(大量的数据),于是就想能不能把c++程序封装成模块,然后以python为主体去调用它们。最后发现了这个库:ctypes。它是python的外部函数库,里面包含了C/C++的很多数据类型,我们可以利用这个库来调用编写好的C/C++模块(通常这些模块要编译成动态链接库供python使用)。下面对使用ctypes的方法进行简单的介绍。
C:没什么要求,正常写就行。
C++:要在源文件的函数定义之前,用
extern "C" {}
对定义的函数进行声明,因为
python的ctypes可以调用C而无法调用C++代码,加了
extern "C"
之后会让编译器按照C语言而不是C++的方式进行编译。C++代码可以使用类等数据结构或C++头文件,只不过函数在定义前必须要提前声明在
extern "C"{}
里边。
// filename = test.cpp
#include<iostream>
using namespace std;
//要在extern里对所有定义的函数进行声明
extern "C"{
void func();
void func()
cout<<"hello !"<<endl;
对于C++源文件,我使用g++编译器进行编译(对C源文件则使用gcc),编译动态链接库时必须给编译器指明(可能用到的)库文件、头文件目录。。
动态链接库的文件名最好以“lib”开头,原因是gcc/g++编译选项-l后面只能接动态链接库文件名“libXXX.so”除去“lib”和扩展名之后的部分,比如g++ test.cpp -lXXX
。但也不是必须的,若不以“lib”开头,则编译选项只能用-L指定库的路径,例如g++ test.cpp -L ./libXXX.so
。当然,和普通编译程序一样。
综上所述,若此动态链接库将来不会用于gcc/g++的-l选项编译过程,则此库的文件名没有限制。
例如,以下命令对test.cpp
源文件进行编译,输出动态链接库.so文件。
g++ test.cpp -fPIC -shared -o libtest.so
若要调用动态链接库libtest.so
内的函数,则首先应import库。然后使用cdll对象的加载库方法,加载动态链接库到libc对象,通过libc对象的方法便可以调用到动态链接库中的函数。
from ctypes import *
libc=cdll.LoadLibrary("./libtest.so")
libc.func()
将会输出结果
hello !
上面是最简单的调用C++函数的例子,若调用过程有参数传递,则在python程序中应注意数据类型的规范定义。因为python默认函数的参数类型和返回类型为 int 型,所以与外部函数传参、返回值时,变量必须用ctypes数据类型来明确定义,常用的ctypes数据类型见官方文档。C/C++代码仍按照自己的方式正常传递参数。下面我来结合例子进行详细说明。
在test.cpp中定义了两个函数,分别传递整形和整形指针参数。
#include<iostream>
using namespace std;
extern "C"{
int add(int a, int b);
void reverse(int *a);
int add(int a, int b)
cout << "a+b=" << (a + b) << endl;
return (a+b);
void reverse(int* a)//取相反数
*a = -(*a);
用以上源文件生成动态链接库libtest.so
。
下面是python代码及程序运行结果。
from ctypes import *
def func1(a, b):
libc = cdll.LoadLibrary("./libtest.so")
libc.add.restype = c_int
num = libc.add(a, b)
print(num)
def func2(a):
libc = cdll.LoadLibrary("./libtest.so")
a = (c_int)(a)
a_p = POINTER(c_int)(a)
libc.reverse(a_p)
return a.value
if __name__ == "__main__":
# print(func3())
func1(2,3)
print(func2(1123))
输出结果为
a+b=5
-1123
对于外部函数的输入参数
,则要用argtypes方法和参数类型的列表或元组(元组效率比列表高)来定义,例如
# 这里动态链接库加载为了library对象
library.func2.argtypes = (c_float, c_double)
对于外部函数的返回值
,用外部函数的restype方法进行数据类型的定义,如
library.func1.restype = c_int
对于将要传递给外部函数的变量,用ctypes数据类型对变量进行定义,形式类似于强制类型转换,例如定义C浮点型变量1.234:a = c_float(1.234)
,通过ctypes变量的value方法访问、赋值变量的值:a.value = 2.345
,print(a.value)
。
对于数组,直接使用元素数据类型*n即可,例如,array = c_int * 4
。
对于字符串,C语言函数所需的参数一般是字符指针,所以需要把python中的字符串(对象)转换成C语言的字符串(字节串)再进行参数传递。有两种方法实现,既可以使用python内置的bytes()
对象,也可以使用python内置的字符串之encode()
方法。
// C/C++函数声明
extern "C"{
void fun(char * a);
str1 = "good job."
str2 = "hello, world"
str1 = c_char_p(bytes(str1, "utf-8"))
str2 = c_char_p(bytes(str2, "utf-8"))
ptr = (c_char_p * 2)(str1, str2)
library.test(ptr)
对于结构体,必须定义一个继承自ctypes中Structrue
的类,类中必须含有_fields_属性
,它是一个二元组构成的列表,每个二元组为(field name, field type)
,C的结构体中所有成员变量都以此种方式进行声明。该子类还有可选的一个_pack_
属性,用于规定实例中结构体的字节对齐方式。注意,把结构体向外部函数传参时,应该总是通过指针传递。
from ctypes import *
class point(Structure):
_pack_ = 1 #按1个字节对齐
_fields_ = [("a", c_int),
("b", c_double),
("c", c_char)]
对于指针型变量,见下面的表格。
ctypes内的函数
byref(obj, offset)
返回obj的地址(指向obj的指针),obj为一个ctypes数据类型对象,offset为地址偏移量。该方法生成的指针只能作为参数来调用外部函数。
pointer(obj)
返回一个指向obj的指针实例。
POINTER(type)
返回一个指向type类型的指针类型,type必须是ctypes数据类型。
后两个的联系为,pointer(obj) == POINTER(obj_type)(obj)
。如果只是想生成一个调用外部函数所需要的指针,则使用byref
函数最合适,它构造起来最快,但返回的指针无法用于其他用途。
from ctypes import *
a = c_float(3.14) # 生成一个ctypes类型的对象
b = byref(a,0)
c = pointer(a)
d = POINTER(c_float)(a)
print(b)
print(c)
print(d)
运行结果为
<cparam 'P' (0x7fe8bda74c48)>
<__main__.LP_c_float object at 0x7fe8bda919d8>
<__main__.LP_c_float object at 0x7fe8bda919d8>