Luna作为一套比较出名的Lua与C++绑定的框架,以其简洁的代码且贴近面向对象的书写方式,与游戏脚本的需求重合性较高,因此被广泛应用在游戏行业中。
然而比较有趣的是,虽然该框架用途广泛,但是很少人知道这套框架的作者,甚至不知道框架的名字。
2003年,Lenny Palozzi
在Lua社区中发表了名为:《A template class for binding C++ to Lua》的文章。文章中,作者介绍了一种C++与Lua绑定的新框架,并将这个框架命名为Luna。
文中提出了几个思路:
- 将构造函数以className为key存到全局表中,当用户调用className()时,也就会显式调用C++的构造函数
- 在构造函数中,为用户生成一个userdata,将类对象指针存入其中,同时将类的函数指针以函数名为key也存入其中,返回该userdata给用户在lua中使用,作为Lua曾的类对象。
- 当用户以面向对象的方式,对该类对象执行<userdata>.<functionName>操作时,会触发C++中的函数,C++会从table取出类指针以及函数指针,以正确的执行类成员函数。
这些也就是Luna的核心思维,后来很多人尝试对Luna框架进行了优化(包括之后会提到的Lunar),但是最终都没有完全脱离这套模式。
《A template class for binding C++ to Lua》一文虽然已经写得足够详细,但是涉及的Lua版本过低,且其代码泛化不足。Lua5作为用途最广泛的版本,大部分人自然更需要以Lua5版本的Luna。5年后的2008年,《Simpler Cpp Binding》这篇文章应运而生。
算法
本文主要以《Simpler Cpp Binding》作为分析的参考。
定义
定义类名
类名之后需要注册到全局表中,让Lua使用者能调用到构造函数
static const char className[];
const char Account::className[] = "Account";
定义需要导出的函数
为了安全性与实用性,并不需要把所有C++函数都导出给Lua使用者使用
#define method(class, name) {#name, &class::name}
Luna<Account>::RegType Account::methods[] = {
method(Account, deposit),
method(Account, withdraw),
method(Account, balance),
{0,0}
};
RegType是一个结构体数组,以{函数名,函数指针}的形式保存所有需要导出的函数。
注册
面向对象的类,实际上是一组数据(变量)和定义在这种数据上的操作方法(函数)组成的。Luna也是参照这一理念涉及的,它将函数放在全局对象上,而数据放在一个table中交给用户使用。
注册Lua类对象(数据)
luaL_newmetatable(L, T::className);
lua_pushliteral(L, "__metatable");
lua_pushvalue(L, methods);
lua_settable(L, metatable); // hide metatable from Lua getmetatable()
lua_pushliteral(L, "__index");
lua_pushvalue(L, methods);
lua_settable(L, metatable);
lua_pushliteral(L, "__tostring");
lua_pushcfunction(L, tostring_T);
lua_settable(L, metatable);
lua_pushliteral(L, "__gc");
lua_pushcfunction(L, gc_T);
lua_settable(L, metatable);
metatable重定向了table中一些重要的元操作,将会作为Lua类对象(实际上是一个userdata)的主元表,其具体结构如下:
$\text{metatable} \begin{cases} \text{__metatable = method} \\ \text{__index = method} \\ \text{__tostring = tostring_T} \\ \text{__gc = gc_T} \end{cases}$
注册全局对象(函数)
lua_newtable(L);
int methods = lua_gettop(L);
lua_pushstring(L, T::className);
lua_pushvalue(L, methods);
lua_settable(L, LUA_GLOBALSINDEX);
lua_newtable(L); // mt for method table
int mt = lua_gettop(L);
lua_pushliteral(L, "__call");
lua_pushcfunction(L, new_T);
lua_pushliteral(L, "new");
lua_pushvalue(L, -2); // dup new_T function
lua_settable(L, methods); // add new_T to method table
lua_settable(L, mt); // mt.__call = new_T
lua_setmetatable(L, methods);
// fill method table with methods from class T
for (RegType *l = T::methods; l->name; l++) {
/* edited by Snaily: shouldn't it be const RegType *l ... ? */
lua_pushstring(L, l->name);
lua_pushlightuserdata(L, (void*)l);
lua_pushcclosure(L, thunk, 1);
lua_settable(L, methods);
}
method以函数名为key, 函数指针的闭包为value,包含了所有需要用到的函数信息,挂在全局表的className下。其具体结构如下:
$\text{method} \begin{cases} \text{new = new_T} \\ \text{__metatable = mt } \lbrace \text{__call = new_T} \\ ... \\ \text{function} \\ ... \end{cases}$
调用
创建Lua类对象
创建对象通过<className>.new()进行,实际是上从全局表中取到<className>中存的method这个table,并执行method中的new函数,这个函数映射到了C++中的new_T。
static int new_T(lua_State *L) {
lua_remove(L, 1); // use classname:new(), instead of classname.new()
T *obj = new T(L); // call constructor for T objects
userdataType *ud =
static_cast<userdataType*>(lua_newuserdata(L, sizeof(userdataType)));
ud->pT = obj; // store pointer to object in userdata
luaL_getmetatable(L, T::className); // lookup metatable in Lua registry
lua_setmetatable(L, -2);
return 1; // userdata containing pointer to T object
}
new_T函数去创建一个新的C++对象,并把指针存入一个userdata中,将metatable作为该userdata的主元表返回给调用者,该userdata即为Lua层面的类对象。
调用Lua类方法
调用类方法通过<userdata>.<functionName>进行,该操作会触发原表中对应<functionName>的闭包,该闭包会以函数指针为upvalue执行thunk函数。
static int thunk(lua_State *L) {
// stack has userdata, followed by method args
T *obj = check(L, 1); // get 'self', or if you prefer, 'this'
lua_remove(L, 1); // remove self so member function args start at index 1
// get member function from upvalue
RegType *l = static_cast<RegType*>(lua_touserdata(L, lua_upvalueindex(1)));
return (obj->*(l->mfunc))(L); // call member function
}
thunk函数中,C++取得userdata中保存的类指针,同时从upvalue上拿到函数指针,也就可以执行该C++类对象中的对应成员函数。
优化
双向调用
Luna一个很明显的缺陷就是其调用单向性,只支持从Lua调用C++接口,无法从C++层主动触发Lua函数。
2016年出现了一篇名为《Cpp Binding With Lunar》的文章,文中在Luna的基础上加入了从C++调用Lua的流程,并将自己这套优化后的Luna框架叫做Lunar。
为了达到这个目的,作者加了两个接口Push和Call,从今天的眼光看来,这两个函数实现都比较简单。
Push接口,允许C++直接把已经存在的类对象推给Lua,无需再通过Lua的new接口创建。
static int push(lua_State *L, T *obj, bool gc=false) {
if (!obj) { lua_pushnil(L); return 0; }
luaL_getmetatable(L, T::className); // lookup metatable in Lua registry
if (lua_isnil(L, -1)) luaL_error(L, "%s missing metatable", T::className);
int mt = lua_gettop(L);
subtable(L, mt, "userdata", "v");
userdataType *ud = static_cast<userdataType*>(pushuserdata(L, obj, sizeof(userdataType)));
if (ud) {
ud->pT = obj; // store pointer to object in userdata
lua_pushvalue(L, mt);
lua_setmetatable(L, -2);
if (gc == false) {
lua_checkstack(L, 3);
subtable(L, mt, "do not trash", "k");
lua_pushvalue(L, -2);
lua_pushboolean(L, 1);
lua_settable(L, -3);
lua_pop(L, 1);
}
}
lua_replace(L, mt);
lua_settop(L, mt);
return mt; // index of userdata containing pointer to T object
}
除开一些参数合法性的检查,函数主要作用就是把传进来的obj指针生成一个带主元表的userdata放到栈上。
Call接口,允许C++直接调用堆栈上userdata中注册的函数(既可以是C++导出的,也可以是Lua脚本里自行声明的)
static int call(lua_State *L, const char *method,
int nargs=0, int nresults=LUA_MULTRET, int errfunc=0)
{
int base = lua_gettop(L) - nargs; // userdata index
if (!luaL_checkudata(L, base, T::className)) {
lua_settop(L, base-1); // drop userdata and args
lua_pushfstring(L, "not a valid %s userdata", T::className);
return -1;
}
lua_pushstring(L, method); // method name
lua_gettable(L, base); // get method from userdata
if (lua_isnil(L, -1)) { // no method?
lua_settop(L, base-1); // drop userdata and args
lua_pushfstring(L, "%s missing method '%s'", T::className, method);
return -1;
}
lua_insert(L, base); // put method under userdata, args
int status = lua_pcall(L, 1+nargs, nresults, errfunc); // call method
if (status) {
const char *msg = lua_tostring(L, -1);
if (msg == NULL) msg = "(error with no message)";
lua_pushfstring(L, "%s:%s status = %d\n%s",
T::className, method, status, msg);
lua_remove(L, base); // remove old message
return -1;
}
return lua_gettop(L) - base + 1; // number of results
}
同样的,忽略掉参数合法性检查后,该函数主要作用无非是通过lua_pcall调用userdata上的特定函数。
当然,事物的出现并被大家所接受总自然有其合理性,否则以新增2函数=新增1r来算,创造一个lunarrrrrrrr框架对大家来说并不是什么难事(笑