Python ctypes模块使用方法与心得体会

1  简介

ctypes是一个自Python 2.5开始引入的,Python自带的函数库。其提供了一系列与C、C++语言兼容的数据结构类与方法,可基于由C源代码编译而来的DLL动态链接库文件,进行Python程序与C程序之间的数据交换与相互调用。

本文基于Python 3.6.3(64 Bit)以及MinGW GCC 6.3.0(64 Bit)。

注意,在使用ctypes的过程中,Python解释器与C编译器所支持的位数必须一致,即:32位Python解释器必须与32位C编译器配合使用,同理,64位编译器也必须保持一致使用,否则将很有可能导致DLL文件无法被解析、调用等问题。

2  基于GCCDLL/SO动态链接库的编译

本文使用gcc/g++编译器进行C源代码的编译操作。当需要使用gcc进行dll动态链接库文件(Linux上为so文件)的编译时,可使用如下命令进行:

>>> gcc -shared –fPIC xxx.c –oxxx.dll

上述命令中,“-shared”与“–fPIC”参数用于指示编译器进行动态链接库的编译,其后接输入文件名,一般为“.c”或“.cpp”结尾的文件。“-o”参数用于指定输出文件名,一般为“.dll”结尾的动态链接库文件。执行完此命令后,给定的输出路径下就会生成一个动态链接库文件,以供Python代码调用。

如果将上述命令用于C++源码的编译,则应将gcc编译器换为g++,且输入文件拓展名一般为“.cpp”。且如果当前操作系统使用的是Linux,则动态链接库一般以“.so”作为拓展名。

3   C++源码中的extern声明

原理上,Python解释器只可以直接调用C的函数接口,而对于C++的函数接口,则需要通过extern声明转换为C的函数接口,而后才能进行dll编译,以供Python调用。如果不做这样的声明,则将导致Python解释器提示找不到目标函数接口。C++的extern语句语法较为复杂,这里不做详细讨论,只展示本文需要用到的部分。

如果要将C++的函数接口声明为Python可调用的版本,则需要将文件中所有的函数原型声明放入extern声明的代码块中,例:

extern "C"

{

   int add(int aNum, int bNum);

}

int add(int aNum, int bNum)

{ ... }

上述代码中,假设我们声明了一个函数定义add,则需要将其函数原型声明放入extern的声明代码块中。extern声明以extern "C"开头,后接一对花括号代码块,其中包含下面所有函数的函数原型声明。

extern中也可略去原型声明,直接在其内部定义函数,这样做的效果类似于内联函数。例:

extern "C"

{

   int add(int aNum, int bNum)

    {

       ...

    }

}

最后,本节所讨论的extern声明的目的是将C++代码转变为可被Python调用的形式,而对于C语言源码,则无需进行此步骤,直接编译即可。

4  DLL文件解析与简单函数调用

首先,为方便起见,本文中所有的模块导入均通过“*”语法导入,但在实际代码的编写中,强烈不建议使用这种语法:

from ctypes import *

当编译好一个dll或so动态库文件后,在Python中就可通过ctypes模块去解析与调用了。解析dll文件可调用CDLL函数,其接受dll文件名作为参数,返回一个解析后的实例对象:

dllObj = CDLL('1.dll')

取得此对象后,即可像调用实例方法那样,以此对象调用dll文件中的各种函数。也就是说,dll文件中的所有函数,在经过CDLL函数解析并返回一个对象后,均可看做是此对象的实例方法,可以以点号的形式进行调用。例:

假设定义了如下C源代码,并已编译为“1.dll”文件:

int addNum(int xNum, int yNum)

{

   return xNum + yNum;

}

而后即可在Python中传入参数并调用此函数:

dllObj = CDLL('1.dll')

print(dllObj.addNum(1, 2))

输出结果为3。

由此可见,当调用CDLL函数解析dll文件,并得到解析对象后,即可像调用实例方法那样调用dll文件中的函数,且函数的参数就是实例方法的参数。

5  基本ctypes数据类型

下表展示了ctypes模块所提供的所有与C语言对应的基本数据类型:

ctypes类型C类型Python类型

c_bool_Boolbool (1)

c_charchar1个字符的字符串

c_wcharwchar_t1个字符的unicode字符串

c_bytecharint/long

c_ubyteunsigned charint/long

c_shortshortint/long

c_ushortunsigned shortint/long

c_intintint/long

c_uintunsigned intint/long

c_longlongint/long

c_ulongunsigned longint/long

c_longlonglong longint/long

c_ulonglongunsigned long longint/long

c_floatfloatfloat

c_doubledoublefloat

