the evolution of lua

来源:互联网 发布:翻译软件 for mac 编辑:程序博客网 时间:2024/06/05 03:39

欢迎关注公众号《Lua探索之旅》。


声明:本文内容来自于 https://github.com/spin6lock/the_evolution_of_lua_zh_CN

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)。


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程序的指令数了。


Lua 1.x 版本


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协议了。对我们协议的疑问从此都消失了。


Lua 2.x 版本


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界面问世,创建新闻组变得无关紧要了。


Lua 3.x 版本


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。


Lua 4.x 版本


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里的所有键值对,可以这样写:

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 比较适合作为下个版本的版本号。


Lua 5.x 版本


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


将库函数放入table里面是一个巨大的冲击,因为这会影响到所有使用了至少一个库函数的程序。比如,老版本的strfind函数现在叫string.find(在string库里的find域,存储在叫string的table里);openfile变成了io.open;sin变成了math.sin,诸如此类。


为了让转换变的简单点,我们提供了一个兼容脚本,在里面定义了老函数去应用新函数:

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的初衷是增量垃圾收集,模块系统的改进可能是最容易留意到的。另一方面,增量垃圾收集没有被注意到,说明了它成功的避免了长时间的停顿。


原创粉丝点击