November 6th 2007 04:11 pm
Wrapping a C++ container in Python
When moving to Python, the real big problem that arises is the transformation of a Python array into the C++ container the team used for years.
Let’s set some hypothesis :
- there is a separation between the class containing the data and the class that uses the data (iterators, …)
- the containing class can be changed (policy or strategy pattern)
The first hypothesis is derived from the responsibility principle, the two classes have two distinct responsibilities, the first allocates the data space and allows simple access to it, the second allows usual operations (assignation, comparison tests or iterations for instance).
The second one will be the heart of the wrapper. It allows to change the way data is stored and accessed in a simple way.
So here is a simplification of a 3D container class that will store a 3D numpy array. It must be capable of creating a container from a PyObject* and have a method to get the stored array, here getContainer():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | #include <Python.h> #include <numpy/arrayobject.h> template<class Im3DValue> class Container3DPython { protected: /// The inner data object PyArrayObject* imageData; /// Shortcut for self type reference typedef Container3DPython<Im3DValue> Self; public: /// So that other templates know what Im3DValue is typedef Im3DValue value_type; protected: /** * Tries to allocate a new 3D array * @param width is the width of the new array * @param depth is the depth of the array * @param height is the height of the array */ void allocate(int width, int height, int depth) { deallocate(); PyArray_Dims dims; dims.ptr = (npy_intp*)malloc(3 * sizeof(npy_intp)); dims.len = 3; dims.ptr[2] = width; dims.ptr[1] = height; dims.ptr[0] = depth; imageData = reinterpret_cast<PyArrayObject*>(PyArray_SimpleNewFromDescr(dims.len, dims.ptr, PyArray_DescrNewFromType(DataTypeTraits<value_type>::type_num))); Py_INCREF(imageData); free(dims.ptr); } /** * Tries to allocate an array based on a Python object * @param obj is the Python object that will be shared or copied, depending on its content and type */ void allocate(PyObject* obj) { deallocate(); PyArrayObject* array = reinterpret_cast<PyArrayObject*>(PyArray_FromAny(obj, NULL, 3, 3, NPY_FARRAY, NULL)); imageData = reinterpret_cast<PyArrayObject*>(PyArray_CastToType(array, PyArray_DescrNewFromType(DataTypeTraits<value_type>::type_num), 0)); Py_INCREF(imageData); Py_DECREF(array); } /// Deallocates the Python object void deallocate() { Py_XDECREF(imageData); imageData = NULL; } public: ///@{ \name Image3D Accessors /** * Returns the width of the image */ unsigned int width() const { return imageData->dimensions[2]; } /** * Returns the depth of the image */ unsigned int height() const { return imageData->dimensions[1]; } /** * Returns the depth of the image * @return the depth of the image */ unsigned int depth() const { return imageData->dimensions[0]; } /** * Gets the value at a position in the image * @param x is the first coordinate * @param y is the second coordinate * @param z is the third coordinate * @return a reference to the value */ Im3DValue& operator()(int x,int y,int z) { return reinterpret_cast<value_type*>(imageData->data)[x+y*width()+z*width()*height()]; } /** * Gets the value at a position in the image * @param x is the first coordinate * @param y is the second coordinate * @param z is the third coordinate * @return a const reference to the value */ const Im3DValue& operator()(int x,int y,int z) const { return reinterpret_cast<value_type*>(imageData->data)[x+y*width()+z*width()*height()]; } /** * Returns the inner shared pointer * @return the shared pointer */ PyArrayObject* getContainer() const { Py_XINCREF(imageData); return imageData; } ///@} ///@{ \name Operators /** * Assignment operator from a different type of image * @param other is the array * @return self */ Container3DPython& operator=(PyObject* other) { allocate(other); return *this; } /// Destructor virtual ~Container3DPython() { deallocate(); } /** * Simple constructor * @param width is the width of the new image * @param height is the height of the new image * @param depth is the depth of the new image */ Container3DPython(unsigned int width, unsigned int height, unsigned int depth) : imageData(NULL) { allocate(width, height, depth); } /** * Constructor from an array * @param array is the array to copy */ Container3DPython(PyObject* array) :imageData(NULL) { allocate(array); } ///@} }; |
Once this skeleton is ready, it is possible to create typedefs to reference new containers.
typedef Container<Container3DPython<float> > Container3DfPython; |
Once this is done, the associated swig file will declare some simple typemaps :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | %{
#define SWIG_FILE_WITH_INIT
#define PY_ARRAY_UNIQUE_SYMBOL PyArray_API
%}
%include "numpy.i"
%init %{
import_array();
%}
%{
#include "stdio.h"
#include <numpy/arrayobject.h>
#include <Container3DPython.hpp>
#include <Container.hpp>
%}
%typemap(in) Container3DfPython
{
$1 = new Container3DfPython($input);
}
%typemap(freearg) Container3DfPython
{
delete $1;
}
%typemap(out) Container3DfPython
{
$result = reinterpret_cast<PyObject*>($1->getContainer());
delete $1;
} |
With this method, the wrapped container can be passed to a class constructor and stored in an instance, deleted at the end. If the numpy SWIG wrappers are used, only pointers are given to the function, and it is freed at the end of the function, thus class instance cannot be created and used later.
Note that the described container class will cast the array into the appropriate type thanks to a trait structure DataTypeTraits
Note also that if you have multiple C++ files that uses the container, you will have to define NO_IMPORT_ARRAY for them (but not for the SWIG generated file).
1 Comment »
One Response to “Wrapping a C++ container in Python”
Leave a Reply
You must be logged in to post a comment.


Matt’s blog » Enabling thread support in SWIG and Python on 28 Mar 2008 at 8:56 am #
[...] that does not satisfy me. Indeed, some of my wrappers must retain the GIL while they are used (see this item). So here are the features that can be used [...]