lua的历史与演进

lua的优点:

可移植性
容易嵌入
体积小
高效率
这些优点都来自于lua的设计目标:简洁。从Scheme获得了很多灵感,包括匿名函数,合理的语义域概念

3 lua前身


巴西被商贸限制,引入计算机软件和硬件受限,巴西人有强烈的民族情绪去创造自己的软件。三名作者都是同一个实验室Tecgraf的,这个实验室与很多工业实体有合作关系。成立的头十年,重点是创造交互性的图形软件,帮助合作伙伴进行设计。巴西石油公司是其中一个重要伙伴。有大量的遗留数据需要处理。于是诞生了DEL,一个领域专用语言,主要用来描述数据流图的数据的。后来人们对DEL需求越来越多,不止是一门简单的数据描述语言可以解决的了。

lua为解决实际问题而生,受到三位作者学过的语言影响。自己创造的是..(两个句号)连接字符串。自豪的是,在13年的演进里,lua的类型系统只修改了两次。lua诞生的时候,基本类型包括nil,number,string,table,C function,Lua function,userdata。97年的时候,Lua3.0将C function和Lua function合并了。03年的时候,提出了boolean值类型,增加了thread协程类型。

1993年,第一版Lua由Waldemar在Roberto指导下完成。词法分析用了Unix上经典的yacc和lex。解释器将lua代码翻译成针对一个基于栈的虚拟机的指令。C API很容易扩展,因此最早只有5个函数(print,tonumber,type,next,nextvar)和3个库(input/output, string, math)。

5 Lua的历史


lua的发布模式和其他社区不一样。alpha版本已经相当稳定,beta版本几乎可以作为final版,除非是用来修复bug。这个发布模式对于lua的稳定性有很大帮助,但不利于尝试新特性。因此,从5.0版本开始,添加了新的work版本。work版本是lua当前开发版的snapshot,有助于lua社区迈向开源社区的哲学:早发布、多发布。

lua的标准库被刻意保持在一个很小的范围,因为大部分需要的功能都会由宿主或第三方库提供。4.0对C API重新设计了,C API有很大改动,之后就向着完美一点点前进了。结果是不再有任何内置函数,所有标准库都是基于C API实现,没有通过特别的后门去访问Lua内部结构。

Lua的vm在4.0版本以前一直是基于栈的。在3.1版本,我们对多个指令添加了变量来提高性能。后来发现这个实现复杂度很高,性能提升不明显,于是在Lua 3.2版本去掉了。从Lua 5.0开始,vm改为基于寄存器。代码生成器因此有更多机会去优化和减少常见Lua程序的指令数了。

5.1 Lua 1

Lua的成功使得Lua被大规模用在数据描述上,而预编译Lua代码为VM字节码的特性,迫使Lua编译器必须非常快,即使是对于大型项目。通过将lex产生的scanner替换为一个手写的版本,我们几乎让Lua编译器的速度翻了一倍。同时,我们修改了Lua VM的table构造函数,构造一个大型的表不再需要一条条指令传参数进去了,可以一次调用完成。从那以后,优化重点就变成了预编译时间。

1994年,我们发布了带有这些优化的lua版本:Lua1.1。这次发布和第一篇描述Lua的设计和实现的文章是重合的。之前从未公开发布过的版本,就被称为Lua 1.0(Lua1.0的一个1993年7月的snapshot在2003年10月发布,以此庆祝Lua十周年)

Lua 1.1最早以源码方式在ftp提供下载。这远在开源运动兴盛蓬勃之前。Lua1.1有限制性的用户协议,对于学术用途是免费的,但是商业用途则需要协商。那部分协议没有凑效:尽管我们有一些初始联系人,从没有商业使用是经过协商的。其它脚本语言(比如Tcl)的免费促使我们意识到,限制商用甚至会反过来影响学术用途的发展,因为一些学术项目是打算最终走向市场的。因此,当发布Lua2.1的时候,我们将Lua作为无限制免费软件发布了。我们天真的以一种学院气的口吻对之前的协议重新措辞,并觉得新协议写的很直观了。稍后,随着开源许可证的散播,我们的协议文本变成了干扰着一些用户的噪音:我们的协议没有清晰的说明是否和GPL协议相容。在2002年5月,在邮件列表里进行了冗长的讨论后,我们决定将来发布的Lua版本(从Lua 5.0开始)都使用非常清晰直观的MIT协议。2002年7月,自由软件基金会(FSF)确认了我们之前的协议是GPL兼容的。但我们已经决定采纳MIT协议了。对我们协议的疑问从此都消失了。

5.2 Lua 2

1990年的时候,面向对象迈向巅峰,对于Lua没有面向对象的支持,我们受到了很大的压力。我们不想将Lua变成面向对象,因为我们不想“修复”一种编程范式(fix a programming paradigm)。特别是,我们不觉得Lua需要将对象和类作为基础语言概念,我们觉得可以透过table来实现(table可以保存方法和数据,因为函数是第一类对象)。直到今天,Lua也没有强加任何对象和类模型给用户,我们初心不变。很多用户建议和实现了面向对象模型;面向对象也是邮件列表里经常讨论的问题,我们觉得这是健康的。

另一方面,我们希望允许Lua可以面向对象编程。我们决定提供一套灵活的机制,让用户可以选择对应用来说合适的模型,而不是修复模型。1995年2月发布的Lua2.1,标志着这些灵活语义的问世,极大的增加了Lua的表达能力,从此,灵活语义就变成了Lua的标志。

灵活语义的一个目标是允许table作为对象和类的基础。为了实现这个目标,我们需要实现table的继承。另一个目标是将userdata变成应用数据的天然代理,可以作为函数参数而不只是一个句柄。我们希望能够索引userdata,就好像他们只是一个table,可供调用他们身上的方法。这让Lua可以更为自然的实现自己的主要设计目标:通过提供脚本访问到应用服务和数据,从而扩展应用。我们决定实现一套fallback机制,让Lua把未定义行为交给程序员处理,而不是直接在语言本身实现这些特性。

在Lua2.1的时候,我们提供了fallback机制,支持以下行为:table索引,算术操作符,字符串拼接,顺序比较,函数调用。当这些操作应用到“错误”的类型上,对应的fallback就会被调用到,允许程序员决定Lua如何处理。table索引fallback允许userdata和其它值类型表现的跟表一样。我们也定义了当Key不在table时的fallback,从而实现多种形式的继承(通过委托)。为了完善面向对象编程,我们添加了两个语法糖:function a:foo(…)就好比function a.foo(self,…)一样,以及a:foo(…)作为a.foo(a, …)的语法糖。在6.8节我们会讨论fallback的细节。

从Lua1.0开始,我们就提供了值类型的内省函数(introspective functions):type,可以用来获取Lua值的类型;next,可以用来遍历一个table;以及nextvar,可以遍历全局环境。(正如第四章所述,这是为了实现类似SOL的类型检查)为了应付用户对完整调试设施的强烈需求,1995年12月发布的Lua2.2引入了一个debug API来获取运行中的函数信息。这个API为用户提供了以C语言编写自己的内省函数工具链的手段,比如编写自己的调试器和性能分析工具。debug API刚开始的时候相当简洁:debug库允许访问Lua的调用栈,访问当前执行的代码行行数,以及一个可以查找指定值的变量名的函数。根据M.Sc.的Tomas Gorham的工作,debug API在1996年5月发布的Lua2.4版本里得到了完善,提供了函数访问局部变量,提供了钩子在行数变化和函数调用时触发。

因为Lua在Tecgraf的广泛使用,很多大型的图形源文件都是用Lua写的,作为图形编辑器的输出格式。加载这些源文件会随着文件大小变得越来越大和越来越复杂而变的越来越长。从第一版开始,Lua就预编译所有程序到字节码,再执行字节码。大型代码文件的加载时间可以通过保存bytecode来缩减。这和处理图形源文件特别有关系。所以在Lua 2.4版本,我们引入了一个外部编译器Luac,可以编译一个Lua文件为字节码并保存为二进制文件。这个二进制文件的格式经过精心选择,可以轻松加载同时体积小巧。通过luac,程序员可以在运行期避免词法分析和代码生成,这些操作早期还是比较耗时的。除了更快的加载,luac还允许离线的语法检查,以及随意的用户改动。很多产品(比如模拟人生和Adobe的Lightroom)都是透过预编译格式发布Lua脚本的。

在luac的实现过程里,我们开始将Lua的核心重构成清晰的分离模块。于是,我们现在可以轻易移除词法解析的模块(词法解析器,语法解析器和代码生成器),这些部分占了大约35%的核心代码,剩下的部分可以加载预编译的Lua脚本。这种代码剪裁,对于在移动设备、机器人和感应器这些小设备里嵌入Lua,是有显著意义的。

从第一版开始,Lua就自带有一个库来进行字符串处理。这个库在Lua2.4之前功能有限。但是,随着Lua的成熟,Lua需要进行更重量级的字符串处理。我们认为,沿革Snobol,Icon,Awk和Perl的传统,为Lua添加模式匹配是自然而然的。但是,我们不想将第三方的模式匹配引擎打包到Lua里面去,因为这些引擎通常都很大,我们也希望避开因为引入第三方库带来的代码版权问题。

1995年的第二学期,作为Roberto指导下的学生项目,Milton Jonathan,Pedro Miller Rabinovitch,Pedro Willemsens和Vinicius Almendra为Lua写出了一个模式匹配库。那个设计的经验引导我们写出了我们自己的模式匹配引擎。1996年的12月,我们在Lua2.5中添加了两个函数:strfind(最早职能查找纯文本)和新的gsub函数(名字来源于Awk)。gsub函数可以用来全局替换符合指定模式的子串。它接受一个新的字符串或者一个函数作为参数,函数参数会在每次遇到匹配时调用,并预期该函数返回新子串以供替换。为了缩小实现的规模,我们没有支持完整的正则表达式。我们支持的模式包括字符类,重复,以及捕获(但是没有可选或组匹配)除了简洁性,这种模式匹配还十分强大,是Lua的一个强有力的补充。

那一年是Lua历史上的转折点,因为Lua获得了全球的曝光。在1996年6月,我们在《Software:Practice & Experience》杂志上发布了一篇Lua的文章,为Lua带来了外部的关注,至少是学术圈子的关注。在1996年的12月,Lua 2.5刚刚发布后,杂志Dr.Dobb’s Journal也发表了Lua的文章。Dr.Dobb’s Journal是一本面向程序员的流行刊物,那篇文章为Lua带去了软件工业界的关注。在那次发布之后,我们收到了很多消息,其中一条是1997年1月收到的,来自Bret Mogilefsky,LucasArts出品的冒险游戏——Grim Fandango的首席程序员。Bret告诉我们,他从Dr.Dobb’s上读到了Lua,他们打算用Lua代替自己写的脚本语言。1998年10月,Grim Fandango发布,1999年5月Bret告诉我们“大量的游戏都是用Lua写的”。那个时候,Bret参加了GDC(Game Developers’ Conference, 游戏开发者会议)的有关游戏脚本的圆桌会议。他谈到了Grim Fandango应用Lua的成功经验。很多我们认识的开发者,都是在那次事件里认识到Lua的。在那以后,Lua在游戏程序员之间口耳相传,变成了游戏工业里可资销售的技能。

