programming in lua (7)

第 25 章。

lua 的一个重要的应用是作为程序配置文件的语言(configuration language)。下面通过一些例子循序渐进地说明其应用。

一个简单的例子

假设一个 c 语言程序要画一个窗口界面,窗口的长和宽可以由用户指定。要实现这个功能还有另外更简单的方法,例如使用环境变量或只包含 (key, value) 形式的普通文本文件,但是如果使用普通文本文件你还是需要对其进行解析。程序的配置文件如下:

-- conf.lua
-- define window size
width = 200
height = 300

下面的程序演示了怎样使用 lua 提供的接口来获得这两个变量的值:

#include <stdio.h>

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

int main(void)
{
    lua_State* l;

    l = luaL_newstate();
    luaL_openlibs(l);

    if (luaL_loadfile(l, "./conf.lua") != 0) {
        fprintf(stderr, "luaL_loadfile err: %s.\n", lua_tostring(l, -1));
        goto end;
    }

    if (lua_pcall(l, 0, 0, 0) != 0) {
        fprintf(stderr, "lua_pcall err: %s.\n", lua_tostring(l, -1));
        goto end;
    }

    lua_getglobal(l, "width");
    if (!lua_isnumber(l, -1))
        fprintf(stderr, "'width' should be a number.\n");
    else
        printf("width = %d\n", lua_tointeger(l, -1));
    lua_pop(l, 1);

    lua_getglobal(l, "height");
    if (!lua_isnumber(l, -1))
        fprintf(stderr, "'height' should be a number.\n");
    else
        printf("height = %d\n", lua_tointeger(l, -1));
    lua_pop(l, 1);

end:
    lua_close(l);
    return 0;
}

程序首先用 luaL_newstate() 或 luaL_openlibs() 准备必要的环境,然后使用函数

int luaL_loadfile (lua_State *L, const char *filename);

加载并解析 lua 文件。完成后使用 lua_pcall() 执行文件中的语句。函数

void lua_getglobal (lua_State *L, const char *name);

将 lua 文件中名为 name 的全局变量的值放入栈中。如果变量 name 不存在,lua 会向栈中放入一个 nil 值。然后使用 lua_isnumber() 判断获取的内容是否是数值,如果是的话就将其转换成 c 语言对应的类型。

这个例子很简单,只是用来说明 lua 的用法,实现这个功能可以使用其它更简单的方法。但是使用 lua 作为配置文件还有更强大的地方,例如可以使用注释,还可以根据环境变量来作出不同的选择:

-- configuration file
if getenv("DISPLAY") == ":0.0" then
  width = 300; height = 300
else
  width = 200; height = 200
end

table 操作

假设现在要求窗口的颜色可以由用户来指定,并且颜色值由三个整数值来组成:“R”表示红色,“G”表示绿色,“B”表示蓝色。在 c 语言中,这三个值的范围是 [0, 255]。在 lua 中,因为所有的数值都是实数,我们可以把这三个值的范围限定为 [0, 1]。

一种方法是另外提供三个环境变量来分别指定这三个值:

-- configuration file
width = 200
height = 300
background_red = 0.30
background_green = 0.10
background_blue = 0

不过这种做法有两个缺点:如果用户需要指定不同部分的颜色,例如窗口背景颜色,字体颜色,菜单背景颜色等等,每个部分都需要增加三个变量,这样太麻烦了;而且用户可能会定义一些颜色常量,例如蓝色(BLUE),这样指定颜色的时候就可以写成“background=BLUE”的形式,而不需要为分别为三个值赋值。为了避免这些问题,我们可以使用一个 table 来表示颜色:

background = {r = 0.30, g = 0.10, b = 0}

用户可以很容易地自定义颜色常量并使用它:

BLUE = {r = 0, g = 0, b = 1}

background = BLUE

下面的代码可以用来获取 background 的 rgb 值:

#include <stdio.h>

#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>

#define MAX_COLOR 255

/* assume that table is on the stack top */
static int getfield(lua_State* l, const char* key)
{
    int result = -1;

    lua_getfield(l, -1, key);
    if (!lua_isnumber(l, -1))
        goto end;

    result = (int)(lua_tonumber(l, -1) * MAX_COLOR);

end:
    lua_pop(l, 1);  /* element that lua_getfield() pushed */
    return result;
}

