龙空技术网

Python程序员实用技巧:利用ctypes来访问C代码

程序员爱学习 75

前言:

目前朋友们对“python ctypes模块”都比较珍视,我们都想要了解一些“python ctypes模块”的相关内容。那么小编同时在网络上网罗了一些对于“python ctypes模块””的相关内容,希望同学们能喜欢,看官们一起来了解一下吧!

利用ctypes来访问C代码1 问题

我们有一些C函数已经被编译为共享库或者DLL了。我们想从纯Python代码中直接调用这些函数,而不必额外编写C代码或者使用第三方的扩展工具。

2 解决方案

对于用C语言编写的小程序,使用Python标准库中的ctypes模块来访问通常是非常容易的。要使用ctypes,必须首先确保想要访问的C代码已经被编译为与Python解释器相兼容(即,采用同样的体系结构、字长、编译器等)的共享库了。对于本小节来说,假设已经创建了共享库libsample.so,其中包含了本章介绍中所示的那些代码。我们进一步假设文件libsample.so与接下来展示的sample.py放置在同一个目录中了。

要访问这个共享库,需要创建一个Python模块来包装它,示例如下:

# sample.pyimport ctypesimport os# Try to locate the .so file in the same directory as this file_file = 'libsample.so'_path = os.path.join(*(os.path.split(__file__)[:-1] + (_file,)))_mod = ctypes.cdll.LoadLibrary(_path)# int gcd(int, int)gcd = _mod.gcdgcd.argtypes = (ctypes.c_int, ctypes.c_int)gcd.restype = ctypes.c_int# int in_mandel(double, double, int)in_mandel = _mod.in_mandelin_mandel.argtypes = (ctypes.c_double, ctypes.c_double, ctypes.c_int)in_mandel.restype = ctypes.c_int# int divide(int, int, int *)_divide = _mod.divide_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))_divide.restype = ctypes.c_intdef divide(x, y): rem = ctypes.c_int() quot = _divide(x, y, rem) return quot,rem.value# void avg(double *, int n)# Define a special type for the 'double *' argumentclass DoubleArrayType: def from_param(self, param): typename = type(param).__name__ if hasattr(self, 'from_' + typename): return getattr(self, 'from_' + typename)(param) elif isinstance(param, ctypes.Array): return param else: raise TypeError("Can't convert %s" % typename) # Cast from array.array objects def from_array(self, param): if param.typecode != 'd': raise TypeError('must be an array of doubles') ptr, _ = param.buffer_info() return ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double)) # Cast from lists/tuples def from_list(self, param): val = ((ctypes.c_double)*len(param))(*param) return val from_tuple = from_list # Cast from a numpy array def from_ndarray(self, param): return param.ctypes.data_as(ctypes.POINTER(ctypes.c_double))DoubleArray = DoubleArrayType()_avg = _mod.avg_avg.argtypes = (DoubleArray, ctypes.c_int)_avg.restype = ctypes.c_doubledef avg(values): return _avg(values, len(values))# struct Point { }class Point(ctypes.Structure): _fields_ = [('x', ctypes.c_double), ('y', ctypes.c_double)]# double distance(Point *, Point *)distance = _mod.distancedistance.argtypes = (ctypes.POINTER(Point), ctypes.POINTER(Point))distance.restype = ctypes.c_double

如果一切顺利,现在应该可以加载这个模块并使用相应的C函数了。例如:

>>> import sample>>> sample.gcd(35,42)7>>> sample.in_mandel(0,0,500)1>>> sample.in_mandel(2.0,1.0,500)0>>> sample.divide(42,8)(5, 2)>>> sample.avg([1,2,3])2.0>>> p1 = sample.Point(1,2)>>> p2 = sample.Point(4,5)>>> sample.distance(p1,p2)4.242640687119285>>>
3 讨论

本节中有几个地方需要进行讨论。第一个问题是关于将C和Python代码打包在一起。如果要用ctypes来访问自己编译的C代码,得确保把共享库放在sample.py模块可以找得到的地方。一种可能是将得到的.so文件与所支撑的Python代码放在同一个目录中。这正是本节给出的解决方案中首先完成的——sample.py查询__file__变量,看看自己被安装到何处,然后在同样的目录下构建一个路径指向libsample.so文件。

如果要把C库安装到别处,那么必须相应地调整路径。如果C库已经作为标准库安装到你的机器上了,那么可以使用ctypes.util.find_library()函数。示例如下:

>>> from ctypes.util import find_library>>> find_library('m')'/usr/lib/libm.dylib'>>> find_library('pthread')'/usr/lib/libpthread.dylib'>>> find_library('sample')'/usr/local/lib/libsample.so'>>>

再次申明,如果ctypes无法找到C库则不能继续工作。因此,需要花几分钟时间考虑一下该如何安装库。

一旦知道了C库的位置,可以使用ctypes.cdll.LoadLibrary()来加载。在解决方案中,_path是指向共享库的完整路径,而下列语句则用来加载C库:

_mod = ctypes.cdll.LoadLibrary(_path)

一旦成功加载了C库,我们需要编写代码来提取特定的符号并为其附上类型签名。这正是由如下代码完成的:

# int in_mandel(double, double, int)in_mandel = _mod.in_mandelin_mandel.argtypes = (ctypes.c_double, ctypes.c_double, ctypes.c_int)in_mandel.restype = ctypes.c_int

在这份代码中,.argtypes属性是一个元组,其中包含了函数的输入参数,而.restype表示返回类型。ctypes中定义了许多类型对象(例如c_double、c_int、c_short、c_float等),它们用来代表常见的C数据类型。如果想要Python传递正确的参数类型并对数据做正确的转换,那么给值附上类型签名就是至关重要的了(如果不这么做,不仅代码不会正常工作,而且还会使得整个解释器进程崩溃)。

