第二十五课 扩展应用程序

xiaoxiao2021-02-27  402

基础 第一个任务是一个简单的配置应用。假设C程序有一个窗口,并希望用户指定窗口的初始大小。显然,对于这种简单的任务,有多种比Lua 更简单的做法,例如使用环境变量或者使用记录了名值对的文件。不过就算使用一个简单的文本文件,也需要进行分析。因此使用Lua来作为配置文件。下面是这种文件最简单的形式,它可以包含如下内容: --定义窗口大小 width = 200 height = 300 此时,必须用Lua API来指挥Lua分析这个文件,并获取全局变量width和height的值。下面这个 load函数完成了此项工作: 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)); } 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"); } *w = lua_tointeger(L, -2); *h = lua_tointeger(L, -1); } 假设已经创建了一个Lua状态,这个函数调用 luaL_loadfile从文件fname加载程序块,然后调用lua_pcall运行编译好的程序块。若发生错误(例如配置文件中的语法错误),这两个函数就会把错误消息压入栈,并返回一个非0的错误代码。此时,程序就调用 lua_tostring从栈顶获取该消息。 当运行完程序块后,程序需要获取全局变量的值。程序调用了lua_getglobal两次,这个函数除了第一个常规的lua_State参数外,还需要变量的名称。每次 调用这个函数,它都会将相应的全局变量值压入栈中。因此width处于索引-2上,height位于 索引-1上。另外,由于 栈事先是空的,也可以从栈底进行 索引,第一个值使用索引1,第二个值使用2。不过,自上向下的索引可以使代码即使是在栈不为空的情况下依然可以工作。接下来程序调用lua_isnumber来检查两个值是否为数字。最后调用 lua_tointeger将这些值转换为整数,并赋予对应的参数变量。 那么是否值得用Lua来完成这类任务呢?其实对于这类简单的任务,用一个简单的文件来记录这两个数字就足够了,这比Lua更易于使用。但使用Lua却可以带来一些优势。首先,Lua会处理所有的语法细节或语法错误,包括配置文件中的注释。其次,用户可以实现一些更复杂的配置逻辑。例如,脚本可以提示用户某些信息,或者查询一个环境变量来选择合适的大小: --配置文件 if getenv("DISPLAY") == ":0.0" then width = 300; height = 300 else width = 200; height = 200 end 即使是在这样一个简单的配置示例中,用户也可能有很多实现需求。不过无论是何种实现,只要脚本定义了这两个变量,C程序则无须修改就能工作。 最后一个使用Lua的理由是,它更易于将新的配置机制添加到程序中。这种简易性可以让人形成一种态度,从而使程序变得更加灵活。 table操作 接下来要完成的任务是配置一个窗口的背景颜色。假设,颜色的格式是由3个数字组成的,每个数字都是RGB的一个颜色分量。在C语言中,这些数字通常是在区间[0,255]中的整型。但在Lua中,由于所有的数字都是实数,所以可以使用区间[0,1]。 一种基本的做法是要求用户将每个分量设置在不同的全局变量中: --配置文件 width = 200 height = 300 background_red = 0.30 background_green = 0.10 background_blue = 0 但是这种做法有两个缺点:第一,它太冗长了;第二,无法预定义常用颜色。如果能定义常用颜色,用户就可以简单地写出background = WHITE。为了避免这些缺点,使用table来表示颜色: background = {r=0.30, g=0.10, b=0} 使用table可以让脚本变得更加结构化。现在,用户就可以很容易地在配置文件中预定义常用颜色了: 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 table"); } red = getfield(L, ""r); green = getfield(L, "g"); blue = getfield(L, "b"); 现获取全局变量background的值,并确认其是一个table。然后,使用getfield获取颜色中的各个分量。不过这个函数不是API函数,因此必须定义它。然而由于在这里又遇到了多态的问题,所以需要更多版本的getfield函数,以针对不同的key类型、value类型和错误处理等。Lua API只提供了一个函数lua_gettable,它能处理所有的类型。但它需要知道table在栈中的位置,然后才会从栈中弹出key,并压入相应的value。getfield的定义如下: #define MAX_COLOR 255 /*假设table位于栈顶*/ int getfield(lua_State *L, const char *key) { int result; lua_pushstring(L, key); lua_gettable(L, -2); //获取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); //删除数字 return result; } 这个函数假设table位于栈顶。当用 lua_pushstring压入key后,table就位于索引-2上。在返回前,getfield弹出从栈中检索到的值,并使栈保持为调用前的样子。 由于经常需要用字符串来索引table,为此Lua 5.1提供了一个 lua_gettable的特化版本lua_getfield。通过这个函数,可以将如下两行: lua_pushstring(L, key); lua_gettable(L, -2); 重写为: lua_getfield(L, -1, key); 由于没有向栈中压入字符串,所以当调用 lua_getfield时,table的索引仍为-1。 接下来继续扩展这个示例。现在就为用户定义 各种常用颜色。用户除了可以使用自己创建的颜色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}, <other colors> {NULL, 0, 0, 0} /*结尾*/ }; 接下来要根据这些颜色名来创建相应的全局变量,然后用颜色table来初始化这些变量。最终结果应等价于用户在其脚本中写入如下内容: WHITE = {r = 1, g = 1, b = 1} RED = {r = 1, g = 0, b = 0} <其他颜色> 定义一个辅助函数setfield来设置table字段。它会将字段名和字段值压入栈中。然后调用lua_settable: /*假设table位于栈顶*/ void setfield (lua_State *L, const char *index, int value) { lua_pushstring(L, index); lua_pushnumber(L, (double)value / MAX_COLOR); lua_settable(L, -3); } 就像其他API函数一样,lua_settable能处理各种类型,它会从栈中获取所需的操作数。lua_settable要求传入一个table索引参数,然后它会设置这个table,并弹出key和value。setfield函数假设在调用前table已经在栈顶(索引为-1)。当压入key和value后,table就位于索引-3。 Lua 5.1同样为字符串key提供了一个lua_settable的特化版本,名为 lua_setfield。通过这个函数,可以将上述的setfield的定义重写为: void setfield (lua_State *L, const char *index, int value) { lua_pushnumber(L, (double)value / MAX_COLOR); lua_setfield(L, -2, index); } 接下来的一个函数是setcolor,它用于定义一个颜色。它会创建一个table,并设置相应的字段,最后将这个table赋予相应的全局变量: void setcolor (lua_State *L, struct ColorTable *ct) { lua_newtable(L); /*创建一个table*/ setfield(L, "r", ct->red); /*table.r = ct->r*/ setfield(L, "g", ct->green); setfield(L, "b", ct->blue); lua_setglobal(L, ct->name); /*'name' = table*/ } setcolor先调用lua_newtable,这个函数会创建一个空的table,并将其压入栈中。然后,setcolor调用setfield来设置table的各个字段。最后,lua_setglobal弹出table,并根据名称将其赋予全局变量。 通过上述函数,下面这个循环便会为配置脚本注册所有的颜色: int i = 0; while (colortable[i].name != NULL) { setcolor(L, &colortable[i++]; } 记住,应用程序必须在运行脚本前,执行这个循环。 下面是另一种实现“具名(Named)”颜色的做法。 lua_getglobal(L, "background"); if (lua_isstring(L, -1)) { const char *colorname = lua_tostring(L, -1); int i; for (i = 0; colortable[i].name != NULL; ++i) { if (strcmp(colorname, colortable[i].name) == 0) { break; } if (colortable[i].name == NULL) { error(L, "invalid color name (%s)", colorname); } else { 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又可以是字符串。若以这种方式来实现,应用程序则无须在运行用户脚本前做任何事情。不过,它需要在获取颜色时做更多的事情。当它获取变量background的值时,必须测试该值是否为合法的字符串,这需要在颜色表中查找该字符串。 在C程序中,用字符串来表示选项并不是一个好方法,因为编译器无法检测到 拼写错误。在Lua中,全局变量无须声明,因此若用户错误地拼写了一个颜色,Lua也不会报错误。如果用户写了WITE,而非WHITE, background变量会变成 nil。而应用程序却只知道background是nil,除此之外没有其他信息可以说明错误的原因。另一方面,使用字符串时,若background的值拼写错误,则应用程序可以将这个信息附加到错误消息中,还可以用大小写无关的方式来比较字符串, 如用户可以写“ white”、“WHITE”或“White”。此外,如果用户脚本很小,而颜色很多,那么就需要注册许多颜色,但只有其中一些会被用户用到。在这种情况下,使用字符串方式可以避免 这种开销。 调用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函数来调用这个Lua函数: /*调用Lua中定义的函数‘f'*/ double f (double x, double y) { double z; /*压入函数和参数*/ lua_getglobal(L, "f"); //待调用的函数 lua_pushnumber(L, x); //压入第一个参数 lua_pushnumber(L, y); //压入第二个参数 /*完成调用(2个参数,1个结果)*/ if (lua_pcall(L, 2, 1, 0) != 0) { error(L, "error running function 'f' : %s", lua_tostring(L, -1)); } /*检测结果*/ if (!lua_isnumber(L, -1)) { error(L, "function 'f' must return a number"); } z = lua_tonumber(L, -1); lua_pop(L, 1); return z; } 在调用lua_pcall时,第二个参数是传给待调用函数的参数数量,第三个参数是期望的结果数量,第四个参数是一个错误处理函数的索引。就像Lua的赋值一样,lua_pcall会根据要求的数量来调整实际结果的数量,即压入nil或丢弃多余的结果。在压入结果前, lua_pcall会先 删除栈中的函数以及其参数。如果一个函数会返回多个结果,那么第一个结果最先压入。例如,函数返回了3个结果,第一个的索引就是-3,最后一个的索引是-1。 如果在 lua_pcall的运行过程中有任何错误,lua_pcall会返回一个非零值,并在栈中压入一条错误消息。不过即使如此,它仍会弹出函数以及其参数。然而,在压入错误消息前,如果存在一个错误处理函数,lua_pcall就会先调用它。通过lua_pcall的最后一个参数可以指定这个错误处理函数。零表示没有错误处理函数,那么 最终的错误消息就是原来的消息。若传入非零参数,那么这个参数就应该是一个错误处理函数在栈中索引。因此,错误处理函数必须先压入栈中,也就是必须位于 待调用函数以及其参数的下面。 对于普通的错误,lua_pcall会返回错误代码LUA_ERRRUN。但有两种特殊的错误情况,不会运行错误处理函数。第一种是内存分配错误,对于这种错误,lua_pcall总是返回 LUA_ERRMEM。第二类错误则发生在Lua运行错误处理函数时,在这种情况中,是没有必要再次调用错误处理函数的,因此lua_pcall会立即返回错误代码LUA_ERRERR。 一个通用的调用函数 本例作为一个更高级的示例,将编写一个调用Lua函数的辅助函数,其中用到了C语言的可变参数机制。这个辅助函数称为call_va,它接受一个待调用函数的名字、一个描述参数类型和结果类型的字符串,以及所有的参数变量和所有存放结果的指针。call_va会处理所有的API细节。通过这个函数,可以将前例写为: call_va("f", "dd>d", x, y, &z); 其中字符串“dd>d"表示”两个双精度类型的参数和一个双精度类型的结果“。在这段描述字符串中,可以用字母‘d'表示双精度浮点数、’i'表示整数、‘s'表示字符串,而’>‘表示参数与结果的分隔符。如果函数没有结果,’>'便是可选的。 以下是call_va的实现,这个函数执行了与第一示例中相同的步骤:压入函数、压入参数、完成调用和获取结果。 #include <stdarg.h> void call_va (const char *func, const char *sig, ...) { va_list vl; int narg, nres; //参数和结果的数量 va_start(vl, sig); lua_getglobal(L, func); //压入函数 //压入参数 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': { if (!lua_isnumber(L, nres)) { error(L, "wrong result type"); } *va_arg(vl, double *) = lua_tonumber(L, nres); break; } case 'i': { if (!lua_isnumber(L, nres)) { error(L, "wrong result type"); } *va_arg(vl, int *) = lua_tointeger(L, nres); break; } case 's': { if (!lua_isstring(L, nres)) { error(L, "wrong result type"); } *va_arg(vl, const car **) = lua_tostring(L, nres); break; } default: { error(L, "invalid option (%c)", *(sig - 1)); } } ++nres; } va_end(vl); } 以上大部分代码都很直观,不过有些 地方需要说明一下。首先,无须检查func是否为一个函数,因为lua_pcall会发现这类错误。其次,由于它要压入任意数量的参数,因此必须确保栈中有足够的空间。第三,由于函数可能会返回字符串,因此call_va不能将结果弹出栈。调用者必须在使用完字符串结果(或将字符串复制到其他缓冲)后弹出所有结果。
转载请注明原文地址: https://www.6miu.com/read-1969.html

最新回复(0)