int main(void)
{
    lua_State* l;
    int result;

    l = luaL_newstate();
    luaL_openlibs(l);

    if (luaL_loadfile(l, "./conf.lua") != 0) {
        fprintf(stderr, "luaL_loadfile err: %s.\n", lua_tostring(l, -1));
        goto end;
    }

    if (lua_pcall(l, 0, 0, 0) != 0) {
        fprintf(stderr, "lua_pcall err: %s.\n", lua_tostring(l, -1));
        goto end;
    }

    lua_getglobal(l, "background");
    if (!lua_istable(l, -1)) {
        fprintf(stderr, "'background' should be a table.\n");
        goto end;
    }

    result = getfield(l, "r");
    if (result < 0) {
        fprintf(stderr, "invalid component in background color.\n");
        goto end;
    }
    printf("r = %d\n", result);

    result = getfield(l, "g");
    if (result < 0) {
        fprintf(stderr, "invalid component in background color.\n");
        goto end;
    }
    printf("g = %d\n", result);

    result = getfield(l, "b");
    if (result < 0) {
        fprintf(stderr, "invalid component in background color.\n");
        goto end;
    }
    printf("b = %d\n", result);

end:
    lua_pop(l, 1);
    lua_close(l);
    return 0;
}

程序首先使用 lua_getglobal() 获取变量 background 并且确保它是一个 table 类型,然后使用函数

void lua_getfield (lua_State *L, int index, const char *k);

获取对应字段的值。lua_getfield() 从版本 5.1 开始提供,它从位于 index 的 table 中获取关键字为 k 的域,并将其值压入栈顶。

下面来扩展这个例子。用户可以使用程序所提供的 color table,也可以自定义新的颜色。程序将会创建几种颜色变量,效果等同于下面的 lua 语句:

WHITE = {r=1, g=1, b=1}
RED   = {r=1, g=0, b=0}
-- other colors

为了实现这个功能,我们先定义一下 c 语言中的数据结构:

struct ColorTable {
    char *name;
    unsigned char red, green, blue;
} colortable[] = {
    {"WHITE",    MAX_COLOR, MAX_COLOR, MAX_COLOR},
    {"RED",      MAX_COLOR,   0,   0},
    {"GREEN",      0, MAX_COLOR,   0},
    {"BLUE",       0,   0, MAX_COLOR},
    /* other colors */
    {NULL,         0, 0, 0} /* sentinel */
};

为了设置颜色值,我们定义一个辅助函数 setfield(),用来为 table 的某个域赋值:

/* assume that table is on the stack top when this function is called */
void setfield(lua_State* l, const char* key, int value)
{
    lua_pushnumber(l, (double)value / MAX_COLOR);
    lua_setfield(l, -2, key);
}

lua 从 5.1 开始提供了函数

void lua_setfield (lua_State *L, int index, const char *k);

来为 table 的某个域赋值,即执行语句“t[k]=v”。其中 index 是 t 所在的栈位置索引,k 为域的名称,v 为栈顶元素,因此在调用这个函数前需要把 v 的值先压入栈中。执行完后函数会把 v 的值从栈顶弹出,不需手动清理。

下一个辅助函数是 setcolor()。它创建一个新的 table,并把指定的颜色加入到这个 table 中:

void setcolor (lua_State *L, struct ColorTable *ct)
{
    lua_newtable(L);                 /* creates a table */
    setfield(L, "r", ct->red);       /* table.r = ct->r */
    setfield(L, "g", ct->green);     /* table.g = ct->g */
    setfield(L, "b", ct->blue);      /* table.b = ct->b */
    lua_setglobal(L, ct->name);      /* ’name’ = table */
}

函数

void lua_newtable (lua_State *L);

创建一个新的 table 并把其压入栈中;然后使用辅助函数 setfield() 分别为三个颜色域赋值;最后使用函数

void lua_setglobal (lua_State *L, const char *name);

把栈顶的元素设为一个全局变量 name,并且将其从栈里弹出。

最后使用下面的循环设置所有颜色:

int i = 0;
while (colortable[i].name != NULL)
    setcolor(L, &colortable[i++]);

实现这个功能还有另一种方法。先看下面的代码片段:

lua_getglobal(L, "background");
if (lua_isstring(L, -1)) {    /* value is a string? */
    const char *name = lua_tostring(L, -1); /* get string */
    int i;    /* search the color table */
    for (i = 0; colortable[i].name != NULL; i++) {
        if (strcmp(colorname, colortable[i].name) == 0)
            break;
    }
    if (colortable[i].name == NULL) /* string not found? */
        error(L, "invalid color name (%s)", colorname);
    else { /* use colortable[i] */
        red = colortable[i].red;
        green = colortable[i].green;
        blue = colortable[i].blue;
    }
} else if (lua_istable(L, -1)) {
    red = getfield(L, "r");
    green = getfield(L, "g");
    blue = getfield(L, "b");
} else
    error(L, "invalid value for ’background’");

