第27章 撰写C函数的技巧

本文介绍了C函数在Lua中的高效运用技巧,包括数组操作、字符串处理及状态保存的方法。通过具体的代码实例,深入探讨了如何利用Lua API进行数组操作以提升性能,如何正确处理字符串以避免内存溢出等问题,以及如何在C函数中安全地保存状态。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

27 撰写C函数的技巧
官方的 API 和辅助函数库 都提供了一些帮助程序员如何写好 C 函数的机制。在这一章我们将讨论数组操纵、 string 处理、在 C 中存储 Lua 值等一些特殊的机制。
27.1 数组操作
Lua 中数组实际上就是以特殊方式使用的 table 的别名。我们可以使用任何操纵 table 的函数来对数组操作,即 lua_settable lua_gettable 。然而,与 Lua 常规简洁思想( economy and simplicity )相反的是, API 为数组操作提供了一些特殊的函数。这样做的原因出于性能的考虑:因为我们经常在一个算法(比如排序)的循环的内层访问数组,所以这种内层操作的性能的提高会对整体的性能的改善有很大的影响。
API 提供了下面两个数组操作函数:
void lua_rawgeti (lua_State *L, int index, int key);
void lua_rawseti (lua_State *L, int index, int key);
关于的 lua_rawgeti lua_rawseti 的描述有些使人糊涂,因为它涉及到两个索引: index 指向 table 在栈中的位置; key 指向元素在 table 中的位置。当 t 使用负索引的时候( otherwise you must compensate for the new item in the stack ),调用 lua_rawgeti(L,t,key) 等价于:
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);
注意这两个寒暑都是用 raw 操作,他们的速度较快,总之,用作数组的 table 很少使用 metamethods
下面看如何使用这些函数的具体的例子,我们将前面的 l_dir 函数的循环体:
lua_pushnumber(L, i++);             /* key */
lua_pushstring(L, entry->d_name); /* value */
lua_settable(L, -3);
改写为:
lua_pushstring(L, entry->d_name); /* value */
lua_rawseti(L, -2, i++);           /* set table at key 'i' */
下面是一个更完整的例子,下面的代码实现了 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_getn(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 */
}
这里面引入了三个新的函数。 luaL_checktype (在 lauxlib.h 中定义)用来检查给定的参数有指定的类型;否则抛出错误。 luaL_getn 函数栈中指定位置的数组的大小( table.getn 是调用 luaL_getn 来完成工作的)。 lua_call 的运行是无保护的,他与 lua_pcall 相似,但是在错误发生的时候她抛出错误而不是返回错误代码。当你在应用程序中写主流程的代码时,不应该使用 lua_call ,因为你应该捕捉任何可能发生的错误。当你写一个函数的代码时,使用 lua_call 是比较好的想法,如果有错误发生,把错误留给关心她的人去处理。
27.2 字符串处理
C 函数接受一个来自 lua 的字符串作为参数时,有两个规则必须遵守:当字符串正在被访问的时候不要将其出栈;永远不要修改字符串。
C 函数需要创建一个字符串返回给 lua 的时候,情况变得更加复杂。这样需要由 C 代码来负责缓冲区的分配和释放,负责处理缓冲溢出等情况。然而, Lua API 提供了一些函数来帮助我们处理这些问题。
标准 API 提供了对两种基本字符串操作的支持:子串截取和字符串连接。记住, lua_pushlstring 可以接受一个额外的参数,字符串的长度来实现字符串的截取,所以,如果你想将字符串 s i j 位置(包含 i j )的子串传递给 lua ,只需要:
lua_pushlstring(L, s+i, j-i+1);
下面这个例子,假如你想写一个函数来根据指定的分隔符分割一个字符串,并返回一个保存所有子串的 table ,比如调用:
split("hi,,there", ",")
应该返回表 {"hi", "", "there"} 。我们可以简单的实现如下,下面这个函数不需要额外的缓冲区,可以处理字符串的长度也没有限制。
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 */
}
Lua API 中提供了专门的用来连接字符串的函数 lua_concat 。等价于 Lua 中的 .. 操作符:自动将数字转换成字符串,如果有必要的时候还会自动调用 metamethods 。另外,她可以同时连接多个字符串。调用 lua_concat(L,n) 将连接 ( 同时会出栈 ) 栈顶的 n 个值,并将最终结果放到栈顶。
另一个有用的函数是 lua_pushfstring
const char *lua_pushfstring (lua_State *L,
                                const char *fmt, ...);
