我将世界上第一个JavaScript引擎编译回JavaScript

2020-12-05 00:26:18

1995年,当我只有一岁的时候,一个名叫Brendan Eich的大洋彼岸的人用了十天的时间创造了一种编程语言,今天我赖以生存。

快速创建JavaScript的故事在程序员社区中广为人知。但是,也许今天没有多少人会记得(甚至没有经历过)“原始JavaScript”的含义。就像,更不用说那时阅读JS引擎的代码了。

但是,在2020年,我们有机会进一步了解这一历史。在HOPL-IV上,由Brendan Eich和ES6的主要作者Allen Wirfs-Brock共同撰写的关于编程语言历史的会议JavaScript The First 20 Years提供了JavaScript诞生和发展的详细历史。作为本书中文版的翻译,我对原始版本中的600多个参考链接进行了校对,其中一个指向最早的JS引擎的源代码。这激发了我的好奇心-最早的JS引擎代码是否仍可以在今天编译并运行?如果可能的话,我是否可以再进一步将其编译回JavaScript,使其在网络上重新流行起来?所以我做了这个尝试。

最早的JS引擎称为Mocha(Netscape内部Web脚本语言项目的代号),其第一个原型由Eich在1995年5月编写。在整个1995年和1996年的大部分时间,Eich是唯一的全职开发人员在JavaScript引擎上。 Mocha的代码库仍主要由该原型中的代码组成,直到1996年8月Netscape 3.0发行为止。Netscape3.0发行的JS版本称为JavaScript 1.1,这标志着JavaScript初始开发阶段的完成。之后,Eich又花了两周时间重写Mocha,以获取功能更强大的引擎,即今天的Firefox SpiderMonkey。

如果您搜索Netscape源代码,则可能只能从1998 Mozilla项目中找到SpiderMonkey引擎的源代码。 Mocha引擎的实际源代码位于Web上的(未识别)Netscape 3.0.2浏览器源压缩文件中。但是,当Mocha的源代码完全被放弃时,如何使它复活呢?

实际上,总有两种了解任何软件的方法:" top-down"和“自下而上”。前者是在体系结构级别获得宏观知识,而后者是在代码级别解决微观问题。由于我已经熟悉使用QuickJS之类的JS引擎,因此我选择直接采用自下而上的方法。基本思想很简单:逐步移植引擎的每个模块,最终将它们放在一起并运行。

最初的Mocha将Makefile用于构建系统,但显然在当今的OS平台上不再起作用-那是MacOS仍在使用PPC处理器的时代!但是最终,构建系统只是一个辅助工具,可以自动执行gcc和clang等编译器。 C项目中的编译过程可以总结如下:

使用gcc -c命令,编译用作库的.c源代码。到.o目标文件中,一个接一个。这会将C源代码中的每个函数编译为所谓的" symbols"。在二进制可执行文件中,就像ES模块中导出的功能一样。请注意,此时,可以从每个目标文件中任意调用.h文件中包含的其他库的API。这不会导致编译错误,对外部符号的调用仅记录在目标文件中。

使用ar命令将这些.o对象文件转换为.a格式的静态库。生成的.a文件将包含项目中的所有符号,类似于cat * .js>>的效果。 all.js。我们还可以创建动态库来节省空间,但是它们会使事情变得更加复杂,因此我们在这里跳过它们。

使用gcc -l命令编译调用该库的.c源代码,这会将其输出链接到.a静态库。链接器将链接符号依赖项,就像匹配" tenons"一样。在每个目标文件中。对于第一步中的每个目标文件,链接器必须找到调用外部API内的每个符号,并且任何丢失的符号都将导致链接时间错误-但只要链接阶段成功,我们就可以得到一个可执行文件主要功能为切入点的文件。

编译每个Mocha的.c源文件(即条目除外),以.o格式获取包含其符号的目标文件。

合并包含这些符号的.o对象文件,并将它们包装到.a静态库文件libmocha.a中。

编译mo_shell.c入口文件,将其链接到最终可执行文件libmocha.a静态库。

在此过程中需要解决许多依赖关系,其中最典型的是对prxxx.h的依赖关系。这是Netscape在过去开发的Netscape Portable Runtime跨平台库,其源代码也包含在Netscape 3源代码树中。但是我没有一次将所有内容提交到新移植的Mocha代码库中。这里的方法是仅在遇到缺少的NSPR依赖关系时,递归地手动引入所涉及的NSPR头文件和相应的源代码。因此,我们可以通过这种方式剥离出最小可用的Mocha源树。

