加载动态链接库实战#

#include <math.h>

// 辗转相除法(Euclidean algorithm)来计算最大公约数
int gcd(int x, int y) {
    int g = y;
    while (x > 0) {
        g = x;
        x = y % x;
        y = g;
    }
    return g;
}

int divide(int a, int b, int *remainder) {
    int quot = a / b;
    *remainder = a % b;
    return quot;
}

double avg(double *a, int n) {
    int i;
    double total = 0.0;
    for (i = 0; i< n ; i++) {
        total += a[i];
    }
    return total / n;
}

typedef struct Point {
    double x, y;
} Point;

double distance(Point *p1, Point *p2) {
    return hypot(p1->x - p2->x, p1->y - p2->y);
}

导出动态库:

!gcc -shared -o libtest.so test.c
import ctypes
_mod = ctypes.cdll.LoadLibrary("./libtest.so")

成功加载 C 库后,需要编写代码提取特定的符号并为其附上类型签名。

# int dcd(int, int)
gcd = _mod.gcd
type(gcd)
ctypes.CDLL.__init__.<locals>._FuncPtr
gcd(2, 4)
2
gcd(2, 4.5)
---------------------------------------------------------------------------
ArgumentError                             Traceback (most recent call last)
Cell In[7], line 1
----> 1 gcd(2, 4.5)

ArgumentError: argument 2: TypeError: Don't know how to convert parameter 2

ctypes .restype & .argtypes#

.argtypes 指定函数输入参数类型,.restype 表示返回类型。

备注

给值附上参数类型至关重要,如果没有这样做,不仅代码可能不会正常运行,而且还会使得整个解释器进程崩溃。

# int dcd(int, int)
gcd = _mod.gcd
gcd.argtypes = ctypes.c_int, ctypes.c_int
gcd.restype = ctypes.c_int
gcd(2, 4)
2
gcd(2, 4.5)
---------------------------------------------------------------------------
ArgumentError                             Traceback (most recent call last)
Cell In[10], line 1
----> 1 gcd(2, 4.5)

ArgumentError: argument 2: TypeError: 'float' object cannot be interpreted as an integer

ctypes 指针参数#

divide = _mod.divide
divide.argtypes = ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_int)
x = 0
divide(10, 3, x)
---------------------------------------------------------------------------
ArgumentError                             Traceback (most recent call last)
Cell In[13], line 2
      1 x = 0
----> 2 divide(10, 3, x)

ArgumentError: argument 3: TypeError: expected LP_c_int instance instead of int

对于涉及指针的参数,必须构建兼容的 ctypes 对象:

x = ctypes.c_int()
divide(10, 3, x)
3
x, x.value
(c_int(1), 1)

可以通过 .value 修改指针数据。

x.value = 4
x
c_int(4)

对于 C 调用约定(calling convention)属于非 Pythonic 的情况,通常需要编写小型包装函数来处理它。

# 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_int

def divide(x, y):
    rem = ctypes.c_int()
    quot = _divide(x, y, rem)
    return quot, rem.value
divide(7, 4)
(1, 3)

ctypes 使用自定义的数据类型调用函数#

您也可以通过自定义 ctypes 参数转换方式来允许将你自己的类实例作为函数参数。ctypes 会寻找 _as_parameter_ 属性并使用它作为函数参数。属性必须是整数、字符串、字节串、ctypes 实例或者带有 _as_parameter_ 属性的对象:

class Bottles:
    def __init__(self, number):
        self._as_parameter_ = number

libc = ctypes.CDLL("libc.so.6")
bottles = Bottles(42)
libc.printf(b"%d bottles of beer\n", bottles)
19

如果你不想将实例数据存储在 _as_parameter_ 实例变量中,可以定义一个根据请求提供属性的 property

ctypes 自定义函数参数类型#

如果你定义了自己的类并将其传递给函数调用,则你必须为它们实现 from_param() 类方法才能够在 argtypes 序列中使用它们。from_param() 类方法将接受传递给函数调用的 Python 对象,它应该进行类型检查或者其他必要的操作以确保这个对象是可接受的,然后返回对象本身、它的 _as_parameter_ 属性或在此情况下作为 C 函数参数传入的任何东西。 同样,结果应该是整数、字符串、字节串、ctypes 实例或具有 _as_parameter_ 属性的对。

from_param()(obj) 会将 obj 适配为一个 ctypes 类型。 当该类型出现在外部函数的 argtypes 元组中时它将会被调用并传入在该外部函数中使用的实际对象;它必须返回一个可被用作函数调用参数的对象。

所有 ctypes 数据类型都带有 from_param()(obj) 类方法的默认实现,它通常会返回 obj,如果该对象是此类型的实例的话。

当调用外部函数时,每个实际参数都会被传给 argtypes 元组中条目的 from_param() 类方法,该方法允许将实际参数适配为此外部函数所接受的对象。例如,argtypes 元组中的 c_char_p 条目将使用 ctypes 转换规则把作为参数传入的字符串转换为字节串对象。

新特性:现在可以在 argtypes 中放入非 ctypes 类型的条目,但每个条目必须具有 from_param() 方法用于返回一个可作为参数的值(整数、字符串、ctypes 实例)。这样就允许定义可将将自定义对象适配为函数参数的适配器。

可以将 list 转换为 ctypes 数组:

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

对于 array,from_

import array

a = array.array("d", [1, 2, 3])
a
array('d', [1.0, 2.0, 3.0])

提取底层的内存指针:

ptr, _ = a.buffer_info()
ptr
140638110228912

转换为 ctypes 指针:

ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))
<__main__.LP_c_double at 0x7fe8dccae0d0>
_avg = _mod.avg
_avg([3, 4], 2)
---------------------------------------------------------------------------
ArgumentError                             Traceback (most recent call last)
Cell In[26], line 2
      1 _avg = _mod.avg
----> 2 _avg([3, 4], 2)

ArgumentError: argument 1: TypeError: Don't know how to convert parameter 1

为了支持 C 的数组,自定义如下结构:

class DoubleArrayType:
    def from_param(self, param):
        typename = type(param).__name__
        if hasattr(self, f'from_{typename}'):
            return getattr(self, f'from_{typename}')(param)
        elif isinstance(param, ctypes.Array):
            return param
        else:
            raise TypeError(f"不支持{typename}转换")
        
    def from_array(self, param):
        """转换 array.array 对象"""
        if param.typecode != "d":
            raise TypeError(f"必须为 double 类型数组")
        ptr, _ = param.buffer_info()
        return ctypes.cast(ptr, ctypes.POINTER(ctypes.c_double))
    
    def from_list(self, param):
        """转换 list 对象"""
        val = (ctypes.c_double * len(param))(*param)
        return val
    
    # 转换 tuple 对象
    from_tuple = from_list

    def from_ndarray(self, param):
        """转换 numpy array 对象"""
        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_double

def avg(values):
    return _avg(values, len(values))
avg([1, 2, 3])
2.0
import array
avg(array.array("d", [1, 2, 6]))
3.0
import numpy as np

avg(np.array([1.0, 2.0, 3.0]))
2.0

ctypes 结构体和联合#

结构体和联合必须派生自 StructureUnion 基类,这两个基类是在 ctypes 模块中定义的。 每个子类都必须定义 _fields_ 属性。_fields_ 必须是一个 2元组 的列表,其中包含 字段名称 和 字段类型。

type 字段必须是 ctypes 类型,比如 c_int,或者其他 ctypes 类型: Union, Array, POINTER()

下面是简单的 Point 结构体,它包含名称为 xy 的两个变量:

class Point(ctypes.Structure):
    _fields_ = [("x", ctypes.c_double),
                ("y", ctypes.c_double),]
p1 = Point(1, 2)
p2 = Point(4, 5)
Point(1, 2, 3)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[34], line 1
----> 1 Point(1, 2, 3)

TypeError: too many initializers

下面可以直接调用 C 函数:

# double distance(Point *, Point *)
distance = _mod.distance
distance.argtypes = ctypes.POINTER(Point), ctypes.POINTER(Point)
distance.restype = ctypes.c_double

distance(p1, p2)
4.242640687119285

ctypes 结构体和联合中的位域#

可以创建包含位字段的结构体和联合。 位字段只适用于整数字段,位宽度是由 _fields_ 元组中的第三项来指定的:

class Int(ctypes.Structure):
    _fields_ = [("first_16", ctypes.c_int, 16),
                ("second_16", ctypes.c_int, 16)]

print(Int.first_16)
print(Int.second_16)
<Field type=c_int, ofs=0:0, bits=16>
<Field type=c_int, ofs=0:16, bits=16>

Array#

数组是一个序列,包含指定个数元素,且必须类型相同。

创建数组类型的推荐方式是使用一个类型乘以一个正数:

TenPointsArrayType = Point * 10
TenPointsArrayType
__main__.Point_Array_10

可以组合:

class MyStruct(ctypes.Structure):
    _fields_ = [("a", ctypes.c_int),
                ("b", ctypes.c_float),
                ("point_array", Point * 4)]

print(len(MyStruct().point_array))
4

指定正确类型的数据来初始化:

TenIntegers = ctypes.c_int * 10
ii = TenIntegers(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
print(ii)

for i in ii: print(i, end=" ")
<__main__.c_int_Array_10 object at 0x7fe8dc1dc450>
1 2 3 4 5 6 7 8 9 10

ctypes.pointer()#

可以将 ctypes 类型数据传入 ctypes.pointer() 函数创建指针:

i = ctypes.c_int(42)
pi = ctypes.pointer(i)
pi
<__main__.LP_c_int at 0x7fe8dc1dc150>

指针实例拥有 contents 属性,它返回指针指向的真实对象,如上面的 i 对象:

pi.contents
c_int(42)

注意 ctypes 并没有 OOR (返回原始对象), 每次访问这个属性时都会构造返回新的相同对象:

pi.contents is i
False
pi.contents is pi.contents
False

将这个指针的 contents 属性赋值为另一个 c_int 实例将会导致该指针指向该实例的内存地址:

i = ctypes.c_int(99)
pi.contents = i
pi.contents
c_int(99)

指针对象也可以通过整数下标进行访问:

pi[0]
99

通过整数下标赋值可以改变指针所指向的真实内容:

print(i)

pi[0] = 22
print(i)
c_int(99)
c_int(22)

使用 0 以外的索引也是合法的,但是你必须确保知道自己为什么这么做,就像 C 语言中: 你可以访问或者修改任意内存内容。通常只会在函数接收指针是才会使用这种特性,而且你 知道 这个指针指向的是一个数组而不是单个值。

内部细节, pointer() 函数不只是创建了一个指针实例,还创建了一个指针 类型 。这是通过调用 POINTER() 函数实现的,它接收 ctypes 类型为参数,返回一个新的类型:

PI = ctypes.POINTER(ctypes.c_int)
PI
__main__.LP_c_int
PI(42)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[48], line 1
----> 1 PI(42)

TypeError: expected c_int instead of int
PI(ctypes.c_int(42))
<__main__.LP_c_int at 0x7fe8dc1dd950>

无参调用指针类型可以创建 NULL 指针。NULL 指针的布尔值是 False

null_ptr = ctypes.POINTER(ctypes.c_int)()
print(bool(null_ptr))
False
type(null_ptr)
__main__.LP_c_int