int(T::*)(lua_State*)
can be registered. But as I'll show, this restriction can be overcome. The end result is a clean interface to register classes, and familiar Lua table semantics of classes in Lua. The solution explained here is based on a template class that I wrote called
Luna.
int()(lua_State*)
, that is, a function that takes an lua_State
pointer as an argument and returns an integer. Actually, that is the only C data type that Lua supports in registering. To register any other type you have to use the extension mechanisms that Lua provides, tag methods, closures, etc. In building a solution that allows us to register C++ classes to Lua, we must make use of those extension mechanisms.
Class registration is accomplished by registering a table constructor function with the name of the class. A table constructor function is a static method of the template class that returns a table object.
Note: Static class member functions are compatible with C functions, assuming their signatures are the same, thus we can register them in Lua. The code snippets below are member functions of a template class, 'T' is the class being bound.
static void Register(lua_State* L) { lua_pushcfunction(L, &Luna<T>::constructor); lua_setglobal(L, T::className); if (otag == 0) { otag = lua_newtag(L); lua_pushcfunction(L, &Luna<T>::gc_obj); lua_settagmethod(L, otag, "gc"); /* tm to release objects */ } }Object instantiation is accomplished by passing any arguments the user passed to the table constructor function to the constructor of the C++ object, creating a table that represents the object, registering any member functions of the class to that table, and finally returning the table to Lua. The object pointer is stored as a userdata in the table at index 0. The index into the member function map is stored as a closure value for each function. More on the member function map later.
static int constructor(lua_State* L) { T* obj= new T(L); /* new T */ /* user is expected to remove any values from stack */ lua_newtable(L); /* new table object */ lua_pushnumber(L, 0); /* userdata obj at index 0 */ lua_pushusertag(L, obj, otag); /* have gc call tm */ lua_settable(L, -3); /* register the member functions */ for (int i=0; T::Register[i].name; i++) { lua_pushstring(L, T::Register[i].name); lua_pushnumber(L, i); lua_pushcclosure(L, &Luna<T>::thunk, 1); lua_settable(L, -3); } return 1; /* return the table object */ }Unlike C functions, C++ member functions require an object of the class for the function to be called. Member function calling is accomplished by a function that "thunks" the calls by acquiring the object pointer and member function pointer and making the actual call. The member function pointer is indexed from the member function map by the closure value, the object pointer from the table at index 0. Note that all class functions in Lua are registered with this function.
static int thunk(lua_State* L) { /* stack = closure(-1), [args...], 'self' table(1) */ int i = static_cast<int>(lua_tonumber(L,-1)); lua_pushnumber(L, 0); /* userdata object at index 0 */ lua_gettable(L, 1); T* obj = static_cast<T*>(lua_touserdata(L,-1)); lua_pop(L, 2); /* pop closure value and obj */ return (obj->*(T::Register[i].mfunc))(L); }Garbage collection is accomplished by setting a garbage collection tag method for the userdata in the table. When the garbage collector is run the 'gc' tag method will be called which simply deletes the object. The 'gc' tag method is registered during class registration with a new tag. At object instantiation above, the userdata is tagged with the tag value.
static int gc_obj(lua_State* L) { T* obj = static_cast<T*>(lua_touserdata(L, -1)); delete obj; return 0; }With that in mind, there are a few requirements that a class must comply with as you may have noticed:
lua_State*
int(T::*)(lua_State*)
public static const char[]
member called className
public static const Luna<T>::RegType[]
member called Register
Luna<T>::RegType
is a function map. name
is the name of the function that the member function mfunc
will be registered as.
struct RegType { const char* name; const int(T::*mfunc)(lua_State*); };Here's an example of how to register a C++ class to Lua. A call to
Luna<T>::Register()
will register the class; the only public interface of the template class. To use the class in Lua you create an instance of it by calling its table constructor function.
class Account { double m_balance; public: Account(lua_State* L) { /* constructor table at top of stack */ lua_pushstring(L, "balance"); lua_gettable(L, -2); m_balance = lua_tonumber(L, -1); lua_pop(L, 2); /* pop constructor table and balance */ } int deposit(lua_State* L) { m_balance += lua_tonumber(L, -1); lua_pop(L, 1); return 0; } int withdraw(lua_State* L) { m_balance -= lua_tonumber(L, -1); lua_pop(L, 1); return 0; } int balance(lua_State* L) { lua_pushnumber(L, m_balance); return 1; } static const char[] className; static const Luna<Account>::RegType Register }; const char[] Account::className = "Account"; const Luna<Account>::RegType Account::Register[] = { { "deposit", &Account::deposit }, { "withdraw", &Account::withdraw }, { "balance", &Account::balance }, { 0 } }; [...] /* Register the class Account with state L */ Luna<Account>::Register(L); -- In Lua -- create an Account object local account = Account{ balance = 100 } account:deposit(50) account:withdraw(25) local b = account:balance()The table of an Account instance looks like this:
0 = userdata(6): 0x804df80 balance = function: 0x804ec10 withdraw = function: 0x804ebf0 deposit = function: 0x804f9c8
The thunk mechanism is the core of the class, as it "thunks" the call. It does so by taking the object pointer from the table the function call is associated to, and indexing the member function map for the member function pointer. (Lua table function calls of table:function()
is syntactic sugar for table.function(table)
. When the call is made Lua first pushes the table on the stack, then any arguments). The member function index is a closure value, pushed onto the stack last (after any arguments). Initially I had the object pointer as a closure which meant having 2 closure values, a pointer to object(a void*) and the member function index(an int) for every class instantiated for every function; which seemed rather costly but provided quick access to the object pointer. As well, a userdata object in the table for garbage collection purposes was required. In the end I opted to index the table for the object pointer and save on resources, as a result increasing the function call overhead; a table lookup for the object pointer.
All facts considered, the implementation makes use of only a few of Lua's available extension mechanisms, closures for holding the index to the member function, the 'gc' tag method for garbage collection, and function registration for table constructor and member function calls.
Why allow only member functions with the signature int(T::*)(lua_State*)
to be registered? This allows your member functions to interact directly with Lua; retrieving arguments and returning values to Lua, calling any Lua API function, etc. Moreover, it provides an identical interface that C functions have when registered to Lua, making it easier for those wishing to use C++.
Objects are simply new'ed when created, more control as to how an object is created should be given to the user. For example, the user may wish to register a singleton class. One solution is to have the user implement a static create()
member function that returns a pointer to the object. This way the user may implement a singleton class, simply allocate the object via new, or anything else. The constructor
function could be modified to call create()
rather than new
to get an object pointer. This pushes more policy unto the class but is much more flexible. A "hook" for garbage collection may be of use to some as well.
The full source of the template class, around 70 lines of source, is available from the Lua add-ons page.