0%

《lua 程序设计》读书笔记(14):在 Lua 中调用 C 语言 & 编写 C 函数的技巧

上一篇文章主要讲解如何在 C 代码中调用 Lua 代码,即将 C 代码作为应用程序代码,Lua 代码作为库代码。这篇文章将介绍如何在 Lua 代码中调用 C 语言,即将 Lua 代码作为应用代码,将 C 代码作为库代码。

在 Lua 中调用 C 语言

Lua 可以调用 C 语言函数,但是并不意味着 Lua 可以调用所有的 C 函数。当 Lua 调用 C 函数时,也使用了一个与 C 调用 Lua 函数时相同类型的栈,C 函数从栈中获取参数,并将结果压入栈中。每个函数都有其私有的局部栈,当 Lua 调用一个 C 函数时,第一个参数总是位于这个局部栈中索引为 1 的位置,依次类推。

另外,当 Lua 调用 C 函数时,必须注册该函数,即必须以一种恰当的方式来为 Lua 提供该 C 函数的地址。

C 函数

如下是一个可供 Lua 调用的 C 函数:

1
2
3
4
5
static int l_sin(lua_State *L) {
double d = lua_tonumber(L, 1);
lua_pushnumber(L, sin(d));
return 1;
}

所有在 Lua 中注册的函数都必须使用一个相同的原型,该原型就是定义在 lua.h 中的 lua_CFunciton

1
typedef int (*lua_CFunction)(lua_State *L);

该函数的返回值表示压入栈中的返回值的个数。该函数在压入结果前无需清空栈。在该函数返回后,Lua 会自动保存值并清空整个栈。

在 Lua 中,要调用这个函数之前,还需要通过 lua_pushcfunction 注册该函数。lua_pushcfunction 会获取一个指向 C 函数的指针,然后在 Lua 中创建一个 function 类型,表示待注册的函数。注册完成后,C 函数就可以像其他 Lua 函数一样被 Lua 调用了。

如下代码将 l_sin 添加到 lua 解释器中:首先将函数类型的值压入栈中,之后将该值赋值给全局变量 mysin。

1
2
lua_pushcfunction(L, l_sin);
lua_setglobal(L, "mysin");

要编写一个更专业的正弦函数,首先必须检查其参数的类型,Lua 辅助库可以帮我们完成这个任务。luaL_checknumber 可以检查指定的参数是否为一个数字。

如下实现了读取目录的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <dirent.h>
#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);

dir = opendir(path);
if (dir == NULL) {
lua_pushnil(L);
lua_pushstring(L, strerror(errno));
return 2;
}

lua_newtable(L);
i = 1;
while ((entry = readdir(dir)) != NULL) {
lua_pushinteger(L, i++);
lua_pushstring(L, entry->d_name);
lua_settable(L, -3);
}

closedir(dir);
return 1;
}

注意,由于 l_dir 是在 lua 中被调用的,因此上述代码中所以一 lua_ 开头的函数都是在向 Lua 返回值。

延续

由于 Lua 的 CAPI 提供了 lua_pcalllua_call,因此一个被 Lua 调用的 C 函数也可以通过这些 API 回调 Lua 函数。通常 Lua 是可以处理这种调用顺序的,毕竟与 C 的集成是一大特点。但是有一种情况这种调用会有问题,就是协程。此时调用顺序为:宿主程序(C)-> 脚本代码(Lua) -> C 库接口(C)-> Lua 函数(Lua)。

一般 Lua 是可以处理这种调用顺序的。Lua 中每个协程都有自己的栈,其中保存了该协程所挂起调用的信息,具体来说就是栈中存储了每一个调用的返回地址、参数和局部变量。对于 Lua 的调用,解释器只需要这个栈即可,我们称其为软栈。然而对于 C 的调用,解释器必须使用 C 语言栈,因为 C 函数的返回地址和局部变量都位于 C 语言栈中。

对于解释器来说,拥有多个软栈并不难,但是 ISO C 的运行时环境却只能拥有一个内部栈。因此 Lua 中的协程并不能挂起 C 函数的执行,如果一个 C 函数位于从 resume 到对应 yield 的调用路径中,那么 Lua 无法保存 C 函数的状态以便于在下次 resume 时恢复状态

Lua 5.2 及后续版本中,用延续(continuation)改善了对这个问题的处理。Lua 5.2 使用长跳转实现了 yield,并用相同的方式实现了错误处理。长跳转简单丢弃 C 语言栈中关于 C 函数的所有信息,因而无法 resume 这些函数。但是一个 C 函数 foo 可以指定一个延续函数 foo_k。当解释器发现它应该恢复 foo 执行时,如果长跳转(long jump)已经丢弃了 C 语言栈中有关 foo 信息,则调用 foo_k 来替代。

