烦恼一般都是想太多了。

0%

PIL.30.编写C函数的技术

核心 API 和辅助库都提供了一些帮助写 C 函数的方法。在本章,覆盖了进行数组操作,字符串操作,和在 Lua 里面存储 C 值的方法。

数组操作

一个 数组,在 Lua 中,仅仅是一个特定方式使用的 表。我们可以使用操纵表的那些通用函数来操纵数组,比如 lua_settable, lua_gettable。然而,API 提供了几个特定的函数以整数为键来访问和更新表。

void lua_geti (lua_State *L, int index, int key);
void lua_seti (lua_State *L, int index, int key);

5.3 版本 Lua 只提供这些函数的 raw 版本,lua_rawgeti, lua_rawseti,不会访问 元表。当访问元表这个区别并不重要的适,raw 版本的会更快一些。

对于 lua_geti, lua_seti 的描述有点让人迷惑,因为其涉及到了两个索引;index 指定要访问的是元素在栈上的位置,key 引用的是在 表中的元素。lua_geti(L, t, key)t 是整数的时候,和下面的等价:

lua_pushnumber(L, key);
lua_gettable(L, t);

lua_seti(L, t, key) 与下面代码等价:

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

下面是一个比较完整的逦,在 C 中实现了一个 map 函数(对于数组的每个元素应用一个函数,然后将函数调用的结果替换原来元素的值):

                int l_map (lua_State *L) {
int i, n;
/* 1st argument must be a table (t) */
luaL_checktype(L, 1, LUA_TTABLE);
/* 2nd argument must be a function (f) */
luaL_checktype(L, 2, LUA_TFUNCTION);
n = luaL_len(L, 1); /* get size of table */
for (i = 1; i <= n; i++) {
lua_pushvalue(L, 2); /* push f */
lua_geti(L, 1, i); /* push t[i] */
lua_call(L, 1, 1); /* call f(t[i]) */
lua_seti(L, 1, i); /* t[i] = result */
}
return 0; /* no results */
}

这个函数引入了几个新的函数:

  • luaL_checktype(lauxlib.h)保证指定栈上位置的元素具有指定类型,否则就会产生一个错误。
  • lua_len 与 length 操作符等价。因为元方法代存在,这个操作符的结果是一个对象,不仅仅只是数字;因此,lua_len 会将结果压到栈上。函数 luaL_len 以整型返回长度,如果强制转换失败为整型失败的话就会抛出错误。
  • lua_call 进行不受保护的调用。与 lua_pcall 类似,不过其会传播错误,而不是返回一个错误代码。当我们在应用中写主要代码的时候,我们不应该使用 lua_call,因为我们想要捕获任何错误。当我们在写函数的时候,使用 lua_call 是可行的;如果出现错误,只将其丢给关心它的人就行了。lua_calllua_pcall 的区别就在于对于出现错误时的处理方式不同,lua_pcall 会将错误信息压到栈上,返回一个错误代码;而 lua_call 并不会捕捉错误,而只是将错误传播出来。

字符串操作

当 C 中 Lua 收到一个字符串参数,这里只有两条必须要遵守的岣:在使用过程中不要从栈上弹出这个字符串,也不要试图修改字符串。

当想要从 C 中传递字符串给 Lua 的话就稍微麻烦一些。在这种情况下,由 C 代码来关注缓存区的分配与释放,缓冲区的溢出,以及一些其他 C 做起来比较可能的事情。因此 Lua API 提供了一些函数来帮助我们完成这些任务。

标准的API 提供了两个最基本的字符串操作:字符串截取和字符串拼接。为了截取部分字符串,需要记住的是,lua_pushlstring 将字符串的长度作为一个额外的参数。因此,如果我们想从传递 Lua 字符串 s 从位置 [i,j] 的部分,可以这样做:

lua_pushlstring(L, s + i, j - i + 1);

