Lua中的“数组”就是特殊方式使用的表。像lua-settable和lua-gettable这种用来操作表的通用函数,也可用于操作数组。不过,CAPI为使用整数索引的表的访问和封信提供了专门的函数。
代码语言:javascript复制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)等价于如下的代码:
代码语言:javascript复制lua_pushnumber(L,key);
lua_gettable(L,t);
调用lua_seti(L,t,key)等价于如下的代码:
代码语言:javascript复制lua_pushnumber(L,key);
lua_insert(L,-2);
lua_settable(L,t);
作为使用这些函数的具体示例,下面实现了函数map,该函数对数组中的所有元素调用一个指定的函数,然后用词函数返回的结果替换掉对应的数组元素。
代码语言:javascript复制C语言中的函数map
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,就必须:
代码语言:javascript复制lua_pushlstring(L,s i,j-i 1);
举个例子,假设需要编写一个函数,该函数根据指定的分隔符来分隔字符串,并返回一张包含子串的表。例如,调用split(“hi:ho:there”,”:”)应该返回表{“hi”,”ho”,”there”}。下面示例演示了该函数的一种简单实现:
代码语言:javascript复制分隔字符串
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;/*将结果表返回*/
}
代码语言:javascript复制该函数无须缓冲区,并能处理任意长度的字符串,Lua语言会负责处理所有的内存分配。 要连接字符串,Lua提供了一个名为lua_concat的特殊函数,该函数类似于Lua中的连接操作符(..),它会将数字转换为字符串,并在必要时调用元方法。此外,该函数还能一次连接两个以上的字符串。调用lua_concat(L,n)会连接栈最顶端的n个值,并将结果压入栈。 另一个有帮助的函数是lua_pushfstring:
const char *lua_pushfstring(lua_State *L, const char *fmt, ...);
该函数在某种程度上类似于C函数sprintf,它们都会根据格式字符串和额外的参数来创建字符串。然而,与sprintf不同,使用lua_pushfstring时不需要提供缓冲区。不管字符串有多大,Lua都会动态地为我们创建。lua_pushfstring会将结果字符串压入栈中并返回一个指向它的指针,该函数能够接受如下所示字符。
%s 插入一个以 结尾的字符串 %d 插入一个int %f 插入一个Lua语言的浮点数 %p 插入一个浮点数 %I 插入一个Lua语言的整型数 %c 插入一个以int表示的单字节字符 %U 插入一个以int表示的UTF-8字节序列 %% 插入一个百分号
该函数不能使用诸如宽度或者精度之类的修饰符。 当只需要连接几个字符串时,lua_concat和lua_pushfstring都很有用。不过,如果需要连接很多字符串,此时,我们可以用由辅助库提供的缓冲机制。 缓冲机制的简单用法只包含两个函数:一个用于在组装字符串时提供任意大小的缓冲区;另一个用于将缓冲区中的内容转换为一个Lua字符串。下面示例用源文件lstrlib.c中string.upper的实现演示了这些函数。
代码语言:javascript复制函数string.upper
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;
}
代码语言:javascript复制使用辅助库中缓冲区的第一步是声明一个luaL_Buffer类型的变量。第二步是调用luaL_buffinitsize获取一个指向指定大小缓冲区的指针,之后就可以自由地使用该缓冲区来创建字符串了。最后需要调用luaL_pushresultsize将缓冲区中的内容转换为一个新的Lua字符串,并将该字符串压栈。其中,第二步调用时就确定了字符串的最终长度。通常情况下,像我们的示例一样,字符串的最终大小与缓冲区大小相等,但也可能更小。加入我们并不知道返回字符串的准确长度,但知道其最大不超过多少,那么可以操守地为其分配一个较大的空间。 请注意,luaL_pushresultsize并未获取Lua状态作为其第一个参数。在初始化之后,缓冲区保存了对Lua状态的引用,因此在调用其他操作缓冲区的函数时无需再传递该状态。 加入不知道返回结果大小的上限值,我们还可以通过逐步增加内容的方式来使用辅助库的缓冲区。辅助库提供了一个用于缓冲区中增加内容的函数:luaL_addvalue用于在栈顶增加一个Lua字符串,luaL_addlstring用于增加一个长度明确的字符串,luaL_addstring用于增加一个以 结尾的字符串,luaL_addchar用于增加一单个字符。这些函数的原型如下:
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);
代码语言:javascript复制下面示例通过函数table.concat的一个简化的实现演示了这些函数的使用。 示例 函数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);
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”的值,可以使用如下的调用:
代码语言:javascript复制lua_getfield(L,LUA_REGISTRYINDEX,"Key");
注册表是一个普通的Lua表,因此可以使用除nil外的任意Lua值来检索它。不过,由于所有的C语言模块共享的是同一个注册表,为了避免冲突,我们必须谨慎地选择作为键的值。当允许其他独立的库访问我们的数据时,字符串类型的键尤为有用,因为这些库只需知道键的名字就可以了。对于浙西键,选择名字时没有一种可以绝对避免冲突的方法;不过,诸如避免使用常见的名字,以及用库名或类似的东西作为键名的前缀,仍然是好的做法。
在注册表中不能使用数值类型的键,因为Lua语言将其用作引用系统的保留字。引用系统由辅助库中的一对函数组成,有了这两个函数,我们在表中存储值时不必担心如何创建唯一的键。函数luaL_ref用于创建新的引用:
代码语言:javascript复制int ref = luaL_ref(L,LUA_REGISTRYINDEX);
上述调用会从栈中弹出一个值,然后分配一个新的整型的键,使用这个键将从栈中弹出的值保存到注册表中,最后返回该整型键,而这个键就被称为引用。
顾名思义,我们主要是在需要一个C语言结构体中保存一个指向Lua值的引用时使用引用。正如我们之前所看到的,不应该将指向Lua字符串的指针保存在获取该指针的函数之外。此外,Lua语言甚至没有提供指向其他对象的指针。因此,我们无法通过指针来引用Lua对象。当需要这种指针时,我们可以创建一个引用并将其保存在C语言中。
要将于引用ref关联的值压入栈中,只要这样写就行:
代码语言:javascript复制lua_rawgeti(L,LUA_REGISTRYINDEX,ref);
最后,要释放值和引用,我们可以调用luaL_unref:
代码语言:javascript复制luaL_unref(L,LUA_REGISTRYINDEX,ref);
在这句调用后,再次调用luaL_ref会再次返回相同的引用。
引用系统将nil视为一种特殊情况。无论何时为一个nil值调用luaL_ref都不会创建新的引用,而是会返回一个常量引用LUA_REFNIL。如下的调用没什么好处:
代码语言:javascript复制luaL_unref(L,LUA_REGISTRYINDEX,LUA_REFNIL);
而如下的代码则会像我们期望地一样像栈中压入一个nil:
代码语言:javascript复制lua_rawgeti(L,LUA_REGISTRYINDEX,LUA_REFNIL);
引用系统还定义了一个常量LUA_NOREF,这是一个不同于其他合法引用的整数,它可以用于表示无效的引用。
当创建Lua状态时,注册表中有两个预定义的引用:
LUA_RIDX_MAINTHREAD
指向Lua状态本身,也就是其主线程。
LUA_RIDX_GLOBALS
指向全局变量。
另一种在注册表中创建唯一键的方法是,使用代码中静态变量的地址,C语言的链接编辑器会确保键在所有已加载的库中的唯一性。要使用这种方法,需要用到函数lua_pushlightuserdata,该函数会在栈中压入一个表示C语言指针的值。下面的代码演示了如何使用这种方法在注册表中保存和获取字符串:
代码语言:javascript复制/*具有唯一地址的变量*/
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语言指针作为键。使用这两个函数,可以将上面的代码重写为:
代码语言:javascript复制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。该函数是一个工厂函数,每次调用都会返回一个新的计数函数,如下所示:
代码语言:javascript复制c1 = newCounter()
print(c1(),c1(),c1()) -- 1 2 3
c2 = newCounter()
print(c2(),c2(),c2()) -- 1 2 4
尽管所有的计数器都适用相同的C语言代码,但它们各自都保留了独立的计数器。工厂函数的代码形如:
代码语言:javascript复制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的定义:
代码语言:javascript复制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压栈,并将其复制一份作为新上值的值,再将其返回。
接下来是一个更高级的示例,我们将使用上值来实现元组。元组是一种具有匿名字段的常量结构,我们可以用一个数值索引来获取某个特定的字段,或者一次性地获取所有字段。在我们的实现中,将元组表示为函数,元组的值存储在函数的上值中。当使用数值参数来调用该函数时,函数会返回特定的字段。当不使用参数来调用该函数时,则返回所有字段。一下代码演示了元组的使用:
代码语言:javascript复制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来表示所有的元组,代码参考下示例。
代码语言:javascript复制元组的实现
#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;
}
代码语言:javascript复制由于调用元组时既可以使用数字作为参数也可以不用数字作为参数,因此t_tuple使用luaL_optinteger来获取可选参数。该函数类似于luaL_checkinteger,但当参数不存在时不会报错,而是返回指定的默认值。 C语言函数中最多可以有255个上值,而lua_upvaluindex的最大索引值是256。因此,我们使用luaL_argcheck来确保这些范围的有效性。 当访问一个不存在的上值时,结果是一个类型为LUA_TNONE的伪值。函数t_tuple使用lua_isnone测试指定的上值是否存在。不过,我们永远不应该使用负数或者超过256的索引值来调用lua_upvalueindex,因此必须对用户提供索引进行检查。函数luaL_argcheck可用于检查给定的条件,如果条件不符合,则会引发错误并返回一条友好的错误信息:
> 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将这个函数实现为如下的宏:
代码语言:javascript复制#define luaL_newlib(L,lib) (luaL_newlibtable(L,lib),(luaL_setfuncs(L,lib,0))
宏luaL_newlibtable只是为库创建了一张新表。然后,函数luaL_setfuncs将列表lib中的函数添加到位于栈顶的新表中。
我们这里感兴趣的是luaL_setfuncs的第三个参数,这个参数给出了库中的新函数共享的上值个数。当调用lua_pushcclosure时,这些上值的初始值应该位于栈顶。因此,如果要创建一个库,这个库中的所有函数共享一张表作为它们唯一的上值,则可以使用如下的代码:
代码语言:javascript复制/* 创建库的表*/
luaL_newlibtable(L,lib);
/* 创建共享上值 */
lua_newtable(L);
/*将表'lib'中的函数加入到新库中,将之前的表共享为上值*/
luaL_setfuncs(L,lib,1);
最后一个函数调用从栈中删除了这张共享表,只留下了新库。