ctypes是Python的外部函数,提供了与C兼容的类型,并允许调用DLL库中的函数。
要使函数能被Python调用,需要编译为动态库:
# -fPIC使得位置独立
# -shared代表这是动态库
g++ -fPIC -shared -o libTest.so test.cpp
为保证函数接口能被外部识别,需要导出为纯C的:
#ifdef __cplusplus
extern "C"
{
#endif
void * callForTest(char *params);
#ifdef __cplusplus
};
#endif
在python中要使用DLL库,需要先通过cdll来加载(cdll载入按标准的 cdecl调用协议导出的函数):
from ctypes import cdll
target = cdll.LoadLibrary("libTest.so")
通过in_dll()可获取库中导出的变量:
# 获取 Python 库本身的 Py_OptimizeFlag
opt_flag = c_int.in_dll(pythonapi, "Py_OptimizeFlag")
ctypes 定义了一些和C兼容的基本数据类型,所有基础类型都继承自ctypes._SimpleCData
:
ctypes 类型 | C 类型 | Python 类型 |
---|---|---|
c_bool | _Bool | bool (1) |
c_char | char | 1-character bytes |
c_wchar | wchar_t | 1-character str |
c_byte | char | int |
c_ubyte | unsigned char | int |
c_short | short | int |
c_ushort | unsigned short | int |
c_int | int | int |
c_uint | unsigned int | int |
c_long | long | int |
c_ulong | unsigned long | int |
c_longlong | __int64 or long long | int |
c_ulonglong | unsigned __int64 or unsigned long long | int |
c_size_t | size_t | int |
c_ssize_t | ssize_t or Py_ssize_t | int |
c_float | float | float |
c_double | double | float |
c_longdouble | long double | float |
c_char_p | char* (NUL terminated) | bytes or None |
c_wchar_p | wchar_t* (NUL terminated) | str or None |
c_void_p | void* | int or None |
除了整数、字符串以及字节串之外,所有的Python类型都必须使用它们对应的ctypes类型包装,才能够被正确地转换为所需的C语言类型。通过ctypes创建的类型是可变的(通过修改.value
):
i = c_int()
# i.value == 0
i.value = 99
# i.value == 99
c_wchar_p("Hello, World")
# c.value == "Hello, World"
当给指针类型的对象c_char_p, c_wchar_p 和 c_void_p等赋值时,将改变它们所指向的内存地址,而不是它们所指向的内存区域的内容。若底层函数可能会改变指针地址,则需要通过create_string_buffer创建:
p = create_string_buffer(3) # create a 3 byte buffer, initialized to NUL bytes
p = create_string_buffer(b"Hello") # create a buffer containing a NUL terminated string
p = create_string_buffer(b"Hello", 10) # create a 10 byte buffer
p.value = b"Hi"
print(sizeof(p), repr(p.raw))
# 10 b'Hi\x00lo\x00\x00\x00\x00\x00'
以libc库为例:
cdll.LoadLibrary("libc.so.6")
printf = libc.printf
# 通过设置argtypes属性来指定函数的必选参数类型
# 指定数据类型可以防止不合理的参数传递,并且会自动尝试将参数转换为需要的类型(否则必须手动转换)
printf.argtypes = [c_char_p, c_char_p, c_int, c_double]
printf(b"String '%s', Int %d, Double %f\n", b"Hi", 10, 2.2)
strchr = libc.strchr
# 默认返回int,其他类型需通过restype属性来设定
strchr.restype = c_char_p
strchr.argtypes = [c_char_p, c_char]
strchr(b"abcdef", b"d")
# 构造缓冲区,获取输出
i = c_int()
f = c_float()
s = create_string_buffer(b'\x00' * 32) # 创建长度32,全部为NULL的缓冲区
libc.sscanf(b"1 3.14 Hello", b"%d %f %s", byref(i), byref(f), s)
print(i.value, f.value, repr(s.value))
# 1 3.140000104904175 b'Hello'
数组是一个序列,包含指定个数元素,且必须类型相同。创建数组类型的推荐方式是使用一个类型乘以一个正数:
_length_
属性:指明数组中元素数量的正整数。_type_
属性:指明每个元素的类型。class POINT(Structure):
_fields_ = ("x", c_int), ("y", c_int)
pointsArray = POINT * 5
print(sizeof(pointsArray))
# 40
pa = pointArray(POINT(1,2), POINT(3,4)) # 后面三个全为0
print(sizeof(pa), len(pa))
# 40 5
for i in pa: print(i.x, i.y, end=";")
# 1 2;3 4;0 0;0 0;0 0;
可以将ctypes类型数据传入pointer()函数创建指针:
contents
属性:返回指针指向的真实对象( 每次访问这个属性时都会构造返回一个新的相同对象)_type_
属性:指明所指向的类型。i = c_int(12)
pi = pointer(i)
ic = pi.contents
print(ic, ic is i)
# c_int(12) False
# 通过下标访问与修改内容
pi[0]=34
print(pi[0], pi.contents)
# 34 c_int(34)
# 修改指针指向
ii = c_int(45)
pi.contents = ii
print(pi[0])
# 56
结构体必须通过子类化ctypes.Structure
来创建,并且至少要定义一个 _fields_
类变量,并允许通过直接属性访问来读取和写入字段。
_fields_
属性:定义结构体字段的序列。 其中的条目必须为2元组或3元组。
_pack_
属性:一个可选的小整数,它允许覆盖实体中结构体字段的对齐方式。对于不完整类型:即在结构体中包含指向自身的指针:
struct cell; /* forward declaration */
struct cell {
char *name;
struct cell *next;
};
在python中不能在类中使用自身,需要先定义结构,然后在设定_fields_
属性:
class cell(Structure):
pass
cell._fields_ = [("name", c_char_p),
("next", pointer(cell))]
必须先为回调函数创建一个类(明确调用约定,返回值类型以及参数信息);CFUNCTYPE()
工厂函数使用 cdecl 调用约定创建回调函数类型。
qsort = libc.qsort
qsort.restype=None
# 第一个参数为返回值类型,后续一次为对应函数参数
CMPFun = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
def py_cmp_func(a,b):
print('py_cmp_func', a[0], b[0])
return 0
cmp_fun = CMPFun(py_cmp_func)
IntArray5 = c_int * 5
ia = IntArray5(1, 9, 7, 5, 8)
qsort(ia, len(ia), sizeof(c_int), py_cmp_func)
注意:回调函数是在Python之外的另外一个线程使用(外部代码调用这个回调函数), ctypes 会在每一次调用时创建一个虚拟 Python 线程。这个行为在大多数情况下是合理的,但也意味着如果有数据使用 threading.local 方式存储,将无法访问,就算它们是在同一个C线程中调用的。
一些常用工具函数:
以C++回调一个python函数为例:
C++中的回调定义:
#ifdef __cplusplus
extern "C"
{
#endif
typedef void (*PrintOutput)(const char* outputs);
void set_callback(PrintOutput func);
#ifdef __cplusplus
};
#endif
python中使用回调:
from ctypes import cdll, c_char_p, CFUNCTYPE, POINTER
target = cdll.LoadLibrary("/workspace/libTest.so")
PrintCallback = CFUNCTYPE(None, c_char_p)
def print_callback(outputs):
print("outputs:", outputs)
py_callback = PrintCallback(print_callback)
target.set_callback.restype = None
target.set_callback(py_callback)