programming in lua (9)

第 27 章,主要介绍 c 语言和 lua 交互的一些技巧。这一章涉及到第 2 部分的一些内容。

数组操作

在 lua 中,数组是一种特殊的 table,除了可以使用 lua_settable() 和 lua_gettable() 等操作 table 的函数外,另外也有一些专门用来操作数组的函数。使用这些有针对性的函数有两个好处:一是性能上的提升,例如我们经常在循环访问数组的所有元素;二是像整数数组,字符串等常见的类型使用一些有针对性的操作会比较方便。

lua 提供了两个函数来访问指定下标的数组元素:

void lua_rawgeti (lua_State *L, int index, int key);
void lua_rawseti (lua_State *L, int index, int key);

其中 index 是 table 在栈中的位置,key 是数组的下标。当 index 为正整数时,

lua_rawgeti(L, index, key);

和下面两个调用等价:

lua_pushnumber(L, key);
lua_rawget(L, index);

因为 lua_pushnumber() 会改变栈顶的位置,如果 index 为负数,那么在调用 lua_rawget() 的时候要注意使用 index-1 而不是 index。

当 index 为正整数时,下面的调用

lua_rawseti(L, index, key);

等价于:

lua_pushnumber(L, key);
lua_insert(L, -2); /* put 'key' below previous value */
lua_rawset(L, index);

下面的例子使用 lua_rawgeti() 遍历一个整型数组,再把数组的每个元素加 5,最后遍历修改后的数组:

int l_iterate(lua_State* l)
{
    int i, n;

    /* 1st arg must be a table */
    luaL_checktype(l, 1, LUA_TTABLE);

    n = lua_objlen(l, 1); /* get size of table */

    for (i = 0; i < n; ++i) {
        int value;

        lua_rawgeti(l, 1, i);
        value = lua_tonumber(l, -1);
        printf("%d -> %d\n", i, value);

        /* change value */
        lua_pushinteger(l, value + 5);
        lua_rawseti(l, 1, i);
    }

    for (i = 0; i < n; ++i) {
        int value;
        lua_rawgeti(l, 1, i);
        value = lua_tonumber(l, -1);
        printf("%d -> %d\n", i, value);
    }

    return 0; /* no result */
}

字符串操作

当 c 函数从 lua 中获取一个字符串变量时要注意两点:一是在访问完字符串后再把它弹出栈,二是不要修改栈里的字符串。

lua 标准 api 提供了两个常用的字符串操作:取子串和字符串连接。例如要把字符串中的某部分传递给 lua 可以使用函数

const char *lua_pushlstring (lua_State *L, const char *s, size_t len);

把从位置 s 起的 len 个字符传给 lua,返回的 const char* 指向 lua 内部的该子串的副本。

要把两个字符串拼接起来可以使用函数

void lua_concat (lua_State *L, int n);

把栈顶的 n 个字符串按照入栈顺序依次拼接起来。函数执行完后会把位于栈顶的 n 个字符串都弹出,然后把结果压入栈。如果 n 为 1 函数什么都不做,如果 n 为 0 则返回一个空串。另外还有一个类似于 sprintf() 的函数

const char *lua_pushfstring (lua_State *L, const char *fmt, ...);

函数把结果压入栈中并且返回指向结果字符串的指针。目前第二个参数 fmt 支持的格式只有 %%%s%d%f%c,不支持宽度或精度控制。

当处理少量字符串时上面两个函数显得很方便,但是要处理大量字符串时,使用上面的函数一个接一个地处理效率很低,因为 lua 需要不断地申请和释放内存,还有在两块内存间复制中间结果的字符串。在 lua 辅助函数库里提供了一种两层的 buffer 机制:第一层类似于普通的 IO 缓冲区,即先把零散的字符串收集起来,填满缓冲区后一次性地把缓冲区传给下一层;第二层使用了 lua_concat() 和在 11.6 节介绍过的栈算法把字符串拼接起来。下面看看 lstrlib.c 中 string.upper() 的实现:

static int str_upper (lua_State *L) {
  size_t l;
  size_t i;
  luaL_Buffer b;
  const char *s = luaL_checklstr(L, 1, &l);
  luaL_buffinit(L, &b);
  for (i = 0; i < l; i++)
    luaL_addchar(&b, toupper((unsigned char)(s[i])));
  luaL_pushresult(&b);
  return 1;
}

