LLVM Cauldron - Wuthering Bytes 2016-09-08

Generating Python & Ruby bindings from C++

Jonathan B Coe

jonathanbcoe@gmail.com

https://github.com/ffig/ffig

[Updated Links and API use on 2018-01-25]

Write a C++ class out to a file in the current working directory


In [1]:
outputfile = "Shape.h"

In [2]:
%%file $outputfile
#include <stdexcept>
#include <string>

#ifdef __clang__
  #define C_API __attribute__((annotate("GENERATE_C_API")))
#else
  #define C_API
#endif

#include <ffig/attributes.h>

struct FFIG_EXPORT Shape
{
  virtual ~Shape() = default;
  virtual double area() const = 0;
  virtual double perimeter() const = 0;
  virtual const char* name() const = 0;
} __attribute__((annotate("GENERATE_C_API")));

static const double pi = 4.0;

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

Compile our header to check it's valid C++


In [3]:
%%sh
clang++-3.8 -x c++ -fsyntax-only -std=c++14 -I../ffig/include Shape.h

Read the code using libclang


In [4]:
import sys
sys.path.insert(0,'..')

import ffig.clang.cindex

index = ffig.clang.cindex.Index.create()
translation_unit = index.parse(outputfile, ['-x', 'c++', '-std=c++14', '-I../ffig/include'])

In [5]:
import asciitree

def node_children(node):
    return (c for c in node.get_children() if c.location.file.name == outputfile)

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