c_char_pchar *string或None

c_wchar_pwchar_t *unicode或None

c_void_pvoid *int/long或None

由上表可以看出,ctypes的命名规律是前置的“c_”加上C语言中的数据类型名,构成ctypes中定义的C语言数据类型。上表中较为常用的类型主要包括c_int、c_double、c_char以及c_char_p等。这三种类型将在下一节详细讨论。

6  函数的输入、输出数据类型

首先考虑如下改写的代码:

double addNum(double xNum, double yNum)

{

   return xNum + yNum;

}

此代码唯一的改动之处在于将上文的addNum函数的输入以及输出的数据类型均由int转成了double。此时如果直接编译此文件,并在Python中调用,就会发现程序抛出了ctypes定义的ctypes.ArgumentError异常,提示参数有错误。

出现此问题的原因在于DLL文件无法在调用其中函数时自动设定数据类型,而如果不对类型进行设定,则调用函数时默认的输入、输出类型均为int。故如果函数的参数或返回值包含非int类型时,就需要对函数的参数以及返回值的数据类型进行设定。

设定某个函数的参数和返回值的数据类型分别通过设定每个函数的argtypes与restype属性实现。argtypes需要设定为一个tuple,其中依次给出各个参数的数据类型,这里的数据类型均指ctypes中定义的类型。同理,由于C语言函数只能返回一个值,故restype属性就需要指定为单个ctypes类型。

对于上文的返回值为double的addNum,代码修改为如下形式即可运行:

dllObj = CDLL('1.dll')

dllObj.addNum.argtypes = (c_double,c_double)

dllObj.addNum.restype = c_double

print(dllObj.addNum(1.1, 2.2))

上述代码在调用addNum之前,分别设定了此函数的输入参数为两个double,返回值也为double,然后以两个小数作为参数调用这个函数,返回值为3.3,结果正确。

对于一个返回值类型为char指针的C语言函数,则只需要设定restype为c_char_p,函数即可直接返回Python的str类型至Python代码中。例:

设有如下返回字符串指针的C语言函数:

char *helloStr()

{

   return "Hello!";

}

则Python的调用代码:

dllObj = CDLL('1.dll')

dllObj.addNum.restype = c_char_p

print(dllObj.helloStr())

上述代码调用了返回字符串指针的helloStr函数,并先行设定返回值类型为c_char_p,则调用后即可直接获得一个Python字符串“Hello!”。

7  类方法的调用

对于C++中类方法的调用,可以通过多种方法实现。一般情况下,可以编写一个函数,其中动态地声明一个实例指针,然后通过指针调用某个类方法,调用完成后,再释放此时动态申请的内存。例:

extern "C"

{

   void hello();

}

class Test

{

   public:

       void hello();

};

void Test::hello()

{

   printf("Hello!");

}

void hello()

{

   Test *testP = new Test;

   testP -> hello();

   delete testP;

}

上述代码定义了一个Test类,而后定义了一个Test类的hello方法,再定义了一个名为hello的函数作为接口函数。在接口函数中,首先通过new语句创建了一个实例指针,然后通过此指针调用了Test类的hello方法,调用完成后,再释放此指针。故在Python中,只需要调用这个接口函数hello,即可实现调用Teat类中的hello方法。

8  高级ctypes数据类型——数组

首先,由于Python和C的数组在数据结构上有本质的差别,故不可以直接通过赋值等简单操作进行数据结构转换,而需要一种二者兼容的数据结构来进行数据的存储与传递。

ctypes中重载了Python的乘号运算符(本质上是重载了ctypes数据结构基类的__mul__方法,使其所有的子类数据结构均适用),使得乘号变成了定义任意长度数组类的方法。具体的语法非常简单,将一个基本的ctypes数据类型乘上一个整数,即可得到一个新的类,这个类就是基于此数据类型的某个长度的数组类。例:

c_int * 10    #相当于C中的int [10]

c_double * 5  #相当于C中的double [5]

注意,通过将基本类型乘上一个数的方式得到的只是一个新的类,而不是这个类的实例,要得到一个真正的实例对象,就需要实例化这个类。实例化时,类可接受不超过声明长度的,任意数量的参数,作为数组的初始值。没有接受到参数的部分会自动初始化为0、None或其他布尔值为False的量,具体情况视数据类型而定,如果声明的是整形或浮点型数组,那么就会初始化为0,而如果声明的是字符串相关的量,那么就会初始化为None。本节只讨论数字类型的数组,字符串相关的数组将在下文进行讨论。例:

5个0的整形数组:

(c_int * 5)()

