I want to write C++ code and call it from Python without doing any extra work.
In [1]:
%%file Tree.hpp
#ifndef FFIG_DEMOS_TREE_H
#define FFIG_DEMOS_TREE_H
#include <memory>
class Tree {
std::unique_ptr<Tree> left_;
std::unique_ptr<Tree> right_;
int data_ = 0;
public:
Tree(int children) {
if(children <=0) return;
left_ = std::make_unique<Tree>(children-1);
right_ = std::make_unique<Tree>(children-1);
}
Tree* left() { return left_.get(); }
Tree* right() { return right_.get(); }
int data() const { return data_; }
void set_data(int x) { data_ = x; }
};
#endif // FFIG_DEMOS_TREE_H
Let's compile this to ensure we've not made any mistakes.
In [2]:
%%sh
clang++ -std=c++14 -fsyntax-only Tree.hpp
In [3]:
%%file Tree_c.h
#ifndef FFIG_DEMOS_TREE_C_H
#define FFIG_DEMOS_TREE_C_H
#define C_API extern "C" __attribute__((visibility("default")))
struct CTree_t;
typedef CTree_t* CTree;
C_API CTree Tree_create(int children);
C_API void Tree_dispose(CTree t);
C_API CTree Tree_left(CTree t);
C_API CTree Tree_right(CTree t);
C_API int Tree_data(CTree t);
C_API void Tree_set_data(CTree t, int x);
#endif // FFIG_DEMOS_TREE_C_H
In [4]:
%%file Tree_c.cpp
#include "Tree_c.h"
#include "Tree.hpp"
CTree Tree_create(int children) {
auto tree = std::make_unique<Tree>(children);
return reinterpret_cast<CTree>(tree.release());
}
void Tree_dispose(CTree t) {
auto* tree = reinterpret_cast<Tree*>(t);
delete tree;
}
CTree Tree_left(CTree t) {
auto* tree = reinterpret_cast<Tree*>(t);
return reinterpret_cast<CTree>(tree->left());
}
CTree Tree_right(CTree t) {
auto* tree = reinterpret_cast<Tree*>(t);
return reinterpret_cast<CTree>(tree->right());
}
int Tree_data(CTree t) {
auto* tree = reinterpret_cast<Tree*>(t);
return tree->data();
}
void Tree_set_data(CTree t, int x) {
auto* tree = reinterpret_cast<Tree*>(t);
tree->set_data(x);
}
We can build this to create a shared library with our C-API symbols exposed.
In [5]:
%%file CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
set(CMAKE_CXX_STANDARD 14)
add_compile_options(-fvisibility=hidden)
add_library(Tree_c SHARED Tree_c.cpp)
In [6]:
%%sh
rm -rf CMakeFiles/
rm CMakeCache.txt || echo
cmake . -GNinja
cmake --build .
strip -x libTree_c.dylib
In [7]:
%%sh
nm -U libTree_c.dylib
We can use Python's ctypes module to interact with the C shared-library.
In [8]:
import ctypes
ctypes.c_object_p = ctypes.POINTER(ctypes.c_void_p)
tree_lib = ctypes.cdll.LoadLibrary("libTree_c.dylib")
We need to tell ctypes about the arguments and return types of the functions.
By default ctypes assumes functions take no arguments and return an integer.
In [9]:
tree_lib.Tree_create.argtypes = [ctypes.c_int]
tree_lib.Tree_create.restype = ctypes.c_object_p
tree_lib.Tree_dispose.argtypes = [ctypes.c_object_p]
tree_lib.Tree_dispose.restype = None
tree_lib.Tree_left.argtypes = [ctypes.c_object_p]
tree_lib.Tree_left.restype = ctypes.c_object_p
tree_lib.Tree_right.argtypes = [ctypes.c_object_p]
tree_lib.Tree_right.restype = ctypes.c_object_p
tree_lib.Tree_data.argtypes = [ctypes.c_object_p]
tree_lib.Tree_data.restype = ctypes.c_int
tree_lib.Tree_set_data.argtypes = [ctypes.c_object_p, ctypes.c_int]
tree_lib.Tree_set_data.restype = None
We'll leave the string-related methods for now, interop there is not so easy.
Let's see what we can do with the fledgling Python API.
In [10]:
root = tree_lib.Tree_create(2)
In [11]:
root
Out[11]:
In [12]:
tree_lib.Tree_data(root)
Out[12]:
In [13]:
tree_lib.Tree_set_data(root, 42)
tree_lib.Tree_data(root)
Out[13]:
In [14]:
tree_lib.Tree_dispose(root)
So far, so not-very-Pythonic.
We want classes!
In [15]:
class Tree(object):
def __init__(self, children=None, _p=None):
if _p:
self._ptr = _p
self._owner = False
else:
self._ptr = tree_lib.Tree_create(children)
self._owner = True
def __del__(self):
if self._owner:
tree_lib.Tree_dispose(self._ptr)
def __repr__(self):
return "<Tree data:{}>".format(self.data)
@property
def left(self):
p = tree_lib.Tree_left(self._ptr)
if not p:
return None
return Tree(_p=p)
@property
def right(self):
p = tree_lib.Tree_right(self._ptr)
if not p:
return None
return Tree(_p=p)
@property
def data(self):
return tree_lib.Tree_data(self._ptr)
@data.setter
def data(self, x):
tree_lib.Tree_set_data(self._ptr, x)
In [16]:
t = Tree(2)
t
Out[16]:
In [18]:
t.data = 42
t
Out[18]:
In [19]:
t.left
Out[19]:
In [20]:
t.left.data = 6
In [21]:
t.left
Out[21]:
This looks good but we our crude attempts at memory management will fail if we start working with temporaries.
In [22]:
# This kills the kernel
#left = Tree(3).left.left
#left.data
Our Python classes don't know enough about the underlying C++ to do the memory management.
Our C-API implementation needs a re-think.
We can imbue the pointers passed across the API boundary with object lifetime detail
We can use the aliasing constructor of shared_ptr to keep objects alive while any subobject is exposed across the API boundary.
In [23]:
%%file Tree_2_c.cpp
#include <memory>
#include "Tree_c.h"
#include "Tree.hpp"
using Tree_ptr = std::shared_ptr<Tree>*;
CTree Tree_create(int children) {
auto tree = std::make_unique<Tree>(children);
Tree_ptr p = new std::shared_ptr<Tree>(tree.release());
return reinterpret_cast<CTree>(p);
}
void Tree_dispose(CTree t) {
delete reinterpret_cast<Tree_ptr>(t);
}
CTree Tree_left(CTree t) {
const auto& tree = *reinterpret_cast<Tree_ptr>(t);
auto left = tree->left();
if(!left)
return nullptr;
Tree_ptr p = new std::shared_ptr<Tree>(tree, left);
return reinterpret_cast<CTree>(p);
}
CTree Tree_right(CTree t) {
const auto& tree = *reinterpret_cast<Tree_ptr>(t);
auto right = tree->left();
if(!right)
return nullptr;
Tree_ptr p = new std::shared_ptr<Tree>(tree, right);
return reinterpret_cast<CTree>(p);
}
int Tree_data(CTree t) {
const auto& tree = *reinterpret_cast<Tree_ptr>(t);
return tree->data();
}
void Tree_set_data(CTree t, int x) {
const auto& tree = *reinterpret_cast<Tree_ptr>(t);
tree->set_data(x);
}
In [24]:
%%file CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
set(CMAKE_CXX_STANDARD 14)
add_compile_options(-fvisibility=hidden)
add_library(Tree_2_c SHARED Tree_2_c.cpp)
In [25]:
%%sh
rm -rf CMakeFiles/
rm CMakeCache.txt
cmake . -GNinja
cmake --build .
strip -x libTree_2_c.dylib
In [26]:
import ctypes
ctypes.c_object_p = ctypes.POINTER(ctypes.c_void_p)
tree_lib2 = ctypes.cdll.LoadLibrary("libTree_2_c.dylib")
In [27]:
tree_lib2.Tree_create.argtypes = [ctypes.c_int]
tree_lib2.Tree_create.restype = ctypes.c_object_p
tree_lib2.Tree_dispose.argtypes = [ctypes.c_object_p]
tree_lib2.Tree_dispose.restype = None
tree_lib2.Tree_left.argtypes = [ctypes.c_object_p]
tree_lib2.Tree_left.restype = ctypes.c_object_p
tree_lib2.Tree_right.argtypes = [ctypes.c_object_p]
tree_lib2.Tree_right.restype = ctypes.c_object_p
tree_lib2.Tree_data.argtypes = [ctypes.c_object_p]
tree_lib2.Tree_data.restype = ctypes.c_int
tree_lib2.Tree_set_data.argtypes = [ctypes.c_object_p, ctypes.c_int]
tree_lib2.Tree_set_data.restype = None
In [28]:
class Tree2(object):
def __init__(self, children=None, _p=None):
if _p:
self._ptr = _p
else:
self._ptr = tree_lib2.Tree_create(children)
def __del__(self):
tree_lib2.Tree_dispose(self._ptr)
def __repr__(self):
return "<Tree data:{}>".format(self.data)
@property
def left(self):
p = tree_lib2.Tree_left(self._ptr)
if not p:
return None
return Tree2(_p=p)
@property
def right(self):
p = tree_lib2.Tree_right(self._ptr)
if not p:
return None
return Tree2(_p=p)
@property
def data(self):
return tree_lib2.Tree_data(self._ptr)
@data.setter
def data(self, x):
tree_lib2.Tree_set_data(self._ptr, x)
In [29]:
# This no longer kills the kernel
left = Tree2(3).left.left.left
left.data
Out[29]:
In [30]:
root = Tree2(3)
left = root.left
left.data = 42
del root
left.data
Out[30]:
In addition to memory management, we have string translation and exception handling to think about.
Given time constraints, I won't cover that here.
Writing C-API and Python bindings out by hand is time consuming and more than a little error prone.
There's not a lot of creativity required once the approach is worked out.
We want to generate it.
libclang has Python bindings and exposes enough of the AST that we can extract all the information we need.
In [31]:
import sys
sys.path.insert(0,'..')
import ffig.clang.cindex
index = ffig.clang.cindex.Index.create()
translation_unit = index.parse("Tree.hpp", ['-x', 'c++', '-std=c++14', '-I../ffig/include'])
In [32]:
import asciitree
def node_children(node):
return (c for c in node.get_children() if c.location.file.name == "Tree.hpp")
print(asciitree.draw_tree(translation_unit.cursor,
lambda n: [c for c in node_children(n)],
lambda n: "%s (%s)" % (n.spelling or n.displayname, str(n.kind).split(".")[1])))
We create some simple classes of our own to make handling the relevant AST info easy.
In [33]:
import ffig.cppmodel
import ffig.clang.cindex
model = ffig.cppmodel.Model(translation_unit)
In [34]:
model
Out[34]:
In [35]:
model.classes[-5:]
Out[35]:
In [36]:
model.classes[-1].methods
Out[36]:
Jinja2 is a lightweight web-templating engine used in Flask and we can use it to generate code from AST info.
In [37]:
from jinja2 import Template
template = Template(R"""
C++ 17 will bring us:
{%for feature in features%}
* {{feature}}
{% endfor%}
""")
print(template.render(
{'features':['variant',
'optional',
'inline variables',
'fold-expressions',
'mandated-copy-elision']}))
We can feed the AST info to a Jinja2 template to write some code for us.
In [38]:
tree_ast = model.classes[-1]
In [39]:
from jinja2 import Template
template = Template(R"""\
#ifndef {{class.name|upper}}_H
#define {{class.name|upper}}_H
struct {{class.name}}_t;
typedef {{class.name}}_t* {{class.name}};
{{class.name}} {{class.name}}_create();
{{class.name}} {{class.name}}_dispose({{class.name}} my{{class.name}});
{%- for m in class.methods %}{% if not m.arguments %}
{{class.name}} {{class.name}}_{{m.name}}({{class.name}} my{{class.name}});
{%- endif %}{% endfor %}
#else // {{class.name|upper}}_H
#endif // {{class.name|upper}}_H
""")
print(template.render({'class':tree_ast}))
FFIG can be invoked to generate bindings for us.
FFIG requires that a class is annotated to create bindings for it.
In [40]:
%%file Shape.h
#include "ffig/attributes.h"
#include <stdexcept>
#include <string>
struct FFIG_EXPORT Shape {
virtual ~Shape() = default;
virtual double area() const = 0;
virtual double perimeter() const = 0;
virtual const char* name() const = 0;
};
static const double pi = 3.14159;
class Circle : public Shape {
const double radius_;
public:
double area() const override {
return pi * radius_ * radius_;
}
double perimeter() const override {
return 2 * pi * radius_;
}
const char* name() const override {
return "Circle";
}
Circle(double radius) : radius_(radius) {
if ( radius < 0 ) {
std::string s = "Circle radius \""
+ std::to_string(radius_) + "\" must be non-negative.";
throw std::runtime_error(s);
}
}
};
In [41]:
%%sh
cd ..
rm -rf demos/ffig_output
mkdir demos/ffig_output
python -m ffig -b rb.tmpl python -m Shape -i demos/Shape.h -o demos/ffig_output
ls -R demos/ffig_output
In [42]:
%cat ffig_output/Shape_c.h
In [43]:
%%sh
cd ../
python -m ffig -b rb.tmpl python -m Shape -i demos/Shape.h -o demos
In [44]:
%%file CMakeLists.txt
cmake_minimum_required(VERSION 3.0)
set(CMAKE_CXX_STANDARD 14)
include_directories(../ffig/include)
add_compile_options(-fvisibility=hidden)
add_library(Shape_c SHARED Shape_c.cpp)
In [45]:
%%sh
rm -rf CMakeFiles/
rm CMakeCache.txt
cmake . -GNinja
cmake --build .
strip -x libShape_c.dylib
In [47]:
import Shape
c = Shape.Circle(5)
print("A {} with radius {} has area {}".format(c.name(), 8, c.area()))
In [48]:
%%script /opt/intel/intelpython27/bin/python
import Shape
Shape.Config.set_library_path(".")
c = Shape.Circle(8)
print("A {} with radius {} has area {}".format(c.name(), 8, c.area()))
In [49]:
Shape.Circle(-5)
In [51]:
%%ruby
load "Shape.rb"
c = Circle.new(8)
puts("A #{c.name()} with radius #{8} has area #{c.area()}")
FFIG is MIT-licensed and hosted on GitHub.
We'd really like input on:
Other issues are on: https://github.com/FFIG/ffig/issues
Our policy for PRs is to approve things that are tested and do not break existing code.
No change is too small!