实现
扩展语言一般都是通过应用程序解释执行的。简单的扩展语言直接从源码解释执行,另一方面嵌入语言是编程语言,拥有复杂的语法(syntax)和语义(semantics)。
嵌入语言一个更有效的实现技术就是设计一个适合语言的虚拟机,将扩展程序编译为虚拟机的字节码,通过虚拟机仿真模拟解释字节码。(Betz 1988,1991;Franks 1991)。
我们为Lua的实现选择一个混合的架构,它比直接解释lua代码有如下优势:因为词法语法分析(lexical and syntactical)只做一遍,使用一个外部解析器,在实际嵌入前,能够发现简单的错误,缩短开发周期,执行速度快。
如果用一个外部编译器,为在字节码级别扩展程序提供可能,预先编译可以提高下载速度,安全的环境和更小的运行时间。
扩展语言可以通过标准工具(如lex和yacc)产生语法语义解析代码(Levine-Mason-Brown 1992)。尤其是在Unix环境,由于存在良好的编译器构造工具,产生了至少70种小语言。而Lua的实现使用yacc做语义分析器,一开始我们使用lex写语法分析器,经过实际项目的性能分析,我们发现这个模块在下载和执行扩展程序时消耗了一半的时间,后来我们直接用C重新写了这个模块,这个新的语法分析器速度比以前提高了2倍。
Lua虚拟机
在Lua实现中,我们使用了堆栈虚拟机(stack virtual machine)。这就意味着Lua里没有RAM(随机访问存储器),所有的临时的值和局部变量都保存在一个堆栈里。
此外,Lua里也没有通用寄存器(general purpose registers),只有用于控制堆栈和执行程序的专用控制寄存器(special control registers)这些寄存器有:栈的基址寄存器,栈顶寄存器和程序计数寄存器(base of stack,top of stack and program counter)。虚拟机顺序执行程序指令,这样的程序叫做字节码(bytecodes)。
程序的执行通过解释字节码来完成,每一个指令的操作对应到栈顶部分的操作。例如语句:a = b + f(c)
被编译成:
PUSHGLOBAL b
PUSHGLOBAL f
PUSHMARK
PUSHGLOBAL c
CALLFUNC
ADJUST 2
ADD
STOREGLOBAL a
Lua虚拟机大约有60个指令,因此它可以用8bit字节编码。
许多指令是不需要参数的,如ADD,这些指令直接在栈上获取正确的一个字节进行编译。其他指令需要参数,并且超过一个字节,如PUSHGLOBAL,STOREGLOBAL。有些体系结构通过NOP指令进行对齐边界的填充,在这样的架构上,无论这些参数采用1个字节,2个字节还是4个字节,都会面临对齐问题。有很多指令只是为了优化而存在的,例如有一种PUSH指令,只将数字作为参数,将这个数字压入栈,但是还有单字节优化版本,只用于将一般的值压入栈,如0和1。因此,我们有PUSHNIL,PUSH0,PUSH1,PUSH2等。这样的优化指令减少了编译后字节码的空间,同时解释指令的时间也减少了。
Lua支持可变参数(multiple assignment),以及函数支持多个返回值(multiple return values),因此有时数值列表必须在运行时进行调整。考虑到可变参数长度,如果比我们需要的数值多,多余的数值将被抛弃,如果比我们需要的数值少,数值列表使用多个nil扩展到我们需要的个数。这样的调整通过ADJUST指令在栈上进行调整。
虽然多返回值,可变参数是Lua的强大功能,但是这也正是Lua解释器,编译器复杂的源头。由于没有函数类型声明,编译器不知道一个函数会返回多少参数,因此调整必须在运行时执行。同样编译器不知道一个函数有多少参数,因为参数个数在运行时是变化的,参数列表在PUSHMARK和CALLFUNC指令之间。
扩展Lua的一个方式就是通过宿主程序分配字节码来实现,虽然这个策略(strategy)对于解释器来说非常简单,但这样做的缺点是为Lua增加外部扩展程序不足200个,因为Lua有8bit字节码,而且已经使用了约60个用于原始指令集。我们选择了宿主程序通过注册外部函数,像执行Lua原生函数一样执行这些外部函数。因此就有了单一指令CALLFUNC,解释器根据被调用的函数类型决定做什么。
Franks推荐了一个不同的策略,宿主程序的所有扩展函数都可以在嵌入语言里调用,也不需要明确的注册。这个动作通过读取,解释链接器产生的map文件自动完成。这个方案对于应用程序开发人员非常方便,但是不便的是需要依赖格式化的map文件和使用操作系统的重定位功能(Franks使用了DOS的一个特殊编译器)。
内部数据结构
就像前面说的,Lua里的变量是无类型的,只有值有类型。因此值的结构里有2个成员,一个type,一个union包含实际的值。这样的结构出现在栈里,也出现在符号表(symbol table)里。符号表保存了所有的全局符号。
Numbers直接存在union里,Strings串保存在独立的数组里,function的值保存在一个指向字节码数组的指针里,Cfunction类型的值包含一个由宿主程序提供的实际的C函数指针。userdata类型的值与Cfunction类型的值一样。
Tables值的保存由哈希表实现(hash tables)(collisions handled by separate chaining),如果在创建一个table时指定大小,对应的哈希表的大小就是用指定的大小。因此根据预期的table目录数目确定table的尺寸,发生冲突的可能就越小,而且通过index定位效果也好。另外,如果table用做数字数组,在创建时设定正确的尺寸,保证不会发生冲突。
Lua里所有的内部数据结构都是动态分配数组。当在这些数组中没有足够的空闲slots时,垃圾回收自动完成。垃圾回收采用标记-清除(mark-sweep)算法。如果因为所有大的值都在引用致使没有重新找到空间,就重新分配一个数组,这个数组大小为原来数组的2倍。
由于避免了显式的内存管理,垃圾回收对于编程人员是非常方便的。当Lua作为一个独立的语言时,垃圾回收机制是个优点。但是Lua主要是作为嵌入语言,当Lua在宿主程序作为一个嵌入语言时,对于通过lua与宿主程序进行接口编程的人来说,垃圾回收带来一个新的问题,他们不能将Lua的各种table和string存放在C变量里,因为当这些值在Lua的环境里没有任何进一步的引用时,这些值会在垃圾回收执行时被回收。说白了,编程人员必须在返回lua控制之前,将lua里的这些值复制到C的变量里。
总结
从1993年下半年开始,Lua应经被广泛用于生产环境,主要应用在一下几方面:
user configuration of application environment
general purpose data-entry with user defined dialogs and validation procedures
description of user interfaces
programmer description of application objects
storage of structured graphical metafiles,used for communication between graphical
editors and application programs
这部分内容主要描述了lua解释器的实现,基于栈式虚拟机来实现,并且通过一个简单的例子,解释了从lua源码到字节码的转换。这对我们理解c代码中lua虚拟机的实现将会有所帮助。
另外也提到了垃圾回收的机制,也可能对理解其实现代码有所帮助,同时也提到了垃圾回收机制带来一个新问题。