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/
A first example of using pybind11
¶
Create a new subdirectory - e.g. example1
and create the following 5
files in it:
funcs.hpp
funcs.cpp
wrap.cpp
setup.py
test_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
<%
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)
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 numpy
arrays simply by including the pybind/eigen.h
header.
<%
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;
Eigen::MatrixXd inv(Eigen::MatrixXd xs) {
return xs.inverse();
}
double det(Eigen::MatrixXd xs) {
return xs.determinant();
}
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))