Last Updated: 2023-09-21 11:26:58 Thursday
-- TOC --
通过ctypes模块,我们可以直接调用动态链接库中的接口,它是Python访问外部C接口的通道。这个模块帮过我的大忙,我用它测试过第三方的SDK,调用DLL中的接口测试Windows驱动。
貌似ctypes只能调用C接口!
Python和C的混合编程,最简单直接的方法,就是使用标准库中的ctypes模块。通过ctypes模块可以实现调用动态链接库中的函数接口。
下面的测试代码,在Python中引入了标准C库函数,并调用:
>>> from ctypes import *
>>> libc = CDLL('libc.so.6')
>>> libc.printf(b'https://cs.pynote.net\n')
https://cs.pynote.net
11
>>> libc.time(None)
1606122509
下面的测试代码,测试一个自己写的C函数,在python中被调用的步骤:
$ cat test.c
int addone(int a) {
return a+1;
}
$ gcc -fPIC -shared test.c -o test.so
$ python3 -q
>>> from ctypes import *
>>> tt = CDLL('./test.so')
>>> tt.addone(1)
2
>>> tt.addone(10)
11
调用动态链接库中的接口,就是这么直接。在Linux平台下使用CDLL
,在Windows平台使用WinDLL
。
注意,这与import一个用C/C++写的Python扩展模块是不一样的!使用ctypes模块相对比较简单,只是代码会稍显冗余,需要做很多参数和返回值的转换,有些转换做起来并不是容易。而用C/C++编写Python扩展模块,需要调用Python C API才能完成,但在使用上,就与用Python写的模块没有差别。另一个需要注意的差异是:使用ctypes模块具有更好的兼容性,用Python C API的方式,只能与CPython一起工作!(当然,有谁在乎其它Python实现吗!)
先准备一个需要指针的C接口:
void swap(int *a, int *b) {
int temp;
temp = *a;
*a = *b;
*b = temp;
}
编译成.so库后,在python中测试:
>>> s = CDLL('./sort.so')
>>> ca = c_int(1)
>>> cb = c_int(2)
>>> ca
c_int(1)
>>> cb
c_int(2)
>>> s.swap.restype = None # suppress default random int return value
>>> s.swap(byref(ca), byref(cb))
>>> ca
c_int(2)
>>> cb
c_int(1)
>>> ca.value
2
>>> cb.value
1
首先创建两个c_int
对象,ca和cb,然后在调用C语言编写的swap函数时,通过byref
函数获得两个c_int对象的指针,byref函数的入参只能是转换后的c_int对象(还有很多其它的c_*
函数用来做类型转换)。最后的结果,成功交换两个变量的值。
直接使用Python中的None
,实际效果就是传入一个NULL
空指针。
除了int和bytes(对应char*接口),其它基础类型数据,在传入ctypes模块调用的C函数之前,都要进行类型转换。其实,就是将Python可以用的类型,转换成c_*
类型,这些类型能够被ctypes模块处理,用来传递给底层的C函数。
还有个细节:默认ctypes调用C函数返回的都是int类型,如果不是int,要特别指定!(当然,void没有返回值,就不用指定啦)
下面这个函数(C),传入两个float类型,返回也是float:
float addfloat(float a, float b) {
return a+b;
}
注意下面的代码,在调用C函数前,先定义其入参和返回值的类型:
>>> from ctypes import *
>>> s = CDLL('./sort.so')
>>> s.addfloat.argtypes = (c_float, c_float)
>>> s.addfloat.restype = c_float
>>> s.addfloat(1.234, 4.321)
5.555000305175781
除了int,其它类型必须要这样指定类型后才可调用。
当C接口的返回类型为void时,指定restype为Python的None!
c_char_p
对应C语言的指针。
>>> cs = c_char_p(b'1234')
>>> id(cs)
139654143470272
>>> print(cs)
c_char_p(139654143368032)
>>> cs.value
b'1234'
>>> cs.value = b'abcde'
>>> id(cs)
139654143470272
>>> print(cs)
c_char_p(139654143323744)
修改c_char_p对象的value,不会导致cs的地址变化,只是c_char_p指向的地址有变化。c_char_p就是指针,在调用C接口传递参数时,无需byref。其实,直接使用bytes对象传入,就是传指针的效果,前面已有示例,bytes对应的是一个内存块。
开一个缓冲区,用来获取接口的返回值...
上面说到ctypes模块提供的指针类型c_char_p,如果修改其value属性,地址也会发生变化。而create_string_buffer函数提供了一个alternative,修改值地址不变。
>>> 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'
这段测试代码来自Python官网,注意最后那个赋值操作,在raw层面,有一个\0作为字符串的结束。其实不用repr(p.raw),p.raw就是bytearray,可以直接使用。
create_string_buffer
很好用,下面是我自己的一个测试代码,获取C接口的返回值:
def tcp_test():
import socket
s = socket.socket()
ip = create_string_buffer(16)
port = create_string_buffer(8)
ret = sup.get_tcp_connect_proxy(s.fileno(),
b'114.215.183.12', 10050, ip, port)
if ret != 0:
sys.exit(5)
ip = ip.value.decode()
port = int.from_bytes(port.value, 'big')
print('ip',ip)
print('port',port)
s.connect((ip, port))
...
坑:port.value
有可能不能返回预期的值,如果port.raw
的第1个byte是\x00
,port.value
就是一个空的bytearray!
下面是用C实现的冒泡排序算法:
void bubble(int a[], int n) {
int i,j;
for (i=0; i<n-1; ++i) {
for (j=0; j<n-i-1; ++j) {
if (a[j] > a[j+1])
a[j] ^= a[j+1] ^= a[j] ^= a[j+1];
}
}
}
编译成.so后,在python中调用测试:
>>> from ctypes import *
>>> s = CDLL('./sort.so')
>>> a = [3,1,2,9,5,4,7,6]
>>> len(a)
8
>>> arr = (c_int*8)(*a)
>>> for i in arr: print(i, end=' ')
...
3 1 2 9 5 4 7 6 >>>
>>> s.bubble(byref(arr),8)
7
>>> for i in arr: print(i, end=' ')
...
1 2 3 4 5 6 7 9 >>>
创建arr数组的方式,c_int*8
,这是python建议创建C类型数组的方式,用*
操作,调用得到的对象,参数为 *a
,这是个python的unpacking操作。
下面的代码,给出用ctypes定义一个多维数组的方法:
>>> from ctypes import *
>>> a1 = (c_int*6)(1,1,1,1,1,1)
>>> a2 = (c_int*6)(2,2,2,2,2,2)
>>> arr = ((c_int*6)*2)(a1,a2)
>>> arr
<__main__.c_int_Array_6_Array_2 object at 0x7f7b9f4425c0>
>>> for i in range(2):
... for j in range(6):
... print(arr[i][j])
...
1
1
1
1
1
1
2
2
2
2
2
2
测试代码(C):
#include <stdio.h>
typedef struct {
int a;
int b;
float c;
} abc;
void show_struct(abc *pt) {
printf("%d %d %f", pt->a,pt->b,pt->c);
}
在C代码中定义的结构体,要跟在Python中定义的结构体对象,在结构上保持一致!下面是Python测试代码:
>>> from ctypes import *
>>> ccc = CDLL('./ccc.so')
>>>
>>> class abc(Structure):
... _fields_ = [('a',c_int),('b',c_int),('c',c_float)]
...
>>> abc01 = abc(1,2,3.1415)
>>> abc01
<__main__.abc object at 0x7f869a5785c0>
>>> abc01.a
1
>>> abc01.b
2
>>> abc01.c
3.1414999961853027
>>>
>>> ccc.show_struct(byref(abc01))
12
1 2 3.141500>>>
Python中定义结构体,必须要继承ctypes.Structure对象,然后按照规范,自己定义_fields_
成员,如上面代码。在Python中定义一个符合C语言语义的结构体,常用于通过ioctl调用传递参数等场景。
这里有一个使用ctypes.Structure的示例,设置网卡的混杂模式,通过fcntl.ioctl
将一个C语言结构体传递给驱动,代码。
也可以不用继承ctypes.Structure,而是直接使用struct提供的功能。
我用Python测试Linux系统接口,在参数处理部分,需要明确C基础类型的sizeof长度,可使用Python自带的ctypes模块。
>>> from ctypes import *
>>> for i in [x for x in dir() if x.startswith('c_')]:
... if i != 'c_buffer':
... print(i, sizeof(eval(i)))
...
c_bool 1
c_byte 1
c_char 1
c_char_p 8
c_double 8
c_float 4
c_int 4
c_int16 2
c_int32 4
c_int64 8
c_int8 1
c_long 8
c_longdouble 16
c_longlong 8
c_short 2
c_size_t 8
c_ssize_t 8
c_ubyte 1
c_uint 4
c_uint16 2
c_uint32 4
c_uint64 8
c_uint8 1
c_ulong 8
c_ulonglong 8
c_ushort 2
c_void_p 8
c_voidp 8
c_wchar 4
c_wchar_p 8
sys模块中也有一些信息,没搞懂怎么用:
>>> import sys
>>> sys.int_info
sys.int_info(bits_per_digit=30, sizeof_digit=4)
>>> sys.float_info
sys.float_info(max=1.7976931348623157e+308, max_exp=1024, max_10_exp=308, min=2.2250738585072014e-308, min_exp=-1021, min_10_exp=-307, dig=15, mant_dig=53, epsilon=2.220446049250313e-16, radix=2, rounds=1)
字节序可通过sys模块获取:
>>> sys.byteorder
'little'
前面的内容多次提到,给C函数传递某类型的指针,一般需要使用byref
接口。而如果C函数返回了一个指针,如何处理呢?下面的C代码,init函数申请内存,返回了一个指向结构体的指针:
#include <stdlib.h>
typedef struct {
int a;
int b;
float c;
} abc;
abc *init(int a, int b, float c) {
abc *pt;
pt = (abc *)malloc(sizeof(abc)); // assume always success
pt->a = a;
pt->b = b;
pt->c = c;
return pt;
}
void destroy(abc *pt) {
free(pt);
}
在Python代码中,我们也需要定义一个对等的结构体,来获取C函数init的返回值,测试代码如下:
>>> from ctypes import *
>>> ccc = CDLL('./ccc.so')
>>>
>>> class abc(Structure):
... _fields_ = [('a',c_int),('b',c_int),('c',c_float)]
...
>>>
>>> ccc.init.restype = POINTER(abc)
>>> p = ccc.init(1,2,c_float(3.1415))
>>> p
<__main__.LP_abc object at 0x7f8ae466b7c0>
>>> p.contents.a
1
>>> p.contents.b
2
>>> p.contents.c
3.1414999961853027
>>> ccc.destroy(p)
0
>>> p.contents.a
1775710480
>>> p.contents.b
22032
>>> p.contents.c
5.1590288340490325e-39
定义abc类型,对应C代码中的结构体;设置返回值类型为POINTER(abc)
,POINTER要大写,小写的pointer含义不同。然后调用init,就得到了p,p.contents内就包含了结构体的值。在调用destroy的时候,直接传入p,它是一个pointer实例,不需要使用byref函数,它已经是指针了。
在C代码中malloc的memory,需要在C代码中free!
不继承ctypes.Structure也可以,使用struct:
import struct
from ctypes import *
cc = CDLL('./ccc.so')
cc.init.restype = POINTER(c_char*12)
p = cc.init(10,20,c_float(3.1415))
print(p.contents)
print(struct.unpack('iif', p.contents))
cc.destroy(p)
如果你也像我一样,日常的开发工作是以python为主,C/C++为辅。或者你觉得用C语言的工具来做C函数的单元测试太麻烦。你可以试着用Python来完成C函数的单元测试。
Python的开发效率显然是比较高的,只是牺牲一定的性能。C的特点是贴近硬件和高性能,但不适合用来开发一般的应用,效率太低。它们两者是可以结合的,用Python来做C函数的单元测试,只是结合的一个方面。
Python自带unittest单元测试框架,用来测试C函数,也非常方便。
sort.c中有两个C函数:
void bubble(int a[], int n)
{
int i,j;
for (i=0; i<n-1; ++i){
for (j=0; j<n-i-1; ++j){
if (a[j] > a[j+1])
a[j] ^= a[j+1] ^= a[j] ^= a[j+1];
}
}
}
void select(int a[], int n)
{
int i,j,k;
for (i=n; i>1; --i){
k = 0;
for (j=1; j<i; ++j){
if (a[k] < a[j])
k = j;
}
if (k != j-1)
a[k] ^= a[j-1] ^= a[k] ^= a[j-1];
}
}
以上代码分别是冒泡排序和选择排序,两个经典的内部排序算法。
下面是用python的unittest框架写的用来测试这两个函数的测试集:
class test_sort_c(unittest.TestCase):
@classmethod
def setUpClass(cls):
print('\n# sort.c:')
shell('gcc -W -fPIC -shared -O3 sort.c -o sort.so')
cls.sort = ctypes.CDLL('./sort.so')
@classmethod
def tearDownClass(cls):
os.remove('sort.so')
def get_sort(self):
return test_sort_c.sort
def setUp(self):
self.a01 = (1,2,3,4,5,6,7,8,9)
self.ca01 = (ctypes.c_int*9)(*self.a01)
self.dca01 = ctypes.byref(self.ca01)
self.a02 = (9,8,7,6,5,4,3,2,1)
self.ca02 = (ctypes.c_int*9)(*self.a02)
self.dca02 = ctypes.byref(self.ca02)
self.a03 = (6,1,9,3,7,-2,5,-4,0,8,2)
self.ca03 = (ctypes.c_int*len(self.a03))(*self.a03)
self.dca03 = ctypes.byref(self.ca03)
def tearDown(self):
...
def test_bubble(self):
sort = self.get_sort()
sort.bubble(self.dca01, 9)
self.assertEqual(tuple(self.ca01), self.a01)
sort.bubble(self.dca02, 0)
self.assertEqual(tuple(self.ca02), self.a02)
sort.bubble(self.dca02, -1)
self.assertEqual(tuple(self.ca02), self.a02)
sort.bubble(self.dca02, 9)
self.assertEqual(tuple(self.ca02), self.a01)
sort.bubble(self.dca03, len(self.a03))
self.assertEqual(list(self.ca03), sorted(self.a03))
def test_select(self):
sort = self.get_sort()
sort.select(self.dca01, 9)
self.assertEqual(tuple(self.ca01), self.a01)
sort.select(self.dca02, 0)
self.assertEqual(tuple(self.ca02), self.a02)
sort.select(self.dca02, -1)
self.assertEqual(tuple(self.ca02), self.a02)
sort.select(self.dca02, 9)
self.assertEqual(tuple(self.ca02), self.a01)
sort.select(self.dca03, len(self.a03))
self.assertEqual(list(self.ca03), sorted(self.a03))
在setUpClass中,将sort.c编译成sort.so,并加载到类变量sort。setUp用来准备测试数据,给两个test_开头的单元测试函数。测试结束后,通过tearDownClass函数删除sort.so文件。
直接在函数接口使用Python的bytes对象,就是传递指针的效果。
>>> from ctypes import *
>>> libc = CDLL('libc.so.6')
a>>> libc.printf(b"abcde 12345\n")
abcde 12345
12
继续看代码。下面是一段C代码,接收一个char *str
指针,然后以字符串的方式从这个地址开始打印:
#include <stdio.h>
void print_str(char *str) {
printf("%s\n",str);
}
在Python中,可以这样来调用(编译成.so这部分就省略了,下同):
>>> from ctypes import *
>>> cc = CDLL('./c2.so')
>>> cc.print_str(b'abcde')
abcde
6
>>> cc.print_str(b'abcde1234567')
abcde1234567
13
继续借用上面的例子看代码:
>>> a = c_char_p(b'abcde 12345 kkkkk')
>>> cc.print_str(a)
abcde 12345 kkkkk
18
准备一个C函数,接收一个结构体指针,然后打印结构体中各项的内容:
#include <stdio.h>
typedef struct {
int a;
int b;
double c;
} abc;
void show_abc(abc *in) {
printf("%d %d %f\n", in->a, in->b, in->c);
}
下面是Python代码的调用方式(不继承ctypes.Structure):
>>> from ctypes import *
>>> cc = CDLL('./c2.so')
>>>
>>> import struct
>>> cc.show_abc(struct.pack('iid',1,2,3))
1 2 3.000000
13
struct.pack返回的就是一个已经格式化后的bytes对象,'iid'是格式化字符串,对应C代码中的abc结构体,必须要对应哈。给C函数传递bytes对象,就是传递指针!
传递函数指针,其实,就是直接通过ctypes使用函数名。
/* for qsort in libc */
int
cmp_int(const void *keyval, const void *datum) {
//return (*(int*)keyval - *(int*)datum);
int k = *(int*)keyval;
int d = *(int*)datum;
return k==d ? 0 : k<d ? -1 : 1;
}
这个C函数,是给C标准库中的qsort准备的,下面的Python代码,会将这个函数传递给libc.qsort(代码省略了部分内容):
def test_libc_qsort(self):
libc = ctypes.CDLL('libc.so.6')
libc.qsort(self.dca02,
len(self.a02),
ctypes.sizeof(ctypes.c_int),
self.get_sort().cmp_int)
self.assertEqual(list(self.ca02), sorted(self.a02))
libc.qsort(self.dca03,
len(self.a03),
ctypes.sizeof(ctypes.c_int),
self.get_sort().cmp_int)
self.assertEqual(list(self.ca03), sorted(self.a03))
from ctypes import WinDLL
lib = WinDLL(dllfile)
当dll文件在当前路径下,windows平台要使用
.\filename
,Linux平台使用./filename
。
在Python中调用的C接口,此C接口需要输入一个callback回调。我们同样可以用Python的一个函数接口来模拟这个C风格的callback,当C接口内调用这个callback的时候,实际上在调用Python接口。
假设C接口定义如下:
typedef void (*listener)(void* context, int32_t event, int32_t code, const char* extra);
int set_listener(listener callback, void* context);
我们要在Python中调用set_listener,而此C接口需要一个callback,我们用Python,来提供这个callback,如下:
LISTENER_CB = CFUNCTYPE(None, c_void_p, c_int, c_int, c_char_p)
def listener_cb(a,b,c,d):
print('-- event, code --',b,c)
cb = LISTENER_CB(listener_cb)
a = testlib.set_listener(cb, None)
当testlib.set_listener内部调用cb接口的时候,Python会根据CFUNCTYPE的定义,处理传递的参数,使它们兼容(None对应void返回)。
本文链接:https://cs.pynote.net/sf/python/202112281/
-- EOF --
-- MORE --