lua_pcalllklua_pcall 类似,但是支持被调用的函数中途 yield。lua_pcalllk 支持设置一个延续函数,当解释器发现需要 resume 时,它会调用延续函数。

延续体系是一种为了支持 yield 而设计的精巧机制,但它也不是万能的,某些 C 函数可能会需要给他们的延续传递相当多的上下文。

C 模块

Lua 模块就是一个代码段,其中定义了一些函数并将其存储在恰当的地方(通常是表中的元素)。为 Lua 编写的 C 语言模块可以模仿这种行为。除了 C 函数定义外,C 模块还必须定义一个特殊的函数,该特殊函数相当于 Lua 库中的主代码段,用于注册模块中的所有 C 函数,并将其存储在恰当的地方(通常也是表中)。其他初始化工作也应该在函数中完成。

Lua 通过注册过程感知 C 函数,一旦一个 C 函数用 Lua 表示和存储,Lua 就会通过对其地址的直接引用来调用它。通常一个 C 模块中只有一个用于打开库的公共函数,其他函数都是私有的,在 C 中被声明为 static。

luaL_newlib 接收一个由 C 函数及其对应函数名组成的数组,并将这些函数注册一个新的表中。如下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
static int l_dir(lua_State *L) {
......
}

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

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

编写完这个库之后,还必须将其链接到解释器。如果 Lua 解释器支持动态连接的话,那么最简单的方法就是使用动态链接机制。此时必须将代码编译成一个动态链接库(即 Linux 下的 .so 文件),并将这个库放到 C 语言路径中的某个地方。完成了以上步骤之后,就可以使用 require 直接在 Lua 中加载这个模块了:

1
local mylib = require "mylib"

此时动态库 mylib 链接到 Lua 中,并查找函数 luaopen_mylib,将其注册为一个 C 语言函数,然后调用它以打开模块。动态链接器总是以 luaopen_ + 模块名 这样的函数

如果解释器不支持动态链接器,就必须连同新库一起重新编译 Lua 语言。而且需要告诉独立解释器,它应该在打开一个新的 lua_State 时打开这个库,一种简单的做法是把 luaopen_mylib 添加到由 luaL_openlibs 打开的标准库列表(该列表位于 linit.c 中)。

编写 C 函数的技巧

官方 API 和辅助库都提供了一些机制用来帮助用户编写 C 函数。

数组操作

Lua 中的数组就是以特殊方式使用的表。lua_settablelua_gettable 这种用来操作表的通用函数,也可以用来操作数组。但是 CAPI 也为使用整数索引的表访问/更新提供了专门的函数:

1
2
void lua_geti(lua_State *L, int index, int key);
void lua_seti(lua_State *L, int index, int key);

如下实现了一个 map 函数,该函数对数组中的所有元素调用一个指定的函数,然后用该函数返回的结果替换掉对应的数组元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int l_map(lua_State *L) {
int i, n;

luaL_checktype(L, 1, LUA_TTABLE);
luaL_checktype(L, 2, LUA_TFUNCTION);
n = luaL_len(L, 1);

for (i = 1; i <= n; i++) {
lua_pushvalue(L, 2);
lua_geti(L, 1, i);
lua_call(L, 1, 1);
lua_set(L, 1, i);
}

return 0;
}
  • luaL_checktype 用于确保指定的参数具有指定的类型
  • lua_call 做的是不受保护的调用,该函数类似于 lua_pcall,但是发生错误时 lua_call 会传播错误而不是返回错误码。在应用中编写代码主函数时,一般不用 lua_call 因为我们需要捕获所有的错误。但是编写函数时使用 lua_call 是合适的。如果发生错误,由关心错误的人去处理

字符串操作

当 C 函数接受到一个 Lua 字符串作为参数时,必须遵守规则:在使用字符串期间不能从栈中将其弹出,而且不应该修改字符串。当 C 函数需要创建一个返回给 Lua 的字符串时,由 C 语言负责缓冲区分配、释放等任务。

LUA API 提供了一些函数来帮助完成这些任务:

  • lua_pushlstring 将字符串中指定长度的子串压入栈中
  • lua_concat 用于连接字符串,它可以将数字转换为字符串,lua_concat(L, n) 会连接栈最顶端的 n 个值,并将结果压入栈
  • lua_pushfstring 类似于 C 语言的 sprintf,它会根据格式字符串和额外的参数来创建字符串。不管字符串多大,Lua 都会动态为我们创建,lua_pushfstring 会将结果字符串压入栈中并返回一个指向它的指针

如下函数根据指定的分隔符(单个字符)来分割字符串,并返回一张包含子串的表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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);

