Commit 5ebe3fe5 authored by Stefan Behnel's avatar Stefan Behnel

updated C library tutorial as proposed by Terry Reedy

parent 432b797c
...@@ -2,8 +2,8 @@ Using C libraries ...@@ -2,8 +2,8 @@ Using C libraries
================= =================
Apart from writing fast code, one of the main use cases of Cython is Apart from writing fast code, one of the main use cases of Cython is
to call external C libraries from Python code. As seen for the C to call external C libraries from Python code. As Cython code
string decoding functions above, it is actually trivial to call C compiles down to C code itself, it is actually trivial to call C
functions directly in the code. The following describes what needs to functions directly in the code. The following describes what needs to
be done to use an external C library in Cython code. be done to use an external C library in Cython code.
...@@ -21,6 +21,8 @@ type that can encapsulate all memory management. ...@@ -21,6 +21,8 @@ type that can encapsulate all memory management.
The C API of the queue implementation, which is defined in the header The C API of the queue implementation, which is defined in the header
file ``libcalg/queue.h``, essentially looks like this:: file ``libcalg/queue.h``, essentially looks like this::
/* file: queue.h */
typedef struct _Queue Queue; typedef struct _Queue Queue;
typedef void *QueueValue; typedef void *QueueValue;
...@@ -40,6 +42,8 @@ file ``libcalg/queue.h``, essentially looks like this:: ...@@ -40,6 +42,8 @@ file ``libcalg/queue.h``, essentially looks like this::
To get started, the first step is to redefine the C API in a ``.pxd`` To get started, the first step is to redefine the C API in a ``.pxd``
file, say, ``cqueue.pxd``:: file, say, ``cqueue.pxd``::
# file: cqueue.pxd
cdef extern from "libcalg/queue.h": cdef extern from "libcalg/queue.h":
ctypedef struct Queue: ctypedef struct Queue:
pass pass
...@@ -59,54 +63,64 @@ file, say, ``cqueue.pxd``:: ...@@ -59,54 +63,64 @@ file, say, ``cqueue.pxd``::
bint queue_is_empty(Queue* queue) bint queue_is_empty(Queue* queue)
Note how these declarations are almost identical to the header file Note how these declarations are almost identical to the header file
declarations, so you can often just copy them over. One exception is declarations, so you can often just copy them over. One noteworthy
the last line. The return value of the ``queue_is_empty`` method is difference is the first line. ``Queue`` is in this case used as an
actually a C boolean value, i.e. it is either zero or non-zero, *opaque handle*; only the library that is called knows what is really
indicating if the queue is empty or not. This is best expressed by inside. Since no Cython code needs to know the contents of the
Cython's ``bint`` type, which is a normal ``int`` type when used in C struct, we do not need to declare its contents, so we simply provide
but maps to Python's boolean values ``True`` and ``False`` when an empty definition (as we do not want to declare the ``_Queue`` type
converted to a Python object. Another difference is the first which is referenced in the C header) [#]_.
line. ``Queue`` is in this case used as an *opaque handle*; only the
library that is called know what is actually inside. Since no Cython code
needs to know the contents of the struct, we do not need to declare its contents,
so we simply provide an empty definition (as we do not want to declare
the ``_Queue`` type which is referenced in the C header) [#]_.
.. [#] There's a subtle difference between ``cdef struct Queue: pass`` .. [#] There's a subtle difference between ``cdef struct Queue: pass``
and ``ctypedef struct Queue: pass``. The former declares a type and ``ctypedef struct Queue: pass``. The former declares a
which is referenced in C code as ``struct Queue``, while the type which is referenced in C code as ``struct Queue``, while
latter is referenced in C as ``Queue``. This is a C language the latter is referenced in C as ``Queue``. This is a C
quirk that Cython is not able to hide. Most modern C libraries language quirk that Cython is not able to hide. Most modern C
use the ``ctypedef`` kind of struct. libraries use the ``ctypedef`` kind of struct.
Another exception is the last line. The integer return value of the
``queue_is_empty`` method is actually a C boolean value, i.e. it is
either zero or non-zero, indicating if the queue is empty or not.
This is best expressed by Cython's ``bint`` type, which is a normal
``int`` type when used in C but maps to Python's boolean values
``True`` and ``False`` when converted to a Python object.
Next, we need to design the Queue class that should wrap the C queue. Next, we need to design the Queue class that should wrap the C queue.
It will live in a file called ``queue.pyx``. [#]_
.. [#] Note that the name of the ``.pyx`` file must be different from
the ``cqueue.pxd`` file with declarations from the C library,
as both do not describe the same code. A ``.pxd`` file next to
a ``.pyx`` file with the same name defines exported
declarations for code in the ``.pyx`` file.
Here is a first start for the Queue class:: Here is a first start for the Queue class::
# file: queue.pyx
cimport cqueue cimport cqueue
cimport python_exc
cdef class Queue: cdef class Queue:
cdef cqueue.Queue _c_queue cdef cqueue.Queue _c_queue
def __cinit__(self): def __cinit__(self):
self._c_queue = cqueue.queue_new() self._c_queue = cqueue.queue_new()
Note that it says ``__cinit__`` rather than ``__init__``. While Note that it says ``__cinit__`` rather than ``__init__``. While
``__init__`` is available as well, it is not guaranteed to be run (for ``__init__`` is available as well, it is not guaranteed to be run (for
instance, one could create a subclass and forget to call the ancestor instance, one could create a subclass and forget to call the
constructor). Because not initializing C pointers often leads to ancestor's constructor). Because not initializing C pointers often
crashing the Python interpreter without leaving as much as a stack leads to crashing the Python interpreter without leaving as much as a
trace, Cython provides ``__cinit__`` which is *always* called on stack trace, Cython provides ``__cinit__`` which is *always* called on
construction. However, as ``__cinit__`` is called during object construction. However, as ``__cinit__`` is called during object
construction, ``self`` is not fully construction, ``self`` is not fully constructed yet, and one must
constructed yet, and one must avoid doing anything with ``self`` but avoid doing anything with ``self`` but assigning to ``cdef`` fields.
assigning to ``cdef`` fields.
Note also that the above method takes no parameters, although subtypes Note also that the above method takes no parameters, although subtypes
may want to accept some. Although it is guaranteed to get called, the may want to accept some. Although it is guaranteed to get called, the
no-arguments ``__cinit__()`` method is a special case here as it does no-arguments ``__cinit__()`` method is a special case here as it does
not prevent subclasses from adding parameters as they see fit. If not prevent subclasses from adding parameters as they see fit. If
parameters are added they must match those of any declared ``__init__`` parameters are added they must match those of any declared
method. ``__init__`` method.
Before we continue implementing the other methods, it is important to Before we continue implementing the other methods, it is important to
understand that the above implementation is not safe. In case understand that the above implementation is not safe. In case
...@@ -117,45 +131,67 @@ only reason why the above can fail is due to insufficient memory. In ...@@ -117,45 +131,67 @@ only reason why the above can fail is due to insufficient memory. In
that case, it will return ``NULL``, whereas it would normally return a that case, it will return ``NULL``, whereas it would normally return a
pointer to the new queue. pointer to the new queue.
The normal way to get out of this is to raise an exception, but The normal Python way to get out of this is to raise an exception, but
allocating a new exception instance may actually fail when we are in this specific case, allocating a new exception instance may
running out of memory. Luckily, CPython provides a function actually fail because we are running out of memory. Luckily, CPython
``PyErr_NoMemory()`` that raises the right exception for us. We can provides a function ``PyErr_NoMemory()`` that safely raises the right
thus change the init function as follows:: exception for us. We can thus change the init function as follows::
cimport cpython.exc # standard cimport from CPython's C-API
cimport cqueue
cdef class Queue:
cdef cqueue.Queue _c_queue
def __cinit__(self): def __cinit__(self):
self._c_queue = cqueue.queue_new() self._c_queue = cqueue.queue_new()
if self._c_queue is NULL: if self._c_queue is NULL:
python_exc.PyErr_NoMemory() cpython.exc.PyErr_NoMemory()
The next thing to do is to clean up when the Queue is no longer used. The ``cpython`` package contains pre-defined ``.pxd`` files that ship
To this end, CPython provides a callback that Cython makes available with Cython. If you need any CPython C-API functions, you can cimport
as a special method ``__dealloc__()``. In our case, all we have to do them from this package. See Cython's ``Cython/Includes/`` source
is to free the Queue, but only if we succeeded in initialising it in package for a complete list of ``.pxd`` files, including parts of the
standard C library.
The next thing to do is to clean up when the Queue instance is no
longer used (i.e. all references to it have been deleted). To this
end, CPython provides a callback that Cython makes available as a
special method ``__dealloc__()``. In our case, all we have to do is
to free the C Queue, but only if we succeeded in initialising it in
the init method:: the init method::
def __dealloc__(self): def __dealloc__(self):
if self._c_queue is not NULL: if self._c_queue is not NULL:
cqueue.queue_free(self._c_queue) cqueue.queue_free(self._c_queue)
At this point, we have a compilable Cython module that we can test. At this point, we have a working Cython module that we can test. To
To compile it, we need to configure a ``setup.py`` script for compile it, we need to configure a ``setup.py`` script for distutils.
distutils. Based on the example presented earlier on, we can extend Reusing the basic script from the main tutorial::
the script to include the necessary setup for building against the
external C library. Assuming it's installed in the normal places from distutils.core import setup
(e.g. under ``/usr/lib`` and ``/usr/include`` on a Unix-like system), from distutils.extension import Extension
we could simply change the extension setup from from Cython.Distutils import build_ext
setup(
cmdclass = {'build_ext': build_ext},
ext_modules = [Extension("queue", ["queue.pyx"])]
)
We can extend this script to include the necessary setup for building
against the external C library. Assuming it's installed in the normal
places (e.g. under ``/usr/lib`` and ``/usr/include`` on a Unix-like
system), we could simply change the extension setup from
:: ::
ext_modules = [Extension("hello", ["hello.pyx"])] ext_modules = [Extension("queue", ["queue.pyx"])]
to to
:: ::
ext_modules = [ ext_modules = [
Extension("hello", ["hello.pyx"], Extension("queue", ["queue.pyx"],
libraries=["calg"]) libraries=["calg"])
] ]
...@@ -167,13 +203,13 @@ flags, such as:: ...@@ -167,13 +203,13 @@ flags, such as::
LDFLAGS="-L/usr/local/otherdir/calg/lib" \ LDFLAGS="-L/usr/local/otherdir/calg/lib" \
python setup.py build_ext -i python setup.py build_ext -i
Once we have compiled the module for the first time, we can try to Once we have compiled the module for the first time, we can now import
import it:: it and instantiate a new Queue::
PYTHONPATH=. python -c 'import queue.Queue as Q; Q()' PYTHONPATH=. python -c 'import queue.Queue as Q ; Q()'
However, our class doesn't do much yet so far, so However, this is all our Queue class can do so far, so let's make it
let's make it more usable. more usable.
Before implementing the public interface of this class, it is good Before implementing the public interface of this class, it is good
practice to look at what interfaces Python offers, e.g. in its practice to look at what interfaces Python offers, e.g. in its
...@@ -198,12 +234,12 @@ Here is a simple implementation for the ``append()`` method:: ...@@ -198,12 +234,12 @@ Here is a simple implementation for the ``append()`` method::
Again, the same error handling considerations as for the Again, the same error handling considerations as for the
``__cinit__()`` method apply, so that we end up with this ``__cinit__()`` method apply, so that we end up with this
implementation:: implementation instead::
cdef append(self, int value): cdef append(self, int value):
if not cqueue.queue_push_tail(self._c_queue, if not cqueue.queue_push_tail(self._c_queue,
<void*>value): <void*>value):
python_exc.PyErr_NoMemory() cpython.exc.PyErr_NoMemory()
Adding an ``extend()`` method should now be straight forward:: Adding an ``extend()`` method should now be straight forward::
...@@ -214,7 +250,7 @@ Adding an ``extend()`` method should now be straight forward:: ...@@ -214,7 +250,7 @@ Adding an ``extend()`` method should now be straight forward::
for i in range(count): for i in range(count):
if not cqueue.queue_push_tail( if not cqueue.queue_push_tail(
self._c_queue, <void*>values[i]): self._c_queue, <void*>values[i]):
python_exc.PyErr_NoMemory() cpython.exc.PyErr_NoMemory()
This becomes handy when reading values from a NumPy array, for This becomes handy when reading values from a NumPy array, for
example. example.
...@@ -230,15 +266,14 @@ which provide read-only and destructive read access respectively:: ...@@ -230,15 +266,14 @@ which provide read-only and destructive read access respectively::
return <int>cqueue.queue_pop_head(self._c_queue) return <int>cqueue.queue_pop_head(self._c_queue)
Simple enough. Now, what happens when the queue is empty? According Simple enough. Now, what happens when the queue is empty? According
to the documentation, the functions return a ``NULL`` pointer, which is to the documentation, the functions return a ``NULL`` pointer, which
typically not a valid value. Since we are simply casting to and from is typically not a valid value. Since we are simply casting to and
ints, we cannot distinguish anymore if the from ints, we cannot distinguish anymore if the return value was
return value was ``NULL`` because the queue was empty or because the ``NULL`` because the queue was empty or because the value stored in
value stored in the queue was ``0``. However, in Cython code, we the queue was ``0``. However, in Cython code, we would expect the
would expect the first case to raise an exception, whereas the second first case to raise an exception, whereas the second case should
case should simply return ``0``. To deal with this, we need to simply return ``0``. To deal with this, we need to special case this
special case this value, and check if the queue really is empty or value, and check if the queue really is empty or not::
not::
cdef int peek(self) except? 0: cdef int peek(self) except? 0:
cdef int value = \ cdef int value = \
...@@ -264,30 +299,29 @@ an exception was raised, and if so, propagate the exception. This ...@@ -264,30 +299,29 @@ an exception was raised, and if so, propagate the exception. This
obviously has a performance penalty. Cython therefore allows you to obviously has a performance penalty. Cython therefore allows you to
indicate which value is explicitly returned in the case of an indicate which value is explicitly returned in the case of an
exception, so that the surrounding code only needs to check for an exception, so that the surrounding code only needs to check for an
exception when receiving this special value. All other values will be exception when receiving this exact value. All other values will be
accepted almost without a penalty. accepted almost without a penalty.
Now that the ``peek()`` method is implemented, the ``pop()`` method is Now that the ``peek()`` method is implemented, the ``pop()`` method
almost identical. It only calls a different C function:: also needs adaptation. Since it removes a value from the queue,
however, it is not enough to test if the queue is empty *after* the
removal. Instead, we must test it on entry::
cdef int pop(self) except? 0: cdef int pop(self) except? 0:
cdef int value = \ if cqueue.queue_is_empty(self._c_queue):
<int>cqueue.queue_pop_head(self._c_queue) raise IndexError("Queue is empty")
if value == 0: return <int>cqueue.queue_pop_head(self._c_queue)
# this may mean that the queue is empty, or
# that it happens to contain a 0 value
if cqueue.queue_is_empty(self._c_queue):
raise IndexError("Queue is empty")
return value
Lastly, we can provide the Queue with an emptiness indicator in the Lastly, we can provide the Queue with an emptiness indicator in the
normal Python way:: normal Python way by defining the ``__bool__()`` special method (note
that Python 2 calls this method ``__nonzero__``, whereas Cython code
can use both)::
def __nonzero__(self): def __bool__(self):
return not cqueue.queue_is_empty(self._c_queue) return not cqueue.queue_is_empty(self._c_queue)
Note that this method returns either ``True`` or ``False`` as the Note that this method returns either ``True`` or ``False`` as we
return value of the ``queue_is_empty`` function is declared as a declared the return type of the ``queue_is_empty`` function as
``bint``. ``bint``.
Now that the implementation is complete, you may want to write some Now that the implementation is complete, you may want to write some
...@@ -305,21 +339,17 @@ callable from C code with fast C semantics and without requiring ...@@ -305,21 +339,17 @@ callable from C code with fast C semantics and without requiring
intermediate argument conversion from or to Python types. intermediate argument conversion from or to Python types.
The following listing shows the complete implementation that uses The following listing shows the complete implementation that uses
``cpdef`` methods where possible. This feature is obviously not ``cpdef`` methods where possible::
available for the ``extend()`` method, as the method signature is
incompatible with Python argument types.
::
cimport cqueue cimport cqueue
cimport python_exc cimport cpython.exc
cdef class Queue: cdef class Queue:
cdef cqueue.Queue* _c_queue cdef cqueue.Queue* _c_queue
def __cinit__(self): def __cinit__(self):
self._c_queue = cqueue.queue_new() self._c_queue = cqueue.queue_new()
if self._c_queue is NULL: if self._c_queue is NULL:
python_exc.PyErr_NoMemory() cpython.exc.PyErr_NoMemory()
def __dealloc__(self): def __dealloc__(self):
if self._c_queue is not NULL: if self._c_queue is not NULL:
...@@ -328,14 +358,14 @@ incompatible with Python argument types. ...@@ -328,14 +358,14 @@ incompatible with Python argument types.
cpdef append(self, int value): cpdef append(self, int value):
if not cqueue.queue_push_tail(self._c_queue, if not cqueue.queue_push_tail(self._c_queue,
<void*>value): <void*>value):
python_exc.PyErr_NoMemory() cpython.exc.PyErr_NoMemory()
cdef extend(self, int* values, Py_ssize_t count): cdef extend(self, int* values, Py_ssize_t count):
cdef Py_ssize_t i cdef Py_ssize_t i
for i in range(count): for i in xrange(count):
if not cqueue.queue_push_tail( if not cqueue.queue_push_tail(
self._c_queue, <void*>values[i]): self._c_queue, <void*>values[i]):
python_exc.PyErr_NoMemory() cpython.exc.PyErr_NoMemory()
cpdef int peek(self) except? 0: cpdef int peek(self) except? 0:
cdef int value = \ cdef int value = \
...@@ -347,24 +377,37 @@ incompatible with Python argument types. ...@@ -347,24 +377,37 @@ incompatible with Python argument types.
raise IndexError("Queue is empty") raise IndexError("Queue is empty")
return value return value
cpdef int pop(self) except? 0: cdef int pop(self) except? 0:
cdef int value = \ if cqueue.queue_is_empty(self._c_queue):
<int>cqueue.queue_pop_head(self._c_queue) raise IndexError("Queue is empty")
if value == 0: return <int>cqueue.queue_pop_head(self._c_queue)
# this may mean that the queue is empty,
# or that it happens to contain a 0 value
if cqueue.queue_is_empty(self._c_queue):
raise IndexError("Queue is empty")
return value
def __nonzero__(self): def __bool__(self):
return not cqueue.queue_is_empty(self._c_queue) return not cqueue.queue_is_empty(self._c_queue)
As a quick test with numbers from 0 to 9999 indicates, using this The ``cpdef`` feature is obviously not available for the ``extend()``
Queue from Cython code with C ``int`` values is about five times as method, as the method signature is incompatible with Python argument
fast as using it from Cython code with Python values, almost eight types. However, if wanted, we can rename the C-ish ``extend()``
times faster than using it from Python code in a Python loop, and method to e.g. ``c_extend()``, and write a new ``extend()`` method
still more than twice as fast as using Python's highly optimised instead that accepts an arbitrary Python iterable::
``collections.deque`` type from Cython code with Python integers.
cdef c_extend(self, int* values, Py_ssize_t count):
cdef Py_ssize_t i
for i in range(count):
if not cqueue.queue_push_tail(
self._c_queue, <void*>values[i]):
cpython.exc.PyErr_NoMemory()
cpdef extend(self, values):
for value in values:
self.append(value)
As a quick test with numbers from 0 to 9999 on the author's machine
indicates, using this Queue from Cython code with C ``int`` values is
about five times as fast as using it from Cython code with Python
values, almost eight times faster than using it from Python code in a
Python loop, and still more than twice as fast as using Python's
highly optimised ``collections.deque`` type from Cython code with
Python integers.
.. [CAlg] Simon Howard, C Algorithms library, http://c-algorithms.sourceforge.net/ .. [CAlg] Simon Howard, C Algorithms library, http://c-algorithms.sourceforge.net/
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment