第25章 扩展你的程序

25 扩展你的程序
作为配置语言是LUA的一个重要应用。在这个章节里,我们举例说明如何用LUA设置一个程序。让我们用一个简单的例子开始然后展开到更复杂的应用中。
首先,让我们想象一下一个简单的配置情节:你的C程序(程序名为PP)有一个窗口界面并且可以让用户指定窗口的初始大小。显然,类似这样简单的应用,有多种解决方法比使用LUA更简单,比如环境变量或者存有变量值的文件。但,即使是用一个简单的文本文件,你也不知道如何去解析。所以,最后决定采用一个LUA配置文件(这就是LUA程序中的纯文本文件)。在这种简单的文本形式中通常包含类似如下的信息行:
-- configuration file for program `pp'
-- define window size
width = 200
height = 300
现在,你得调用LUA API函数去解析这个文件,取得widthheight这两个全局变量的值。下面这个取值函数就起这样的作用:
#include <lua.h>
#include <lauxlib.h>
#include <lualib.h>
 
void load (char *filename, int *width, int *height) {
    lua_State *L = lua_open();
    luaopen_base(L);
    luaopen_io(L);
    luaopen_string(L);
    luaopen_math(L);
 
    if (luaL_loadfile(L, filename) || lua_pcall(L, 0, 0, 0))
       error(L, "cannot run configuration file: %s",
           lua_tostring(L, -1));
 
    lua_getglobal(L, "width");
    lua_getglobal(L, "height");
    if (!lua_isnumber(L, -2))
       error(L, "`width' should be a number/n");
    if (!lua_isnumber(L, -1))
       error(L, "`height' should be a number/n");
    *width = (int)lua_tonumber(L, -2);
    *height = (int)lua_tonumber(L, -1);
 
    lua_close(L);
}
首先,程序打开LUA包并加载了标准函数库(虽然这是可选的,但通常包含这些库是比较好的编程思想)。然后程序使用luaL_loadfile方法根据参数filename加载此文件中的信息块并调用lua_pcall函数运行,这些函数运行时若发生错误(例如配置文件中有语法错误),将返回非零的错误代码并将此错误信息压入栈中。通常,我们用带参数index值为-1lua_tostring函数取得栈顶元素(error函数我们已经在24.1章节中定义)。
解析完取得的信息块后,程序会取得全局变量值。为此,程序调用了两次lua_getglobal函数,其中一参数为变量名称。每调用一次就把相应的变量值压入栈顶,所以变量widthindex值是-2而变量heightindex值是-1(在栈顶)。(因为先前的栈是空的,需要从栈底重新索引,1表示第一个元素2表示第二个元素。由于从栈顶索引,不管栈是否为空,你的代码也能运行)。接着,程序用lua_isnumber函数判断每个值是否为数字。lua_tonumber函数将得到的数值转换成double类型并用(int)强制转换成整型。最后,关闭数据流并返回值。
Lua是否值得一用?正如我前面提到的,在这个简单的例子中,相比较于lua用一个只包含有两个数字的文件会更简单。即使如此,使用lua也带来了一些优势。首先,它为你处理所有的语法细节(包括错误);你的配置文件甚至可以包含注释!其次,用可以用lua做更多复杂的配置。例如,脚本可以向用户提示相关信息,或者也可以查询环境变量以选择合适的大小:
-- configuration file for program 'pp'
if getenv("DISPLAY") == ":0.0" then
    width = 300; height = 300
else
    width = 200; height = 200
