pil.29lua中调用c函数

7k 词

我们在说Lua调用C函数的时候,不是说Lua可以调用所有的C函数,我们必须在传递参数和获得结果之间遵从一些协议。同时,必须要注册C函数,也就是说,要以合适的方式给Lua这个函数的地址。

我们先来看一个简单的函数:

static int (lua_State *L) {
double d = lua_tonumber(L, 1);
lua_pushnumber(L, sin(d));
return 1;
}

从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);
lua_setglobal(L, "mysin");

第一行压入一个函数类型的值;第二行把这个值赋给全局变量mysin。在这些修改后,我们就可以在Lua脚本中使用mysin这个函数了,我们在后面再讨论一些链接C函数到Lua的比较好的方式。我们这里先看一下怎么写C函数。

对一个更专业sin函数,必须检查参数的类型,lua辅助库可以帮我们完全这个工作。luaL_checknumber检查是不是给了一个数值参数:一旦出错,就会给出一个错误提示信息;不然就返回这个数值。修改后代码应该如下:

    static int  (lua_State *L) {
double d = luaL_checknumber(L, 1);
lua_pushnumber(L, sin(d));
return 1;
}

在上面的定义后,我们如果调用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"}。代码如下:



#include <errno.h>
#include <string.h>
#include "lua.h"
#include "lauxlib.h"
static int l_dir (lua_State *L) {
DIR *dir;
struct dirent *entry;
int i;
const char *path = luaL_checkstring(L, 1);
/* open directory */
dir = opendir(path);
if (dir == NULL) { /* error opening the directory? */
lua_pushnil(L); /* return nil... */
lua_pushstring(L, strerror(errno)); /* and error message */
return 2;
}
/* create result table */
lua_newtable(L);
i = 1;
while ((entry = readdir(dir)) != NULL) { /* for each entry */
lua_pushinteger(L, i++); /* push key */
lua_pushstring(L, entry->d_name); /* push value */
lua_settable(L, -3); /* table[i] = entry name */
}
closedir(dir);
return 1; /* table is already on top */
}

此函数通过 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()
print(pcall(coroutine.yield))
end)
co
--> false atttemp to yield across metamethod/C-call boundary

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) {
int status;
luaL_checkany(L, 1); /* at least one parameter */
status = lua_pcall(L, lua_gettop(L) - 1, LUA_MULTRET, 0);
lua_pushboolean(L, (status == LUA_OK)); /* status */
lua_insert(L, 1); /* status is first result */
return lua_gettop(L); /* return status + all results */

如果通过lua_pcall调用的函数让出时间片,想要恢复luaB_pcall是不可能的。因此,无论合适,只要在一个受保护的调用中让出时间片,解释器会抛出一个错误。Lua 5.3实现pcall框架上和下面相似:


static int finishpcall (lua_State *L, int status, intptr_t ctx) {
(void)ctx; /* unused parameter */
status = (status != LUA_OK && status != LUA_YIELD);
lua_pushboolean(L, (status == 0)); /* status */
lua_insert(L, 1); /* status is first result */
return lua_gettop(L); /* return status + all results */
}
static int luaB_pcall (lua_State *L) {
int status;
luaL_checkany(L, 1);
status = lua_pcallk(L, lua_gettop(L) - 1, LUA_MULTRET, 0,
0, finishpcall);
return finishpcall(L, status, 0);
}

这和Lua 5.1有三个重要的不同:

  1. lua_pcallk替换了lua_pcall
  2. 将所有在调用后要做的事情放在一个复制函数finishcall中。
  3. lua_callk返回的状态可能是:LUA_YIELD, LUA_OK,或者一个错误。

如果在调用中没有让出时间片的情况,lua_pcallklua_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。当没有错误时,statusLUA_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) {
as before
}

然后,我们定义一个数组:数组包含模块内的所有函数和他们期待的名字。数组类型luaL_Reg,包含两个字段的结构:函数名(字符串),函数指针。

static const struct luaL_Reg mylib [] = {
{"dir", l_dir},
{NULL, NULL} /* sentinel */
};

在我们的函数中,只有一个函数l_dir需要声明。数组的最后一对始终是{NULL, NULL},用来表示结束。

最后,我们定义一个主函数,使用luaL_newlib

int luaopen_mylib (lua_State *L) {
luaL_newlib(L, mylib);
return 1;
}

调用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

#include <errno.h>
#include <string.h>
#include "lua.h"
#include "lauxlib.h"

static int l_dir (lua_State *L) {
DIR *dir;
struct dirent *entry;
int i;
const char *path = luaL_checkstring(L, 1);
/* open directory */
dir = opendir(path);
if (dir == NULL) { /* error opening the directory? */
lua_pushnil(L); /* return nil... */
lua_pushstring(L, strerror(errno)); /* and error message */
return 2;
}
/* create result table */
lua_newtable(L);
i = 1;
while ((entry = readdir(dir)) != NULL) { /* for each entry */
lua_pushinteger(L, i++); /* push key */
lua_pushstring(L, entry->d_name); /* push value */
lua_settable(L, -3); /* table[i] = entry name */
}
closedir(dir);
return 1; /* table is already on top */
}

static const struct luaL_Reg mylib [] = {
{"dir", l_dir},
{NULL, NULL}
};

int luaopen_mylib (lua_State *L) {
luaL_newlib(L, mylib);
return 1;
}

把上面代码保存到一个mylib.c文件内。
然后我们的运行环境是macOS,和Linux编译代码有所不同:

gcc -fPIC -o mylib.o -c mylib.c
gcc -O2 -bundle -undefined dynamic_lookup -o mylib.so mylib.o

我们可以写一个lua脚本t.lua

local mylib = require "mylib"

local t = mylib.dir(".")
for k, v in pairs(t) do
print(k, v)
end

然后,用lua t.lua,看一下输出:

1	.
2 ..
3 mylib.c
4 mylib.o
5 mylib.so
6 t.lua