我们说用Lua可以调用C语言函数,但这并不意味着Lua可以调用所有的C函数。当C语言调用Lua函数时,该函数必须遵循一个简单的规则来传递参数和获取结果。
同样,当Lua调用C函数时,这个C函数也必须遵循某种规则来获取参数和返回结果。此外,当Lua调用C函数时,我们必须注册该函数,即必须以一种恰当的方式为Lua提供该C函数的地址。
Lua调用C函数时,也使用一个与C语言调用Lua函数时相同类型的栈,C函数从栈中获取参数,并将结果压入栈中。
此处的重点在于,这个栈不是一个全局结构;每个函数都有其私有的局部栈。当Lua调用一个C函数时,第一个参数总是位于这个局部栈中索引为1的位置。即使一个C函数调用了Lua代码,而且Lua代码又再次调用了同一个C函数,这些调用每一次都只会看到本次调用自己的私有栈,其中索引为1的位置上就是一个参数。
C函数
先举一个例子,让我们实现一个简化版本的正弦函数,该函数返回某个给定数的正弦值:
1 | static int l_sin(lua_State *L){ |
所有在Lua中注册的函数都必须使用一个相同的原型,该原型就是定义在lua.h中的lua_CFunction:
1 | typedef int (*lua_CFunction)(lua_State *L); |
从C语言的角度看,这个函数只有一个指向Lua状态类型的指针作为参数,返回值为一个整型数,代表压入栈中的返回值的个数。因此,该函数在压入结果前无须清空栈。在该函数返回后,Lua会自动保存返回值并清空整个栈。
在Lua中,调用这个函数前,还必须通过lua_pushcfunction注册该函数。函数lua_pushcfunction会获取一个指向C函数的指针,然后在Lua中创建一个”function”类型,代表待注册的函数。一旦完成注册,C函数就可以像其他Lua函数一样行事了。
一种快速测试函数l_sin的方法是,将其代码放到简单解释器中,并将下列代码添加到luaL_openlibs调用的后面:
1 | lua_pushcfunction(L,l_sin); |
上述代码的第一行压入一个函数类型的值,第二行将这个值赋给全局变量mysin。完成这些修改后,我们就可以在Lua脚本中使用新函数mysin了。
要编写一个更专业的正弦函数,必须检查其参数的类型,而辅助库可以帮助我们完成这个任务。函数luaL_checknumber可以检查指定的参数是否为一个数字:如果出现错误,该函数会抛出一个告知性的错误信息;否则,返回这个数字。只需对上面这个正弦函数稍作修改:
1 | static int l_sin(lua_State *L){ |
做了上述修改后,如果调用mysin(‘a’)就会出现如下的错误:
1 | bad argument #1 to 'mysin' (number expected, got string) |
函数luaL_checknumber会自动用参数的编号(#1)、函数名(“mysin”)、期望的参数类型及实际的参数类型来填写错误信息。
下面是一个更复杂的示例,编写一个函数返回指定目录下的内容。由于ISO C中没有具备这种功能的函数,因此Lua没有在标准库中提供这样的函数。这里,我们假设使用一个POSIX兼容的操作系统。这个函数以一个目录路径字符串作为参数,返回一个列表,列出该目录下的内容。例如,调用dir(“/home/lua”)会得到形如{“.”,”..”,”src”,”bin”,”lib”}的表。该函数的完整代码如下:
一个读取目录的函数
1 |
|
该函数先使用与luaL_checknumber类似的函数luaL_checkstring检查目录路径是否为字符串,然后使用函数opendir打开目录。如果无法打开目录,该函数会返回nil以及一条用函数strerror获取的错误信息。在打开目录后,该函数会创建一张新表,然后用目录中的元素填充这张新表。最后,该函数关闭目录并返回1,在C语言中即表示该函数将其栈顶的值返回给了Lua。
在某些情况中,l_dir的这种实现可能会造成内存泄露。该函数调用的三个Lua函数均可能由于内存不足而失败。这三个函数中的任意一个执行失败都会引发错误,并中断函数l_dir的执行,进而也就无法调用closedir了。
延续
通过lua_pcall和lua_call,一个被Lua调用的C函数也可以回调Lua函数。标准库中有一些函数就是这么做的:table.sort调用了排序函数,string.gsub调用了替换函数,pcall和xpcall以保护模式来调用函数。如果你还记得Lua代码本身就是被C代码调用的,那么你应该知道调用顺序类似于:C调用Lua,Lua又调用了C,C又调用了Lua。
通常,Lua语言可以处理这种调用顺序;毕竟,与C语言的集成是Lua的一大特点。但是,有一种情况下,这种相互调用会有问题,那就是协程。
Lua语言中的每个协程都有自己的栈,其中保存了该协程所挂起调用的信息。具体地说,就是该栈中存储了每一个调用的返回地址、参数及局部变量。对于Lua函数的调用,解释器只需要这个栈即可,我们将其成为软栈。然而,对于C函数的调用,解释器必须使用C语言栈。毕竟,C函数的返回地址是局部变量都位于C语言栈中。
对于解释器来说,拥有多个软栈并不难;然而,ISO C的运行时环境却只能拥有一个内部栈。因此,Lua中的协程不能挂起C函数的执行:如果一个C函数位于从resume到对应yield的调用路径中,那么Lua无法保存C函数的状态以便在下次resume时恢复状态。请考虑如下的示例:
1 | co = coroutine.wrap(function() |
函数pcall是一个C语言函数;因此,Lua5.1不能将其挂起,因为ISO C无法挂起一个C函数并在之后恢复其运行。
在Lua5.2及后续版本中,用延续改善了对这个问题的处理。Lua5.2使用长跳转实现了yield,并使用相同的方式实现了错误信息处理。长跳转简单地丢弃了C语言栈中关于C函数的所有信息,因而无法resume这些函数。但是,一个C函数foo可以指定一个延续函数foo_k,该函数也是一个C函数,在要恢复foo的执行时它就会被调用。也就是说,当解释器发现它应该恢复函数foo的执行时,如果长调转已经丢弃了C语言栈中有关foo的信息,则调用foo_k来替代。
为了说得更具体些,我们将pcall的实现作为示例。在Lua5.1中,该函数的代码如下:
1 | static int luaB_pcall(lua_State *L){ |
如果程序正在通过lua_pcall被调用的函数yield,那么后面就不能恢复luaB_pcall的执行。因此,如果我们在保护模式的调用下试图yield时,解释器就会抛出异常。Lua5.3使用基本类似于下面示例中的方式实现了pcall。
使用延续实现pcall
1 | static int finishpcall (lua_State *L, int status, intptr_t ctx){ |
与Lua5.1中的版本相比,上述实现有三个重要的不同点:首先,新版本用lua_pcallk替换了lua_pcall;其次,新版本在调用完lua_pcallk后把完成的状态传给了新的辅助函数finishpcall;第三,lua_pcallk返回的状态除了LUA_OK或者一个错误外,还可以是LUA_YIELD。
如果没有发生yield,那么lua_pcallk的行为与lua_pcall的行为完全一样。但是,如果发生yield,情况则大不相同。如果一个被原来lua_pcall调用的函数想要yield,那么Lua5.3会像Lua5.1版本一样引发错误。但当被新的lua_pcallk调用的函数yield时,则不会出现发生错误:Lua会做一个长跳转并且丢弃C语言栈中有关luaB_pcall的元素,但是会在协程软栈中保存传递给函数lua_pcallk的延续函数的引用。后来,当解释器发现应该返回到luaB_pcall时,它就会调用延续函数。
当发生错误时,延续函数finishpcall也可能会被调用。与原来的luaB_pcall不同,finishpcall不能获取lua_pcallk所返回的值。因此,finishpcall通过额外的参数status获取这个结果。当没有错误时,status是LUA_YIELD而不是LUA_OK,因此延续函数可以检查它是如何被调用的。当发生错误时,status还是原来的错误码。
除了调用的状态,延续函数还接收一个上下文。lua_pcallk的第5个参数是一个任意的整型数,这个参数被当做延续函数的最后一个参数来传递。这个值允许原来的函数直接向延续函数传递某些任意的信息。
Lua5.3的延续体系是一种为了支持yield而设计的精巧机制,但它也不是万能的。某些C函数可能会需要它们的延续传递相当多的上下文。例如,table.sort将C语言栈用于递归,而string.gsub则必须跟踪捕获,还要跟踪和一个用于存放部分结果的缓冲区。虽然这些函数能以”yieldbale”的方式重写,但与增加的复杂性和性能损失相比,这样做似乎并不值得。
C模块
Lua模块就是一个代码段,其中定义了一些Lua函数并将其存储在恰当的地方。为Lua编写的C语言模块可以模仿这种行为。除了C函数的定义外,C模块还必须定义一个特殊的函数,这个特殊的函数相当于Lua库中的主代码段,用于注册模块中所有的C函数,并将它们存储在恰当的地方。与Lua的主代码段一样,这个函数还应该初始化模块中所有需要初始化的其他东西。
Lua通过注册过程感知到C函数。一旦一个C函数用Lua表示和存储,Lua就会通过对其地址的直接引用来调用它。换句话说,一旦一个C函数完成注册,Lua调用它时就不再依赖于其函数名、包的位置以及可见性规则。通常,一个C模块中只有一个用于打开库的公共函数;其他所有的函数都是私有的,在C语言中被声明为static。
当我们使用C函数来扩展Lua程序时,将代码设计为一个C模块是个不错的想法。因为即使我们现在只想注册一个函数,但迟早总会需要其他的函数。通常,辅助库为这项工作提供了一个辅助函数。宏luaL_newlib接收一个由C函数及其对应函数名组成的数组,并将这些函数注册到一个新表中。举个例子,假设我们要用之前定义的函数l_dir创建一个库。首先,必须定义这库函数:
1 | static int l_dir(lua_State *L){ |
然后,声明一个数组,这个数组包含了模块中所有的函数及其名称。数组元素的类型为luaL_Reg,该类型是由两个字段组成的结构体,这两个字段分别是函数名和函数指针。
1 | static const struct luaL_Reg mylib[] = { |
在上述例子中,只声明了一个函数。数组的最后一个元素永远是是{NULL,NILL},并以此标识数组的结尾。最后,我们使用函数luaL_newlib声明一个主函数:
1 | int luaopen_mylib(lua_State *L){ |
对函数luaL_newlib的调用会新创建一个表,并使用由数组mylib指定的”函数名-函数指针”填充这个新创建的表。当luaL_newlib返回时,它把这个新创建的表留在栈中,在表中它打开了这个库。然后,函数luaopen_mylib返回1,表示将这个表返回给Lua。
编写完这个库以后,我们还必须将其链接到解释器。如果Lua解释器支持动态链接的话,那么最简便的方法是使用动态链接机制。在这种情况下,必须将这个库放到C语言路径中的某个地方。在完成了这些步骤后,就可以使用require在Lua中直接加载这个模块了:
1 | local mylib = requrire "mylib" |
上述的语句会将动态库mylib链接到Lua,查找函数luaopen_mylib,将其注册为一个C语言函数,然后调用它以打开模块。
动态链接器必须知道函数luaopen_mylib的名字才能找到它。它总是寻找名为”luaopen + 模块名”这样的函数。因此,如果我们的模块名为mylib,那么该函数应该命名为luaopen_mylib。
如果解释器不支持动态链接,就必须连同新库一起重新编译Lua语言。除了重新编译,还需要以某种方式告诉独立解释器,它应该在打开一个新状态时打开这个库。一个简答的做法是把luaopen_mylib添加到由lua_openlibs打开的标住库列表中,这个列表位于文件linit.c中。