Skip to content
Anders Langlands edited this page Mar 13, 2021 · 8 revisions

Binding tutorials

Welcome to the cppmm wiki! This will ultimately host more filled out documentation, but for now we'll just have a simple tutorial.

This tutorial will walk you through creating binding files using a very simple, header-only target library which we'll extend to add new features to as we need to demonstrate how to bind them.

This assumes you have successfully built cppmm and are currently in a build directory underneath the main repository directory. If you're working from somewhere else, you'll need to edit the example commands appropriately.

00 - cppmm architecture

Before we get started, let's take a moment to examine the cppmm architecture. Binding a C++ library is a multi-step process. First we create a binding file, which is a C++ file that describes the classes, methods and functions we want to bind, and how we want to bind them. This is read by our first binary, astgen, which spits out the AST for the binding interface as JSON. Then asttoc reads the AST and generates a C library that wraps the C++ API.

-- TODO: DIAGRAM HERE

01 - Binding a simple class

Our target "library"

What's the simplest possible C++ class we can bind to get started? Perhaps something like this:

// file: tutorial/01_simple_class/include/hello.hpp
#pragma once

#include <iostream>

namespace hello {

class Hello {
public:
    void hello() const { std::cout << "Hello, world!\n"; }
};

} // namespace hello

Our first binding file

In order to start binding this to C, we first need to create AST, and to do that we need to create a binding file to feed to astgen. We'll call that file c-hello.cpp and start our binding file in it. The final binding file for each step of the tutorial is in tutorials/xxx/bind, but we're working through, let's just create it in the current (build) directory

// file: c-hello.cpp
#include <hello.hpp>

namespace cppmm_bind {

}

First, we need to include the header for the part of the library that we want to bind. In this case it's just the hello.hpp we defined earlier. Next we put everything we declare in the cppmm_bind namespace. astgen relies on this to know what parts of the AST it's navigating are part of the binding declarations, and what are potentially part of the library we want to bind. You must wrap your entire binding files in the cppmm_bind namespace.

Now we can start to fill out our binding. First, we want to bind the class Hello:

// file: c-hello.cpp
#include <hello.hpp>

namespace cppmm_bind {

namespace hello {

struct Hello {
    using BoundType = ::hello::Hello;
};

} // namespace hello

} // namespace cppmm_bind

Here we've mirrored the namespace hierarchy of our library inside the cppmm_bind namespace. This doesn't matter for the simple example we're doing here, but will become important when we're binding free functions later, so it's good practice to always do it.

Inside cppmm_bind::hello we're declaring a struct called Hello. Why a struct and not a class? astgen ignores any private or protected methods (and any automatically generated methods) so using struct here saves us from having to type public:.

Next we have using BoundType = ::hello::Hello;. What does this line do? It tells astgen that this struct in the binding file (cppmm::hello::Hello) is declaring what we want to bind from the library type ::hello::Hello.

As an experiment, try removing the BoundType line from the binding file - we get an error and no json output is generated, because astgen doesn't know what you're trying to bind.

Generating AST

Let's try running astgen on this binding file and see what we get:

> ./astgen/astgen c-hello -v 1 -o 01_simple_class/ast -- -I../tutorials/01_simple_class/include

Note the format of the arguments here. The positional arguments to astgen are a list of binding files, or a directory containing binding files (where everything with a .cpp extension will be considered a binding file). The -v 1 flag sets verbosity to 1 (warnings). The -o flag tells astgen where we want to write the output JSON files to. Then we have the double dash which marks the start of arguments to be passed to libclang, which is just the header include paths we need to find the library.

This command should generate the file 01_simple_class/ast/c-hello.json:

{
    "kind": "TranslationUnit",
    "filename": "/home/anders/code/cppmm/build/c-hello.cpp",
    "source_includes": [
        "#include <hello.hpp>"
    ],
    "include_paths": [
        "../tutorial/01_simple_class/include"
    ],
    "id": 0,
    "decls": [
        {
            "kind": "Namespace",
            "name": "hello",
            "id": 1,
            "short_name": "hello"
        },
        {
            "kind": "Record",
            "name": "hello::Hello",
            "short_name": "Hello",
            "namespaces": [
                1
            ],
            "id": 2,
            "abstract": false,
            "trivially_copyable": true,
            "size": 8,
            "align": 8,
            "alias": "Hello",
            "attributes": null,
            "fields": null,
            "methods": null
        }
    ]
}

Here you can see the AST of our target class hello::Hello. You can see it's a Record (clang's name for structs, classes etc), it's trivially copyable, it's 1 byte (8 bits) in size, it's in the hello namespace, and it doesn't have any methods.

If you were paying attention, you should have noticed that astgen gave us a warning when we ran it:

