清晰的 ctypes 类结构方式

30

我定义了一个ctypes类和一个相关的便捷函数,代码如下:

class BNG_FFITuple(Structure):
    _fields_ = [("a", c_uint32),
                ("b", c_uint32)]


class BNG_FFIArray(Structure):
    _fields_ = [("data", c_void_p),
                ("len", c_size_t)]

    # Allow implicit conversions from a sequence of 32-bit unsigned ints
    @classmethod
    def from_param(cls, seq):
        return seq if isinstance(seq, cls) else cls(seq)

    def __init__(self, seq, data_type = c_float):
        array_type = data_type * len(seq)
        raw_seq = array_type(*seq)
        self.data = cast(raw_seq, c_void_p)
        self.len = len(seq)


def bng_void_array_to_tuple_list(array, _func, _args):
    res = cast(array.data, POINTER(BNG_FFITuple * array.len))[0]
    return res

convert = lib.convert_to_bng
convert.argtypes = (BNG_FFIArray, BNG_FFIArray)
convert.restype = BNG_FFIArray
convert.errcheck = bng_void_array_to_tuple_list
drop_array = lib.drop_array 
drop_array.argtypes = (POINTER(BNG_FFIArray),)

然后我定义了一个简单的便利函数:

def f(a, b):
    return [(i.a, i.b) for i in iter(convert(a, b))]

大部分工作都没有问题,但我有两个问题:

  • 它不够灵活;我想使用 c_float 来实例化 BNG_FFITuple 而不是 c_uint32(因此字段为 c_float),反之亦然,所以 BNG_FFIArraydata_typec_uint32。但我不清楚如何做到这一点。
  • 我想通过将 POINTER(BNG_FFIArray) 返回到我的 dylib(参见 drop_array - 我已经在我的 dylib 中定义了一个函数)来释放现在由 Python 拥有的内存,但我不确定应该在什么时候调用它。

是否有一种更加整洁、更符合 Python 风格并更加安全的封装方式呢?我担心如果内存清理没有以健壮的方式定义(在 __exit__ 上?__del__ 上?),任何出现问题都会导致未被释放的内存。


5
你是否需要将BNG_FFITuple作为FFI参数,还是仅在Python中使用?如果仅在Python中使用,建议使用collections.namedtuple。只需为intfloat转换定义单独的errcheck函数即可。你可以在BNG_FFIArray.__del__中释放数组,但是使用指向lib.drop_array的类引用BNG_FFIArray._drop_array,以避免模块撤销在对象的__del__终结器被调用之前将lib设置为None而出现问题。 - Eryk Sun
3
我不确定我理解了,我的dylib函数需要一个带有"data"和"len"字段的结构体,类型必须适当,但它不一定要被称为特定的名称。 - urschrei
4
在函数bng_void_array_to_tuple_list中,你将结果转换为BNG_FFITuple数组。你是否曾经将BNG_FFITuple传回给库?如果没有,那么使用ctypes结构体没有任何意义,可以将结果转换为普通的Python tuplenamedtuple。一旦转换为BNG_FFIArray,它是数组的唯一引用,因此可以使用其__del__终结器调用drop_array - Eryk Sun
3
好的,没问题。不,这是一次单向旅行;在bng_void_array_to_tuple_list之后不会再使用它。 - urschrei
1
你的库是否受限于分配和返回内存,或者你能否计算返回数组的大小并从Python传递一个指向它的指针(以便Python拥有所有内存)? - Patrick Maupin
显示剩余7条评论
2个回答

3

由于您对 Rust 有一定的控制权,因此最干净的做法是在调用之前从 Python 中预分配结果数组,并将所有内容传递给单个结构。

下面的代码假定进行了这种修改,但同时指定了如果无法进行此操作,则需要进行解除分配的位置。

请注意,如果您进行此类封装,则不需要指定库函数的参数和结果处理等信息,因为您只会从一个地方调用实际函数,并且始终具有完全相同类型的参数。

我不了解 Rust(甚至我的 C 程序也有点生疏),但下面的代码假定您重新定义 Rust 来匹配类似于以下内容的等效内容:

typedef struct FFIParams {
    int32 source_ints;
    int32 len;
    void * a;
    void * b;
    void * result;
} FFIParams;

void convert_to_bng(FFIParams *p) {
}

这里是Python代码。最后需要注意的是,由于参数结构的重用,这段代码不支持多线程操作。如果需要进行多线程操作,则需要进行相应的修改。

from ctypes import c_uint32, c_float, c_size_t, c_void_p
from ctypes import Structure, POINTER, pointer, cast
from itertools import izip, islice

_test_standalone = __name__ == '__main__'

if _test_standalone:
    class lib(object):
        @staticmethod
        def convert_to_bng(ptr_params):
            params = ptr_params.contents
            source_ints = params.source_ints
            types = c_uint32, c_float
            if not source_ints:
                types = reversed(types)
            length = params.len
            src_type, dst_type = types
            src_type = POINTER(length * src_type)
            dst_type = POINTER(length * 2 * dst_type)
            a = cast(params.a, src_type).contents
            b = cast(params.b, src_type).contents
            result = cast(params.result, dst_type).contents

            # Assumes we are converting int to float or back...
            func = float if source_ints else int
            result[0::2] = map(func, a)
            result[1::2] = map(func, b)

class _BNG_FFIParams(Structure):
    _fields_ = [("source_ints", c_uint32),
                ("len", c_size_t),
                ("a", c_void_p),
                ("b", c_void_p),
                ("result", c_void_p)]

class _BNG_FFI(object):

    int_type = c_uint32
    float_type = c_float
    _array_type = type(10 * int_type)

    # This assumes we want the result to be opposite type.
    # Maybe I misunderstood this -- easily fixable if so.
    _result_type = {int_type: float_type, float_type: int_type}

    def __init__(self):
        my_params = _BNG_FFIParams()
        self._params = my_params
        self._pointer = POINTER(_BNG_FFIParams)(my_params)
        self._converter = lib.convert_to_bng


    def _getarray(self, seq, data_type):
        # Optimization for pre-allocated correct array type
        if type(type(seq)) == self._array_type and seq._type_ is data_type:
            print("Optimized!")
            return seq
        return (data_type * len(seq))(*seq)

    def __call__(self, a, b, data_type=float_type):
        length = len(a)
        if length != len(b):
            raise ValueError("Input lengths must be same")

        a, b = (self._getarray(x, data_type) for x in (a, b))

        # This has the salutary side-effect of insuring we were
        # passed a valid type
        result = (length * 2 * self._result_type[data_type])()

        params = self._params
        params.source_ints = data_type is self.int_type
        params.len = length
        params.a = cast(pointer(a), c_void_p)
        params.b = cast(pointer(b), c_void_p)
        params.result = cast(pointer(result), c_void_p)
        self._converter(self._pointer)

        evens = islice(result, 0, None, 2)
        odds = islice(result, 1, None, 2)
        result = list(izip(evens, odds))

        # If you have to have the converter allocate memory,
        # deallocate it here...

        return result

convert = _BNG_FFI()

if _test_standalone:
    print(convert([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], c_float))
    print(convert([1, 2, 3], [4, 5, 6], c_uint32))
    print(convert([1, 2, 3], (c_uint32 * 3)(4, 5, 6), c_uint32))

3
这是一个修改后的代码版本,在被调用的DLL中分配返回数组。由于使用纯Python测试会更难,并且我不懂rust,所以我为实际测试构建了一个简陋的C库:
#include <stdlib.h>
#include <stdio.h>

typedef struct FFIParams {
    int source_ints;
    int len;
    void * a;
    void * b;
} FFIParams, *FFIParamsPtr;

typedef int * intptr;
typedef float * floatptr;

void * to_float(FFIParamsPtr p) {
    floatptr result;
    intptr a = p->a;
    intptr b = p->b;
    int i;
    int size = sizeof(result[0]) * 2 * p->len;
    result = malloc(size);
    printf("Allocated %x bytes at %x\n", size, (unsigned int)result);
    for (i = 0; i < p->len; i++) {
        result[i*2+0] = (float)(a[i]);
        result[i*2+1] = (float)(b[i]);
    }
    return result;
}