修改了prtypes.h中的类型定义,将具有潜在兼容性问题的类型(如unsigned short)替换为C99标准中的uint16_t等类型,以及类似的Bool类型。

添加了MOCHAFILE宏,以强制Mocha进入读取文件的CLI模式,而不是Netscape浏览器中使用的嵌入式模式。

最后,我设法用一个非常简单的bash脚本编译了Mocha的所有模块。我确信经过几天认真的C语言学习就足以理解它:

函数compile_objs(){echo" compiling OBJS ..." $ CC -Iinclude src / mo_array.c -c -o out / mo_array.o $ CC -Iinclude src / mo_atom.c -c -o out / mo_atom.o $ CC -Iinclude src / mo_bcode.c -c -o out /mo_bcode.o $ CC -Iinclude src / mo_bool.c -c -o out / mo_bool.o $ CC -Iinclude src / mo_cntxt.c -c -o out / mo_cntxt.o $ CC -Iinclude src / mo_date.c- Wno-dangling-else -c -o out / mo_date.o $ CC -Iinclude src / mo_emit.c -c -o out / mo_emit.o $ CC -Iinclude src / mo_fun.c -c -o out / mo_fun.o $ CC -Iinclude src / mo_math.c -c -o out / mo_math.o $ CC -Iinclude src / mo_num.c -Wno-non-literal-null-conversion -c -o out / mo_num.o $ CC -Iinclude src / mo_obj.c -c -o out / mo_obj.o $ CC -Iinclude src / mo_parse.c -c -o out / mo_parse.o $ CC -Iinclude src / mo_scan.c -c -o out / mo_scan.o $ CC -Iinclude src / mo_scope.c -c -o out / mo_scope.o $ CC -Iinclude src / mo_str.c -Wno-non-literal-null-conversion -c -o out / mo_str.o $ CC -Iinclude src / mocha.c -c -o out / mocha.o $ CC -Iinclude src / mochaapi.c -Wno-non-literal-null-conversion -c -o out / mochaapi.o $ CC -Iinclude src / mochalib。 c -c -o out / mochalib.o $ CC -Iinclude src / prmjtime.c -c -o out / prmjtime.o $ CC -Iinclude src / prtime.c -c -o out / prtime.o $ CC -Iinclude src / prrena.c -c -o out /prarena.o $ CC -Iinclude src / prhash.c -c -o out / prhash.o $ CC -Iinclude src / prprf.c -c -o out / prprf.o $ CC -Iinclude src / prdtoa.c \ -Wno逻辑非括号\ -Wno-shift-op括号\ -Wno括号\ -c -o out / prdtoa.o $ CC -Iinclude src / log2.c -c -o out / log2.o $ CC -Iinclude src / longlong.c -c -o out / longlong.o}

在此过程中引发编译器警告的同时,我也看到了一些令人惊讶的代码。例如,mo_date.c中的这个:

如果(i< = st + 1)转到语法;对于(k =(sizeof(wtb)/ sizeof(char *)); --k> = 0;)如果(date_regionMatches(wtb [k],0,s,st,i-st,1)){int行动= ttb [k]; if(action!= 0)if(action == 1)/ * pm * / if(hour> 12 || hour< 0)转到语法;否则小时+ = 12;否则,如果(操作< = 13)/ *月! * /如果(mon< 0)mon = / *字节* /(动作-2);否则转到语法;否则tzoffset =操作-10000;打破; }如果(k< 0)转到语法;

而且有很多评论让我想起了它的历史,例如mocha.c中的一个:

我还找到了一些代码来举例说明1995年的混乱兼容性问题。它们使我更好地理解了当时的人们为什么期望一次编写一次,随处可见。 Java:

#如果已定义(AIXV3)#包括" os / aix.h"#elif已定义(BSDI)#包括" os / bsdi.h"#elif已定义(HPUX)#包括" os / hpux.h"#elif定义(IRIX)#include" os / irix.h"#elif定义(LINUX)#include" os / linux。 h"#elif定义(OSF1)#include" os / osf1.h"#elif定义(SCO)#include" os / scoos.h"#elif定义(SOLARIS)#包括" os / solaris.h"#elif定义(SUNOS4)#包括" os / sunos.h"#elif定义的(UNIXWARE)#include&#34 ; os / unixware.h"#elif定义(NEC)#包括" os / nec.h"#elif定义(SONY)#包括" os / sony.h&# 34;#elif defined(NCR)#include" os / ncr.h"#elif defined(SNI)#include" os / reliantunix.h"#endif