process_binding.cpp:1555 [warning] Method hello::Hello::hello() -> void const[[user_provided, ]] is present in the library but not declared in the binding

You should hopefully be able to decipher that this is telling us that the library type we're trying to bind has a method we haven't declared in the binding. While this should be obvious, in more complex libraries whose APIs are in a constant state of flux (like many VFX libraries), getting told when we've missed a method can be very helpful.

astgen will likewise warn us if we declare a binding method that's not there in the library. This could be because we've made a typo, or perhaps its because we're trying to bind a slightly newer version of the library and the method signiature has changed. Whatever the reason, it's very helpful when we generate the AST to get a complete list of everything that didn't match so we can investigate and fix where necessary.

So let's add the missing method to the binding file:

// file: c-hello.cpp
#include <hello.hpp>

namespace cppmm_bind {

namespace hello {

struct Hello {
    using BoundType = ::hello::Hello;
    void hello();
};

} // namespace hello

} // namespace cppmm_bind

Now if we run the astgen again we don't get any warnings and our generated AST has the method in it:

 process_binding.cpp:1555 [warning] Method hello::Hello::hello() -> void const[[user_provided, ]] is present in the library but not declared in the binding
 process_binding.cpp:1564 [warning] Method cppmm_bind::hello::Hello::hello() -> void[[user_provided, ]] is declared in the binding but not present in the library

Oops! Looks like we forgot to add the const specifier to the method. Good thing astgen tells us! Let's fix that:

// file: c-hello.cpp
#include <hello.hpp>

namespace cppmm_bind {

namespace hello {

struct Hello {
    using BoundType = ::hello::Hello;
    void hello() const;
};

} // namespace hello

} // namespace cppmm_bind

Ok now we don't get any warnings and the AST has our method in it:

{
    "kind": "TranslationUnit",
    "filename": "/home/anders/code/cppmm/build/c-hello.cpp",
    "source_includes": [
        "#include <hello.hpp>"
    ],
    "include_paths": [
        "../tutorial/01_simple_class/include"
    ],
    "id": 1,
    "decls": [
        {
            "kind": "Namespace",
            "name": "hello",
            "id": 2,
            "short_name": "hello"
        },
        {
            "kind": "Record",
            "name": "hello::Hello",
            "short_name": "Hello",
            "namespaces": [
                2
            ],
            "id": 3,
            "abstract": false,
            "trivially_copyable": true,
            "size": 8,
            "align": 8,
            "alias": "Hello",
            "attributes": null,
            "fields": null,
            "methods": [
                {
                    "kind": "Method",
                    "id": 0,
                    "short_name": "hello",
                    "qualified_name": "hello::Hello::hello",
                    "in_binding": true,
                    "in_library": false,
                    "static": false,
                    "user_provided": true,
                    "const": true,
                    "virtual": false,
                    "overloaded_operator": false,
                    "copy_assignment_operator": false,
                    "move_assignment_operator": false,
                    "constructor": false,
                    "copy_constructor": false,
                    "move_constructor": false,
                    "conversion_decl": false,
                    "destructor": false,
                    "attributes": null,
                    "return": {
                        "kind": "BuiltinType",
                        "id": 0,
                        "type": "void",
                        "const": false
                    },
                    "params": null
                }
            ]
        }
    ]
}

As you can see the JSON AST gets very verbose, so we won't be examining it much more. Congratulations! You've generated your first AST.

One thing you might be wondering is, if we "connect" our binding struct declaration to the library type using the BoundType= alias, does it matter what the binding struct is called? The answer is no! It could be called anything you like - try changing it and see that whatever you call it, the output JSON is the same. The binding struct is just a device to "hook into" the library type. This is an important concept to understand early: the binding struct is just a way to associate a list of method signiatures with a library type. It is not a "reflection" of the library type itself. This will become very important when we get to methods that take an instance of the parent class as arguments.

Our first C library

Now that we've got our AST generated, we can use asttoc to generate a C wrapper for our target library:

./asttoc/asttoc 01_simple_class/ast -o 01_simple_class/c

The positional argument here is the directory containing the AST JSON files we just created, and the -o flag specifies where we want to output the resulting C project to.

This will generate the directory 01_simple_class/c containing a complete C project. So let's build it:

cd 01_simple_class/c
mkdir build && cd build
cmake ..
make

This should build a shared library libmm_binding.so. Let's test it! Create the following C file in the current directory:

/* file: hello.c */
#include <c-hello_.h>

int main(int argc, char** argv) {
    hello_Hello h;
    hello_Hello_hello(&h);
}

Compile and run it like so:

gcc hello.c -o hello -I.. -L. -lbmm_binding
env LD_LIBRARY_PATH=. ./hello

And you should see:

Hello, world!

It works!

Clone this wiki locally