Lots more details on writing wrappers
From DANSE
| Table of contents |
Every wrapper function does three things:
- Converts the inputs from Python objects to C or C++ objects,
- calls the C++ function,
- converts the output to a Python object with the result and return it.
Let's first run through these steps with numbers and strings, for which there's an immediate connection between Python and C++ types; we can use a function, PyArg_ParseTuple, which is built in to the API. Later examples look at tasks like working with C++ class instances and using aggregate Python types such as dictionaries and lists.
Simple examples
Convert input
Every wrapper has the same signature as every other:
PyObject * wrap_a_function(PyObject *, PyObject * args)
PyObject (and everything else around here with the Py-prefix) is defined for us in the API. The first pointer argument, used for some highly specific purpose, is generally ignored. (When you do need it, you'll need a lot more help than I can give). The second argument is a PyTuple (a "subtype" of PyObject) that holds the tuple of arguments that the user called the function with in Python.
//One integer: int a; int ok = PyArg_ParseTuple(args, "i",&a); if(!ok) return 0;
//Two integers int a, b; int ok = PyArg_ParseTuple(args, "ii",&a, &b); if(!ok) return 0;
//One integer, a string, two doubles int anint; char * astring; // don't allocate memory! double dub1, dub2; int ok = PyArg_ParseTuple(args, "isdd", &anint, &astring, &dub1, &dub2); if(!ok) return 0;
A complete list of what can go into the format string is given in the extension documentation (look in the "ext" subdirectory of the "doc" directory in your Python distribution, or look online at http://docs.python.org/ext/ext.html ). In the current documentation, you want section 1.7, "Extracting Parameters in Extension Functions".
PyArg_ParseTuple() checks the types of the objects in the tuple "args" against the types given in the format string. If there's a discrepancy, it sets an appropriate exception and returns a null pointer; The Python interpreter regards the null pointer as failure and raises an exception. So if you're using PyArg_ParseTuple(), most of the error checking is done for you! Once PyArg_ParseTuple has succesfully returned, do any additional processing or checking of the input data that's appropriate. For example, Python has no unsigned type, so you might need to make sure that an integer is greater than zero.
Call the function
It's your function, call it.
Convert the output
Convert the output to a Python object. The API gives a function called Py_BuildValue; here are some examples:
// return an integer
PyObject *py_result = Py_BuildValue("i", result);
// return a string
PyObject *py_result = Py_BuildValue("s", astring);
Complete wrapper example
Here's a complete example of wrapping a function that takes a double, a string, and an int (in that order) and returns an int. We expect that the arguments will come in the order string, int, double.
PyObject *wrap_bogus(PyObject *, PyObject * args)
{
//One: get the arguments from Python
int anint = 0;
double adub = 0;
char * astring = 0;
int ok = PyArg_ParseTuple( args, "sid", &astring,
&anint, &adub);
if(!ok) return 0;
//do any checking of arguments here
...
//Two: make the function call
int result = bogus( adub, astring, anint);
//do any postprocessing of the result
...
//Three: build a Python object to return
return PyBuildValue("i",result);
}
class instances
How do you use C++ classes from Python? You wrap the class's constructor(s) to dynamically allocate an instance of a C++ class and pass a handle back to Python. That handle can then be used with other wrappers for class methods.
We get the arguments to the constructor from the Python API, create the object using new, and return a pointer to that object to the interpreter. Use the API function PyCObject_FromVoidPtr to return the pointer to Python. Here's an example with a real (if trivial) class called Numbers:
class Numbers
{
public:
Numbers(int first, double second)
: one( first), two(second){}
double NumMemberMult(void){ return m_first*m_second;}
private:
int m_first;
double m_second;
};
Here's a wrapper that creates a new instance of Numbers:
PyObject *wrap_new_Numbers(PyObject *, PyObject* args)
{
//First, extract the arguments from a Python tuple
int arg1;
double arg2;
int ok = PyArg_ParseTuple(args,"id",&arg1,&arg2);
if(!ok) return 0;
//Second, dynamically allocate a new object
Numbers *newnum = new Numbers(arg1, arg2);
//Third, wrap the pointer as a "PyCObject" and
// return that object to the interpreter
return PyCObject_FromVoidPtr( newnum, PyDelNumbers);
}
Look familiar? This wrapper has essentially the same structure as wrap_bogus(). That's because ALL wrappers have this structure.
The pointer to the dynamically allocated object, newnum, goes out of scope as soon as wrap_new_Numbers() returns. The only thing keeping this from being the mega-classic memory leak is that the Python interpreter has an object that will keep track of the address of the new object.
The interpreter tracks that object for us, and when we lose interest in it (its reference count goes to zero), the interpreter will call a C++ function to delete the object. The second slot in PyCObject_AsVoidPtr() is a pointer to a function with return type void and one void * argument. So the overall signature of PyCObject_AsVoidPtr is:
PyObject * PyCObject_FromVoidPtr( void *, void (*DeleteFunction)(void*));
You must supply the function pointed to (in this example called PyDelNumbers). It has the "delete" corresponding to the "new" in the wrapper. Here's the definition of that function:
static void PyDelNumbers(void *ptr)
{
//std::cout<<"Called PyDelNumbers()\n"; //Good to see once
Numbers * oldnum = static_cast<Numbers *>(ptr);
delete oldnum;
return;
}
The user can't actually do anything in the interpreter with the pointer, except send it back to the C++ library to do some other stuff, like call a C++ class method on it, or give it as an argument to another function. Here's an example of wrapping a class method.
PyObject *wrap_Numbers_MemberMult(PyObject *, PyObject* args)
{
// First, get the PyCObject from the args tuple
PyObject *pynum = 0;
int ok = PyArg_ParseTuple( args, "O", &pynum);
//"O" is for Object
if(!ok) return NULL;
// Convert the PyCObject to a void pointer:
void * temp = PyCObject_AsVoidPtr(pynum);
// Cast the void pointer to a Numbers pointer:
Numbers * thisnum = static_cast<Numbers *>(temp);
//Second, make the function call
double result = thisnum->NumMemberMult();
//Third, build & return a Python float object
return Py_BuildValue("d",result);
}
All you have to do is
- get the PyCObject out of the tuple, then extract and cast the pointer to the appropriate type, and then
- use it, then
- bundle up the result and send it back to the interpreter.
aggregate types
Okay, so how about working with Python's aggregate types like lists and dictionaries? The API convenience functions that we use to go back and forth between simple Python objects and C objects don't have a fixed way to handle more complex objects like sequences. This probably makes sense: there's more than one way to represent a dictionary in C++, and maybe even more than one sensible way. We'll have to implement our own conversions, which really only means we have to look at the extensive API support to see what functions exist for working with dictionaries, lists, etc.
Example: Unpacking a Python Dictionary
Here's an example that takes a Python dictionary and extracts its contents. All you have to do is extract the dictionary from the args tuple, and get to work. This code has lots more checking of types than previous examples, where the API did that behind the scenes. It assumes that you've sent in a dictionary with strings as keys and floating point numbers as values.
PyObject * wrap_dict(PyObject *, PyObject *args)
{
PyObject * dict = 0;
int ok = PyArg_ParseTuple(args, "O", &dict);
if (!ok) return 0;
// is the object really a dictionary?
if ( !PyDict_Check(dict))
{
// set except context for TypeError
PyErr_SetString( PyExc_TypeError,
"That was not a dictionary!");
// tell interpreter to raise an exception
return 0;
}
//PyDict_Keys returns a PyList with the keys:
PyObject * key_list = PyDict_Keys(dict);
int list_size = PyList_Size(key_list);
// use STL vector to store numbers from dictionary
std::vector<double> my_numbers( list_size);
//Get keys and corresponding values from the dictionary
for( int i=0; i<list_size; i++)
{
PyObject * pystring = 0;
pystring = PyList_GetItem(key_list, i);
int isstring = PyString_Check(pystring);
if(! isstring)
{
PyErr_SetString( TypeError,
"I only know how to process "
"dictionaries whose keys are "
"strings");
return 0;
}
//convert the PyString object to a regular char *
char * temp = PyString_AsString(pystring);
//Get the value for the key
PyObject *pydub = 0;
pydub = PyDict_GetItem(dict, pystring);
//Convert to a C++ double:
my_numbers[i] = PyFloat_AsDouble(pydub);
std::cout<<"Value # "<<i<<" is "<<dub<<"\n";
} // ends for( int i=0; i<list_size; i++)
// Do something with "my_numbers" array
...
Example: Creating a Python list out of C++ variables
You'd build up a Python list in C++ in the same way. This example constructs a list of strings:
...
//Build a list; send it back to interpreter
PyObject *newlist = PyList_New(0);
int ok = PyList_Append( newlist,
PyString_FromString("first"));
// Check the API documentation for meaning of
// return values.
if(ok != 0)
{
// set exception context, raise (return 0)
}
ok = PyList_Append( newlist,
PyString_FromString("second"));
if( ok != 0) ...
ok = PyList_Append( newlist,
PyString_FromString("third"));
if( ok != 0) ...
return newlist;
}
