首页 > 代码库 > Lua1.1 Lua 的设计和实现 (二)
Lua1.1 Lua 的设计和实现 (二)
(接上篇)
--------------------------------------
实现
--------------------------------------
扩展语言总是由应用程序以某种方式解释执行的。简单的扩展语言可以直接从源代码进行解释执行。另一方面,嵌入式语言通常是强大的编程语言,具有复杂的语法和语义。一个更有效的嵌入式语言实现技术是设计适合语言需求的虚拟机,编译扩展程序成虚拟机的字节码,然后通过解释执行字节码来模拟虚拟机(Betz 1988, 1991; Franks 1991)。我们选择这种混合架构来实现Lua;和直接执行源代码相比,它拥有如下优点:
因为词法和语法解析只进行一次,可能在实际嵌入之前使用外部解析器,识别简单的早期错误,获得更短的开发周期和更快的执行速度;
如果使用一个外部编译器时,可以只提供字节码形式的扩展程序,也就是预编译,从而可以使加载更快,环境更安全,运行时更小(不过,连接几个预编译的扩展程序可能是一项艰巨的任务)。
这种架构率先用于 Smalltalk(Goldberg–Robson 1983; Budd 1987)(字节码就是从它那里借来的术语),也成功用于基于P码(Clark–Koehler 1982)的 UCSD 的 Pascal 系统。在这些系统中,字节码虚拟机被用来减少复杂性并提高可移植性。这个方法也用于移植 BCPL 编译器(Richards–Whitby-Strevens 1980)。
扩展程序的编译器代码可以使用标准工具生成,如 lex 和 yacc (Levine–Mason–Brown 1992)。构造编译器的好工具的存在,并在七十年代末被广泛使用是当时小语言的萌发的主要原因,特别是在Unix环境里。我们实现 Lua 时使用 YACC 进行语法分析。最初,我们使用的 lex 写的词法分析器。通过对生产程序进行性能分析,我们发现,这个模块占用了差不多一半的加载和执行程序的时间。然后,我们直接用 C 重写了这个模块;新的词法分析器的速度超过旧的两倍多。
-------------------
Lua 的虚拟机
-------------------
Lua 中使用的虚拟机是一个堆栈机。这意味着它不具有随机存取存储器:所有的临时值和局部变量保存在栈里。此外,它不具有通用寄存器,只有几个特殊的控制寄存器来控制堆栈和程序的执行。这些寄存器栈底,栈顶和程序计数器(base of stack, top of stack and program counter)。
虚拟机的程序是指令序列,称为字节码。程序的执行是通过解释字节码实现的,每一次指令操作都在栈顶进行。例如,语句
a = b + f(c)
被编译为:
PUSHGLOBAL "b"
PUSHGLOBAL "f"
PUSHMARK
PUSHGLOBAL "c"
CALLFUNC
ADJUST 2
ADD
STOREGLOBAL "a"
Lua的虚拟机有大约有 60 条指令;相应地,能够使用 8 位的字节码进行表示。许多指令(例如,ADD)不需要额外的参数;这些指令直接在栈上运行,并且编译后的代码只占用一个字节。其他指令(例如,PUSHGLOBAL 和 STOREGLOBAL)需要额外的参数,需要超过一个字节的占用。因为参数可以采用一个,两个或四个字节,这在某些体系架构上造成了字节对齐问题,不过可以通过填充空(NOP)指令来解决边界对齐的问题。
许多指令只是为了优化而存在。例如,有一种 PUSH 指令,需要一个数字作为参数并将其压栈,但也有单字节优化版本用于常用值的压栈,例如 0 和 1。因此,我们有 PUSHNIL,PUSH0,PUSH2,PUSH3。这样的优化同时减少了字节码的空间占用,和指令执行的时间占用。
回想一下,Lua 支持多重赋值和多个返回值。所以,有时候,值列表必须在运行时调整到给定长度:如果实际值多于所需,那么多余的值会被扔掉;如果需要的值多于实际的,根据需要在列表中进行 nil 扩展。调整通过 ADJUST 指令在栈上完成。
尽管多重赋值和多重返回是 Lua 中的一个强大的功能,便它们同进也是编译器和解释器复杂度的一个重要来源。因为函数没有类型声明,编译器不知道函数会返回多少值。因此,调整必须在运行时完成。同样,编译器不知道函数使用多少参数。因为这个数字在运行时可能会有所不同,在 PUSHMARK 和 CALLFUNC 指令之间参数列表中是相等的。
一种扩展 Lua 使用由主机提供的函数的方法是将每个这样的函数赋值给字节码做为指令(Betz 1988)。虽然这种策略将简化解释器,但它的缺点是只有少于 200 个的外部函数可以添加,因为 Lua 中只有 8 位字节码,并且 Lua 自己已经使用了其中的 60 个做为根本指令。所以我们选择了宿主显式注册外部函数并且
像对待原生的Lua函数一样处理这些外部函数。因此,单一 CALLFUNC 指令就足够了;解释器根据被调用的函数类型决定该做什么。
一个相当不同的策略由 Franks 提出(1991):宿主中的所有外部函数可以被嵌入语言调用;不需要进行显式注册。这是通过阅读和解释由链接器生成的符号表来完成。该解决方案对应用程序员来说是很方便的,但是是不可移植的,依赖于符号表文件的格式和所使用操作系统的重定位策略(Franks 使用了一个特定的 DOS 编译器)。
-------------------
内部数据结构
-------------------
正如前面所提到的,Lua 的变量没有类型;值才有。因此,值由拥有两个字段的结构体(struct)实现:一个类型(type)和一个包含实际值的联合体(union)。这些结构出现在栈和符号表中,符号表(symbol table)持有所有的全局符号。
数值直接存储到联合体中。字符串保存在一个数组中;字符串(string)类型的值是指向该数组指针。函数类型的值是指向字节码数组的指针。类型 Cfunction 值是实际指向宿主程序中 C 函数的指针,用户数据类型(userdata) 的值与之类似。
表(table)被实现为哈希表,由单独的链接处理哈希碰撞(这也就是为什么一个表中索引是任意的原因)。如果在创建表时给出了它的尺寸(size),那么该尺寸就被当作哈希表的大小来用。因此,通过给哈希表提供一个近似等于表中元素数目的尺寸,会减少一些哈希碰撞,从而得到更高效的索引位置。此外,如果表作为数组来用,也就是只有数值下标,在创建表时选择合适的尺寸可以做主保证没有哈希碰撞。
所有的 Lua 内部数据结构都是动态分配的数组。当这些数组中没有更多的空位置(free slots)时,会自动执行垃圾回收,Lua 的垃圾回复算法用的是标记-清除(mark-and-sweep)算法。如果没有空间被回收(由于所有的值都被引用中),则数组会重新分配,尺寸扩大一倍。
垃圾回收为程序员提供了便利,因为它避免了显式的内存管理。当 Lua 作为一个独立的语言(它经常是)来使用时,垃圾回收是很有价值的。然而,当 Lua 嵌入到宿主程序(这是它的主要目的)中使用时,垃圾回收给与 Lua 进行交互的应用程序员带来了新的烦恼:应注意不要把 Lua 中的表和字符串存储到 C 语言变量中,因为这些值可能在垃圾回收过程中被回收,如果在 Lua 中他们没有其它引用的话。更确切地说,程序员必须在控制返回到 Lua 之前明确拷贝这些值到 C 变量中。虽然这是一个不同的模式,但是它至少不比使用标准 C 语言库中的 malloc-free 协议的内存管理差。
--------------------------------------
结论
--------------------------------------
Lua 自 93 年中被广泛应用于生产中,执行以下任务:
应用程序中用户的配置;
通用数据录入,使用用户定义的确认程序;
用户界面的描述;
应用程序对象的编程说明;
存储结构化的图形图元文件,用于图形编辑器和应用程序之间的通信。
此外,Lua 是目前正在考虑的可一个视化编程系统的基础。
对用户和开发人员来说,在运行时加载并执行 Lua 程序使配置变得很简单。此外,通用的嵌入式语言的存在降低了语言的不兼容,并鼓励更好的设计,把应用程序的配置问题和应用程序其它的主要问题清楚的分割开来。
本文中所描述的 Lua 实现可以从匿名的 ftp 中下载:http://www.lua.org/ftp/lua-1.1.tar.gz
-------------------
致谢
-------------------
感谢在 ICAD 和 TeCGraf 工作的全体员工使用和测试 Lua。文中提到正在开发中的工业应用和 PETROBRAS (CENPES) 和 ELETROBRAS (CEPEL) 的研究中心是合作伙伴关系。
--------------------------------------
参考文献
--------------------------------------
M. Abrash, D. Illowsky, "Roll your own minilanguages with mini-interpreters", Dr. Dobb‘s Journal 14 (9) (Sep 1989) 52–72.
A. V. Aho, B. W. Kerninghan, P. J. Weinberger, The AWK programming language, Addison-Wesley, 1988.
B. Beckman, "A Scheme for little languages in interactive graphics", Software, Practice & Experience 21 (1991) 187–207.
J. Bentley, "Programming pearls: little languages", Communications of the ACM 29 (1986) 711–721.
J. Bentley, More programming pearls, Addison-Wesley, 1988.
D. Betz, "Embedded languages", Byte 13 #12 (Nov 1988) 409–416.
D. Betz, "Your own tiny object-oriented language", Dr. Dobb‘s Journal 16 (9) (Sep 1991) 26–33.
T. Budd, A Little Smalltalk, Addison-Wesley, 1987.
R. Clark, S. Koehler, The UCSD Pascal handbook: a reference and guidebook for programmers, Prentice-Hall, 1982.
M. Cowlishaw, The REXX programming language, Prentice-Hall, 1990.
L. H. de Figueiredo, C. S. de Souza, M. Gattass, L. C. G. Coelho, "Geração de interfaces para captura de dados sobre desenhos", Anais do SIBGRAPI V (1992) 169–175 [in Portuguese].
N. Franks, "Adding an extension language to your software", Dr. Dobb‘s Journal 16 (9) (Sep 1991) 34–43.
A. Goldberg, D. Robson, Smalltalk-80: the language and its implementation, Addison-Wesley, 1983.
R. Ierusalimschy, L. H. de Figueiredo, W. Celes Filho, "Reference manual of the programming language Lua", Monografias em Ciência da Computação 4/94, Departamento de Informática, PUC-Rio, 1994.
J. R. Levine, T. Mason, D. Brown, Lex & Yacc, O‘Reilly and Associates, 1992.
C. Nahaboo, A catalog of embedded languages, available from colas@indri.inria.fr.
M. Richards, C. Whitby-Strevens, BCPL: the language and its compiler, Cambridge University Press, 1980.
B. Ryan, "Scripts unbounded", Byte 15 (8) (Aug 1990) 235–240.
R. Valdés, "Little languages, big questions", Dr. Dobb‘s Journal 16 (9) (Sep 1991) 16–25.
Lua1.1 Lua 的设计和实现 (二)