almost 4 years ago

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:

  1. 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's ctypes 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.
  2. To expose class constructors we write a function that returns a pointer to the object.
  3. 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.
  4. 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;

← Calling C and C++ code from Python