LinMao's Blog
学习科研记录与分享!

Python中ctypes模块

最近做项目一直使用Python来调用C中的动态链接库,有的地方不是很清楚,系统整理一下。

模块ctypes是Python内建的用于调用动态链接库函数的功能模块,一定程度上可以用于Python与其他语言的混合编程。由于编写动态链接库,使用C/C++是最常见的方式,故ctypes最常用于Python与C/C++混合编程之中。

ctypes 实现了一系列的类型转换方法,Python的数据类型会包装或直接推算为C类型,作为函数的调用参数;函数的返回值也经过一系列的包装成为Python类型。也就是说,PyObject* <-> C types的转换是由ctypes内部完成的。

ctypes 有以下优点:

  • Python内建,不需要单独安装
  • 可以直接调用二进制的动态链接库
  • 在Python一侧,不需要了解Python内部的工作方式
  • 在C/C++一侧,也不需要了解Python内部的工作方式
  • 对基本类型的相互映射有良好的支持

ctypes 有以下缺点:

  • 平台兼容性差
  • 不能够直接调用动态链接库中未经导出的函数或变量(这个现在不明白什么意思,但是不影响后面的使用)
  • 对C++的支持差

 

简单的例子

先写一个简单的C语言程序func.c,然后编译成动态链接库:

接下来把它编译为动态链接库。Windows下动态链接库的扩展名是dll,Linux下是so,Mac OS X下是dylib。其实扩展名是给人看的,对于机器来说无所谓,在linux下扩展名命名为dll也没有问题。GCC编译命令如下:

-shared–fPIC参数用于指示编译器进行动态链接库的编译

然后写一个python程序func.py来测试一下:

输出:

所以一个简单的python调用C语言完成了,但是真正的代码怎么会这么简单。感觉Python和C之间的调用主要的是解决两个语言之间的数据类型映射关系,也就是用Python调用C时主要是解决怎么把python中的数据转换成C函数参数中对应的数据类型以及把C函数返回值转换成python中的数据类型,而这个工作就是ctypes模块要做的。

 

类型映射:基本类型

对于数字和字符串等基本类型。ctype作为Python和C之间的桥梁,在python调用C时,将python中的值类型通过ctypes中间类型转换成C中的类型;在调用后返回值时,将C中值类型通过ctypes中间类型转换成python中的类型。ctypes中间类型的值可以通过构造函数中传递对应python类型的值来初始化ctypes中间变量。还可以通过修改中间类型的成员变量value的值,(中间变量).value。

ctypes中间类型,C类型和python类型对应关系如下表:

ctypes 类型 C 类型 Python 数据类型
c_bool _Bool bool (1)
c_char char 单字符字节对象
c_wchar wchar_t 单字符字符串
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 __int64long long int
c_ulonglong unsigned __int64unsigned long long int
c_size_t size_t int
c_ssize_t ssize_tPy_ssize_t int
c_float float float
c_double double float
c_longdouble long double float
c_char_p char * (NUL terminated) 字节串对象或 None
c_wchar_p wchar_t * (NUL terminated) 字符串或 None
c_void_p void * int 或 None

举个例子,用python来调用C语言中的printf函数,当然只是在这里举这个例子而已,其实正常情况下没有必要这么调用,因为python中就有print函数。

在Windows中,printf 函数位于%SystemRoot%\System32\msvcrt.dll,在Mac OS X中,它位于 /usr/lib/libc.dylib,在Linux中,一般位于 /usr/lib/libc.so.6。所以写一个python代码:

从最后一行中可以看到C中printf函数原型为:

第一个参数用c_char_p创建一个C字符串,并以构造函数的方式用一个Python字符串初始化它;后两个参数是一个int型和一个double型的变量,相应地用c_int和c_double创建对应的C类型变量,并以构造函数的方式初始化它们。

如果不用构造函数,还可以用value成员:

上面这一块代码与下面这一句代码等价:

这里面可以看成是先用构造函数创建几个PyObject,比如上面的str_format,int_val和double_val,然后在设置对象中成员变量value的值。感觉作用机理是,python将这些PyObject传递给ctypes,然后ctypes将相应PyObject中的成员变量value值取出传递给C中的函数。

另外,C语言中经常有一些函数接受指针变量并修改指针变量所指向的值。这种情况下相当于数据从C函数中流回到python。可以使用value成员来获取该值。

同样以C中的strcat函数为例,函数原型如下:

函数功能是将后面一个字符串src接到前面一个字符串dest后面,返回新的字符串(要保证dest字符数组够长)。当然这里面也是用来举例子,正常情况下没有必要这么调用,python中加号’+’重载直接可以字符串连接。

在Windows中,strcat函数位于%SystemRoot%\System32\msvcrt.dll,在Mac OS X中,它位于 /usr/lib/libc.dylib,在Linux中,一般位于 /usr/lib/libc.so.6。所以写一个python代码:

另外,当 ctypes 可以判断类型对应关系的时候,可以直接将Python类型赋予C函数,而不需要使用ctypes构造函数来传递。ctypes 会进行隐式类型转换。例如:

但是,当 ctypes 无法确定类型对应的时候,会触发异常。

这里面会报错,第三个浮点数类型不能进行隐式类型转换,会报错。所以,对于这种情况,不管能不能进行隐式类型转换,都先用ctypes构造函数显示转换一下。

高级类型映射:数组

在C语言中,char代表简单的字符类型,而char [ ]代表字符数组类型。ctypes中也是一样,使用数组是需要预先生成所需要使用的数组类型。

写一个C语言代码func2.c如下:

然后执行下面命令编译成动态链接库:

然后在写一个python代码func2.py来调用上面的动态链接库:

ctypes类型重载了操作符”*”,这里的”*”不是乘号。type_int_array_10为创建的数组类型,这里是数组类型而不是数组变量,也就是相当C语言中int [10],想要得到数组变量就需要实例化,即my_array = type_int_array_10(),就是相当于C语言中my_array = int [10]。my_array的每一个成员的类型应该是 c_int,这里将它索引为2的成员赋予值 c_int(5),由于隐式转换的存在,这里写 my_array[2] = 5也可以,但是个人觉得还是显式指定类型就行。

返回值类型映射

ctypes 规定,总是假设返回值的类型为int。对于上面的array_get函数而言,碰巧函数返回值也是int,所以返回的数值能被正确取到,但是如果C语言中函数返回值不是int,则需要在函数调用之前显式告诉ctypes返回值类型。

上面的strcat.py中作如下修改:

上面的代码在没有指定strcat的返回值类型的时候,输出一长串数字,猜测可能是把字符串的首地址当成十进制数打印出来了。另外,我还发现一个问题,在python中的动态链接库中可以直接调用clib.strcat(‘a’, ‘b’)(隐式类型转换),但是在C语言中不能直接strcat(“a”, “b”)这样调用,必须给前面的那个字符数组先分配足够大的空间,不然会报错,而在python中根本不需要考虑这一点,可能是ctypes自动给前面的字符串分配空间,不得不说这对python程序源很友好。

定义一个“高维数组”的方法类似,之所以加了引号,是因为C语言里并没有真正的高维数组,同样ctype也是利用数组的数组实现的:

高级类型映射:简单指针类型

ctypes 和C一样区分指针类型和指针变量。语言里,int *是指针类型,用它声明的变量就叫指针变量,指针变量可以被赋予某个变量的地址。

在ctypes中,指针类型用 POINTER(ctypes_type) 创建,例如创建一个类似于C语言的int *:

上面的type_p_int是一个类型,这个类型是指向int的指针类型,只有将指针类型例化之后才能得到指针变量。指针类型的成员变量contents代表指针所指向的内容。这段代码在C语言里相当于:

另外,还可以直接使用 ctypes 提供的pointer()得到一个变量的指针变量 :

上面的代码#——-前后输出一样,pointer()就是取变量的地址,相当于C语言中&操作符。

注意:POINTER()是用来定义指针类型,是指针类型而不是指针变量,同时与注意定义指针类型和定义数组的区别,上面的p_int[0]方式注意和数组的调用方式区分开;pointer()是用来取地址的,相当于C语言中的&操作符,与指针中的[指针变量].contents互为逆运算。

高级类型映射:结构体

C语言中经常会用到结构体,ctypes提供了很多类型的PyObject到C的映射,同样也提供了结构体类型的映射。python中将结构体定义为一个class(继承ctypes中Structure的class);结构体成员变量的定义在python中是在_fields_中定义,在python中调用结构体的成员变量值的方式:[结构体变量名].[成员变量名]。

现在分别从结构体作为函数参数和函数返回值来进行说明ctypes中的结构体,先写一个C语言程序func3.c如下:

先写一个python代码func3.py,验证结构体指针作为函数参数:

输出:

再写一个python代码func4.py,验证结构体作为函数参数,结构体指针作为函数返回值:

输出:

 

Reference:

聊聊Python ctypes 模块

ctypes — Python 的外部函数库

Python Ctypes 结构体指针处理(函数参数,函数返回)

 

 

 

 

 

赞(0) 打赏
转载请注明出处:LinMao's Blog(林茂的博客) » Python中ctypes模块

评论 1

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
  1. #1

    python中还可以使用cffi来集成C语言程序,例如:
    https://www.cnblogs.com/ccxikka/p/9637545.html
    https://zhuanlan.zhihu.com/p/52485004

    linmao4年前 (2020-10-10)回复

LinMao's Blog(林茂的博客)

了解更多联系我们

觉得文章有用就打赏一下作者吧~

支付宝扫一扫打赏

微信扫一扫打赏