FFIG - A foreign function interface generator for C++

https://github.com/FFIG/ffig

jonathanbcoe@gmail.com


I want to write C++ code and call it from Python without doing any extra work.


For Project Managers

A language like Python can be written to resemble executable pseudo-code and is a great langauge for defining high-level acceptance criteria.

Developer engagement is easy when requirements are code.


For Clients

It's easy to put together interactive demos with Python. Jupyter notebook allows one to display graphs, images, videos and tabulated data.

The ability to change demos on the fly in response to client queries is seriously impressive.


For Developers

I can break the edit-compile-link-test cycle with an interpreted scripting language.

Once the design is right I can drive it into C++.


Calling C++ from Python

  • Write some C++ code.
  • Define a C-API for binary compatibility.
  • Solving problems with ownership.
  • Define classes and functions that use a Foreign Function Interface to communicate with C.

Code-generation

  • Parse C++ code.
  • Transform parsed source code into a easy-to use form.
  • Define a template that transforms parsed source code into C
  • Define a template that transforms parsed source code into classes, functions and FFI calls.


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


Overwriting Tree.hpp

Let's compile this to ensure we've not made any mistakes.


In [2]:
%%sh
clang++ -std=c++14 -fsyntax-only Tree.hpp

Defining a C-API

We want a C-API so that we have a well-defined and portable binary interface.

We'll have to re-model our code as C does not support classes.

Free functions with an extra leading argument for this should suffice.


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


Overwriting 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);
}


Overwriting Tree_c.cpp

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)


Overwriting CMakeLists.txt

In [6]:
%%sh
rm -rf CMakeFiles/
rm CMakeCache.txt || echo

cmake . -GNinja
cmake --build .
strip -x libTree_c.dylib


-- The C compiler identification is AppleClang 8.1.0.8020042
-- The CXX compiler identification is AppleClang 8.1.0.8020042
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/jon/DEV/FFIG/demos
[1/2] Building CXX object CMakeFiles/Tree_c.dir/Tree_c.cpp.o
[2/2] Linking CXX shared library libTree_c.dylib

In [7]:
%%sh
nm -U libTree_c.dylib


0000000000001c30 T _Tree_create
0000000000001fc0 T _Tree_data
0000000000001ea0 T _Tree_dispose
0000000000001f00 T _Tree_left
0000000000001f60 T _Tree_right
0000000000002010 T _Tree_set_data

Python interop

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]:
<__main__.LP_c_void_p at 0x1031987b8>

In [12]:
tree_lib.Tree_data(root)


Out[12]:
0

In [13]:
tree_lib.Tree_set_data(root, 42)
tree_lib.Tree_data(root)


Out[13]:
42

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]:
<Tree data:0>

In [18]:
t.data = 42
t


Out[18]:
<Tree data:42>

In [19]:
t.left


Out[19]:
<Tree data:0>

In [20]:
t.left.data = 6

In [21]:
t.left


Out[21]:
<Tree data:6>

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.

Defining a C-API (Again)

We can imbue the pointers passed across the API boundary with object lifetime detail

The aliasing constructor of std::shared_ptr

We can use the aliasing constructor of shared_ptr to keep objects alive while any subobject is exposed across the API boundary.

template class shared_ptr { shared_ptr(const shared_ptr& parent, T* child); };


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);
}


Overwriting Tree_2_c.cpp

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)


Overwriting CMakeLists.txt

In [25]:
%%sh
rm -rf CMakeFiles/
rm CMakeCache.txt

cmake . -GNinja
cmake --build .
strip -x libTree_2_c.dylib


-- The C compiler identification is AppleClang 8.1.0.8020042
-- The CXX compiler identification is AppleClang 8.1.0.8020042
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/jon/DEV/FFIG/demos
[1/2] Building CXX object CMakeFiles/Tree_2_c.dir/Tree_2_c.cpp.o
[2/2] Linking CXX shared library libTree_2_c.dylib

A Safer Python API


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]:
0

In [30]:
root = Tree2(3)
left = root.left
left.data = 42

del root
left.data


