Lua连续教程之编写C函数的技巧

Lua中的“数组”就是特殊方式使用的表。像lua-settable和lua-gettable这种用来操作表的通用函数,也可用于操作数组。不过,CAPI为使用整数索引的表的访问和封信提供了专门的函数。

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

Lua5.3之前的版本只提供了这些函数的原始版本,即lua_rawgeti和lua_rawseti。这两个函数类似于lua_geti和lua_seti,但进行的是原始访问。当区别并不明显时,那么原始版本可能会稍微快一点。
lua_geti和lua_seti的描述有一点令人困惑,因为其用了两个索引:index表示在栈中的位置,key表示元素在表中的位置。当t为正数时,那么调用lua_geti(L,t,key)等价于如下的代码:

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

调用lua_seti(L,t,key)等价于如下的代码:
1
2
3
lua_pushnumber(L,key);
lua_insert(L,-2);
lua_settable(L,t);

作为使用这些函数的具体示例,下面实现了函数map,该函数对数组中的所有元素调用一个指定的函数,然后用词函数返回的结果替换掉对应的数组元素。

C语言中的函数map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int l_map(lua_State *L){
int i , n;
/*第一个参数必须是一张表(t) */
luaL_checktype(L,1,LUA_TTABLE);
/*第二个参数必须是一个函数(f) */
luaL_checktype(L,2,LUA_TFUNCTION);
n = luaL_len(L,1);/*获取表的大小*/
for (i = 1;i <=n; i++){
lua_pushvalue(L,2); /*压入f*/
lua_geti(L,1,i); /*压入t[i]*/
lua_call(L,1,1); /*调用f(t[i])*/
lua_seti(L,1,i); /*t[i] = result */
}
return 0; /* 没有返回值*/
}

这个示例还引入了三个新函数:luaL_checktype、luaL_len和lua_call。
函数luaL_checktype确保指定的参数具有指定的类型,否则它会引发一个错误。
原始的lua_len类似于长度运算符。由于元方法的存在,该运算符能够返回任意类型的对象,而不仅仅是数字;因此,lua_len会在栈中返回其结果。函数luaL_len会将长度作为整型数返回,如果无法进行强制类型转换则会引发错误。
函数lua_call做的是不受保护的调用,该函数类似于lua_pcall,但在发生错误时lua_call会传播错误而不是返回错误码。在一个应用中编写主函数时,不应使用lua_call,因为我们需要捕获所有的错误。不过,编写一个函数时,一般情况下使用lua_call是个不错的注意;如果发生错误,就留给关心错误的人去处理吧。

字符串操作

当C函数接收到一个Lua字符串为参数时,必须遵守两条规则:在使用字符串期间不能从栈中将其弹出,而且不应该修改字符串。
当C函数需要创建一个返回给Lua的字符串时,要求则更高。此时,是C语言代码负责缓冲区的分配/释放、缓冲区溢出,以及其他对C语言来说比较困难的任务。因此,LuaAPI提供了一些函数来帮助完成这些任务。
标准API为两种常用的字符串操作提供了支持,即子串提取和字符串连接。要提取子串,那么基本的操作lua_pushlstring可以获取字符串长度作为额外的参数。因此,如果要把字符串s从i到j(包含)的子串传递给Lua,就必须:

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

举个例子,假设需要编写一个函数,该函数根据指定的分隔符来分隔字符串,并返回一张包含子串的表。例如,调用split(“hi:ho:there”,”:”)应该返回表{“hi”,”ho”,”there”}。下面示例演示了该函数的一种简单实现:

分隔字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int l_split(lua_State *L){
const char *s = luaL_checkstring(L,1);
const char *sep = luaL_checkstring(L,2);
const char *e;
int i = 1;
lua_newtable(L); /*结果表*/
/* 依次处理每个分隔符*/
while ((e = strchr(s,*sep)) != NULL) {
lua_pushlstring(L,s,e - s); /*压入子串*/
lua_rawseti(L,-2,i++); /* 向表中插入*/
s = e + 1; /*跳过分隔符*/
}
/* 插入最后一个子串*/
lua_pushstring(L,s);
lua_rawseti(L,-2,i);
return 1;/*将结果表返回*/
}

