Lua_第24章 扩展你的程序

 第24章扩展你的程序
 
 
       作为配置语言是 LUA的一个重要应用。在这个章节里,我们举例说明如何用 LUA 设 置一个程序。让我们用一个简单的例子开始然后展开到更复杂的应用中。
       首先,让我们想象一下一个简单的配置情节:你的 C程序(程序名为 PP)有一个 窗口界面并且可以让用户指定窗口的初始大小。显然,类似这样简单的应用,有多种解决方法比使用LUA更简单,比如环境变量或者存有变量值的文件。但,即使是用一个 简单的文本文件,你也不知道如何去解析。所以,最后决定采用一个 LUA 配置文件(这就是 LUA 程序中的纯文本文件)。在这种简单的文本形式中通常包含类似如下的信息行:
 

-- configuration filefor program 'pp'
-- define windowsize
width = 200
height = 300


现在,你得调用 LUA  API 函数去解析这个文件,取得 width 和 height这两个全局变量的值。下面这个取值函数就起这样的作用:
 

#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

       这个方法有两个缺点:第一,太冗余(为了表示窗口的背景,窗口的前景,菜单的 背景等,一个实际的应用程序可能需要几十个不同的颜色);第二,没有办法预定义共同 部分的颜色,比如,假如我们事先定义了WHITE,用户可以简单的写 background= WHITE来表示所有的背景色为白色。为了避免这些缺点,我们使用一个 table 来表示颜色:
 
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");


一般来说,我们首先获取全局变量 backgroud 的值,并保证它是一个 table。然后, 我们使用 getfield 函数获取每一个颜色组件。这个函数不是 API 的一部分,我们需要自己定义他:
 
#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;
}

       这里我们再次面对多态的问题:可能存在很多个 getfield 的版本,key 的类型,value 的类型,错误处理等都不尽相同。Lua API 只提供了一个 lua_gettable 函数,他接受 table 在栈中的位置为参数,将对应 key 值出栈,返回与 key 对应的 value。我们上面的 getfield函数假定 table 在栈顶,因此,lua_pushstring 将 key入栈之后,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 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 */
}

       lua_newtable 函数创建一个新的空 table 然后将其入栈,调用 setfield 设置 table 的域, 最后 lua_setglobal将 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 和全局变量),最终用户可能只是用其中几个,这会让人觉得很怪异。在使用字符串表示的时候,应避免这种情况出现。
 
24.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  函数:
 
<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;
}

 
       可以调用 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 通用的函数调用 
  
       看一个稍微高级的例子,我们使用 C的 vararg来封装对 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 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 不能从栈中 弹出结果,在调用者获取临时字符串的结果之后(拷贝到其他的变量中),由调用者负责 弹出结果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值