Implementing the buffer protocol¶
Note
This page uses two different syntax variants:
Cython specific
cdef
syntax, which was designed to make type declarationsconcise and easily readable from a C/C++ perspective.Pure Python syntax which allows static Cython type declarations inpure Python code,followingPEP-484 type hintsandPEP 526 variable annotations.
To make use of C data types in Python syntax, you need to import the special
cython
module in the Python module that you want to compile, e.g.importcython
If you use the pure Python syntax we strongly recommend you use a recentCython 3 release, since significant improvements have been made herecompared to the 0.29.x releases.
Cython objects can expose memory buffers to Python codeby implementing the “buffer protocol”.This chapter shows how to implement the protocoland make use of the memory managed by an extension type from NumPy.
A matrix class¶
The following Cython/C++ code implements a matrix of floats,where the number of columns is fixed at construction timebut rows can be added dynamically.
# distutils: language = c++fromcython.cimports.libcpp.vectorimportvector@cython.cclassclassMatrix:ncols:cython.uintv:vector[cython.float]def__cinit__(self,ncols:cython.uint):self.ncols=ncolsdefadd_row(self):"""Adds a row, initially zero-filled."""self.v.resize(self.v.size()+self.ncols)
# distutils: language = c++fromlibcpp.vectorcimportvectorcdefclassMatrix:cdefunsignedncolscdefvector[float]vdef__cinit__(self,unsignedncols):self.ncols=ncolsdefadd_row(self):"""Adds a row, initially zero-filled."""self.v.resize(self.v.size()+self.ncols)
There are no methods to do anything productive with the matrices’ contents.We could implement custom__getitem__
,__setitem__
, etc. for this,but instead we’ll use the buffer protocol to expose the matrix’s data to Pythonso we can use NumPy to do useful work.
Implementing the buffer protocol requires adding two methods,__getbuffer__
and__releasebuffer__
,which Cython handles specially.
# distutils: language = c++fromcython.cimports.cpythonimportPy_bufferfromcython.cimports.libcpp.vectorimportvector@cython.cclassclassMatrix:ncols:cython.Py_ssize_tshape:cython.Py_ssize_t[2]strides:cython.Py_ssize_t[2]v:vector[cython.float]def__cinit__(self,ncols:cython.Py_ssize_t):self.ncols=ncolsdefadd_row(self):"""Adds a row, initially zero-filled."""self.v.resize(self.v.size()+self.ncols)def__getbuffer__(self,buffer:cython.pointer[Py_buffer],flags:cython.int):itemsize:cython.Py_ssize_t=cython.sizeof(self.v[0])self.shape[0]=self.v.size()//self.ncolsself.shape[1]=self.ncols# Stride 1 is the distance, in bytes, between two items in a row;# this is the distance between two adjacent items in the vector.# Stride 0 is the distance between the first elements of adjacent rows.self.strides[1]=cython.cast(cython.Py_ssize_t,(cython.cast(cython.p_char,cython.address(self.v[1]))-cython.cast(cython.p_char,cython.address(self.v[0]))))self.strides[0]=self.ncols*self.strides[1]buffer.buf=cython.cast(cython.p_char,cython.address(self.v[0]))buffer.format='f'# floatbuffer.internal=cython.NULL# see Referencesbuffer.itemsize=itemsizebuffer.len=self.v.size()*itemsize# product(shape) * itemsizebuffer.ndim=2buffer.obj=selfbuffer.readonly=0buffer.shape=self.shapebuffer.strides=self.stridesbuffer.suboffsets=cython.NULL# for pointer arrays onlydef__releasebuffer__(self,buffer:cython.pointer[Py_buffer]):pass
# distutils: language = c++fromcpythoncimportPy_bufferfromlibcpp.vectorcimportvectorcdefclassMatrix:cdefPy_ssize_tncolscdefPy_ssize_t[2]shapecdefPy_ssize_t[2]stridescdefvector[float]vdef__cinit__(self,Py_ssize_tncols):self.ncols=ncolsdefadd_row(self):"""Adds a row, initially zero-filled."""self.v.resize(self.v.size()+self.ncols)def__getbuffer__(self,Py_buffer*buffer,intflags):cdefPy_ssize_titemsize=sizeof(self.v[0])self.shape[0]=self.v.size()//self.ncolsself.shape[1]=self.ncols# Stride 1 is the distance, in bytes, between two items in a row;# this is the distance between two adjacent items in the vector.# Stride 0 is the distance between the first elements of adjacent rows.self.strides[1]=<Py_ssize_t>(<char*>&(self.v[1])-<char*>&(self.v[0]))self.strides[0]=self.ncols*self.strides[1]buffer.buf=<char*>&(self.v[0])buffer.format='f'# floatbuffer.internal=NULL# see Referencesbuffer.itemsize=itemsizebuffer.len=self.v.size()*itemsize# product(shape) * itemsizebuffer.ndim=2buffer.obj=selfbuffer.readonly=0buffer.shape=self.shapebuffer.strides=self.stridesbuffer.suboffsets=NULL# for pointer arrays onlydef__releasebuffer__(self,Py_buffer*buffer):pass
The methodMatrix.__getbuffer__
fills a descriptor structure,called aPy_buffer
, that is defined by the Python C-API.It contains a pointer to the actual buffer in memory,as well as metadata about the shape of the array and the strides(step sizes to get from one element or row to the next).Itsshape
andstrides
members are pointersthat must point to arrays of type and sizePy_ssize_t[ndim].These arrays have to stay alive as long as any buffer views the data,so we store them on theMatrix
object as members.
The code is not yet complete, but we can already compile itand test the basic functionality.
>>>frommatriximportMatrix>>>importnumpyasnp>>>m=Matrix(10)>>>np.asarray(m)array([],shape=(0,10),dtype=float32)>>>m.add_row()>>>a=np.asarray(m)>>>a[:]=1>>>m.add_row()>>>a=np.asarray(m)>>>aarray([[1.,1.,1.,1.,1.,1.,1.,1.,1.,1.],[0.,0.,0.,0.,0.,0.,0.,0.,0.,0.]],dtype=float32)
Now we can view theMatrix
as a NumPyndarray
,and modify its contents using standard NumPy operations.
Memory safety and reference counting¶
TheMatrix
class as implemented so far is unsafe.Theadd_row
operation can move the underlying buffer,which invalidates any NumPy (or other) view on the data.If you try to access values after anadd_row
call,you’ll get outdated values or a segfault.
This is where__releasebuffer__
comes in.We can add a reference count to each matrix,and lock it for mutation whenever a view exists.
# distutils: language = c++fromcython.cimports.cpythonimportPy_bufferfromcython.cimports.libcpp.vectorimportvector@cython.cclassclassMatrix:view_count:cython.intncols:cython.Py_ssize_tv:vector[cython.float]# ...def__cinit__(self,ncols:cython.Py_ssize_t):self.ncols=ncolsself.view_count=0defadd_row(self):ifself.view_count>0:raiseValueError("can't add row while being viewed")self.v.resize(self.v.size()+self.ncols)def__getbuffer__(self,buffer:cython.pointer[Py_buffer],flags:cython.int):# ... as beforeself.view_count+=1def__releasebuffer__(self,buffer:cython.pointer[Py_buffer]):self.view_count-=1
# distutils: language = c++fromcpythoncimportPy_bufferfromlibcpp.vectorcimportvectorcdefclassMatrix:cdefintview_countcdefPy_ssize_tncolscdefvector[float]v# ...def__cinit__(self,Py_ssize_tncols):self.ncols=ncolsself.view_count=0defadd_row(self):ifself.view_count>0:raiseValueError("can't add row while being viewed")self.v.resize(self.v.size()+self.ncols)def__getbuffer__(self,Py_buffer*buffer,intflags):# ... as beforeself.view_count+=1def__releasebuffer__(self,Py_buffer*buffer):self.view_count-=1
Flags¶
We skipped some input validation in the code.Theflags
argument to__getbuffer__
comes fromnp.asarray
(and other clients) and is an OR of boolean flagsthat describe the kind of array that is requested.Strictly speaking, if the flags containPyBUF_ND
,PyBUF_SIMPLE
,orPyBUF_F_CONTIGUOUS
,__getbuffer__
must raise aBufferError
.These macros can becimport
’d fromcpython.buffer
.
(The matrix-in-vector structure actually conforms toPyBUF_ND
,but that would prohibit__getbuffer__
from filling in the strides.A single-row matrix is F-contiguous, but a larger matrix is not.)
References¶
The buffer interface used here is set out inPEP 3118, Revising the buffer protocol.
A tutorial for using this API from C is on Jake Vanderplas’s blog,An Introduction to the Python Buffer Protocol.
Reference documentation is available forPython 3andPython 2.The Py2 documentation also describes an older buffer protocolthat is no longer in use;since Python 2.6, thePEP 3118 protocol has been implemented,and the older protocol is only relevant for legacy code.