快速掌握Lua 5.3 —— 编写提供给Lua使用的C库函数的技巧

Posted VermillionTear

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了快速掌握Lua 5.3 —— 编写提供给Lua使用的C库函数的技巧相关的知识,希望对你有一定的参考价值。

Q:什么是”registry”?

A:有时候,我们需要在程序中使用一些非局部的变量。在C中我们可以使用全局变量或是静态变量来实现,而在为Lua编写C库的过程中,使用以上类型的变量并不是一个好的方式。首先,这些变量中无法存储Lua的值。其次,这些变量如果在多个Lua状态机中被使用,则很可能造成非预期的结果。
一个替代方案是,将这些值存储在Lua的全局变量中。这种方式解决了上面提到的两个问题,Lua全局变量可以存储任何Lua的值,同时每一个Lua状态机都有自己独立的一套全局变量。但这依旧不是最好的方式,因为是Lua的全局变量,Lua程序可以随意的修改变量的值,这很可能对C库中的函数在使用这些变量时造成影响。
为了进一步避免上述情况,Lua提供了一张特殊的”table”,它可以供C代码随意使用。但是对于Lua代码,访问却是被禁止的。这个特殊的”table”便是”registry”,

Q:什么是”pseudo-index”?

A:”pseudo-index”类似于虚拟栈中正常的索引,但其与正常索引的区别在于,虽然使用它也是通过虚拟栈,但是其所对应的值并不是存储在虚拟栈中。
LUA_REGISTRYINDEX就是一个”pseudo-index”,定义在”lua.h”中,它用于通过虚拟栈访问”registry”(但”registry”并非实际存储在虚拟栈中),使用时按照虚拟栈中正常索引的使用方式使用。
例如,你可以通过如下方式获取”registry”中索引为”Key”的元素的值,

lua_pushstring(L, "Key");
lua_gettable(L, LUA_REGISTRYINDEX);

Q:如何保证”registry”中的索引唯一?

A:”registry”就是一个普通的Lua的”table”,因此你可以像访问其他”table”一样使用非nil的值作为”key”存取它的元素。然而,由于所有的C库共享相同的”registry”,你必须注意使用什么样的值作为”key”才不会导致命名冲突。
一个防止命名冲突的方法是使用”static”变量的地址作为”key”,这样C链接器就会保证这个地址在所有的库中唯一。
“mylib.c”文件中(lua_pushlightuserdata将一个代表C指针的值放到虚拟栈内,之后的章节会详细介绍):

#include <stdio.h>
#include <string.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

static int l_test_registry(lua_State *L)

    // "key"变量存储什么值不重要,因为用到的是它本身的地址。
    static const char key = 'k';
    static const char key1 = 'k';

    int my_number = 0;

    lua_pushlightuserdata(L, (void *)&key);    // 将"key"入栈。
    lua_pushnumber(L, 9);    // 将"value"入栈。
    lua_settable(L, LUA_REGISTRYINDEX);    // "registry[key] = value"。

    // 不同的"key",操作"registry"中不同的元素。
    lua_pushlightuserdata(L, (void *)&key1);
    lua_pushnumber(L, 10);
    lua_settable(L, LUA_REGISTRYINDEX);

    // 相同的"key",操作"registry"中相同的元素。
    lua_pushlightuserdata(L, (void *)&key);
    lua_pushnumber(L, 11);
    lua_settable(L, LUA_REGISTRYINDEX);

    lua_pushlightuserdata(L, (void *)&key);    // 将"key"入栈。
    lua_gettable(L, LUA_REGISTRYINDEX);    // "registry[key]"。
    my_number = lua_tonumber(L, -1);    // 获得虚拟栈顶的结果。
    printf("%d\\n", my_number);    // 11

    lua_pushlightuserdata(L, (void *)&key1);
    lua_gettable(L, LUA_REGISTRYINDEX);
    my_number = lua_tonumber(L, -1);
    printf("%d\\n", my_number);    // 10

    return 0;    // 没有返回值。


static const struct luaL_Reg mylib[] = 
    "my_test_registry", l_test_registry,
    NULL, NULL
;

extern int luaopen_mylib(lua_State* L)

    luaL_newlib(L, mylib);

    return 1;

将“mylib.c”编译为动态连接库,

prompt> gcc mylib.c -fPIC -shared -o mylib.so -Wall
prompt> ls
mylib.c    mylib.so    a.lua

“a.lua”文件中:

local mylib = require "mylib"

mylib.my_test_registry()
--[[ results: 
11
10
]]

当然你也可以使用字符串作为”registry”的”key”,只要你保证这些字符串唯一。当你打算允许其他的独立库访问你的数据时,字符串的”key”会非常有用(因为其他独立库可以明确的知道”key”的名字)。
最后,不要使用数值作为”registry”的”key”,因为他们是供”reference system”使用的。

Q:什么是”reference system”?

A:”reference system”由一对儿定义在辅助库中的函数组成。使用他们,你可以无需关心如何创建唯一的”key”,便可以在”registry”中自由的存取数据。

/* 生成一个唯一的"key",将栈顶的值出栈作为"value",
 * 从虚拟栈的索引"t"处获得"table",之后做相当于"table[key] = value"的操作。
 * 函数返回生成的"key"。
 * 如果"value""nil",则不会生成"key",同时函数返回"LUA_REFNIL"。
 * 同时Lua还定义了"LUA_NOREF",它代表一个与"luaL_ref"所返回的任何值都不同的值。
 */
int luaL_ref(lua_State *L, int t);

