That is a quite complicated question! There are some aspects that need to be considered.
The speed:
Let's start with a simple int-buffer (I've skipped the unnecessary &c_buffer[0]-business):
%%cython
cdef class CMyClass:
cdef int c_buffer[1000]
def get_array(self):
return <int[:1000]>self.c_buffer
def get_memoryview(self):
return memoryview(<int[:1000]>self.c_buffer)
"Typed memory view" is somewhat opaque in Cython, there are some classes which are very similar and are returned from the function depending on the signature of the function:
However, none of these above is the memoryview you are returning in your second-function: it returns Python's memoryview.
Quite confusing! Personally, I keep it simple and trust Cython to return the best suitable class - to me it just a buffer.
When we measure the speed, the first version will be faster, because wrapping array_obj into a Python's memoryview just adds complexity:
>>>c=CMyClass()
>>>%timeit c.get_array()
377 ns ± 1.69 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
>>>%timeit c.get_memoryview()
561 ns ± 2.31 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
The lifetime:
The memory from c_buffer isn't copied:
>>>c=CMyClass()
>>>c.get_array()[0]=42
>>>print(c.get_memoryview()[0])
Which sounds like a Good Thing, but isn't! The problem: c_buffer isn't a Python-object and when it goes out of scope, the data-pointer of the memory-view becomes dangling:
>>c=CMyClass()
>>>c.get_array()[0]=42
>>>c=c.get_array() # old c-object is now destroyed
>>>print(c[0]) # anything can happen!
-304120624
I got lucky, the python didn't crash but it could, because after binding c to the memoryview, the underlying object is destroyed and the memory freed.
Using std::vector will not help you there. What you need is a real Python-object with reference counting! For example we could use Cython's array for that:
%%cython
from cython.view cimport array as cvarray
cdef class CMyClass:
cdef int[:] c_buffer
def __cinit__(self):
self.c_buffer = cvarray(shape=(1000,), itemsize=sizeof(int), format="i")
def get_array(self):
cdef int[:] res=self.c_buffer # nobody needs to know which class we use
return res
Now the code from above is safe:
>>c=CMyClass()
>>>c.get_array()[0]=42
>>>c=c.get_array() # old c-object is now destroyed
>>>print(c[0]) # but the underlying memory is still alive
42
Custom structs:
But what about customs structs, as in your example above? The probably easiest way is to use numpy:
%%cython -a
import numpy as np
cimport numpy as np
#define a type for memory view
ctypedef packed struct my_struct_t:
np.float32_t x
np.float32_t y
#define a type for numpy-array (is a python-object)
my_struct = np.dtype([
('x', np.float32, 1),
('y', np.float32, 1),
])
cdef class CMyClass:
cdef object c_buffer
def __cinit__(self):
self.c_buffer = np.empty(1000,dtype=my_struct)
def get_array(self):
cdef my_struct_t[:] res=self.c_buffer
return res
Which works as advertised:
>>>c=CMyClass()
>>>c.get_array()[0]={'x':42,'y':42}
>>>c=c.get_array() # old c-object is now destroyed
>>>print(c[0]) # but this is still ok
{'x': 42.0, 'y': 42.0}
Two more remarks:
using numpy is slower - get_array() is three times slower than the original get_array() version
using my_struct_t c_buffer would not really help you there (beside being dangerous), because there would be no rule how to translate the data from c-struct to an python object, but this check happens at run time, when the elements of the array are acccessed.