Shape.h (TRANSLATION_UNIT)
  +--Shape (STRUCT_DECL)
  |  +--FFIG:EXPORT (ANNOTATE_ATTR)
  |  +--GENERATE_C_API (ANNOTATE_ATTR)
  |  +--~Shape (DESTRUCTOR)
  |  |  +-- (COMPOUND_STMT)
  |  +--area (CXX_METHOD)
  |  +--perimeter (CXX_METHOD)
  |  +--name (CXX_METHOD)
  +--pi (VAR_DECL)
  |  +-- (FLOATING_LITERAL)
  +--Circle (CLASS_DECL)
     +--struct Shape (CXX_BASE_SPECIFIER)
     |  +--struct Shape (TYPE_REF)
     +--radius_ (FIELD_DECL)
     +-- (CXX_ACCESS_SPEC_DECL)
     +--area (CXX_METHOD)
     |  +-- (CXX_OVERRIDE_ATTR)
     |  +-- (COMPOUND_STMT)
     |     +-- (RETURN_STMT)
     |        +-- (BINARY_OPERATOR)
     |           +-- (BINARY_OPERATOR)
     |           |  +--pi (UNEXPOSED_EXPR)
     |           |  |  +--pi (DECL_REF_EXPR)
     |           |  +--radius_ (UNEXPOSED_EXPR)
     |           |     +--radius_ (MEMBER_REF_EXPR)
     |           +--radius_ (UNEXPOSED_EXPR)
     |              +--radius_ (MEMBER_REF_EXPR)
     +--perimeter (CXX_METHOD)
     |  +-- (CXX_OVERRIDE_ATTR)
     |  +-- (COMPOUND_STMT)
     |     +-- (RETURN_STMT)
     |        +-- (BINARY_OPERATOR)
     |           +-- (BINARY_OPERATOR)
     |           |  +-- (UNEXPOSED_EXPR)
     |           |  |  +-- (INTEGER_LITERAL)
     |           |  +--pi (UNEXPOSED_EXPR)
     |           |     +--pi (DECL_REF_EXPR)
     |           +--radius_ (UNEXPOSED_EXPR)
     |              +--radius_ (MEMBER_REF_EXPR)
     +--name (CXX_METHOD)
     |  +-- (CXX_OVERRIDE_ATTR)
     |  +-- (COMPOUND_STMT)
     |     +-- (RETURN_STMT)
     |        +-- (UNEXPOSED_EXPR)
     |           +--"Circle" (STRING_LITERAL)
     +--Circle (CONSTRUCTOR)
        +--radius (PARM_DECL)
        +--radius_ (MEMBER_REF)
        +--radius (UNEXPOSED_EXPR)
        |  +--radius (DECL_REF_EXPR)
        +-- (COMPOUND_STMT)
           +-- (IF_STMT)
              +-- (BINARY_OPERATOR)
              |  +--radius (UNEXPOSED_EXPR)
              |  |  +--radius (DECL_REF_EXPR)
              |  +-- (UNEXPOSED_EXPR)
              |     +-- (INTEGER_LITERAL)
              +-- (COMPOUND_STMT)
                 +-- (DECL_STMT)
                 |  +--s (VAR_DECL)
                 |     +--std (NAMESPACE_REF)
                 |     +--string (TYPE_REF)
                 |     +-- (UNEXPOSED_EXPR)
                 |        +-- (CALL_EXPR)
                 |           +-- (UNEXPOSED_EXPR)
                 |              +-- (UNEXPOSED_EXPR)
                 |                 +--operator+ (CALL_EXPR)
                 |                    +-- (UNEXPOSED_EXPR)
                 |                    |  +-- (UNEXPOSED_EXPR)
                 |                    |     +--operator+ (CALL_EXPR)
                 |                    |        +-- (UNEXPOSED_EXPR)
                 |                    |        |  +--"Circle radius \"" (STRING_LITERAL)
                 |                    |        +--operator+ (UNEXPOSED_EXPR)
                 |                    |        |  +--operator+ (DECL_REF_EXPR)
                 |                    |        +-- (UNEXPOSED_EXPR)
                 |                    |           +-- (UNEXPOSED_EXPR)
                 |                    |              +--to_string (CALL_EXPR)
                 |                    |                 +--to_string (UNEXPOSED_EXPR)
                 |                    |                 |  +--to_string (DECL_REF_EXPR)
                 |                    |                 |     +--std (NAMESPACE_REF)
                 |                    |                 +--radius_ (UNEXPOSED_EXPR)
                 |                    |                    +--radius_ (MEMBER_REF_EXPR)
                 |                    +--operator+ (UNEXPOSED_EXPR)
                 |                    |  +--operator+ (DECL_REF_EXPR)
                 |                    +-- (UNEXPOSED_EXPR)
                 |                       +--"\" must be non-negative." (STRING_LITERAL)
                 +-- (UNEXPOSED_EXPR)
                    +-- (CXX_THROW_EXPR)
                       +-- (CALL_EXPR)
                          +-- (UNEXPOSED_EXPR)
                             +-- (UNEXPOSED_EXPR)
                                +-- (CXX_FUNCTIONAL_CAST_EXPR)
                                   +--std (NAMESPACE_REF)
                                   +--class std::runtime_error (TYPE_REF)
                                   +-- (UNEXPOSED_EXPR)
                                      +--runtime_error (CALL_EXPR)
                                         +--s (UNEXPOSED_EXPR)
                                            +--s (DECL_REF_EXPR)

Turn the AST into some easy to manipulate Python classes


In [6]:
from ffig import cppmodel

In [7]:
model = cppmodel.Model(translation_unit)

In [8]:
model


Out[8]:
<cppmodel.Model filename=Shape.h, classes=['_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', 'sigevent', 'sigaction', 'sigvec', 'sigstack', 'timeval', 'rusage', 'rusage_info_v0', 'rusage_info_v1', 'rusage_info_v2', 'rusage_info_v3', 'rusage_info_v4', 'rlimit', 'proc_rlimit_control_wakeupmon', 'is_scalar', 'is_destructible', 'exception', 'bad_exception', 'exception_ptr', 'exception_ptr', 'nested_exception', 'timespec', 'tm', 'ios_base', 'logic_error', 'runtime_error', 'domain_error', 'invalid_argument', 'length_error', 'out_of_range', 'range_error', 'overflow_error', 'underflow_error', 'piecewise_construct_t', 'type_info', 'bad_cast', 'bad_typeid', 'nothrow_t', 'bad_alloc', 'bad_array_new_length', 'bad_array_length', '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', '_PairT', 'bad_weak_ptr', 'pointer_safety', 'char_traits', 'char_traits', 'char_traits', 'char_traits', 'basic_string', 'basic_string', 'Shape', 'Circle'], functions=['_OSSwapInt16', '_OSSwapInt32', '_OSSwapInt64', 'set_unexpected', 'get_unexpected', 'unexpected', 'set_terminate', 'get_terminate', 'terminate', 'uncaught_exception', 'uncaught_exceptions', 'current_exception', 'rethrow_exception', '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', '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[]', 'tuple_cat', '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', 'get_pointer_safety', 'declare_reachable', 'declare_no_pointers', 'undeclare_no_pointers', 'align', 'operator""sv', 'operator""sv', 'operator""sv', 'operator""sv', 'iswblank', 'iswascii', 'iswhexnumber', 'iswideogram', 'iswnumber', 'iswphonogram', 'iswrune', 'iswspecial', 'stoi', 'stol', 'stoul', 'stoll', 'stoull', 'stof', 'stod', 'stold', 'to_string', 'to_string', 'to_string', 'to_string', 'to_string', 'to_string', 'to_string', 'to_string', 'to_string', 'stoi', 'stol', 'stoul', 'stoll', 'stoull', 'stof', 'stod', 'stold', 'to_wstring', 'to_wstring', 'to_wstring', 'to_wstring', 'to_wstring', 'to_wstring', 'to_wstring', 'to_wstring', 'to_wstring', 'operator""s', 'operator""s', 'operator""s', 'operator""s']>