来个例子,如果我们想写一个用来根据指定的分隔符分隔字符串的函数,之后返回分隔后的子字符表。我们调用 split("hi:ho:there",:) 应该返回 表 {“hi”,”ho”,”table”}。

      static int l_split (lua_State *L) {
const char *s = luaL_checkstring(L, 1); /* subject */
const char *sep = luaL_checkstring(L, 2); /* separator */
const char *e;
int i = 1;
lua_newtable(L); /* result table */
/* repeat for each separator */
while ((e = strchr(s, *sep)) != NULL) {
lua_pushlstring(L, s, e - s); /* push substring */
lua_rawseti(L, -2, i++); /* insert it in table */
s = e + 1; /* skip separator */
}
/* insert last substring */
lua_pushstring(L, s);
lua_rawseti(L, -2, i);
return 1; /* return the table */
}

在这例子中,我们没有用到缓冲区,所以可以处理任意长度的字符串:Lua 自动会进行的内存的分配。(在我们创建表的时候,我们 知道其没有元表;所以我们可以用 raw 操作来进行操作)。

为了拼接字符串,Lua 提供了一个特定函数,叫做 lua_concat。出与连接操作符 .. 等价:其会将数字转换为字符串,必要的时候会触发元方法。而且,其能一次性就拼接多个字符串。lua_concat(L, n) 会拼接(并弹出)栈顶的 n 个元素,然后将应该压到栈上去。

另外一个帮助函数是:

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

这个看起来有点像 C 函数 sprintf,根据参数和格式来创建字符串。但与其不同的是,我们不需要分配缓冲区。Lua 为我们动态的创建这作品字符串。 这个函数会将结果压到栈上,并返回一个指向此字符串的指针。支持下面几个格式化符:

  • %s 插入以 ‘\0’ 结尾的字符串
  • %d int
  • %f Lua float
  • %p pointer
  • %I Lua integer
  • %c char,整型值
  • %U UTF-8 字节序列
  • %% % 号

其不支持修饰符,比如宽度和精度。

当我们想要拼接少数字符串的时候,lua_concat, lua_pushfstring 都是很有用的。然而,当我们需要拼接多个字符或者字符串的时候,一个一个的进行拼接是非常没有效率的。这样的情况下我们就使用 辅助库提供的 缓冲区特性

在简单的情况下,缓冲区特性由两个函数进行工作:一个函数给予一个任意大小的缓冲区,我们在缓冲区内拼接字符;另外一个转换内容。下面例子是展示了用法:

    static int str_upper (lua_State *L) {
size_t l;
size_t i;
luaL_Buffer b;
const char *s = luaL_checklstring(L, 1, &l);
char *p = luaL_buffinitsize(L, &b, l);
for (i = 0; i < l; i++)
p[i] = toupper(uchar(s[i]));
luaL_pushresultsize(&b, l);
return 1;
}

第一步就是声明一个 luaL_Buffer 类型的变量,接着使用 luaL_buffinitsize 来获取一个缓冲区指针;后面我们就可以在这个缓冲区来拼接字符串。最后一步 luaL_pushresultsize 会将缓冲区的内容转换成一个 Lua String,并把它压到栈上。luaL_pushresultsize 调用中的大小就是字符串最终的大小。通常,在我们的例子也是这样,这个大小是和缓冲区的大小一致的,但也可能会更小。我们不是确切的知道缓冲区最终的大小,但是有一个最大值,我们可以分配一个更大的值。

注意 luaL_pushresultsize 并没有接收一个 lua_State 作为参数,这是因为在 初始化后,缓冲区就持有了一个对 lua_State 的引用。

我们也可以通过零散的向缓冲区添加内容,而不需要了解最终的字符串大小。辅助库就提供了几个函数来向缓冲区添加内容:

  • luaL_addvalue 会将栈顶的字符串添加到缓冲区
  • luaL_addlstring 添加一个定长的字符串。
  • luaL_addstring 添加一个’\0’结尾的字符串。
  • luaL_addchar 添加单个字符。
void luaL_buffinit   (lua_State *L, luaL_Buffer *B);
void luaL_addvalue (luaL_Buffer *B);
void luaL_addlstring (luaL_Buffer *B, const char *s, size_t l);
void luaL_addstring (luaL_Buffer *B, const char *s);
void luaL_addchar (luaL_Buffer *B, char c);
void luaL_pushresult (luaL_Buffer *B);

下面是一个 table.concat 的简化版本:

       static int tconcat (lua_State *L) {
luaL_Buffer b;
int i, n;
luaL_checktype(L, 1, LUA_TTABLE);
n = luaL_len(L, 1);
luaL_buffinit(L, &b);
for (i = 1; i <= n; i++) {
lua_geti(L, 1, i); /* get string from table */
luaL_addvalue(b); /* add it to the buffer */
}
luaL_pushresult(&b);
return 1;
}

在使用缓冲区的时候我们要考虑几个细节。我们在初始化一个缓冲区后,其很有可能在栈上维护一些内部的数据。因此可,我们不能假设我们的栈顶会和我们使用缓冲区前是一致的。尽管我们可以在使用缓冲区期间使用栈,对于 push/pop 的使用一定要是平衡的。但是 luaL_addvalue 一直假设我们要添加的字符串位于栈顶。

C 函数中存储状态

经常,C 函数需要保持一些非本地的数据,也就是说,在函数的调用生命周期之外的数据。在 C 中我们会使用 全局 (extern) 或者静态变量。当我们编写为 Lua 编写库函数的时候,这却不能工作。首先,我们不能将一个 通常意义上的 Lua 值存在到一个 C 变量中。然后,一个库函数在多个 lua_State 的时候使用这个变量就不能正常工作。

更好的办法是向 Lua 寻求一下帮助。 一个 Lua 函数有两个地方可以存在非本地的数据:全局变量和非本地的变量。C API 提供了了个类似的地方来存非本地的数据:注册表与上值。

注册表

注册表是一个全局表,其只能通过 C 代码访问。通常,我们用他来存在需要在多个模块间共享的数据。

注册表的索引总是位于 LUA_REGISTRYINDEX 这个伪索引。我们可以如同普通索引一样使用这个伪索引,唯一的例外就是对于操作栈本身的函数无法接受伪索引。例如 lua_remote, lua_insert。如果我们想要获取一个存在在注册表中的 Key 的值:

lua_getfield(L, LUA_REGISTRYINDEX, "Key");

注册也是一普通的 Lua 表。因此我们可以用任何非 nil Lua 值进行索引。然而,因为所有的 C 模块都共享这个注册表,我们必须要谨慎的选择要用做索引的键来避免冲突。

我们永远也不要使用数字在全局表中作为索引,这个这是 Lua 引用系统 保留的做法。这个系统由一辅助库的一对函数组成,其允许我们不用担心如何创建唯一的键来在表中存在数据。luaL_ref 会创建一个新的引用:

int ref = luaL_ref(L, LUA_REGISTRYINDEX);

这个调用,会从栈上弹出一个值,然后把他存到注册表内(以一个整型作为键),接着返回这个键。我们将这个整型的键,叫做引用。

就和名字暗示的一样,我们使用这个引用主要是在我们需要 C 结构中引用一个 Lua 值的时候。我们不应该接收到 Lua 字符串参数的函数外存储字符串的指针。同时,Lua 并不提供指向其他对象的指针,比如表和函数的指针。因此,我们不能通过指针来访问 Lua 对象。这样的情况下我们就智能创建一个引用了。

如果我们想要将引用所关联的值压到栈上,我们可以简单的这样做:

lua_rawgeti(L, LUA_REGISTRYINDEX, ref);

如果我们想要释放引用和值,

luaL_unref(L, LUA_REGISTRYINDEX, ref);

引用系统将 nil 当做一个特殊情况。如果我们为 nil 值调用 luaL_ref,其不会创建新的索引,而是返回常索引 LUA_REFNIL,下面的调用没有任何影响:

luaL_unref(L, LUA_REGISTRYINDEX, LUA_REFNIL);

下面的调用会压入一个 nil,如我们所期待的那样:

lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_REFNIL);

引用系统也定义了一个常量 LUA_NOREF,其也是一个整型,不过和任何有效的引用都不一样。这用来标示被当作引用的那个值是无效的。

当我们建立一个 lua_State 的时候,有几个预定义的引用:

  • LUA_RIDX_MAINTHREAD 引用 Lua_State 自身,其也是一个主线程。
  • LUA_RIDX_GLOBALS 全局环境。

另一个比较安全的在注册表内建立唯一键的方法是使用我们代码中静态变量地址作为键。C 的链接器会保证这个键在所有加载的库中唯一。为了使用这个选项,我们需要函数 lua_pushlightuserdata,会将一个代表 C 指针的值压到栈上。

/* variable with a unique address */
static char Key = 'k';
/* store a string */
lua_pushlightuserdata(L, (void *)&Key); /* push address */
lua_pushstring(L, myStr); /* push value */
lua_settable(L, LUA_REGISTRYINDEX); /* registry[&Key] = myStr */
/* retrieve a string */
lua_pushlightuserdata(L, (void *)&Key); /* push address */
lua_gettable(L, LUA_REGISTRYINDEX); /* retrieve value */
myStr = lua_tostring(L, -1); /* convert to string */

为了简化使用地址作为唯一键的方式,Lua 5.2 就提供了两个新的函数 :lua_rawgetp, luarawsetp。这个和 lua_rawgeti, lua_rawseti 相似,但是其是使用 C 指针(Light userdata)作为键的。使用这两个函数 前面的例子就可以简化:

static char Key = 'k';
/* store a string */
lua_pushstring(L, myStr);
lua_rawsetp(L, LUA_REGISTRYINDEX, (void *)&Key);
/* retrieve a string */
lua_rawgetp(L, LUA_REGISTRYINDEX, (void *)&Key);
myStr = lua_tostring(L, -1);

所有的函数都使用 raw 访问。由于注册表是没有元表的,raw 访问和常规的访问没有什么区别,只不过更高效一点。

上值

注册表提供了类似全局变量的东西,而 上值 实现了与只能在一个函数可间的 C 静态变量等价的东西。每当我们在 Lua 创建一个 C 函数的时候,我们可以以任意多个上值关联给它,每个上值持有一个 Lua 值。在后续对这个函数的调用中,其可以自由的访问他的上值,(使用伪索引)。

我们把这个将 C 函数与其上值的关联叫做 闭包。一个 C 闭包 是一个 C 对 Lua 闭包的近似。实际上,我们可以用相同的函数,不同的上值创建不同的闭包。

看一个简单的例子:newCounter。这个函数是一个工厂函数,调用它 就会返回一个计数函数。

c1 = newCount()
print(c1(), c1(), c1()) --> 1 2 3
c2 = newCounter()
print(c2(), c2(), c1()) --> 1 2 4

尽管所有 counter 代码共享 同样的 C代码,但每个计数器都是独立的。工厂函数类似:

      static int counter (lua_State *L);  /* forward declaration */
int newCounter (lua_State *L) {
lua_pushinteger(L, 0);
lua_pushcclosure(L, &counter, 1);
return 1;
}

关键之处在与 lua_pushcclosure,其会创建一个新的闭包。其第二个参数是一个基础函数,第三个参数是指定上值个数。

在创建闭包前,我们必须将上值的初始值压到栈上。lua_pushcclosure 会将新的闭包留在栈上。

现在我们来看看 counter 函数的定义:

      static int counter (lua_State *L) {
int val = lua_tointeger(L, lua_upvalueindex(1));
lua_pushinteger(L, ++val); /* new value */
lua_copy(L, -1, lua_upvalueindex(1)); /* update upvalue */
return 1; /* return new value */
}

这里的关键元素就是 lua_upvalueindex,其会对上值产生一个伪索引。实际上 lua_upvalueindex(1) 会产生运行函数第一个上值的伪索引。再次重复,伪索引也和正常的栈索引没有什么区别,只是其不存在与栈上而已。