Out[30]:
42

In addition to memory management, we have string translation and exception handling to think about.

Given time constraints, I won't cover that here.


FFIG

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.

Parsing C++ with libclang

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])))


Tree.hpp (TRANSLATION_UNIT)
  +--Tree (CLASS_DECL)
     +--left_ (FIELD_DECL)
     |  +--std (NAMESPACE_REF)
     |  +--unique_ptr (TEMPLATE_REF)
     |  +--class Tree (TYPE_REF)
     +--right_ (FIELD_DECL)
     |  +--std (NAMESPACE_REF)
     |  +--unique_ptr (TEMPLATE_REF)
     |  +--class Tree (TYPE_REF)
     +--data_ (FIELD_DECL)
     +-- (CXX_ACCESS_SPEC_DECL)
     +--Tree (CONSTRUCTOR)
     |  +--children (PARM_DECL)
     |  +-- (COMPOUND_STMT)
     |     +-- (IF_STMT)
     |     |  +-- (BINARY_OPERATOR)
     |     |  |  +--children (UNEXPOSED_EXPR)
     |     |  |  |  +--children (DECL_REF_EXPR)
     |     |  |  +-- (INTEGER_LITERAL)
     |     |  +-- (RETURN_STMT)
     |     +-- (UNEXPOSED_EXPR)
     |     |  +--operator= (CALL_EXPR)
     |     |     +--left_ (MEMBER_REF_EXPR)
     |     |     +--operator= (UNEXPOSED_EXPR)
     |     |     |  +--operator= (DECL_REF_EXPR)
     |     |     +-- (UNEXPOSED_EXPR)
     |     |        +-- (UNEXPOSED_EXPR)
     |     |           +--make_unique (CALL_EXPR)
     |     |              +--make_unique (UNEXPOSED_EXPR)
     |     |              |  +--make_unique (DECL_REF_EXPR)
     |     |              |     +--std (NAMESPACE_REF)
     |     |              |     +--class Tree (TYPE_REF)
     |     |              +-- (UNEXPOSED_EXPR)
     |     |                 +-- (BINARY_OPERATOR)
     |     |                    +--children (UNEXPOSED_EXPR)
     |     |                    |  +--children (DECL_REF_EXPR)
     |     |                    +-- (INTEGER_LITERAL)
     |     +-- (UNEXPOSED_EXPR)
     |        +--operator= (CALL_EXPR)
     |           +--right_ (MEMBER_REF_EXPR)
     |           +--operator= (UNEXPOSED_EXPR)
     |           |  +--operator= (DECL_REF_EXPR)
     |           +-- (UNEXPOSED_EXPR)
     |              +-- (UNEXPOSED_EXPR)
     |                 +--make_unique (CALL_EXPR)
     |                    +--make_unique (UNEXPOSED_EXPR)
     |                    |  +--make_unique (DECL_REF_EXPR)
     |                    |     +--std (NAMESPACE_REF)
     |                    |     +--class Tree (TYPE_REF)
     |                    +-- (UNEXPOSED_EXPR)
     |                       +-- (BINARY_OPERATOR)
     |                          +--children (UNEXPOSED_EXPR)
     |                          |  +--children (DECL_REF_EXPR)
     |                          +-- (INTEGER_LITERAL)
     +--left (CXX_METHOD)
     |  +--class Tree (TYPE_REF)
     |  +-- (COMPOUND_STMT)
     |     +-- (RETURN_STMT)
     |        +--get (CALL_EXPR)
     |           +--get (MEMBER_REF_EXPR)
     |              +--left_ (UNEXPOSED_EXPR)
     |                 +--left_ (MEMBER_REF_EXPR)
     +--right (CXX_METHOD)
     |  +--class Tree (TYPE_REF)
     |  +-- (COMPOUND_STMT)
     |     +-- (RETURN_STMT)
     |        +--get (CALL_EXPR)
     |           +--get (MEMBER_REF_EXPR)
     |              +--right_ (UNEXPOSED_EXPR)
     |                 +--right_ (MEMBER_REF_EXPR)
     +--data (CXX_METHOD)
     |  +-- (COMPOUND_STMT)
     |     +-- (RETURN_STMT)
     |        +--data_ (UNEXPOSED_EXPR)
     |           +--data_ (MEMBER_REF_EXPR)
     +--set_data (CXX_METHOD)
        +--x (PARM_DECL)
        +-- (COMPOUND_STMT)
           +-- (BINARY_OPERATOR)
              +--data_ (MEMBER_REF_EXPR)
              +--x (UNEXPOSED_EXPR)
                 +--x (DECL_REF_EXPR)

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]:
<cppmodel.Model filename=Tree.hpp, classes=['is_scalar', 'add_lvalue_reference', 'add_lvalue_reference', 'add_lvalue_reference', 'add_lvalue_reference', 'add_rvalue_reference', 'add_rvalue_reference', 'add_rvalue_reference', 'add_rvalue_reference', 'is_destructible', 'exception', 'bad_exception', 'exception_ptr', 'exception_ptr', 'nested_exception', '_opaque_pthread_attr_t', '_opaque_pthread_cond_t', '_opaque_pthread_condattr_t', '_opaque_pthread_mutex_t', '_opaque_pthread_mutexattr_t', '_opaque_pthread_once_t', '_opaque_pthread_rwlock_t', '_opaque_pthread_rwlockattr_t', '_opaque_pthread_t', 'type_info', 'bad_cast', 'bad_typeid', 'bad_alloc', 'bad_array_new_length', 'bad_array_length', 'nothrow_t', 'piecewise_construct_t', 'timespec', 'tm', 'ios_base', 'less', 'allocator_arg_t', 'input_iterator_tag', 'output_iterator_tag', 'forward_iterator_tag', 'bidirectional_iterator_tag', 'random_access_iterator_tag', 'tuple', 'atomic_flag', 'allocator', 'allocator', 'auto_ptr', 'bad_weak_ptr', 'pointer_safety', 'Tree'], functions=['swap', 'set_unexpected', 'get_unexpected', 'unexpected', 'set_terminate', 'get_terminate', 'terminate', 'uncaught_exception', 'uncaught_exceptions', 'current_exception', 'rethrow_exception', 'set_new_handler', 'get_new_handler', 'operator new', 'operator new', 'operator delete', 'operator delete', 'operator delete', 'operator new[]', 'operator new[]', 'operator delete[]', 'operator delete[]', 'operator delete[]', 'operator new', 'operator new[]', 'operator delete', 'operator delete[]', 'isascii', 'isalnum', 'isalpha', 'isblank', 'iscntrl', 'isdigit', 'isgraph', 'islower', 'isprint', 'ispunct', 'isspace', 'isupper', 'isxdigit', 'toascii', 'tolower', 'toupper', 'digittoint', 'ishexnumber', 'isideogram', 'isnumber', 'isphonogram', 'isrune', 'isspecial', 'iswalnum', 'iswalpha', 'iswcntrl', 'iswctype', 'iswdigit', 'iswgraph', 'iswlower', 'iswprint', 'iswpunct', 'iswspace', 'iswupper', 'iswxdigit', 'towlower', 'towupper', 'tuple_cat', 'strchr', 'strpbrk', 'strrchr', 'memchr', 'strstr', 'atomic_flag_test_and_set', 'atomic_flag_test_and_set', 'atomic_flag_test_and_set_explicit', 'atomic_flag_test_and_set_explicit', 'atomic_flag_clear', 'atomic_flag_clear', 'atomic_flag_clear_explicit', 'atomic_flag_clear_explicit', 'atomic_thread_fence', 'atomic_signal_fence', 'declare_reachable', 'declare_no_pointers', 'undeclare_no_pointers', 'get_pointer_safety', 'align']>

