Чистый способ структурирования класса ctypes
Я определил класс 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))]
Большая часть работы работает отлично, но у меня есть две проблемы:
- Это недостаточно гибко; Я бы хотел создать экземпляр
BNG_FFITuple
с помощью c_float
вместо c_uint32
(поэтому поля c_float
) и наоборот, поэтому BNG_FFIArray
data_type
- c_uint32
. Однако я не знаю, как это сделать.
- Я хочу освободить память, которая теперь принадлежит Python, отправив
POINTER(BNG_FFIArray)
обратно в мой dylib (см. drop_array
- я уже определил функцию в своем dylib), но я не уверен, в какой момент я должен это назвать.
Есть ли способ инкапсулировать все это более аккуратным, более путинским способом, что также безопаснее? Я обеспокоен тем, что без очистки памяти определяется надежным способом (на __exit__
? __del__
?) Что все, что пойдет не так, приведет к свободной памяти
Ответы
Ответ 1
Поскольку у вас есть некоторый контроль над ржавчиной стороной, самая чистая вещь - предварительно выделить массив результатов из Python перед вызовом и передать все в одной структуре.
В приведенном ниже коде предполагается эта модификация, но также указывается место, где вы делаете освобождение, если вы не можете этого сделать.
Обратите внимание, что если вы делаете такую инкапсуляцию, вам не нужно указывать такие параметры, как параметры и обработку результатов для функции библиотеки, потому что вы вызываете фактическую функцию только из одного места и всегда с точно те же самые параметры.
Я не знаю ржавчины (и даже мой C немного ржавый), но приведенный ниже код предполагает, что вы переопределяете свою ржавчину, чтобы соответствовать эквиваленту чего-то вроде этого:
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))
Ответ 2
Вот модифицированная версия кода, который выделяет возвращаемый массив в вызываемой DLL. Поскольку это было бы труднее протестировать с чистым Python, и, поскольку я не знаю ржавчины, я построил сырую библиотеку 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