内容


在可插入脚本的应用程序中嵌入 Lua

使用专用小型语言嵌入应用程序脚本

Comments

Lua 是一种小型脚本语言。它有多小呢?Lua 使用一个定制模式匹配特性,而不是 POSIX 正则表达式,因为一个完整的正则表达式实现比所有标准的 Lua 库加起来还要大。Lua 提供的字符串匹配要简单得多,它虽然没有 POSIX 那么强大,但大小仅是 POSIX 的一小部分。

Lua 变量不是强类型的;虽然可以检查一个值的类型,但是无法阻止一个变量的类型随着时间而改变。这两点正好适合脚本语言。Lua 的类型系统非常简单,但很灵活。数组和关联数组并合并为一种类型,即 table。string、number(只有浮点数)、boolean 和特殊的 nil 类型都是基本类型。也许更有趣的是,函数(function)也是一种基本类型。就像其他类型一样,可以方便地将函数赋给变量,不需要特殊的语法。另外,Lua 还支持定制的 userdata 对象,开发人员可以通过定义 userdata 对象来处理基本类型以外的其他类型。

对于习惯使用其他语言的程序员来说,Lua 最令人惊讶的地方是,只有 falsenil 被看作 false;任何非 boolean 类型的对象总是被看作 true。对于习惯 C 语言的人来说,这有些奇怪,因为在 C 中就可以用 1 和 0 作为 true 和 false。不过这很容易适应。

Lua 是用可移植的 C 编写的。它还可以与 C++ 一起使用,但是核心语言非常易于移植;虽然有少数特性需要借助宿主特性,但是 Lua 可脱离平台依赖良好地运行。Lua 无需进行大量的 autoconf 测试,因为它严格遵从标准。Lua 是在 MIT 许可下发布的,可完全免费用于任何用途,包括商业用途。(因此很多程序员随意将它嵌入到应用程序中)。

为什么嵌入一种语言?

嵌入一种脚本语言可以带来很多的好处。我将使用初学 Lua 时用的一个例子:Blizzard 的大规模多玩家在线 RPG,World of Warcraft(WoW)。WoW 的用户界面完全使用 Lua 实现的;开发人员提供一些基本的 API 调用,以便真正与呈现引擎交互,并请求关于世界的数据,然后使用 Lua 作为用户界面代码的核心。

这样一来,将用户界面代码置于沙箱中,使之远离游戏本身的干扰,从而提高安全性和可靠性,这一点就变得容易得多。反过来,这又意味着 Blizzard 可以向玩家开放用户界面,使玩家可以定制代码,改变他们与游戏交互的方式。

通常,对于很多类型的任务,与低级语言相比,脚本语言更易于引入到程序中。具有隐式分配和关联数组特性,并支持垃圾收集的语言,通常有助于更快地开发更简单的代码。它也许没那么快,但是在很多情况下,这并不是问题;例如,用户界面只需要比用户的键盘输入或鼠标动作快就行了。

使用脚本语言有几种方式。第一种方式,也是最简单的方式,是使用它控制一个程序的行为,并使用 C 代码作为一个真正用 Lua 编写的程序的实现细节。第二种方法是主要用 C 编写一个程序,然后使用嵌入的 Lua 来存储和报告数据和配置。第三种方式,也是最灵活的方式,是混合上述两种方式,使用 Lua 脚本编写一些动作,而用 C 代码管理其他部分。Lua 与 C 之间的接口非常简单,因此这种方式非常流行。

编写引擎

对于主要用 Lua 编写的程序,如果 CPU 时间主要用于逐个的操作,顶层控制的分量很轻,那么可以编写一个引擎。这有助于将实现细节与高级设计相分离。用 Lua 而不是 C 来实现程序的核心逻辑可以大大减少开发时间。

对于这种类型的设计,显然期望 Lua-to-C 接口主要由将从 Lua 调用的 C 函数的定义组成,并期望一旦开始执行脚本,将来所有对 C 代码的使用都从脚本中调用。

可编写脚本的配置文件

