2020.6.22更新:
增加了部分案例,并在python2和python3下都进行了调试。
——————————————————————————————————————————
因为工作需求,最近要使用python在linux环境下调用c/c++的动态库,执行动态库中的函数。这种没接触过的内容,自然首先开启百度谷歌大法。经过一番搜索,尝试使用python的ctypes模块。
一、初识
首先自然是查询文档了。
附文档链接:
python2.7 ctypes文档
python3.6 ctypes文档
python2.7文档描述:“ctypes is a foreign function library for Python. It provides C compatible data types, and allows calling functions in DLLs or shared libraries. It can be used to wrap these libraries in pure Python.”
大意是ctypes是python的一个外部函数库,提供了c的兼容数据类型,允许调用DLL或者共享库中的函数。通过该模块能以纯python的代码对这些库进行封装(最后这句话不理解 =_=,看别人的文章,说是这样你就可以直接 import xxx 来使用这些函数库了)。
可知ctypes只提供了c的兼容,因此若是c++代码,需要使其以c的方式进行编译。
(想了解如何用c的方式编译c++代码,见链接:c方式编译c++)
二、使用
注明:本文均采用python2.7版本
新增了python3的调试,除了str编码内容,其余无需变动
看了文档,就要开始使用了。根据我遇到的使用场景,主要有3个步骤需要处理。
0_0
1.动态库的引入
这一步操作很简单,只需调用方法LoadLibrary,查看文档:
LoadLibrary(name):Load a shared library into the process and return it. This method always returns a new instance of the library.
加载动态库到进程中并返回其实例对象,该方法每次都会返回一个新的实例。
举个栗子,编写如下代码:
# -*- coding: utf-8 -*-
from ctypes import *
#引入动态库libDemo.so
library = cdll.LoadLibrary("./libDemo.so")
2.函数的声明和调用
因为ctypes只能调用C编译成的库,因此不支持重载,需要在程序中显式定义函数的参数类型和返回类型。
不过在介绍前,需要先了解ctypes的基本数据类型
该表格列举了ctypes、c和python之间基本数据的对应关系,在定义函数的参数和返回值时,需记住几点:
- 必须使用ctypes的数据类型
- 参数类型用关键字argtypes定义,返回类型用restype定义,其中argtypes必须是一个序列,如tuple或list,否则会报错
- 若没有显式定义参数类型和返回类型,python默认为int型
举个栗子:
/******C端代码*********/
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include "demo.h"
int hello()
{
printf("Hello world\n");
return 0;
}
编写c端代码demo.c, 用GCC指令编译成动态库libDemo.so :gcc -fPIC -shared -o libDemo.so demo.c
编写python端代码linkTest.py进行调用:
# -*- coding: utf-8 -*-
from ctypes import *
#引入动态库libDemo.so
library = cdll.LoadLibrary("./libDemo.so")
library.hello()
执行结果:
3.C语言和python之间数据类型的转换
这部分内容是当初学习的重点,踩了不少坑。
3.1 基本数据类型
这部分已经在上面提到过了,根据对应表格进行转换使用即可。
样例代码在上述demo.c和linkTest.py中继续添加:
C端代码:
......(省略上述代码)
int basicTest(int a, float b)
{
printf("a=%d\n", a);
printf("b=%f\n", b);
return 100;
}
python端代码
def c_basic_test():
library.basicTest.argtypes = [c_int, c_float]
library.basicTest.restype = c_void_p
a = c_int(10)
b = c_float(12.34)
library.basicTest(a, b)
执行结果:
3.1-增加 字符指针及数组类型
c代码:
void arrayTest(char * pStr, unsigned char *puStr)
{
int i = 0;
printf("%s\n", pStr);
for (i = 0; i < 10; i++) {
printf("%c ", puStr[i]);
}
printf("\n");
}
python代码:
该样例有几个注意点:
<1>使用了ctypes的create_string_buffer和from_buffer_copy函数。
create_string_buffer函数会分配一段内存,产生一个c_char类型的字符串,并以NULL结尾。
同理有个类似函数create_unicode_buffer函数,返回的是c_wchar类型
from_buffer_copy函数则是创建一个ctypes实例,并将source参数内容拷贝进去
<2>python2与python3的字符差异:
python2默认都是ascii编码,python3中str类型默认是unicode类型,而ctypes参数需传入bytes-like object(提示这么说的)。因此python3中的字符串都需转换编码,如样例所示。
def c_array_test():
# 以下两方法皆可,但需显式说明c_ubyte数组的大小,个人感觉不方便,求指导
# library.arrayTest.argtypes = [c_char_p, c_ubyte*16]
library.arrayTest.argtypes = [c_char_p, POINTER(c_ubyte * 16)]
library.arrayTest.restype = c_void_p
# python2
# str_info = create_string_buffer("Fine,thank you")
# python3,str都是unicode格式,需转为其余编码,可使用encode函数或b前缀
# 以下两种方法皆可
str_info = create_string_buffer(b"Fine,thank you")
str_info = create_string_buffer("Fine,thank you".encode('utf-8'))
# 调用类型需配套
u_str_info = (c_ubyte * 16).from_buffer_copy(b'0123456789abcdef')
# library.arrayTest(str_info, u_str_info)
library.arrayTest(str_info, byref(u_str_info))
执行结果:
3.2 指针
- 一级指针
指针是c语言中的重要内容,难免要经常使用。因为我处理的主要是api接口的转换,涉及的指针处理就是定义指针类型、获取地址或值,而ctypes中都提供了相应的函数。
在上面的对应表格中,我们可以看到char * 和 void * 已经有专用类型了,直接使用即可,对于其他类型的指针,ctypes提供了两种定义方式:pointer 和POINTER。
查询文档:
ctypes.POINTER(type)
This factory function creates and returns a new ctypes pointer type. Pointer types are cached and reused internally, so calling this function repeatedly is cheap. type must be a ctypes type.
ctypes.pointer(obj)
This function creates a new pointer instance, pointing to obj. The returned object is of the type POINTER(type(obj)).
大意是POINTER必须传入ctypes类型,创建出新的ctypes 指针类型(pointer type),而pointer传入一个对象,创建出一个新的指针实例。可见POINTER创建出了pointer,我一般选择POINTER来使用(具体差别还没太多研究。。=_=).
传输地址,ctypes提供了byref函数:
ctypes.byref(obj[, offset])
Returns a light-weight pointer to obj, which must be an instance of a ctypes type. offset defaults to zero, and must be an integer that will be added to the internal pointer value.
…
The returned object can only be used as a foreign function call parameter. It behaves similar to pointer(obj), but the construction is a lot faster.
大意是返回一个指向ctypes实例对象的轻量级指针,函数中还可以通过参数(必须是int)来设置偏移地址,这个返回的对象只能用于外部函数调用的参数。
有了这几个函数,指针就能实现啦。见实例:
C端代码
void pointerTest(int * pInt, float * pFloat)
{
*pInt = 10;
*pFloat = 12.34;
}
python端代码
def c_pointer_test():
library.pointerTest.argtypes = [POINTER(c_int), POINTER(c_float)]
library.pointerTest.restype = c_void_p
int_a = c_int(0)
float_b = c_float(0)
library.pointerTest(byref(int_a), byref(float_b))
print("out_a:", int_a.value)
print("out_b:", float_b.value)
可见我们在python中预设的值都在函数中被更改了,指针有效。
- 补充样例:外部传输字符空间,在函数内部进行操作
c代码:
void mallocTest(char *pszStr)
{
strcpy(pszStr, "Happay Children's Day!");
}
python代码:
def c_malloc_test():
library.mallocTest.argtypes = [c_char_p]
library.mallocTest.restype = c_void_p
word = (c_char * 32)()
library.mallocTest(word)
print("out_word:", word.value)
执行成功!
- 二级指针
在实际应用中,我处理的二级指针是在c端接口传送一个char*指针的地址作为参数,在接口中给char * 指针分配空间和赋值。
根据指针的功能,很容易实现,见实例。
c端接口代码
/**函数定义*/
void doublePointTest(int ** ppInt, char ** ppStr)
{
printf("before int:%d\n", **ppInt);
**ppInt = 10086;
*ppStr = (char*)malloc(10 * sizeof(char));
strcpy(*ppStr, "Happy National Day!");
}
//释放函数
void freePoint(void *pt) {
if (pt != NULL) {
free(pt);
pt = NULL;
}
}
/**函数调用,运行会输出接口中复制的字符串*/
int useDoublePoint()
{
int num_e = 10;
int * pInt = &num_e;
char *pStr_b = NULL;
doublePointTest(&pInt, &pStr_b);
printf("after int:%d\n", *pInt);
printf("out str:%s\n", pStr_b);
//必须释放动态内存
freePoint(pStr_b);
}
python端代码
# 函数定义及测试
def c_double_point_and_free_test():
library.doublePointTest.argtypes = [POINTER(POINTER(c_int)), POINTER(c_char_p)]
library.doublePointTest.restype = c_void_p
library.freePoint.argtypes = [c_void_p]
library.freePoint.restype = c_void_p
int_a = c_int(10)
# 注意POINTER和pointer的区别,因为POINTER必须指向ctypes类型,此处只能用pointer
int_pt = pointer(int_a)
word_pt = c_char_p()
library.doublePointTest(byref(int_pt), byref(word_pt))
print("out_word:", word_pt.value)
# contents是指针内容
print("out_value:", int_pt.contents.value)
library.freePoint(word_pt)
执行结果:
另一个c端接口,需要传入一个char* 指针的地址,出参为这个指针指向的内容和内容长度。该接口用于处理图片的rgb值,因此内容都是0-255的值,样例代码如下
......(省略上述代码)
#define LENGTH 8
void doubleUnsignedPointerTest( char ** ppChar, int *num )
{
int i;
*num = LENGTH;
*ppChar = (char*)malloc(LENGTH*sizeof(char));
for(i=0;i<LENGTH;i++)
{
(*ppChar)[i] = i;
}
}
C端调用代码
int num = 0, i;
doubleUnsignedPointerTest(&pChar, &num);
for(i=0;i<num;i++)
{
printf("pChar[%d]:%d\n", i, pChar[i]);
}
freePointer(pChar);
这时python端若使用c_char_p来构造类似上述的代码,在输出时会提示超出范围异常(out of range)。
查询了文档,原因在于:
class ctypes.c_char_p
Represents the C char * datatype when it points to a zero-terminated string…
(大意是c_char_p指针必须指向’\0’结尾的字符串。因此输出时遇到了第一个’\0’就中断了)
所以根据对应表格,使用POINTER嵌套构造了双重指针,使用后无误
python端代码
......(省略上述代码)
library.doubleUnendPointerTest.argtypes = [POINTER(POINTER(c_byte)), POINTER(c_int)]
library.doubleUnendPointerTest.restype = c_void_p
num = c_int(0)
word_pt = POINTER(c_byte)() #若使用c_char类型,在下面输出时会以字符类型输出,有乱码,因此选择c_byte
library.doubleUnendPointerTest(byref(word_pt), byref(num))
print "num:", num.value
for i in range(8):
print "key:", i, " value:", word_pt[i]
执行结果:
3.3 结构体、共用体
结构体、共用体是c中常用类型,使用前需要先定义其成员类型,在python中也是同样的处理。查看文档:
Structures and unions must derive from the Structure and Union base classes which are defined in the ctypes module. Each subclass must define a fields attribute. fields must be a list of 2-tuples, containing a field name and a field type.
The field type must be a ctypes type like c_int, or any other derived ctypes type: structure, union, array, pointer.
大意是结构体和共用体必须继承Sturcture和Unino类,定义其成员必须使用_field_属性。该属性是一个list,其成员都是2个值的tuple,分别是每个结构体/共用体成员的类型和长度,而且定义类型必须使用ctype类型或由ctype组合而成的新类型。
以此写个样例:
c端代码
//结构体
typedef struct _rect
{
int index;
char info[16];
}Rect;
<1>读取结构体
C代码:
int readRect(Rect rect)
{
printf("value=============\n");
printf("index:%d\ninfo:%s\n", rect.index, rect.info);
return 0;
}
python代码:
# 结构体
class Rect(Structure):
_fields_ = [
('index', c_int),
('info', c_char * 16)
]
def c_read_rect():
library.readRect.argtypes = [Rect]
library.readRect.restype = c_void_p
rect_a = Rect(10, b"Hello")
library.readRect(rect_a)
<2>读取结构体,传参为指针
C代码:
int readRectPoint(Rect * pRect)
{
printf("point==============\n");
printf("index:%d\n", pRect->index);
printf("info:%s\n", pRect->info);
return 0;
}
python代码:
def c_read_rect_point():
library.readRectPoint.argtypes = [POINTER(Rect)]
library.readRectPoint.restype = c_void_p
rect_a = Rect(10, b"Hello")
library.readRectPoint(byref(rect_a))
<3>类似,可以传输结构体数组给动态库,实质是传输结构体数组指针,也就是首元素指针
C代码:
void readRectArray(Rect *pRectArray)
{
int i;
for(i=0;i<5;i++)
{
printf("pRectArray.index:%d\n", pRectArray[i].index);
printf("pRectArray.info:%s\n", pRectArray[i].info);
}
}
python代码:
def c_read_rect_array():
library.readRectArray.argtypes = [POINTER(Rect)]
library.readRectArray.restype = c_void_p
rect_array = (Rect * 5)()
for i in range(5):
# python2
# rect_array[i] = Rect(i, "Hello_" + str(i))
# python3
rect_array[i] = Rect(i, bytes("Hello_"+str(i), encoding='utf-8') )
# 以下两方法皆可
# library.readRectArray(rect_array)
library.readRectArray(byref(rect_array[0]))
执行结果:
<4> 从动态库中获取结构体数组内容
C代码:
/**函数定义*/
Rect * obtainRectArray(int *pArrayNum)
{
int num = 5;
*pArrayNum = num;
Rect *pArray = (Rect*)malloc(num * sizeof(Rect));
for (int i = 0; i < num; i++) {
pArray[i].index = i;
sprintf(pArray[i].info,"%s_%d", "Hello", i);
}
return pArray;
}
//必须释放内存
void freeRect(Rect *pRect)
{
free(pRect);
}
/**c中调用方式*/
void testObtainRectArray(int *num)
{
int num = 0;
Rect *pstRectArray = obtainRectArray(&num);
for (int i = 0; i < num; i++) {
printf("index:%d\n", pstRectArray[i].index);
printf("info:%s\n", pstRectArray[i].info);
}
freeRect(pstRectArray);
}
python代码:
ctypes的pointer有一个contents方法,通过该方法可以获取指针的内容。但在这个样例中,contents只能获取首元素的内容,后来才发现竟然能循环读取= =
def c_obtain_rect_array_and_free():
library.obtainRectArray.argtypes = [POINTER(c_int)]
# library.obtainRectArray.restype = Array()
library.obtainRectArray.restype = POINTER(Rect)
library.freeRect.argtypes = [POINTER(Rect)]
library.freeRect.restype = c_void_p
num = c_int(10)
rect_pt = library.obtainRectArray(byref(num))
num = num.value
print("num:", num)
# 这种赋值方法是真的没想到,找了好久= = 之前一直在rect_pt.contents上面绕
# rect_pt.contents只能输出首元素的内容,如rect_pt.contents.index
rect_array = [rect_pt[i] for i in range(num)]
for item in rect_array:
print("index:", item.index)
print("info:", item.info)
library.freeRect(rect_pt)
执行结果:
三、结语
通过文档,python调用C动态库还是比较容易实现的,只是第一次使用没有经验,摸索了一段时间。其实文档还有很多内容,待后续再慢慢学习吧。
附源码:https://pan.baidu.com/s/10dUqfrVQYYDDFTgNAa0Pmw
提取码: yctx
四、补充
1,动态库so中的函数,如fun_add,可能使用了c++的函数模块,直接增加extern "C"无法编译,可以再封装一层函数,如fun_c_add,该函数直接调用fun_add函数,则fun_c_add可以增加extern "C"编译通过,python也可调用;
2,特殊的函数指针,如unsigned char *,转化为python,函数的定义参数格式是POINTER(c_ubyte),较为复杂,可以用c_void_p类型代替。