Search code examples
cbindingluatolua++

binding structs and ctor/dtor with tolua++


Let's say I want to bind a piece of code to Lua that looks like this:

typedef struct bar {
  void * some_data;
} bar;
bar * bar_create(void);
void bar_do_something(bar * baz);
void bar_free(bar * baz);

I want to create these objects from a Lua script, and not explicitly manage their lifetime. Prefereably, I would like my script to write

require "foo"
local baz = foo:bar()
baz:do_something()
baz = nil

Problem: For that to work as expected, I need to somehow tell tolua++ about bar_create and bar_free being the constructor/destructor for bar. How do I do that? For classes, tolua++ claims to automatically use their ctor/dtor, but for structs?

The best thing I can come up with is this definition of foo.pkg:

module foo {
  struct bar {
    static tolua_outside bar_create @ create();
    tolua_outside bar_do_something @ do_something();
    tolua_outside bar_free @ free();
  };
}

which would mean I have to call create() and free() explicitly.


Solution

  • The bar functions can be imported into Lua using tolua++ and wrapped to yield an object-style interface, garbage collection included.

    To demonstrate the passing of arguments, I have changed the bar interface to

    bar * bar_create(int x);
    int bar_do_something(bar * baz, int y);
    void bar_free(bar * baz);
    

    and written a test implementation that prints out x, y, etc. when the functions are called.

    The bar_create() Lua function returns a userdata value. Lua deallocates such user data by calling the __gc method stored in the metatable of the data. Given a userdata value and a destructor gc, the __gc method is overwritten such that it first calls gc and then calls the original gc method:

    function wrap_garbage_collector(userdata, gc)
        local mt = getmetatable(userdata)
        local old_gc = mt.__gc
        function mt.__gc (data)
            gc(data)
            old_gc(data)
        end
    end
    

    Userdata of the same type share the same metatable; therefore the wrap_garbage_collector() function should be called only once for each class (assuming that tolua++'s metatables are constructed once and deallocated only at exit).

    At the bottom of this answer is a complete bar.pkg file that imports the bar functions and adds a bar class to a Lua module named foo. The foo module is loaded into the interpreter (see for example my SO tolua++ example) and used like this:

    bars = {}
    
    for i = 1, 3 do
        bars[i] = foo.bar(i)
    end
    
    for i = 1, 3 do
        local result = bars[i]:do_something(i * i)
        print("result:", result)
    end
    

    The test implementation prints out what happens:

    bar(1)
    bar(2)
    bar(3)
    bar(1)::do_something(1)
    result: 1
    bar(2)::do_something(4)
    result: 8
    bar(3)::do_something(9)
    result: 27
    ~bar(3)
    ~bar(2)
    ~bar(1)
    

    The construction of the bar class below is a little elaborate: the build_class() utility returns a class (a Lua table) given the constructor, destructor, and the class methods. Adjustments will no doubt be needed, but as a prototype demonstration the example should be OK.

    $#include "bar.hpp"
    
    // The bar class functions.
    bar * bar_create(int x);
    int bar_do_something(bar * baz, int y);
    void bar_free(bar * baz);
    
    $[
        -- Wrapping of the garbage collector of a user data value.
        function wrap_garbage_collector(userdata, gc)
            local mt = getmetatable(userdata)
            local old_gc = mt.__gc
            function mt.__gc (data)
                gc(data)
                old_gc(data)
            end
        end
    
        -- Construction of a class.
        --
        -- Arguments:
        --
        --   cons : constructor of the user data
        --   gc : destructor of the user data
        --   methods : a table of pairs { method = method_fun }
        --
        -- Every 'method_fun' of 'methods' is passed the user data 
        -- as the first argument.
        --
        function build_class(cons, gc, methods)
            local is_wrapped = false
            function class (args)
                -- Call the constructor.
                local value = cons(args)
    
                -- Adjust the garbage collector of the class (once only).
                if not is_wrapped then
                    wrap_garbage_collector(value, gc)
                    is_wrapped = true
                end
    
                -- Return a table with the methods added.
                local t = {}
                for name, method in pairs(methods) do
                    t[name] =
                        function (self, ...)
                            -- Pass data and arguments to the method.
                            return (method(value, ...))
                        end
                end
    
                return t
            end
            return class
        end
    
        -- The Lua module that contains our classes.
        foo = foo or {}
    
        -- Build and assign the classes.
        foo.bar =
            build_class(bar_create, bar_free,
                        { do_something = bar_do_something })
    
        -- Clear global functions that shouldn't be visible.
        bar_create = nil
        bar_free = nil
        bar_do_something = nil
    $]