使用 Lua 编写可嵌入式脚本
2006 年 6 月 12 日
虽然编译性编程语言和脚本语言各自具有自己独特的优点,但是如果我们使用这两种类型的语言来编写大型的应用程序会是什么样子呢?Lua 是一种嵌入式脚本语言,它非常小,速度很快,功能却非常强大。在创建其他配置文件或资源格式(以及与之对应的解析器)之前,请尝试一下 Lua。
尽管诸如 Perl、Python、PHP 和 Ruby 之类的解释性编程语言日益被 Web 应用程序广泛地采纳 —— 它们已经长期用来实现自动化系统管理任务 —— 但是诸如 C、C++ 之类的编译性编程语言依然是必需的。编译性编程语言的性能是脚本语言所无法企及的(只有手工调优的汇编程序的性能才能超过它),有些软件 —— 包括操作系统和设备驱动程序 —— 只能使用编译代码来高效地实现。实际上,当软件和硬件需要进行无缝地连接操作时,程序员本能地就会选择 C 编译器:C 非常基础,距离 “原始金属材料非常近” —— 即可以操作硬件的很多特性 —— 并且 C 的表现力非常强大,可以提供高级编程结构,例如结构、循环、命名变量和作用域。
然而,脚本语言也有自己独特的优点。例如,当某种语言的解释器被成功移植到一种平台上以后,使用这种语言编写的大量脚本就可以不加任何修改在这种新平台上运行 —— 它们没有诸如系统特定的函数库之类的依赖限制。(我们可以考虑一下 Microsoft® Windows® 操作系统上的许多 DLL 文件和 UNIX® 及 Linux® 上的很多 libcs)。另外,脚本语言通常都还会提供高级编程构造和便利的操作,程序员可以使用这些功能来提高生产效率和灵活性。另外,使用解释语言来编程的程序员工作的速度更快,因为这不需要编译和链接的步骤。C 及其类似语言中的 “编码、编译、链接、运行” 周期缩减成了更为紧凑的 “编写脚本、运行”。
Lua 新特性
与其他脚本语言一样,Lua 也有自己的一些特性:
- Lua 类型。在 Lua 中,值可以有类型,但是变量的类型都是动态决定的。nil、布尔型、数字 和 字符串 类型的工作方式与我们期望的一样。
- Nil 是值为
nil
的一种特殊类型,用来表示没有值。 - 布尔型的值可以是
true
和false
常量。(Nil
也可以表示 false,任何非nil
的值都表示 true。) - Lua 中所有的数字都是双精度的(不过我们可以非常简便地编写一些代码来实现其他数字类型)。
- 字符串是定长字符数组。(因此,要在一个字符串后面附加上字符,必须对其进行拷贝。)
- Nil 是值为
- 表、函数 和线程 类型都是引用。每个都可以赋值给一个变量,作为参数传递,或作为返回值从函数中返回。例如,下面是一个存储函数的例子:
-- example of an anonymous function
-- returned as a value
-- see http://www.tecgraf.puc-rio.br/~lhf/ftp/doc/hopl.pdf
function add(x)
return function (y) return (x + y) end
end
f = add(2)
print(type(f), f(10))
function 12 |
- Lua 线程。线程是通过调用内嵌函数
coroutine.create(f)
创建的一个协同例程 (co-routine),其中 f 是一个 Lua 函数。线程不会在创建时启动;相反,它是在创建之后使用coroutine.resume(t)
启动的,其中 t 就是一个线程。每个协同例程都必须使用coroutine.yield()
偶尔获得其他协同例程的处理器。 - 赋值语句。Lua 允许使用多种赋值语句,可以先对表达式进行求值,然后再进行赋值。例如,下面的语句:
i = 3
a = {1, 3, 5, 7, 9}
i, a[i], a[i+1], b = i+1, a[i+1], a[i]
print (i, a[3], a[4], b, I)
|
会生成 4 7 5 nil nil
。如果变量列表的个数大于值列表的个数,那么多出的变量都被赋值为 nil
;因此,b
就是 nil。如果值的个数多于变量的个数,那么多出的值部分就会简单地丢弃。在 Lua 中,变量名是大小写敏感的,这可以解释为什么 I
的值是 nil。
- 块(Chunk)。 块 可以是任何 Lua 语句序列。块可以保存到文件中,或者保存到 Lua 程序中的字符串中。每个块都是作为一个匿名函数体来执行的。因此,块可以定义局部变量和返回值。
- 更酷的东西。Lua 具有一个标记-清理垃圾收集器。在 Lua 5.1 中,垃圾收集器是以增量方式工作的。Lua 具有完整的词法闭包(这与 Scheme 类似,而与 Python 不同)。Lua 具有可靠的尾部调用语义(同样,这也与 Scheme 类似,而与 Python 不同)。
在 Programming in Lua 和 Lua-users wiki (链接请参见后面的 参考资料 部分)中可以找到更多 Lua 代码的例子。
在所有的工程任务中,要在编译性语言和解释性语言之间作出选择,就意味着要在这种环境中对每种语言的优缺点、权重和折中进行评测,并接受所带来的风险。
在两个世界之间最好地进行混合
如果您希望充分利用这两个世界的优点,应该怎样办呢,是选择最好的性能还是选择高级强大的抽象?更进一步说,如果我们希望对处理器密集且依赖于系统的算法和函数以及与系统无关且很容易根据需要而进行修改的单独逻辑进行优化,那又当如何呢?
对高性能代码和高级编程的需要进行平衡是 Lua(一种可嵌入式脚本语言)要解决的问题。在需要时我们可以使用编译后的代码来实现底层的功能,然后调用 Lua 脚本来操作复杂的数据。由于 Lua 脚本是与编译代码独立的,因此我们可以单独修改这些脚本。使用 Lua,开发周期就非常类似于 “编码、编译、运行、编写脚本、编写脚本、编写脚本 ...”。
例如,Lua Web 站点 “使用” 页面(请参见 参考资料)列出了主流市场上的几个计算机游戏,包括 World of Warcraft 和(家用版的)Defender,它们集成 Lua 来实现很多东西,从用户界面到敌人的人工智能都可以。Lua 的其他应用程序包括流行的 Linux 软件更新工具 apt-rpm 的扩展机制,还有 “Crazy Ivan” Robocup 2000 冠军联赛的控制逻辑。这个页面上的很多推荐感言都对 Lua 的小巧与杰出性能赞不绝口。
开始使用 Lua
Lu a 5.0.2 版本是撰写本文时的最新版本,不过最近刚刚发布了 5.1 版本。您可以从 lua.org 上下载 Lua 的源代码,在 Lua-users wiki(链接请参见 参考资料)上可以找到预先编译好的二进制文件。完整的 Lua 5.0.2 核心文件中包括了标准库和 Lua 编译器,不过只有 200KB 大小。
如果您使用的是 Debian Linux,那么可以以超级用户的身份运行下面的命令来快速安装 Lua 5.0:
# apt-get install lua50
|
本文中给出的例子都是在 Debian Linux Sarge 上运行的,使用的是 Lu a 5.0.2 和 2.4. 27-2-686 版本的 Linux 内核。
在系统上安装好 Lua 之后,我们可以首先来试用一下单独的 Lua 解释器。(所有的 Lua 应用程序必须要嵌入到宿主应用程序中。解释器只是一种特殊类型的宿主,对于开发和调试工作来说非常有用。)创建一个名为 factorial.lua 的文件,然后输入下面的代码:
-- defines a factorial function
function fact (n)
if n == 0 then
return 1
else
return n * fact(n-1)
end
end
print("enter a number:")
a = io.read("*number")
print(fact(a))
|
factorial.lua 中的代码 —— 更确切地说是任何 Lua 语句序列 —— 都称为一个块,这在上面的 Lua 特性 中已经进行了介绍。要执行刚才创建的代码块,请运行命令 lua factorial.lua
:
$ lua factorial.lua
enter a number:
10 3628800
|
或者像在其他解释性语言中一样,我们可以在代码顶部添加一行 “标识符”(#!
),使这个脚本变成可执行的,然后像单独命令一样来运行这个文件:
$ (echo '#! /usr/bin/lua'; cat factorial.lua) > factorial
$ chmod u+x factorial
$ ./factorial
enter a number:
4 24
|
Lua 语言
Lua 具有现代脚本语言中的很多便利:作用域,控制结构,迭代器,以及一组用来处理字符串、产生及收集数据和执行数学计算操作的标准库。在 Lua 5.0 Reference Manual 中有对 Lua 语言的完整介绍(请参见 参考资料)。
在 Lua 中,只有值 具有类型,而变量的类型是动态决定的。Lua 中的基本类型(值)有 8 种: nil,布尔型,数字,字符串,函数,线程,表 以及 用户数据。前 6 种类型基本上是自描述的(例外情况请参见上面的 Lua 特性 一节);最后两个需要一点解释。
Lua 表
在 Lua 中,表是用来保存所有数据的结构。实际上,表是 Lua 中惟一的 数据结构。我们可以将表作为数组、字典(也称为散列 或联合数组)、树、记录,等等。
与其他编程语言不同,Lua 表的概念不需要是异构的:表可以包含任何类型的组合,也可以包含类数组元素和类字典元素的混合体。另外,任何 Lua 值 —— 包括函数或其他表 —— 都可以用作字典元素的键值。
要对表进行浏览,请启动 Lua 解释器,并输入清单 1 中的黑体显示的代码。
清单 1. 体验 Lua 表
$ lua
> -- create an empty table and add some elements
> t1 = {}
> t1[1] = "moustache"
> t1[2] = 3
> t1["brothers"] = true
> -- more commonly, create the table and define elements
> all at once
> t2 = {[1] = "groucho", [3] = " chico ", [5] = "harpo"}
> t3 = {[t1[1]] = t2[1], accent = t2[3], horn = t2[5]}
> t4 = {}
> t4[t3] = "the marx brothers"
> t5 = {characters = t2, marks = t3}
> t6 = {["a night at the opera"] = "classic"}
> -- make a reference and a string
> i = t3
> s = "a night at the opera"
> -- indices can be any Lua value
> print(t1[1], t4[t3], t6[s])
moustache the marx brothers classic
> -- the phrase table.string is the same as table["string"]
> print(t3.horn, t3["horn"])
harpo harpo
> -- indices can also be "multi-dimensional"
> print (t5["marks"]["horn"], t5.marks.horn)
harpo harpo
> -- i points to the same table as t3
> = t4[i]
the marx brothers
> -- non-existent indices return nil values
> print(t1[2], t2[2], t5.films)
nil nil nil
> -- even a function can be a key
> t = {}
> function t.add(i,j)
>> return(i+j)
>> end
> print(t.add(1,2))
3
> print(t['add'](1,2))
3
> -- and another variation of a function as a key
> t = {}
> function v(x)
>> print(x)
>> end
> t[v] = "The Big Store"
> for key,value in t do key(value) end
The Big Store
|
正如我们可能期望的一样,Lua 还提供了很多迭代器函数来对表进行处理。全局变量 table
提供了这些函数(是的,Lua 包就是表)。有些函数,例如 table.foreachi()
,会期望一个从 1(数字 1)开始的连续整数范围:
> table.foreachi(t1, print)
1 moustache
2 3
|
另外一些函数,例如 table.foreach()
,会对整个表进行迭代:
> table.foreach(t2,print)
1 groucho
3
chico
5 harpo
> table.foreach(t1,print)
1 moustache
2 3
brothers true
|
尽管有些迭代器对整数索引进行了优化,但是所有迭代器都只简单地处理 (key, value) 对。
现在我们可以创建一个表 t
,其元素是 {2, 4, 6, language="Lua", version="5", 8, 10, 12, web="www.lua.org"}
,然后运行 table.foreach(t, print)
和 table.foreachi(t, print)
。
用户数据
由于 Lua 是为了嵌入到使用另外一种语言(例如 C 或 C++)编写的宿主应用程序中,并与宿主应用程序协同工作,因此数据可以在 C 环境和 Lua 之间进行共享。正如 Lua 5.0 Reference Manual 所说,userdata
类型允许我们在 Lua 变量中保存任意的 C 数据。我们可以认为 userdata
就是一个字节数组 —— 字节可以表示指针、结构或宿主应用程序中的文件。
用户数据的内容源自于 C,因此在 Lua 中不能对其进行修改。当然,由于用户数据源自于 C,因此在 Lua 中也没有对用户数据预定义操作。不过我们可以使用另外一种 Lua 机制来创建对 userdata
进行处理的操作,这种机制称为 元表(metatable)。
元表
由于表和用户数据都非常灵活,因此 Lua 允许我们重载这两种类型的数据的操作(不能重载其他 6 种类型)。元表 是一个(普通的)Lua 表,它将标准操作映射成我们提供的函数。元表的键值称为事件;值(换而言之就是函数)称为元方法。
函数 setmetatable()
和 getmetatable()
分别对对象的元表进行修改和查询。每个表和 userdada
对象都可以具有自己的元表。
例如,添加操作对应的事件是 __add
。我们可以推断这段代码所做的事情么?
-- Overload the add operation
-- to do string concatenation
--
mt = {}
function String(string)
return setmetatable({value = string or ''}, mt)
end
-- The first operand is a String table
-- The second operand is a string
-- .. is the Lua concatenate operator
--
function mt.__add(a, b)
return String(a.value..b)
end
s = String('Hello')
print((s + ' There ' + ' World!').value )
|
这段代码会产生下面的文本:
Hello There World!
|
函数 String()
接收一个字符串 string
,将其封装到一个表({value = s or ''}
)中,并将元表 mt
赋值给这个表。函数 mt.__add()
是一个元方法,它将字符串 b
添加到在 a.value
中找到的字符串后面 b
次。这行代码 print((s + ' There ' + ' World!').value )
调用这个元方法两次。
__index
是另外一个事件。__index
的元方法每当表中不存在键值时就会被调用。下面是一个例子,它记住 (memoize) 函数的值:
-- code courtesy of
Rici
Lake
, [email protected]
function Memoize(func, t)
return setmetatable(
t or {},
{__index =
function(t, k)
local v = func(k);
t[k] = v;
return v;
end
}
)
end
COLORS = {"red", "blue", "green", "yellow", "black"}
color = Memoize(
function(node)
return COLORS[math.random(1, table.getn(COLORS))]
end
)
|
将这段代码放到 Lua 解释器中,然后输入 print(color[1], color[2], color[1])
。您将会看到类似于 blue black blue
的内容。
这段代码接收一个键值 node
,查找 node 指定的颜色。如果这种颜色不存在,代码就会给 node
赋一个新的随机选择的颜色。否则,就返回赋给 node 的颜色。在前一种情况中,__index
元方法被执行一次以分配一个颜色。后一种情况比较简单,所执行的是快速散列查找。
Lua 语言提供了很多其他功能强大的特性,所有这些特性都有很好的文档进行介绍。在碰到问题或希望与专家进行交谈时,请连接 Lua Users Chat Room IRC Channel(请参见 参考资料)获得非常热心的支持。
嵌入和扩展
除了语法简单并且具有功能强大的表结构之外,Lua 的强大功能使其可以与宿主语言混合使用。由于 Lua 与宿主语言的关系非常密切,因此 Lua 脚本可以对宿主语言的功能进行扩充。但是这种融合是双赢的:宿主语言同时也可以对 Lua 进行扩充。举例来说,C 函数可以调用 Lua 函数,反之亦然。
Lua 与宿主语言之间的这种共生关系的核心是宿主语言是一个虚拟堆栈。虚拟堆栈与实际堆栈类似,是一种后进先出(LIFO)的数据结构,可以用来临时存储函数参数和函数结果。要从 Lua 中调用宿主语言的函数(反之亦然),调用者会将一些值压入堆栈中,并调用目标函数;被调用的函数会弹出这些参数(当然要对类型和每个参数的值进行验证),对数据进行处理,然后将结果放入堆栈中。当控制返回给调用程序时,调用程序就可以从堆栈中提取出返回值。
实际上在 Lua 中使用的所有的 C 应用程序编程接口(API)都是通过堆栈来进行操作的。堆栈可以保存 Lua 的值,不过值的类型必须是调用程序和被调用者都知道的,特别是向堆栈中压入的值和从堆栈中弹出的值更是如此(例如 lua_pushnil()
和 lua_pushnumber()
。
清单 2 给出了一个简单的 C 程序(节选自 参考资料 中 Programming in Lua 一书的第 24 章),它实现了一个很小但却功能完善的 Lua 解释器。
清单 2. 一个简单的 Lua 解释器
1 #include <stdio.h>
2 #include <lua.h>
3 #include <lauxlib.h>
4 #include <lualib.h>
5
6 int main (void) {
7 char buff[256];
8 int error;
9 lua_State *L = lua_open(); /* opens Lua */
10 luaopen_base(L); /* opens the basic library */
11 luaopen_table(L); /* opens the table library */
12 luaopen_io(L); /* opens the I/O library */
13 luaopen_string(L); /* opens the string lib. */
14 luaopen_math(L); /* opens the math lib. */
15
16 while (fgets(buff, sizeof(buff), stdin) != NULL) {
17 error = luaL_loadbuffer(L, buff, strlen(buff), "line") ||
18 lua_pcall(L, 0, 0, 0);
19 if (error) {
20 fprintf(stderr, "%s", lua_tostring(L, -1));
21 lua_pop(L, 1); /* pop error message from the stack */
22 }
23 }
24
25 lua_close(L);
26 return 0;
27 }
|
第 2 行到第 4 行包括了 Lua 的标准函数,几个在所有 Lua 库中都会使用的方便函数以及用来打开库的函数。第 9 行创建了一个 Lua 状态。所有的状态最初都是空的;我们可以使用 luaopen_...()
将函数库添加到状态中,如第 10 行到第 14 行所示。
第 17 行和 luaL_loadbuffer()
会从 stdin
中以块的形式接收输入,并对其进行编译,然后将其放入虚拟堆栈中。第 18 行从堆栈中弹出数据并执行之。如果在执行时出现了错误,就向堆栈中压入一个 Lua 字符串。第 20 行访问栈顶(栈顶的索引为 -1
)作为 Lua 字符串,打印消息,然后从堆栈中删除该值。
使用 C API,我们的应用程序也可以进入 Lua 状态来提取信息。下面的代码片段从 Lua 状态中提取两个全局变量:
..
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");
..
width = (int) lua_tonumber(L, -2);
height = (int) lua_tonumber(L, -1);
..
|
请再次注意传输是通过堆栈进行的。从 C 中调用任何 Lua 函数与这段代码类似:使用 lua_getglobal()
来获得函数,将参数压入堆栈,调用 lua_pcall()
,然后处理结果。如果 Lua 函数返回 n 个值,那么第一个值的位置在堆栈的 -n 处,最后一个值在堆栈中的位置是 -1。
反之,在 Lua 中调用 C 函数也与之类似。如果您的操作系统支持动态加载,那么 Lua 可以根据需要来动态加载并调用函数。(在必须使用静态加载的操作系统中,可以对 Lua 引擎进行扩充,此时调用 C 函数时需要重新编译 Lua。)
结束语
Lua 是一种学习起来容易得难以置信的语言,但是它简单的语法却掩饰不了其强大的功能:这种语言支持对象(这与 Perl 类似),元表使表类型具有相当程度的可伸展性,C API 允许我们在脚本和宿主语言之间进行更好的集成和扩充。Lua 可以在 C、C++、C#、Java™ 和 Python 语言中使用。
在创建另外一个配置文件或资源格式(以及相应的处理程序)之前,请尝试一下 Lua。Lua 语言及其社区非常健壮,具有创新精神,随时准备好提供帮助。