最近写的代码中出了一个奇怪的 bug ,很难调试出来。经过一个晚上的挣扎,终于发现了问题。
第一个问题,在 C 函数中,不能随意的时候 lua_State 中的虚拟机堆栈,如果需要大量使用堆栈,应该先调用 lua_checkstack 。少量使用堆栈,(在 LUA_MINSTACK 20 )之下时则没有问题。这个问题其实在文档里有写,我看过忘记了 :( 不过我个人还是觉得 lua_checkstack 的语义有点奇怪,从字面上看,这个 api 不应该有副作用。它能增加可用堆栈的大小违背了 checkstack 的词义。
第二个问题,当从 lua 调用 C 函数时,当参数数量不足的时候,并不会填入 nil 作为缺省参数。比如,写了一个 C 函数,接受两个参数。当 lua 中调用这个 C 函数时,如果仅传入一个参数,那么在 C 中 stack 上 index 2 位置的值并不一定是 nil 。这个时候我们应该用 lua_gettop 得到准确的参数个数以做适当的处理,或者直接在进入 C 函数时调用一次 lua_settop(L,2) 强制堆栈扩展到两个。
第三个问题,就是一开始最为迷惑我的问题。在生成 cclosure 的时候,upvalue 不能超过 255 个。而这一点并没在文档中说明,运行时压入超过 255 个 upvalue 也不会报错。知道仔细查看源码才发现其中的秘密。
按 lua 的设计,upvalue 的个数应该只受内存大小的限制。但是我的程序在生成一个拥有 258 个 upvalue 的 cclosure 时,upvalue 的个数变成了奇怪的 2 个。我的直觉告诉我,2 == 258%256 。查看源码证实了我的想法,cfunction 的 upvalue 的数量用了一个 byte 记录。究其原因,在于 lua 的 object 用了好几个标记量方便虚拟机运作(比如 gc 用到的 mark 位),当 upvalue 的数量也用一个 byte 的时候,刚好四个 byte 凑齐一个 32bit 字。
为什么我会用到如此之多的 upvalue ,这里就牵扯到最近发现的一个给 lua 写 C 扩展的技巧。
一般,C 对象保存在 lua 中都采用 userdata 的形式。userdata 会参与 gc 的过程,所以不必考虑内存释放的问题。但是,往往 C 对象会对 lua 中其它对象做使用上的引用,而 userdata 本身不具备这个能力。以前我的做法是,使用 lua_ref 得到一个整数引用,放在 userdata 中,到 userdata 的 __gc 被触发或者用户主动释放时,作 lua_unref 操作。这样做不太美观,也有一点点效率问题。
最近发现,简单的 C 扩展,我们完全可以用 C closure 实现,把扩展数据放在 upvalue 中。引用的 lua 对象可以直接放进 upvalue ,而其他 C 数据,可以再生成一个 userdata 写入 upvalue 。这个 closure 可以根据调用时的输入参数决定对它内部的数据做何种操作。
用这个技术,我实现了一个简单的循环队列,它会比用 lua table 实现的队列稍微高效一些。使用时,可以调用 C 扩展函数创建一个队列对象(其实是一个 c closure),然后用这个 closure 做进队列和出队列的操作。当传入参数的时候,就把参数进队列,不传参数的时候就把队首的元素出队列。同时可以根据返回值知道队列时候为空和为满。
========================================================================================
关键函数:
void *lua_newuserdata (lua_State *L, size_t size);
新建full userdata。
(1)分配一块指定大小的内存;
(2)将该full userdata压栈;
(3)返回该内存块的地址给主机程序,主机程序能够随意使用这块内存。
void luaL_argcheck (lua_State *L,int cond,int arg,const char *extramsg);
检查条件是否满足。
void luaL_newmetatable (lua_State *L, const char *tname);
创建userdata可用的metatable。
如果registry已经有tnme键值,则函数返回0;
否则,创建一个[tname, metatable],并放入registry,并返回1。
两种情况下,都会讲tname对应的值入栈。
堆栈+1
void *luaL_checkudata (lua_State *L, int index, const char *tname);
检查在栈中指定位置的对象是否为带有给定名字的metatable(registry中键tname对应的值)的usertata。是则返回userdata地址,否则返回NULL。
void luaL_getmetatable (lua_State *L, const char *tname);
获取registry中的tname对应的metatable,并入栈。注意区分lua_getmetatable函数。
void luaL_setmetatable (lua_State *L, const char *tname);
将栈顶对象的metatable设置为registry表中键tname对应的值。注意区分lua_setmetatable函数。
int lua_getmetatable (lua_State *L, int index);
获取index对应的table的metatable,并入栈。如果该table没有metatable,则返回0,且堆栈不变。
void lua_setmetatable (lua_State *L, int index);
将栈顶的table出栈并设置给index处的值作为metatable。
堆栈-1
(二)利用metatable标识userdata来增加代码的安全性
上面的C库是有缺陷的,比如我们怎么确保例子中setarray的第一个参数就是我们想要的数组userdata,而不是别的不相关的userdata呢?userdata是一种lua类型,它可以用来表示宿主语言中的各种自定义类型对象,为了区分特定类型,我们使用的方法是:
我们单独为该数组创建一个metatable,每次创建数组userdata时,我们设置其和metatable的关联。每次我们访问数组的时候,都检查一下其是否有一个正确的metatable即可。也就是利用不同的metatable来标记不同类型的userdata。因为Lua代码不能够改变userdata的metatable,所以Lua不会伪造我们的代码。
(五)light userdata
light userdata不同于full userdata,它有如下特点:
(1)full userdata代表Lua中的C对象,light userdata代表一个C指针的值(也就是一个void *类型的值)。由于它是一个值,我们不能创建他们(同样的,我们也不能创建一个数字)。
(2)仅仅是一个指针,像数字一样,没有metatables,light userdata不需要垃圾收集器来管理她。
(3)可以用于表示不同类型的对象,我们在Lua中使用light userdata表示C对象。
因为它是一个值,任何指向同一个C地址的light userdata都相等。
void lua_pushlightuserdata (lua_State *L, void *p);
将一个light userdata入栈。
(六)userdata相关的资源释放
Lua以__gc元方法的方式提供了finalizers。这个元方法只对userdata类型的值有效。当一个userdata将被收集的时候,并且userdata有一个__gc域,Lua会调用这个域的值(应该是一个函数):以userdata作为这个函数的参数调用。这个函数负责释放与userdata相关的所有资源,比如说文件描述符、窗口句柄等。