/* 从虚拟栈的索引"t"处获得"table",之后做相当于"table[ref] = nil"的操作。
 * "ref"同时也被释放(可以再次供"luaL_ref"使用)。
 * 如果"ref""LUA_NOREF"或是"LUA_REFNIL",则函数不做任何操作。
 */
void luaL_unref(lua_State *L, int t, int ref);

luaL_ref所返回的”key”称之为”reference”。

static int l_test_registry(lua_State *L)

    int ref = 0, value = 0;

    lua_pushinteger(L, 9);    // 数据入栈。
    // 将数据存入"registry",并得到数据所对应的"reference"。
    ref = luaL_ref(L, LUA_REGISTRYINDEX);
    // 从"registry"中获取"reference"所对应的数据,并将其入栈。
    lua_rawgeti(L, LUA_REGISTRYINDEX, ref);
    value = lua_tointeger(L, -1);    // 获取栈顶的数据。
    printf("%d\\n", value);    // 9
    luaL_unref(L, LUA_REGISTRYINDEX, ref);    // 释放"reference"。

    return 0;

Q:如何在C库函数中实现”Closure”?

A:”registry”为C库函数提供了一种实现全局变量的合理有效的方式,而”Closure”将为C库函数提供一种实现静态变量的合理有效的方式。
与Lua中的”Closure”类似,你可以为C库函数绑定多个”upvalue”,每一个”upvalue”都可以存储一个Lua值。之后当C库函数的这个”Closure”被调用时,它就可以自由的访问它的”upvalues”(通过”pseudo-indices”访问到)。

/* 创建一个C中的"Closure",将栈顶的"n"个值作为其"upvalues"。
 * 弹出栈顶作为"upvalues""n"个值,最后将"Closure"入栈。
 */
void lua_pushcclosure(lua_State *L, lua_CFunction fn, int n);

// 返回当前运行的函数的第"i""upvalue""pseudo-indice"int lua_upvalueindex(int i);

接下来的例子,我们将在C库函数中重新实现newCounter(之前在快速掌握Lua 5.3 —— 函数 的“什么是”Closures”?”部分使用Lua代码实现过)。
“mylib.c”文件中:

#include <stdio.h>
#include <string.h>
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

// "Closure"的函数代码部分。
static int counter(lua_State *L)

    // 获取"Closure"的第一个"upvalue"。
    double val = lua_tonumber(L, lua_upvalueindex(1));
    /* 将该"upvalue"加1,然后入栈。
     * (这里还没有实际的将"Closure"的"upvalue"更新)
     * 该值将作为"Closure"的返回值。
     */
    lua_pushnumber(L, ++val);
    /* 将新的"upvalue"的值复制一份儿,然后入栈。
     * (因为下面"lua_replace"会弹出此值)
     * 该值将用于实际更新"Closure"的"upvalue"值。
     */
    lua_pushvalue(L, -1);
    /* 获取"Closure"的"upvalue",并实际更新它。
     * (同时会弹出栈顶用于更新"upvalue"的值)
     */
    lua_replace(L, lua_upvalueindex(1));

    return 1;    // 返回最新的"upvalue"值。


// 创建一个C中的"Closure"。
static int l_newCounter(lua_State *L)

    lua_pushnumber(L, 0);    // "upvalue"入栈,初始值为0。
    // "counter"函数作为"Closure"的函数代码部分,它有一个"upvalue"。
    lua_pushcclosure(L, &counter, 1);

    return 1;    // 将创建的"Closure"作为返回值返回。


static const struct luaL_Reg mylib[] = 
    "my_newCounter", l_newCounter,
    NULL, NULL
;

extern int luaopen_mylib(lua_State* L)

    luaL_newlib(L, mylib);

    return 1;

将“mylib.c”编译为动态连接库,

prompt> gcc mylib.c -fPIC -shared -o mylib.so -Wall
prompt> ls
mylib.c    mylib.so    a.lua

“a.lua”文件中:

local mylib = require "mylib"

newCounter1 = mylib.my_newCounter()
print(newCounter1())    --> 1.0
print(newCounter1())    --> 2.0
newCounter2 = mylib.my_newCounter()
print(newCounter2())    --> 1.0
print(newCounter1())    --> 3.0

附加:

1、Lua API中大部分函数可以接受”pseudo-index”,除了一些会操作虚拟栈本身的函数,例如lua_removelua_insert等。
2、当使用字符串作为”registry”的”key”时,一些好的习惯可以避免”key”的冲突。比如使用库的名称作为前缀,或是使用”universal unique identifier(uuid)”。很多系统都有专门的工具来产生”uuid”,比如Linux系统下的”uuidgen”。
3、实际上,”reference system”可以应用于任何的”table”,但是他们典型的应用是在”registry”中。
4、可以通过相同的函数代码,绑定不同的”upvalues”,从而创建不同的”Closures”。C库函数中的”Closure”同样如此。
5、C中的”Closure”与Lua中的”Closure”不同之处在于,C中的”Closure”不能共享”upvalues”,每一个”Closure”都有自己独立的”upvalues”。然而,我们可以设置不同”Closure”的”upvalues”指向同一个”table”,这样这个”table”就变成了一个所有”Closure”共享数据的地方。

以上是关于快速掌握Lua 5.3 —— 编写提供给Lua使用的C库函数的技巧的主要内容,如果未能解决你的问题,请参考以下文章

快速掌握Lua 5.3 —— userdata

快速掌握Lua 5.3 —— 资源管理

快速掌握Lua 5.3 —— 扩展你的程序

快速掌握Lua 5.3 —— 字符串库

快速掌握Lua 5.3 —— 从Lua中调用C函数

快速掌握Lua 5.3 —— userdata