因为Lua的国际化曝光,向我们提问Lua的信息越来越多。1997年2月,我们建立了邮件列表,好更有效率的处理这些问题,让其他人也能帮忙回答问题,并开始建设Lua社区。这个列表至今发布了超过38000条消息。很多热门游戏对Lua的使用,吸引了很多人到邮件列表里。现在已经有超过1200个订阅者了。Lua列表十分友善,同时又富有技术性,我们对此深感幸运。邮件列表逐渐变成了Lua社区的焦点所在,也是Lua演进的动力源泉。所有的重大事件,都是首先在邮件列表里出现的:发布重大通知,请求新特性,bug报告等等。

在Usenet新闻组里建立comp.lang.lua讨论组曾经在邮件列表里讨论过两次,分别是1998年的4月和1999年的7月。两次的结论都是邮件列表的流量不能保证创建一个新闻组。而且,更多人倾向于邮件列表。随着多个阅读和搜索完整邮件存档的web界面问世,创建新闻组变得无关紧要了。

5.3 Lua 5.3

Lua2.1里引入的fallback机制,可以很好的支持灵活扩展的语义,但这个机制是全局的:每个事件只有一个钩子。这让共享或重用代码变的很艰难,因为同一事件的fallback在模块里只能定义一次,不能共存。1996年12月,听取了Stephan Herrmann的建议后,我们在1997年7月发布的Lua 3.0中,我们解决了fallback冲突问题。我们将fallback替换为tag方法:钩子是以(event, tag)的形式挂在字典里的。Tags是在Lua2.1引入的整数标签,可以附在userdata上。最初的动机是希望同类的C对象,在Lua里都有相同的tag(不过,Lua没有强迫要对Tag提供解释)。Lua3.0里,我们对所有值类型提供了tag支持,以支持对应的tag方法。6.8节里还会继续讨论fallback的演进。

1998年7月Lua 3.1引入了函数式编程,Lua拥有了匿名函数和upvalue支持的函数闭包。引入闭包是被高阶函数的存在所启发的,例如gsub,可以使用函数作为参数。在Lua 3.1点工作中,邮件列表里讨论了多线程和协作式多任务,这些讨论主要源于Bret Mogilefsky在Grim Fandango中,对Lua2.5和Lua 3.1 alpha版所作的改动。虽然没有最终定论,但这个话题一直很热门。协作式多任务在2003年4月发布的Lua5.0里提供了,详见6.7节。

从Lua 1.0到Lua 3.2,C API总体上没有变化,都是针对一个隐式Lua虚拟机进行操作的。但是,新的应用程序比如web,需要多状态支持。为了解决这一问题,Lua 3.1引入了多个独立Lua状态的设计,可以在运行时自由切换。而完全可重入的API则需要等到Lua4.0。同时,两个非官方的Lua3.2修改版出现了,它们都带有显式的Lua状态,一个是1998年,Roberto Ierusalimschy和Anna Hester为CGILua写的、基于Lua 3.2 alpha的版本。另一个是1999年,Erik Hougaard基于Lua 3.2 final写的版本。Erik的版本是公开发布的,并应用在Crazy Ivan robot里面。CGILua是作为CGILua发行版的一部分发布,从未以独立包的形式出现。

1999年7月,Lua 3.2主要是一个维护性的版本。没有新奇的特性,除了一个可以用Lua编写调试器相关代码的debug库。但是,从那时起,Lua就处在一个相对稳定的版本,因此Lua 3.2有很长的生命期。因为下一个版本Lua 4.0是用的全新的不兼容API,很多用户就只停留在Lua 3.2,没有再迁移到4.0版本了。比如Tecgraf就从来没有迁移到4.0版本,打算直接上Lua 5.0.很多Tecgraf的产品还是用的Lua 3.2

5.4 Lua 4

2000年12月,Lua 4.0正式发布。正如上文所述,4.0的主要改变是完全可重入的API,为那些需要多份Lua state的应用而设计。因为改造API为完全可重入已经是主要改动,我们借此机会重新设计了API,依赖清晰的堆栈实现与C层的值交换。这是Reuben Thomas在2000年7月提出的。

Lua 4同时引入了for语句,这是邮件列表中的日常话题和大部分Lua用户最想要的特性。我们早期没有引入for语句,是因为while循环更为一般化。但是,用户总是投诉忘记在while循环的尾部更新控制变量,从而引起死循环。而且,我们在好语法这一点上没有达成一致。我们觉得Modula语言的for语句限制性太大了,因为它既不能迭代table里的元素,也不能迭代文件里的行。C传统上的for循环也不适合Lua。基于Lua 3.1引入的闭包和匿名函数,我们决定使用高阶函数来实现迭代。所以,Lua 3.1提供了一个高阶函数来迭代table,可以对table的每一对键值对回调用户自定义函数。比如,要打印table t里面的每一个键值对,只需要写foreach(t,print)。

在Lua 4.0我们终于设计了一个for循环,它有两种方式:一个数字式的循环以及一个表遍历的循环(1997年10月由Michael Spalinski提出)。这两种方式覆盖了大部分常用应用环境;对于更一般化的循环,依然有while循环可供使用。要打印table t里的所有键值对,可以这样写:

1
2
3
for k, v in t do
print(k, v)
end

添加for循环只是一个很简单的一个改动,但它的确改变了Lua程序的外观。Roberto不得不重写了Programming in Lua草稿里的许多例子。Roberto在1998年开始写书了,但是他从来都没写完,因为Lua是一个不断变动的目标。随着Lua 4.0的发布,书里大部分内容需要改写,几乎所有的代码块都要重写了。

Lua 4.0发布后,我们开始为Lua 4.1工作。在Lua 4.1版本,我们面临的主要挑战,大概是要不要支持、如何支持多线程吧,这在当时是一个大问题。随着Java的用户增长以及Pthreads的出现,很多程序员开始考虑多线程,将其作为编程语言的关键特性进行考量。但是,对于我们来说,Lua支持多线程需要考虑几个严肃的问题。首先,在C层面支持多线程就会用到非ANSI C的原语——尽管Pthread很流行,但仍然有很多平台(当时和现在)都缺乏这个库的支持。第二,更重要的是,我们不相信标准多线程模型:那个共享内存的抢占式并发模型。我们仍然认为,对于a=a+1都没有确定结果的语言,没人能写出正确的程序。

对于Lua 4.1,我们开始用Lua的经典方式解决这些难题:我们只实现了一个简单的多栈共存的机制,我们称为threads(线程)。外部的库可以使用这些Lua线程来实现多线程,比如基于Pthreads来实现。同样的机制也能用来实现协程,一个协作式的、非抢占的多线程模型。2001年7月,Lua 4.1 alpha发布,带有额外的多线程和协程库支持;同时也引入了弱表,以及标志性的基于寄存器的虚拟机。我们一直很想用基于寄存器的虚拟机进行实验。

Lua 4.1 alpha发布后的第二天,John D.Ramsdell在邮件列表里开始了关于语法域的大讨论。经过数十封邮件的讨论,我们清晰的发现,Lua需要完全词法作用域的支持,而不是Lua 3.1开始的upvalue机制。在2001年10月,我们想到了实现高效的完全词法作用域的支持,并在同年的12月发布了一个可以工作的版本。那个版本还引入了新的table混合模型,可以在合适的时候将table作为数组实现。因为那个版本实现了新的基础算法,我们决定作为可工作版本来发布,尽管我们已经为Lua 4.1发布了一个alpha版本。

2002年2月,我们为Lua 4.1发布了一个新的工作版本,带有三个相对新颖的特性:一个基于迭代函数的通用的for loop,取代tags和fallbacks的元表和元方法,以及协程。在那次发布后,我们意识到Lua 4.1带来了太多的巨变——可能Lua 5.0 比较适合作为下个版本的版本号。

5.5 Lua 5

对于Lua 4.1这个版本号来说,最后的改动来自于Christian Lindig和Norman Ramsey在哈佛大学召开的Lua库设计会议。这次会议的一个主要结论是,Lua需要某种模块系统。我们一直认为模块可以透过table来完成,但是连标准库都没有按这种方式实现。于是我们决定下个版本迈出这一步。

将库函数放入table里面是一个巨大的冲击,因为这会影响到所有使用了至少一个库函数的程序。比如,老版本的strfind函数现在叫string.find(在string库里的find域,存储在叫string的table里);openfile变成了io.open;sin变成了math.sin,诸如此类。为了让转换变的简单点,我们提供了一个兼容脚本,在里面定义了老函数去应用新函数:

1
2
3
strfind = string.find
openfile = io.open
sin = math.sin

但是,将库打包到table里面去始终是一个大改动。2002年6月,当我们放出了带有这一改动的可工作版本时,我们放弃了”Lua 4.1”这个名字,并将其命名为”Lua 5.0 work0”。最终版本的进展从那时开始就相当稳定了,2003年4月,我们发布了Lua 5.0。这次发布让Lua的特性得以稳定下来,让Roberto可以完成他的新书了,新书在2003年12月发布。

Lua 5.0发布后不久,我们立即开始Lua 5.1的工作。最初的动机是实现增量式的垃圾回收系统,以满足游戏程序员的需要。Lua使用的是传统的标记-清除垃圾回收算法,直到Lua 5.0为止,垃圾回收都是原子执行的。副作用是对于某些应用,可能会在垃圾回收时有很长的暂停。在那个时间点,我们的主要考虑是,添加写屏障需要实现增量式垃圾回收,对于Lua的性能会有负面影响。为了补偿这一劣势,我们也常侍了分代回收。我们也希望保留老一套的自适应行为,可以根据总内存占用调节垃圾收集的频率。而且我们希望回收器保持简洁的特点,就像Lua的其他部分一样。

我们在增量式分代GC上花了超过一年的时间。但因我们没有接触过对内存有强烈需求的应用(比如游戏),所以很难在真实场景中测试收集器的表现。从2004年的3月到12月,我们放出了数个可工作版本,试图得到收集器在应用中表现的具体反馈。我们最终收到了奇怪的内存分配行为的报告,并成功重现了但没有解释。2005年1月,一名Lua社区的活跃成员Mike Pall,通过内存分配图解释了问题的根源:在某些特定场景,增量行为、分代行为和自适应行为之间会有微妙的交互,导致收集器“自适应”到越来越低频率的收集上去。因为太复杂和不可预测,我们放弃了分代方式,并在Lua 5.1实现了更简单的增量回收。

在这段时间里,程序员尝试了Lua 5.0的模块系统。新包开始涌现,老包也开始转移到新系统去。包作者们希望指导构建模块的最佳实践。2005年7月,在Lua 5.1的开发期间,一场由Mark Hamburg组织的国际性Lua会议,在San Jose的Adobe召开。其中一场演讲是关于Lua 5.1的新特性,引发了关于模块和包的冗长讨论。结果是我们对模块系统作出了一些细微但收效甚大的改变。尽管我们对Lua有“机制而非策略”的指南,我们还是定义了一系列的策略来编写模块和加载包,并做了些小改动让这些策略跑的更好。2006年2月,Lua 5.1发布。尽管Lua 5.1的初衷是增量垃圾收集,模块系统的改进可能是最容易留意到的。另一方面,增量垃圾收集没有被注意到,说明了它成功的避免了长时间的停顿。

6 特性演进


在这一节,我们会详细讨论Lua的一些特性的演进。

6.1 类型

Lua的类型系统是相对稳定的。很长时间里,Lua只有6个基本类型:nil,number,string,table,function和userdata(实际上,直到Lua 3.0为止,之前的C函数和Lua函数有不同的内部类型,但是这个差异对于调用者是透明的)。唯一真正的改变来自于Lua 5.0,这个版本引入了两个新类型:thread和boolean。