In [35]:
model.classes[-5:]


Out[35]:
[<cppmodel.Class allocator>,
 <cppmodel.Class auto_ptr>,
 <cppmodel.Class bad_weak_ptr>,
 <cppmodel.Class pointer_safety>,
 <cppmodel.Class Tree>]

In [36]:
model.classes[-1].methods


Out[36]:
[<cppmodel.Method Tree * left()>,
 <cppmodel.Method Tree * right()>,
 <cppmodel.Method int data() const>,
 <cppmodel.Method void set_data(int)>]

Jinja2 templates

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']}))


C++ 17 will bring us:

* variant

* optional

* inline variables

* fold-expressions

* mandated-copy-elision


Jinja2 and libclang

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}))


#ifndef   TREE_H
#define   TREE_H

struct Tree_t;
typedef Tree_t* Tree;

Tree Tree_create();

Tree Tree_dispose(Tree myTree);

Tree Tree_left(Tree myTree);

Tree Tree_right(Tree myTree);

Tree Tree_data(Tree myTree);

#else  // TREE_H
#endif // TREE_H

Generating a Python API with FFIG

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);
    }
  }
};


Overwriting Shape.h

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


Shape
Shape.rb
Shape_c.cpp
Shape_c.h

demos/ffig_output/Shape:
__init__.py
_py2.py
_py3.py

