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
System
Box
Particle
- the
main
function (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 --user
Alternatively, one some version of Ubuntu
(e.g. under WSL on Windows) one may need
apt install python-pybind11
Once this is installed, the following command should return a valid path
python -m pybind11 --includes
If 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
System
object - 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 included
Define 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
(myvicsek, m) {
PYBIND11_MODULE// all the contents of the module
}
These lines of code specifically
- defines a Python module named
myvicsek
. - refer with
m
to 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.
::class_<System> system_class(m, "System"); pybind11
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_class
with two parameters: the modulem
and the name with which thesystem_class
will 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
.def(pybind11::init<int, double, double, double, int>(),
system_class::arg("particleNumber"),
pybind11::arg("sideLength"),
pybind11::arg("timeStep"),
pybind11::arg("noiseStrength"),
pybind11::arg("seed")); pybind11
With this syntax we are telling pybind
:
- that we are referring to the constructor (
init
stands 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::arg
syntax 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:
.def("randomStart", &System::randomStart); system_class
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()
.def_readonly("particles", &System::particles); system_class
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!