Chapter 27: Techniques for Writing C Functions
官方API 和辅助库都提供了一些机制用来帮助C 函数的编写。本章将讨论数组操作,string 操作,和将lua 值存在C 中。
27.1 Array Manipulation
“数组”只是表在特殊用法下的一个别名。数组操作可以使用lua_settable 和lua_gettable,但是API 还为数组操作提供了别的函数。提供这些额外函数的原因是性能:在内部循环进行数组访问是常见的操作(例如sorting),所以此操作的任何性能提升对整个算法来说会有很大影响。另一个原因是像string keys,integer keys 需要特别处理。
API 提供两个数组操作函数:
void lua_rawgeti (lua_State *L, int index, int key);
void lua_rawseti (lua_State *L, int index, int key);
其中index 是表在栈中的索引,key 是元素在表中的索引。
lua_rawgeti(L,t,key) (其中t 为正数)等价于:
lua_pushnumber(L, key);
lua_rawget(L, t);
lua_rawseti(L,t,key) (其中t 为正数)等价于:
lua_pushnumber(L, key);
lua_insert(L, -2); /* put 'key' below previous value */
lua_rawset(L, t);
它们快速,并且数组很少使用元表。
以下代码实现map function:用给定的函数作用于数组的每一个元素,并用函数值替换掉原元素。
main code 中用lua_pcall 而不用lua_call,因为你想要捕获错误。当你写函数,使用lua_call 是一个好主意,如遇到错误它会留给别人处理。
map function
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 = lua_objlen(L, 1); /* get size of table */
for (i = 1; i <= n; i++) {
lua_pushvalue(L, 2); /* push f */
lua_rawgeti(L, 1, i); /* push t[i] */
lua_call(L, 1, 1); /* call f(t[i]) */
lua_rawseti(L, 1, i); /* t[i] = result */
}
return 0; /* no results */
}
测试可以运行lua 解释器,然后用dofile(‘a.lua’) 来运行脚本
-- 测试
-- a.lua
function cut(i)
return i - i % 0.01
end
function printTable(t)
for i, v in ipairs(t) do
print(v)
end
end
print(my.sin(90))
--> 1
t = {math.pi, 2^0.5}
printTable(t)
--> 3.1415926535898
--> 1.4142135623731
my.map(t, cut)
printTable(t)
-->3.14
-->1.41
27.2 String Manipulation
当C 函数从Lua 获得一个string 参数,必须遵守两个准则:不要从栈中弹出这个string,不要修改这个string。
如果要从C 创建一个string 返回给lua 那么还有更多值得注意的地方。这种情况下buffer 的分配和释放,缓冲区溢出等问题都由C 负责。尽管如此,Lua API 也为这些任务提供了一些函数。
标准API 提供了两最基本的串操作函数:子串提取和串连接。lua_pushlstring 接受一个长度作为额外参数。因此,如果你想给lua 传一个string从位置i 到j的子串,可以这样做
给lua 传一个从位置i 到j的string 的子串
lua_pushlstring(L, s + i, j - i + 1);
要以一个给定的分隔符(单个字符)来splits 一个string,并返回一个子串的表。例如,调用split("hi,ho,there",",") 会返回{“hi”, “ho”, “there”}
对一个string 分割子串
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); /* result */
/* repeat for each separator */
while ((e = strchr(s, *sep)) != NULL) {
lua_pushlstring(L, s, e-s); /* push substring */
lua_rawseti(L, -2, i++);
s = e + 1; /* skip separator */
}
/* push last substring */
lua_pushstring(L, s);
lua_rawseti(L, -2, i);
return 1; /* return the table */
}
t = my.split("hi,ho,there",",")
printTable(t)
--> hi
--> ho
--> there
这个函数既不需要额外的buffer,也不限制串的长度。
lua_concat 用来连接strings。和Lua 的“…” 操作符类似:它将数字转成strings,需要的时侯triggers 元方法。
lua_concat(L,n) 连接栈顶的n 个元素,并一一将它们弹出,连接的结果会放在栈顶上。
lua_pushfstring 类似C 的sprintf
const char *lua_pushfstring (lua_State *L, const char *fmt, ...);
不同于sprintf,这里不需要你提供buffer,lua 会动态为你创建string,要多大有多大,也无需担心缓冲区溢出此类问题。此函数将结果string 放栈顶,并返回它的一个指针。此函数不能指定宽度和精度。“%%” for %,“%s”for strings,“%d”for integers,“%f”for lua 数字,既double,“%c” for 一个字符或一个整数。
当我们只想连接少量strings 时,lua_concat 和lua_pushfstring 都很有用。但是对连接大量stirngs 来说一个接一个的连接效率太低,就像我们在11.6 节看到的。我们可以使用由辅助库提供的buffer。auxlib 库以两种levels 实现这些buffer。第一level 是类似buffer I/O 操作:它在本地栈中收集小的strings(或单个字符),当buffer 满了就将它们传给Lua(使用lua_pushlstring)。第二level 使用lua_concat 和一个栈算法的变体,就是11.6 节我们看到的,连接多个buffer 的结果。
下面代码是lstrlib.c 中string.upper 的实现:
str_upper 的实现
static int str_upper (lua_State *L) {
size_t l;
size_t i;
luaL_Buffer b;
const char *s = luaL_checklstring(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;
}
使用auxlib 的buffer,第一步是声明一个luaL_Buffer,并使用LuaL_buffinit 把它初始化。初始化完成后,luaL_Buffer里面存了一个状态 State L 的拷贝,所以当调用操作这个buffer 的其它函数时不需要给它传State L。
luaL_addchar将单个字符放入buffer 中。luaL_addlstring 将定长string 入栈,luaL_addstring 将以0 结尾的string 入栈。最后,luaL_pushresult 将最终结果string 放在栈顶,并清空buffer。这些函数的原型如下:
void luaL_buffinit (lua_State *L, luaL_Buffer *B);
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);
使用这些函数不用担心buffer 分配,溢出和其它细节。我们看到连接算法相当高效,str_upper 函数处理巨大strings(大于1 M)没有任何问题。
使用luaL_Buffer 需要注意一个问题,当你在buffer 中装入东西,它会将一些中间结果保存在Lua 栈上。因此,操作buffer时不要假定栈顶会保持和你操作buffer 之前一样的状态。
??
luaL_addstring(&b, lua_tostring(L, 1)); /* BAD CODE */
安全起见,往buffer 加东西用luaL_addvalue函数:
void luaL_addvalue (luaL_Buffer *B);
当然,如果栈顶的元素不是一个string,或number 则出错。
27.3 Storing State in C Functions
C 中我们常用全局变量或static 变量来保存那些需要长久存在的值。但是当你为lua 编写库函数时,global 或static 变量不是一个好方法。首先,你不能将lua 的范型值存在C 变量中。其次,使用这些global 或static 值的库不能被用于multiple Lua states。
一个Lua 函数有三个地方存放非局部变量:全局变量,函数环境,和non-local variables。
C API 也提供三个位置来存放非局部数据:registry, environments, 和 upvalues。
registry 是一个只能由C 代码访问的全局表。registry通常用来存放那些被多个模块共享的数据。environments 通常用来存放单个模块的私有数据。像lua 函数一样,每一个C 函数都有自已的环境表。通常同个模块的所有函数共享同一个环境表,所以它们可以共享数据。最后,一个C 函数也以有upvalues,它是lua values,关联到那个特殊函数。
The registry
pseudo-index 像栈的索引,但是与之相关联的数据不在栈里面。Lua API 的多数函数接受索引作为参数,同时也接受伪索引(pseudo-index)—— 但是对那些操作栈本身的函数除外,如lua_remove 和lua_insert。例如,要取得存在registry 里面以“key”为索引的值,可以使用下面的调用:
获取registry 的值
lua_getfield(L, LUA_REGISTRYINDEX, "Key");
registry 是一个普通的表。你可以使用除了nil 外的任意lua 值来索引它。但是因为所有C 模块共享同一个registry,你必须小心选择作为keys 的值,以避免发生碰撞。当你想允许其它独立的库访问你的数据,使用string 作为key 特别有用,因为它们只需知道key 的名字。
对string key 名字的选取应避免通常的名字,使用库名为前缀命名是一个好办法。另一个选择是使用唯一标识(uuid),一个uuid 是128 位的数字(字串的十六进制表示),由MAC 地址的组合,加上一个时间戮,再加上一个随机组件,这样就保证了不和其它任何uuid 相同。
不要使用数字作为registry 的keys,因为这些keys 保留给reference system 使用。这个system 由auxiliary library 中的一堆函数组成,这些函数充许你往一个table 存入一些值,而不用去关心如何创建唯一名字。
int r = luaL_ref(L, LUA_REGISTRYINDEX);
这个调用从栈中弹出一个值,以一个新的整数做为key,将这个值存进registry,并返回这个key。我们称这个key 为一个reference。
我们通常在这样的情况下使用引用:当需要存储一个引用,这个引用是C 结构内部的一个Lua 值。C 函数获得的指向lua strings 的指针不能被存储在这个函数的外部。Lua 甚至不提供指向lua strings 之外的对象的指针,如表或函数指针。所以我们不能通过指针来引用一个lua 对象。当需要这样的指针时,我们创建一个引用,并存储在C 中。
将一个与引用r 相关联的值压栈
lua_rawgeti(L, LUA_REGISTRYINDEX, r);
同时释放引用及其相关联的值
luaL_unref(L, LUA_REGISTRYINDEX, r);
引用系统对nil 有特别的对待。当以nil 值为参数调用luaL_ref,它不会创建一个新引用,而是返回常量LUA_REFNIL。
这个调用无效
luaL_unref(L, LUA_REGISTRYINDEX, LUA_REFNIL);
这个调用将一个nil 值入栈
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_REFNIL);
引用系统还定义了一个常量LUA_NOREF,它是一个不同于任一引用的整数,可以用它来标记无效引用。任何试图使用LUA_NOREF 来取值的行为都会返回nil,并且任何释放LUA_NOREF 的行为都是无效的。
另一种创建registy keys 的方法是使用static 变量的地址作为key:C 链接器保证这个key 在所有库中是唯一的。这种选择需要lua_pushlightuserdata 函数,它将一个C 指针对应的一个值压入lua 栈。下面的代码展示了如何使用这种方法从registry存储和取回一个string:
使用lua_pushlightuserdata从registry存储和取回一个string
/* variable with an unique address */
static const 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 */
我们将会在28.5 节对light userdata 进行更详细的讨论。
Environments for C functions
因为在lua 5.1,每一个我们在lua 注册的C 函数都有自已的环境表。这些函数可以通过一个pseudo-index 访问它的环境,这和通过pseudo-index 访问registry 是一样的。环境的pseudo index 是LUA_ENVIRONINDEX。
通常和在模块中使用环境是一样的。我们为模块创建一个表,并让模块中的所有的函数都共享这个表。在C 中设置这样的被共享环境的方法也类似于在Lua 中我们设置这些环境的方法:我们简单地改变main chunk 的环境,所以由它创建的所有函数会自动继承这个新环境。在C 中设置这样一个环境的代码像这样:
int luaopen_foo (lua_State *L) {
lua_newtable(L);
lua_replace(L, LUA_ENVIRONINDEX);
luaL_register(L, <libname>, <funclist>);
...
}
luaopen_foo 函数创建一个新表作为被共享的environment,并且使用lua_replace 来设置这个表作为它自已的environment。然后,当它调用luaL_register,所有这里被创建出来的新函数将继承这个当前环境。
你应该总是使用environment 而不是register,除非你需要和其它模块共享数据。特别地,你可以使用引用系统应用环境表,来创建仅对模块可见的引用。
Upvalues
register 提供全局变量,environment 提供模块变量,而upvalues实现同等机制C 静态变量,仅在特殊函数内部可见。每次你在lua 中创建一个新的C 函数,你可以用任意数量的upvalues 和这个函数相关联,每个upvalue 可以存一个lua 值。稍后,当这个函数被调用,它可以使用pseuso-indices自由访问它的任意upvalues。
我们称这种C 函数及其upvalues 的结合体为一个C closure。C closure 与Lua closure 类似。C closure 的一个有趣事实是,你可以使用相同的函数代码加上不同的upvalues创建不同的闭包
看一个简单的例子,让我们创建一个C函数newCounter(6.1 节我们用lua 定义了同样的东西)。这个函数是一个工厂:每次被调用都返回一个新的记数器。虽然所有计数器共享同样的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,它创建一个新闭包。它的第二参数是一个基函数(在这个例子中是记数器),第三参数是upvalues 的个数(此例中是1)。创建一个新闭包之前,我们必须将其upvalues的初始值压入栈中。在我们的例子中,我们为单独的upvalue压入数字0 作为初始值。像预期的那样,lua_pushcclosure 将新闭包放在栈上,所以闭包准备好了作为newCounter 的结果返回。
现在,让我们来看counter 的定义:
static int counter (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 */
}
这里的关键函数是lua_upvalueindex(它实际上是一个宏),它产生一个upvalue的pseudo-index。这个pseudo-index 像任何栈index 一样,除了它不活在栈中。表达式lua_upvalueindex(1) 是对函数的第一个upvalue 的索引的引用。所以对lua_tointeger 的调用作为一个数取回第一个upvalue 的当前值。然后,计数器函数压入新值++val,拷贝它,并使用一个拷贝来替换upvalue 的值。最后,它返回另一个copy 作为计数器的返回值。
作为一个更高级的例子,我们将使用upvalues 实现tuples。tuple 是一种带匿名域的常量记录。你可以使用一个数值索引取得一个特定的域,也可以一次获取所有域。在我们的实现中,我们将tuples 表示为函数,这些函数将自已的值存储在它们的upvalues 中。如果以一个数值为参数调用uple 函数,它会返回特定的域,而当以空参调用时,它就返回其所有域。下面的代码演示了tuples 的使用:
x = tuple.new(10, "hi", {}, 3)
print(x(1)) --> 10
print(x(2)) --> hi
print(x()) --> 10 hi table: 0x8087878 3
C 中,我们将所有tuples表示为同样的t_tuple函数。因为我们既可以传空参也可以传一个数值参数,t_tuple 使用luaL_optint 来获取它的可选参数。luaL_optint 函数类似于luaL_checkint,不同的地方在于如果缺少参数它会返回一个指定的默认值(此例是0),而不会去抱怨。
当我们索引一个不存在的值,结果是一个pseudo-value,它的类型是LUA_TNONE。(栈中也会发生类似情况,如果索引是top 之上)所以,我们的t_tuple 函数使用lua_isnone 来测试它是否有一个指定的upvalue。但是,我们不应该以一个负索引调用lua_upvalueindex,所以当用户提供一个索引时我们必须检查这种情况。luaL_argcheck 函数检查一个给定的条件,需要的时侯就引发一个错误。
创建tuples 的函数t_new 是trivial:因为它的参数已经在栈中,它只需要调用lua_pushcclosure来创建一个tuple 的闭包,使用这些参数作为upvalues。最后,数组tuplelib 和函数luaopen_tuple 是用来创建一个tuple 库的标准代码,使用那个单独的new函数。
Upvalues 仅在特殊函数内部可见的变量
---lmylib.c .c
int t_tuple (lua_State *L) {
int op = luaL_optint(L, 1, 0);
if (op == 0) { /* no arguments? */
int i;
/* push each valid upvalue onto the stack */
for (i = 1; !lua_isnone(L, lua_upvalueindex(i)); i++)
lua_pushvalue(L, lua_upvalueindex(i));
return i - 1; /* number of values in the stack */
}
else { /* get field 'op' */
luaL_argcheck(L, 0 < op, 1, "index out of range");
if (lua_isnone(L, lua_upvalueindex(op)))
return 0; /* no such field */
lua_pushvalue(L, lua_upvalueindex(op));
return 1;
}
}
int t_new (lua_State *L) {
lua_pushcclosure(L, t_tuple, lua_gettop(L));
return 1;
}
static const luaL_Reg mylib[] = {
{"sin", my_sin},
{"dir", l_dir},
{"map", l_map},
{"split", l_split},
{"new", t_new},
{NULL, NULL}
};
LUALIB_API int luaopen_my (lua_State *L) {
luaL_register(L, LUA_MYLIBNAME, mylib);
return 1;
}
-- a.lua
x = my.new(10, "hi", {}, 3)
print(x(1)) --> 10
print(x(2)) --> hi
print(x()) --> 10 hi table: 0x8087878 3