In [42]:
%cat ffig_output/Shape_c.h


// This code was generated by FFIG <http://ffig.org>.
// Manual edits will be lost.

#pragma once

#define Shape_RC_SUCCESS 0
#define Shape_RC_FAIL 1

// FIXME: Use something nicer than _MBCS to see if we are on VS
#ifdef _MBCS
#ifdef Shape_c_EXPORTS
#define EXPORT extern "C" __declspec(dllexport)
#else
#define EXPORT extern "C" __declspec(dllimport)
#endif
#else
#define EXPORT extern "C" __attribute__((visibility("default")))
#endif

typedef const void* Shape_Shape;

#ifdef __cplusplus
extern "C"
{
#endif
EXPORT void Shape_clear_error();

EXPORT const char* Shape_error();

EXPORT void Shape_Shape_dispose(Shape_Shape myShape);


EXPORT int Shape_Shape_area(
    Shape_Shape myShape,
        double* rv);

EXPORT int Shape_Shape_perimeter(
    Shape_Shape myShape,
        double* rv);

EXPORT int Shape_Shape_name(
    Shape_Shape myShape,
        const char** rv);


EXPORT int Shape_Circle_create(
    double radius,
    Shape_Shape* rv);
#ifdef __cplusplus
}
#endif

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)


Overwriting CMakeLists.txt

In [45]:
%%sh
rm -rf CMakeFiles/
rm CMakeCache.txt

cmake . -GNinja
cmake --build .
strip -x libShape_c.dylib


-- The C compiler identification is AppleClang 8.1.0.8020042
-- The CXX compiler identification is AppleClang 8.1.0.8020042
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/jon/DEV/FFIG/demos
[1/2] Building CXX object CMakeFiles/Shape_c.dir/Shape_c.cpp.o
[2/2] Linking CXX shared library libShape_c.dylib

In [47]:
import Shape

c = Shape.Circle(5)

print("A {} with radius {} has area {}".format(c.name(), 8, c.area()))


A Circle with radius 8 has area 78.53975

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()))


A Circle with radius 8 has area 201.06176

In [49]:
Shape.Circle(-5)


---------------------------------------------------------------------------
Shape_error                               Traceback (most recent call last)
<ipython-input-49-750d55e859ab> in <module>()
----> 1 Shape.Circle(-5)

~/DEV/FFIG/demos/Shape/_py3.py in __init__(self, radius, _p)
     79       rc = conf.lib.Shape_Circle_create(radius, byref(self.ptr))
     80       if rc != 0:
---> 81         raise Shape_error()
     82 
     83 

Shape_error: Circle radius "-5.000000" must be non-negative.

In [51]:
%%ruby
load "Shape.rb"
c = Circle.new(8)

puts("A #{c.name()} with radius #{8} has area #{c.area()}")


A Circle with radius 8 has area 201.06176

FFIG needs you!

FFIG is MIT-licensed and hosted on GitHub.

We'd really like input on:

  • Support for returning non-primitives by value
  • Support for standard library types
  • Generating boost::python bindings

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!