该函数无须缓冲区,并能处理任意长度的字符串,Lua语言会负责处理所有的内存分配。
要连接字符串,Lua提供了一个名为lua_concat的特殊函数,该函数类似于Lua中的连接操作符(..),它会将数字转换为字符串,并在必要时调用元方法。此外,该函数还能一次连接两个以上的字符串。调用lua_concat(L,n)会连接栈最顶端的n个值,并将结果压入栈。
另一个有帮助的函数是lua_pushfstring:
1
const char *lua_pushfstring(lua_State *L, const char *fmt, ...);

该函数在某种程度上类似于C函数sprintf,它们都会根据格式字符串和额外的参数来创建字符串。然而,与sprintf不同,使用lua_pushfstring时不需要提供缓冲区。不管字符串有多大,Lua都会动态地为我们创建。lua_pushfstring会将结果字符串压入栈中并返回一个指向它的指针,该函数能够接受如下所示字符。


%s 插入一个以\0结尾的字符串
%d 插入一个int
%f 插入一个Lua语言的浮点数
%p 插入一个浮点数
%I 插入一个Lua语言的整型数
%c 插入一个以int表示的单字节字符
%U 插入一个以int表示的UTF-8字节序列
%% 插入一个百分号


该函数不能使用诸如宽度或者精度之类的修饰符。
当只需要连接几个字符串时,lua_concat和lua_pushfstring都很有用。不过,如果需要连接很多字符串,此时,我们可以用由辅助库提供的缓冲机制。
缓冲机制的简单用法只包含两个函数:一个用于在组装字符串时提供任意大小的缓冲区;另一个用于将缓冲区中的内容转换为一个Lua字符串。下面示例用源文件lstrlib.c中string.upper的实现演示了这些函数。

函数string.upper

1
2
3
4
5
6
7
8
9
10
11
static int str_upper (lua_State *L){
size_t l;
size_t i;
luaL_Buffer b;
const char *s = luaL_checklstring(L,1,&1);
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字符串,并将该字符串压栈。其中,第二步调用时就确定了字符串的最终长度。通常情况下,像我们的示例一样,字符串的最终大小与缓冲区大小相等,但也可能更小。加入我们并不知道返回字符串的准确长度,但知道其最大不超过多少,那么可以操守地为其分配一个较大的空间。
请注意,luaL_pushresultsize并未获取Lua状态作为其第一个参数。在初始化之后,缓冲区保存了对Lua状态的引用,因此在调用其他操作缓冲区的函数时无需再传递该状态。
加入不知道返回结果大小的上限值,我们还可以通过逐步增加内容的方式来使用辅助库的缓冲区。辅助库提供了一个用于缓冲区中增加内容的函数:luaL_addvalue用于在栈顶增加一个Lua字符串,luaL_addlstring用于增加一个长度明确的字符串,luaL_addstring用于增加一个以\0结尾的字符串,luaL_addchar用于增加一单个字符。这些函数的原型如下:
1
2
3
4
5
6
void luaL_buffinit   (lua_State *L, luaL_Buffer *B);
void luaL_addvalue (luaL_Buffer *B);
void lua_addlstring (luaL_Buffer *B, const char *s, size_t l);
void lua_addstring (luaL_Buffer *B, const char *s);
void luaL_addchar (luaL_Buffer *B, char c);
void luaL_pushresult (luaL_Buffer *B);

下面示例通过函数table.concat的一个简化的实现演示了这些函数的使用。
示例 函数table.concat的一个简化的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
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);
luaL_addvalue(b);
}
luaL_pushresult(&b);
return 1;
}

在该函数中,首先调用luaL_buffinti来初始化缓冲区。然后,向缓冲区中逐个增加元素,本例中用的是luaL_addvalue。最后,luaL_pushresult刷新缓冲区并在栈顶留下最终的结果字符串。
在使用辅助库的缓冲区时,我们必须注意一个细节。初始化一个缓冲区后,Lua栈可能还会保留某些内部数据。因此,我们不能假设在使用缓冲区之前栈顶仍然停留在最初的位置。此外,尽管使用缓冲区时我们可以将该栈用于其他用途,但在访问栈之前,对栈的压入和弹出次数必须平衡。唯一的例外是luaL_addvalue,该函数会假设要添加到缓冲区的字符串是位于栈顶的。