除了使用全局变量外,用户可以把颜色写成这样的形式

background = "BLUE"

因此 background 可能是一个字符串或者是一个 table 类型。如果是一个字符串,就到 colortable 中查找是否存在该名称的颜色;如果是一个 table 类型就直接获取三个域的值。

哪个选择更好些呢?在 c 程序中,使用字符串来表示选项不算是一个好的选择,因为编译器不能检测出字符串的拼写错误。但是在 lua 中,由于全局变量不需声明即可使用,因此对于全局变量名称的拼写错误,lua 不会给出任何错误信息(因为这不是错误)。例如下面一段脚本:

WHITE = {r = 1, g = 1, b = 1}

background = WITE

如果在 lua 脚本中用户把“WHITE”写成了“WITE”,在 c 程序中获得的 background 的值就是 nil(WITE 未初始化),程序得不到任何有用的信息。如果使用字符串形式的赋值,在 c 程序中就能够发现拼写错误(见上面的代码片段),而且程序比较字符串时可以忽略大小写,例如“white”,“WHITE”,甚至“White”都可以看成是等价的。并且如果程序中只使用了少量的颜色,但是在脚本中却不得不定义大量的颜色常量供用户使用,而使用字符串就省却了定义常量的麻烦。

在 c 程序中使用 lua 脚本定义的函数

c 程序可以使用 lua 脚本中定义的所有变量,包括函数。例如你可以写一个划分图像的程序,其中划分的函数可以由 lua 脚本来定义。使用函数的方法很简单:先把要调用的函数压入栈中;接着按顺序压入函数参数;然后使用 lua_pcall() 来调用函数;最后获得函数返回值。

例如在 lua 脚本中定义了这样的函数:

function f(x, y)
    return (x + y)
end

在 c 语言中使用这个函数:

/* call a function 'f' defined in Lua */
double f (double x, double y)
{
    double z;
    /* push functions and arguments */
    lua_getglobal(L, "f"); /* function to be called */
    lua_pushnumber(L, x);    /* push 1st argument */
    lua_pushnumber(L, y);    /* push 2nd argument */

    /* do the call (2 arguments, 1 result) */
    if (lua_pcall(L, 2, 1, 0) != 0)
        error(L, "error running function 'f': %s",
              lua_tostring(L, -1));

    /* retrieve result */
    if (!lua_isnumber(L, -1))
        error(L, "function 'f' must return a number");

    z = lua_tonumber(L, -1);
    lua_pop(L, 1); /* pop returned value */
    return z;
}

lua_pcall() 的函数原型是

int lua_pcall (lua_State *L, int nargs, int nresults, int msgh);

第二个参数是要调用的函数的参数个数,第三个参数是返回值的个数,第四个参数是错误处理相关的(后面会讲到),函数执行的结果会压入栈中。在把结果压入栈之前它会把需要调用的函数以及传递给函数的参数弹出栈。lua_pcall() 会根据你需要的参数个数作出调整,例如使用 nil 填充或者忽略多余的返回值。如果一个函数返回多个值,返回值按顺序入栈。例如一个函数返回三个值,第一个返回值的索引是 -3,第二个是 -2,第三个是 -1。

如果 lua_pcall() 执行出错会返回一个非零值,并且把错误信息压入栈中(会把函数及传递给函数的参数先出栈)。如果调用 lua_pcall() 时指定了错误处理函数,在把错误信息入栈前这个错误处理函数将会被调用。我们可以通过 lua_pcall() 的第四个参数来指定错误处理函数,0 表示不使用;否则这个值表示错误处理函数所在的栈位置索引。因此如果需要使用错误处理函数,这个函数需要在 lua_pcall() 的所有参数入栈前被放入栈中(具体到上面的代码片段,也就是在第一个 lua_getglobal() 被调用之前)。

对于一般的错误,lua_pcall() 返回一个 LUA_ERRRUN。另外有两种特殊的错误,它们出错的时候并不会执行错误处理函数(即使用户已经指定)。第一个是内存分配错误,对于这个错误 lua_pcall() 返回 LUA_ERRMEM;第二个是在执行错误处理函数的时候出错了,在这种情况下再运行一遍错误处理函数就没意思了,因此 lua_pcall() 立即返回 LUA_ERRERR。

一个通用的函数调用框架

这部分是作者利用 c 提供的不定参数实现的一个类似 printf() 的功能,不看了。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注