LUA并不支持直接访问C++的类,但是通过使用LUA的C API和C++ templates,可以在LUA注册C++类,并访问其成员,这种方法叫做Luna。
LunaWrapper是一个简单的Luna实现,它的具体做法可概括为如下:
1.初始化LunaWrapper时调用其注册函数Register,在Register中通过LUA C API把一个C函数定义为一个全局LUA函数,函数名是LUA中要访问的C++ class类名,以便能在lua中这样创建C++对象:foo = Foo()。这个C函数其实主要是new一个Foo对象出来,关于这个C函数具体实现见下文。在Register中还创建了一个关联C++类名的元表,并且把这个元表的__gc方法设置为一个C函数gc_obj,当LUA中没有谁在引用foo,垃圾回收foo时会调用gc_obj,后面可以看见 gc_obj其实是delete了这个Foo对象。代码见下:
static void Register(lua_State *L) { lua_pushcfunction(L, &Luna<T>::constructor); lua_setglobal(L, T::className); luaL_newmetatable(L, T::className); lua_pushstring(L, "__gc"); lua_pushcfunction(L, &Luna<T>::gc_obj); lua_settable(L, -3); }
C函数在C++中当然可以使用静态成员函数代替了,至于模板的使用,则方便了导出各种不同的C++类类型。总的来说Register函数是在lua加载LunaWrapper时初始化时调用的函数。
2.构造函数。构造函数当然是要创建出一个这样的C++对象,也当然还要做些事情能让LUA访问到它的成员。这里用到了LUA的userdata和metatable,userdata保存C++对象的指针,userdata自己本身则被保存在一个新建的表中,这个表后面可以看见它是构造函数的返回值,LUA中都是直接对它进行操作,也就是说我们需要在内部把LUA对它的访问转掉到C++对象的成员上来,让在LUA中看来是直接在访问C++对象。最后,代码遍历C++类的到处成员函数表,把每个函数名做为key,转调函数Luna<T>::thunk做为值添加进返回表中中。
static int constructor(lua_State *L) { T* obj = new T(L); lua_newtable(L); lua_pushnumber(L, 0); T** a = (T**)lua_newuserdata(L, sizeof(T*)); *a = obj; luaL_getmetatable(L, T::className); lua_setmetatable(L, -2); lua_settable(L, -3); // table[0] = obj; 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; }
3.转调函数。它所做的事很简单,转掉C++类的相应成元函数。这里用到了LUA的闭包C API, 把相应的成员函数在数组中的索引值与转掉函数关联起来,目的是能调用到正确的成员函数。当然对象指针也能通过table[0]轻易获得。
static int thunk(lua_State *L) { int i = (int)lua_tonumber(L, lua_upvalueindex(1)); lua_pushnumber(L, 0); lua_gettable(L, 1); T** obj = static_cast<T**>(luaL_checkudata(L, -1, T::className)); lua_remove(L, -1); return ((*obj)->*(T::Register[i].mfunc))(L); }
4.析构函数。Luna使用了lua的userdata保存了对象的指针,并且在没有谁引用userdata时,delete它保存的指针。
static int gc_obj(lua_State *L) { T** obj = static_cast<T**>(luaL_checkudata(L, -1, T::className)); //检查在栈中指定位置的对象是否为带有给定名字的metatable的usertatum delete (*obj); return 0; }
因为在构造时这个userdata被放在table[0]位置,而在lua中table对应C++对象,当lua中没有在引用这个table时,table会被gc,其key和value也会gc,而userdata gc时会释放C++对象,这样就做到了生命周期的一致。
LunaWrapper完整代码和测试例子在:https://github.com/persistentsnail/luna-test
参考资料:LunaWrapper