用ctypes模块调用C接口

Last Updated: 2023-09-21 11:26:58 Thursday

-- TOC --

通过ctypes模块,我们可以直接调用动态链接库中的接口,它是Python访问外部C接口的通道。这个模块帮过我的大忙,我用它测试过第三方的SDK,调用DLL中的接口测试Windows驱动。

貌似ctypes只能调用C接口!

调用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_*函数用来做类型转换)。最后的结果,成功交换两个变量的值。

如何传入NULL空指针

直接使用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指针类型c_char_p

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对应的是一个内存块。

create_string_buffer

开一个缓冲区,用来获取接口的返回值...

上面说到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是\x00port.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

继承ctypes.Structure

测试代码(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提供的功能。

用ctypes查看C基础类型的sizeof值

我用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函数返回的指针

前面的内容多次提到,给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函数的单元测试

如果你也像我一样,日常的开发工作是以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文件。

再谈通过ctypes向C函数传指针

bytes

直接在函数接口使用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

c_char_p又一例

继续借用上面的例子看代码:

>>> a = c_char_p(b'abcde 12345 kkkkk')
>>> cc.print_str(a)
abcde 12345 kkkkk
18

用struct.pack封装结构体指针

准备一个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))

调用Windows下的DLL

from ctypes import WinDLL
lib = WinDLL(dllfile)

当dll文件在当前路径下,windows平台要使用.\filename,Linux平台使用./filename

用Python来模拟C接口的callback参数

在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 --