Using Python with C++. Part3.

Stefano C
4 min readMar 10, 2023

--

Using Python and C++ together using Pybind library.

Summary

In this episode we are going to use numpy arrays and python lists to handle data that will passed to some c++ functions to do some additional calculations.

Process some data

Let’s take as example a function ProcessSomeData that calculates the square of an input array of size N (pointed by pIn) and stores the result in the output buffer (pointed by pOut).

void ProcessSomeData(const float* pIn, float* pOut, const size_t N)
{
for (size_t i = 0; i < N; i++)
{
pOut[i] = pIn[i] * pIn[i];
}
}

Naive implementation

As we did for the other examples, we would expect to write something like this:

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

m.def("process_some_data", &ProcessSomeData);
}

and let PyBind11 do the magic. In Python, using numpy arrays, i will write:

import example
import numpy as np

input_array = np.array([1,2,3,4], dtype=np.float32)
output_array = np.array([0,0,0,0], dtype=np.float32)

samples = 4

example.process_some_data(input_array, output_array, samples)

Unfortunately this will throw an error:

example.process_some_data(a, b, samples)
TypeError: process_some_data(): incompatible function arguments. The following argument types are supported:
1. (arg0: float, arg1: float, arg2: int) -> None

Invoked with: array([1., 2., 3., 4.], dtype=float32), array([0., 0., 0., 0.], dtype=float32), 4

Using py::array_t

Fortunately pybind11 has a wrapper py::array_t<T> that makes possible to cast a NumPy array into a c style array. So, let’s modify the function m.def(“process_some_data”, &ProcessSomeData) with a better one (don’t forget to #include <pybind11/numpy.h>) :

 

m.def("process_some_data", [](const py::array_t<float> &input, py::array_t<float>& out)
{
py::buffer_info input_buffer = input.request();
py::buffer_info out_buffer = out.request();

auto *ptr1 = static_cast<float *>(input_buffer.ptr);
auto *ptr2 = static_cast<float *>(out_buffer.ptr);
auto samples = input_buffer.shape[0];

ProcessSomeData(ptr1, ptr2, samples);
});

Here i have declared a lambda which takes as arguments two py::array_t<float> and calling .request() i can access to the py::buffer_info structure that gives me a direct access to the pointer (of type void) which i cast to a pointer of type float (ptr1, ptr2). The call .shape[0] gives me the length of the array. Here a detailed view of buffer_info struct:

struct buffer_info {
void *ptr = nullptr; // Pointer to the underlying storage
ssize_t itemsize = 0; // Size of individual items in bytes
ssize_t size = 0; // Total number of entries
std::string format; // format
ssize_t ndim = 0; // Number of dimensions
std::vector<ssize_t> shape; // Shape of the tensor (1 entry per dim.)
std::vector<ssize_t> strides; // strides
bool readonly = false;
....

Let’s run again the python script. This time is working as expected:

import example
import numpy as np

samples = 4
input_array = np.array([1,2,3,4], dtype=np.float32)
output_array = np.array([0,0,0,0], dtype=np.float32)
example.process_some_data(input_array, output_array)

print(input_array)
print(output_array)

[1. 2. 3. 4.]
[1. 4. 9. 16.]

A short break…

The Office — season 6, episode Gossip. Andy says: Here it is: truck, to refrigerators, to dumpster, 360 onto the pallets, backflip gainer to the trash can. Do you have sometime the sensation that going from python to c++ and backward is a bit like Parkour? :)

Using STL containers

Consider now a function that takes two std::vector<float> as arguments passed by reference and write the corresponding binding (remember to #include <pybind11/stl.h> file otherwise you’ll get an error).

void ProcessSomeDataII(const std::vector<float>&input, std::vector<float>& output)
{
for (size_t i = 0; i < input.size(); i++)
{
output[i] = input[i] * input[i];
}
}


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

m.def("process_some_dataII", ProcessSomeDataII);
}

Let’s try to launch again the python script, this time using two python lists input_array and output_array:


input_array = [1,2,3,4]
output_array = [0,0,0,0]

example.process_some_dataII(input_array, output_array)

print(input_array)
print(output_array)

[1, 2, 3, 4]
[0, 0, 0, 0]

We expected to see b = [1, 4, 9, 16] so there’s something that is not working fine. From the pybind11 manual we read: […. pybind11 will construct a new std::vector<int> and copy each element from the Python list. The same thing happens in the other direction: a new list is made to match the value returned from C++…].

So, here we found the issue: std::vector are copied and not passed by reference. In order to do make this happens we have to override the automatic conversion with a custom wrapper (i.e. the above-mentioned approach 1). This requires some manual effort and more details are available in the Making opaque types section.

A workaround

Before talking about Opaque Types a first workaround is to rewrite the function in order to return a std::vector<float>. This means that we are facing two copy operations:

  • The first copy from the input_array to std::vector<float>input
  • The second copy from std::vector<float>output to output_array
std::vector<float> ProcessSomeDataIII(const std::vector<float>&input)
{
std::vector<float>output;
output.resize(input.size());
for (size_t i = 0; i < input.size(); i++)
{
output[i] = input[i] * input[i];
}
return output;
}


m.def("process_some_dataIII", ProcessSomeDataIII);

In python, calling the new function will work as expected:

input_array = [1,2,3,4]
output_array = [0,0,0,0]

output_array = example.process_some_dataIII(input_array)

print(input_array)
print(output_array)

[1, 2, 3, 4]
[1.0, 4.0, 9.0, 16.0]

As said, this works but remember that copy operations are expensive and lead to inefficient code especially for large arrays.

At this point we would like to find another better strategy to manage python lists and std::vector objects. The answer is making the types opaque in order to enable the passing by reference operations but we will talk about this in the next story. So that’s all for now, next time we will see other interesting examples.

So, if you like this content, please consider to follow me on Medium :) Happy reading, Stefano.

--

--

Stefano C

Master Degree in Physics, work in audio industry. Passion for C++, python, audio, robotics, electronics and programming. Modena, italy.