Using C++ objects from Python
Aims
- Improve the Python interface to use the code
- Discover the notion of bindings
Learning outcomes
- Minimal usage of
pybind11
Interacting with the C++ code
Our C++ is now constituted of many pieces:
- the objects
SystemBoxParticle
- the
mainfunction (and source code)
The main.cpp file seems critical : without it, the program does not even begin.
However, all the conceptual pieces that we need for the simulation do exist without the main.cpp code: the System contains all the functions and variables needed for the simulation to run.
It would be ideal to delegate to C++ all the heavy calculations related to the system update and instead use an easier, more flexible scripting language (such as Python) to coordinate the simulation, its parameters and its plotting.
pybind11 precisely allows us to do this .
The pybind11 library
Pybind11 is header only library for C++ that simplifies the inter-operability (bindings) between the two languages. This chiefly means the ability to call C++ functions and objects from Python.
While Python is written in C/C++ itself, providing such binding using the native Python interface is quite complex.
pybind11 provides a number of tools that work at high level (i.e. at the level of class declarations).
The library is header only, means that its installation is very simple (it is a small number of headers .h files that do not need to be pre-compiled).
Installation
The most generic installation is via Python’s package manager pip
pip install pybind11 --userAlternatively, one some version of Ubuntu (e.g. under WSL on Windows) one may need
apt install python-pybind11Once this is installed, the following command should return a valid path
python -m pybind11 --includesIf this works, pybind11 is correctly installed and one can proceed with the construction of the bindings.
Binding the System
The goal of the bindings is to produce a new Python package that exposes (some) properties of the C++ code.
We do not need to expose everything, only the parts we are interested in. Given the hierarchical structure of our code, the minimum we can expose is the System class itself. If, in Python, we have a means to
- construct a
Systemobject - initialise it
- update it according to the dynamics
- store its configurations
we can run a basic simulation in Python/C++ of the Vicsek model.
To bind our code we need to create a bindings.cpp file. This file will include all the instructions needed to convert the C++ code to Python, leveraging the library. we do not need a corresponding bindings.h file as we are not defining any new object: we are only providing the instruction for pybind11 to map a C++ object to a Python object.
Header Inclusions
The bindings.cpp first needs to include the correct libraries and header files.
For pybind11 we include the standard
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>and specifically for our System.h we include its header.
#include "system.h" // Ensure this header is includedDefine the Python module
We now need to define the properties of the new Python module that we will create. To do so, we use a special C++ macro defined inside pybind11 to automatically generate a C++ function that constructs a Python module.
At this level, let’s axiomatically accept that to define a Python module from C++ with pybind11 we need to call the following construct
PYBIND11_MODULE(myvicsek, m) {
// all the contents of the module
}These lines of code specifically
- defines a Python module named
myvicsek. - refer with
mto the Python module object where bindings are added.
We now simply need to provide the instructions to expose the structure of the System to Python.
First, we need to tell Python that we have a class definition with name System. So we construct a dummy object system_class that represent the class itself.
pybind11::class_<System> system_class(m, "System");In this line, we use a special class defined in the pybind11 library and apply it to our System class. Notice that:
- We call the
class_on our classSystem(with a syntax similar to the standardstd::vector<Type>). - We construct the dummy
system_classwith two parameters: the modulemand the name with which thesystem_classwill appear on the Python side. In this caseSystem.
We need now to map member functions and member variables to python.
pybind provides us with many methods to expose classes and their parts. For example:
.def(): binds a member function to the class..def_readonly()binds a member variable to a read-only attribute..def_readwrite()binds a member variable to an attribute that can be overwritten.
and various others (see documentation here).
We shall only use .def() and .def_readonly() for simplicity.
Mapping the constructor
The constructor is a member function, so we need to use .def().
We need to describe how the constructor works at a very high lebvel: this means essentially just telling Python what kind od inputs it takes:
For this, pybind has a specific syntax
system_class.def(pybind11::init<int, double, double, double, int>(),
pybind11::arg("particleNumber"),
pybind11::arg("sideLength"),
pybind11::arg("timeStep"),
pybind11::arg("noiseStrength"),
pybind11::arg("seed"));With this syntax we are telling pybind:
- that we are referring to the constructor (
initstands for initialisation) - the constructor has a specific sequence of parameters, with specific types
- the parameters have particular names (and we can see such names also in Python). Note the
pybind11::argsyntax to identify the strings a parameter names (“arguments”).
You can in principle omit the specification of the parameter names, but it is always more informative to have them.
Member functions and methods
Our system class also has member functions and member variables. pybind allows us to expose both, and we can simply choose what to bind and what not to bind.
As above, we can use .def to bind member functions, for example the random initialisation:
system_class.def("randomStart", &System::randomStart);where we tell Python that a new member function (a method) exists for our class and binds directly to its address (&) in the C++ class instance.
Similarly, we can expose the particles member variable in read-only mode simply using .def_readonly()
system_class.def_readonly("particles", &System::particles);Again, with this syntax, we use our dummy system_class instance and tell Python that an attribute called "particles" will exist and will point to the address (&) of the member variable System::particles.
Running from Python
If you have managed to bind both the System and the Particle class successfully, you are finally ready to use your C++ code efficiently from Python.
This means that you can now freely
- initialise systems with arbitary parameters (number of particles, noise levels etc)
- update them performing teh Vicsek dynamics
- query the state of the particles inb the system and measure any statistic
- visualise the system as it evolves, with no need to save the data to file.
With the correct bindings, now you can simply code in Python and benefit from the speedup of the underlying C++ code. You can even think about distributing your package, for example via pip. Enjoy!