使用ctypes时,一个多少有些棘手的地方在于原始的C代码中可能会用到一些惯用法,而它们在概念上不能清晰地映射到Python中。divide()函数就是个很好的例子,因为它是通过其中一个参数来返回值的。尽管这在C中是非常常见的技术,但放在Python中往往就不清楚应该如何工作了。例如,我们不能直接像这样做:

>>> divide = _mod.divide>>> divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))>>> x = 0>>> divide(10, 3, x)Traceback (most recent call last): File "<stdin>", line 1, in <module>ctypes.ArgumentError: argument 3: <class 'TypeError'>: expected LP_c_intinstance instead of int>>>

就算这么做行的通,也会违反Python中整数是不可变对象的事实,可能会导致整个解释器进程卡死在黑洞中。对于涉及指针的参数,通常必须构建一个兼容的ctypes对象,然后像下面这样传入:

>>> x = ctypes.c_int()>>> divide(10, 3, x)3>>> x.value1>>>

这里我们创建了一个ctypes.c_int对象,并把它作为指针对象传递给函数。与普通的Python整数不同,c_int对象是可变的。可以根据需要通过.value属性来获取或修改值。

对于那些C调用约定(calling convention)属于非Pythonic(Pythonic是俚语,表示按照Python的方式来优雅的工作)的情况,通常都需要编写一个小型的包装函数来处理。在解决方案中,这个包装函数使得divide()函数用一个元组来返回两个结果值:

# int divide(int, int, int *)_divide = _mod.divide_divide.argtypes = (ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int))_divide.restype = ctypes.c_intdef divide(x, y): rem = ctypes.c_int() quot = _divide(x,y,rem) return quot, rem.value

avg()函数则带来了全新的挑战。底层的C代码期望接收一个指针以及长度来代表一个数组。但是从Python的角度来看,我们必须考虑下列问题:什么是数组?它是列表还是元组?亦或是array模块中的array对象?是numpy中的数组吗?还是以上都有可能呢?在实践中,一个Python“数组”可能有着许多不同的形式,而且也许我们会想支持这多种可能。

类DoubleArrayType展示了如何处理这种情况。在这个类中我们定义了方法from_param()。这个方法的任务就是接受一个单独的参数并将其范围缩小为一个兼容的ctypes对象(在本例中就是指向ctypes.c_double的指针)。在from_param()中,我们可以自由地做任何想做的事。在我们的解决方案中,参数的类型名被提取出来并发送给更加具体的方法。例如,如果传递的是列表,则类型名就是list,调用的就是from_list()方法。

对于列表和元组,from_list()方法会执行转换到ctypes数组对象的操作。这看起来有点古怪,但下面是将列表转换为ctypes数组的交互式例子:

>>> nums = [1, 2, 3]>>> a = (ctypes.c_double * len(nums))(*nums)>>> a<__main__.c_double_Array_3 object at 0x10069cd40>>>> a[0]1.0>>> a[1]2.0>>> a[2]3.0>>>

对于array对象,from_array()方法会提取底层的内存指针并将其转换为一个ctypes指针对象。示例如下:

>>> import array>>> a = array.array('d',[1,2,3])>>> aarray('d', [1.0, 2.0, 3.0])>>> ptr_ = a.buffer_info()>>> ptr4298687200>>> ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))<__main__.LP_c_double object at 0x10069cd40>>>>

from_ndarray()则对numpy数组做了处理。

通过定义DoubleArrayType类并在avg()的类型签名中使用,可以看到,函数可接受多种不同形式的数组输入:

>>> import sample>>> sample.avg([1,2,3])2.0>>> sample.avg((1,2,3))2.0>>> import array>>> sample.avg(array.array('d',[1,2,3]))2.0>>> import numpy>>> sample.avg(numpy.array([1.0,2.0,3.0]))2.0>>>

本节的最后部分是展示如何同简单的C结构体打交道。对于结构体来说,只用定义一个类,并在其中包含适当的字段和类型,示例如下:

class Point(ctypes.Structure): _fields_ = [('x', ctypes.c_double), ('y', ctypes.c_double)]

一旦定义完成,就可以在类型签名中使用它,也可以在需要实例化结构体对象的代码中使用。示例如下:

>>> p1 = sample.Point(1,2)>>> p2 = sample.Point(4,5)>>> p1.x1.0>>> p1.y2.0>>> sample.distance(p1,p2)4.242640687119285>>>

最后再多说几句:如果所有你想做的只是在Python中访问几个C函数,那么ctypes是很有用的库。但是,如果打算访问一个庞大的C库,就应该看看其他的方法,比如Swig(或者Cython。

大型库的主要问题在于由于ctypes并不是全自动化处理的,我们将不得不花费大量时间来编写所有的类型签名,就像示例中的那样。根据库的复杂程度,我们可能也不得不编写大量的小型包装函数和支撑类(类似于DoubleArrayType)。此外,除非完全理解了所有C接口的底层细节,包括内存管理和错误处理,否则很容易会让Python由于段错误、非法访问或其他类似的错误而产生灾难性的崩溃。

作为ctypes之外的选择,读者可以去看看CFFI()。CFFI提供了很多相同的功能,但使用的是C的语法,而且支持更多高级的C代码。在写作本节时,相对来说CFFI依然是一个很新的项目,但对它的使用已经得到了极大的增长。甚至有一些关于在今后的Python版本中将其纳入到Python标准库中的讨论。因此,CFFI绝对是值得去留意的项目。

标签: #python ctypes模块