Using pybind11¶
The package pybind11 is provides an elegant way to wrap C++ code for
Python, including automatic conversions for numpy arrays and the C++
Eigen linear algebra library. Used with the cppimport package,
this provides a very nice work flow for integrating C++ and Python:
- Edit C++ code
- Run Python code
pip install pybind11
pip install cppimport
Clone the Eigen library - no installation is required as Eigen is a header only library.
hg clone https://bitbucket.org/eigen/eigen/
Resources¶
`pybind11<http://pybind11.readthedocs.io/en/latest/>`__`cppimport<https://github.com/tbenthompson/cppimport>`__`Eigen<http://eigen.tuxfamily.org>`__
A first example of using pybind11¶
Create a new subdirectory - e.g. example1 and create the following 5
files in it:
funcs.hppfuncs.cppwrap.cppsetup.pytest_funcs.py
First write the C++ header and implementation files
In funcs.hpp
int add(int i, int j);
In funcs.cpp
int add(int i, int j) {
return i + j;
};
Next write the C++ wrapper code using pybind11 in wrap.cpp. The
arguments "i"_a=1, "j"_a=2 in the exported function definition tells
pybind11 to generate variables named i with default value 1 and
j with default value 2 for the add function.
#include <pybind11/pybind11.h>
#include "funcs.hpp"
namespace py = pybind11;
using namespace pybind11::literals;
PYBIND11_PLUGIN(wrap) {
py::module m("wrap", "pybind11 example plugin");
m.def("add", &add, "A function which adds two numbers",
"i"_a=1, "j"_a=2);
return m.ptr();
}
Finally, write the setup.py file to compile the extension module.
This is mostly boilerplate.
import os, sys
from distutils.core import setup, Extension
from distutils import sysconfig
cpp_args = ['-std=c++11', '-stdlib=libc++', '-mmacosx-version-min=10.7']
ext_modules = [
Extension(
'wrap',
['funcs.cpp', 'wrap.cpp'],
include_dirs=['pybind11/include'],
language='c++',
extra_compile_args = cpp_args,
),
]
setup(
name='wrap',
version='0.0.1',
author='Cliburn Chan',
author_email='cliburn.chan@duke.edu',
description='Example',
ext_modules=ext_modules,
)
Now build the extension module in the subdirectory with these files
python setup.py build_ext -i
And if you are successful, you should now see a new funcs.so
extension module. We can write a test_funcs.py file test the
extension module:
import wrap
def test_add():
assert(wrap.add(3, 4) == 7)
if __name__ == '__main__':
test_add()
And finally, running the test should not generate any error messages:
$ python test_funcs.py
Using cppimport¶
In the development stage, it can be distracting to have to repeatedly rebuild the extension module by running
python setup.py clean
python setup.py build_ext -i
every single time you modify the C++ code. The cppimport package
does this for you.
Create a new sub-directory exaample2 and copy the files
func.hpp, funcs.cpp and wrap.cpp from example1 over. For
the previous example, we just need to add some annotation (between
<% and %> delimiters) to the top of the wrap.cpp file
<%
cfg['compiler_args'] = ['-std=c++11', '-stdlib=libc++', '-mmacosx-version-min=10.7']
cfg['sources'] = ['funcs.cpp']
setup_pybind11(cfg)
%>
#include "funcs.hpp"
#include <pybind11/pybind11.h>
namespace py = pybind11;
PYBIND11_PLUGIN(wrap) {
py::module m("wrap", "pybind11 example plugin");
m.def("add", &add, "A function which adds two numbers");
return m.ptr();
}
and use cpppimport in the test_funcs.py file.
import cppimport
funcs = cppimport.imp("wrap")
def test_add():
assert(funcs.add(3, 4) == 7)
if __name__ == '__main__':
test_add()
You can now run
$ python test_funcs.py
without any need to manually build the extension module. Any updates
will be detected by cppimport and it will automatically trigger a
re-build.
Vectorizing functions for use with numpy arrays¶
Example showing how to vectorize a square function. Note that from
here on, we don’t bother to use separate header and implementation files
for these code snippets, and just write them together with the wrapping
code in a code.cpp file. This means that with cppimport, there
are only two files that we actually code for, a C++ code.cpp file
and a python test file.
<%
cfg['compiler_args'] = ['-std=c++11', '-stdlib=libc++', '-mmacosx-version-min=10.7']
setup_pybind11(cfg)
%>
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
double square(double x) {
return x * x;
}
PYBIND11_PLUGIN(code) {
pybind11::module m("code", "auto-compiled c++ extension");
m.def("square", py::vectorize(square));
return m.ptr();
}
and the test file
import cppimport
import numpy as np
code = cppimport.imp("code")
if __name__ == '__main__':
xs = np.random.rand(10)
print(xs)
print(code.square(xs))
ys = range(10)
print(code.square(ys))
Using numpy arrays as function arguments and return values¶
Example showing how to pass numpy arrays in and out of functions.
These numpy array arguments can either be generic py:array or
typed py:array_t<double>. The properties of the numpy array can
be obtained by calling its request method. This returns a struct
of the following form:
struct buffer_info {
void *ptr;
size_t itemsize;
std::string format;
int ndim;
std::vector<size_t> shape;
std::vector<size_t> strides;
};
Here is C++ code for two functions - the function twice shows how to
change a passed in numpy array in-place using pointers; the function
sum shows how to sum the elements of a numpy array. By taking
advantage of the information in buffer_info, the code will work for
arbitrary n-d arrays.
<%
cfg['compiler_args'] = ['-std=c++11', '-stdlib=libc++', '-mmacosx-version-min=10.7']
setup_pybind11(cfg)
%>
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
// Passing in an array of doubles
void twice(py::array_t<double> xs) {
py::buffer_info info = xs.request();
auto ptr = static_cast<double *>(info.ptr);
int n = 1;
for (auto r: info.shape) {
n *= r;
}
for (int i = 0; i <n; i++) {
*ptr++ *= 2;
}
}
// Passing in a generic array
double sum(py::array xs) {
py::buffer_info info = xs.request();
auto ptr = static_cast<double *>(info.ptr);
int n = 1;
for (auto r: info.shape) {
n *= r;
}
double s = 0.0;
for (int i = 0; i <n; i++) {
s += *ptr++;
}
return s;
}
PYBIND11_PLUGIN(code) {
pybind11::module m("code", "auto-compiled c++ extension");
m.def("sum", &sum);
m.def("twice", &twice);
return m.ptr();
}
and the test code
import cppimport
import numpy as np
code = cppimport.imp("code")
if __name__ == '__main__':
xs = np.arange(12).reshape(3,4).astype('float')
print(xs)
print("np :", xs.sum())
print("cpp:", code.sum(xs))
print()
code.twice(xs)
print(xs)
More on working with numpy arrays¶
This example shows how to use array access for numpy arrays within
the C++ function. It is taken from the pybind11 documentation, but
fixes a small bug in the official version. As noted in the
documentation, the function would be more easily coded using
py::vectorize.
<%
cfg['compiler_args'] = ['-std=c++11', '-stdlib=libc++', '-mmacosx-version-min=10.7']
setup_pybind11(cfg)
%>
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
py::array_t<double> add_arrays(py::array_t<double> input1, py::array_t<double> input2) {
auto buf1 = input1.request(), buf2 = input2.request();
if (buf1.ndim != 1 || buf2.ndim != 1)
throw std::runtime_error("Number of dimensions must be one");
if (buf1.shape[0] != buf2.shape[0])
throw std::runtime_error("Input shapes must match");
auto result = py::array(py::buffer_info(
nullptr, /* Pointer to data (nullptr -> ask NumPy to allocate!) */
sizeof(double), /* Size of one item */
py::format_descriptor<double>::value, /* Buffer format */
buf1.ndim, /* How many dimensions? */
{ buf1.shape[0] }, /* Number of elements for each dimension */
{ sizeof(double) } /* Strides for each dimension */
));
auto buf3 = result.request();
double *ptr1 = (double *) buf1.ptr,
*ptr2 = (double *) buf2.ptr,
*ptr3 = (double *) buf3.ptr;
for (size_t idx = 0; idx < buf1.shape[0]; idx++)
ptr3[idx] = ptr1[idx] + ptr2[idx];
return result;
}
PYBIND11_PLUGIN(code) {
py::module m("code");
m.def("add_arrays", &add_arrays, "Add two NumPy arrays");
return m.ptr();
}
with test code
import cppimport
import numpy as np
code = cppimport.imp("code")
if __name__ == '__main__':
xs = np.arange(12)
print(xs)
print(code.add_arrays(xs, xs))
Using the C++ eigen library to calculate matrix inverse and determinant¶
Example showing how Eigen vectors and matrices can be passed in and
out of C++ functions. Note that Eigen arrays are automatically
converted to/from numpy arrays simply by including the
pybind/eigen.h header. Because of this, it is probably simplest in
most cases to work with Eigen vectors and matrices rather than
py::buffer or py::array where py::vectorize is insufficient.
<%
cfg['compiler_args'] = ['-std=c++11', '-stdlib=libc++', '-mmacosx-version-min=10.7']
cfg['include_dirs'] = ['/Users/cliburn/hg/eigen']
setup_pybind11(cfg)
%>
#include <pybind11/pybind11.h>
#include <pybind11/eigen.h>
#include <Eigen/LU>
namespace py = pybind11;
// convenient matrix indexing comes for free
double get(Eigen::MatrixXd xs, int i, int j) {
return xs(i, j);
}
// takes numpy array as input and returns double
double det(Eigen::MatrixXd xs) {
return xs.determinant();
}
// takes numpy array as input and returns another numpy array
Eigen::MatrixXd inv(Eigen::MatrixXd xs) {
return xs.inverse();
}
PYBIND11_PLUGIN(code) {
pybind11::module m("code", "auto-compiled c++ extension");
m.def("inv", &inv);
m.def("det", &det);
return m.ptr();
}
and test code
import cppimport
import numpy as np
code = cppimport.imp("code")
if __name__ == '__main__':
A = np.array([[1,2,1],
[2,1,0],
[-1,1,2]])
print(A)
print(code.det(A))
print(code.inv(A))
Using pybind11 with openmp¶
Here is a standard example of using OpenMP to integrate the value of
\(\pi\) written using pybind11.
<%
cfg['compiler_args'] = ['-std=c++11', '-fopenmp']
cfg['linker_args'] = ['-fopenmp']
setup_pybind11(cfg)
%>
#include <omp.h>
#include <pybind11/pybind11.h>
#include <pybind11/numpy.h>
namespace py = pybind11;
double calc_pi(int n) {
/* Acquire GIL before calling Python code */
py::gil_scoped_acquire acquire;
int i;
double step = 1.0/n;
double s = 0;
#pragma omp parallel
{
double x;
#pragma omp for reduction(+:s)
for (i=0; i<n; i++) {
x = (i+0.5) * step;
s += 4.0/(1 + x*x);
}
}
return step * s;
};
PYBIND11_PLUGIN(code) {
pybind11::module m("code", "auto-compiled c++ extension");
m.def("calc_pi", [](int n) {
/* Release GIL before calling into C++ code */
py::gil_scoped_release release;
return calc_pi(n);
});
return m.ptr();
}
And here is the test code.
import cppimport
import numpy as np
code = cppimport.imp("code")
if __name__ == '__main__':
n = int(1e9)
print(code.calc_pi(n))
In [ ]: