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.算法

相关文章
相关标签/搜索