Lua 中写 C 扩展库时用到的一些技巧

Lua 中写 C 扩展库时用到的一些技巧(转)

通常,C 扩展库中 C 代码会有一些数据要放在 lua 状态机中。Lua 提供的方案是放在它的 注册表 中。如文档所言,因为 Lua 的注册表是全局共享的,选择 key 的时候就要千万小心了。整数 key 已经被 reference 系统用掉了,一般我们会采用字符串作 key 。

从 C 中压入字符串的效率不是最高,这是因为外部字符串进入状态机时需要重新 hash 并检查唯一性所致。关于避免直接压入字符串的问题,以前写过一篇 blog 谈过。( btw, 以前那个方法也不是最好的解决方案,不过还是可以作为一个参考的 :)

很容易想到,最方便且能保证唯一性的 key 是一个 light userdata 。这一点,在参考手册以及 Programming in Lua 中都有提到。

我们可以用一个 static 变量的地址作为 key 在注册表做索引保存需要的值。比如如下代码:

static const void *key = 0;

lua_pushlightuserdata(L, (void *)&key);
lua_pushnumber(L, myNumber);
lua_settable(L, LUA_REGISTRYINDEX);

这样就把一个 C 中的数字 myNumber 放在注册表中了。如果以后要读出来当然可以用这样的代码:

lua_pushlightuserdata(L, (void *)&key);
lua_gettable(L, LUA_REGISTRYINDEX);
myNumber = lua_tonumber(L, -1);

这里用的是一个 static 变量的地址作 key ,所以绝对不会和被的扩展库冲突。但是这样做有一个缺点,就是 key 这个东西以一个全局变量形式出现,不太美观。而且,当我们想用到这个 key 的时候,不是那么容易拿到。要么,你得 extern 出这个 key ;要么你需要以一个单件的形式暴露一个函数拿到这个 key ,效率上或许又打了一些折扣了。

现在就给出一个折中的方案,或许能让绝大多数人满意: 那就是,以一个 light userdata 做 key ,这个 key 本身再和一个唯一字符串关联起来。每次再需要拿到 key 指针的函数中这些写:

static const void *key = 0;

if (key==0) {
    lua_getfield (L, LUA_REGISTRYINDEX, "MyExtensionLibrary");
    key=lua_touserdata(L,-1);
    lua_pop(L,1);
    if (key==0) {
        key=(void*)&key;
        lua_pushlightuserdata(L,key);
        lua_setfield(L, LUA_REGISTRYINDEX, "MyExtensionLibrary");
    }
}

这样,我们就可以对 key 做一次惰性初始化了。以上只是示例,初始化部分可以做成一个函数和宏方便重复调用。


顺便再提一个容易被忽略的小技巧: 我们通常用 full userdata 来实现复杂的 C 结构,或是 C++ 中的对象,把它们用于 Lua 中。经常我们需要把 Lua 中的一堆数据关联在这个 userdata 上。为了让 Lua 的 gc 过程可以正确的工作,我们需要做一些额外的工作。

最下策的解决方案是,在 userdata 中保存一个相关的 Lua 表的 reference ,然后给 userdata 加上一个元表,实现一个 gc 的元方法。在 gc 事件发生时,unref 掉这个 Lua 表。这个方案最糟糕的地方在于,实现过于繁琐。而且给 gc 过程带来许多额外的执行开销。

中策是在注册表中保留一个映射表,并把它设置为 weak table 。每次 userdata 创建出来,就以 userdata 为 key 在这个影射表中保存相关联的 Lua 数据。那么这样,垃圾回收的过程就可以由 Lua 本身自行维护了。其不方便之处在于,每次 C 函数想访问 userdata 相关的 Lua 数据时,需要先拿到注册表中的这个映射表(这个操作或许用的到这篇 blog 前半部分提到的东西)。

其实我们有一个上策,Lua 设计的时候就考虑到了。那就是 full userdata 可以拥有一个独立的环境表。这个东西对 Lua 本身毫无意义。但是却参与 gc 。我们只需要用 lua_getfenvlua_setfenv 来读写这个表就够了 :D