Creating a Python Wrapper for C++ Classes
This is for Python folks who are not C or C++ experts but need to use a C++ library in their code. This article will show you how to create C++ objects from your Python code and call it's methods as needed.
1. Write an "extern" block for the Classes and methods you want accessible from Python
At the bottom of your C++ code add an extern "C" block.
//Exporting a FlashCards object and two of its methods, getSequence and getCurrentQA.
extern "C" {
/*
To expose a C++ class, call its constructor with a *new*, thus returning a
pointer to the object.
*/
FlashCards * FlashCards_export (char * fn){ return new FlashCards("civics100.txt"); }
/*
To expose a method that returns a C data type, take the pointer to the object it belongs to as the first argument and
then add its regular arguments. Call the method on that object pointer and return the result.
Note: the method is called on an object pointer hence the arrow vs the dot
*/
int FlashCards_getSequence_export(FlashCards* fcs){ return (fcs->getSequence());}
// To expose a method that returns a C++ data type convert the return type to a C data type. The code below shows
// how to convert a string return type to a c character pointer return type
const char * FlashCards_getCurrentQA_export (FlashCards* fcs) {
std::string s = fcs->getCurrentQA(); // C++ string
// string converted to C character pointer
return std::strcpy (new char [s.length()+1], s.c_str()) ;
};
Explainer
The code is heavily commented. Here's the lowdown:
- Each function in the
extern
block exposes a C++ class constructor or a method to the outside world (in our case to some python code). We will use python'sctypes
module to call this exposed classes and methods.ctypes
only supports C data types and therefore the functions in this block must receive and return C data types. - To expose class constructors we write a function that returns a pointer to the object.
- To expose methods of an object: we need to take the pointer to the object it belongs to as the first parameter and then add its regular parameters. We then call the method on that object pointer and return its result.
- If the returned results or the parameters are not C data types, we need to convert them. For example, typically C++ methods will return string data type as opposed to a character pointer, if that is the case we will need the exposing function to convert the string to a character pointer with code like this:
std::strcpy (new char [str.length()+1], str.c_str());
Build the library that will be shared across the two languages
This is system dependent. The following has been tested in a Mac.
Compile and create an object file named flashcards.o
g++ -c -fPIC flashcards.cpp -o flashcards.o -std=c++17
Create a shared library fc.so
g++ -shared -W1,-soname,fc.so -o fc.so flashcards.o
That is all from the C++ side. Now lets move to the Python side.
Load the library and create Python wrapper
Add the following to the top of your python code.
lib = ctypes.cdll.LoadLibrary('./fc.so')
Now all the functions that you have exposed in C++ are accessible via the lib variable.
For example we can now refer to the exposed FlashCards_getSequence_export function in python as lib.FlashCards_getSequence_export
The Wrapper Class
In the __init__
of the Python class add argtypes
and restypes
attributes for every exposed methods' return and argument types. argtypes
should be a list (i.e. []
)
This is a must.
At last line of the __init__
add self.obj = lib.FlashCards_export(fn)
which is the call to the original C++ constructor. This will add the object pointer from C++ to the python code.
For each methods we call the C++ exposed function and return the value. If the C++ function returns a character pointer we need to decode it to make it a Python string.
class FlashCards (object):
def __init__(self, fn):
lib.FlashCards_export.argtypes = [ctypes.c_char_p]
lib.FlashCards_export.restype = ctypes.c_void_p # type for object pointers
lib.FlashCards_getCurrentQA_export.argtypes = [ctypes.c_void_p]
lib.FlashCards_getCurrentQA_export.restype = ctypes.c_char_p
self.obj = lib.FlashCards_export(fn)
def getCurrentQA (self):
return str (lib.FlashCards_getCurrentQA_export(self.obj).decode())
That is it !!!. You can now write something like:
civics =FlashCards ("civics100.txt".encode('utf-8'))
civics.getCurrentQA ()
Notes:
Decide which methods and objects you want to be accessible
Unless you are writing a full wrapper of a C++ code base, you do not need to bring in all the code to Python. If you need to do that, handcrafting the wrapper is not a good idea. You should find or write a wrapper generator. In most cases your app will need only a few methods and their classes from the C++ code base. To simplify the handcrafting process you can try not to bring in the element classes of a objects that have containers. For example if you have a FlashCards object that stores and manipulates bunch of Card objects you may have C++ code that does something like the following:
FlashCards fcs ();
fcs.getNext().getQ();
// get me the next card object and call its getQ method.
In situations like this instead of bringing in two classes and writing complex wrappers that models interaction between
two class pointers, you should add a FlashCards method like getNextQ ()
which will execute getNext and then getQ of the getNext and return the Q.
FlashCards fcs ();
fcs.getNextQ(); // get me the Q of the next card object
Now wrap getNextQ;