Introduction
Pybind11 can be used to import C++ libraries into Python. Here I create a C++ project, the robot, to resemble real-life software as much as possible. Then, I write a piece of code to compile the project as a Python module. The key point is to only expose end-user (API) classes and functions.
Goal
A detailed C++ project, the robot, is created, then its API is exposed to Python.
In the example we see:
- C++ classes and their components are re-defined as python classes:
- constructors
- methods
- public data members
- setter and getter (property)
- A class template is also exposed.
- Polymorphic objects are injected into constructors.
- C++ functions are converted to Python functions
- Polymorphic objects are passed to functions in Python
- STL containers data are received in Python
Final Code
The final code is on GitHub. It is explained in the following sections.
Robot Code
The code creates robots and asks them to do tasks.
An interface for the robot is:
class IRobot
{
public:
std::string name;
ISpeech &speech;
IRobot(std::string name_, ISpeech &speech_) :
name(name_), speech(speech_){};
virtual void Walk() = 0;
void Talk() { speech.Talk(); }
};
Where we have two different speech devices, V1 and V2:
class ISpeech
{
protected:
std::string model;
public:
ISpeech(std::string model_) : model(model_){};
virtual void Talk() = 0;
};
class SpeechV1 : public ISpeech
{
public:
SpeechV1(std::string model_) : ISpeech(model_){};
void Talk() override {
std::cout << "Talking with Speech version 1.0. \n"; }
};
class SpeechV2 : public ISpeech
{
public:
SpeechV2(std::string model_) : ISpeech(model_){};
void Talk() override {
std::cout << "Talking with Speech version 2.0. \n"; }
};
Robot T800 specs:
- A polymorphic class made of
IRobot
- Has constructor accepts
ISpeech
objects - has Public member:
year
- Has virtual function:
Walk()
- Provides a complex STL container:
GetData()
class T800 : public IRobot
{
public:
int year;
T800(std::string name, int year_, ISpeech &speech_) : IRobot(name, speech_), year(year_){};
virtual void Walk() override{std::cout << "T-800 is walking...\n";};
auto GetData(){
std::vector<std::tuple<std::string,double>> data;
data.push_back(std::make_tuple("book", 1.5));
data.push_back(std::make_tuple("table", 2.5));
data.push_back(std::make_tuple("wall", 3.5));
return data;
}
};
And Robot T1000 specs:
- is a class template
- is a polymorphic class made of
IRobot
- Has constructor accepts
ISpeech
objects - Has property
Height
- Has virtual function
Walk
template <typename T>
class T1000 : public IRobot
{
T height;
public:
T1000(std::string name_, T height_, ISpeech &speech_)
: IRobot(name_, speech_), height(height_){};
const auto &GetHeight() { return height; }
auto SetHeight(T height_) { height = height_ ;}
virtual void Walk() override { std::cout << "T-1000 is walking...\n"; };
};
And finally we have a helper function which accepts any robot:
void Move(IRobot& robot){
std::cout<< robot.name <<"\n";
robot.Walk();
}
pybind11 binding
The classes and their members are declared in the following way:
PYBIND11_MODULE(moduleName, m)
{
// define all classes
py::class_<NameOfClass, ClassInterface>(m, "NameOfClass")
.def(py::init<type1, type2, type3>()) // constructor
.def("aFunction", &NameOfClass::aFunction)
.def_readwrite("aPublicMember", &NameOfClass::aPublicMember);
// define all standalone functions
m.def("StandAloneFunction", &StandAloneFunction);
}
- moduleName: the name of the module in Python
- m : pybind11 handle, accept it as it is
- NameOfClass: the name of the class to be exposed to Python
- ClassInterface: the interface or parent of the class. It is not necessary, if polymorphism is not needed to be captured in Python.
- type1, typ2,…: the type of arguments to the constructor of the class
- aFunction: name of a function member of the class
- aPublicMember: a public member of the class
We can also declare functions that don’t belong to any class:
PYBIND11_MODULE(moduleName, m)
{
// define all classes
// ...
// define all standalone functions
m.def("StandAloneFunction", &StandAloneFunction);
}
Robot to Python
Now we can declare robot code to Python.
I create a folder pythonApi
outside src
folder and put binding code there. In this way, the main code is not polluted.
The binding code is:
#include "../src/t800.h"
#include "../src/t1000.h"
#include "../src/helper.h"
#include "../pybind11/include/pybind11/pybind11.h"
// to convert C++ STL containers to python list, see T800.GetData()
#include "../pybind11/include/pybind11/stl.h"
namespace py = pybind11;
using namespace std;
PYBIND11_MODULE(robot, m)
{
// Do not add abstract class constructor
// We are just declaring it to python. Because
// It is an argument type in T800, T1000 constructors
// and also an argument type of Move() function.
py::class_<ISpeech>(m, "ISpeech");
// Add the base class to work polymorphism.
// For example T800 constructed with ISpeech, if
// we don't declare it here, python doesn't allow
// injectign SpeechV1 to T800 constructor.
py::class_<SpeechV1, ISpeech>(m, "SpeechV1")
.def(py::init<string>()); //Constructor
py::class_<SpeechV2, ISpeech>(m, "SpeechV2")
.def(py::init<string>()); // Constructor
py::class_<IRobot>(m, "IRobot"); // Abstract, do not add constructor
py::class_<T800, IRobot>(m, "T800")
.def(py::init<string, int, ISpeech &>()) // constructor
.def("Walk", &T800::Walk)
.def("Talk", &T800::Talk)
.def("GetData", &T800::GetData)
// read-write public data memeber
// you can use def_readonly as well.
.def_readwrite("year", &T800::year);
using T = double;
py::class_<T1000<T>, IRobot>(m, "T1000")
.def(py::init<string, T, ISpeech &>()) // constructor
.def("Walk", &T1000<T>::Walk) // method
.def("Talk", &T1000<T>::Talk) // method
// Define property with getter and setter
.def_property("height", &T1000<T>::GetHeight,&T1000<T>::SetHeight);
m.def("Move", &Move);
}
Compile
Clone pybind11
In the project folder, clone pybind11
git clone https://github.com/pybind/pybind11.git
Look at .gitignore
file, pybind11 repo is ignored as we just read it.
CMake
The CMake Code is as simple as:
cmake_minimum_required(VERSION 3.1.0)
project(robot)
set (CMAKE_CXX_STANDARD 20)
add_subdirectory(pybind11)
pybind11_add_module(robot "./pythonApi/robot.cpp")
Note that name robot
is consistent with the module name we use in bindings
PYBIND11_MODULE(robot, m) {/* code */}
Make
Open a terminal, in the project folder, run
mkdir build
cd build
cmake ..
make
In the build
directory, you should have a compiled module with the name similar to:
robot.cpython-39-x86_64-linux-gnu.so
Run
In a terminal being in the build
directory, run
python
in Python, import the library
import robot
and work with the classes and functions imported.
Example
There is a Python example in pythonApi/example.py
to use robot module:
from robot import *
s1 = SpeechV1("model_V1_ab")
s2 = SpeechV2("model_V2_yz")
# Create robots with different Speech systems
t800 = T800("Jack", 2020, s1)
t1000 = T1000("Kate", 0, s2)
print("t800:")
# returns STL container in python list
print(t800.GetData())
t800.Talk()
print("t1000:")
t1000.Talk()
# set, get property
t1000.height=1.90
print(t1000.height)
# Moves any robot
Move(t800)
Move(t1000)
Latest Posts
- A C++ MPI code for 2D unstructured halo exchange
- Essential bash customizations: prompt, ls, aliases, and history date
- Script to copy a directory path in memory in Bash terminal
- What is the difference between .bashrc, .bash_profile, and .profile?
- A C++ MPI code for 2D arbitrary-thickness halo exchange with sparse blocks