再说C模块的编写
前言
在《Lua“控制”C》中对Lua调用C函数做了初步的学习,而这篇才是重中之重,这篇文章会重点的总结C模块编写过程中遇到的一些问题,比如数组操作、字符串操作和C函数的状态保存等问题。现在就开始吧。
数组操作
在Lua中应该不能叫数组,而是一种table的东西;而在C语言中,没有table这种东西,只有数组。Lua中的table可以使关联的,也就 是key=>value键值对,而C中,数组不是关联的,下标是从0开始的。当然了,Lua中的数组表示,只是table的一个子集,就是因为这种 关系,就有了C数组和Lua table的交互关系了。
比如lua_settable和lua_gettable这种操作table的API(其实之前我一直用的都是lua_setfield和lua_getfield),也可以操作数组。然而,API为数组操作提供了专门的函数,出于以下两个原因:
- 性能;我们一般使用C语言来扩展Lua,都是用来做一些Lua难以做到,而C却非常容易做到的事情,比如一些追求效率的算法;如果提高了访问数组的效率,那就能提高整个算法的性能了;
- 便利;整数key是非常常用的,所以提供专门的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中的元素。这两个函数都是原始操作,比涉及元表的table访问更快。通常,作为数组使用的table很少会用到元表。
下面就来一个实例,看看如何使用上面的两个API函数,不知道你会不会PHP,在PHP中,有一个array_walk函数,这个函数允许用户定义一个函数,然后对数组中的每个函数都应用这个函数。我现在就来实现这个功能。把重点代码贴上来:
static int array_walk(lua_State *L)
{
// 和写别的函数一行,先检查参数的合法性
// 第一个参数必须是一个table
luaL_checktype(L, 1, LUA_TTABLE);
// 第二个参数必须是一个用户定义的函数
luaL_checktype(L, 2, LUA_TFUNCTION);
// 获取table的大小
int iLen = lua_objlen(L, 1);
for (int i = 1; i <= iLen; ++i)
{
// 将用户定义的函数压入栈
lua_pushvalue(L, 2);
// 将参数table的所以i对应的值压入栈
lua_rawgeti(L, 1, i);
// 调用用户定义的函数
lua_call(L, 1, 1);
lua_rawseti(L, 1, i);
}
// 没有返回值压入栈中
return 0;
}
代码比较简单,不多说,哪里不懂的地方,可以留言。对于代码中出现的luaL_checktype和lua_call函数,这里说一下。 luaL_checktype用来检查给定的参数符合特定的类型,从而防止由于参数类型错误而引起的后续错误;如果参数不正确,这个函数就会引发一个错 误。
lua_call运行在无保护的模式下,这个是它和lua_pcall最大的区别,所以它在发生错误时,会传播错误,而不是简单的返回一个错误代 码。在我们的实际编程开发中,在一个应用程序中编写主函数时,不应该使用lua_call,因为这样需要捕获所有的错误;而编写C函数时,通常可以用 lua_call,当错误发生时,就应该让错误显示出来。
上面只是贴出了关键代码,可以点击这里下载完整工程。
字符串操作
实际开发中,我们都是在和各种字符串打交道,现在我们就来完成这个功能,Lua传进一个字符串到C模块中,C模块进行字符串处理。
当一个C函数从Lua接收到一个字符串参数时,必须遵守两条规则:
- 不要在访问字符串时,从栈中弹出它;
- 不要修改字符串。
当一个C函数需要创建一个字符串返回给Lua时,C代码还必须处理字符串缓冲的分配和释放等问题。Lua API也提供了一些函数来帮助完成这些任务。
标准API为两种常用的字符串操作提供了支持:提取子串和字符串连接。lua_pushlstring支持提取子串,它接受一个额外的字符串长度参 数,这就好比我们在压入栈时,对字符串进行了一个截取操作。下面我先来完成一个简单的功能,根据指定的切割符号来切割字符串,将子串保存在一个table 中,然后向Lua返回这个table。来吧!!!
static int split(lua_State *L)
{
// 传进来两个参数,先检查参数的合法性
const char *pSrc = luaL_checkstring(L, 1);
const char *pSep = luaL_checkstring(L, 2);
lua_newtable(L);
int index = 1;
char *pLocation = NULL;
while ((pLocation = strchr(pSrc, *pSep)) != NULL)
{
// 压入字符串
lua_pushlstring(L, pSrc, pLocation - pSrc);
// 设置结果表
lua_rawseti(L, -2, index++);
// 跳过分隔符
pSrc = pLocation + 1;
}
// 把最后一部分压入table中
// eg.abc,def,cg
// 现在把cg放到结果表中
lua_pushstring(L, pSrc);
lua_rawseti(L, -2, index);
return 1;
}
把重点代码贴上来了。无需多解释,慢慢看,能看懂的。Lua测试代码如下:
require "split"
local str = "abc,de,fg"
local strsep = ","
local tbRet = MySplit.split(str, strsep)
for _, v in pairs(tbRet) do
print(v)
end
单击这里下载完整项目代码。
为了连接字符串,Lua API提供了一个叫lua_concat的函数。它类似于Lua中的“..”操作符。不过,它可以同时连接多个字符串,调用lua_concat(L, n)连接(并弹出)栈顶的n个值,然后压入结果。此外,这个函数会将数字转换为字符串,并在需要的时候调用元方法(__tostring)。还有另外一个 有用的函数是lua_pushfstring,这个函数和C中的sprintf有点类似,它们都会根据一个格式字符串和一些额外的参数来创建一个新字符 串;但是与sprintf不同的是,无需提供这个新字符串的缓冲。Lua会动态的创建一个足够大的缓冲区来存放字符串,确保不会有缓冲溢出的问题。这个函 数会将结果字符串压入栈中,并返回一个指向它的指针,当前这个函数接受的指示符只有以下几种:
- %%,表示字符%;
- %s,表示字符串;
- %d,表示整数;
- %f,表示Lua中的数字, 即双精度浮点数;
- %c,接受一个整数,并将它格式化为一个字符,和string.char功能类似。
除了上述列出的指示符以外,它不接受任何其它选项。
如果只是连接一些字符串的话,这样简单的工作,lua_concat和lua_pushfstring就能够很简单的完成;但是,如果要连接很多字 符串的话,为了提高效率,我们可以使用辅助库,也就是lauxlib.h中定义的API函数来完成这项工作。辅助库提供了什么呢?它提供了一种缓冲机制, 包含了两个层面的缓冲:
- 在本地缓冲区中收集较小的字符串,并在本地缓冲区满了以后,将结果传递给Lua(通过lua_pushlstring);
- 使用lua_concat或其它算法来连接多次缓冲区填满后的结果。
为了更好的描述辅助库的缓冲机制,来看一段string.upper的源代码,可以去Lua源代码中的lstrlib.c文中查看。
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(uchar(s[i])));
luaL_pushresult(&b);
return 1;
}
不要惊讶,Lua的代码你可以随心所欲的阅读,伟大的开源,分享的力量。使用缓冲区分为以下几步:
- 声明一个luaL_Buffer变量;
- 使用luaL_buffinit来初始化它;
- 调用luaL_add*系列函数向缓冲区添加字符或字符串;
- 调用luaL_pushresult更新缓冲区,将最终的结果字符串留在栈顶。
在调用luaL_buffinit初始化以后,这个变量中就会保留一份状态L的副本,所以在后续调用luaL_add*系列函数时,就不用传递lua_State参数了。
通过使用这些函数,就可以使用缓冲机制,我们也不用再去关心缓冲的分配、溢出等细节了。另外,这种连接算法也非常高效。用str_upper函数处理大型的字符串也不会有什么问题。
前言
在《再说C模块的编写(1)》中主要总结了Lua调用C函数时,对数组和字符串的操作,而这篇文章将重点总结如何在C函数中保存状态。
什么叫做在C函数中保存状态?比如你现在使用Lua调用了C函数Func1,但是Func1中有一些数据在调用完以后保存下来,供以后使用。而这些 数据就是所谓的状态,也就是我们需要保存的东东。根据目前总结的所有内容,是无法做到在C函数中保存状态的。有人就会说了,Lua调用C时,把所有的需要 保存的状态都返回到Lua中,当调用下一个函数时,将需要的状态当做参数再传进去,不错,是一个办法,但是很麻烦。而这里我将总结3种比较方便,但是稍微 有点难理解的方法在C函数中保存状态。
- 方法一:注册表;
- 方法二:环境;
- 方法三:upvalue。
注册表是一个全局的table,它只能被C代码访问。通常,可以用它来保存那种需要在几个模块中共享的数据;但是,如果需要保存一个模块的私有数 据,那么应该使用环境,与Lua函数一样,每个C函数都有自己的环境table,通常情况下,一个模块内的所有函数共享同一个环境table,由此它们可 以共享数据。最后,C函数也可以拥有upvalue,upvalue是一种与特定函数相关联的Lua值。现在我就逐一开始分析,总结。Let’s go.
注册表
注册表总是位于一个“伪索引”上,这个索引值由LUA_REGISTRYINDEX定义。伪索引就像是一个栈中的索引,但它所关联的值不在栈中;所 完这句话,你想到了什么?C++中,使用new开辟空间,这个空间是在堆上开辟的,而指向这个堆的变量却是存放在栈上的。伪索引和这个意思差不多。Lua API中的大多数函数都能接受伪索引,但像lua_remove和lua_insert这种操作栈本身的函数却只能使用普通索引。
注册表是一个普通的Lua table,可以使用任何Lua值(nil除外)来索引它。例如,要获取注册表中key为“JellyThink”的值,可以这么做:
lua_getfield(L, LUA_REGISTRYINDEX, "JellyThink");
现在就出现了一个很棘手的问题,由于所有的C模块共享同一个注册表,为了避免使用冲突,必须谨慎的选择key的值,为了保证key的唯一性,避免冲突,建议使用UUID作为key值。
在注册表中,不要使用数字类型的key,因为这种key是被“引用系统”所保留的。“引用系统”是由辅助库中的一系列函数组成的,它可以在向一个table存储value时,忽略如何创建一个唯一的key。例如以下调用:
int r = luaL_ref(L, LUA_REGISTRYINDEX);
会从栈中弹出一个值,然后用一个新分配的整数key来将这个值保存到注册表中,最后返回这个整数key。这个key被称为“引用”。
你问我什么情况下使用注册表?注册表是一个全局的table,它可以在多个C模块中共享数据,在一个C模块中注册了一个Lua值的引用,在其它C模块照样可以使用这个引用。光说不练,雁过还的拔毛呢,下面就来一个实例,做以下试验:
- 准备CModule和CModule2两个C模块;
- Lua调用CModule,传入一个table,在CModule中注册,然后在CModule2中打印这个table中的值;
- Lua调用CModule,传入一个function,在CModule中注册,然后在CModule2中调用这个Lua function;
- Lua调用CModule,传入一个string值,在CModule中注册,然后在CModule2中打印这个string的值。
引用系统将nil视为一种特殊情况。为一个nil值调用luaL_ref时,并不会创建新引用,而是返回一个常量引用LUA_REFNIL。对 LUA_REFNIL使用lua_rawgeti时,会向栈中压入一个nil。下面就拿注册一个function为例子,简单分析以下,主要还是要理解代 码:
static int RegisterFunc(lua_State *L)
{
// 第一个参数是function,检查参数
luaL_checktype(L, 1, LUA_TFUNCTION);
// 复制一份到栈顶
lua_pushvalue(L, 1);
lua_pushvalue(L, 1);
// 将这个function注册到注册表中
int iRef = luaL_ref(L, LUA_REGISTRYINDEX);
// 再用key来建立索引,key是JellyThink_Function
lua_setfield(L, LUA_REGISTRYINDEX, "JellyThink_Func");
// 将这个iRef压入栈返回,在CModule2中根据这个ref引用得到对应的function
lua_pushinteger(L, iRef);
// 返回一个参数
return 1;
}
Lua代码传递一个function变量进来,首先检查参数是否正确(貌似这个是通用做法);然后复制两份(为什么?传递进来的变量,在函数中一般都要做一个拷贝,不要修改传递进来的参数,当然了,out类型参数另说。)。接下来,看代码中的注释就能搞定了。单击这里下载代码CModule.zip。
环境
从5.1开始,在Lua中注册的所有C函数都有自己的环境table。一个函数可以像访问注册表那样,通过一个伪索引来访问它的环境table。环境table的伪索引是LUA_ENVIRONINDEX。
这种使用环境的方法与在Lua模块中使用环境的方法差不多,都是先为模块创建一个新的table,然后使模块中的所有函数都共享这个table。只 不过,在Lua中使用了一个setfenv函数,而在C模块中,只不过是设置table为LUA_ENVIRONINDEX。下面就来看一段环境的代码。
int luaopen_EnvironIndexDemo(lua_State *L)
{
lua_newtable(L);
lua_replace(L, LUA_ENVIRONINDEX);
luaL_register(L, "CModule", arrayFunc);
return 1;
}
这个注册函数比以前写的注册函数要多两行代码,先要创建一个新的table,然后调用lua_replace将新的table作环境table。然后调用luaL_register时,所有新建的函数都会继承当前环境。
static int SetValue(lua_State *L)
{
// Lua传递的值,先检查参数
luaL_checkinteger(L, 1);
lua_pushvalue(L, 1);
lua_setfield(L, LUA_ENVIRONINDEX, "JellyThink");
return 0;
}
// 从环境中取出对应的值
static int GetValue(lua_State *L)
{
lua_getfield(L, LUA_ENVIRONINDEX, "JellyThink");
return 1;
}
上面先将值设置到模块环境table中。然后再从中取出来。这个和上面说的注册表有很多的相似之处。尽管可能使用环境来代替注册表,但是如果没有在 不同模块之间共享数据的需要,就尽可能的不要使用注册表;使用环境创建的引用,只是在本模块中可见,这样缩小了数据的使用范围了,减小了数据被错改的可 能,增加了数据的安全性。可以单击这里下载完整代码EnvironIndexDemo.zip。
upvalue
注册表提供了全局变量的存储,环境提供了模块变量的存储,而upvalue机制则实现了一种类似于C语言中静态变量的机制。对upvalue不熟悉的伙计,可以看看《Lua中的闭包》 这篇文章。而这种upvalue机制,可以让我们定义一个只在特定的函数中可见的变量。每当在Lua中创建一个函数时,都可以将任意数量的upvalue 与这个函数相关联。每个upvalue都可以保存一个Lua值。以后,在调用这个函数时,就可以通过伪索引来访问这些upvalue了。
将这种C函数与upvalue的关联称为closure(也叫闭包,多么熟悉的名字)。一个C closure类似于Lua closure。closure可以用同一个函数代码来创建多个closure,每个closure可以拥有不同的upvalue。接下来,来一个简单的 实例,上代码:
static int count(lua_State *L)
{
int iValue = lua_tointeger(L, lua_upvalueindex(1));
lua_pushinteger(L, ++iValue);
lua_pushvalue(L, -1);
lua_replace(L, lua_upvalueindex(1));
return 1;
}
static int newCount(lua_State *L)
{
lua_pushcclosure(L, &count, 1);
return 1;
}
以上是部分重要代码,在Lua中,调用newCount,就能得到一个闭包函数count,在Lua中,就可以像使用普通变量一样来使用这个 count函数了,在Lua中每次调用count函数,它每次会从upvalue中得到之前保存的upvalue,返回给Lua。单击这里下载完整工程Upvalue1。

浙公网安备 33010602011771号