在C函数中保存状态

通常情况下,C函数需要保存一些非局部数据,即生存时间超出C函数执行时间的数据。在C语言中,我们通常使用全局变量或静态变量来满足这种需求。然而,当我们为Lua编写库函数时,这并不是一个好办法。首先,我们无法在一个C语言变量中保存普通的Lua值。其次,使用这类变量的库无法用于多个Lua状态。
更好的办法是从Lua语言中寻求帮助。Lua函数有两个地方可用于存储非局部数据,即全局变量和非局部变量,而CAPI也提供了两个类似的地方来存储非局部数据,即注册表和上值。

注册表

注册表是一张只能被C代码访问的全局表。通常情况下,我们使用注册表来存储多个模块间共享的数据。
注册表总是位于伪索引LUA_REGISTRYINDEX中。伪索引就像是一个栈中的索引,但它所关联的值不在栈中。LuaAPI中大多数接受索引作为参数的函数也能将伪索引作为参数,像lua_remove和lua_insert这种操作栈本身的函数除外。例如,要获取注册表中键为”key”的值,可以使用如下的调用:

1
lua_getfield(L,LUA_REGISTRYINDEX,"Key");

注册表是一个普通的Lua表,因此可以使用除nil外的任意Lua值来检索它。不过,由于所有的C语言模块共享的是同一个注册表,为了避免冲突,我们必须谨慎地选择作为键的值。当允许其他独立的库访问我们的数据时,字符串类型的键尤为有用,因为这些库只需知道键的名字就可以了。对于浙西键,选择名字时没有一种可以绝对避免冲突的方法;不过,诸如避免使用常见的名字,以及用库名或类似的东西作为键名的前缀,仍然是好的做法。
在注册表中不能使用数值类型的键,因为Lua语言将其用作引用系统的保留字。引用系统由辅助库中的一对函数组成,有了这两个函数,我们在表中存储值时不必担心如何创建唯一的键。函数luaL_ref用于创建新的引用:
1
int ref = luaL_ref(L,LUA_REGISTRYINDEX);

上述调用会从栈中弹出一个值,然后分配一个新的整型的键,使用这个键将从栈中弹出的值保存到注册表中,最后返回该整型键,而这个键就被称为引用。
顾名思义,我们主要是在需要一个C语言结构体中保存一个指向Lua值的引用时使用引用。正如我们之前所看到的,不应该将指向Lua字符串的指针保存在获取该指针的函数之外。此外,Lua语言甚至没有提供指向其他对象的指针。因此,我们无法通过指针来引用Lua对象。当需要这种指针时,我们可以创建一个引用并将其保存在C语言中。
要将于引用ref关联的值压入栈中,只要这样写就行:
1
lua_rawgeti(L,LUA_REGISTRYINDEX,ref);

最后,要释放值和引用,我们可以调用luaL_unref:
1
luaL_unref(L,LUA_REGISTRYINDEX,ref);

在这句调用后,再次调用luaL_ref会再次返回相同的引用。
引用系统将nil视为一种特殊情况。无论何时为一个nil值调用luaL_ref都不会创建新的引用,而是会返回一个常量引用LUA_REFNIL。如下的调用没什么好处:
1
luaL_unref(L,LUA_REGISTRYINDEX,LUA_REFNIL);

而如下的代码则会像我们期望地一样像栈中压入一个nil:
1
lua_rawgeti(L,LUA_REGISTRYINDEX,LUA_REFNIL);

引用系统还定义了一个常量LUA_NOREF,这是一个不同于其他合法引用的整数,它可以用于表示无效的引用。
当创建Lua状态时,注册表中有两个预定义的引用:
LUA_RIDX_MAINTHREAD
指向Lua状态本身,也就是其主线程。
LUA_RIDX_GLOBALS
指向全局变量。
另一种在注册表中创建唯一键的方法是,使用代码中静态变量的地址,C语言的链接编辑器会确保键在所有已加载的库中的唯一性。要使用这种方法,需要用到函数lua_pushlightuserdata,该函数会在栈中压入一个表示C语言指针的值。下面的代码演示了如何使用这种方法在注册表中保存和获取字符串:
1
2
3
4
5
6
7
8
9
10
11
/*具有唯一地址的变量*/
static char Key = 'k';
/* 保存字符串*/
lua_pushlightuserdata(L,(void *)&Key); /* 压入地址*/
lua_pushstring(L,myStr); /*压入值*/
lua_settable(L,LUA_REGISTRYINDEX); /* registry[&Key] = myStr */