我所知道的每个程序员都至少编写过一段这样的代码,这段代码什么也不做,只是将配置值存储到一个文件中,并在以后恢复那些值。(在我使用过的配置文件当中,Apple 的属性列表也许是我最喜欢的)。但是,可以使用一种嵌入式脚本语言作为这些文件的格式,使用户可以拥有壮观的配置选项阵列。Ion 窗口管理器使用 Lua 作为配置文件,从而使用户可以编写强大而灵活的配置。

对用户来说,最棒的是他们不再局限于简单的赋值;用 Lua 表达的配置文件可以有注释、条件等。可以提供一个优先的 API,用于获取可能影响配置选择的数据。

混合

Lua 与 C 之间可以来回嵌套,因为 Lua 解释器是可重入的(reentrant)。如果 C 程序在一个脚本上调用解释器,该脚本调用一个 C 函数,而这个 C 函数又再次使用 Lua 解释器,这是允许的。

World of Warcraft 基本上就是将这样的模型用于它的用户界面;用户界面中的 Lua 调用可以反过来调用引擎,而引擎又将事件交付到用 Lua 编写的用户界面。这样便得到一个灵活的界面,这样的界面具有良好的隔离性和安全性,为用户提供了很大的自主空间,并且缓冲区溢出或崩溃的风险很小。在基于 C 的 API 中,嵌入的代码几乎都会导致崩溃;当使用 Lua 界面时,如果用户界面代码导致崩溃,则存在需要修复的 bug。

构建和使用 Lua

构建 Lua 很容易;只需运行 make <platform>;如果不想依赖特定于平台的特性,那么可以依靠 posix,甚至是 ansi。构建后得到一个库 liblua.a,可以将它链接到程序。恭喜!您已经嵌入了 Lua。当然,要想真正使用它,还需要做一些工作。

Lua 的可重入性源于将所有解释器状态保存在一个对象中;可以有多个解释器,它们之间不共享变量,也没有共享的全局项。为了与 Lua 交互,必须从创建一个 Lua 状态开始:

lua_State *l;
l = lua_open();

如果 lua_open() 调用失败,则返回一个 null 指针。否则,就有了一个有效的 Lua 解释器状态。当然,如果没有库,还是不够。可以使用 luaL_openlibs() 函数添加标准库:

luaL_openlibs(l);

Lua 状态现在可以执行代码了。下面是一个程序中的一个循环,它只是执行作为 Lua 代码的参数:

清单 1. 将参数作为 Lua 代码执行
  for (i = 1; i < argc; ++i) {
    if (luaL_loadbuffer(l, argv[i], strlen(argv[i]), "argument")) {
      fprintf(stderr, "lua couldn't parse '%s': %s.\n",
		 		 argv[i], lua_tostring(l, -1));
      lua_pop(l, 1);
    } else {
      if (lua_pcall(l, 0, 1, 0)) {
        fprintf(stderr, "lua couldn't execute '%s': %s.\n",
		 		 argv[i], lua_tostring(l, -1));
        lua_pop(l, 1);
      } else {
        lua_pop(l, lua_gettop(l));
      } 
    }
  }

luaL_loadbuffer() 函数将一个脚本编译成 Lua 代码。如果有语法错误,从这里可以观察到失败。错误消息被返回到栈上。否则,使用 lua_pcall() 函数执行编译后的代码。同样,如果有错误,错误消息被返回到栈上。注意,每个对 Lua 或关于 Lua 的 C 语言调用都带有一个 Lua 状态作为一个参数;没有缺省的状态。

理解 Lua 栈

Lua 解释器使用一个栈接口来与调用代码通信。由 C 代码将发送到 Lua 代码的数据 push 到栈上;Lua 解释器返回的响应也被 push 到栈上。如果传递给 luaL_loadbuffer() 的代码有错误,则错误消息被 push 到栈上。

栈上的项有类型和值。lua_type() 函数查询一个对象的类型;lua_to<type>() 函数(例如 lua_tostring())产生被强制转换成特定 C 类型的值。用 Lua 编写的代码总是严格遵从栈模型;但是,C 代码则可以探查栈的其余部分,甚至可以在栈中插入值。

