详解ctypes模块及如何调用C函数

2020年11月23日 / 168次阅读 / Last Modified 2020年12月4日
ctypes

python和C的混合编程,最简单直接的方法,就是使用标准库中的ctypes模块。通过ctypes模块可以实现调用.so动态链接库中的foreign fucntion。

很多在python中调用so库中的C函数的需求,都是为了性能。比如业务流程中某个功能很消耗资源,运行时间太长,我们就可以想办法用C来重写这个功能,然后编程成一个so库,让python代码调用。

下面的代码,在python中引入了标准C库函数:

>>> from ctypes import *
>>> libc = CDLL('libc.so.6')
>>> libc.printf(b'pynote.net\n')
pynote.net
11
>>> libc.time(None)
1606122509
>>> libc.time(None)
1606122511

下面的代码,测试一个自己写的C函数,在python中被调用的步骤:

int addone(int a)
{
    return a+1;
}

这个函数就是对入参加1,然后返回。

用gcc编译成so动态链接库:

$ gcc -fPIC -shared test.c -o test.so

这样,我们就得到test.so,它与其它python代码再同一路径下。以下测试代码中的C函数,都用此方法编译成.so库,其中-fPIC的含义,请参考:gcc常用编译参数

然后,在python中调用这个用C编写的addone函数:

>>> tt = CDLL('./test.so')
>>> tt.addone(1)
2
>>> tt.addone(10)
11
>>> tt.addone(100)
101
>>> tt.addone(1000)
1001

成功!不过,上面的例子过于简单了。

我们都知道,python中一切都是对象,而C语言是无对象的,参数变量都是各种不同内存长度的基础类型;而且,python函数传递的是reference,而C默认是call by value,很多时候value是某个内存地址。因此,使用ctypes调用C函数,如何在python和C之间传递参数成了重要的技术点。

传入指针

上面的例子,都不涉及参数类型,使用python的ctypes模块,自动处理了int和byte string!下面的例子,我们用C写一个swap函数,入参是int *:

void swap(int *a, int *b)
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

编译成.so库后,在python中测试:

>>> s = CDLL('./sort.so')
>>> a = 1; b = 2
>>> ca = c_int(a); cb = c_int(b)
>>> ca
c_int(1)
>>> cb
c_int(2)
>>> s.swap(byref(ca), byref(cb))
-841123440
>>> ca
c_int(2)
>>> cb
c_int(1)
>>> ca.value
2
>>> cb.value
1

以上代码首先创建两个ctype,ca和cb,调用c_int,这个函数是ctypes模块提供的。然后再调用C语言编写的swap函数时,通过byref函数获得两个c_int对象的指针,byref函数的入参只能是转换后的对象(当然还有其它的c_*函数用来做类型转换)。最后的结果,成功交换两个变量的值。

指定入参和返回类型

除了int,其它基础类型数据,在传入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 type相关操作

ctypes模块提供的c_*系列函数,其实不能用函数啦,callable更准确一些,他们的作用是将python对象转换成ctypes模块可以传递给底层C函数的ctype对象。

ctypes模块有sizeof操作

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

需要注意一个细节:指针类型的value如果被改变,这些类型的地址也会发生变化,正如python的immutable对象,这些指针类型的底层是byte string,对于python来说,也是immutable 的。

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

在Python中修改会改变地址,但在C中修改就不会改变地址:

void chstr(char *p, int n)
{
    for(int i=0; i<n; ++i)
        ++p[i];
}

上面这个函数,在Python中的调用情况:

>>> from ctypes import *
>>> s = CDLL('./sort.so')
>>> cc = c_char_p(b'12345')
>>> cc
c_char_p(140004687356320)
>>> id(cc)
140004687144384
>>> cc.value
b'12345'
>>> s.chstr(cc,5)
5
>>> cc
c_char_p(140004687356320)
>>> cc.value
b'23456'
>>> s.chstr(cc,5)
5
>>> cc.value
b'34567'
>>> s.chstr(cc,5)
5
>>> cc.value
b'45678'
>>> id(cc)
140004687144384
>>> print(cc)
c_char_p(140004687356320)
>>> cc
c_char_p(140004687356320)
>>> s.chstr(cc,5)
5
>>> id(cc)
140004687144384

注意:cc是c_char_p类型,调用C函数时,并没有使用byref。

create_string_buffer()

上面说到ctypes模块提供的指针类型,如果修改其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作为字符串的结束。

传入数组

老规矩,先写个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];
        }
    }
}

以上代码,交换数值部分,请参考:用XOR异或做数值交换

编译成.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操作

题外话:我测试了这个bubble函数与python内置的sorted函数的效率,后者胜,这至少说明,python内部已经有速度上的优化,那些部分用C来写,还真的要好好测试!

多维数组

C语言是强类型的,而且在编程过程中,始终要意识到内存的问题。Python是无类型的,内存也是解释器管理,因此编程会更简单,也更少出bug。Python有些对象是mutable,比如list,可以随意扩展大小。而C中,原生就没有这样的数据类型。

下面的代码,给出用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语言中大量的被使用,当需要用ctypes模块给C函数传入结构体时,按模块约定,只能传地址。

测试代码(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_成员,如上面代码。

如何处理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));
    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函数,它已经是指针了。

多测试一点:

>>> p = ccc.init(11,22,c_float(6.666))
>>> p
<__main__.LP_abc object at 0x7fb47d7665c0>
>>> p.contents.a
11
>>> p.contents.b
22
>>> p.contents.c
6.665999889373779
>>> ccc.destroy(p)

关于ctypes模块的内容,我觉得已经差不多了。

-- EOF --

本文链接:https://www.pynote.net/archives/2869

留言区

《详解ctypes模块及如何调用C函数》有4条留言

您的电子邮箱地址不会被公开。 必填项已用*标注

  • 麦新杰

    c_int*n,这是运算符重载。 [回复]

  • 麦新杰

    有了ctypes中定义的C类型,以及so库,代码写起来就是C的风格啦! [回复]

  • 麦新杰

    用python来给C函数做单元测试,用ctypes模块,这种方法有什么好处? [回复]

  • 麦新杰

    用c_*系列函数创建给C接口的对象,但没有转回的,也不需要,用.value就可以了。 [回复]


前一篇:
后一篇:

More


©Copyright 麦新杰 Since 2019 Python笔记

go to top