while ((e = strchr(s, *sep)) != NULL) {
lua_pushlstring(L, s, e - s);
lua_rawseti(L, -2, i++);
s = e + 1;
}

lua_pushlstring(L, s);
lua_rawseti(L, -2, i++);

return 1;
}

当连接多个字符串时,逐个连接效率较低。此时我们可以使用辅助库提供的缓冲机制。缓冲机制的简单用法只包含两个函数:

  • 一个用于在组装字符串时提供任意大小的缓冲区
  • 另一个用于将缓冲区的内容转换为一个 Lua 字符串

如下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
static int str_upper(lua_State *L) {
size_t l;
size_t i;
luaL_Buffer b;
const char *s = luaL_checklstring(L, 1, &l);
char *p = luaL_buffinitsize(L, &b, l):
for (i = 0; i < l; i++) {
p[i] = toupper(uchar(s[i]));
}
luaL_pushresultsize(&b, l);
return 1;
}
  • 首先声明一个 luaL_Buffer 类型的变量,然后调用 luaL_buffinitsize 获取一个指向指定大小缓冲区的指针
  • luaL_pushresultsize 将缓冲区的内容转换为一个新的 Lua 字符串,并将该字符串压栈

如果不知道返回结果大小的上限值,可以通过逐步增加内容的方式来使用辅助缓冲区。辅助库提供了一些用于向缓冲区中增加内容的函数,这里函数包括 luaL_addvalueluaL_addlstring 等。

如下示例对 table 中的元素进行连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static int tconcat(lua_State *L) {
luaL_buffer b;
int i, n;

luaL_checktype(L, 1, LUA_TTABLE);
n = luaL_len(L, 1)

luaL_buffinit(L, &b);
for (i = 1; i <= n; i++) {
lua_geti(L, 1, i);
luaL_addvalue(b)
}

luaL_pushresult(&b);
return 1;
}

需要注意,在初始化一个缓冲区后,Lua 栈中可能会保留某些内部数据,我们不能假设在使用缓冲区之前栈顶仍然停留在最初的位置。

在 C 函数中保存状态

如果想在 C 函数中保存一些非局部数据,即生存时间超过 C 函数执行时间的数据,我们通常使用全局变量或者静态变量来实现。但是当为 Lua 编写库函数时,这并不是好办法。首先我们无法在一个 C 变量中保存普通的 Lua 值,另外,使用这类变量的库也无法用于多个 Lua 状态

Lua 函数有两个地方地方用于存储非局部数据,即全局变量和非局部变量,而 CAPI 也提供了两个类似的地方来存储非局部数据:注册表和 upvalue。

注册表是一张能够被 C 代码访问的全局表。通常使用注册表来存储多个模块间共享的数据。注册表总是位于伪索引 LUA_REGISTRYINDEX 中,例如要获取注册表中键为 key 的值,可以使用 lua_getfield(L, LUA_REGISTRYINDEX, "key")

在注册表中不能使用数值类型的键,因为 Lua 将其用作引用系统的保留字。引用系统由辅助库中的一对函数组成,有了这两个函数,我们可以在表中存储值时不用担心如何创建唯一键。

1
int ref = luaL_ref(L, LUA_REGISTRYINDEX);

该调用会从栈中弹出一个 value,然后分配一个新的整型键,使用这个键将从栈中弹出的值保存到注册表中,最后返回该整型键,而这个键就被称为引用。要将与引用 ref 关联的值压入栈中,只需要:

1
lua_rawgeti(L, LUA_REGISTRYINDEX, ref);

要释放值和引用,则可以调用 luaL_unref(L, LUA_REGISTRYINDEX, ref)

引用系统将 nil 视为一种特殊情况:

  • 为一个 nil 值调用 luaL_ref 不会创建新的引用,而是返回一个常量引用 LUA_REFNIL
  • lua_rawgeti(L, LUA_REGISTRYINDEX, LUA_REFNIL) 向栈中压入一个 nil

常量 LUA_NOREF 可以用于表示无效引用。创建 Lua 状态时,注册表有两个预定义的引用:

  • LUA_RIDX_MAINTHREAD:指向 Lua 状态本身,即主线程
  • LUA_RIDX_GLOBALS:指向全局变量

另一种在注册表中创建唯一键的方法是,使用代码中的静态变量的地址。C 的链接器会确保键在所有已加载的库中的唯一性。要使用这种方法,需要用到 lua_pushuserdata,该函数会在栈中压入一个表示 C 指针的值。如下是一个示例:

1
2
3
4
5
6
7
8
9
static char key = 'k';

lua_pushlightuserdata(L, (void *)&key);
lua_pushstring(L, myStr);
lua_settable(L, LUA_REGISTRYINDEX); // registry[&key] = myStr

