ctypes 提供与 C 语言兼容的数据类型,可以直接调用动态链接库中的导出函数。
为使用 ctypes,必须依次完成以下步骤:
加载动态链接库
将 Python 对象转换成 ctypes 所能识别的参数
使用 ctypes 所能识别的参数调用动态链接库中的函数
cdll
windll
oledll
它们的不同之处是动态链接库中的函数所遵守的函数调用方式(calling convention
)以及返回方式。
cdll
用于加载遵循 cdecl
调用约定的动态链接库;windll
用于加载遵循 stdcall
调用约定的动态链接库;oledll
与 windll
的唯一不同是默认其载入的函数统一返回 Windows HRESULT 错误编码。
函数调用约定
函数调用约定是指函数参数入栈的顺序、哪些参数入栈、哪些通过寄存器传值、函数返回时帧的回收方式(由调用者负责清理,还是被调用者负责清理)、函数名称的修饰方法等。
常见的调用约定有
cdecl
和stdcall
两种。在《程序员的自我修养 -- 链接、装载与库》一书的第 10 章有对函数调用约定的更详细介绍。
cdecl
规定函数参数列表以从右到左的方式入栈,并且由函数的调用者负责清除帧上的参数。stdcall
的参数入栈方式与cdecl
一致,但由被调用者负责清理帧。stdcall
是 Win32 API 函数所使用的调用约定。
Linux:
>>> import ctypes
>>> libc = ctypes.CDLL("libc.so.6")
>>> libc.printf.argtypes = [ctypes.c_char_p]
>>> libc.printf(b"Hello, %s!\n", b"World")
Hello, World!
14
或者:
>>> import ctypes
>>> libc = ctypes.cdll.LoadLibrary("libc.so.6")
>>> libc.printf.argtypes = [ctypes.c_char_p]
>>> libc.printf(b"Hello, %s!\n", b"World")
Hello, World!
14
其它示例:
>>> import ctypes
>>> libc = ctypes.CDLL("libc.so.6") # Linux
>>> libc.atoi.argtypes = [ctypes.c_char_p]
>>> libc.atoi.restype = ctypes.c_int
>>> libc.atoi(b"12345")
12345
>>> libc.time(None)
1699500602
// filename: foo.c
char* myprint(char *str)
{
puts(str);
return str;
}
float add(float a, float b)
{
return a + b;
}
将 foo.c
编译为动态链接库:
gcc -fPIC -shared foo.c -o foo.so
foo.so
#FILENAME:foo.py
from ctypes import *
foo = CDLL('./foo.so')
myprint = foo.myprint
myprint.argtypes = [POINTER(c_char)] # 参数类型为 char 指针
myprint.restype = c_char_p # 返回类型为 char 指针
res = myprint(b"hello ctypes")
print(res)
add = foo.add
add.argtypes = [c_float, c_float] # 参数为两个 float
add.restype = c_float # 返回类型为 float
print(add(1.3, 1.2))
执行结果:
$ python3 foo.py
hello ctypes
b'hello ctypes'
2.5
>>> from ctypes.util import find_library
>>> find_library("m")
'libm.so.6'
>>> find_library("c")
'libc.so.6'
>>> find_library("bz2")
'libbz2.so.1.0'
函数默认返回 C int 类型,如果需要返回其它类型,那么需要设置 restype 属性。
xxxxxxxxxx
>>> from ctypes import *
>>> from ctypes.util import find_library
>>> libc = cdll.LoadLibrary(find_library("c"))
>>> strchr = libc.strchr
>>> strchr(b"abcdef", ord("d"))
21151987
>>> strchr.restype = c_char_p
>>> strchr(b"abcdef", ord("d"))
b'def'
>>> strchr(b"abcdef", ord("x"))
在 Python 中,定义回调函数类型,类似于 C 中的函数指针。比如:
xxxxxxxxxx
void (*callback)(void* arg1, void* arg2)
定义为
xxxxxxxxxx
callack = CFUNCTYPE(None, c_void_p, c_void_p)
其中,None 表示返回类型是 void,也可以是其它类型;其余两个参数与 C 中的回调参数一致。
使用如下方式注册回调函数:
xxxxxxxxxx
def _callback(arg1, arg2):
# do sth
# ...
# return sth
cb = callback(_callback)
另外,使用 ctypes 可以避免 GIL 带来的问题。
回调函数示例如下:
xxxxxxxxxx
// callback.c
void showNumber(int n, void (*print)())
{
(*print)(n);
}
编译成动态链接库:
xxxxxxxxxx
gcc -fPIC -shared -o callback.so callback.c
测试代码如下:
xxxxxxxxxx
# FILENAME: callback.py
from ctypes import *
_cb = CFUNCTYPE(None, c_int)
def pr(n):
print("this is %d" % n)
cb = _cb(pr)
callback = CDLL("./callback.so")
showNumber = callback.showNumber
showNumber.argtypes = [c_int, c_void_p]
showNumber.restype = c_void_p
for i in range(10):
showNumber(i, cb)
执行:
xxxxxxxxxx
$ python3 callback.py
this is 0
this is 1
this is 2
this is 3
this is 4
this is 5
this is 6
this is 7
this is 8
this is 9
Union(联合体/共用体):
Union 中可以定义多个成员,其大小由最大的成员决定。
Union 成员共享同一块内存,一次只能使用其中一个成员。
对某个成员赋值,将覆盖其它成员的值(因为它们共享一块内存。前提是成员所占字节数相同,当成员所占字节数不同时只覆盖相应字节上的值,比如对 char 成员赋值不会将整个 int 成员覆盖掉,因为 char 只占一个字节,而 int 占四个字节)
所有成员都从低地址开始存放
结构体和联合必须从 Structure 和 Union 继承,子类必须定义 __fields__
属性,__fields__
属性必须是二元组列表,包含 Field 名称和 Field 类型,Field 类型必须是 ctypes 类型,比如 c_int, 或者其它继承自 ctypes 的类型,比如结构体、联合、数组、指针。
xxxxxxxxxx
from ctypes import *
class Point(Structure):
__fields__ = [
("x", c_int),
("y", c_int),
]
def __str__(self):
return "x={0.x}, y={0.y}".format(self)
class Rect(Structure):
__fields__ = [
("upperleft", Point),
("lowerright", Point),
]
def __str__(self):
return "upperleft:[{0.upperleft}], lowerright:[{0.lowerright}]".format(self)
point = Point(x=10, y=20)
print("point1:", point)
rect = Rect(upperleft=Point(x=1, y=2), lowerright=Point(x=3, y=4))
print("rect1:", rect)
运行:
xxxxxxxxxx
$ python3 test.py
point1: x=10, y=20
rect1: upperleft:[x=1, y=2], lowerright:[x=3, y=4]
定义有 10 个 Point 元素的数组:
xxxxxxxxxx
TenPointsArrayType = Point * 10
初始化和使用数组:
xxxxxxxxxx
from ctypes import *
TenIntegersArrayType = c_int * 10
array1 = TenIntegersArrayType(*range(1, 11))
for i in array1:
print(i)
pointer()
可以创建指针,Pointer
实例的 contents
属性返回指针指向的内容。
xxxxxxxxxx
>>> from ctypes import *
>>> i = c_int(42)
>>> p = pointer(i)
>>> p
<__main__.LP_c_int object at 0x7f260cdff268>
>>> p.contents
c_int(42)
可以改变指针指向的内容:
xxxxxxxxxx
>>> i = c_int(99)
>>> p.contents = i
>>> p.contents
c_int(99)
可以按数组的方式访问,并且改变值:
xxxxxxxxxx
>>> p[0]
99
>>> p[0] = 22
>>> i
c_int(22)
很多情况下,C 函数需要传递指针或引用,ctypes 也完美支持这一点。
byref()
用来传递引用参数,pointer()
也可以完成同样的工作,但是 pointer()
将创建实际的指针对象,如果不需要指针对象,用 byref()
将快很多。
xxxxxxxxxx
>>> from ctypes import *
>>> i = c_int()
>>> f = c_float()
>>> s = create_string_buffer(b'\000' * 32)
>>> print(i.value, f.value, repr(s.value))
0 0.0 b''
>>> libc = CDLL("libc.so.6")
>>> libc.sscanf(b"1 3.14 Hello", b"%d %f %s", byref(i), byref(f), s)
3
>>> print(i.value, f.value, repr(s.value))
1 3.140000104904175 b'Hello'
如果需要可变字符串,那么使用 create_string_buffer()
:
xxxxxxxxxx
>>> from ctypes import *
>>> p = create_string_buffer(3) # create a 3 byte buffer, initialized to NUL bytes
>>> print(sizeof(p), repr(p.raw))
3 b'\x00\x00\x00'
>>> p = create_string_buffer(b"Hello") # create a buffer containing a NUL terminated string
>>> print(sizeof(p), repr(p.raw))
6 b'Hello\x00'
>>> print(repr(p.value))
b'Hello'
>>> p = create_string_buffer(b"Hello", 10) # create a 10 byte buffer
>>> print(sizeof(p), repr(p.raw))
10 b'Hello\x00\x00\x00\x00\x00'
>>> p.value = b"Hi"
>>> print(sizeof(p), repr(p.raw))
10 b'Hi\x00lo\x00\x00\x00\x00\x00'
c_char_p
、c_wchar_p
、c_void_p
赋值只改变它们指向的内存地址,而不改变内存的内容:
xxxxxxxxxx
>>> from ctypes import *
>>> s = b"Hello, World"
>>> c_s = c_char_p(s)
>>> print(c_s.value)
b'Hello, World'
>>> print(c_s)
c_char_p(140494369176272)
>>> c_s.value = b"Hi, there"
>>> print(c_s.value)
b'Hi, there'
>>> print(c_s)
c_char_p(140494369176752)
>>> print(s)
b'Hello, World'
xxxxxxxxxx
>>> from ctypes import *
>>> i = c_int(42)
>>> print(i)
c_int(42)
>>> print(i.value)
42
>>> i.value = -99
>>> print(i)
c_int(-99)
>>> print(i.value)
-99
char *
时,如何释放内存如果将 restype
设置为 c_char_p
,ctypes 将返回常规的 Python 字符串对象。一种简单的方式是使用 void *
和强制转换。
string.c:
xxxxxxxxxx
char *get(void)
{
char *buf = "Hello World";
char *new_buf = strdup(buf);
printf("allocated address: %p\n", new_buf);
return new_buf;
}
void freeme(char *ptr)
{
printf("freeing address: %p\n", ptr);
free(ptr);
}
在 Python 中使用:
xxxxxxxxxx
from ctypes import *
lib = cdll.LoadLibrary('./string.so')
lib.get.argtypes = []
lib.get.restype = c_void_p
lib.freeme.argtypes = c_void_p,
lib.freeme.restype = None
ptr = lib.get()
print(ptr)
print(hex(ptr))
print(cast(ptr, c_char_p).value)
lib.freeme(ptr)
也可以使用 c_char_p
的子类,因为 ctypes 不对简单类型的子类调用 getfunc
:
xxxxxxxxxx
from ctypes import *
class c_char_p_sub(c_char_p):
pass
lib = cdll.LoadLibrary('./string.so')
lib.get.argtypes = []
lib.get.restype = c_char_p_sub
lib.freeme.argtypes = [c_char_p_sub]
lib.freeme.restype = None
ptr = lib.get()
print(ptr)
print(ptr.value)
lib.freeme(ptr)
访问 value
属性将返回字符串。在本示例中,可以将 freeme
的参数改为更通用的 c_void_p
,它接受任何指针类型或整型地址。
原文地址:
How do you convert a char * with 0-value bytes into a python string?
翻译如下:
使用 POINTER(c_char)
获取指向二进制数据数组的指针。为将其放进字符串,只需获取其切片,因为对于 ctypes 指针,数组索引可按预期工作:
xxxxxxxxxx
from ctypes import *
s = b"hello\x00world" # create a string containing null bytes
sz = len(s)
p = c_char_p(s) # obtain a pointer of various types
p2 = cast(p, POINTER(c_char))
address = cast(p, c_void_p).value
# by default it is interpreted as null-terminated
print(p.value)
# various methods of explicitly specifying the full length
print(p2[:sz])
print(string_at(p, size=sz))
print((c_char * sz).from_address(address).raw)