0%

《lua 程序设计》读书笔记(13):C 语言 API 总览 & 扩展应用

Lua 是一种嵌入式语言,这就意味着 Lua 并不是一个独立的应用,而是一个库,它可以链接到其他应用程序,将 Lua 功能融入这些应用。这就涉及到 Lua 提供的 C API,通过这些 API,实现在自己的应用程序中通过 Lua 来扩展应用。

C 语言 API 总览

之前我们编写的 Lua 程序都是通过 Lua 解释器运行的,Lua 解释器是一个小应用,它是用 Lua 标准库实现的独立解释器。这个解释器负责与用户的交互,将用户的文件和字符串传递给 Lua 标准库,由标准库完成主要工作。

由于能够被当做库来扩展应用程序,所以 Lua 是一种嵌入式语言。同时,使用了 Lua 的程序可以在 Lua 环境中注册新的函数,例如用 C 实现的函数,从而增加一些无法直接用 Lua 编写的功能。因此 Lua 是一种可扩展的语言。这两种对 Lua 的定位分别对应 C 和 Lua 之间两种形式

  • 第一种形式中(嵌入式语言),C 拥有控制权,而 Lua 被用作库,这种交互形式中的 C 代码被称为应用代码
  • 第二种形式中(可扩展语言),Lua 拥有控制权,而 C 被用作库,此时 C 代码被称为库代码

应用代码和库代码都使用相同的 API 和 Lua 通信,这些 API 被称为 CAPI。CAPI 是一个由函数、常量和类型组成的集合,通过它,C 代码就可以和 Lua 语言交互。CAPI 遵循 C 的操作模式。

第一个示例

如下实现了一个简单的 Lua 独立解释器:

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
#include <stdio.h>
#include <string.h>
#include "lua.h"
#include "lauxlib.h"
#include "lualib.h"