引入的类型thread用于表示协程。像其他Lua值类型一样,thread是第一类的值。为了避免创造新的语法,所有针对thread的基础操作都是通过library来提供的。

很长时间里,我们都很抗拒引入布尔值类型:nil就是false,其他所有一切都是true。这种情况对于我们的目的是简洁够用的。但是,nil也作为table里元素缺失的值,以及未定义变量的默认值。在某些应用里,需要一个table的域被置为false但仍然可见表示存在,一个显式的false可以表示这种情况。Lua 5.0里,我们最终引入了布尔值类型true和false。nil还是作为false。回顾这个设计,可能在布尔表达式里引用nil报错会是更好的解决办法,就跟其他表达式里一样。这样,nil扮演未定义值类型的代理,就会更加具有一致性。但是,这个改动很可能会导致很多现存的程序报错。LISP有类似的问题,空列表代表了nil和false。Scheme显式使用false,并将空列表作为true。但一些Scheme实现仍然将空列表作为false。

6.2 Table

Lua 1.1有三种语法来创建一个table:@(),@[]和@{}。最简单的形式是@(),用于创建一个空表。可以在创建时提供一个可选的长度参数,作为优化性能的提示信息。@[]这种形式是用来构建数组的,比如@[2,4,9,16,25]。在这种table里,key是隐式的从1开始的自然数。@{}这种形式是用来构造记录的,比如@{name=”John”,age=35}。这种table是键值对的集合,键是显式的字符串。一个table可以用这三种的其中一种方式构造,构造后可以动态修改,不因构造方式受限。此外,还能在构造列表和记录的时候提供用户函数调用,比如@foo[]或者@foo{}。这个语法是从SOL里继承的,是过程数据表述的表达式,Lua的一个主要特性(参见第二节)。语法是构建好table后,调用对应的函数,将构造好的table作为单参数传入函数。函数允许随意检查和修改table,但是函数的返回值会被忽略:table就是表达式的最终值。

Lua 2.1里,table创建的语法是统一的,并得到简化:开头的@被移除了,唯一的构造方式是{…}。Lua 2.1也允许混合的构造方式,比如

grades{8.5, 6.0, 9.2; name=”John”, major=”math”}
数组的部分和记录的部分被分号分割开。最后,foo{…}变成了foo({…})的语法糖。也就是说,函数加上表构造器,变成了普通的函数调用。所以,函数需要显式返回table(或其他值)。从构造器里拿掉@是一个平凡的改动,但它的确改变了语言的感觉,不仅仅是它的外观。改变语言观感的平凡改动不应该被忽视。