幸运的是,所有这些C代码都可以毫无问题地进行编译。这里没有进行任何多余的更改来保留历史遗产。一旦有了所有目标文件,只需使用以下bash脚本行链接到libmocha静态库,即可创建Mocha的可执行文件!

函数compile_native(){export CC = clang export AR = ar compile_objs echo" linking ..." $ AR -rcs out / libmocha.a out / * .o $ CC -Iinclude -Lout -lmocha tests / mo_shell.c -o out / mo_shell echo" mocha shell已编译! "}

获得Mocha的本机版本后,我们如何获得它的WASM版本?真的很简单,只需用WASM编译器emcc替换本机编译器命令gcc(实际上是macOS上的clang)! Emscripten编译器支持JavaScript和WASM作为编译后端,并且切换输出格式只需更改其中一个编译标志即可:

函数compile_web(){export CC = emcc export AR = emar compile_objs echo" linking ..." $ AR -rcs out / libmocha.a out / * .o $ CC -Iinclude -Lout -lmocha tests / mo_shell.c \ --shell-file src / shell.html \ -s NO_EXIT_RUNTIME = 0 \ -s WASM = $ 1 \ -O2 \ -o $ 2 echo" mocha shell已编译! "}函数compile_js(){compile_web 0 out / mocha_shell_js.html}函数compile_wasm(){compile_web 1 out / mocha_shell_wasm.html}

在使用Mocha引擎之后,我没有重写Makefile,因为我发现手动实现的bash脚本虽然不能增量编译,但非常易于使用,并且可以轻松构建不同的产品:

但是,默认情况下,Emscripten编译器本身具有很高的攻击性,它会在打开页面后立即输出HTML来执行WASM内容。为了简单起见,WASM引擎页面在此处直接嵌入到iframe中。每次" Run"单击页面上的按钮,将输入框的内容插入到localStorage中,然后重新加载相应的WASM iframe页面,在该页面中,同步读取localStorage中的JS脚本,作为Emscripten模拟的stdin的标准输入,最后Mocha已启动并自动解释脚本。

该过程非常简单,我想任何普通的前端开发人员都可以轻松地实现它。这是最终结果:

就是这样!我们已重新安装"网络浏览器中世界上第一个JS引擎!

从查找源代码到在线获取WASM版本,仅花了不到三天的时间。因此,我个人认为Mocha引擎在可移植性方面经过了深思熟虑,并且具有良好的工程质量。但是,它的某些基本设计(例如引用计数)具有固有的性能瓶颈,因此需要对其进行重写,这是另一回事。

在撰写本文时,它正好是JavaScript正式发布25周年(1995年12月4日,Netscape和Sun联合发布)。该事件的新闻稿也是JavaScript The First 20 Years的附件。作为中国的前端开发人员,我很高兴看到这本书在中国获得了良好的反响(我个人相关文章的阅读量约为60,000,GitHub翻译项目为2.2k星)。有趣的是,JavaScript的创建者Brendan Eich在他的Twitter头像上也有汉字,但是不幸的是,您只能看到"无一"这个词。 ("无"代表null,"一"代表一个),看起来他正在练习太极拳:

但是,多亏了我的朋友顾一玲,我找到了Eich头像的原始照片。这里的汉字不是形而上的,而是对程序员的鼓励,上面写着:“人贡献越多,对整个生态系统的发展越有利,开源已成为一种文化。”

也许我在这里所做的这种小做法,也是这种文化的体现。

C的创造者丹尼斯·里奇(Dennis Ritchie)表示,成功的方法是靠幸运-抓紧快速发展的事物,并让自己在处于困境的时候继续前进在正确的时间正确的位置。"这正是JavaScript发生的情况。现在,这种语言已经为SpaceX Dragon上的第一架人类航天器的GUI提供了动力,甚至还可以通过James Webb太空望远镜飞入宇宙。但是,当我们回顾一切的开始时,无疑存在着所有缺陷的1995年版本的Mocha引擎在正确的时间正确的位置-否则也许我们今天就要编写VBScript。

回首1995年,到2020年底,这似乎是一个不可思议的时刻:WTO成立,申根协定生效,Windows 95,Java& JavaScript已发布。四分之一世纪之后,有些东西已经普及了,有些东西已经改变,有些东西可能永远也不会回来。

忘掉坏东西。今天,我们举杯庆祝1995、2020和JavaScript。