int main(void) {
char buf[256];
int error;

lua_State *L = luaL_newstate();
luaL_openlibs(L);

fprintf(stdout, "welcome to use out luac\n");
while (fgets(buf, sizeof(buf), stdin) != NULL) {
error = luaL_loadstring(L, buf) || lua_pcall(L, 0, 0, 0);
if (error) {
fprintf(stderr, "%s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
}

lua_close(L);
return 0;
}

使用如下方式编译该程序,需要链接 lua 库和数学库(顺序很重要):

1
gcc -g -o luac luac.c -llua -lm

程序运行如下:

1
2
3
4
5
# ./luac
welcome to use out luac
print("hello, world")
hello, world
^C

接下来对该程序进行说明:

  • lua.h 头文件声明了 Lua 提供的基础函数,包括 创建新 Lua 环境调用 Lua 函数读写环境中的全局变量注册供 Lua 语言调用的新函数。该头文件中声明的内容均以 lua_ 作为前缀
  • lauxlib.h 头文件声明了辅助库(auxlib)所提供的函数,所有声明都以 luaL_ 开头。辅助库使用 lua.h 提供的基础 API 来提供更高层次的抽象,特别是对标准库用到的相关机制进行抽象。基础 API 追求经济性和正交性,而辅助库则追求对常见任务的实用性

Lua 标准库没有定义任何 C 全局变量,所有的状态均保存在动态的结构体 lua_State 中。Lua 所有函数都接收一个该结构指针作为参数,这种设计使得 Lua 是可以重入的,可以直接用于编写多线程代码。

  • luaL_newstate 创建新的 Lua 状态 时,新环境中没有包含预定义的函数。头文件 lualib.h 中声明了用于打开这些库的函数,luaL_openlibs 用于打开所有标准库
  • luaL_loadstring 用于编译用户输入的每一行内容,如果没有错误,则返回 0,并向栈中压入编译后得到的函数
  • lua_pcall 从栈中弹出编译后的函数,并以保护模式运行。如果没有发生错误,该函数返回 0
  • 以上两个函数在发生错误时,都会向栈中压入一条错误信息,可以通过 lua_tostring 获取错误信息。在打印之后,使用 lua_pop 将其从栈中删除

如果将 C 作为 C 代码编译出来后,又要在 C++ 中使用,可以引入 lua.hpp 来替代 lua.h

Lua 和 C 之间的通信主要组件是无处不在的虚拟栈。几乎所有 API 调用都是在操作这个栈中的值,Lua 与 C 之间所有的数据交换都是通过这个栈来完成的,还可以通过这个栈来保存中间结果。

如果想在 Lua 和 C 之间交换数据时,会面对两个问题:

  • 问题 1:动态类型和静态类型体系之间不匹配
  • 问题 2:自动内存管理和手动内存管理之间的不匹配问题

通过栈在 Lua 和 C 之间交换数据,栈中的每个元素都能保存 Lua 中任意类型的值:

  • 当我们想从 Lua 中获取一个值,只需要调用 Lua,Lua 就会将指定值压入栈中,之后从栈中取出值即可
  • 当想要将一个值传给 Lua 时,则首先要将这个值压入栈中,然后调用 Lua,Lua 会将其从栈中弹出

尽管仍然需要一个不同的函数将每种 C 类型的值压入栈中,还需要一个不同的函数从栈中弹出每种 C 语言类型的值,但是避免了过多的组合。另外,由于栈是 Lua 状态的的一部分,因此垃圾收集器知道 C 正在使用哪些值。

针对每一种能用 C 直接表示的 Lua 数据类型,CAPI 都有一个对应的压栈函数:

  • lua_pushnil
  • lua_pushboolean
  • lua_pushnumber
  • lua_pushinteger
  • lua_pushlstring
  • lua_pushstring

无论何时向栈中压入一个元素,都应该确保栈中有足够的空间,lua_checkstack 可以用来检查栈中是否有足够的空间。辅助库也提供了高层函数来检查栈空间 luaL_checkstack。如果有可能,这两个函数会尝试增加栈的大小,以容纳所需的额外空间。

CAPI 使用索引来引用栈中的元素,第一个被压入栈中的元素索引为 1,第二个被压入的元素索引为 2,以此类推。还可以以栈顶为参照,使用负数索引来访问栈中的元素,此时 -1 表示栈顶元素(即最后被压入的元素)。

要检查栈中的一个元素是否为特定类型,CAPI 提供了一系列名为 lua_is* 的函数,其中 * 可以是任意一种任意 Lua 数据类型,这些函数包括 lua_isnilllua_isnumberlua_isstringlua_istable

lua_type 用于返回栈中元素的类型,每一种类型都由一个对应的常量表示。lua_to* 用于从栈中获取一个值。

除了上述在 C 语言和栈之间交互数据的函数之外,CAPI 还提供了下列通用栈操作的函数:

  • lua_gettop:返回栈中元素的个数,即栈顶元素的索引
  • lua_settop:将栈顶设置为一个指定的值,即修改栈中元素的数量。lua_settop(L, 0)用于清空栈
  • lua_pushvalue:用于将指定索引上的元素的副本压入栈
  • lua_rotate:将指定索引到栈顶位置之间的元素旋转 n 个位置。n 为正数,表示将元素向栈顶方向移动,n 为负数则表示向相反方向移动
  • lua_remove:删除指定索引的元素,并将该位置之上的所有元素向下移动以填补空缺
  • lua_insert:用于将栈顶元素移动到指定位置,并上移指定位置之上的所有元素以开辟出一个元素的空间
  • lua_replace:将栈顶元素移动到指定索引上(并且将这个栈顶元素弹出),不移动任何元素
  • lua_copy:将一个索引上的值复制到另一个索引上,原值不受影响

如下程序展示了上述 API 的使用:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <stdio.h>
#include "lua.h"
#include "lauxlib.h"

static void stackDump(lua_State *L) {
int i;
int top = lua_gettop(L);

for (i = 1; i <= top; i++) {
int t = lua_type(L, i);
switch (t) {
case LUA_TSTRING: {
printf("'%s'", lua_tostring(L, i));
break;
}
case LUA_TBOOLEAN: {
printf(lua_toboolean(L, i) ? "true" : "false");
break;
}
case LUA_TNUMBER: {
printf("%g", lua_tonumber(L, i));
break;
}
default: {
printf("%s", lua_typename(L, t));
break;
}
}
printf(" ");
}

printf("\n");
}


int main(void) {
lua_State *L = luaL_newstate();

lua_pushboolean(L, 1);
lua_pushnumber(L, 10);
lua_pushnil(L);
lua_pushstring(L, "hello");

stackDump(L);

lua_pushvalue(L, -4);
stackDump(L);

lua_replace(L, 3);
stackDump(L);

lua_settop(L, 6);
stackDump(L);

lua_rotate(L, 3, 1);
stackDump(L);

lua_remove(L, -3);
stackDump(L);

lua_settop(L, -5);
stackDump(L);

lua_close(L);
return 0;
}

使用 CAPI 进行错误处理

LUA API 中的绝大多数函数都可能抛出异常。Lua 使用异常来提示错误,而没有在 API 的每个操作中使用错误码。而 C 语言没有提供异常处理机制。为了解决这个问题,Lua 使用了 C 中的 setjmp 机制,提供一个类似异常处理的机制,因此大多数 API 函数都可以抛出异常(即调用 longjmp)而不是直接返回。当编写库函数时(被 Lua 调用的 C 函数),由于 Lua 会捕获所有异常,使用 longjmp 并不用进行额外操作。但是编写应用代码(调用 Lua 的 C 代码时),则必须提供一种捕获异常的方式。

如果应用调用了 Lua API 中的函数,就可能发生错误。Lua 通过长跳转来提示错误,但是如果没有对应的 setjmp,解释器就无法长跳转。此时 API 中的任何错误都会导致 Lua 调用紧急函数,当函数返回之后,应用就会退出。可以通过 lua_atpanic 来设置自己的紧急函数。

要正确处理应用代码的错误,需要通过 Lua 来调用我们的代码,这样 Lua 才能设置合适的上下文来捕获异常,即在 setjmp 的上下文中运行代码。可以把 C 代码封装到一个函数 F 中,然后使用 lua_pcall 调用这个函数。通过这种方式,C 代码会在保护模式中运行,即使发生内存分配失败,lua_pcall 也会返回一个对应的错误码,使得解释器能够保持一致的状态。

在为 Lua 编写库函数时,通常无需进行错误处理。库函数抛出的错误要么被 Lua 中的 pcall 捕获,要么被应用代码中的 lua_pcall 捕获。因此 C 库函数检查到错误时,只需要调用 lua_error(或者 luaL_error),它会收拾 Lua 系统的残局,然后跳转会保护模式调用处,并传递错误信息。

内存分配

Lua 语言核心对内存分配不进行任何假设,它只会通过一个分配函数来分配和释放内存,当用户创建 Lua 状态时必须提供该函数。luaL_newstate 是一个用默认分配函数来创建 Lua 状态的辅助函数。该默认分配函数使用了 C 标准库中的 malloc-realloc-free。如果要完整控制 Lua 的内存分配,则需要使用原始的 lua_newstate 来创建 Lua 状态。

Lua 内部不会为了重用而缓存空闲内存,它假定分配函数会完成这种缓存工作,Lua 也不会试图压缩内存碎片。

扩展应用

Lua 的重要用途之一就是用作配置语言,接下来会介绍如何使用 Lua 来配置一个程序。

基础知识

假设 C 程序有一个窗口,并希望用户能够指定窗口的初始大小。我们使用一个 Lua 配置文件(也是一个普通的文本文件,只不过它是一个 Lua 程序),例如内容如下:

1
2
width = 200
height = 300

我们需要使用 Lua API 来指挥 Lua 语言解析该文件,并获取全局变量 width 和 height 的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int getglobalint(lua_State *L, const char *var) {
int isnum, result;

lua_getglobal(L, var);
result = (int)lua_tointegerx(L, -1, &isnum);
if (!isnum) {
error(L, "'%s' should be a number\n", var);
}

lua_pop(L, 1);
return result;
}

void load(lua_State *L, const char *fname, int *w, int *h) {
if (luaL_loadfile(L, fname) || lua_pcall(L, 0, 0, 0)) {
error(L, "cannot run config. file: %s", lua_tostring(L, -1));
}

*w = getglobalint(L, "width");
*h = getglobalint(L, "height");
}

这段代码的关键是调用 lua_getglobal 让 Lua 将相应的全局变量的值压入栈中,之后调用 lua_tointegerx 将这个值转换为整型以保证类型正确。这段代码还用到了之前封装的一些库函数,这里不再详述。

使用 Lua 的好处是:

  • Lua 为我们处理了所有语法细节,甚至配置文件都可以有注释
  • 可以 Lua 实现更复杂的配置

操作表

在配置中,使用 Lua 的表结构可以让脚本变得更加架构化。例如对于如下配置文本:

1
2
BLUE = {red = 0, green = 0, blue = 1.0}
backend = BLUE;

可以通过代码实现对 table 的操作:

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
#define MAX_COLOR 255

int getcolorfield(lua_State *L, const char *key) {
int result, isnum;

lua_pushstring(L, key);
lua_gettable(L, -2);

result = (int)(lua_tonumberx(L, -1, &isnum) * MAX_COLOR);
if (!isnum) {
error(L, "invalid component'%s' in color", key);
}

lua_pop(L, 1);
return result;
}

lua_getglobal(L, "background");
if (!lua_istable(L, -1)) {
error(L, "'background' is not a table");
}

red = getcolorfield(L, "red");
green = getcolorfield(L, "green");
blue = getcolorfield(L, "blue");

lua_gettable 以表在栈中的位置为参数,获取以栈顶元素为 key 的对应 value,之后从栈中弹出 key、再压入相应的 value。

可以继续扩展该示例,为用户引入颜色名字。用户除了可以使用颜色表,还可以直接使用常用颜色的预定义名称。要实现该功能,需要在 C 程序中就要有一张颜色表,然后用这个颜色表在 Lua 中定义对应的全局变量。

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
struct ColorTable {
char *name;
unsigned char red, green, blue;
} color_table[] = {
{"WHITE", MAX_COLOR, MAX_COLOR, MAX_COLOR},
{"RED", MAX_COLOR, 0, 0},
{"GREEN", 0, MAX_COLOR, 0},
{"BLUE", 0, 0, MAX_COLOR},
{NULL, 0, 0, 0}
};

void setcolorfield(lua_State *L, const char *index, int value) {
lua_pushstring(L, index);
lua_pushnumber(L, (double) value / MAX_COLOR);
lua_settable(L, -3);
}

void setcolor(lua_State *L, struct ColorTable *ct) {
lua_newtable(L);

setcolorfield(L, "red", ct->red);
setcolorfield(L, "green", ct->green);
setcolorfield(L, "blue", ct->blue);
lua_setglobal(L, ct->name);
}

int i = 0;
while (color_table[i] != NULL) {
setcolor(L, &color_table[i++]);
}

函数 lua_newtable 用于创建一个空表,并将其压入栈中,之后的 setcolorfield 设置表的各个字段,最后函数 lua_setglobal 弹出表,并将其设置为具有指定名称全局变量的值,这样就为 Lua 脚本注册了指定的颜色。

除了给 Lua 脚本注册颜色名称,还有一种解决该问题的思路。即在应用程序中(C 代码中)根据 Lua 脚本的配置来在 C 程序中搜索对应的颜色配置。

尽管 Lua 的 CAPI 追求简洁性,但是 Lua 也没有做得过于激进。因此 CAPI 为一些常用操作提供了一些简便方法:

  • lua_getfield 用于通过字符串类型的键来检索表,该函数会返回结果的类型
  • lua_setfield 用于为字符串类型的键来设置表中的 value
  • lua_createtable 可以创建表并为元素预分配空间

调用 Lua 函数

调用 Lua 函数的 API 规范很简单,首先将待调用的函数压栈,然后压入函数的参数,接着调用 lua_pcall 进行实际的调用,最后从栈中取出结果。假设 Lua 配置脚本中有如下函数:

1
2
3
function f(x, y)
return (x^2 * math.sin(y)) / (1 - x)
end

如下展示了从 C 语言中调用 Lua 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
double f(lua_State *L, double x, double y) {
int isnum;
double z;

lua_getglobal(L, "f");
lua_pushnumber(L, x);
lua_pushnumber(L, y);

if (lua_pcall(L, 2, 1, 0) != LUA_OK) {
error(L, "error running function 'f': %s", lua_tostring(L, -1));
}

z = lua_tonumberx(L, -1, &isnum);
if (!isnum) {
error(L, "function 'f' should return a number");
}

lua_pop(L, 1);
return z;
}

在调用函数 lua_pcall 时,第二个参数表示传递的参数数量,第三个参数是期望的结果数量,第四个参数代表错误处理函数。lua_pcall 会根据所要求的数量来调整返回值的个数,即压入 nil 或者丢弃多余的结果。在压入结果前,lua_pcall 会把函数和其参数从栈中移除。当返回多个结果时,第一个结果最先被压入。

如果 lua_pcall 在运行过程中出现错误,它会返回一个错误码,并在栈中压入一条错误信息(仍会弹出函数及其参数)。如果有错误处理函数,在压入错误信息之前,lua_pcall 会先调用错误处理函数。lua_pcall 的最后一个参数即表示错误处理函数在栈中的索引(0 表示没有错误处理函数)。

如下说明 lua_pcall 返回的错误码:

  • 对于普通的错误,返回 LUA_ERRRUN
  • 对于内存分配失败,返回 LUA_ERRMEM
  • 对于错误处理函数本身出错,返回 LUA_ERRGCMM

一个通用的调用函数

如下实现了一个调用 Lua 函数的包装程序:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <stdarg.h>

void call_va(lua_State *L, const char *func, const char *sig, ...) {
va_list vl;
int narg, nres;

va_start(vl, sig);
lua_getglobal(L, func);

luaL_checkstack(L, 1, "too many arguments");

for (narg = 0; *sig; narg++) {
luaL_checkstack(L, 1, "too many arguments");
switch (*sig++) {
case 'd':
lua_pushnumber(L, va_arg(vl, double));
break;
case 'i':
lua_pushinteger(L, va_arg(vl, int));
break;
case 's':
lua_pushstring(L, va_arg(vl, char*));
break;
case '>':
goto endargs;
default:
error(L, "invalid option (%c)", *(sig - 1));
}
}

endargs:

nres = strlen(sig);
if (lua_pcall(L, narg, nres, 0) != 0) {
error(L, "error calling '%s': %s", func, lua_tostring(L, -1));
}

nres = - nres;
while (*sig) {
switch(*sig++) {
case 'd': {
int isnum;
double n = lua_tonumberx(L, nres, &isnum);
if (!isnum) {
error(L, "wrong result type");
}
*va_arg(vl, double*) = n;
break;
}
case 'i': {
int isnum;
int n = lua_tonumberx(L, nres, &isnum);
if (!isnum) {
error(L, "wrong result type");
}
*va_arg(vl, int*) = n;
break;
}
case 's': {
const char *s = lua_tostring(L, nres);
if (s == NULL) {
error(L, "wrong result type");
}
*va_arg(vl, char**) = s;
break;
}
default: {
error(L, "invalid option (%c)", *(sig -1));
}
}

nres++;
}

va_end(vl);
}