Python C++ Bindings

Calling C++ from Python

Python C++ Bindings

As a developer writing high-performance C++ libraries, I often find myself needing to integrate them into Python for easier scripting and flexibility. Given the speed and efficiency of C++, it’s crucial for me to maintain that performance while making the functionality accessible from Python.

I frequently turn to pybind11 for this task. It’s a lightweight solution that, once set up, is simple to use and doesn’t add unnecessary overhead.

I want to share some of the tips and tricks I’ve picked up along the way, and show how you can leverage pybind11 to bridge the gap between C++ and Python in your own projects.

C/C++ Function

Calling a function through pybind11 is straightforward. Once your function is defined, especially if marked with extern "C" for compatibility, the binding is simple. Here’s an example of how to expose a function to Python using pybind11:


#include "binding_function.h"

int add(int i, int j)
{
    retun i + j;
}

PYBIND11_MODULE(pybind11_iface, m)
{
    // Expose the 'add' function to Python
    m.def("add", &add, "A function which adds two numbers", pybind11::arg("i") = 1, pybind11::arg("j") = 1);
}

In this case, the function add is a simple C++ function that takes two arguments and adds them together. By using pybind11::def, the function becomes callable directly from Python. The pybind11::arg declarations allow you to specify default arguments, making it even easier to call from Python. Once bound, the function behaves as a regular Python function while keeping the performance benefits of the underlying C++ implementation.

C++ Class

Calling a C++ class from Python is also quite straightforward when using pybind11. If your class has public members and methods, they can be easily exposed to Python. Here’s an example of how to bind a class with a function to Python:


class CppClass
{
  public:
    CppClass();
    ~CppClass();
    uint32_t class_function();
};

PYBIND11_MODULE(pybind11_iface, m)
{
    // Binding the C++ class to Python
    pybind11::class_<CppClass>(m, "CppClass")
        .def(pybind11::init<>())  // Bind constructor
        .def("class_function", &CppClass::class_function);  // Bind method
}

In this example, CppClass is exposed to Python with its constructor and the class_function method. Once bound, you can create an instance of CppClass and call its methods directly from Python. This allows you to seamlessly interact with your C++ logic within Python while keeping the full speed of C++. I will cover more complex topics like handling std::vector later.

PyBind11 Class

pybind11 offers the ability to define C++ classes that can manipulate Python objects, for example, exposing numpy.array memory that can be overwritten in C++. However, since numpy arrays and std::vector are not directly compatible, it’s necessary to copy data back and forth between them. Below is an example where I define a class in C++ that inherits from CppClass and handles numpy.array manipulation.


class PyClass : public CppClass
{
  public:
    using CppClass::CppClass;
    pybind11::memoryview numbers();
    void UpdateVectorNumpy(py::array_t<double> array);
};

pybind11::memoryview PyClass::numbers()
{
    return pybind11::memoryview::from_memory(n, sizeof(uint32_t) * 4, false);
}

void PyClass::UpdateVectorNumpy(py::array_t<double> array)
{
    py::buffer_info buf = array.request();
    double* ptr         = static_cast<double*>(buf.ptr); // Get the pointer to numpy array memory
    size_t size         = static_cast<size_t>(buf.size);

    // Use the existing C++ function but operate directly on the memory
    std::vector<double> vec(ptr, ptr + size); // Create a vector from numpy memory

    // Call the original C++ function that modifies the vector
    CppClass::UpdateVector(vec);

    // Directly copy the modified vector back into the numpy array (in-place update)
    for (size_t i = 0; i < size; ++i)
    {
        ptr[i] = vec[i]; // Reflect changes in the numpy array
    }
}

PYBIND11_MODULE(pybind11_iface, m)
{
    pybind11::class_<PyClass>(m, "PyClass")
        .def(pybind11::init<>())
        .def("class_function", &PyClass::class_function)
        .def("numbers", &PyClass::numbers)
        .def("update_vector", &PyClass::UpdateVector)
        .def("update_vector_numpy", &PyClass::UpdateVectorNumpy);
}

In this example, the PyClass class inherits from CppClass and exposes the numbers and UpdateVectorNumpy functions to Python. The numbers function returns a memoryview from a C++ array, and the UpdateVectorNumpy function allows modifying a numpy.array directly from C++, reflecting the changes back into the Python side.

STL Containers Manipulation

One common issue when working with std::vector in C++ and numpy.array in Python is the memory overhead caused by copying data back and forth between the two. When it’s necessary to return a Python object from C++ without wasting memory, you can define a direct binding to the C++ std::vector<double>. This allows you to allocate the vector directly from Python, manipulate it in C++, and avoid unnecessary data copies.

Below is an example of how to bind std::vector<double> and expose it to Python:


void CppClass2::UpdateVector2(std::vector<double>& vv)
{
    for (size_t i = 0; i < vv.size(); ++i) {
        vv[i] = (static_cast<double>(i) + 1) * 1.1;
    }
}

#include <pybind11/stl.h>
#include <pybind11/stl_bind.h>

PYBIND11_MAKE_OPAQUE(std::vector<double>)

