I'm trying to return an array of structs in Cython.
// .pyx
from libc.stdint cimport uint8_t
cdef extern from "<apriltag.h>":
cdef struct apriltag_detection:
int id
double c[2]
double p[4][2]
ctypedef apriltag_detection apriltag_detection_t
cdef extern from "tag36h11_detector/tag36h11_detector.h":
apriltag_detection_t* scan_frame(int width, int height, uint8_t* data);
cdef class Detection:
# how do I "link" this to the struct defined above?
def __cinit__(self):
pass
def __dealloc__(self):
pass
def detect(width, height, frame):
return scan_frame(width, height, frame)
Ideally, I would like to call the detect
function in Python code, and get a list of Detection
objects where Detection
is a wrapper class for the C struct apriltag_detection
which is typedef'd to apriltag_detection_t
.
I'm getting the following compilation error:
tag36h11_detector.pyx:22:21: Cannot convert 'apriltag_detection_t *' to Python object
I can't find a reference to returning a pointer to a struct, or an array of structs, anywhere in the documentation.
UPDATE 3
// .h
typedef struct detection_payload {
int size;
apriltag_detection_t** detections;
} detection_payload_t;
I'm trying to convert the above struct into a Python object that holds size
and a Python list that holds apriltag_detection_t
objects.
// .pyx
cdef extern from "<apriltag.h>":
cdef struct apriltag_detection:
int id
double c[2]
double p[4][2]
ctypedef apriltag_detection apriltag_detection_t
cdef extern from "tag36h11_detector/tag36h11_detector.h":
cdef struct detection_payload:
int size
apriltag_detection_t** detections
ctypedef detection_payload detection_payload_t
detection_payload* scan_frame(int width, int height, uint8_t* data)
...
cdef class Detection:
cdef apriltag_detection* _d
def __cinit__(self):
self._d = NULL
cdef _setup(self, apriltag_detection* d):
self._d = d
def __dealloc__(self):
self._d = NULL
property id:
def __get__(self):
return self._d.id
property c:
def __get__(self):
return self._d.c
property p:
def __get__(self):
return self._d.p
cdef Detection_create(apriltag_detection_t* d):
return Detection()._setup(d)
cdef class DetectionPayload:
cdef detection_payload* _p
def __cinit__(self):
self._p = NULL
cdef _setup(self, detection_payload* p):
self._p = p
self.size = p.size
self.detections = []
for i in range(0, self.size):
apriltag_detection_t* detection = self._p.detections[i]
d = Detection_create(detection)
self.detections+=[d]
def __dealloc__(self):
_p = NULL
property size:
def __get__(self):
return self.size
property detections:
def __get__(self):
return self.detections
I'm getting several syntax errors on the line:
apriltag_detection_t* detection = self._p.detections[I]
Specifically, on the pointer apriltag_detection_t*
UPDATE 2
This compiles and imports fine now. Still no progress on arrays
from libc.stdint cimport uint8_t
cdef extern from "<apriltag.h>":
cdef struct apriltag_detection:
int id
double c[2]
double p[4][2]
ctypedef apriltag_detection apriltag_detection_t
cdef extern from "tag36h11_detector/tag36h11_detector.h":
apriltag_detection_t* scan_frame(int width, int height, uint8_t* data);
cdef class Detection:
cdef apriltag_detection* _d
def __cinit__(self):
self._d = NULL
cdef _setup(self, apriltag_detection* d):
self._d = d
def __dealloc__(self):
self._d = NULL
property id:
def __get__(self):
return self._d.id
property c:
def __get__(self):
return self._d.c
property p:
def __get__(self):
return self._d.p
cdef Detection_create(apriltag_detection_t* d):
return Detection()._setup(d)
def detect(width, height, frame):
cdef apriltag_detection_t* detection = scan_frame(width, height, frame)
return Detection_create(detection)
UPDATE 1
I tried following the post linked below, and this is what I have so far.
from libc.stdint cimport uint8_t
cdef extern from "<apriltag.h>":
cdef struct apriltag_detection:
int id
double c[2]
double p[4][2]
ctypedef apriltag_detection apriltag_detection_t
cdef extern from "tag36h11_detector/tag36h11_detector.h":
apriltag_detection_t* scan_frame(int width, int height, uint8_t* data);
cdef class Detection:
cdef apriltag_detection* _d;
def __cinit__(self):
self._d = NULL
def _setup(self, apriltag_detection* d):
self._d = d
def __dealloc__(self):
self._d = NULL
property id:
def __get__(self):
return self._t.id
property c:
def __get__(self):
return self._t.c
property p:
def __get__(self):
return self._t.p
cdef Detection_create(apriltag_detection_t* d):
return Detection()._setup(d)
def detect(width, height, frame):
return <Detection>scan_frame(width, height, frame)
Although this is closer than I was before, I'm still getting the error:
tag36h11_detector.pyx:33:30: Cannot convert 'apriltag_detection_t *' to Python object
On the line
cdef Detection_create(apriltag_detection_t* d):
return Detection()._setup(d)
Moreover, I have no idea how to return a Python list...
You seem to have solved your problems, but I would like to answer the question nevertheless. First because I would like to try it out and second because I think you have some problems with memory management and I would like to point it out.
We will wrap the following simple C-interface:
//creator.h
typedef struct {
int mult;
int add;
} Result;
typedef struct {
int size;
Result *arr;
} ResultArray;
ResultArray create(int size, int *input){
//whole file at the end of the answer
}
which processes the input-array and return a C-array of structs along with the number of elements in this array.
Our wrapping pyx-file looks then like following:
#result_import.pyx (verion 0)
cdef extern from "creator.h":
ctypedef struct Result:
int mult
int add
ctypedef struct ResultArray:
int size
Result *arr
ResultArray create(int size, int *input)
def create_structs(int[::1] input_vals):
pass
The most notable part: I use a memoryview (int[::1]
) to pass my input array and this has two advantages:
numpy
, array
since Python3) which is more flexible as to use a numpy array[::1]
) that the input is continuousIn the test-script I use numpy, but one could also go with build-in array:
#test.py
import result_import
import numpy as np
a=np.array([1,2,3],dtype='int32')
result=result_import.create_structs(a)
for i,el in enumerate(result):
print i, ": mult:", el.mult, " add:", el.add
Now nothing works yet, but everything is set up.
First scenario: we just wanna have normal python-objects nothing fancy! One possibility would be:
#result_import.pyx (verion 1)
#from cpython cimport array needed for array.array in Python2
from libc.stdlib cimport free
....
class PyResult:
def __init__(self, mult, add):
self.mult=mult
self.add=add
def create_structs(int[::1] input_vals):
cdef ResultArray res=create(len(input_vals), &input_vals[0])
try:
lst=[]
for i in range(res.size):
lst.append(PyResult(res.arr[i].mult, res.arr[i].add))
finally:
free(res.arr)
return lst
I convert the whole data to python-objects and use a simple list. Very simple, but there are two things worth noting:
try...finally
to make sure it happens even if an exception is thrown.NULL
, I have to call free
-function.Now our test.py
works - nice!
Second scenario: however if I need only some of the elements and convert them all - it is just inefficient. Furthermore, I keep all elements twice in the memory (at least for some time) - this is a downside of the naive approach. So I would like to create PyResult
-objects on demand somewhere later in program.
Let's write a wrapper list:
#result_import.pyx (verion 2)
...
cdef class WrappingList:
cdef int size
cdef Result *arr
def __cinit__(self):
self.size=0
self.arr=NULL
def __dealloc__(self):
free(self.arr)
print "deallocated"#just a check
def __getitem__(self, index):
if index<0 or index>=self.size:
raise IndexError("list index out of range")
return PyResult(self.arr[index].mult, self.arr[index].add)
def create_structs(int[::1] input_vals):
cdef ResultArray res=create(len(input_vals), &input_vals[0])
lst=WrappingList()
lst.size, lst.arr=res.size, res.arr
return lst
So the class WrappingList
behaves a lot like a list, keeps the whole C-array without copying and creates PyResult
-objects only when needed. Things worth mentioning:
__dealloc__
is called when the WrapperingList
-object is destroyed - this is the place where we release the memory which was given to us by the C-code. At the end of test.py
we should see "deallocated", otherwise something gone wrong...__getitem__
is used for iteration. Third scenario: Python code should not only read the results but also change them, so the changed data can be passed back to the C-code. For that let's make PyResult
a proxy:
#result_import.pyx (verion 3, last)
...
cdef class PyResult:
cdef Result *ptr #ptr to my element
def __init__(self):
self.ptr=NULL
@property
def mult(self):
return self.ptr.mult
@mult.setter
def mult(self, value):
self.ptr.mult = value
@property
def add(self):
return self.ptr.add
@add.setter
def add(self, value):
self.ptr.add = value
cdef class WrappingList:
...
def __getitem__(self, index):
if index>=self.size:
raise IndexError("list index out of range")
res=PyResult()
res.ptr=&self.arr[index]
return res
Now, the PyResult
-object holds a pointer to the corresponding element and can change it directly in the C-array. However, there are some pitfalls, I should mention:
PyResult
should not live longer than the parent-WrappingList
-object. You can fix it by adding a reference to parent in the PyResult
-class.add
or mult
) is quite costly, because every time a new Python-object must be created, registered and then deleted. Let's change the test-script, to see the proxies in action:
#test.py(second version)
import result_import
import numpy as np
a=np.array([1,2,3],dtype='int32')
result=result_import.create_structs(a)
for i,el in enumerate(result):
print i, ": mult:", el.mult, " add:", el.add
el.mult, el.add=42*i,21*i
# now print changed values:
for i,el in enumerate(result):
print i, ": mult:", el.mult, " add:", el.add
There is still a lot to improve, but I guess enough for one answer:)
Attachments:
Sloppy creator.h
- one needs to check the result of malloc
:
//creator.h
typedef struct {
int mult;
int add;
} Result;
typedef struct {
int size;
Result *arr;
} ResultArray;
ResultArray create(int size, int *input){
ResultArray res;
res.size=size;
res.arr=(Result *)malloc(size*sizeof(Result));//todo: check !=0
for(int i=0;i<size;i++){
res.arr[i].mult=2*input[i];
res.arr[i].add=2+input[i];
}
return res;
}
setup.py:
from distutils.core import setup, Extension
from Cython.Build import cythonize
setup(ext_modules=cythonize(Extension(
name='result_import',
sources = ["result_import.pyx"]
)))
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With