第24章扩展你的程序
作为配置语言是 LUA的一个重要应用。在这个章节里,我们举例说明如何用 LUA 设 置一个程序。让我们用一个简单的例子开始然后展开到更复杂的应用中。
首先,让我们想象一下一个简单的配置情节:你的 C程序(程序名为 PP)有一个 窗口界面并且可以让用户指定窗口的初始大小。显然,类似这样简单的应用,有多种解决方法比使用LUA更简单,比如环境变量或者存有变量值的文件。但,即使是用一个 简单的文本文件,你也不知道如何去解析。所以,最后决定采用一个 LUA 配置文件(这就是 LUA 程序中的纯文本文件)。在这种简单的文本形式中通常包含类似如下的信息行:
-- configuration filefor program 'pp' -- define windowsize width = 200 height = 300
#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 包并加载了标准函数库C虽然这是可选的,但通常包含这些 库是比较好的编程思想)。然后程序使用 luaL_loadfile 方法根据参数 filename 加载此文件 中的信息块并调用 lua_pcall 函数运行,这些函数运行时若发生错误(例如配置文件中有 语法错误),将返回非零的错误代码并将此错误信息压入栈中。通常,我们用带参数 index 值为-1 的 lua_tostring 函数取得栈顶元素。
解析完 取得 的信息 块后 ,程序会取得全局变量值。为此,程序调用了两次 lua_getglobal 函数,其中一参数为变量名称。每调用一次就把相应的变量值压入栈顶,所以变量 width的 index 值是-2 而变量 height 的 index 值是-1(在栈顶)。(因为先前的栈是空的,需要从栈底重新索引,1 表示第一个元素 2表示第二个元素。由于从栈顶索引, 不管栈是否为空,你的代码也能运行)。接着,程序用 lua_isnumber
函数判断每个值是否 为数字。lua_tonumber 函数将得到的数值转换成 double 类型并用(int)强制转换成整型。 最后,关闭数据流并返回值。
Lua 是否值得一用?正如我前面提到的,在这个简单的例子中,相比较于 lua用一个 只包含有两个数字的文件会更简单。即使如此,使用 lua 也带来了一些优势。首先,它 为你处理所有的语法细节(包括错误);你的配置文件甚至可以包含注释!其次,用可以 用 lua 做更多复杂的配置。例如,脚本可以向用户提示相关信息,或者也可以查询环境 变量以选择合适的大小:
-- configuration filefor program 'pp' if getenv("DISPLAY") == ":0.0" then width = 300; height= 300 else width = 200; height= 200 end在这样简单的配置情节中,很难预料用户想要什么;不过只要脚本定义了这两个变 量,你的 C 程序无需改变就可运行。最后一个使用 lua 的理由:在你的程序中很容易的加入新的配置单元。方便的属性 添加使程序更具有扩展性。
24.1表操作
现在,我们打算使用 Lua 作为配置文件,配置窗口的背景颜色。我们假定最终的颜色有三个数字(RGB)描述,每一个数字代表颜色的一部分。通常,在 C 语言中,这些数字使用[0,255]范围内的整数表示,由于在 Lua中所有数字都是实数,我们可以使用更自然的范围[0,1]来表示。
一个粗糙的解决方法是,对每一个颜色组件使用一个全局变量表示,让用户来配置 这些变量:
-- configuration filefor program 'pp' width = 200 height = 300 background_red =0.30 background_green =0.10 background_blue = 0
background ={r=0.30, g=0.10, b=0}表的使用给脚本的结构带来很多灵活性,现在对于用户C或者应用程序)很容易预 定义一些颜色,以便将来在配置中使用:
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 validcolor table"); red = getfield("r"); green = getfield("g"); blue =getfield("b");
#define MAX_COLOR 255 /* assume thattable is on the stacktop */ 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; }
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 thattable 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_settable 以 table 在栈中的索引作为参数,并将栈中的 key和 value 出栈,用这两个值修改 table。Setfield 函数假定调用之前 table 是在栈顶位置(索引为-1)。 将 index 和 value入栈之后,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 */ }
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 误写成 WITE, background 变量将为 nil(WITE 的值没有初始化),然后应用程序就认为 background 的值 为 nil。没有其他关于这个错误的信息可以获得。另一方面,使用字符串表示,background的值也可能是拼写错了的字符串。因此,应用程序可以在发生错误的时候,定制输出的 错误信息。应用可以不区分大小写比较字符串,因此,用户可以写"white","WHITE", 甚至"White"。但是,如果用户脚本很小,并且颜色种类比较多,注册成百上千个颜色(需 要创建成百上千个 table 和全局变量),最终用户可能只是用其中几个,这会让人觉得很怪异。在使用字符串表示的时候,应避免这种情况出现。
function f (x, y) return (x^2 * math.sin(y))/(1 - x) end并且我们想在 C 中对于给定的 x,y 计算 z=f(x,y)的值。假如你己经打开了 lua 库并且 运行了配置文件,你可以将这个调用封装成下面的 C 函数:
<pre name="code" class="csharp">/* 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; }
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 ofarguments 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 thecall */ nres =strlen(sig); /* numberof expected results*/ if (lua_pcall(L, narg, nres,0) != 0) /* do thecall */ error(L, "error runningfunction `%s': %s",func, lua_tostring(L, -1)); /* retrieve results*/ nres =-nres; /* stackindex of firstresult */ 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, constchar **) = lua_tostring(L, nres); break; default: error(L, "invalid option (%c)", *(sig- 1)); } nres++; } va_end(vl); }
尽管这段代码具有一般性,这个函数和前面我们的例子有相同的步骤:将函数入栈, 参数入栈,调用函数,获取返回结果。大部分代码都很直观,但也有一点技巧。首先,不需要检查 func是否是一个函数,lua_pcall可以捕捉这个错误。第二,可以接受任意多个参数,所以必须检查栈的空间。第三,因为函数可能返回字符串,call_va 不能从栈中 弹出结果,在调用者获取临时字符串的结果之后(拷贝到其他的变量中),由调用者负责 弹出结果。
原文:http://blog.csdn.net/heyuchang666/article/details/51241052