pil.29lua中调用c函数
我们在说Lua调用C函数的时候,不是说Lua可以调用所有的C函数,我们必须在传递参数和获得结果之间遵从一些协议。同时,必须要注册C函数,也就是说,要以合适的方式给Lua这个函数的地址。
我们先来看一个简单的函数:
static int (lua_State *L) { |
从C的位置来看,这个函数从Lua state获取一个参数,然后把结果压入Lua state。因此,函数在压入结果前不需要清理栈。在函数返回后,Lua会自动的保存结果然后清理C函数的栈。
在我们可以在Lua中用这个函数前,我们必须先注册。我们使用lua_pushcfunction
来实现:获取这个C函数的地址,在Lua中建立一个function
的值来保存这个地址。一旦注册后,C函数就跟其他Lua内的函数一样了。
一个快速但是很不简洁的方法是在官方的lua解释器代码lua.c
中放入 l_sin
的代码,然后在调用了luaL_openlibs
函数后加入下面的两行:
lua_pushcfunction(L, l_sin); |
第一行压入一个函数类型的值;第二行把这个值赋给全局变量mysin。在这些修改后,我们就可以在Lua脚本中使用mysin
这个函数了,我们在后面再讨论一些链接C函数到Lua的比较好的方式。我们这里先看一下怎么写C函数。
对一个更专业sin函数,必须检查参数的类型,lua辅助库可以帮我们完全这个工作。luaL_checknumber
检查是不是给了一个数值参数:一旦出错,就会给出一个错误提示信息;不然就返回这个数值。修改后代码应该如下:
static int (lua_State *L) { |
在上面的定义后,我们如果调用mysin('a')
,就会得到如下的错误:
bad argument #1 to 'mysin' (number expected, got string)
作为一个更复杂的例子,我们来写一个返回指定目录内容的函数。Lua内在标准库内没有提供这个函数,ISO C不提供这样的操作。我们假设我们的系统兼容 POSIX。我们的函数————我们会在Lua把它叫做dir,在C中叫l_dir
————获取一个字符串的路径参数,然后返回所有的目录项。具体来说,dir("/home/lua")
会返回一个表{".", "..", "src", "bin", "lib"}
。代码如下:
|
此函数通过 luaL_checkstring
来检查参数是否为一个字符串。然后通过系统调用opendir
来打开目录。如果无法打开目录,就会返回一个nil
与错误信息。在打开目录后,会创建一个表,然后把目录项都放在里面。最后,关闭目录,返回值1,这在Lua中表示到达了栈的顶部。(lua_settable
会从栈中弹出 键和值。因此,在循环后,在栈顶的元素就是返回的表)
接续函数
通过lua_pcall, lua_call
,一个在Lua调用的C函数,依然可以调用Lua。某些标准库函数就会这样做:table.sort
可以调用一个排序函数;string.gsub
可以调用一个替换函数;pcall, xpcall
可以在保护模式下调用函数。如果我们记住,Lua的 main函数代码也是从C(宿主程序)调用的,我们的调用流程就跟这样的:C(宿主)调用Lua(脚本),Lua(脚本)调用C(库函数),Lua库函数调用Lua(回调)。
通常,Lua这样做是没有什么问题的;与C的集合还是Lua语言的一个特色。然后,也有某些情况下这样的交互会导致一些困难:比如协程。
Lua中的每个协程都有自己的栈,其中保留了这个协程所有挂起的调用信息。特别地,栈内保存了返回地址,参数,以及每个调用的本地变量。对于调用Lua函数,解释器只需要这个栈,我们叫做soft stack
。然而,对于调用C函数,解释器必须使用C栈。毕竟,C函数中的返回地址和本地变量是存在与C栈中的。
让解释器拥有多个soft stack
是非常容易的,但是ISO C运行时只有一个内部的栈。因此,Lua协程不能挂起一个C函数的执行:如果一个C函数想要在协程内恢复到其让出时间片的地方,Lua不能C函数的状态来让其恢复。试着看一下下面的代码:Lua 5.1
co = coroutine.wrap(function() |
pcall
是一个C函数;所以Lua 5.1不能挂起它,因为ISO C没有一个可以挂起C函数然后恢复运行的方式。
Lua 5.2和后续的版本通过continuations来减轻这样的困难。Lua通过 long jumps 来实现 yields(让出时间片),这和实现错误是一样的。一个 long jump只是简单的丢C栈中的C函数信息,所以这是不可能恢复运行这个函数的。然而,一个C函数foo
可以指定一个连续函数foo_k
,这个函数用来在恢复foo
的时候进行执行。这就是说,如果解释器检查到要恢复执行foo
,但是一个long jump已经丢弃了其在栈中的信息,它就会去调用foo_k
。
为了让事情变得更具体一点,我们看一下pcall
的实现例子。在Lua 5.1中,其代码如下:
static int luaB_pcall (luaState *L) { |
如果通过lua_pcall
调用的函数让出时间片,想要恢复luaB_pcall
是不可能的。因此,无论合适,只要在一个受保护的调用中让出时间片,解释器会抛出一个错误。Lua 5.3实现pcall
框架上和下面相似:
|
这和Lua 5.1有三个重要的不同:
- 用
lua_pcallk
替换了lua_pcall
。 - 将所有在调用后要做的事情放在一个复制函数
finishcall
中。 lua_callk
返回的状态可能是:LUA_YIELD, LUA_OK,或者一个错误。
如果在调用中没有让出时间片的情况,lua_pcallk
与lua_pcall
工作起来是一样的。然后,在有让出时间片的情况时,情况就变得非常不同。如果被lua_pcall
调用的函数试出让出时间片,Lua会抛出一个错误。但是当lua_pcallk
调用的函数要这样做时,这将没有错误:Lua进行一个long jump,然后丢弃所有C栈中luaB_pcall
的信息,但是在协程soft stack
中保留了一个到continuation function(接续函数)
的引用(我们的例子中是finishpcall
)。后续在解释器检查到要继续执行luaB_pcall
的时候,就会去调用这个接续函数。
在发生错误的时候也可以调用finishpcall
。和原始的luaB_pcall
不一样,finishpcall
不能获得lua_pcallk
返回的值。所以,其通过一个额外的参数来获得这个值,status。当没有错误时,status是LUA_YIELD而不是LUA_OK,这样接续函数就知道它是被怎么样调用的。如果出现了错误,status就是原始的错误代码。
和调用返回的状态一起,接续函数也接收一个context,上下文.lua_pcallk
的第五个参数是一个专门的整数,将会被传递为接续函数的最后一个参数。(参数的类型,intptr_t
,允许指针传递)这个值允许原始的函数传输一些专门的信息到接续函数。(我们的例子没有用这个特性)
Lua 5.3的接续系统是一个非常机灵的做法,但这不是万能的。某些C函数需要传递很多的上下文给他们的接续函数。比如table.sort
,使用C栈来进行递归;string.gsub
,必须保持一个快照和缓存来给部分结果使用。尽管可以写一个yieldable的函数来替换,但这似乎并不值得增加复杂性和性能的降低。
模块
一个Lua模块就是一个定义了一些Lua函数并且存储到一个合适地方的chunk(大块代码)
,典型例子是表的条目。Lua的C模块模拟了这种行为。在C函数的定义中,也不许定义一个在Lua库中扮演 main chunk的函数。这个函数应该注册模块中的所有C函数和存储到一个合适的地方。和Lua main chunk相似,这函数也会初始化所有需要初始化的东西。
Lua通过这个注册过程来了解C函数。一旦一个C函数在Lua中存储并表示出来,Lua通过直接也不应该其地址来调用它,这地址是在我们注册的时候给到Lua的。换句话说,Lua不依赖一个函数名,包位置或可见性规则。典型地,一个C模块只有一个 公共(外部)函数,也就是打开这个库的函数。所有其他函数都可以是私有的,在C中用static
声明。
当我们用C函数扩展LUa时,像C模块一样设计我们的代码是非常棒的,即使我们只想注册一个C函数。通常,辅助库提供了一个帮助函数来完成这个任务。宏luaL_newlib
把C函数和他们期待的名字放在数组内,然后注册到一个新表中。举个例子,我们想建个库,函数就是我们先前定义的l_dir
。
首先,我们必须定义库函数:
static int l_dir (lua_State *L) { |
然后,我们定义一个数组:数组包含模块内的所有函数和他们期待的名字。数组类型luaL_Reg
,包含两个字段的结构:函数名(字符串),函数指针。
static const struct luaL_Reg mylib [] = { |
在我们的函数中,只有一个函数l_dir
需要声明。数组的最后一对始终是{NULL, NULL}
,用来表示结束。
最后,我们定义一个主函数,使用luaL_newlib
:
int luaopen_mylib (lua_State *L) { |
调用luaL_newlib
创建一个新表,然后用mylib内的键值对进行填充。当其返回时,luaL_newlib
将保存库的表留在栈上。luaopen_mylib
返回1来向Lua返回这个表。
在完成这个库后,我们必须把它和解释器链接。最方便的就是用动态链接特性,但这要Lua解释器的支持。 这种情况下,必须先把代码建立成一个动态库(mylib.so
,Linux-like系统),然后把它放在C路径中。在这些步骤后,可以通过require
来加载代码:
local mylib = require "mylib" |
这个调用让mylib
动态库与Lua相链接,先找到luaopen_mylib
函数,以一个C函数注册,然后调用它打开模块。(这个行为就解释了为什么luaopen_mylib
必须和其他C函数一样有类似的原型)
为了找到luaopen_mylib
,动态链接器必须知道其名字。总是会使用luaopen_
加上模块名来进行查找。因此,如果我们的库是mylib
,被调用的函数就会是luaopen_mylib
。
如果解释器不支持动态链接,必须使用新库来重新编译Lua。
实际操作
把上面的总结一下,得出我们的代码:
// mylib.c |
把上面代码保存到一个mylib.c
文件内。
然后我们的运行环境是macOS,和Linux编译代码有所不同:
gcc -fPIC -o mylib.o -c mylib.c |
我们可以写一个lua脚本t.lua
:
local mylib = require "mylib" |
然后,用lua t.lua
,看一下输出:
1 . |