函数先定义了一个 luaL_Buffer 类型 b,然后使用函数

void luaL_buffinit (lua_State *L, luaL_Buffer *B);

来初始化这个变量。在 luaL_Buffer 中保存了 L 的位置,所以在后续调用中不必把 L 作为参数。lua 提供了一系列的 luaL_add* 函数来向 luaL_Buffer 中添加数据:

void luaL_addchar    (luaL_Buffer *B, char c);
void luaL_addlstring (luaL_Buffer *B, const char *s, size_t l);
void luaL_addstring  (luaL_Buffer *B, const char *s);

最后使用

void luaL_pushresult (luaL_Buffer *B);

清理 luaL_Buffer 并且把最终结果放入栈顶。使用这些函数我们不需担心内存的分配和释放,字符串是否越界等,并且也很高效。

在使用 luaL_Buffer 时要注意一个细节。当往 luaL_Buffer 中放东西时,一些中间变量可能会被保存在对应的 lua_State 的栈中,因此在使用过程中栈顶位置可能会变化。在使用 luaL_Buffer 的过程中(即往其中放了一些东西,但是还没最后执行 luaL_pushresult())也可以调用其它函数:

luaL_addchar(&b, 'c');
/* do something else */
luaL_addstring(&b, "ccc");

但是当需要再次使用 luaL_add 函数往 luaL_Buffer 中添加内容时,必须把栈恢复到上一次调用 luaL_add 的状态。但是有一种情况不能利用上面的函数实现:将 lua 函数返回的内容放到 luaL_Buffer 中。例如要把 lua 函数中返回的字符串放到 luaL_Buffer 中,在执行 luaL_addstring() 前你不能把这个字符串弹出栈,如果不把字符串弹出栈又不能对 luaL_Buffer 进行操作(因为返回的字符串在栈顶,已经改变了栈的状态)。当然这种情况可以先把字符串复制一份,然后把字符串弹出,再调用 luaL_addstring() 把字符串放到 luaL_Buffer 中,但是这样的做法太繁琐了。于是 lua 提供了另一个函数

void luaL_addvalue (luaL_Buffer *B);

用来把栈顶的字符串或数值放到 luaL_Buffer 中。

在 c 函数中保存 lua 状态

在 c 语言中有些变量会被很多函数使用,这些变量通常会被声明为全局变量。但是对于用于 lua 函数库中的数据类型来说使用 c 风格的全局变量却不是那么方便,一是因为 c 语言中找不到一个可以表示所有 lua 变量的通用类型,二是这样的变量不能用于多个 lua_State 中。

lua 中有 3 种方法保存非局部数据:全局变量,函数环境(function environments,参考第 14 章),非局部变量(non-local variables,就是闭包里的那种变量)。lua 提供的 c 函数库也提供了 3 种方法:registry(真麻烦,不知道该翻译成什么),环境(environments),和非局部变量(upvalues,和 non-local variables 是一个意思)。

registry 是一个只能在 c 环境中访问的全局 table,能被若干个模块共享。如果需要保存私有变量,应该使用 environments。像 lua 函数一样,每个 c 函数可以有自己的 environment table,但是在一个模块中的所有 c 函数一般都共享一个 environment table。另外每个 c 函数也可以有 upvalue,就像 lua 中的闭包一样(不过 5.2 中已经去掉了 environments)。

registry

registry 只能使用一个“伪索引”——LUA_REGISTRYINDEX,来访问其中的内容。伪索引就像栈索引一样,不过它所指向的内容并不在栈中。大部分的栈操作函数都能使用 LUA_REGISTRYINDEX,除了一些只在栈内部进行操作的函数如 lua_insert() 和 lua_remove() 等(个人理解为,数据的源和目的都默认为栈,即栈-栈,而不是从用户环境-栈或栈-用户环境,因为 LUA_REGISTRYINDEX 指向的内容实际并不存在栈里)。例如要从栈里获取关键字为“key”的内容:

lua_getfield(L, LUA_REGISTRYINDEX, "Key");