前三个数为1-3,后续全为0的10长度浮点型数组:

(c_double * 10)(1, 2, 3)

对于Python而言,数字类型的数组是一个可迭代对象,其可通过for循环、next方法等方式进行迭代,以获取其中的每一个值。例:

for i in (c_double * 10)(1, 2, 3):

   print(i)

输出结果为1.0、2.0、3.0以及后续的7个0.0。

而对于C接口而言,这样的实例对象就等同于在C中创建的数组指针,可直接作为实参传入并修改其中的值。通过这样的兼容数据类型,即可实现Python与C之间的数据传递。

另外,数组对象在数据类型上可看作是指针,且指针变量在ctypes中就等同于int类型,故所有涉及到指针传递的地方,均无需考虑修改argtypes属性的问题。直接以默认的int类型传入即可。下文中也将多次使用到这一性质。例:

C部分:

extern "C"

{

   void intList(int numList[]);

}

void intList(int numList[])

{

   for (int i = 0; i < 10; i++)

       numList[i] = i;

}

这段代码定义了一个intList函数,作用为传入一个整形数组指针(为简单起见,此数组假定为10个整形长度),而后将数组中的值依次赋值为0-9。

Python部分:

dllObj = CDLL('1.dll')

numList = (c_int * 10)()

dllObj.intList(numList)

for i in numList:

   print(i)

这段代码首先定义了一个数组实例numList,长度为10个int,而后将这个实例像传入数组指针一样直接传入dll文件中的intList函数,调用完成后遍历此数组,依次输出每个值。结果即为在C源码中定义的0-9。

由此可见,通过ctypes定义的数组数据类型是一种同时兼容Python与C的数据结构,对于Python而言,这种数据结构是一个可迭代对象,可通过for循环进行遍历求值,而对于C而言,这样的数组结构就等同于在C中声明的数组指针。故可以将Python中定义的数组传入C中进行运算,最后在Python中读取此数组的运算结果,从而实现了数组类型的数据交换。

9  高级ctypes数据类型——高维数组

高维数组与一维数组在语法上大体类似,但在字符串数组上略有不同。本节首先讨论数字类型的高维数组。此外,为简单起见,本节全部内容均对二维数组进行讨论,更高维度的数组在语法上与二维数组是相似的,这里不再赘述。

高维数组类可简单的通过在一维数组类外部再乘上一个数字实现:

(c_int * 4) * 3

这样即得到了一个4 * 3的二维int类型数组的类。

二维数组类的实例化与初始化可看作是多个一维数组初始化的叠加,可通过一个二维tuple实现,且如果不给出任何初始化值(即一对空括号),则其中所有元素将被初始化为0。例:

((c_int * 4) * 3)()

这样就得到了一个所有值均为0的二维数组对象。又例:

((c_int * 4) * 3)((1, 2, 3, 4), (5, 6))

上述代码只实例化了第一个一维数组的全部以及第二个一维数组的前两个值,而其他所有值均为0。

二维数组在使用时与一维数组一致,其可直接作为指针参数传入C的函数接口进行访问,在C语言内部其等同于C语言中声明的二维数组。而对于Python,这样的数组对象可通过双层的for循环去迭代获取每个数值。

10  高级ctypes数据类型——字符串数组

字符串数组在ctypes中的行为更接近于C语言中的字符串数组,其需要采用二维数组的形式来实现,而不是Python中的一维数组。首先,需要通过c_char类型乘上一个数,得到一个字符串类型,而后将此类型再乘上一个数,就能得到可以包含多个字符串的字符串数组。例:

((c_char * 10) * 3)()

上例即实例化了一个3字符串数组,每个字符串最大长度为10。

对于C语言而言,上述的字符串数组实例可直接当做字符串指针传入C函数,其行为等同于在C中声明的char (*)[10]指针。下详细讨论Python中对此对象的处理。

首先,字符串数组也是可迭代对象,可通过for循环迭代取值,对于上例的对象,其for循环得到的每一个值,都是一个10个长度的字符串对象。这样的字符串对象有两个重要属性:value和raw。value属性得到是普通字符串,即忽略了字符串终止符号(即C中的\0)以后的所有内容的字符串,而raw字符串得到的是当前对象的全部字符集合,包括终止符号。也就是说,对于10个长度的字符串对象,其raw的结果就一定是一个10个长度的字符串。例:

for i in ((c_char * 10) * 3)():

   print(i.value)

   print(i.raw)

上述代码中,i.value的输出全为空字符串(b''),而对于i.raw,其输出则为b'\x00\x00...',总共十个\x00。也就是说,value会忽略字符串终止符号后的所有字符,是最常用的取值方式,而raw得到不忽略终止字符的字符串。