这种接口虽然简单,但是其功能出奇强大。待执行的代码都被同等对待;首先被 push 到栈上,然后等待 lua_pcall() 函数来执行它。

使用 lua_pcall()

除了要在其上面进行操作的 Lua 状态外,lua_pcall() 函数还带有 3 个参数。待执行的代码并非这些参数之一;这个代码由 luaL_loadbuffer() 或获得代码的其他函数 push 到栈上。实际上,lua_pcall() 以传递给要执行的代码的栈参数的数量、期望返回的结果的数量以及一个错误处理程序为参数,最后一个参数是可选的。要调用一个函数,首先要 push 该函数,然后 push 该函数??带的参数(按顺序)。返回的参数以同样的顺序 push。返回的第一个值在下,最后一个值在上。

无论是对于发送参数还是获取结果值,Lua 都自动更正值的数量,以便与传递给 lua_pcall() 的数量匹配;如果没有提供足够的值,那么剩下的参数以 nil 值填充,如果有额外的值,多出的值被自动丢弃。(这与 Lua 在多个赋值操作上的行为是一样的)。

如果提供错误处理程序,那么它应该是用于处理任何发生的错误的 Lua 代码在栈上的索引。对于这篇概述性的文章,我不会详细讨论错误处理;但是要知道两点,首先,错误处理是存在的,其次,错误处理是在 Lua 中进行的。这样非常方便。

将 C 嵌入到 Lua 中

用 C 编写 Lua 使用的函数非常容易。如果您曾经编写过嵌入到其他脚本语言中的代码,那么您也许会感到震惊。下面是一个 C 函数,它接收一个数字 x,返回 x + 1

清单 2. 供 Lua 使用的 C 函数
int
l_ink(lua_State *L) {
        int x;
        if (lua_gettop(L) >= 0) {
                x = (int) lua_tonumber(L, -1);
                lua_pushnumber(L, x + 1);
        }
        return 1;
}

该函数是借助一个 Lua 状态参数来调用的;同样,C 与 Lua 之间的所有交互都是通过 Lua 状态栈发生的。返回值是该函数 push 到栈上的对象的数量。为了使 Lua 可以使用该函数,必须做两件事。首先是创建一个表示该函数的 Lua 对象,其次是为它提供一个名称:

lua_pushcfunction(L, l_ink);
lua_setglobal(L, "ink");

可以使用 lua_pushcfunction() 将一个 C 函数指针转换成一个内部 Lua 对象。当然,这个对象被 push 到栈上。然后,lua_setglobal() 函数将栈顶的值赋给一个有名称的全局变量。由于函数在 Lua 中就是值,所以这实际上就是创建一个回调函数。

栈接口大大简化了这一点;在此,没有必要定义或声明函数所带的参数。对于如何调用代码,Lua 是非常灵活的。不过您如果愿意,也可以进行更仔细的检查。luaL_checknumber() 函数还可以用于检查参数,并且还能打印包含信息的错误消息,以及中断函数的执行。

结束语

将 Lua 嵌入到其他语言(特别是 C)编写的代码中非常简单,因此常常使用 Lua 提高用其他语言编写的程序的功能性。相对于发明自己的配置语言或编写自己的表达式解析器而言,这是一个切实可行的替代方案。

习惯于使用更大型脚本语言的程序员会对一些事情感到惊讶。首先,设置一个 Lua 解析器的成本非常小;如果想在沙箱中运行一些东西,则可以直接这样做。虽然 Lua 的很多安全性特性我还没接触过,但是应该清楚,即使在一个 Lua 状态中都可以有效地实现沙箱。

仅返回关于程序状态的数据的函数可以成为配置脚本和数据的好工具。如果您想开始用 Lua 编写某种顶层的逻辑,那很容易,并且非常有效。很多脚本语言勉强能够与其他语言的代码紧密协作,而 Lua 则是完全为了与其他语言紧密协作而设计的。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux, Open source
ArticleID=379204
ArticleTitle=在可插入脚本的应用程序中嵌入 Lua
publish-date=03302009