/* 获取字符串*/
lua_pushlightuserdata(L,(void *)&Key); /* 压入地址*/
lua_gettbale(L,LUA_REGISTRYINDEX); /* 获取值 */
myStr = lua_tostring(L,-1); /*转换为字符串*/

为了简化将变量地址用作唯一键的方法,Lua5.2中引入了两个新函数:lua_rawgetp和lua_rawsetp。这两个函数类似于lua_rawgeti和lua_rawseti,但它们使用C语言指针作为键。使用这两个函数,可以将上面的代码重写为:
1
2
3
4
5
6
7
8
static char Key = 'k';
/* 保存字符串 */
lua_pushstring(L,myStr);
lua_rawsetp(L,LUA_REGISTRYINDEX,(void *)&Key);

/*获取字符串*/
lua_rawgetp(L,LUA_REGISTRYINDEX,(void *)&Key);
myStr = lua_toshtring(L,-1);

这两个函数都使用了原始访问。由于注册表没有元素,因此原始访问和普通访问相同,而且效率还会稍微高一些。

上值

注册表提供了全局变量,而上值则实现了一种类似于C语言静态变量的机制。每一次在Lua中创建新的C函数时,都可以将任意数量的上值与这个函数相关联,而每个上值都可以保存一个Lua值。后面在调用该函数时,可以通过伪索引来自由地访问这些上值。
我们将这种C函数与其上值的关联称为闭包。C语言闭包类似于Lua语言闭包。 特别的,可以用相同的函数代码来创建不同的闭包,每个闭包可以拥有不同的上值。
接下来看一个简单的示例,让我们用C语言创建一个函数newCounter。该函数是一个工厂函数,每次调用都会返回一个新的计数函数,如下所示:

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

尽管所有的计数器都适用相同的C语言代码,但它们各自都保留了独立的计数器。工厂函数的代码形如:
1
2
3
4
5
6
static int counter (lua_State *L);  /*前向声明*/
int newCounter (lua_State *L){
lua_pushinteger(L,0);
lua_pushcclosure(L,&counter,1);
return 1;
}

这里的关键函数是lua_pushcclosure,该函数会创建一个新的闭包。lua_pushcclosure的第二个参数是一个基础函数,第三个参数是上值的数量。在创建一个新的闭包前,我们必须将上值的初始值压栈。在此示例中,我们压入了零作为唯一一个上值的初始值。正如我们预想的那样,lua_pushcclosure会将一个新的闭包留在栈中,并将其作为newCounter的返回值。
现在,来看一下counter的定义:
1
2
3
4
5
6
static int counter (lua_State *L){
int val = lua_tointeger(L, lua_upvalueindex(1));
lua_pushinteger(L,++val);/*新值*/
lua_copy(L,-1,lua_upvalueindex(1)); /*更新上值*/
return 1; /*返回新值*/
}

这里的关键是宏lua_upvalueindex,它可以生成上值的伪索引。特别的,表达式lua_upvalueindex(1)给出了正在运行的函数的第一个上值的伪索引,该为索引同其他的栈索引一样,唯一区别的是它不存在与栈中。因此,调用lua_tointeger会以整型返回一个上值的当前值。然后,函数counter将新值++val压栈,并将其复制一份作为新上值的值,再将其返回。
接下来是一个更高级的示例,我们将使用上值来实现元组。元组是一种具有匿名字段的常量结构,我们可以用一个数值索引来获取某个特定的字段,或者一次性地获取所有字段。在我们的实现中,将元组表示为函数,元组的值存储在函数的上值中。当使用数值参数来调用该函数时,函数会返回特定的字段。当不使用参数来调用该函数时,则返回所有字段。一下代码演示了元组的使用:
1
2
3
4
x = tuple.new(10,"hi",{},3)
print(x(1)) -- 10
print(x(2)) -- hi
print(x(3)) -- 10 hi table:ox8087878 3