lua_pushlightuserdata(L, (void *)&key);
lua_gettable(L, LUA_REGISTRYINDEX);
myStr = lua_tostring(L, -1);

为了简化将变量地址用作唯一键的用法,Lua5.2 提供了 lua_rawgetplua_rawsetp,它们使用 C 语言指针(转换为轻量级用户数据)作为 key,上述代码可以重写为:

1
2
3
4
5
6
7
static char key = 'k';

lua_pushstring(L, myStr);
lua_rawsetp(L, LUA_REGISTRYINDEX, (void *)&key); // registry[&key] = myStr

lua_rawgetp(L, LUA_REGISTRYINDEX, (void *)&key);
myStr = lua_tostring(L, -1);

注册表提供了全局变量,而 upvalue 则实现了一种类似于 C 语言静态变量(只在特定函数中可见)的机制。每一次在 Lua 中创建新的 C 函数时,都可以将任意数量的 upvalue 与这个函数相关联,每一个 upvalue 都可以保存一个 Lua 值。后面在调用该函数时,可以通过伪索引来自由地访问这些 upvalue。我们可以将这种 C 函数与其 upvalue 的关联称为闭包。C 语言的闭包类似于 Lua 闭包。可以使用相同的函数代码来创建不同的闭包,每个闭包可以拥有不同的 upvalue。

1
2
3
4
5
6
7
static int counter(lua_State *L);

int newCounter(lua_State *L) {
lua_pushinteger(L, 0);
lua_pushcclosure(L, &counter, 1);
return 1;
}

lua_pushcclosure 会创建一个新的闭包,它的第二个参数是一个基础函数,第三个参数是 upvalue 的数量。在创建新的闭包前,需要将 upvalue 的初始值压入栈中。lua_pushcclosure 会将一个新的闭包留在栈中,并将其作为 newCounter 的返回值。

counter 的定义如下,其中 lua_upvalueindex 用于生成 upvalue 的伪索引:

1
2
3
4
5
6
static int counter(lua_State *L) {
int val = lua_tointeger(L, lua_upvalueindex(1));
lua_pushinteger(L, ++val);
lua_copy(L, -1, lua_upvalueindex(1));
return 1;
}

如下使用 upvalue 实现了元组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int t_tuple(lua_State *L) {
lua_Integer op = luaL_optinteger(L, 1, 0);
if (op == 0) {
int i;

for (i = 1; !lua_isnone(L, lua_upvalueindex(i)); i++) {
lua_pushvalue(L, lua_upvalueindex(i));
}

return i - 1;
} else {
luaL_argcheck(L, 0 < op && op < 256, 1, "index out of range");
if (lua_isnone(L, lua_upvalueindex(op))) {
return 0;
}

lua_pushvalue(L, lua_upvalueindex(op));
return 1;
}
}


int t_new(lua_State *L) {
int top = lua_gettop(L);
luaL_argcheck(L, top < 256, "too many fields");
lua_pushcclosure(L, t_tuple, top);
return 1;
}


static const struct luaL_Reg tuplelib [] = {
{"new", t_new},
{NULL, NULL}
};


int luaopen_tuple(lua_State *L) {
luaL_newlib(L, tuplelib);
return 1;
}

这段代码要点如下:

  • luaL_optinteger 可以获取可选参数,当参数不存在时,返回指定的默认值
  • lua_argcheck 可以检查参数的有效性,它可以检查给定的条件,如果条件不符合,则会引发错误并返回一条错误信息
  • lua_isnone 用于测试指定的 upvalue 是否存在

我们经常需要在同一个库的所有函数之间共享某些值或变量,虽然可以使用注册表来完成该任务,但也可以使用 upvalue。但是与 Lua 的闭包不同,C 的闭包不能共享 upvalue,每个闭包都有独立的 upvalue。但是可以设置不同函数的 upvalue 指向同一张表,这张表就称为一个共同的环境。

当我们使用 luaL_newlib 打开 C 语言库时,它会调用 luaL_newlibtable

1
2
#define luaL_newlib(L, lib) \
(luaL_newlibtable(L, lib), luaL_setfuncs(L, lib, 0))

luaL_newlibtable 只是为库创建一个新表(该表预先分配的大小等同于指定库中函数的数量),然后 luaL_setfuncs 将列表 lib 中的函数添加到位于栈顶的新表中,该函数的第三个参数指定了库中新函数共享的 upvalue 值个数。

因此如果要创建一个库,这个库中的所有函数共享一张表作为它的 upvalue,可以使用如下代码:

1
2
3
luaL_newlibtable(L, lib);
lua_newtable(L);
luaL_setfuncs(L, lib, 1);