PYBIND11_MODULE(pybind11_iface, m)
{
    pybind11::class_<CppClass2>(m, "CppClass2")
        .def(pybind11::init<>())
        .def_readwrite("vv_", &CppClass2::vv_)  // Expose vv_ as read/write
        .def("set_vv", [](CppClass2& self, pybind11::array_t<double> arr) {
            // Check that the numpy array is contiguous
            pybind11::buffer_info buf = arr.request();
            if (buf.ndim != 1) throw std::runtime_error("Expected a 1-dimensional array");

            // Convert numpy array to std::vector<double>
            self.vv_ = std::vector<double>(static_cast<double*>(buf.ptr), static_cast<double*>(buf.ptr) + buf.size);
        })
        .def("update_vector2", &CppClass2::UpdateVector2);

    // Bind std::vector<double> directly to Python as VectorDouble
    pybind11::bind_vector<std::vector<double>>(m, "VectorDouble");
}

With this setup, you can now create and manipulate a std::vector<double> directly from Python:


import py_iface

# Create a VectorDouble from Python
v = py_iface.VectorDouble([0.0, 0.0, 0.0])

# Assign the vector to the class
cpp_obj = py_iface.CppClass2()
cpp_obj.vv_ = v

# Modify the vector from C++ without copying
cpp_obj.update_vector2()

# Access the updated vector from Python
print(cpp_obj.vv_)  # Output: [1.1, 2.2, 3.3]

By directly binding std::vector<double>, you avoid the memory overhead of copying data between numpy and std::vector, and you can safely manipulate the data from both Python and C++.

Complete Example

A full example to show the different way of interfacing Python with C/C++ libraries.

C / C++ function

Header file binding_function.h.


#ifndef _CPP_FUNCTION_H_6B07A2839E3A49C186BE43F44A0BD10F_
#define _CPP_FUNCTION_H_6B07A2839E3A49C186BE43F44A0BD10F_

#include <iostream>

extern "C" int add(int i = 1, int j = 1);

#endif

Source file binding_function.cpp.


#include "binding_function.h"

int add(int i, int j)
{
    return i + j;
}

C++ class

Header file cpp_class.h.


#ifndef _CPP_CLASS_H_9B674BA51D7C4A1B996665618F54FC51_
#define _CPP_CLASS_H_9B674BA51D7C4A1B996665618F54FC51_

#include <iostream>
#include <vector>

class CppClass
{
  public:
    CppClass();
    ~CppClass();
    uint32_t class_function();
    void UpdateVector(std::vector<double>& vv);

  protected:
    int n[3]{1, 2, 3};
    static constexpr uint32_t _n2 = 4;
};

class CppClass2
{
  public:
    CppClass2();
    ~CppClass2();
    std::vector<double> vv_;
    void UpdateVector2(std::vector<double>& vv);
};

#endif

Source file cpp_class.cpp.


#include "cpp_class.h"

CppClass::CppClass() {};

CppClass::~CppClass() {};

uint32_t CppClass::class_function()
{
    std::cout << "Class Member Function Called" << '\n';
    return _n2;
};

void CppClass::UpdateVector(std::vector<double>& vv)
{
    for (size_t i = 0; i < vv.size(); ++i) { vv[i] = (static_cast<double>(i) + 1) * 1.1; }
}

CppClass2::CppClass2() {};

CppClass2::~CppClass2() {};

void CppClass2::UpdateVector2(std::vector<double>& vv)
{
    for (size_t i = 0; i < vv.size(); ++i) { vv[i] = (static_cast<double>(i) + 1) * 1.1; }
}

PyBind11 C++ class

Header file pybinding_class.h.


#ifndef _PYBINDING_CLASS_H_553A1609016E4FFE9D58E317669E67CB1_
#define _PYBINDING_CLASS_H_553A1609016E4FFE9D58E317669E67CB1_

#include <vector>
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>
#include "binding_class.h"

namespace py = pybind11;

class PyClass : public CppClass
{
  public:
    using CppClass::CppClass;
    pybind11::memoryview numbers();
    void UpdateVectorNumpy(py::array_t<double> array);
};

#endif

Source file pybinding_class.cpp.


#include "pybinding_class.hpp"

pybind11::memoryview PyClass::numbers()
{
    return pybind11::memoryview::from_memory(n, sizeof(uint32_t) * 4, false);
};

void PyClass::UpdateVectorNumpy(py::array_t<double> array)
{
    py::buffer_info buf = array.request();
    double* ptr         = static_cast<double*>(buf.ptr); // Get the pointer to numpy array memory
    size_t size         = static_cast<size_t>(buf.size);

    // Use the existing C++ function but operate directly on the memory
    std::vector<double> vec(ptr, ptr + size); // Create a vector from numpy memory

    // Call the original C++ function that modifies the vector
    CppClass::UpdateVector(vec);

    // Directly copy the modified vector back into the numpy array (in-place update)
    for (size_t i = 0; i < size; ++i)
    {
        ptr[i] = vec[i]; // Reflect changes in the numpy array
    }
};