end
在这样简单的配置情节中,很难预料用户想要什么;不过只要脚本定义了这两个变量,你的C程序无需改变就可运行。
最后一个使用lua的理由:在你的程序中很容易的加入新的配置单元。方便的属性添加使程序更具有扩展性。
25.1 表操作
现在,我们打算使用Lua作为配置文件,配置窗口的背景颜色。我们假定最终的颜色有三个数字(RGB)描述,每一个数字代表颜色的一部分。通常,在C语言中,这些数字使用[0,255]范围内的整数表示,由于在Lua中所有数字都是实数,我们可以使用更自然的范围[0,1]来表示。
一个粗糙的解决方法是,对每一个颜色组件使用一个全局变量表示,让用户来配置这些变量:
-- configuration file for program 'pp'
width = 200
height = 300
background_red = 0.30
background_green = 0.10
background_blue = 0
这个方法有两个缺点:第一,太冗余(为了表示窗口的背景,窗口的前景,菜单的背景等,一个实际的应用程序可能需要几十个不同的颜色);第二,没有办法预定义共同部分的颜色,比如,假如我们事先定义了WHITE,用户可以简单的写background = WHITE来表示所有的背景色为白色。为了避免这些缺点,我们使用一个table来表示颜色:
background = {r=0.30, g=0.10, b=0}
表的使用给脚本的结构带来很多灵活性,现在对于用户(或者应用程序)很容易预定义一些颜色,以便将来在配置中使用:
BLUE = {r=0, g=0, b=1}
...
background = BLUE
为了在C中获取这些值,我们这样做:
lua_getglobal(L, "background");
if (!lua_istable(L, -1))
    error(L, "`background' is not a valid color table");
 
red = getfield("r");
green = getfield("g");
blue = getfield("b");
一般来说,我们首先获取全局变量backgroud的值,并保证它是一个table。然后,我们使用getfield函数获取每一个颜色组件。这个函数不是API的一部分,我们需要自己定义他:
#define MAX_COLOR       255
 
/* assume that table is on the stack top */
int getfield (const char *key) {
    int result;
    lua_pushstring(L, key);
    lua_gettable(L, -2); /* get background[key] */
    if (!lua_isnumber(L, -1))
       error(L, "invalid component in background color");
    result = (int)lua_tonumber(L, -1) * MAX_COLOR;
    lua_pop(L, 1); /* remove number */
    return result;
}
这里我们再次面对多态的问题:可能存在很多个getfield的版本,key的类型,value的类型,错误处理等都不尽相同。Lua API只提供了一个lua_gettable函数,他接受table在栈中的位置为参数,将对应key值出栈,返回与key对应的value。我们上面的getfield函数假定table在栈顶,因此,lua_pushstringkey入栈之后,table-2的位置。返回之前,getfield会将栈恢复到调用前的状态。
我们对上面的例子稍作延伸,加入颜色名。用户仍然可以使用颜色table,但是也可以为共同部分的颜色预定义名字,为了实现这个功能,我们在C代码中需要一个颜色table
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},
    {"BLACK", 0,         0,         0},
    ...
    {NULL,     0,         0,         0}     /* sentinel */
};
我们的这个实现会使用颜色名创建一个全局变量,然后使用颜色table初始化这些全局变量。结果和用户在脚本中使用下面这几行代码是一样的:
WHITE = {r=1, g=1, b=1}
RED    = {r=1, g=0, b=0}
...
脚本中用户定义的颜色和应用中(C代码)定义的颜色不同之处在于:应用在脚本之前运行。
为了可以设置table域的值,我们定义个辅助函数setfield;这个函数将field的索引和field的值入栈,然后调用lua_settable
/* assume that table is at the top */
void setfield (const char *index, int value) {
    lua_pushstring(L, index);
    lua_pushnumber(L, (double)value/MAX_COLOR);
    lua_settable(L, -3);
}
与其他的API函数一样,lua_settable在不同的参数类型情况下都可以使用,他从栈中获取所有的参数。lua_settabletable在栈中的索引作为参数,并将栈中的keyvalue出栈,用这两个值修改tableSetfield函数假定调用之前table是在栈顶位置(索引为-1)。将indexvalue入栈之后,table索引变为-3
Setcolor函数定义一个单一的颜色,首先创建一个table,然后设置对应的域,然后将这个table赋值给对应的全局变量:
void setcolor (struct ColorTable *ct) {
    lua_newtable(L);            /* creates a table */
    setfield("r", ct->red);     /* table.r = ct->r */
    setfield("g", ct->green);   /* table.g = ct->g */
    setfield("b", ct->blue);    /* table.b = ct->b */
    lua_setglobal(ct->name);    /* 'name' = table */
}
lua_newtable函数创建一个新的空table然后将其入栈,调用setfield设置table的域,最后lua_setglobaltable出栈并将其赋给一个全局变量名。
有了前面这些函数,下面的循环注册所有的颜色到应用程序中的全局变量:
int i = 0;
while (colortable[i].name != NULL)
    setcolor(&colortable[i++]);