在C语言中,我们会用同一个函数t_tuple来表示所有的元组,代码参考下示例。

元组的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include "luaxlib.h"

int t_tuple(lua_State *L){
lua_Integer op = luaL_optinteger(L,1,0);
if (op == 0){
int i;
for (i = 1; !lua_isnone(L,lua_upvalueindex(i)); i++)
lua_pushvalue(L,lua_upvalueindex(i));
return i - 1;
}
else {
luaL_argcheck(L,0<op && op <= 256,1,"index out of range");
if (lua_isnone(L,lua_upvalueindex(op)))
return 0;
lua_pushvalue(L,lua_upvalueindex(op));
return 1;
}
}

int t_new(lua_State *L){
int top = lua_gettop(L);
luaL_argcheck(L, top< 256, top ,"too many fields");
lua_pushcclosure(L,t_tuple,top);
return 1;
}

static const struct luaL_Reg tuplelib[] = {
{"new",t_new},
{NULL,NULL}
};

int luaopen_tuple (lua_State *L){
luaL_newlib(L,tuplelib);
return 1;
}

由于调用元组时既可以使用数字作为参数也可以不用数字作为参数,因此t_tuple使用luaL_optinteger来获取可选参数。该函数类似于luaL_checkinteger,但当参数不存在时不会报错,而是返回指定的默认值。
C语言函数中最多可以有255个上值,而lua_upvaluindex的最大索引值是256。因此,我们使用luaL_argcheck来确保这些范围的有效性。
当访问一个不存在的上值时,结果是一个类型为LUA_TNONE的伪值。函数t_tuple使用lua_isnone测试指定的上值是否存在。不过,我们永远不应该使用负数或者超过256的索引值来调用lua_upvalueindex,因此必须对用户提供索引进行检查。函数luaL_argcheck可用于检查给定的条件,如果条件不符合,则会引发错误并返回一条友好的错误信息:
1
2
3
> t = tuple.new(2,4,5)
> t(300)
--> stdin:1:bad argument #1 to 't' (index out of range)

luaL_argcheck的第三个参数表示错误信息的参数编号,第四个参数表示对信息的不出。
创建元组的函数t_new很简单,由于其参数已经在栈中,因此该函数先检查字段的数量是否符合闭包中上值个数的限制,然后将所有上值作为参数调用lua_pushcclosure来创建一个t_tuple的闭包。最后,数组tuplelib和函数luaopen_tuple是创建tuple库的标准代码,该库只有一个函数new。

共享的上值

我们经常需要同一个库的所有函数之间共享某些值或变量,虽然可以用注册表来完成这个任务,但也可以使用上值。
与Lua语言的闭包不同,C语言的闭包不能共享上值,每个闭包都有其独立的上值。但是,我们可以设置不同函数的上值指向一张共同的表,这张表就成为了一个共同的环境,函数在其中能够共享数据。
Lua语言提供了一个函数,该函数可以简化同一个库中所有函数间共享上值的任务。我们已经使用luaL_newlib打开了C语言库。Lua将这个函数实现为如下的宏:

1
#define luaL_newlib(L,lib) \ (luaL_newlibtable(L,lib),(luaL_setfuncs(L,lib,0))

宏luaL_newlibtable只是为库创建了一张新表。然后,函数luaL_setfuncs将列表lib中的函数添加到位于栈顶的新表中。
我们这里感兴趣的是luaL_setfuncs的第三个参数,这个参数给出了库中的新函数共享的上值个数。当调用lua_pushcclosure时,这些上值的初始值应该位于栈顶。因此,如果要创建一个库,这个库中的所有函数共享一张表作为它们唯一的上值,则可以使用如下的代码:
1
2
3
4
5
6
/* 创建库的表*/
luaL_newlibtable(L,lib);
/* 创建共享上值 */
lua_newtable(L);
/*将表'lib'中的函数加入到新库中,将之前的表共享为上值*/
luaL_setfuncs(L,lib,1);

最后一个函数调用从栈中删除了这张共享表,只留下了新库。