DEV Community

Somenath Mukhopadhyay
Somenath Mukhopadhyay

Posted on • Originally published at som-itsolutions.blogspot.com on

The story of Pybinding - a python wrapper around C++...

Pybinding isn't a single, monolithic tool, but rather a concept that has been realized by several different libraries. The most prominent and widely used among them is pybind11. The story of pybind11 is a testament to the enduring need for high-performance computing in Python and the elegant solutions that can arise from a desire for simplicity and efficiency.

The story starts with a common problem: Python is a fantastic language for rapid prototyping, data analysis, and orchestrating complex tasks. However, when it comes to raw computational speed, especially for number-crunching or highly parallelized operations, it can fall short. C++ and other compiled languages, on the other hand, excel in these areas. The question was, how do you get the best of both worlds? How do you write the performance-critical parts of your application in C++ while still enjoying the development speed and ecosystem of Python?

The answer was to create a "binding" – a bridge that allows Python to call C++ code as if it were native Python. Early efforts in this space, such as Boost.Python , were powerful but often came with a steep learning curve and significant compilation overhead. They were a bit like using a sledgehammer to crack a nut – effective, but perhaps a bit unwieldy for many use cases.

This is where the next chapter of the story begins, with the emergence of pybind11. The creator of pybind11, Wenzel Jakob, had a vision for a library that would be:

  • Lightweight and Header-Only: No need to link against a large library. You just include the headers, and the magic happens at compile time.

  • Easy to Use: The syntax should feel "Pythonic" and intuitive, allowing developers to create bindings with minimal boilerplate code.

  • Efficient: The generated code should be fast and have a small binary footprint.

  • Modern: It should fully embrace modern C++ standards, like C++11 and beyond, to take advantage of features like move semantics and templates.

Pybind11 was born from this vision, and it quickly gained traction. It told a new story about how C++ and Python could work together. Instead of a cumbersome, complex process, creating a Python wrapper became a straightforward task.

The story of using pybind11 often goes something like this:

The Setup: A scientist or developer has a C++ library with a highly optimized algorithm. Let's say it's a function that performs a complex matrix multiplication. They want to use this function from their Python scripts, where they are handling data loading, visualization, and other tasks.

The Binding: They write a small C++ file that acts as the "wrapper." This file includes the pybind11 headers and uses its simple syntax to expose the C++ function to Python. They define a Python module, give their C++ function a Python-friendly name, and specify the types of arguments it takes. Pybind11 automatically handles the conversion between Python's data types (like NumPy arrays) and C++'s native types (like std::vector or Eigen matrices).

The Compilation: Using a build system like CMake, they compile this wrapper file. The result is a shared library (.so or .dll) that Python can import.

The Payoff: Back in their Python script, they can now simply import the newly created module and call the C++ function directly. The performance-critical work is handled by the blazing-fast C++ code, while the rest of the application remains in the flexible and easy-to-manage world of Python.

So... here we go...

My exploration of Pybinding11 to refactor one of my projects called FactoryDesignPattern.

Here's the source code...

/*
 * Food.h
 *
 *  Created on: Mar 10, 2021
 *      Author: som
 */

#ifndef FOOD_H_
#define FOOD_H_
#include <string>

using namespace std;

class Food {
public:
    virtual string getName() = 0;

    virtual ~Food(){

    }
};


#endif /* FOOD_H_ */

/*
 * Biscuit.h
 *
 *  Created on: Mar 10, 2021
 *      Author: som
 */

#ifndef BISCUIT_H_
#define BISCUIT_H_

#include "Food.h"

class Biscuit: public Food {
public:
    Biscuit();
    string getName();
    ~Biscuit();
};

#endif /* BISCUIT_H_ */

/*
 * Biscuit.cpp
 *
 *  Created on: Mar 10, 2021
 *      Author: som
 */
#include <iostream>
#include "Biscuit.h"
using namespace std;


Biscuit::Biscuit() {
    // TODO Auto-generated constructor stub
    cout<<"Biscuit is made..."<<endl;

}

Biscuit::~Biscuit(){}


string Biscuit::getName(){
    return "It's a Biscuit";
}

/*
 * Chocolate.h
 *
 *  Created on: Mar 10, 2021
 *      Author: som
 */

#ifndef CHOCOLATE_H_
#define CHOCOLATE_H_

#include <iostream>
#include "Food.h"