这个函数某种程度上类似于 C 语言中的 sprintf ,根据格式串 fmt 的要求创建一个新的字符串。与 sprintf 不同的是,你不需要提供一个字符串缓冲数组, Lua 为你动态的创建新的字符串,按他实际需要的大小。也不需要担心缓冲区溢出等问题。这个函数会将结果字符串放到栈内,并返回一个指向这个结果串的指针。当前,这个函数只支持下列几个指示符: %% (表示字符 '%' )、 %s (用来格式化字符串)、 %d (格式化整数)、 %f (格式化 Lua 数字,即 doubles )和 %c (接受一个数字并将其作为字符),不支持宽度和精度等选项。
当我们打算连接少量的字符串的时候, lua_concat lua_pushfstring 是很有用的,然而,如果我们需要连接大量的字符串(或者字符),这种一个一个的连接方式效率是很低的,正如我们在 11.6 节看到的那样。我们可以使用辅助库提供的 buffer 相关函数来解决这个问题。 Auxlib 在两个层次上实现了这些 buffer 。第一个层次类似于 I/O 操作的 buffers :集中所有的字符串(或者但个字符)放到一个本地 buffer 中,当本地 buffer 满的时候将其传递给 Lua (使用 lua_pushlstring )。第二个层次使用 lua_concat 和我们在 11.6 节中看到的那个栈算法的变体,来连接多个 buffer 的结果。
为了更详细地描述 Auxlib 中的 buffer 的使用,我们来看一个简单的应用。下面这段代码显示了 string.upper 的实现(来自文件 lstrlib.c ):
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_putchar(&b, toupper((unsigned char)(s[i])));
    luaL_pushresult(&b);
    return 1;
}
使用 Auxlib buffer 的第一步是使用类型 luaL_Buffer 声明一个变量,然后调用 luaL_buffinit 初始化这个变量。初始化之后, buffer 保留了一份状态 L 的拷贝,因此当我们调用其他操作 buffer 的函数的时候不需要传递 L 。宏 luaL_putchar 将一个单个字符放入 buffer Auxlib 也提供了 luaL_addlstring 以一个显示的长度将一个字符串放入 buffer ,而 luaL_addstring 将一个以 0 结尾的字符串放入 buffer 。最后, luaL_pushresult 刷新 buffer 并将最终字符串放到栈顶。这些函数的原型如下:
void luaL_buffinit (lua_State *L, luaL_Buffer *B);
void luaL_putchar (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 可以毫无问题的处理大字符串(大于 1MB )。
当你使用 auxlib 中的 buffer 时,不必担心一点细节问题。你只要将东西放入 buffer ,程序会自动在 Lua 栈中保存中间结果。所以,你不要认为栈顶会保持你开始使用 buffer 的那个状态。另外,虽然你可以在使用 buffer 的时候,将栈用作其他用途,但每次你访问 buffer 的时候,这些其他用途的操作进行的 push/pop 操作必须保持平衡 [8] 。有一种情况,即你打算将从 Lua 返回的字符串放入 buffer 时,这种情况下,这些限制有些过于严格。这种情况下,在将字符串放入 buffer 之前,不能将字符串出栈,因为一旦你从栈中将来自于 Lua 的字符串移出,你就永远不能使用这个字符串。同时,在将一个字符串出栈之前,你也不能够将其放入 buffer ,因为那样会将栈置于错误的层次( because then the stack would be in the wrong level )。换句话说你不能做类似下面的事情:
luaL_addstring(&b, lua_tostring(L, 1));   /* BAD CODE */
(译者:上面正好构成了一对矛盾),由于这种情况是很常见的, auxlib 提供了特殊的函数来将位于栈顶的值放入 buffer
void luaL_addvalue (luaL_Buffer *B);
当然,如果位于栈顶的值不是字符串或者数字的话,调用这个函数将会出错。
27.3 C函数中保存状态
通常来说, C 函数需要保留一些非局部的数据,也就是指那些超过他们作用范围的数据。 C 语言中我们使用全局变量或者 static 变量来满足这种需要。然而当你为 Lua 设计一个程序库的时候,全局变量和 static 变量不是一个好的方法。首先,不能将所有的(一般意义的,原文 generic Lua 值保存到一个 C 变量中。第二,使用这种变量的库不能在多个 Lua 状态的情况下使用。
一个替代的解决方案是将这些值保存到一个 Lua 全局变两种,这种方法解决了前面的两个问题。 Lua 全局变量可以存放任何类型的 Lua 值,并且每一个独立的状态都有他自己独立的全局变量集。然而,并不是在所有情况下,这种方法都是令人满意地解决方案,因为 Lua 代码可能会修改这些全局变量,危及 C 数据的完整性。为了避免这个问题, Lua 提供了一个独立的被称为 registry 的表, C 代码可以自由使用,但 Lua 代码不能访问他。
27.3.1 The Registry
registry 一直位于一个由 LUA_REGISTRYINDEX 定义的值所对应的假索引 (pseudo-index) 的位置。一个假索引除了他对应的值不在栈中之外,其他都类似于栈中的索引。 Lua API 中大部分接受索引作为参数的函数,也都可以接受假索引作为参数—除了那些操作栈本身的函数,比如 lua_remove lua_insert 。例如,为了获取以键值 "Key" 保存在 registry 中的值,使用下面的代码:
lua_pushstring(L, "Key");
lua_gettable(L, LUA_REGISTRYINDEX);
registry 就是普通的 Lua 表,因此,你可以使用任何非 nil Lua 值来访问她的元素。然而,由于所有的 C 库共享相同的 registry ,你必须注意使用什么样的值作为 key ,否则会导致命名冲突。一个防止命名冲突的方法是使用 static 变量的地址作为 key C 链接器保证在所有的库中这个 key 是唯一的。函数 lua_pushlightuserdata 将一个代表 C 指针的值放到栈内,下面的代码展示了使用上面这个方法,如何从 registry 中获取变量和向 registry 存储变量:
/* variable with an unique address */
static const char Key = 'k';
 
/* store a number */
lua_pushlightuserdata(L, (void *)&Key); /* push address */
lua_pushnumber(L, myNumber); /* push value */
/* registry[&Key] = myNumber */
lua_settable(L, LUA_REGISTRYINDEX);
 
/* retrieve a number */
lua_pushlightuserdata(L, (void *)&Key);   /* push address */
lua_gettable(L, LUA_REGISTRYINDEX); /* retrieve value */
myNumber = lua_tonumber(L, -1); /* convert to number */
我们会在 28.5 节中更详细的讨论 light userdata
当然,你也可以使用字符串作为 registry key ,只要你保证这些字符串唯一。当你打算允许其他的独立库房问你的数据的时候,字符串型的 key 是非常有用的,因为他们需要知道 key 的名字。对这种情况,没有什么方法可以绝对防止名称冲突,但有一些好的习惯可以采用,比如使用库的名称作为字符串的前缀等类似的方法。类似 lua 或者 lualib 的前缀不是一个好的选择。另一个可选的方法是使用 universal unique identifier uuid ),很多系统都有专门的程序来产生这种标示符(比如 linux 下的 uuidgen )。一个 uuid 是一个由本机 IP 地址、时间戳、和一个随机内容组合起来的 128 位的数字(以 16 进制的方式书写,用来形成一个字符串),因此它与其他的 uuid 不同是可以保证的。
27.3.2 References
你应该记住,永远不要使用数字作为 registry key ,因为这种类型的 key 是保留给 reference 系统使用。 Reference 系统是由辅助库中的一对函数组成,这对函数用来不需要担心名称冲突的将值保存到 registry 中去。(实际上,这些函数可以用于任何一个表,但他们典型的被用于 registry
调用
int r = luaL_ref(L, LUA_REGISTRYINDEX);
从栈中弹出一个值,以一个新的数字作为 key 将其保存到 registry 中,并返回这个 key 。我们将这个 key 称之为 reference
顾名思义,我们使用 references 主要用于:将一个指向 Lua 值的 reference 存储到一个 C 结构体中。正如前面我们所见到的,我们永远不要将一个指向 Lua 字符串的指针保存到获取这个字符串的外部的 C 函数中。另外, Lua 甚至不提供指向其他对象的指针,比如 table 或者函数。因此,我们不能通过指针指向 Lua 对象。当我们需要这种指针的时候,我们创建一个 reference 并将其保存在 C 中。
要想将一个 reference 的对应的值入栈,只需要:
lua_rawgeti(L, LUA_REGISTRYINDEX, r);
最后,我们调用下面的函数释放值和 reference
luaL_unref(L, LUA_REGISTRYINDEX, r);
调用这个之后, luaL_ref 可以再次返回 r 作为一个新的 reference
reference 系统将 nil 作为特殊情况对待,不管什么时候,你以 nil 调用 luaL_ref 的话,不会创建一新的 reference ,而是返回一个常量 reference LUA_REFNIL 。下面的调用没有效果:
luaL_unref(L, LUA_REGISTRYINDEX, LUA_REFNIL);
然而
lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_REFNIL);
像预期的一样,将一个 nil 入栈。
reference 系统也定义了常量 LUA_NOREF ,她是一个表示任何非有效的 reference 的整数值,用来标记无效的 reference 。任何企图获取 LUA_NOREF 返回 nil ,任何释放他的操作都没有效果。
27.3.3 Upvalues
registry 实现了全局的值, upvalue 机制实现了与 C static 变量等价的东东,这种变量只能在特定的函数内可见。每当你在 Lua 中创建一个新的 C 函数,你可以将这个函数与任意多个 upvalues 联系起来,每一个 upvalue 可以持有一个单独的 Lua 值。下面当函数被调用的时候,可以通过假索引自由的访问任何一个 upvalues
我们称这种一个 C 函数和她的 upvalues 的组合为闭包( closure )。记住:在 Lua 代码中,一个闭包是一个从外部函数访问局部变量的函数。一个 C 闭包与一个 Lua 闭包相近。关于闭包的一个有趣的事实是,你可以使用相同的函数代码创建不同的闭包,带有不同的 upvalues
看一个简单的例子,我们在 C 中创建一个 newCounter 函数。(我们已经在 6.1 节部分在 Lua 中定义过同样的函数)。这个函数是个函数工厂:每次调用他都返回一个新的 counter 函数。尽管所有的 counters 共享相同的 C 代码,但是每个都保留独立的 counter 变量,工厂函数如下:
/* forward declaration */
static int counter (lua_State *L);
 
int newCounter (lua_State *L) {
    lua_pushnumber(L, 0);
    lua_pushcclosure(L, &counter, 1);
    return 1;
}
这里的关键函数是 lua_pushcclosure ,她的第二个参数是一个基本函数(例子中卫 counter ),第三个参数是 upvalues 的个数(例子中为 1 )。在创建新的闭包之前,我们必须将 upvalues 的初始值入栈,在我们的例子中,我们将数字 0 作为唯一的 upvalue 的初始值入栈。如预期的一样, lua_pushcclosure 将新的闭包放到栈内,因此闭包已经作为 newCounter 的结果被返回。
现在,我们看看 counter 的定义:
static int counter (lua_State *L) {
    double val = lua_tonumber(L, lua_upvalueindex(1));
    lua_pushnumber(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 的假索引。这个假索引除了不在栈中之外,和其他的索引一样。表达式 lua_upvalueindex(1) 函数第一个 upvalue 的索引。因此,在函数 counter 中的 lua_tonumber 获取第一个 ( 仅有的 )upvalue 的当前值,转换为数字型。然后,函数 counter 将新的值 ++val 入栈,并将这个值的一个拷贝使用新的值替换 upvalue 。最后,返回其他的拷贝。
Lua 闭包不同的是, C 闭包不能共享 upvalues :每一个闭包都有自己独立的变量集。然而,我们可以设置不同函数的 upvalues 指向同一个表,这样这个表就变成了一个所有函数共享数据的地方。
 
 
 
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值