接下来讨论ctypes中对字符串对象的赋值方法。由于ctypes的字符串对象通过某个固定长度的字符串类实例化得到,故在赋值时,这样的字符串对象只可以接受等同于其声明长度的字符串对象作为替代值,这是普通Python字符串做不到的。要得到这样的定长字符串,需要用到ctypes的create_string_buffer函数。

create_string_buffer函数用于创建固定长度的带缓冲字符串。其接受两个参数,第一参数为字符串,第二参数为目标长度,返回值即为被创建的定长度字符串对象,可以赋值给字符串数组中的某个对象。注意,create_string_buffer函数必须接受字节字符串作为其第一参数,在Python2中,普通的字符串就是字节字符串,而在Python3中,所有的字符串默认为Unicode字符串,故可以通过字符串的encode、decode方法进行编码方式的转化。encode方法可将Python3的str转为bytes,其中的encoding参数默认就是UTF-8,故无需给出任何参数即可调用。同理,bytes可通过decode方法,以默认参数将bytes转化为Python3的str,对于Python2而言,无需考虑此问题。例:

charList = ((c_char * 10) * 3)()

strList = ['aaa', 'bbb', 'ccc']

for i in range(3):

   charList[i] = create_string_buffer(strList[i].encode(), 10)

for i in charList:

   print(i.value)

上述代码的核心在于,通过create_string_buffer函数创建了一个10长度的带缓冲字符串,其第二参数10用作指定长度,而其第一参数为一个通过encode方法转化成的bytes字符串,这样得到的对象即可赋值给一个10长度的字符串对象。注意,通过create_string_buffer函数创建的字符串对象,其长度必须严格等同于被赋值的字符串对象的声明长度,即如果声明的是10长度字符串,那么create_string_buffer的第二参数就必须也是10,否则代码将抛出TypeError异常,提示出现了类型不一致。

在字符串数组的初始化过程中,这样的字符串对象也可作为初始化的参数。例:

strList = ['aaa', 'bbb', 'ccc']

charList = ((c_char * 10) *3)(*[create_string_buffer(i.encode(), 10) for i in strList])

for i in charList:

   print(i.value.decode())

上述代码将实例化与初始化合并,通过列表推导式得到了3个10长度的缓冲字符串,并使用星号展开,作为实例化的参数。则这样得到的charList效果等同于上例中通过依次赋值得到的字符串数组对象。最后通过for循环输出字符串对象的value属性(一个bytes字符串),且通过decode方法将bytes转化为str。

11  高级ctypes数据类型——指针

上文已经讨论了Python中的数组指针,而根据指针在Python中的定义,其本质上就是一个int类型的值,所以在传参时无需考虑修改argtypes属性的问题。本节主要讨论单个数字类型的指针。

首先,对于单个字符串,其不需要通过指针指针转换即可当做指针传递。例:

void printStr(char *str)

{

   printf("%s", str);

}

则Python中:

dllObj = CDLL('1.dll')

dllObj.printStr('Hello!')

由此可见,对于单个字符串传进dll,则直接通过字符串传递即可,传递过程中字符串会自动被转化为指针。而对于返回单个字符串的C函数,上文已经讨论过,通过修改restype属性为c_char_p后,即可在Python中直接接收字符串返回值。

对于单个数值的指针,则需要通过byref或者pointer函数取得。首先考虑如下函数:

void swapNum(int *a, int *b)

{

   int temp = *a;

   *a = *b;

   *b = temp;

}

此函数接收两个int类型指针,并在函数内部交换指针所在位置的整形值。此时如果通过Python传入这两个指针参数,就需要使用到byref或者pointer函数。byref函数类似于C语言中的取地址符号&,其直接返回当前参数的地址,而pointer函数更为高级,其返回一个POINTER指针类型,一般来说,如果不需要对指针进行其他额外处理,推荐直接调用byref函数获取指针,这是较pointer更加快速的方法。此外,这两个函数的参数都必须是ctypes类型值,可通过ctypes类型实例化得到,不可直接使用Python内部的数值类型。例:

dllObj = CDLL('1.dll')

a, b = c_int(1), c_int(2)

dllObj.swapNum(byref(a), byref(b))

以上代码首先通过c_int类型实例化得到了两个c_int实例,其值分别为1和2。然后调用上文中的swapNum函数,传入的实际参数为byref函数的返回指针。这样就相当于在C语言中进行形如swapNum(&a, &b)的调用。

要将c_int类型再转回Python类型,可以访问实例对象的value属性:

print(a.value, b.value)

value属性得到的就是Python的数字类型,而经过上述的传递指针的函数调用,此时的输出应为2 1。

对于pointer函数,其同样接受一个ctypes实例作为参数,并返回一个POINTER指针类型,而不是简单的一个指针。POINTER指针类型在传参时也可直接作为指针传入C语言函数,但在Python中,其需要先访问contents属性,得到指针指向的数据,其一般为ctypes类型的实例,然后再访问value属性,得到实例所对应的Python类型的数据。例:

a, b = pointer(c_int(1)), pointer(c_int(2))

print(a.contents.value, b.contents.value)

dllObj.swapNum(a, b)

print(a.contents.value, b.contents.value)

以上代码通过pointer函数创建了两个指针类型变量,并将这两个指针作为参数传入函数进行调用。由此可见,通过pointer函数创建的指针类型可直接当做指针使用。但在将指针转换为Python数据类型时,需要先访问contents属性,得到指针指向的值,由于此值是ctypes类型,故还需要继续访问value属性,得到Python的数值类型。

12  高级ctypes数据类型——结构体

结构体在ctypes中通过类进行定义。用于定义结构体的类需要继承自ctypes的Structure基类,而后通过定义类的_fields_属性来定义结构体的构成。_fields_属性一般定义为一个二维的tuple,而对于其中的每一个一维tuple,其需要定义两个值,第一个值为一个字符串,用作结构体内部的变量名,第二个值为一个ctypes类型,用于定义当前结构体变量所定义的数据类型。注意,在Python中定义的结构体,其变量名,类名等均可以不同于C语言中的变量名,但结构体变量的数量、数据类型与顺序必须严格对应于C源码中的定义,否则可能将导致内存访问出错。例:

class TestStruct(Structure):

   _fields_ = (

       ('x', c_int),

       ('y', c_double),

    )

以上代码即定义了一个结构体类型,其等同于C中的struct声明。此结构体定义了两个结构体变量:x对应于一个int类型,y对应于一个double类型。

结构体类型可以通过实例化得到一个结构对象,在实例化的同时也可传入初始化参数,作为结构变量的值。在得到结构对象后,也可通过点号访问结构体成员变量,从而对其赋值。例:

testStruct = TestStruct(1, 2)

print(testStruct.x, testStruct.y)

testStruct.x, testStruct.y = 10, 20

print(testStruct.x, testStruct.y)

上述代码通过实例化TestStruct类,并为其提供初始化参数,得到了一个结构体实例,第一次输出结果即为1 2.0。而后,再通过属性访问的方式修改了结构体中的两个变量,则第二次输出结果为10 20.0。

上面定义的结构体可直接传入C代码中,且上文已经提到,两边定义的结构体变量的各种名称均可不同,但数据类型、数量与顺序必须一致。例:

struct TestStruct

{

   int a;

   double b;

}

extern "C"

{

   void printStruct(TestStruct testStruct);

}

void printStruct(TestStruct testStruct)

{

   printf("%d %f\n", testStruct.a, testStruct.b);

}

Python部分:

dllObj = CDLL('1.dll')

class TestStruct(Structure):

   _fields_ = (

       ('x', c_int),

       ('y', c_double),

    )

testStruct = TestStruct(1, 2)

dllObj.printStruct(testStruct)

由此可见,在Python中实例化得到的结构体实例,可以直接当做C中的结构体实参传入。

结构体也可以指针的方式传入,通过上节介绍的byref或者pointer函数即可实现转化。同样的,这两个函数都可直接接受结构体实例作为参数进行转化,byref返回简单指针,而pointer返回指针对象,可访问其contents属性得到指针所指向的值。例:

C部分,上述printStruct函数修改为接受结构体指针的版本:

void printStruct(TestStruct *testStruct)

{

   printf("%d %f\n", testStruct -> a, testStruct -> b);

}

Python部分:

testStruct = TestStruct(1, 2)

dllObj.printStruct(byref(testStruct))

上述代码将结构体对象testStruct作为byref的参数,从而将其转换为指针传入printStruct函数中。又例:

testStruct = pointer(TestStruct(1, 2))

dllObj.printStruct(testStruct)

print(testStruct.contents.x,testStruct.contents.y)

上述代码通过结构体对象生成了一个指针类型,并将此指针传入函数,可达到同样的效果。且在Python内部,结构体指针类型可以访问其contents属性,得到指针所指向的结构体,然后可继续访问结构体的x与y属性,得到结构体中保存的值。

你可能感兴趣的:(Python ctypes模块使用方法与心得体会)