记住:应用程序必须在运行用户脚本之前,执行这个循环。
对于上面的命名颜色的实现有另外一个可选的方法。用一个字符串来表示颜色名,而不是上面使用全局变量表示,比如用户可以这样设置background = "BLUE"。所以,background可以是table也可以是string。对于这种实现,应用程序在运行用户脚本之前不需要做任何特殊处理。但是需要额外的工作来获取颜色。当他得到变量background的值之后,必须判断这个值的类型,是table还是string
lua_getglobal(L, "background");
if (lua_isstring(L, -1)) {
    const char *name = lua_tostring(L, -1);
    int i = 0;
    while (colortable[i].name != NULL &&
       strcmp(colorname, colortable[i].name) != 0)
       i++;
    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("r");
    green = getfield("g");
    blue = getfield("b");
} else
    error(L, "invalid value for `background'");
哪个是最好的选择呢?在C程序中,使用字符串表示不是一个好的习惯,因为编译器不会对字符串进行错误检查。然而在Lua中,全局变量不需要声明,因此当用户将颜色名字拼写错误的时候,Lua不会发出任何错误信息。比如,用户将WHITE误写成WITEbackground变量将为nil(WITE的值没有初始化),然后应用程序就认为background的值为nil。没有其他关于这个错误的信息可以获得。另一方面,使用字符串表示,background的值也可能是拼写错了的字符串。因此,应用程序可以在发生错误的时候,定制输出的错误信息。应用可以不区分大小写比较字符串,因此,用户可以写"white""WHITE",甚至"White"。但是,如果用户脚本很小,并且颜色种类比较多,注册成百上千个颜色(需要创建成百上千个table和全局变量),最终用户可能只是用其中几个,这会让人觉得很怪异。在使用字符串表示的时候,应避免这种情况出现。
25.2 调用Lua函数
Lua作为配置文件的一个最大的长处在于它可以定义个被应用调用的函数。比如,你可以写一个应用程序来绘制一个函数的图像,使用Lua来定义这个函数。
使用API调用函数的方法是很简单的:首先,将被调用的函数入栈;第二,依次将所有参数入栈;第三,使用lua_pcall调用函数;最后,从栈中获取函数执行返回的结果。
看一个例子,假定我们的配置文件有下面这个函数:
function f (x, y)
    return (x^2 * math.sin(y))/(1 - x)
