Glue: Binding C++ objects to Lua via Type Traits

Binding plain old functions written in C to the Lua language is not difficult, thanks to Lua’s well designed C API. Binding C++ objects to Lua however can quickly turn into a complex nightmare, as issues of inheritance, memory management, callbacks from C++, and so on multiply levels of required intricacy. Several solutions to ease the writing of bindings exist with various strengths and weaknesses, such as Luabind, CppLua, Luna, SWIG, etc. (see this page on the Lua wiki). For LuaAV however, we ended writing our own C++ binding for several reasons, outlined below. Our binding is called Glue.

First, we were not concerned to auto-generate bindings from header files; often the portions you want to expose to the scripting language are not the same as what you would expose to the C++ developer, and sometimes you may want to expose a more elegant interface based upon the flexible capabilities of the scripting language, rather than a more brittle direct translation.

Secondly, we did not want to rely on a secondary annotation language and separate build step to generate bindings (the approach taken in SWIG, for example). Coding the bindings directly in C++ makes debugging easier, lessens the cognitive load, and allows us to insert ad hoc code into the bindings as needed.

Thirdly, we wanted the binding to be as lightweight as possible – in terms of size, dependencies and run-time performance. In the interests of size and dependencies we avoided the Boost-based LuaBind. In the interest of run-time performance we leveraged C++ templates to move as much computation as possible from run-time to compilation-time.

Finally, we wanted the binding to be as unobtrusive as possible. Several lightweight Lua/C++ binding libraries make use of inheritance, either deriving from a generic Userdata base class, or deriving a Userdata type from the C++ class to be bound. We avoided these approaches via template-based Type Traits. No special derived or base classes are needed at all. Instead, a new type of Glue<T> (where T is the type to bind) is defined and specialized to specify the necessary properties and methods to bind a type to Lua. Note that the Glue<T> type is never instantiated, it has no member data and its methods are all static functions (class methods). By using C++ templates, the entire binding fits into a single C++ header of a few hundred lines of code, whose only dependencies are the Lua headers themselves.

Usage

Probably at this point, an example would be more helpful. Consider the following definition of a class that we wish to bind to Lua:

class Foo  {
public:
Foo(int x) : Base(), x(x) {}
~Foo() {}
int x;
};

The first thing we need to define (the only mandatory item) for a binding to Lua is the name of the class. This will be used in the default implementation of the __tostring metamethod, for example. The type trait of Glue<T> is annotated with the name “Foo” by means of a specialization of the Glue<T>::usr_name() function:

template&lt;&gt; const char * Glue::usr_name() { return "Foo"; }

To install this new binding in a Lua state, Glue<Foo>::define(L) must be called. Typically this would occur in the luaopen_xxx method of a Lua module, or in the construction of the lua_State embedded in an application. Glue<>::define creates the metatable associated with the Foo type (additional methods can be added to the metatable by specializing the Glue<>::usr_mt method).

To push objects of type Foo into Lua, we can now use Glue<Foo>::push(L, foo). This will create a new userdata (a boxed pointer to the object), wrapped in the the proper metatable. To retrieve objects of type Foo from the Lua stack, we can use Glue<Foo>::checkto(L), Glue<Foo>::is(L), and so on.

Caveats

Glue does not make any attempt to ensure a one-to-one binding between userdata and the objects pointed to; it is possible to push a C++ object into Lua several times and end up with several distinct userdata. Glue does, by default, install an __eq metamethod that checks the equality of the boxed pointer (rather than the userdata themselves), whose implementation can be overridden by specializing Glue<>::usr_eq. Glue also offers two methods (usr_set_reference_map and usr_set_reference_map) to implement a one-to-one correspondence between userdata and boxed pointers.

If the reference map is not used, the possibility of multiple pushes may lead to memory errors.

Listing

The full listing can be found in the LuaAV source (lua_glue.h), but here’s a short version:

template&lt;&gt;
class Glue {
public:
/*
required hook to define metatable name
*/

static const char * usr_name();

/*
optional hook to define metatable superclass name
*/

static const char * usr_supername();

/*
optional hook to define a create function (default returns NULL)
arguments at stack indices 1+
*/

static T * usr_new(lua_State * L);

/*
optional hook to specify __gc method (default is no action)
NB: multiple calls to push() the same object will result in an equal
number of calls to gc() the same object (i.e. some form of reference
counting may be appropriate)
*/

static void usr_gc(lua_State * L, T * u);

/*
optional hook to apply additional behavior when pushing a T into Lua
(e.g. reference count increment, create userdata environment, etc.)
userdata is at stack index -1
*/

static void usr_push(lua_State * L, T * u);

/*
optional hook to signal the use of usr_index.  If defined,
the __index metamethod will us usr_index instead of directly
using the metatable
*/

static bool usr_has_index();

/*
optional hook to specify __index method (default is to use the metatable)
key will be at stack index 2
(userdata itself is at index 1)

NOTE: usr_has_index() must return true if this is used
*/

static void usr_index(lua_State * L, T * u);

/*
optional hook to specify __newindex method (default is to signal an error)
key will be at stack index 2, value will be at stack index 3
(userdata itself is at index 1)
*/

static void usr_newindex(lua_State * L, T * u);

/*
optional hook to specify __eq method
(default tests equality of pointers a and b)
NB: Will only apply for objects of the same T (not superclasses)
The Lua manual states that the __eq metamethod "only is selected
when both objects being compared have the same type and the same
metamethod for the selected operation."
*/

static bool usr_eq(lua_State * L, T * a, T * b);

/*
optional hook to convert non-userdata value at stack index idx to a T
e.g. convert a number into a T object...
*/

static T * usr_reinterpret(lua_State * L, int idx);

/*
optional extra hook when retrieving a T from Lua (invoked by Glue::to(), Glue::checkto() etc.)
this is an opportunity to insert extra conditions on verifying the userdata as of valid type
(e.g. checking for a magic number within u)
return u if the condition is met; return NULL if not.
(Lua userdata is at stack index idx)
*/

static T * usr_to(lua_State * L, T * u, int idx);

/*
optional hook to override the default __tostring method
*/

static int usr_tostring(lua_State * L, T * u);

/*
optional hook to add additional fields to metatable
metatable is at stack index -1
*/

static void usr_mt(lua_State * L);

/*  create the metatable
if superclass != NULL, metatable will inherit from the superclass metatable
(which must already be published)
call this e.g. in luaopen_xxx
*/

static void define(lua_State * L);

/*  register either
the constructor function (create; usr_new must be defined)
or the metatable itself
to the table at stack index -1, under the name usr_name
call this e.g. in luaopen_xxx
*/

static void register_ctor(lua_State * L);
static void register_table(lua_State * L);

/*
make the metatable callable with __call
*/

static void register_class(lua_State * L);

/*  Install additional methods to metatable via a luaL_Reg array */
static void usr_lib(lua_State * L, const luaL_Reg * lib);

/*
push a T pointer to the Lua space (also calls usr_push if defined)
NB: pushing the same object will create a new userdatum for each push
*/

static int push(lua_State * L, T * u);
/*  if index idx is a T (checks metatable key), returns it, else return NULL */
static T * to(lua_State * L, int idx = 1);
/*  as above but throws error if not found */
static T * checkto(lua_State * L, int idx = 1);
/*  Lua bound constructor (usr_new must be defined) */
static int create(lua_State * L);
/*  zero the pointer in the userdata */
static void erase(lua_State * L, int idx);

/*
Intended for use in a custom usr_mt function for registering userdata
functions that operate like table fields as opposed to function calls.
Additionally adds functionality for per-userdata custom fields with __newindex.
*/

static void usr_attr_mt(
lua_State *L,
luaL_reg *methods,
luaL_reg *getters = NULL,
luaL_reg *setters = NULL
);

/*
Intended for use in a custom usr_push.  It adds an environment table to the
userdata where per-userdata custom fields are stored.  An environment table is
required if usr_attr_index and usr_attr_newindex are also used.
*/

static void usr_attr_push(lua_State * L);

/*
Intended for use in usr_index.  First checks the enviroment table and then the
metatable.
*/

static int usr_attr_index(lua_State *L, T *u);

/*
Intended for use in usr_newindex.  First checks the metatable and then uses the
environment table.
*/

static int usr_attr_newindex(lua_State *L, T *u);

/*
For userdata with attributes, prototype constructors (table constructors) can
be used to automatically set settable attributes.  This function checks to see
if the argument passed to usr_new is a prototype argument.
*/

static bool usr_attr_is_prototype(lua_State *L);

/*
Uses a prototype constructor to set a userdata's attributes.  Intended to be used
in usr_attr_push where index 1 is the prototype table and index -1 is the userdata.
usr_attr_prototype will call usr_attr_is_prototype to check if it can be used.
*/

static int usr_attr_prototype(lua_State *L);

/*
Utility functions for mapping pointers to userdata values.  Used most often in callback
situations when you need to go from a pointer to the actual userdata value in Lua
and don't want to create a totally new userdata.
*/

static int usr_set_reference_map(lua_State *L, T *u);
static int usr_get_reference_map(lua_State *L, T *u);

// internal methods:
static const char * mt_name(lua_State * L);
static int gc(lua_State * L);
static int tostring(lua_State * L);
static int newindex(lua_State * L);
static int index(lua_State * L);
static int eq(lua_State * L);
private:
// internal methods:
static int class_create(lua_State *L);
// object wrapper:
struct Box { T * ptr; };
};
This entry was posted in Development. Bookmark the permalink.

2 Responses to Glue: Binding C++ objects to Lua via Type Traits

  1. Pingback: terrance

  2. Pingback: max

Leave a Reply