Luna作为一套比较出名的Lua与C++绑定的框架,以其简洁的代码且贴近面向对象的书写方式,与游戏脚本的需求重合性较高,因此被广泛应用在游戏行业中。
然而比较有趣的是,虽然该框架用途广泛,但是很少人知道这套框架的作者,甚至不知道框架的名字。

2003年,Lenny PalozziLua社区中发表了名为:《A template class for binding C++ to Lua》的文章。文章中,作者介绍了一种C++与Lua绑定的新框架,并将这个框架命名为Luna。

文中提出了几个思路:

  1. 将构造函数以className为key存到全局表中,当用户调用className()时,也就会显式调用C++的构造函数
  2. 在构造函数中,为用户生成一个userdata,将类对象指针存入其中,同时将类的函数指针以函数名为key也存入其中,返回该userdata给用户在lua中使用,作为Lua曾的类对象。
  3. 当用户以面向对象的方式,对该类对象执行<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框架对大家来说并不是什么难事(笑

最后修改:2020 年 10 月 27 日 01 : 32 AM
如果觉得我的文章对你有用,请随意赞赏