In [9]:
[f.name for f in model.functions][-5:]


Out[9]:
['to_wstring', 'operator""s', 'operator""s', 'operator""s', 'operator""s']

In [10]:
[c.name for c in model.classes][-5:]


Out[10]:
['char_traits', 'basic_string', 'basic_string', 'Shape', 'Circle']

In [11]:
shape_class = [c for c in model.classes if c.name=='Shape'][0]

In [12]:
["{}::{}".format(shape_class.name,m.name) for m in shape_class.methods]


Out[12]:
['Shape::area', 'Shape::perimeter', 'Shape::name']

Look at the templates the generator uses


In [13]:
%cat ../ffig/templates/json.tmpl


[{% for class in classes %}
{
  "name" : "{{class.name}}"{% if class.methods %},
  "methods" : [{% for method in class.methods %}
    {
      "name" : "{{method.name}}",
      "return_type" : "{{method.return_type}}"
    }{% if not loop.last %},{% endif %}{% endfor %}
  ]{% endif %}
}{% if not loop.last %},{% endif %}{% endfor %}
]

Run the code generator


In [14]:
%%sh
cd ..
python -m ffig -b json.tmpl rb.tmpl python -m Shape -i demos/Shape.h -o demos/

See what it created


In [15]:
%ls


CMakeCache.txt            Makefile                  Shape_c.cpp
CMakeFiles/               PyDataLondon-2017.ipynb   Shape_c.h
CMakeLists.txt            Shape.h                   cmake_install.cmake
CppLondon_Aug-2017.ipynb  Shape.json                libShape_c.dylib*
LLVM-Cauldron.ipynb       Shape.rb                  shape/

In [16]:
%cat Shape.json


[{
  "name" : "Shape",
  "methods" : [    {
      "name" : "area",
      "return_type" : "double"
    },    {
      "name" : "perimeter",
      "return_type" : "double"
    },    {
      "name" : "name",
      "return_type" : "const char *"
    }  ]}]

Build some bindings with the generated code.


In [17]:
%%file CMakeLists.txt

cmake_minimum_required(VERSION 3.0)
set(CMAKE_CXX_STANDARD 14)
add_library(Shape_c SHARED Shape_c.cpp)
target_include_directories(Shape_c PRIVATE ../ffig/include)


Overwriting CMakeLists.txt

In [18]:
%%sh
cmake . 
cmake --build .


-- Configuring done
-- Generating done
-- Build files have been written to: /Users/jon/DEV/FFIG/demos
Scanning dependencies of target Shape_c
[ 50%] Building CXX object CMakeFiles/Shape_c.dir/Shape_c.cpp.o
[100%] Linking CXX shared library libShape_c.dylib
[100%] Built target Shape_c

In [19]:
%%python2
import shape
c = shape.Circle(8)

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


A Circle with radius 8 has area 256.0

In [20]:
%%script pypy
import shape
c = shape.Circle(8)

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


A Circle with radius 8 has area 256.0

In [21]:
%%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 256.0