PyBind11 interface

Source file py_iface.cpp.


#include <iostream>
#include <vector>
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/stl_bind.h>

#include "binding_function.h"
#include "pybinding_class.h"

PYBIND11_MAKE_OPAQUE(std::vector<double>)

PYBIND11_MODULE(pybind11_iface, m)
{
    m.doc()                     = "pybind11 example plugin";

    m.attr("test_integer")      = 1;
    pybind11::object one_string = pybind11::cast("This is a string");
    m.attr("test_string")       = one_string;

    m.def("add", &add, "A function which adds two numbers", pybind11::arg("i") = 1, pybind11::arg("j") = 1);

    pybind11::class_<PyClass>(m, "PyClass")
        .def(pybind11::init<>())
        .def("class_function", &PyClass::class_function)
        .def("numbers", &PyClass::numbers)
        .def("update_vector", &PyClass::UpdateVector)
        .def("update_vector_numpy", &PyClass::UpdateVectorNumpy);

    pybind11::class_<CppClass>(m, "CppClass")
        .def(pybind11::init<>())
        .def("class_function", &CppClass::class_function)
        .def("update_vector", &CppClass::UpdateVector);

    pybind11::class_<CppClass2>(m, "CppClass2")
        .def(pybind11::init<>())
        .def_readwrite("vv_", &CppClass2::vv_) // Expose vv_ as read/write
        .def("set_vv", [](CppClass2& self, pybind11::array_t<double> arr) {
        // Check that the numpy array is contiguous
        pybind11::buffer_info buf = arr.request();
        if (buf.ndim != 1) throw std::runtime_error("Expected a 1-dimensional array");

        // Convert numpy array to std::vector<double>
        self.vv_ = std::vector<double>(static_cast<double*>(buf.ptr), static_cast<double*>(buf.ptr) + buf.size);
    }).def("update_vector2", &CppClass2::UpdateVector2);

    pybind11::bind_vector<std::vector<double>>(m, "VectorDouble");
}

CMakeList.txt

The relevant compilation part of CMakeList.txt.


cmake_minimum_required(VERSION 3.13.4)
project(pybind11_iface)
set (PROJECT_VERSION "1.0")
project(${PROJECT_NAME} VERSION ${PROJECT_VERSION})
# Other C++ or CMake flags

if(WIN32)
  # On Windows, the pybind11 path is different
  set(CMAKE_PREFIX_PATH "$ENV{VIRTUAL_ENV}/Lib/site-packages/pybind11/share/cmake/pybind11")
else()
  # On macOS/Linux, use Python to detect the Python version and set the path
  set(PYTHON_EXECUTABLE "python3")

  # Get the Python version from the current virtual environment
  execute_process(
    COMMAND ${PYTHON_EXECUTABLE} -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"
    OUTPUT_VARIABLE PYTHON_VERSION
    OUTPUT_STRIP_TRAILING_WHITESPACE
  )
  # Set the CMAKE_PREFIX_PATH using the detected Python version
  set(CMAKE_PREFIX_PATH "$ENV{VIRTUAL_ENV}/lib/python${PYTHON_VERSION}/site-packages/pybind11/share/cmake/pybind11")
endif()

set(PYBIND11_FINDPYTHON ON)
find_package(pybind11 REQUIRED)
pybind11_add_module(pybind11_iface pybind11_iface.cpp pybinding_class.cpp binding_class.cpp binding_function.cpp)

Python caller

Source file py_bindings.py.


import pybind11_iface as py_iface
import numpy as np
import struct

print('call a function:', py_iface.add(1, 2))

t = py_iface.PyClass()

print('int from a cpp member function:', t.class_function(), " - expected 4")
mm = t.numbers().tobytes()
_d = struct.unpack('i', mm[4:8])
print("unpack n[1]:", _d[0], " - expected 2")

vec = np.zeros(6)
t.update_vector_numpy(vec)
print (f"vec after update with dedicated interface {vec} [working]")

t2 = py_iface.CppClass()
vec = py_iface.VectorDouble([0., 0., 0.])
print (f"vec before update {vec}")
t2.update_vector(vec)
print (f"vec after update with C++ vector<double> {vec} [working]")

t3 = py_iface.CppClass2()
t3.set_vv(np.zeros(6))
print (f"vec before update {t3.vv_}")
t3.update_vector2(t3.vv_)
print (f"vec after update with C++ vector<double> {t3.vv_} [working]")

Results from python py_bindings.py.


python py_bindings.py

call a function: 3
Class Member Function Called
int from a cpp member function: 4  - expected 4
unpack n[1]: 2  - expected 2
vec after update with dedicated interface [1.1 2.2 3.3 4.4 5.5 6.6]
vec before update VectorDouble[0, 0, 0]
vec after update with C++ vector<double> VectorDouble[1.1, 2.2, 3.3]
vec before update VectorDouble[0, 0, 0, 0, 0, 0]
vec after update with C++ vector<double> VectorDouble[1.1, 2.2, 3.3, 4.4, 5.5, 6.6]

Go to the top of the page