Probing External C++ Functions with a Cython Wrapper in Jupyter
This is a practical demonstration of how cython can be used to wrap a C++ function. This has already been discussed here, as well as here.
But there is a problem with the methon discussed therein: it requires the user
to include the C++ source files (which is not difficult if you're using
distutils from a setup.py), which is currently not supported by the
%cython magic. A workaround is documented here, but I
feel this is unsuitable for two reasons:
- It still requires access to the C++ sources (which can be unwieldy for large projects)
- It uses jupyter to generate a whole lot of termporary (garbade) files, which can clutter up an applications/results dir
Especially point 1 gives me pause, because I'm looking for a way to interact with parts of a larger project (without having to rebuild half of it). Hence we're going to try something else here...
Setup
We want to interact with an external C++ file: addone.cpp which is part of
another project:
.
├── External\ Wrapper.ipynb
└── main_proj
├── Makefile
├── addone.cpp
├── addone.h
└── main.cpp
The main project (creatively main_proj/main.cpp) calls add_one from
addone.cpp. This object is
Let's imagine that we've only got addone.h and addone.o. Then we can still
build a cython wrapper in a %cython magic!
Convert .o files to Linkable Libraries
First we convert addone.o to libaddone.so. Note the name change: adding
-lX to the linker args, makes the linker look for libX.so in the linker's
search path (specified by the -L argument):
clang++ -fpic -shared -Wl,-all_load addone.o -o libaddone.so
Using the Makefile
The Makefile contains a helpful target: libaddone.so which does the work
above for us ;):
CC=clang++
SO=-fpic -shared -Wl,-all_load
libaddone.so: addone.o
$(CC) $(SO) $< -o $@
Build a Cython Wrapper
For example, our add_one function has the signature: double add_one(double)
and is defined in addone.h so the cython wrapper code is simply:
cdef extern from "addone.h":
cdef float add_one(float)
followed by a python function to wrap the cython cdef function if you want to
access it throughout the rest of the notebook.
Use the right %%cython magic arguments
We need to tell cython to use C++ mode: --cplus, and we need to tell it where
to look for libraries and which ones to link. Remember that we've converted our
C++ object file into a shared library. In our case, this library lives in the
./main_proj/ directory (relative to this notebook), so the compiler arguments
are pretty simple -I./main_proj -L./main_proj -laddone. All of these
parameters are documented here.
Dyncamic Linkers Beware
If you're using a dynamic linker (for example macOS' dyld) you'll need to
include ./main_proj in the linker's search path. For example, if
we're using dyld, launch jupyter using:
env DYLD_FALLBACK_LIBRARY_PATH=./main_proj jupyter lab
And we're done!
Example External C++ Function
Now we need an example C++ function contained in an external code project
(main_proj). This example is pretty dumb, but it serves the purpose of an
external function. The contents of addone.cpp is:
#include <iostream>
double add_one(double a) {
std::cout << "Adding 1 to: " << a << std::endl;
return a + 1;
}
with the header file: addone.h:
#ifndef __ADDONE_H__
#define __ADDONE_H__
double add_one(double a);
#endif
An Interactive Wrapper in Jupyter
Having put all of this together, we've got ourselves an interactive wrapper:
%%cython --cplus -I./main_proj -L./main_proj -f -laddone
#cython wrapper code
cdef extern from "addone.h":
cdef float add_one(float)
# python wrapper function for c-function above
def py_add_one(x):
return add_one(x)
We now have a python wrapper for our c-function (which happens to be wrapped in
a cython cdef function). Running:
py_add_one(10)
should output:
Adding 1 to: 10
in the stdout.
Resources
- A demo containing an jupyter notebook, as well as the external C++ code outlined above.