void * to_int(FFIParamsPtr p) {
    intptr result;
    floatptr a = p->a;
    floatptr b = p->b;
    int i;
    int size = sizeof(result[0]) * 2 * p->len;
    result = malloc(size);
    printf("Allocated %x bytes at %x\n", size, (unsigned int)result);
    for (i = 0; i < p->len; i++) {
        result[i*2+0] = (int)(a[i]);
        result[i*2+1] = (int)(b[i]);
    }
    return result;
}

void * convert_to_bng(FFIParamsPtr p) {
    if (p->source_ints)
        return to_float(p);
    return to_int(p);
}

void free_bng_mem(void * data) {
    printf("Deallocating memory at %x\n", (unsigned int)data);
    free(data);
}

这里是调用它的Python代码:

from ctypes import c_uint32, c_float, c_size_t, c_void_p
from ctypes import Structure, POINTER, pointer, cast, cdll
from itertools import izip, islice


class _BNG_FFIParams(Structure):
    _fields_ = [("source_ints", c_uint32),
                ("len", c_size_t),
                ("a", c_void_p),
                ("b", c_void_p)]

class _BNG_FFI(object):

    int_type = c_uint32
    float_type = c_float
    _array_type = type(10 * int_type)
    _lib = cdll.LoadLibrary('./testlib.so')
    _converter = _lib.convert_to_bng
    _converter.restype = c_void_p
    _deallocate = _lib.free_bng_mem

    _result_type = {int_type: float_type,
                    float_type: int_type}

    def __init__(self):
        my_params = _BNG_FFIParams()
        self._params = my_params
        self._pointer = POINTER(_BNG_FFIParams)(my_params)


    def _getarray(self, seq, data_type):
        # Optimization for pre-allocated correct array type
        if type(type(seq)) == self._array_type and seq._type_ is data_type:
            print("Optimized!")
            return seq
        return (data_type * len(seq))(*seq)

    def __call__(self, a, b, data_type=float_type):
        length = len(a)
        if length != len(b):
            raise ValueError("Input lengths must be same")

        a, b = (self._getarray(x, data_type) for x in (a, b))

        # This has the salutary side-effect of insuring we were
        # passed a valid type
        result_type = POINTER(length * 2 * self._result_type[data_type])

        params = self._params
        params.source_ints = data_type is self.int_type
        params.len = length
        params.a = cast(pointer(a), c_void_p)
        params.b = cast(pointer(b), c_void_p)

        resptr = self._converter(self._pointer)
        result = cast(resptr, result_type).contents

        evens = islice(result, 0, None, 2)
        odds = islice(result, 1, None, 2)
        result = list(izip(evens, odds))

        self._deallocate(resptr)

        return result

convert = _BNG_FFI()

if __name__ == '__main__':
    print(convert([1.0, 2.0, 3.0], [4.0, 5.0, 6.0], c_float))
    print(convert([1, 2, 3], [4, 5, 6], c_uint32))
    print(convert([1, 2, 3], (c_uint32 * 3)(4, 5, 6), c_uint32))

当我执行它时,这是结果:

Allocated 18 bytes at 9088468
Deallocating memory at 9088468
[(1L, 4L), (2L, 5L), (3L, 6L)]
Allocated 18 bytes at 908a6b8
Deallocating memory at 908a6b8
[(1.0, 4.0), (2.0, 5.0), (3.0, 6.0)]
Optimized!
Allocated 18 bytes at 90e1ae0
Deallocating memory at 90e1ae0
[(1.0, 4.0), (2.0, 5.0), (3.0, 6.0)]

这是一台32位的Ubuntu 14.04系统。我使用了Python 2.7,并使用gcc --shared ffitest.c -o testlib.so -Wall来构建库。


这个例子足够简单,你不需要一个结构体--你可以直接传递4个参数。但是你的原始问题有一个结构体,我决定将其保留作为更复杂情况的示例。如果你想传递多个参数,就这样做--ctypes并不要求你在每个位置定义有效的类型,它只是允许这样做,当你将c函数暴露给更高级别的代码时,这真的很有用,但当你包装它并且只从一个地方调用它时,这并不是很有用。 - Patrick Maupin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接