registry 也是一个普通的 table,因此可以使用任何值(除了 nil)作为索引。但是因为所有的 c 模块都使用同一个 registry,所以选择索引的时候要注意冲突,使用字符串是一个比较好的选择,并且可以选择一些不常用的前缀。另一个选择是一个 128 bit 的 uuid。

另外不能使用整数作为索引,因为这些整数是为引用系统(reference system)保留的。引用系统的作用是让用户往 registry 里添加数据的时候不需指定索引。例如

int r = luaL_ref(L, LUA_REGISTRYINDEX);

表示从栈里弹出栈顶的数据并把它放到 registry 中,引用系统为这个数据选择了一个索引并将其作为返回值,这个索引被称为引用(reference)。

因为在 c 环境中不能使用指针指向 lua 中的数据,我们可以使用引用来代替指针的功能。将引用指向的数据压入栈顶可以这样:

lua_rawgeti(L, LUA_REGISTRYINDEX, r);

要释放引用及其所关联的值使用函数

luaL_unref(L, LUA_REGISTRYINDEX, r);

nil 仍然被看作是一个特殊的值,如果为 nil 创建一个引用,lua_ref() 会返回一个 LUA_REFNIL 的常量;但是如果使用

lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_REFNIL);

则会向栈顶放入一个 nil 值。

还有一个常量 LUA_NOREF,表示无效的引用。任何想获取这个引用关联的值都会返回 nil,释放这个引用相当于什么都不做。

在 c 环境中可以使用变量的内存地址作为 registry 的索引(在第 28 章中有详细讲解)。

environments

这部分不看了,因为 5.2 中 c 函数不再有 environments。

upvalues

在 c 环境中也可以创建类似 lua 中的闭包:

#include <stdio.h>

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

static int anon_ret_func(lua_State* l)
{
    int val = lua_tointeger(l, lua_upvalueindex(1));
    lua_pushinteger(l, ++val); /* new value */
    lua_pushvalue(l, -1); /* duplicate it */
    lua_replace(l, lua_upvalueindex(1)); /* update upvalue */
    return 1; /* return new value */
}

static int l_counter(lua_State* l)
{
    lua_pushinteger(l, 5);
    lua_pushcclosure(l, anon_ret_func, 1);
    return 1;
}

int main(void)
{
    lua_State* l;

    l = luaL_newstate();
    luaL_openlibs(l);

    lua_pushcfunction(l, l_counter);
    lua_setglobal(l, "counter");

    if (luaL_loadfile(l, "./closure.lua") != 0) {
        fprintf(stderr, "luaL_loadfile err: %s.\n", lua_tostring(l, -1));
        goto end;
    }

    if (lua_pcall(l, 0, 0, 0) != 0)
        fprintf(stderr, "lua_pcall err: %s.\n", lua_tostring(l, -1));

end:
    lua_close(l);
    return 0;
}

下面是上面 c 程序所使用的 lua 脚本:

f = counter()
print(f())
print(f())
print(f())

print ("------------------------------")

g = counter()
print(g())
print(g())

上面的 c 程序所定义的闭包函数 l_counter() 相当于下面的 lua 闭包函数 counter():

function counter ()
    local i = 5
    return function ()
        i = i + 1
        return i
    end
end

anon_ret_func() 就是上面返回的匿名函数。c 环境中的 upvalue 只能通过一个“伪索引”来访问,这个“伪索引”可以通过宏 lua_upvalueindex() 来获得,其中宏的参数是 upvalue 入栈的顺序。如果所访问的 upvalue 不存在或者访问的位置超出栈内元素的个数,使用 lua_type() 会获得一个 LUA_TNONE 的类型。

anon_ret_func() 先取得 upvalue 的值,然后把修改后的 upvalue 替换掉原来的值,最后把新的 upvalue 值返回(对照对应的 lua 程序来理解会比较容易)。函数

void lua_pushcclosure (lua_State *L, lua_CFunction fn, int n);

创建一个闭包,其中 n 表示 upvalue 的个数,这些 upvalue 需要预先压入栈中。完成后函数把所有的 upvalue 弹出栈,并且把匿名函数留在栈中(因为这个函数是返回值)。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注