end
并且我们想在C中对于给定的x,y计算z=f(x,y)的值。假如你已经打开了lua库并且运行了配置文件,你可以将这个调用封装成下面的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时指定参数的个数和返回结果的个数。第四个参数可以指定一个错误处理函数,我们下面再讨论它。和Lua中赋值操作一样,lua_pcall会根据你的要求调整返回结果的个数,多余的丢弃,少的用nil补足。在将结果入栈之前,lua_pcall会将栈内的函数和参数移除。如果函数返回多个结果,第一个结果被第一个入栈,因此如果有n个返回结果,第一个返回结果在栈中的位置为-n,最后一个返回结果在栈中的位置为-1
如果lua_pcall运行时出现错误,lua_pcall会返回一个非0的结果。另外,他将错误信息入栈(仍然会先将函数和参数从栈中移除)。在将错误信息入栈之前,如果指定了错误处理函数,lua_pcall毁掉用错误处理函数。使用lua_pcall的最后一个参数来指定错误处理函数,0代表没有错误处理函数,也就是说最终的错误信息就是原始的错误信息。否则,那个参数应该是一个错误函数被加载的时候在栈中的索引,注意,在这种情况下,错误处理函数必须要在被调用函数和其参数入栈之前入栈。
对于一般错误,lua_pcall返回错误代码LUA_ERRRUN。有两种特殊情况,会返回特殊的错误代码,因为他们从来不会调用错误处理函数。第一种情况是,内存分配错误,对于这种错误,lua_pcall总是返回LUA_ERRMEM。第二种情况是,当Lua正在运行错误处理函数时发生错误,这种情况下,再次调用错误处理函数没有意义,所以lua_pcall立即返回错误代码LUA_ERRERR
25.3 通用的函数调用
看一个稍微高级的例子,我们使用Cvararg来封装对Lua函数的调用。我们的封装后的函数(call_va)接受被调用的函数明作为第一个参数,第二参数是一个描述参数和结果类型的字符串,最后是一个保存返回结果的变量指针的列表。使用这个函数,我们可以将前面的例子改写为:
call_va("f", "dd>d", x, y, &z);
字符串 "dd>d" 表示函数有两个double类型的参数,一个double类型的返回结果。我们使用字母 'd' 表示double'i' 表示integer's' 表示strings'>' 作为参数和结果的分隔符。如果函数没有返回结果,'>' 是可选的。
#include <stdarg.h>
 
void call_va (const char *func, const char *sig, ...) {
    va_list vl;
    int narg, nres;   /* number of arguments and results */
 
    va_start(vl, sig);
    lua_getglobal(L, func); /* get function */
 
    /* push arguments */
    narg = 0;
    while (*sig) {    /* push arguments */
       switch (*sig++) {
 
       case 'd': /* double argument */
           lua_pushnumber(L, va_arg(vl, double));
           break;
 
       case 'i': /* int argument */
           lua_pushnumber(L, va_arg(vl, int));
           break;
 
       case 's': /* string argument */
           lua_pushstring(L, va_arg(vl, char *));
           break;
 
       case '>':
           goto endwhile;
 
       default:
           error(L, "invalid option (%c)", *(sig - 1));
       }
       narg++;
       luaL_checkstack(L, 1, "too many arguments");
    } endwhile:
 
    /* do the call */
    nres = strlen(sig); /* number of expected results */
    if (lua_pcall(L, narg, nres, 0) != 0) /* do the call */
       error(L, "error running function `%s': %s",
           func, lua_tostring(L, -1));
 
    /* retrieve results */
    nres = -nres;     /* stack index of first result */
    while (*sig) {    /* get results */
       switch (*sig++) {
 
       case 'd': /* double result */
           if (!lua_isnumber(L, nres))
              error(L, "wrong result type");
           *va_arg(vl, double *) = lua_tonumber(L, nres);
           break;
 
       case 'i': /* int result */
           if (!lua_isnumber(L, nres))
              error(L, "wrong result type");
           *va_arg(vl, int *) = (int)lua_tonumber(L, nres);
           break;
 
       case 's': /* string result */
           if (!lua_isstring(L, nres))
               error(L, "wrong result type");
           *va_arg(vl, const char **) = lua_tostring(L, nres);
           break;
 
       default:
           error(L, "invalid option (%c)", *(sig - 1));
       }
       nres++;
    }
    va_end(vl);
}
尽管这段代码具有一般性,这个函数和前面我们的例子有相同的步骤:将函数入栈,参数入栈,调用函数,获取返回结果。大部分代码都很直观,但也有一点技巧。首先,不需要检查func是否是一个函数,lua_pcall可以捕捉这个错误。第二,可以接受任意多个参数,所以必须检查栈的空间。第三,因为函数可能返回字符串,call_va不能从栈中弹出结果,在调用者获取临时字符串的结果之后(拷贝到其他的变量中),由调用者负责弹出结果。
 
 

 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值