Lua_第26章撰写 C 函数的技巧

第 26章 撰写 C 函数的技巧

官方的 API 和辅助函数库都提供了一些帮助程序员如何写好 C 函数的机制。在这一章我们将讨论数组操纵、string 处理、在 C 中存储 Lua 值等一些特殊的机制。

26.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 是比较好的想法,如果有错误发生,把错误留给关心她的人去处理

26.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_concatlua_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操作必须保持平衡(即有多少次push就要有多少次pop)有一种情况,即你打算将从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);
当然,如果位于栈顶的值不是字符串或者数字的话,调用这个函数将会出错。

26.3 在 C 函数中保存状态
       通常来说,C 函数需要保留一些非局部的数据,也就是指那些超过他们作用范围的数据。C 语言中我们使用全局变量或者 static 变量来满足这种需要。然而当你为 Lua 设 计一个程序库的时候,全局变量和 static 变量不是一个好的方法首先,不能将所有的 (一般意义的,原文 generic)Lua 值保存到一个 C 变量中。第二,使用这种变量的库不能在多个 Lua 状态的情况下使用。
一个替代的解决方案是将这些值保存到一个 Lua 全局变两种,这种方法解决了前面 的两个问题。Lua 全局变量可以存放任何类型的 Lua 值,并且每一个独立的状态都有他 自己独立的全局变量集。然而,并不是在所有情况下,这种方法都是令人满意地解决方案,因为 Lua 代码可能会修改这些全局变量,危及 C 数据的完整性。为了避免这个问题, Lua 提供了一个独立的被称为 registry 的表,C 代码可以自由使用,但 Lua 代码不能访问他

26.3.1The Registry

       registry 一 直位于一个 由 LUA_REGISTRYINDEX 定义的值所对应的假索引 (pseudo-index)的位置。一个假索引除了他对应的值不在栈中之外,其他都类似于栈中的 索引。Lua API 中大部分接受索引作为参数的函数,也都可以接受假索引作为参数一除 了那些操作栈本身的函数,比如 lua_remove,lua_insert。例如,为了获取以键值 "Key" 保 存在 registry 中的值,使用下面的代码:
<pre name="code" class="csharp">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 */ 

我们会在 27.5 节中更详细的讨论 light userdata。

       当然,你也可以使用字符串作为 registry 的 key,只要你保证这些字符串唯一。当你打算允许其他的独立库房问你的数据的时候,字符串型的 key 是非常有用的,因为他们需要知道 key 的名字。对这种情况,没有什么方法可以绝对防止名称冲突,但有一些好的习惯可以采用,比如使用库的名称作为字符串的前缀等类似的方法。类似 lua 或者 lualib 的前缀不是一个好的选择。另一个可选的方法是使用 universal unique identifier(uuid), 很多系统都有专门的程序来产生这种标示符(比如 linux 下的 uuidgen)。一个 uuid 是一 个由本机 IP 地址、时间戳、和一个随机内容组合起来的 128 位的数字(以16 进制的方式书写,用来形成一个字符串),因此它与其他的 uuid 不同是可以保证的。
 
26.3.2References

       你应该记住,永远不要使用数字作为 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,任何释放他 的操作都没有效果。

26.3.3Upvalues
       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 指向同一个表,这样这个表就变成了一个 所有函数共享数据的地方。

你可能感兴趣的:(lua)