class Chocolate: public Food {
public:
    Chocolate();
    virtual ~Chocolate();
    string getName();
};


#endif /* CHOCOLATE_H_ */

/*
 * Chocolate.cpp
 *
 *  Created on: Mar 10, 2021
 *      Author: som
 */

#include "Chocolate.h"


Chocolate::Chocolate() {
    // TODO Auto-generated constructor stub
    cout<<"Chocolate is made..."<<endl;

}

Chocolate::~Chocolate() {
    // TODO Auto-generated destructor stub
}

string Chocolate::getName(){
    return "It's a Chocolate";
}

/*
 * Factory.h
 *
 *  Created on: Mar 10, 2021
 *      Author: som
 */

#ifndef FACTORY_H_
#define FACTORY_H_

#include <pybind11/pybind11.h>
#include <iostream>
#include <string>

#include "Biscuit.h"
#include "Chocolate.h"

using namespace std;

class Factory{
public:
    static Factory* instance;
    static Factory* getInstance();

    Food* makeFood(const string& type);

private:
    Factory(){}

    // Delete copy constructor & assignment operator (Singleton pattern)
    Factory(const Factory&) = delete;
    Factory& operator=(const Factory&) = delete;
};
//Factory* Factory:: instance =  NULL;


#endif /* FACTORY_H_ */

/*
 * Factory.cpp
 *
 *  Created on: Jan 30, 2025
 *      Author: som
 */
#include "Factory.h"
Factory* Factory::instance = NULL;

Factory* Factory:: getInstance(){
        if(Factory::instance == NULL){
            Factory::instance = new Factory();
        }
        return Factory::instance;
    }

Food* Factory::makeFood(const string& type){
        if(type.compare("bi") == 0){
            return new Biscuit();
        }
        if(type.compare("ch") == 0){
            return new Chocolate();
        }

        return NULL;
    }

//binding.cpp
#include <pybind11/pybind11.h>
#include <memory>

#include "Food.h"
#include "Factory.h"
#include "Biscuit.h"
#include "Chocolate.h"

namespace py = pybind11;

PYBIND11_MODULE(libfoodfactory, m) {
    py::class_<Food, std::shared_ptr<Food>>(m, "Food")
        .def("get_name", &Food::getName);

    py::class_<Biscuit, Food, std::shared_ptr<Biscuit>>(m, "Biscuit")
        .def(py::init<>());

    py::class_<Chocolate, Food, std::shared_ptr<Chocolate>>(m, "Chocolate")
        .def(py::init<>());

    m.def("make_food", [](const std::string& type) -> std::shared_ptr<Food> {
        Food* f = Factory::getInstance()->makeFood(type);
        if (!f) throw std::runtime_error("Unknown food type");
        return std::shared_ptr<Food>(f);  // manage memory in Python
    });
}

# Here's the CMakeLists.txt
cmake_minimum_required(VERSION 3.10)

# Set some basic project attributes
project (FactoryPattern_pybind)

set(CMAKE_CXX_STANDARD 14)

# Set the prefix path to where pybind11 is installed,
# allowing CMake to find its configuration files.
# This assumes pybind11 was installed via pip.
list(APPEND CMAKE_PREFIX_PATH "/home/som/.local/lib/python3.8/site-packages")

# Find the pybind11 package. The REQUIRED keyword ensures
# the build fails if the package isn't found.
find_package(pybind11 REQUIRED)

# Add the shared library (MODULE is for Python extensions)
add_library(foodfactory MODULE
    Biscuit.cpp
    Chocolate.cpp
    Factory.cpp
    bindings.cpp
)

# Set include directories for your source files.
# No need to add pybind11's include path here, as it's
# handled by the pybind11::pybind11 target.
target_include_directories(foodfactory PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})

# Link the library to the pybind11 provided target.
# This ensures all necessary compiler flags and include paths
# from pybind11 are used.
target_link_libraries(foodfactory PRIVATE pybind11::pybind11)
Enter fullscreen mode Exit fullscreen mode

The story of pybind11 is not just about a technical tool ; it's about a paradigm shift. It showed that the barrier between C++ and Python could be lowered significantly, making it easier for developers to leverage the strengths of both languages. It's a story of how a well-designed, modern library can solve a long-standing problem and become the de-facto standard in its domain.

Enjoy...

Top comments (0)