python - c_int()
Forma limpia de estructurar clase de ctypes. (2)
Aquí hay una versión modificada del código que asigna la matriz de retorno en la DLL llamada. Dado que eso sería más difícil de probar con Python puro, y como no conozco el óxido, construí una biblioteca de C para la prueba real:
#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);
}
Aquí está el código de Python que lo llama:
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))
Y aquí está el resultado cuando lo ejecuté:
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)]
Esto pasa a ser un sistema Ubuntu 14.04 de 32 bits. Utilicé Python 2.7 y construí la biblioteca con gcc --shared ffitest.c -o testlib.so -Wall
He definido una clase de ctypes
y una función de conveniencia asociada así:
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),)
Entonces defino una función de conveniencia simple:
def f(a, b):
return [(i.a, i.b) for i in iter(convert(a, b))]
La mayoría de esto funciona perfectamente, pero tengo dos problemas:
- No es lo suficientemente flexible; Me gustaría poder instanciar un
BNG_FFITuple
usandoc_float
lugar dec_uint32
(por lo que los campos sonc_float
), y viceversa, por lo que el tipo deBNG_FFIArray
data_type
esc_uint32
. Aunque no tengo claro cómo hacer esto. - Me gustaría liberar la memoria que ahora es propiedad de Python, enviando un
POINTER(BNG_FFIArray)
nuevo a mi dylib (veadrop_array
- ya definí una función en mi dylib), pero no estoy seguro de en qué Punto que debería llamarlo.
¿Hay una manera de encapsular todo esto de una manera más limpia, más pitón, que también es más segura? Me preocupa que, sin que la limpieza de la memoria se defina de manera robusta (en __exit__
? __del__
?), Cualquier cosa que no funcione correctamente llevará a una memoria no liberada.
Ya que tienes algo de control sobre el lado del óxido, lo más limpio sería realizar una asignación previa de la matriz de resultados de Python antes de la llamada, y pasar todo en una sola estructura.
El código siguiente asume esta modificación, pero también designa el lugar donde haría la desasignación si no puede hacerlo.
Tenga en cuenta que si realiza este tipo de encapsulación, NO necesita especificar cosas como los parámetros y el procesamiento de resultados para la función de biblioteca, ya que solo está llamando a la función real desde un solo lugar, y siempre con exactamente el mismo tipo de parámetros
No sé óxido (e incluso mi C está un poco oxidada), pero el código que aparece a continuación asume que redefine su óxido para que coincida con el equivalente de algo como esto:
typedef struct FFIParams {
int32 source_ints;
int32 len;
void * a;
void * b;
void * result;
} FFIParams;
void convert_to_bng(FFIParams *p) {
}
Aquí está el pitón. Una nota final: esto no es seguro para subprocesos, debido a la reutilización de la estructura del parámetro. Eso es bastante fácil de arreglar si es necesario.
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))