然而,这个语法的简化和table构造器的语义改变带来了副作用。Lua 1.1里,=用来判断相等。Lua 2.1里table构造器统一之后,表达式{a=3}变成有歧义了,因为它可以表示("a", 3)个键值对,或者(1, b),其中b是表达式a=3的值。为了解决二义性,Lua 2.1用==取代了=。改动后,{a=3}表示一个含有键值对(“a”, 3)的table,而{a==3}表示(1, b)`。

这些改变让Lua 2.1变得不兼容Lua 1.1了(所以大版本号也变了)。但是,因为那时候实际上所有的Lua用户都是Tecgraf的,这并不是一个致命的改变:现存的代码可以经过我们编写的特定工具进行转换。table构造器的语法从那时起几乎不变了,唯一的例外是Lua 3.1引入的:记录的key可以用上任意表达式了,比如{[10*x+f(y)]=47}。尤其是这一改动允许了key为任意的字符串了,包括保留字和带有空格的字符串。因此,{function=1}是无效的,因为function是一个保留字,但是{[“function”]=1}是有效的。从Lua 5.0开始,也可以自由的混合数组部分和记录部分了,因此没有必要再在table构造器里使用分号了。

虽然table的语法演进了,但是table的语义得到完整保留:table依然是关联数组,可以存放任意键值对。但是,实践中经常使用的table要么是单纯的数组(连续的整型key)或者记录(字符串作为key)。因为table是Lua里唯一的数据结构机制,我们投入了大量的努力去实现table,让table在Lua的核心代码里高效运行。直到Lua 4.0为止,table都是作为纯哈希表实现的,所有的键值对都是显式存储的。在Lua 5.0版本我们引入了table的混合表示:每个table包含了一个哈希部分和一个数组部分,两个部分都可以是空的。Lua检测一个table是不是作为一个数组来使用,并自动将数字索引的值移动到数组部分,而非原本的存储在哈希部分。这种分裂只在底层实现层次进行;访问table域是透明的,即使是对虚拟机来说。table会自动根据内容使用两个部分。

这个混合机制有两个优点。第一,访问整型key的操作会变得更快了,因为不再需要哈希。第二,更重要的是,数组部分只占原来哈希部分的一半大小,因为哈希部分需要同时存储key和value,而数组部分的key已经隐含在下标了。结果是,如果一个table是作为数组使用的,它的表现就像数组一样,只要它的整型key是密集分布的。而且,哈希部分没有内存或者时间的代价,因为作为数组使用时,哈希部分不存在。反过来说,如果table是作为记录使用而非数组,那么数组部分就是空的。这些节省下来的内存是重要的,因为对于Lua程序来说,创建大量小table是很常见的(比如用table来表示object)。Lua的table也能优雅的处理稀疏数组:语句a={[1000000000]=1}在哈希部分创建了一个键值对,而非一个10亿元素的数组。

另一个打造高效table实现的原因,是我们可以使用table实现各种各样的任务。比如,在Lua 5.0版本里的标准库函数,从Lua 1.1开始就作为全局变量存在,现在移动到table中去了。最近,Lua 5.1带来了完整的包和模块系统,这些系统都是基于table实现的。

table扮演了Lua核心的一个突出角色。有两个场景我们直接用table代替了核心代码的特殊数据结构:Lua 4.0版本,用table来表示全局环境(用于保存全局变量),Lua 5.0实现了可扩展语义(参见6.8)。从Lua 4.0开始,全局变量就存储在普通的Lua table里,称为全局table,这是John Belmonte在2000年4月提出的简化建议。在Lua 5.0我们用元表和元方法取代了tag和tag方法(Lua 3.0引入的)。元表是普通的Lua table,元方法是作为元表的域存储的。Lua 5.0也引入了环境table,可以附加到Lua函数上;它们就是Lua函数索引的全局环境。Lua 5.1将环境变量table扩展到C函数、userdata和协程,取代了全局的环境变量。这些改动简化了Lua的实现、Lua和C程序员所用的API,因为全局变量和元方法可以在Lua里操控,不再需要特殊函数了。

6.3 字符串

字符串在一门脚本语言里扮演了主要的角色。因此,创建和处理字符串的设施是脚本语言易用性的重要部分。

文本字符串的语法有一个有趣的演进过程。从Lua 1.1开始,一个文本串可以用单引号或者双引号来定义,也可以包含像C一样的转义序列。同时使用单引号和双引号来定义字符串,并且具有相同语义,在当时有点不寻常。(比如,在脚本语言的传统里,Perl是会展开双引号字符串里的变量的,而单引号字符串则保持不变)这两种引号表示方式允许字符串包含其中一种引号,而不需要使用转义符。但是任意文本的字符串还是需要转义符序列的。

Lua 2.2引入了长字符串,传统编程语言所没有,却普遍存在于脚本语言的一项特性。长字符串可以有许多行,不需要解析其中的转义符序列;它们提供了一个灵活的方式,将任意文本作为字符串,不再需要担心其中的内容(是否需要转义符)。但是,要为长字符串定义一个好的语法,没有想象中的简单,尤其是它们会被用于包含任意的程序文本(其中可能包含其他的长字符串)。这让我们思考两个问题:长字符串应该如何结束,它们是否可以嵌套。直到Lua 5.0为止,长字符串都是被包含在一对匹配的[[…]]中,并且可以包含嵌套的长字符串。可惜,结束分隔符]]可以是有效Lua程序的一部分,以一种不平衡的方式出现:a[b[i]],或者在其他上下文环境里,比如XML的<[!CDATA[…]]>。

Lua 5.1引入了长字符串的新形式:用[===[…]===]包围文本,=的数量可以是任意的(包括0)。这种新的长字符串不嵌套:一个长字符串遇到匹配数量的=就会结束。不管怎么说,现在包裹任意文本变得更简单了,即使文本包含其他长字符串或者不匹配的]=…=]序列:只要使用适当数量的=字符。

6.4 块注释

Lua的注释是—开始,到行尾结束。这是最简单的注释,非常有效。很多语言使用单行注释,不过它们用的符号不一样。使用—表示注释的语言包括Ada和Haskell。

我们从未觉得需要多行注释,或者块注释,除非是作为关闭一块代码的快捷方式。使用什么语法永远是一个问题:使用C语言的熟悉的//语法,或者其他语言的,都和Lua的单行注释格式不搭配。同样,块注释是否能嵌套也是一个问题,困扰着用户,并影响到词法分析器的复杂度。嵌套的块注释的场景,一般是程序员需要注释掉某些代码块。自然,它们希望代码块里的注释可以正确处理,这只有在块注释可以嵌套的情况下才能发生。

ANSI C支持块注释,但是不允许嵌套。C程序员通常使用C预处理器的惯用法来关闭代码块:#if 0 …#endif。这种方式有清晰的优势,它可以优雅的处理好被关闭代码里的注释。带着同样的动机和启发,我们强调了关闭Lua代码块的需要——不是块注释的——通过在Lua 3.0引入类似C语言的pragma进行条件编译。尽管块注释可以用上条件编译,我们不觉得这是它的正确用法。在Lua 4.0的改造工作中,我们认为条件编译带来的词法器复杂度太大,不值得继续支持,同时对于用户来说,它的语义对于用户有一定复杂性,对于完整宏支持的设施也没有达成共识(参见第7章)。所以,Lua 4.0里我们去掉了条件编译的支持,Lua仍然没有支持块注释。

块注释是在Lua 5.0才被最终引入的,形式是–[[…]]。因为他们故意模仿了长字符串(参见6.3)的语法,所以很容易修改词法器进行支持。这种相似性也帮助用户掌握概念和语法。块注释也可以被用于关闭代码:惯用法是在需要关闭代码的前后加上两行,分别包含—[[和—]]。要打开代码,只需要在第一行添加一个-:这两行便都变成单行注释。

像长字符串一样,块注释也可以嵌套,同样有长字符串所遇到的问题。特别是包含不平衡的]]有效的Lua代码,比如a[b[i]],不能在Lua 5.0被可靠的注释掉。Lua 5.1长字符串的新语法也能用于块注释,形式是—[===[…]===],这个问题因此得到简洁稳定的解决方案。

6.5 函数

Lua里的函数一直是第一类对象。一个函数可以在运行时创建,通过编译和执行包含其定义的字符串实现。随着Lua 3.1引入的匿名函数和upvalue,程序员可以在运行时创建函数,不再需要从文本里编译了。

Lua的函数无论是C或者Lua的,都没有声明。被调用时,他们会接受不定数量的参数:多余的参数会被丢掉,缺失的参数会被赋予nil值。(这吻合了多重赋值的语义)。C函数一直都能处理不同数量的参数。Lua 2.5介绍了可变参数类型Lua函数,标志是参数列表以…结尾(只在Lua 3.0出现的实验特性)。当一个变参函数被调用,对应…的参数将会收集到一个叫arg的table里。这种方式虽然很简单便捷,但是要把这些参数传给另一个函数,就需要解包这个table。因为程序员经常将参数传递给另一个函数,Lua 5.1允许…用于参数列表和赋值表达式的右值。这避免了没必要的创建arg table。

Lua执行的基本单位是代码块(chunk);它只是一个语句序列。Lua的一个代码块就像其他语言的main程序:它可以包含函数定义和可执行代码。(实际上,一个函数定义就是可执行代码:一个赋值)同时,一个代码块和一个普通的Lua函数非常相似。比如,代码块和普通Lua函数一直有着同一类字节码。但是,在Lua 5.0之前,代码块需要一些内部的“魔法”来开始执行。Lua 2.2开始,代码块开始类似普通函数,那时候,作为未记录在文档的特性,函数外可以定义局部变量了(只在Lua 3.1变成了官方发布)。Lua 2.5允许代码块返回值。Lua 3.0代码块变成了内部的函数,只是他们在被编译后立即执行;他们不以函数形式暴露给用户层。最终,在Lua 5.0迈出了这一步,将代码块的加载和执行变成了两步,来为宿主程序员提供更好的控制性和错误报告。结果,Lua 5.0里代码块变成了日常无参数的匿名函数。Lua 5.1里代码块变成了匿名变参函数,因为可以在运行时进行值传递。这些值都可以透过新的…机制进行传递。

从别的角度看,代码块就像其他语言的模块一样:他们一般用来提供全局环境的函数和变量。最初,我们没想过Lua会用于大规模编程,所以我们不觉得需要显式的模块。而且,我们觉得table就足够应付模块的需要了。在Lua 5.0里,我们将所有标准库打包进table里了,以此说明table足够应付模块。这鼓励了其他人将库打包进table里,让共享库变得更为简单。我们现在觉得Lua可以用来大规模编程了,特别是在Lua 5.1带来了基于table的包系统和模块系统之后。

6.6 词法范围

在Lua开发的早期,我们开始考虑全词法域的第一类函数。这是一个优雅的构造,完美演绎了Lua的少而精哲学,提供少量但强大的构造方式。同时也让Lua适合进行函数式编程。但是,我们找不到全词法的合理实现方式。一开始,Lua就使用了一个简单的基于数组的栈来记录活跃记录(所有临时变量和局部变量存储的地方)。这个实现简单高效,我们找不到理由推翻它。当我们允许嵌套函数和全词法域之后,一个内部函数使用的变量,可能有着比创建它的函数更长的生命期,所以我们不能用栈的方式去处理这些变量。

简单的Scheme实现在堆上分配栈帧。1987年,Dybvig[20]描述了如何使用栈去分配栈帧,前提是栈帧不包含嵌套函数使用的变量。他的方法需要编译器提前知道,一个变量是不是嵌套函数的自由变量。这不适合Lua编译器,因为Lua编译器解析完表达式后就立即生成操作变量的代码,它无法得知任一变量是否会在稍后的运行中用作嵌套函数的自由变量。我们希望实现Lua时保持这个设计,因此没有使用Dybvig的方法。基于相同的原因,我们也不能使用先进的编译器技巧,比如数据流分析。

现在,有很多优化策略去避免使用堆来存储栈帧(e.g.,[21]),但它们全都需要编译器有中间表达方式,Lua编译器却没有。McDermott的基于栈的栈帧分配建议[36],特别强调是针对解释器的,是我们所知唯一的不需要中间表示方式的代码生成。就像我们的实现一样【31】,他的提案将变量放在栈上,如果它们被嵌套的闭包使用又超出了词法域,就将它们移动到堆上。但是,他的提案假设环境是以关联表的方式组织的。所以,将一个环境移动到堆上,解释器只需要修正列表头部,所有的局部变量自动到堆上了。Lua使用真实的记录作为活跃记录,局部变量的访问转译成直接访问栈顶对应偏移地址,所以不能使用McDermott的方法。

在很长一段时间里,这些困难一直困扰着我们,无法在Lua里引入嵌套的第一类函数,并实现全词法域支持。最终,在Lua 3.1里,我们接受了一个称为upvalue的妥协实现。在这个方案里,内部函数不能访问和修改运行时的外部变量,但可以访问函数创建时的变量。这些变量称为upvalue。upvalue的主要优势,在于可以简单的实现:所有局部变量在栈上;创建一个函数时,函数被打包在一个闭包之中,闭包包含了函数使用的外部变量的副本。换句话说,upvalue是外部变量的冻结值。为了避免误解,我们创建了新的语法来访问upvalue:%varname。这个语法清晰表明代码是在访问变量的冻结值,而不是变量本身。upvalue虽然不能修改,但依然非常有用。有需要的时候,我们可以使用table作为upvalue来模拟可变外部变量:尽管我们不能改变变量指向的table,我们仍可以改变table的域本身。这个特性在以下场景特别有用:匿名函数作为高阶函数的参数,用于table遍历和模式匹配。

2000年12月,Roberto在他的书【27】的第一版手稿里写到”Lua通过upvalue提供了一种合适的词法作用域形式”。在2001年7月,John D. Ramsdell在邮件列表中争论到,”一门语言要么是词法作用域的,要么不是;在词法作用域前添加’合适的’这个形容词毫无意义”。那条消息促使我们寻找更好的解决方案,以及支持全词法域的方式。2001年10月,我们做出了了全词法域支持的初步实现,并发布到邮件列表中。构想是每个upvalue都是间接访问的,当变量在词法域的时候指向栈;在词法域结束的时候,会将变量值移到堆区,并将指针指向堆区。开放闭包(带有指向栈的upvalue)被保存在一个列表里,允许重定向和重用开放的upvalue。重用对于获得正确语义有其必要性。如果两个闭包,共享一个外部变量,各自有自身的upvalue,那么在词法域结束的时候,每个闭包都有自己的变量拷贝了,但正确的语义要求它们共享变量。为了保证重用,创建闭包的算法是这样做的:对于闭包使用的每个外部变量,首先搜索开放闭包的列表,如果发现一个upvalue指向同样的外部变量,就重用那个upvalue;否则,创建新的upvalue。

Edgar Toering,Lua社区的活跃成员,误解了我们对词法域的描述。结果是他理解的比我们的最初的构想要更好:只保留开放upvalue的列表,而不是开放闭包的列表。因为闭包使用的局部变量的数量,一般远小于使用它们的闭包的数量(局部变量的数量被代码文本静态限制了),他的实现比我们的更加高效。而且也更容易适配到协程中(基本上同一时间实现),因为我们可以为每个栈保留一个独立的upvalue列表。我们在Lua 5.0添加了全词法域支持,用的正是这个算法,因为它符合了我们的所有期望:它可以用一遍编译器实现;没有让只访问内部局部变量的函数增加运行代价,因为它们依然在栈上处理所有局部变量;而且访问外部局部变量的代价仅仅是一次额外的中转【31】。

6.7 协程

我们一直在为Lua寻找某种形式的第一类延续性。这种寻找是受Scheme的第一类延续的启发诞生的(Scheme真是我们的灵感源泉),也是游戏程序员对于”软”多线程的需要(一般描述为”以某种方式挂起一个角色,并且稍候继续”)。

2000年的时候,Maria Julia de Lima在Lua 4.0 alpha版实现了完整的第一类延续性,作为她在读PhD的部分工作成果【35】。她使用了一个简单的实现,因为就像词法域问题一样,以更高明的技巧实现协程,对于Lua总体上的简洁而言,实在太复杂了。对于她的实验来说,结果是令人满意的,但是作为最终产品,就显得太慢了。无论如何,她的实现发现了Lua的一个奇特问题。因为Lua是一门可扩展语言,有可能(而且很普遍)会从C调用Lua,或者从Lua调用C。因此,在一个Lua程序执行的任意点,当前延续性通常都是有部分是Lua,部分是C。尽管可以操作Lua的延续性(特别是通过操控Lua调用栈),却不能操作C的延续性,尤其是在ANSI C标准下。那时候,我们对这个问题没有很深的了解。特别是,我们找不到C调用的准确限制。Lima的实现只是简单的禁止了所有C调用。再次强调,这对于她的实验而言是可用的,但是对于Lua的官方发行版来说,完全无法接受。因为Lua和C代码可以轻易融合正是Lua的一个标记。

2001年12月,Thatcher Ulrich在没有理解这一困难的前提下,在邮件列表里宣布: 我已经为Lua 4.0创建了一个补丁,让Lua到Lua的调用变成非递归的(比如”无堆栈”(stackless))。这带来了sleep()调用的实现, 允许从宿主程序退出【省略】,并让Lua state可以通过新的API函数lua_resume回到刚刚的执行环境。

换句话说,他提议了一个非对称的协程机制,基于两个原语:yield(他称为sleep)和resume。他的补丁遵循了邮件列表中,Bret Mogilefsky提到的,Grim Fandango公司针对Lua 2.5和3.1添加协作式多任务,这一实现的高阶描述(Bret不能提供细节,因为是公司私有的)。

在这次公告之后,2002年2月在哈佛举行了Lua库设计大会,有了一些关于Lua的第一类延续性的讨论。有人认为,如果第一类延续性太复杂,我们可以实现一次性延续性。其他人认为实现对称的协程更好。但我们找不到这些机制的合适实现,可以解决C调用的问题。

我们花了不少时间研究,才意识到为什么在Lua实现对称协程那么困难,为什么Ulrich的基于非对称协程的实现,避开了我们遇到的难点。一次性延续性和对称协程都涉及到操作全延续性。所以,只要这些延续性包含了任何C的部分,就不可能捕捉到他们(除非用ANSI C以外的工具)。相反,一个基于yield和resume的非对称的协程机制,只操作偏序延续性:yield捕获了当前调用点到对应resume之间的延续性【19】。在非对称协程上,当前延续性可以包含C的部分,只要他们在被捕获的延续性之外。换句话说,唯一的限制是我们不能跨C调用的yield。

意识到这一点之后,基于Ulrich的概念验证实现,我们在Lua 5.0实现了非对称协程。主要的改动是为虚拟机执行指令的解释器循环,不再能递归调用了。在之前的版本里,当解释器循环执行一个CALL指令,他会递归调用自己来执行被调用的函数。Lua 5.0里,解释器更像是一个真实的CPU:当他执行一个CALL指令的时候,他将一些上下文信息推到栈上,然后处理被调用函数,被调用函数返回的时候,再恢复上下文。改成这样以后,实现协程变得很直接了。

不像大多数非对称协程的实现,Lua里协程是stackfull【19】的。通过协程,我们可以实现对称协程,甚至是Scheme的call/1cc操作符(调用当前一次性延续)。但是,使用C函数在这些实现里还是受到严重限制。

我们希望Lua 5.0引入的协程成为一个标志,标志着协程作为强有力的控制结构的复兴。

6.8 可扩展语义

在5.2节,我们介绍了可扩展语义的一个例子,Lua 2.1里允许程序员使用fallback作为通用机制,处理Lua未定义的场景。fallback因此提供了一种可恢复异常处理的受限形式。尤其是,通过使用fallback,我们可以让一个值对不是为其设计的操作进行响应,或者让一个值表现得像另一个值一样。例如,我们可以让userdata和table响应算术运算符,userdata可以表现得像table一样,字符串表现得像函数一样,诸如此类。此外,我们可以让一个table响应不存在的key,这是实现继承的基石。通过table索引的fallback机制,以及一点点定义和调用方法的语法糖,面向对象编程及继承在Lua里成为了现实。

尽管对象、类和继承不是Lua的核心概念,他们可以直接在Lua里实现,并根据应用程序的需要采取多种多样的方式实现。换句话说,Lua提供了机制,而非策略——我们由始至终都紧密追随的宗旨。

最简单的继承是通过委托继承,Self率先引入这机制并被其他基于原型的语言所采用,比如NewtonScript和JavaScript。下面的代码展示了Lua 2.1里通过委托实现继承的一个实现。

1
2
3
4
5
6
7
8
9
10
11
12
function Index(a, i)
if i == "parent" then
return nil
end
local p = a.parent
if type(p) == "table" then
return p[i]
else
return nil
end
end
setfallback("index", Index)

当访问一个table的缺失域(比如属性或者方法),就会触发索引的fallback。实现继承就是将索引的fallback指向一条向上的祖系链,并有可能再次触发fallback,直到遇到一个拥有指定域的table,或者这条链完结。

在设置了索引fallback以后,以下代码会打印red,虽然b没有color域:

1
2
3
a=Window{x=100,y=200,color="red"}
b=Window{x=300,y=400,parent=a}
print(b.color)

通过parent域进行委托,这过程中没有黑魔法或者硬编码。程序员拥有完整的自由:他们可以使用parent或者其他名字,可以通过尝试一系列的父系函数实现多继承,诸如此类。我们没有硬编码任一行为的决定,引出了Lua的主要设计概念:元机制。我们提供了方法让用户可以编码他们需要的特性,以他们想要的方式实现,并只实现他们需要的特性,而非将语言本身塞满特性。

Fallback机制极大扩展了Lua的表达能力。但是,fallback是全局的句柄:对于每个事件,只有一个函数可以被执行。结果就是,很难在一个程序里混合不同的继承机制,因为只有一个钩子来实现继承(只有索引fallback)。对于一个单一的开发组,基于自己的对象系统进行开发,这可能不是什么严重的问题,但是一个组尝试使用另一个组的代码,就会变成问题,因为他们的对象系统并不一定一致。不同机制的钩子可以串在一起,但是链式调用很慢,很复杂,充满错误,并且不礼貌。fallback链不鼓励代码共享和重用;实际上几乎没有人使用。这让使用第三方库变得很难。

Lua 2.1允许标记userdata。Lua 3.0我们将标记扩展到所有值,并用标记方法(tag methods)取代了fallback机制。标记方法是只在带有指定标记的值上执行的fallback。这让实现独立的继承机制成为可能。这种情况下不需要链式调用,因为一个标记的方法不会影响另一个标记的方法。

标记方法机制工作的很好,一直存续到Lua 5.0为止,我们在Lua 5.0实现了元表和元方法来取代标记和标记方法。元表只是普通的Lua table,所以可以用Lua直接操作,不需要特殊函数。就像标记一样,元表可以用来表示userdata和table的用户定义类型:所有“同类”对象应该共享同一个元表。不像标记,元表和他们的内容会在所有引用消失后自动被回收掉。(相反,标记和标记方法会等到程序结束才会被回收。)元表的引入同时简化了实现:标记方法需要在Lua核心代码里添加特殊的私有表示方法,元表主要就是标准的table机制。

下面的代码展示了Lua 5.0里,继承是如何实现的。index元方法取代了index标记,元表里则是用index域来表示。代码通过将b的元表里的index域指向a,实现了b继承a。(一般情况下,index元方法都是函数,但我们允许它设为table,以直接支持简单的委托继承。)

1
2
3
4
a=Window{x=100, y=200, color="red"}
b=Window{x=300, y=400}
setmetatable(b, {__index = a})
print(b.color) —>red

6.9 C API
Lua提供了C函数库和宏,允许宿主和Lua通信。这层Lua和C之间的API是Lua的一个主要组成部分;它让Lua变成了一门嵌入式语言。

就像Lua的其他部分一样,API在Lua的演进里发生了很多变化。可惜,和语言本身相比,API设计很少受到外来影响,主要是因为这方面很少研究活动。

API一直是双向的,因为从Lua 1.0开始,我们就考虑了从C调用Lua,以及从Lua调用C,并认为两者同样重要。能从C调用Lua让Lua变成了扩展语言,即一门用于通过配置、宏和其他终端用户定制功能,来扩充应用程序特性的语言。能从Lua调用C让Lua变成了可扩展语言,因为我们可以用C函数扩展Lua,为其提供新设施。(这就是为什么我们说Lua是一门可扩展的扩展语言【30】)这两个视角的共同点是,API必须处理好C和Lua之间的失配:C是静态类型的,Lua是动态类型的,C是人工管理内存,Lua是自动进行垃圾回收的。

目前,C API通过引入一个虚拟栈来交换Lua和C的数据,解决了这两个失配问题。每个Lua的C函数调用都会使用一个新的栈帧,包含了函数调用的参数。如果C函数需要给Lua返回值,它就在返回前把这些值压到栈上。

每个栈槽可以保存任一Lua值类型。每个Lua类型都有C的唯一表示(比如字符串和数字),目前有两个API 函数:一个是注入函数,用于压栈一个Lua值,对应给出的C值;一个是投影函数,可以返回一个C值,对应指定栈位置的Lua值。Lua值类型里没有对应C表示的(比如table和函数),可以通过API及他们的栈位置来操作。

实际上,所有的API函数都从栈上获得他们的操作数,并将结果放到栈上。因为栈可以存放任意Lua值类型,这些API函数都可以操作任意Lua类型,因此解决了类型失配问题。为了避免C使用中的Lua值被回收掉,栈上的所有值都不会被回收。当一个C返回,它的Lua栈帧消失了,所有的C函数使用的Lua值都会被释放。如果没有其他引用,这些值最终都会被回收。这就解决了内存管理的失配问题。

我们花了好多时间才实现成现在这套API的样子。为了讨论API的演进,我们以等价于如下Lua函数的C代码进行说明:

1
2
3
function foo(t)
return t.x
end

换句话说,这个函数接受一个单参数,参数类型应该是table,并返回table里储存在x域的值。尽管很简单,这个例子足够说明API里的三个重要问题:如何获取参数,如何索引table,如何返回值。

在Lua 1.0里,foo函数的C代码如下:

1
2
3
4
5
void foo_l(void) {
lua_Object t = lua_getparam(1);
lua_Object r = lua_getfield(t, "x");
lua_pushobject(r);
}

注意,我们所索引的值是存储在字符串索引x,因为t.x是t[“x”]的语法糖。而且所有的API都是以lua_(或者LUA_)开头的,以此避免和其他C库的命名冲突。

为了暴露这个C函数给Lua,并使用foo作为函数名,我们会这样做

lua_register(“foo”, foo_l);
之后,foo就能像普通Lua函数一样从C调用了:

t = {x = 200}
print(foo(t)) —> 200
API的一个关键部件是lua_Object类型,定义如下:

typedef struct Object *lua_Object;
简而言之,lua_Object是一个抽象类型,用于在C里表示Lua值。传递给C函数的参数,通过调用lua_getparam获得,这个函数会返回lua_Object。在上述例子里,我们调用一次lua_getparam来获得table,这个table应当是foo函数的第一个参数。(其余的参数会自动略过。)当table在C里变成可用的(作为lua_Object),我们就可以通过调用lua_getfield函数获取x域的值。这个值在C里也是表示为lua_Object,最后会通过lua_pushobject压栈,送回到Lua中去。

栈也是API的另一个关键部件。它用来从C里传值给Lua。每个Lua类型都有对应的C版本push函数:number类型有lua_pushnumber,string类型有lua_pushstring,特殊值nil可以用lua_pushnil。也有允许C回传任意Lua值到Lua的lua_pushobject函数。当一个C函数返回,所有栈上的值都会返回给Lua,作为C函数的结果(Lua的函数可以返回多个值)。

概念上,lua_Object是一个union类型,因为它可以指代任意Lua值。很多脚本语言,比如Perl,Python和Ruby,依然使用union类型来在C层表示它们的值。这种表示方法的主要弊端,在于很难为语言设计垃圾回收。没有额外信息的情况下,垃圾回收器不可能知道,一个值是否被C代码里的一个union值任用。没有这个信息,垃圾回收器有可能回收值,导致union变成了悬空指针。即使这个union是C函数的局部变量,这个C函数还是能再次调用Lua,并触发垃圾回收流程。

Ruby通过检查C栈来解决这个问题,这种方法难以移植。Perl和Python则是以另一种方式实现,通过为union类型提供显式引用计数函数来解决这个问题。一旦你增加了一个值的引用计数,垃圾回收器就不会回收那个值了,直到你将计数减为0。但是,让程序员保证引用计数的正确性并不容易。很容易会产生错误,但后期很难查出来(对内存泄漏和悬空指针排查过的人都可以作证)。此外,引用计数解决不了循环引用的问题。

Lua从来没有提供过这类引用计数函数。Lua 2.1之前,为了保证一个未被引用的lua_Object不被回收,你可以做的事情不多,顶多就是自己持有这个对象的引用,并避免调用Lua。(只要你可以保证union指向的值同样储存在一个Lua变量里,你就是安全的。)Lua 2.1带来了一个重要的变化:它跟踪了所有传给C的lua_Object,保证它们在C函数活跃的时候不会被回收掉。当C函数返回到Lua之后,(只有在这个时候)所有这些lua_Object的值引用都会被释放,所以它们可以被回收。【JNI使用一个类似的方法来处理局部引用】

更具体地说,Lua 2.1里,一个lua_Object不再是一个指向Lua内部数据结构的指针,而是一个内部数组的索引,这个内部数组存储了所有传给C的值:

typedef unsigned int lua_Object;
这个改动让lua_Object的使用变得可靠了:当一个值在数组里,Lua就不会回收它。当C函数返回了,整个数组就被释放,并回收掉没有其他引用的函数所使用的值。(这个改动也给实现垃圾回收带来了更多的灵活性,因为它可以按需移动对象了;但是,我们没有这么做)

对于简单的使用,Lua 2.1的行为是非常实际的:这种方案安全,C程序员也不用担心引用计数。每个lua_Object就像C里的一个局部变量:对应的Lua值会保证处理它的C函数的生命期内都存活。对于复杂的使用场景:这个简单的方法有两个缺陷,需要额外的机制保证:有时候,lua_Object的值需要锁定一段比C函数生命期更长的时间;有时候,需要更短的锁定时间。

第一种情况有一个简单的解决办法:Lua 2.1引入了引用系统。函数lua_lock从栈上取一个Lua值,并返回一个引用。这个引入是一个数字,可以以后任意时间使用来获得Lua值,使用lua_getlocked函数即可。(同时还有lua_unlock函数,用于销毁一个引用)通过这种引用方法,要保存非本地的C变量就变得简单了。

第二种情况更为微妙。存在内部数组的对象只有函数返回的时候才会被释放。如果函数使用了太多的值,就会发生数组越界,或者内存不足错误。比如,考虑以下的高阶迭代函数,会迭代调用函数并打印结果,直到调用返回nil:

1
2
3
4
5
6
7
8
9
10
void l_loop(void) {
lua_Object f = lua_getparam(1);
for (;;) {
lua_Object res;
lua_callfunction(f);
res = lua_getresult(1);
if (lua_isnil(res)) break;
printf("%s\n", lua_getstring(res));
}
}

这段代码的问题是,每个调用返回的字符串都不能回收,直到循环的底部(整个函数结束为止),因此有可能发生数组越界或者内存耗尽。这种错误很难追踪,因此Lua 2.1的实现设置了内部数组保存lua_Object的上限。Lua会报错:“C函数里太多对象”而非泛泛的“内存耗尽”错误,让错误变得容易跟踪,但没有完全避免这一问题。

为了解决这个问题,Lua 2.1的API提供了两个函数,lua_beginblock和lua_endblock,为lua_Object的值创建动态范围(blocks);所有在lua_beginblock之后创建的值,都会在对应的lua_endblock调用后,从内部数组里被移除。但是,因为块原则(block discipline)不能强制让C程序员遵守,很容易就会忘记使用这些区块。而且,这些显式作用域控制用起来有点棘手。比如,一个欠周到的修复前述例子的方法,是用块包裹住for循环的循环体,但这样做存在问题:我们需要在break语句所在位置,也调用lua_endblock。这个Lua对象扩大生命周期的困难,经历了数个版本,最后在Lua 4.0才解决,我们重构了整个API。无论如何,就像我们之前提到的那样,对于普通应用,API是非常易用的,而且大部分程序员永远不会遇到这里描述的情景。更重要的是,API是安全的。错误用法会产生定义好的错误,但不会产生悬空指针或者内存泄露。

Lua 2.1还为API带来了其他改变。其中之一是引入了lua_getsubscript,允许使用任何值来索引table。这个函数没有显式参数:它从栈上取出table和key。老的lua_getfield被重定义为宏,以实现兼容:

1
2
3
#define lua_getfield(o,f) \
(lua_pushobject(o), lua_pushstring(f), \
lua_getsubscript())

(C API的向后兼容性通常用宏来实现,只要代价可以接受。)

除了上述修改,从语法上讲,API从Lua 1到Lua 2的变化是很少的。比如说,我们的说明函数foo可以直接用Lua 1.0的版本,在Lua 2.0依然可以运行。lua_Object的含义变了,lua_getfield用新的原语实现了,但对于普通用户来说,就好像没有变化一样。因此,API一直很稳定,持续到Lua 4.0版本。

Lua 2.4扩展了引用机制以支撑弱引用。Lua程序的一个常见设计,是使用一个Lua对象(通常是一个table)作为一个C对象的代理。常见的场景是,C对象必须知道它的代理是什么,并保留对代理的引用。但是,那个引用会阻止对代理对象的回收,即使对象从Lua里不能再访问了。在Lua 2.4版本,就可以创建一个弱引用指向proxy;那个引用不会阻止代理对象的回收。获取一个被回收的引用将返回一个特殊值LUA_NOOBJECT。

Lua 4.0为C API带来了两个新颖的设计:支持多个Lua虚拟机和用于C和Lua交换值的虚拟栈。要实现多个独立的Lua虚拟机,就需要去掉所有全局的虚拟机。直到Lua 3.0为止,都只有一个Lua虚拟机存在,而且使用了很多静态变量,分散在代码里。Lua 3.1 引入了多个独立Lua虚拟机;所有静态变量都归集到一个单一的C结构体里去。添加了一个新的API来切换虚拟机,但任意时刻只有一个Lua虚拟机是活跃的。所有其他API函数都是针对活跃虚拟机操作的,而活跃虚拟机并没有出现在调用里。Lua 4.0在API引入了显式的Lua虚拟机。这引起了和前述版本的不兼容。【17】所有和Lua通讯的C代码(尤其是向Lua注册的C函数)需要在C API中添加一个显式的虚拟机变量。既然所有C函数都必须重写,我们借此机会在Lua 4.0里大改了C-Lua交流方式:我们替换了lua_Object的概念,代之以显式的虚拟栈,用来在Lua和C之间双向通信。这个栈也能用来存储临时值。

在Lua 4.0里,我们的foo函数可以改写如下:

1
2
3
4
5
int foo_l (lua_State *L) {
lua_pushstring(L, "x");
lua_gettable(L, 1);
return 1;
}

第一个区别是函数签名:foo_l现在接受一个Lua虚拟机,用于接收操作数和返回函数返回值。在之前版本里,函数调用完毕后,所有残留在栈上的值都会返回Lua。现在,因为所有操作都使用栈,它可以包含除返回值外的中间值,所以函数需要告诉Lua,栈上到底有几个元素是返回值。另一个区别是,不再需要lua_getparam了,因为函数参数在函数开始时就在栈上,并可以透过他们的index直接访问,就好像其他栈上的值一样。

最后的区别是lua_gettable的使用,这个函数用于替代lua_getsubscript,用于访问table里的域。lua_gettable接受一个栈索引(而不是一个Lua对象),索引指向所操作的table,从栈顶弹出key,并将结果压栈。该函数不改变table在栈中的位置,因为table经常重复索引。在函数foo_l里,lua_gettable使用的table在栈位置1,因为他是函数的第一个参数,key是字符串”x”,需要在调用lua_gettable之前压栈。这个函数调用将栈中的key,换成了对应的table里的值。所以,在lua_gettable之后,栈上有两个值:栈位置1的table,和栈位置2上key索引到的结果。这个C函数返回1,告诉Lua使用栈顶的值作为函数的唯一返回值。

为了更清楚的说明新的API,这是我们循环例子的Lua 4.0实现:

1
2
3
4
5
6
7
8
9
10
int l_loop (lua_State *L) {
for (;;) {
lua_pushvalue(L, 1);
lua_call(L, 0, 1);
if (lua_isnil(L, -1)) break;
printf("%s\n", lua_tostring(L, -1));
lua_pop(L, 1);
}
return 0;
}

为了调用一个Lua函数,我们将它压到栈上,并压入其参数(上面的例子没有)。然后我们调用lua_call,告诉lua要从栈上获取多少个参数(因此隐式的表达了函数在栈上的位置),要在调用里获取多少个结果。在例子里,我们没有使用参数,期待一个结果。lua_call函数将函数和参数从栈上移除,并压入指定数目的结果。调用lua_pop会从栈上去掉一个返回值,让栈在循环开始时处在相同的位置。为方便起见,我们可以用正数索引栈,表示从底部开始的位置,或者负数来表示从栈顶开始的位置。在示例中,我们使用-1作为索引,让lua_isnil和lua_tostring从栈顶开始索引元素,该位置包含了函数调用的结果。

后见之明,在API里使用一个单独的栈是很明显的简化措施,但是Lua 4.0发布的时候,很多用户都抱怨新api的复杂性。尽管Lua 4.0的API有更简洁的概念模型,直接操作栈还是需要一些思考来保证正确性。很多用户都宁愿用前一版的API,即使它没有任何清晰的概念模型来表达其原理。简单的任务不需要概念模型,之前的API因此工作的很好。更复杂的任务经常会打破用户自定义的自有模式,因为大部分的用户从未用C来编码复杂项目。所以,新API第一眼看上去太复杂了。但是,这些怀疑论最后都消失了,因为用户理解并肯定了新模型,这个模型证明了自身的简洁、不易出错。

Lua 4.0多个虚拟机共存的可能性,导致了有关引用机制的意料之外的问题。之前,一个C库如果需要保留固定对象,可以创建这个对象的引用,并存储在全局C变量里。在Lua 4.0,如果一个C库是和多个虚拟机协作的,它就需要为每个虚拟机保留单独的引用,不能用全局C变量来表示了。为了解决这个难题,Lua 4.0引入了注册表(registry),注册表就是一个普通的Lua表,只在C层使用。有了注册表,C库想要保留Lua对象,就能选择唯一的key索引,并在注册表里用这个key来关联对象。因为每个独立的Lua虚拟机都有自己的注册表,C库可以在所有虚拟机里都使用同样的key来操作对应的对象。

我们可以轻易的在注册表上实现之前的引用机制,只需要使用整数key来表示索引即可。要创建一个新的索引,我们只需要找到没使用的整数key,并在那个key存储值。获取一个引用就是一个简单的table访问。但是,我们不能使用注册表实现弱引用。所以,Lua 4.0保持了之前的引用机制。Lua 5.0里,由于语言本身引入了弱表,我们终于可以将核心里的引用机制干掉,并移到库里。

C API在向着完整性缓慢演进。从Lua 4.0开始,所有标准库函数都可以只使用C API来编写。在Lua 4.0之前,Lua有数目可观的内置函数(Lua 1.1有7个,Lua 3.2有35个),大部分可以使用C API编写,但因为追求速度,我们并没有这样做。有少量内置函数是不能用C API来编写的,因为C API并不完整。比如,Lua 3.2之前是不可能用C API来遍历table的内容的,尽管在Lua里可以使用内置函数next去实现。目前,C API也尚不完整,不是所有Lua能干的都可以用C API完成;比如,C API还缺少对Lua值进行算术运算的函数。我们计划下个版本解决这个问题。

6.10 userdata

从第一版开始,Lua的一个重要特性就是可以直接操作C数据,这个特性是通过提供userdata这种特殊Lua数据类型来实现的。这种能力是Lua可扩展性的基础。

对于Lua程序来说,userdata类型在Lua的演进里从未改变:尽管是第一类值类型,userdata还是透明的类型,在Lua里唯一有效的操作就是相等性判断。其他针对userdata的操作(比如创建,探测,修改)都需要C函数提供。

对于C函数而言,userdata类型在Lua的演进里经历很多变更。在Lua 1.0里,一个userdata值只是一个简单的void*指针。这种简化方法等主要问题是,C库没有办法检查一个userdata是不是有效的。尽管Lua代码不能创建userdata值,但可以将一个库创建的userdata传给另一个库,而另一个库期待的其实是指向不同结构的指针。因为C函数没有机制检查这种失配,指针适配往往对应用程序造成致命后果。我们一直认为,Lua程序导致宿主程序崩溃是不可接受的。Lua应该是一门安全的语言。

为了克服指针失配的问题,Lua 2.1引入了标记的概念(也是Lua 3.0里标记方法的概念来源)。一个标记只是一个任意的整数值,关联到一个userdata上。一个userdata的标记只能在创建时设置一次,假设每个C库都使用自己的独特的标记,C代码可以检查userdata的标记,确保userdata就是所期待的类型。(一个库作者如何选择不会和别人重复的tag还是一个问题。这个问题只有在Lua 3.0才得到解决,在这个版本Lua提供了lua_newtag作标记管理)

Lua 2.1更大的问题是C资源的管理。userdata往往指向一块在C里动态分配的内存,当对应userdata在Lua里被回收,这块内存也需要释放掉。但是,userdata是值,不是对象。因此,他们不会被回收(就像number不会被回收一样)。为了克服这个限制,一个典型的设计是利用table作为C结构在Lua中的代理,将实际的userdata存储在预定义的域里。当table被回收,它的析构函数就释放对应的C结构。

这种简单的解决方案会产生潜在问题。因为userdata是存在代理table的一个普通域里面,一个恶意的用户有可能在Lua里污染它。特别是,一个用户可以创建userdata的副本,并在table被回收后继续使用这个副本。这个时候,对应的C结构已经被销毁了,userdata就变成悬空指针了,这会带来灾难性后果。为了改善对userdata生命周期的控制,Lua 3.0将userdata从值类型改为对象类型,以便垃圾回收。用户可以使用userdata析构函数(垃圾回收的标记方法)来释放对应的C结构。Lua的垃圾回收器的正确性,保证了userdata不能在回收后再被使用。

但是,userdata作为对象会带来一致性问题。给出一个userdata,要获得对应的指针是很简单的,但是我们经常需要做相反的事:给出一个C指针,获得对应的userdata。在Lua 2中,两个具有相同指针和相同标记的userdata就是相同的;相等性是基于它们的值。所以,给出指针和标记,我们就能获得userdata。在Lua 3,因为userdata变成了对象,所以相等性变成了一致性:两个userdata只有在两者同一的情况下才会相同(也就是说,这两个userdata是同一个)。每个userdata的创建都不一样。因此,一个指针和标记不足以获得对应的userdata。

为了解决这一困难,并解决与Lua 2的不兼容问题,Lua 3采用了下面的语意来将一个userdata压到栈上:如果Lua已经有一个userdata,拥有给出的指针和标记,那么那个userdata就会被推到栈上;否则就创建一个新的userdata并压入栈。所以,C代码要翻译一个C指针倒对应的userdata就变简单了。(实际上,C代码可以沿用Lua 2的。)

但是,Lua 3的行为有一个主要缺陷:它将两个基本操作合成了一个原语(lua_pushuserdata):搜索userdata和创建userdata。比如,没有办法在不创建userdata的情况下,检查一个给定的C指针有没有对应的userdata。而且,也不能不管C指针,直接创建一个新的userdata。如果Lua已经有对应值的userdata,就不能再创建新的userdata了。

Lua 4引入了新函数lua_newuserdata以缓解这个问题。不像lua_pushuserdata,这个函数总是会创建一个新的userdata。而且,更重要的是,这些userdata可以存储任意C数据,而不仅仅是指针。用户可以告诉lua_newuserdata需要分配多少内存,lua_newuserdata会返回一个指针,指向所分配的内存。通过让Lua给用户分配内存,很多常见的与userdata相关的任务都简化了。比如,C代码不需要处理内存分配错误,因为Lua已经接手了。更重要的是,C代码不需要处理内存释放:这种userdata使用的内存会在userdata被回收的时候,被Lua自动释放。

但是,Lua 4依然没有提供一个漂亮的解决方案来应对搜索问题(即通过给定C指针寻找一个userdata)。所以,它保留了lua_pushuserdata的行为,结果产生了一个混合的系统。只有在Lua 5的时候,我们才删除了lua_pushuserdata,并去掉userdata创建和搜索的关联。实际上,Lua 5将整个搜索设施都移除了。Lua 5也引入了轻userdata(light userdata),只用来存放C指针,跟Lua 1的普通userdata完全一致。应用程序可以使用弱表来关联C指针(light userdata所包含的)和对应的“重”userdata。

正如其他Lua演进一样,Lua 5的userdata比Lua 4的更加灵活;也更容易解释和实现。对于简单的用例,比如只需要存储一个C结构,Lua 5的userdata就很易用。对于更为复杂的需求,比如需要将一个C指针映射到一个Lua的userdata,Lua 5提供了机制(light userdata和弱表)给用户来实现适合宿主应用的策略。

6.11 反射性

Lua从刚开始的版本就支持一些反射设施。提供支持的主要原因是,Lua的建议用法是作为替换SOL的配置语言。正如第四章所述,我们的设想是,程序员若有需要,可以使用语言本身,写日常的类型检查。

比如,如果一个用户写了下面的代码:

T = @track{ y=9, x=10, id=”1992-34” }
我们希望可以检查到,track的确有一个域y,这个域就是一个数字。我们也希望可以校验出track没有外来的域(比如检查出打字输入错误)。为了完成这两个任务,我们需要访问到Lua值的类型,以及遍历一个table的机制,并访问所有键值对。

Lua 1.0用两个函数完成了所需的功能,这两个函数延续到今天:type和next。type函数返回一个描述给定值的类型的字符串(比如number, nil, table之类)。next函数接受一个table和一个键,并返回一个table里的“下一个”键(以任意顺序)。以next(t, nil)调用会返回“第一个”键。通过next函数,我们可以遍历一个table并处理所有的键值对。比如,下面的代码可以打印table t的所有键值对:

k = next(t, nil)
while k do
print(k, t[k])
k = next(t,k)
end
所有这些函数都有一个简单的实现:type检查给定值的内部标记,并返回对应的字符串;next根据内部table的表示,在table里寻找给定的key并访问下一个key。

在Java和Smalltalk这些语言里,反射需要具体的概念比如类、方法、实例变量。而且,这种具像化还需要新的概念比如元类(具体类的类)。Lua不需要这些东西。Lua里,大部分类似Java反射包提供的设施都是免费的:类和模块都是table,方法就是函数。所以,Lua不需要任何特别的机制来具像化;它们就是朴素的程序值。类似的,Lua不需要特殊机制来在运行时建立方法调用(因为函数是第一类值,而且Lua的参数传递机制天然支持可变数量参数传递),也不需要特殊的机制来访问一个全局变量或者根据一个给定的名字访问一个实例变量(因为它们都是普通的table域)。

7 回顾

这一节我们会给出一个Lua演进的简短评论,讨论其中做得好的,我们后悔的,以及我们不后悔,但是觉得可以以不同方式实现的。

最高明的决策,莫过于Lua 1.0的时候决定使用table作为Lua唯一的数据结构机制。table被证明是强大而高效的。table在语言里的核心地位和它的实现,是Lua的主要特性。我们顶住了用户要求添加其他数据结构的压力,主要是添加“真正的”数组和元祖,对此我们表现得十分顽固,但我们也提供了高效实现的table和灵活的设计。比如,我们可以实现一个set(集合),只需要将它的元素作为table的key即可。全赖Lua的table接受任意值作为key,这个特性才得以实现。

另一个则是我们对可移植性的坚持,最初是因为Tecgraf客户处在大量不同的平台上。这允许Lua运行在我们从未梦想得到支持的平台上。特别是,可移植性是Lua被游戏开发广泛采用的原因之一。受限的环境,比如游戏主机,一般不支持完整语意的全套标准C库。最终,通过减少Lua核心对标准C库的依赖,我们将Lua核心打造成只依赖独立的ANSI C实现标准。这一举措主要是为了嵌入的灵活性,但同样增加了可移植性。比如,从Lua 3.1开始,只要摆弄代码里少数几个宏,就可以让Lua使用一个应用程序自定义的内存分配器,而不是直接使用malloc等内存分配函数。从Lua 5.1开始,内存分配器可以在创建Lua虚拟机的时候动态提供。

事后来看,我们认为在一个小型委员会的抚养下成长,对于Lua的演进是十分积极的。由一个庞大委员会设计的语言,倾向于变得十分复杂,从未完全实现过它们支持者的期望。大部分成功的语言都是被抚养长大,而非设计出来的。它们遵循着一个缓慢的自下而上的过程,以一门小语言开始,带着谦虚的目标。语言的成长是真实用户实际反馈的成果,这个过程里设计渐渐浮出水面,实际有效的新特性得到认同。这个描述正是对Lua演进的真实写照。我们倾听用户的反馈和建议,但我们只在三人同意的前提下才添加一个新的特性;否则,就留待未来决定。稍后再添加特性比移除特性要容易得多。这个开发过程对于保持语言的简洁性至关重要,而简洁性是我们最重要的资本。绝大部分Lua的其他特性——速度、体积小、可移植性——都是从简洁性而来。

从第一个版本开始,Lua就有真实用户了,也即是除了我们自己以外的用户,他们根本不关心语言本身,只关心如何高效率的使用这门语言。用户总是为语言做出重要贡献,通过建议,抱怨,使用报告,和问题等形式。我们的小小委员会在管理这类反馈的时候再一次扮演了重要角色:它的结构给我们足够的惯性,去贴近用户、倾听用户,但不完全遵循它们的建议。

Lua的最佳描述是:一个封闭开发的开源项目。这意味着,尽管源码对于审查和采用是免费可得的,Lua还是没有以合作的方式开发。我们会采纳用户建议,但从未逐字逐句采纳他们的代码。我们总是尝试实现我们自己的设计。

Lua演进的另一个不寻常的方面,是我们处理不兼容修改的方式。很长一段时间里,我们都认为简洁和优雅比兼容前一个版本更为重要。当一个老的特性被新特性取代,我们就删掉老的特性。我们经常(但非总是)提供一些兼容性工具,比如一个兼容的库,一个转换脚本,或者编译时的选项来保留老特性。在这些情形下,用户需要在转移到新版本的时候采取一定措施。

我们并未真正后悔这种演进方式。但是,我们最终变得更为保守了。不仅因为我们的用户和代码基变得比以前更大,还因为我们觉得作为一门语言,Lua变得更成熟了。

我们应该从一开始就引入布尔类型,但我们希望从最为简洁的语言开始。没有一开始就引入布尔类型,带来了一些不幸的副作用。其中一个是我们现在有两个false值了:nil和false。另一个是,Lua函数用于警告调用者错误发生的常见用法,是返回nil值接着一段错误信息。如果用false而非nil,就更适合这个场景了,nil可保留作为缺少值的含义。

在算术运算时,自动将字符串转化为数字,是我们从Awk里参考的,其实可以忽略。(在字符串操作中自动将数字转换为字符串则很方便,也更少问题。)

除了我们的“机制,而非策略”的规则外——我们觉得在Lua演进里这个规则很有价值——我们应该为模块和包提供一个准确的指导方针。缺乏通用的构建模块和安装包的策略,限制了不同工作组共享代码,不利于发展整个社区的代码基。Lua 5.1提供了模块和包的指导方针,我们希望能够补救现存的局面。

正如6.4节提到的,Lua 3.0引入了条件编译的支持,主要是为了提供一种方式来关闭代码。我们收到很多请求,要求加强Lua的条件编译,这些请求甚至来自于不使用条件编译的用户!目前最大的需求是一个完整的宏处理器,就像C的预处理器一样。提供这样一个宏处理器,跟我们提供扩展机制的哲学是一致的。但是,我们希望宏也能用Lua编写,而不是其他特殊语言。我们不希望直接在词法器上添加宏设施,以免其变得臃肿不堪,拖慢编译过程。更重要的是,那个时候Lua语法分析器还不是完全可重入的,所以没有办法在词法器里调用Lua。(这个限制在Lua 5.1去掉了)所以邮件列表里和Lua开发者里有无穷无尽的讨论。我们依然希望能够为Lua提供一个宏系统:它会给Lua提供更灵活的语法,支持更灵活的语义。

8 结论

Lua在很多大型公司里大获成功,比如Adobe,Bombardier,Disney,Electronic Arts,Intel,LucasArts,Microsoft,Nasa,Olivetti和Philips。这些公司很多产品都直接在商用产品里嵌入Lua,并向最终用户暴露Lua脚本。

Lua在游戏领域特别成功。最近流行一个说法:“Lua正迅速变成游戏脚本的事实标准”【37】。两个在gamedev.net进行的非正式的投票【5,6】,分别在2003年9月和2006年6月结束,结果显示Lua正是最炙手可热的游戏开发脚本语言。GDC专门针对游戏开发中的Lua的圆桌会议举行了两次,分别是2004年和2006年。很多著名游戏都使用Lua:Baldur’s Gate, Escape from Monkey Island, FarCry, Grim Fandango, Homeworld 2, Illarion, Impossible Creatures, Psychonauts, The Sims, World of Warcraft。现在有两本使用Lua进行游戏开发的书【42,25】,而且很多其他游戏开发的书都会开辟专门的章节来讲述Lua【23,44,41,24】。

Lua在游戏界的广泛使用是我们的惊喜收获。我们并没有为Lua考虑过游戏开发这个目标。(Tecgraf主要考虑的是科学软件)事后看来,这种成功是可以理解的,因为所有让Lua变得与众不同的特质,对游戏开发都十分重要:

可移植性:很多游戏跑在非传统的平台上,比如游戏机上,这些平台需要特殊的开发工具。构建Lua只需要一个ANSI C编译器。

容易嵌入:游戏是性能敏感型应用。他们既需要性能,来应付图形和模拟,还需要灵活性,以应对创意的部分。很多游戏都用至少两种语言开发,这并非偶然,一种语言用于脚本编写,另一种用于构建引擎。在这个框架内,轻松集成Lua到其他语言(在游戏界主要是C++)是一个巨大的优势。

简洁:很多游戏设计师,脚本编写者,关卡作者都不是专业的程序员。对他们来说,一门语法简洁,语义清晰的语言尤其重要。

高效率,小体积:游戏是性能敏感应用程序;分配给脚本运行的事件通常都很小。Lua是最快的脚本语言之一【1】。游戏机是受限环境。脚本解释器应该节约资源。Lua核心的体积只有100K。

对代码的控制:不像其他大部分的软件企业,游戏产品很少演化。很多时候,一个游戏发布之后,就没有更新或者新版本了,只有新游戏。所以,冒险在游戏里使用一门新的脚本语言更容易。一门脚本语言是否会演进,如何演进,对于游戏开发者来说不是一个很关键的点。他们所需要的就只是在游戏里使用的版本。因为他们对Lua源码有完全访问的权限,他们可以选择永远保持在同一个Lua版本。

自由的协议:很多商业游戏是不开源的。很多游戏公司甚至拒绝使用任何开源代码。因为竞争非常激烈,所以游戏公司倾向于保守技术上的秘密。对他们来说,像Lua一样的自由协议是非常方便的。

协程:如果脚本语言支持多任务,编写脚本游戏会变的更简单,因为一个角色或者活动可以被暂停,并稍后恢复。Lua通过协程支持协作式多任务【14】。

过程式数据文件:Lua的原始设计目标是提供威力强大的数据描述设施,这允许游戏使用Lua组织数据文件,替换特殊格式的文本数据文件,带来诸多益处,尤其是同源性和表达力。

致谢
Lua是在大家的帮助下成长起来的。Tecgraf的所有人都以不同形式为Lua贡献——使用这门语言,讨论它,向Tecgraf以外传播它。特别鸣谢Marcelo Gattass,作为Tecgraf的主管,总是鼓励我们并给予我们完全的自由,去演进Lua这门语言和它的实现。Lua不再是Tecgraf的一个产品,但依然在PUC-Rio内开发,开发组属于2004年5月建成的LabLua实验室。

没有用户的话,Lua就只是另外一门语言,注定被遗忘。用户和他们的使用是一门语言的试金石。特别鸣谢我们邮件列表的成员,感谢他们的建议、抱怨和耐心。邮件列表相对较小,但非常友善,并有一些Lua开发团队以外的技术水平很高的人士参与,他们慷慨的向整个社区分享自己的专业知识,让人获益匪浅。

我们感谢Norman Ramsey的牵头,Norman Ramsey建议我们在HOPL III上发表一篇有关Lua的论文,并帮助我们联系到会议管理团队。我们感谢Julia Lawall,帮助我们通读数遍这篇文章的草稿,并代表HOPL III委员会仔细处理本文。我们感谢Norman Ramsey,Julia Lawall,Brent Hailpern, Barbara Ryder,以及未具名的编辑,感谢他们详细的评论和富有建设性的建议。

我们也向Andre ́ Carregal, Anna Hester, Bret Mogilefsky, Bret Victor, Daniel Collins, David Burgess, Diego Nehab, Eric Raible, Erik Hougaard, Gavin Wraith, John Belmonte, Mark Hamburg, Peter Sommerfeld, Reuben Thomas, Stephan Herrmann, Steve Dekorte, Taj Khattra和Thatcher Ulrich致谢,感谢他们对Lua发展历程的补充,并对相关文本进行润色。Katrina Avery做了很好的拷贝-编辑工作。

最后,我们感谢PUC-Rio,IMPA和CNPq对我们Lua工作一如既往的支持,以及FINEP和微软研究院对数个Lua相关项目的支持。

相关文献:

  1. The computer language shootout benchmarks. http://shootout.alioth.debian.org/.
  2. Lua projects. http://www.lua.org/uses.html.
  3. The MIT license. http://www.opensource.org/licenses/mit-license.html.
  4. Timeline of programming languages. http://en.wikipedia.org/wiki/Timeline of programming languages.
  5. Which language do you use for scripting in your gameengine? http://www.gamedev.net/gdpolls/viewpoll.asp?ID=163, Sept. 2003.
  6. Which is your favorite embeddable scripting language?http://www.gamedev.net/gdpolls/viewpoll.asp?ID=788, June 2006.
  7. K. Beck. Extreme Programming Explained: EmbraceChange. Addison-Wesley, 2000.
  8. G. Bell, R. Carey, and C. Marrin. The Virtual RealityModeling Language Specification—Version 2.0.http://www.vrml.org/VRML2.0/FINAL/, Aug. 1996.(ISO/IEC CD 14772).
  9. J. Bentley. Programming pearls: associative arrays. Communicationsof the ACM, 28(6):570–576, 1985.
  10. J. Bentley. Programming pearls: little languages. Communicationsof the ACM, 29(8):711–721, 1986.
  11. C. Bruggeman, O. Waddell, and R. K. Dybvig. Representingcontrol in the presence of one-shot continuations. InSIGPLAN Conference on Programming Language Designand Implementation, pages 99–107, 1996.
  12. W. Celes, L. H. de Figueiredo, and M. Gattass. EDG: umaferramenta para criac¸ ˜ao de interfaces gr´aficas interativas.In Proceedings of SIBGRAPI ’95 (Brazilian Symposium onComputer Graphics and Image Processing), pages 241–248,1995.
  13. B. Davis, A. Beatty, K. Casey, D. Gregg, and J.Waldron. Thecase for virtual register machines. In Proceedings of the 2003Workshop on Interpreters, Virtual Machines and Emulators,pages 41–49. ACM Press, 2003.
  14. L. H. de Figueiredo, W. Celes, and R. Ierusalimschy.Programming advanced control mechanisms with Luacoroutines. In Game Programming Gems 6, pages 357–369.Charles River Media, 2006.
  15. L. H. de Figueiredo, R. Ierusalimschy, and W. Celes. Thedesign and implementation of a language for extendingapplications. In Proceedings of XXI SEMISH (BrazilianSeminar on Software and Hardware), pages 273–284, 1994.1.
  16. L. H. de Figueiredo, R. Ierusalimschy,and W. Celes. Lua:an extensible embedded language. Dr. Dobb’s Journal,21(12):26–33, Dec. 1996.
  17. L. H. de Figueiredo, C. S. Souza, M. Gattass, and L. C. G.Coelho. Gerac¸ ˜ao de interfaces para captura de dados sobredesenhos. InProceedings of SIBGRAPI ’92 (BrazilianSymposium on Computer Graphics and Image Processing),pages 169–175, 1992.
  18. A. de Moura, N. Rodriguez, and R. Ierusalimschy. Coroutinesin Lua. Journal of Universal Computer Science, 10(7):910–925, 2004.
  19. A. L. de Moura and R. Ierusalimschy. Revisiting coroutines.MCC 15/04, PUC-Rio, 2004.
  20. R. K. Dybvig. Three Implementation Models for Scheme.PhD thesis, Department of Computer Science, Universityof North Carolina at Chapel Hill, 1987. Technical Report#87-011.
  21. M. Feeley and G. Lapalme. Closure generation based onviewing LAMBDA as EPSILON plus COMPILE. Journal ofComputer Languages, 17(4):251–267, 1992.
  22. T. G. Gorham and R. Ierusalimschy. Um sistema dedepurac¸ ˜ao reflexivo para uma linguagem de extens˜ao.In Anais do I Simp´osio Brasileiro de Linguagens deProgramac¸ ˜ao, pages 103–114, 1996.
  23. T. Gutschmidt. Game Programming with Python, Lua, andRuby. Premier Press, 2003.
  24. M. Harmon. Building Lua into games. In Game Programming
    Gems 5, pages 115–128. Charles River Media, 2005.
  25. J. Heiss. Lua Scripting f¨ur Spieleprogrammierer. Hit theGround with Lua. Stefan Zerbst, Dec. 2005.
  26. A. Hester, R. Borges, and R. Ierusalimschy. Building flexibleand extensible web applications with Lua. Journal ofUniversal Computer Science, 4(9):748–762, 1998.
  27. R. Ierusalimschy. Programming in Lua. Lua.org, 2003.
  28. R. Ierusalimschy. Programming in Lua. Lua.org, 2nd edition,2006.
  29. R. Ierusalimschy, W. Celes, L. H. de Figueiredo, andR. de Souza. Lua: uma linguagem para customizac¸ ˜ao deaplicac¸ ˜oes. In VII Simp´osio Brasileiro de Engenharia deSoftware — Caderno de Ferramentas, page 55, 1993.
  30. R. Ierusalimschy, L. H. de Figueiredo, and W. Celes. Lua:an extensible extension language. Software: Practice &Experience, 26(6):635–652, 1996.
  31. R. Ierusalimschy, L. H. de Figueiredo, and W. Celes. Theimplementation of Lua 5.0. Journal of Universal ComputerScience, 11(7):1159–1176, 2005.
  32. R. Ierusalimschy, L. H. de Figueiredo, and W. Celes. Lua 5.1Reference Manual. Lua.org, 2006.1. [33] K. Jung and A. Brown. Beginning Lua Programming. Wrox,2007.
  33. L. Lamport. LATEX: A Document Preparation System.Addison-Wesley, 1986.
  34. M. J. Lima and R. Ierusalimschy. Continuac¸ ˜oes em Lua.In VI Simp´osio Brasileiro de Linguagens de Programac¸ ˜ao,pages 218–232,June 2002.
  35. D. McDermott. An efficient environment allocation schemein an interpreter for a lexically-scoped LISP. In ACMconference on LISP and functional programming, pages 154–162, 1980.
  36. I. Millington. Artificial Intelligence for Games. MorganKaufmann, 2006.
  37. B. Mogilefsky. Lua in Grim Fandango. http://www.grimfandango.net/?page=articles&pagenumber=2,May 1999.
  38. Open Software Foundation. OSF/Motif Programmer’s Guide.Prentice-Hall, Inc., 1991.
  39. J. Ousterhout. Tcl: an embeddable command language. InProc. of the Winter 1990 USENIX Technical Conference.USENIX Association, 1990.
  40. D. Sanchez-Crespo. Core Techniques and Algorithms inGame Programming. New Riders Games, 2003.
  41. P. Schuytema and M. Manyen. Game Development with Lua.Delmar Thomson Learning, 2005.
  42. A. van Deursen, P. Klint, and J. Visser. Domain-specificlanguages: an annotated bibliography. SIGPLAN Notices,35(6):26–36, 2000.
  43. A. Varanese. Game Scripting Mastery. Premier Press, 2002.
-------------本文结束 感谢阅读-------------