ctypes 是 Python 内置的一个库,用于调用本地编译库中导出的函数。

注意:由于该库处理的是编译后的代码,因此它相对依赖于操作系统。

Section 189.1: ctypes 数组

正如任何一位优秀的 C 程序员所知,单个值并不能走得太远。真正能推动我们前进的是数组!

>>> c_int * 16
<class '__main__.c_long_Array_16'>

这并不是一个真正的数组,但它已经非常接近了!我们创建了一个表示包含 16 个 int 的数组的类。

现在我们只需要初始化它:

>>> arr = (c_int * 16)(*range(16))
>>> arr
<__main__.c_long_Array_16 object at 0xbaddcafe>

现在,arr 是一个真正的数组,包含从 0 到 15 的数字。

它们可以像任何列表一样被访问:

>>> arr[5]
5
>>> arr[5] = 20
>>> arr[5]
20

就像任何其他 ctypes 对象一样,它也有大小和位置:

>>> sizeof(arr)
64 # sizeof(c_int) * 16
>>> hex(addressof(arr))
'0xc000l0ff'

Section 189.2: 为 ctypes 包装函数

在某些情况下,C 函数接受一个函数指针。作为 ctypes 的热心用户,我们希望能够使用这些函数,甚至将 Python 函数作为参数传递。

让我们定义一个函数:

>>> def max(x, y):
return x if x >= y else y

现在,这个函数接受两个参数并返回相同类型的结果。为了示例的需要,我们假设这种类型是 int。

就像我们在数组示例中所做的那样,我们可以定义一个表示该原型的对象:

>>> CFUNCTYPE(c_int, c_int, c_int)
<CFunctionType object at 0xdeadbeef>

这个原型表示一个返回 c_int(第一个参数)的函数,并接受两个 c_int 参数(其他参数)。

现在我们来包装这个函数:

>>> CFUNCTYPE(c_int, c_int, c_int)(max)

<CFunctionType object at 0xdeadbeef>

函数原型还有另一个用途:它们可以包装 ctypes 函数(如 libc.ntohl),并在调用函数时验证是否使用了正确的参数。

>>> libc.ntohl() # garbage in - garbage out
>>> CFUNCTYPE(c_int, c_int)(libc.ntohl)()
Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
TypeError: this function takes at least 1 argument (0 given)

Section 189.3: 基本用法

假设我们想使用 libc 的 ntohl 函数。

首先,我们必须加载 libc.so:

>>> from ctypes import *
>>> libc = cdll.LoadLibrary('libc.so.6')
>>> libc
<CDLL 'libc.so.6', handle baadf00d at 0xdeadbeef>

然后,我们获取函数对象:

>>> ntohl = libc.ntohl
>>> ntohl
<_FuncPtr object at 0xbaadf00d>

现在,我们可以直接调用这个函数:

>>> ntohl(0x6C)
1811939328
>>> hex(_)
'0x6c000000'

这正是我们期望的结果。

Section 189.4: 常见陷阱

加载文件失败

第一个可能的错误是加载库失败。在这种情况下,通常会引发 OSError。

这要么是因为文件不存在(或者操作系统找不到它):

>>> cdll.LoadLibrary("foobar.so")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.5/ctypes/__init__.py", line 425, in LoadLibrary
    return self._dlltype(name)
File "/usr/lib/python3.5/ctypes/__init__.py", line 347, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: foobar.so: cannot open shared object file: No such file or directory

如你所见,错误信息清晰且具有很强的指示性。

第二个原因是文件被找到了,但格式不正确。

>>> cdll.LoadLibrary("libc.so")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.5/ctypes/__init__.py", line 425, in LoadLibrary
    return self._dlltype(name)
File "/usr/lib/python3.5/ctypes/__init__.py", line 347, in __init__
    self._handle = _dlopen(self._name, mode)
OSError: /usr/lib/i386-linux-gnu/libc.so: invalid ELF header

在这种情况下,文件是一个脚本文件而不是 .so 文件。这在尝试在 Linux 机器上打开 .dll 文件或在 32 位 Python 解释器上打开 64 位文件时也可能会发生。如你所见,在这种情况下错误信息稍显模糊,需要进行一些调查。

访问函数失败

假设我们成功加载了 .so 文件,然后我们需要像第一个示例中那样访问我们的函数。

当使用一个不存在的函数时,会引发 AttributeError:

>>> libc.foo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/usr/lib/python3.5/ctypes/__init__.py", line 360, in __getattr__
    func = self.__getitem__(name)
File "/usr/lib/python3.5/ctypes/__init__.py", line 365, in __getitem__
    func = self._FuncPtr((name_or_ordinal, self))
AttributeError: /lib/i386-linux-gnu/libc.so.6: undefined symbol: foo

Section 189.5: 基本的 ctypes 对象

最基础的对象是一个整数:

>>> obj = ctypes.c_int(12)
>>> obj
c_long(12)

现在,obj 指向一块包含值 12 的内存区域。

可以直接访问该值,甚至可以修改它:

>>> obj.value
12
>>> obj.value = 13
>>> obj
c_long(13)

由于 obj 指向一块内存,我们还可以找出它的大小和位置:

>>> sizeof(obj)
4
>>> hex(addressof(obj))
'0xdeadbeef'

Section 189.6: 复杂用法

让我们将上面的所有示例合并成一个复杂的场景:使用 libc 的 lfind 函数。

关于该函数的更多细节,请查阅手册页。我强烈建议你在继续阅读之前先阅读它。

首先,我们定义正确的原型:

>>> compar_proto = CFUNCTYPE(c_int, POINTER(c_int), POINTER(c_int))
>>> lfind_proto = CFUNCTYPE(c_void_p, c_void_p, c_void_p, POINTER(c_uint), c_uint, compar_proto)

然后,我们创建变量:

>>> key = c_int(12)
>>> arr = (c_int * 16)(*range(16))
>>> nmemb = c_uint(16)

现在我们定义比较函数:

>>> def compar(x, y):
  return x.contents.value - y.contents.value

注意,x 和 y 是 POINTER(c_int),因此我们需要对它们进行解引用并获取它们的值,以便实际比较存储在内存中的值。

现在我们可以将所有内容组合在一起:

>>> lfind = lfind_proto(libc.lfind)
>>> ptr = lfind(byref(key), byref(arr), byref(nmemb), sizeof(c_int), compar_proto(compar))

ptr 是返回的空指针。如果 key 没有在 arr 中找到,值将是 None,但在这种情况下我们得到了一个有效的值。

现在我们可以进行转换并访问该值:

>>> cast(ptr, POINTER(c_int)).contents
c_long(12)

此外,我们还可以看到 ptr 指向 arr 中的正确值:

>>> addressof(arr) + 12 * sizeof(c_int) == ptr
True