Evengard
Evengard

Reputation: 746

lua/C++ binding of objects with static members

I am very interested in the way how can I translate a C++ class into Lua class. And I found a great helper class - LunaWrapper (Description here)

But it seems, that in Lua, the only thing that persist of the class itself (not talking about resulting objects here!) is only the constructor. That is, I can't call any static functions if any. (for example, let's add to the example which is described on LunaWrapper page a function:

static bool Foo::bar(const char* text)
{
    printf("in bar\n");
}
)

So, for example, what I want to do in Lua:


local foo = Foo()
foo:foo()
Foo.bar() -- this should output "in bar", but this wont work! And this is what I want.
foo.bar() -- this should also output "in bar", but this is probably going to work.

How do I do that?

Upvotes: 0

Views: 1457

Answers (1)

mtsvetkov
mtsvetkov

Reputation: 843

After a bit of playing around, this is what I came up with:

First, add another struct to Luna and define the static functions in another table within your class (just to match the style of the LunaWrapper code, you can use whatever way you like to get that information:

struct StaticRegType {
  const char *name;
  int(*mfunc)(lua_State*); // pointers to static members are C-style func pointers
                           // and are incompatible with int(T::*mfunc)() ones
};
...
static const Luna<Foo>::StaticRegType StaticRegister[];
...
const Luna<Foo>::StaticRegType Foo::StaticRegister[] = {
   { "bar", &Foo::bar },
   { 0 }
};

Now for the interesting part. Unfortunately, it turns out you have to change quite a bit of LunaWrapper to get it to do what you want. Instead of making a function called Foo that calls the constructor, we're going to make a table named Foo and attach a metatable with a __call method, so that we maintain the Foo() constructor syntax (this goes in Luna::Register):

lua_newtable(L);
... // here we'll insert our manipulations of the table
lua_setglobal(L, T::className); // setglobal will pop the table off the stack
                                // so we'll do whatever we want to it and then give it
                                // a name to save ourselves the extra lookup

Creating the metatable and adding the constructor and garbage collection functions:

luaL_newmetatable(L, T::className);
lua_pushstring(L, "__gc");
lua_pushcfunction(L, &Luna<T>::gc_obj);
lua_settable(L, -3);
lua_pushstring(L, "__call");
lua_pushcfunction(L, &Luna<T>::constructor);
lua_settable(L, -3);

Now we need to add all of our methods. We're going to use the __index metamethod - two options: 1. set __index to a cfunction that takes the name we tried to call from lua and runs the function, or 2. set __index to a table that contains all of our functions (see this for more information on both options). I prefer the latter since it saves us from looping through all our functions, doing pesky string comparisons and we can do some copy-pasta and reuse the closures from Luna. It does however require us to make a new staticThunk function that will handle our new methods:

static int staticThunk(lua_State *L) {
  int i = (int)lua_tonumber(L, lua_upvalueindex(1));
  return (*(T::StaticRegister[i].mfunc))(L);
}

Note it's quite a bit simpler since we don't need to fetch the object on which we're calling the function (I'm also rather fond of the template abstraction that let's the compiler handle the specifics of triggering the right function for our object, but that was just a good decision on the Luna author's part ;) ).

Now we need to make a table for __index and add the methods to it.

lua_pushstring(L,"__index"));
lua_newtable(L);
// adding the normal methods is the same as in Luna (the for-loop over T::Register)
// add the static methods by going over our new StaticRegister array (remember to use
// staticThunk for them and thunk for the members
lua_settable(L, -3); // push the __index table to the metatable

Almost there... Here's the metatable trickery - we have built the Foo table (even if it's not really named Foo just yet) and now we are going to set the metatable we build for our objects as it's metatable too. This way we can do both Foo.bar() and local foo = Foo(); foo.bar():

lua_setmetatable(L, -2); // at this point the stack contains our metatable right under
                         // our table
lua_setglobal(L, T::className); // name the darn thing

The last thing you need to do is get rid of anything in Luna::constructor that isn't related to actually constructing the object (added benefit - Register actually registers the object type and constructor actually just allocates it, the way it should be if you ask me). We moved the loop that was originally in here to the Register function. And we're done!

Note: One drawback of this solution is that while for convenience it allows you to call both Foo.bar() and foo.bar() it also allows you to call Foo.foo(), which doesn't make sense and would be caught as an illegal attempt to call a member function during compilation of an equivalent C/C++ program, but will fail with a runtime error in Lua. If you want to get rid of this behavior you would have to create one metatable for the Foo table that does not include the member functions and another for the objects you create that includes them.

Upvotes: 1

Related Questions