记一个脚本解释器的开发
最近可以有1个月左右的空闲,可以稍微整理一下这个脚本解释器的开发过程。
一、缘由
2014年左右,我们使用AIR技术,开发了一个3D战争类型的手游。那时候手游开发技术主要是cocos2d,unity,Air稍微小众一些,但是也有。那个时候正是AS3走下坡路的时候,BOSS耳软心活,一会要改用cocos,一会要改用unity,于是萌生了一个自己写一个as 3.0脚本解释器的想法。
二、关于actionscript3。
As3脚本语言,实际上就是ecmascript 262 V4的加强版,也就是说基本上js有的它都有,另外还有java的特性,包含完整的类继承,接口系统,还可以使用js的prototype原型链继承,2方面互不干扰,又可以互为补充,灵活又不失严谨。当年adobe和Mozilla提议将as3作为ecmascript 262 v4,但是受到了巨头公司(主要是微软)的反对,最终ecma没有发布 EcmaScript V4,而是发布了一个和谐版 V3.1。但是V4仍然保留了下来。当然如今已经是ecmascript 已经是6了,中间发生了苹果,安卓的崛起,wp的衰落,年年都是h5游戏元年这些事情大家都知道就不谈了。
a) As3的类继承 见代码,一看就懂吧,都不用解释,和c#基本没区别
package { [Doc] public class FuncTest{ public function FuncTest() ; } } /* 类是唯一可实现接口的 ActionScript 3.0 语言元素。在类声明中使用 implements 关键字可实现一个或多个接口。 下面的示例定义两个接口 IAlpha 和 IBeta 以及实现这两个接口的类 Alpha: */ interface IAlpha { function foo(str:String):String; } interface IBeta { function bar():void; } class Alpha implements IAlpha, IBeta { public function foo(param:String):String { trace("foo", param); return null; } public function bar():void { trace("bar");} } var a=new Alpha(); var alpha:IAlpha = a; var beta:IBeta = IBeta(alpha); alpha.foo("call foo"); beta.bar();
b) As3的原型链 见代码,会js的一看就明白,不用解释了吧.
package { [Doc] public class FuncTest{ public function FuncTest() { } } } /* 类继承 -- 是主要的继承机制,并支持固定属性的继承。固定属性是声明为类定义一部分的变量、常量或方法。现在,可通过存储相关类信息的特殊类对象表示每个类定义。 原型继承 -- 每种类都有一个关联的原型对象,而原型对象的属性由该类的所有实例共享。 在创建一个类实例时,它具有对其类的原型对象的引用,这将作为实例及与其关联的类原型对象间的链接。 运行时,如果在类实例中找不到某属性, 则会检查委托(该类的原型对象)中是否有该属性。 如果原型对象不包含这种属性, 此过程会继续在层次结构中连续的更高级别上对原型对象进行委托检查,直到找到该属性为止。 */ //类继承和原型继承可同时存在,如下例所示: class A { var x = 1 public function A() { A.prototype.px = 2 } } dynamic class B extends A { var y = 3 public function B() { B.prototype.py = 4 } } var b = new B() trace(b.x) // 1 via class inheritance trace(b.px) // 2 via prototype inheritance from A.prototype trace(b.y) // 3 trace(b.py) // 4 via prototype inheritance from B.prototype B.prototype.px = 5 trace(b.px) // now 5 because B.prototype hides A.prototype b.px = 6 trace(b.px) // now 6 because b hides B.prototype var b2=new B() trace(b2.px) // ==5
三、龙书。
编译原理号称有龙,虎,鲸三本圣经。我参考的是龙书。要写脚本解释器,网上确实有许多参考文章,但是大多都是简单的告诉你怎么用简单的技巧去人肉写代码解析,再或者就是叫你去用类似yacc这样的工具,我买了2本书,一本叫“自制编程语言”,一本叫“两周自制脚本语言”。这两本书我读了一下,确实可以自制语言,但是肯定是无法自制如as3这样的大型的语言的。我也尝试使用人肉代码解析,发现这根本就没办法进行下去,稍有地方出错,就要大量修改然后自己也搞不清了。因此,最后我决定怼龙书。在这里,我就直接说出我怼龙书的心得了
a) 龙书有中文版pdf。内容非常丰富,文字也易懂,我个人感觉,值得一读不愧圣经之名
b) 对于脚本解释器而言,只要看到LL(1)就行了。龙书提供了一个极度详细的算法,详细到几乎是一步一步的指导你构建一个First和Follow翻译算法。有了这个算法就可以自己构建文法分析器!
c) 关于LL(1)文法。确实LL(1)文法有许多限制的地方,比如左递归,二义性等,但是这些都是可以解决的,左递归手工慢慢消除,二义性书里也介绍了解决的方案,只要尝试一下,就可以过去。
d) 做出文法分析工具后,就只要不断的尝试去写文法说明,最终就能得到目标语言的文法分析器,进而生成语法树!这就是看龙书的收获
四、从语法树到运行时
我用了3个月的时间,做到了可以解析几乎任何as3代码的语法树。从一般意义上说,这时候只要顺着语法树执行,就可以跑起代码来了。但事实是,做到这一步后,发现后面还有一个更大的坑在等着:自动垃圾收集。大家都知道js也好.net也好,都有垃圾收集器的,那么我们如果要自己实现完整的as3,势必也要自己实现垃圾收集器。这一步我想了很长时间,也没想出太好的办法,除非自己撸个垃圾收集器。。。。。当时BOSS要求用cocos开发新的项目,用C++的话,自动垃圾收集这个麻烦实在太大了。
但是时隔不久,cocos项目做了一半,BOSS突发奇想,又决定用Unity山寨某世面热门游戏一款。于是解释器暂停了,我们全力进行Unity的开发。一年后,游戏全部开发完成,稍有空闲,于是我准备继续将这个解释器进行完成。回到垃圾收集的问题,这个最简单的就是直接用C#的垃圾收集器代劳。因此,说干就干,解释器使用纯.net2.0开发,不用任何3.5开始的语法和类库,比如linq啊,hashset啊 这样可以,嗯,避免将来不必要的麻烦,懂得自然懂:)有了C#的高生产力,奋斗了几个月,解释器大致出炉了
五、解释器的能力
a) 编译时类型检查。对象访问权限控制,包括public ,private,protected等。如果使用类继承,或者编码时指定了变量类型,就能拥有编译时检查。行为和Adobe AIR编译器保持一致。
b) 原型链继承。和js类似,行为与Adobe AIR保持一致。对于封闭的类,可以使用原型链进行扩展。非常类似.net的扩展方法(真的非常像)
c) 闭包。任何函数都是一等对象,所以闭包支持顺理成章。
d) 完整的类继承,接口系统和AIR编译器完全一致。对于类的成员method,使用function.apply不能改变this指针。而其他的函数,则使用apply和call和js一致,和AIR编译器保持一致。
e) 完整的语法支持。支持除了 with {} 和 namespace 之外的所有语法。(namespace不是C#的namespace, as3中类似的是package。)因为with实在是没法搞,玩js的大家都知道蛤蛤。
f) IDE。由于语法等和AIR完全一致,所以大体上可以直接使用flash develop。
g) 扩展语法。扩展as3的语法,加入了yield 也就是说,同样试用yield就可以直接返回一个ienumerator,和C#学的:)
h) 支持结构体。准确的说,是可以将.net的结构体对象链接过来在脚本中使用。大致上是一个nullable的结构体。
i) 操作符重载。为了更好的链接.net的一些类库,特制作操作符重载。
六、还未完成的部分:
a) 目前需要手工将.net类链接到脚本对象,这部分的代码生成器还需开发
b) 目前没有将编译的结果序列化 / 反序列化。这部分工作难度不大,但需要细心和时间。完成后,就可以将编译和执行分离了,每次执行只需加载二进制字节码执行即可,不必编译。
七、解释器能干什么
嗯,这还用问吗?纯.net2.0,连linq都没有使用,不依赖任何第三方库的脚本解释器,自然是可以嵌入Unity了,而且有静态编译检查,还特意加入了yield和结构体,就是为这个做准备的
八、游戏项目从开发到跑路
我们项目开全部完成了,除了UI改了一遍又一遍。。我们满心期待的等着项目上线,总算可以看到结果了。然后端午节过后的中午,BOSS召集我们宣布,他关门了!跑路了!跑路了,跑路了 其实我当时心里想的是,好吧,历经数年没日没夜的加班日子,我终于可以休息了。
九、休息中
。。写点什么吧。嗯。正好又一段时间休息,继续完善脚本解释器。展示一些执行结果
下面展示的是和现有IDE的结合。
下面展示的是yield语句。
下面是结构体TimeSpan的一些链接:展示了操作符重载
下面展示的是getter,setter。没错as3是支持属性的
package { [Doc] public class FuncTest { } } //下例定义 Team 类。Team 类包括用于在该类内检索和设置属性的 getter 和 setter 方法: class Team { var teamName:String; var teamCode:String; var teamPlayers:Array = new Array(); public function Team(param_name:String, param_code:String) { teamName = param_name; teamCode = param_code; } public function get name():String { return teamName; } public function set name(param_name:String):void { teamName = param_name; } } //在脚本中输入下面的代码: var giants:Team = new Team("San Fran", "SFO"); trace(giants.name); giants.name = "San Francisco"; trace(giants.name); /* San Fran San Francisco */
十、最后
a) 解释器目前进度的代码地址:https://github.com/asheigithub/ASTool 欢迎测bug
b) 也可直接下载编译好的demo
c) 有心情的话,后续记录一些开发中的心得。嘛,看找工作的情况了,如果一直失业的话恐怕也不会太有心情哈哈