142

由 QuickJS 想到的

 4 years ago
source link: https://www.tuicool.com/articles/quQr2uU
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前几天,一个叫 Fabrice Bellard 的大牛程序员发布了一个新的 JS 引擎:QuickJS,引起了业界轰动。QuickJS 的主要特点是小,嵌入式,完整支持 ES2019 语法,和其他小型嵌入式 JS 引擎比起来,速度也很快。

登上大牛的主页( https://bellard.org ),瞬间被惊呆了。原来之前耳熟能详的 FFMPEG、QEMU 都是出自他之手。还有一些之前闻所未闻的项目,据说这些都是大神的 Side Project。这些项目范围涉及之广让人瞠目,有涉及科学计算,提出最快速的计算圆周率的算法,并于 2009 年声称打破了圆周率计算的世界纪录,算出小数点后 2.7 万亿位,仅用一台普通个人计算机。有涉及编译器和虚拟机的,1996年,编写了一个简洁但是完整的 C 编译器和一个 Java 虚拟机 Harissa。发明的 TinyCC 是GNU/Linux 环境下最小的 ANSI C 语言编译器,是当前号称编译速度最快的 C 编译器。用 JavaScript 写的一个 PC 虚拟机 Jslinux。图形渲染相关 TinyGL,图片格式 BPG,等等……

他的个人主页也很特别,没有任何 CSS 样式,也没有一张图片,只有纯文本的项目罗列和介绍,甚至没有一点关于 About Me 的信息。然后,在 GitHub、Twitter、Facebook 等地方,都找不到这位神人的踪迹,不禁让人怀疑是不是一个真实存在的人,后来在 Hacker News 上有人证实了确实见过真人,更有人大声惊呼:还有 Fabrice 不能做的东西吗?看来他是一个深居简出,沉浸在代码世界而不受外界干扰的大神级程序员。

这让我更加有兴趣去拜读一下他的代码了。QuickJS 的主页介绍简单明了,代码直接是一个压缩包,名字里带着创建的日期: quickjs-2019-07-09.tar.xz 。这大概是我在十几年前常见的代码共享的方式,而现在已经 2019 年了,大神也没有选择 GitHub 之类的开源协作的平台,甚至让我怀疑他开发过程中是不是有使用 git 之类的源代码管理工具。大神总是特立独行的,直接丢一个压缩包,大概是在说:这就是我所有代码了,有任何问题和疑问,直接给我发邮件吧,不接受任何社交媒体联系方式。

我隐约觉得我的关注重点应该还是他的代码,而不是他的人,也许他本人也是这么认为的。代码解压后,代码是用 C89 写的,不是 C++,甚至不是 C99。这大概是一种技术偏执,在 C 和 C++ 中选择 C 的神级程序员也不在少数,比如开发了 skynet 的云风,还有游戏引擎大牛 Andre Weissflog。在 Andre 的 twitter 上,他贴了一条自己的编程语言使用历程:

Z80 asm => 68k asm => pre-C89 => post-C89 => C++98 => C++11 => C99

经历过 C++11 的各种便利和绚丽,最后还是返璞归真的选择了 C99,或许也给我带来了一些深层的思考:什么才是程序的本质。和 Fabrice 不同,Andre 是一个社交型程序员,甚至使得我有一种感觉,他整天都在发 twitter,但发现他的代码居然一行也没有少写。

这样看来 Fabrice 选择 C89,也没有什么让人奇怪的地方了。代码解压后,自然是先编译一通。没有用 CMake,只有 MakeFile,嗯,一点问题也没有,大概我们都是被 CMake 惯坏了吧,离了它几乎都不懂得怎样编译代码了。大神总是以一种最原始最直接的方式,去组织和掌控着一切。在 linux 上,直接 make 编译,不用多长时间就顺利编过了。

据说 Fabrice 是从 2017 年开启的这个 Side Project,现在的我看他的代码,由如管中窥豹,只能看到一些粗浅的东西,更多东西还得花时间细细研读。但就 QuickJS 而言,在目前的版本里,它是没有 JIT 的,所以和 Google 的 V8 性能比起来,还是有很大差距的。它将一段 JS 代码编译生成一个可执行二进制的方式,是先将 JS 编译成字节码,然后将字节码嵌入到一段 c 的代码里,然后解释执行这段字节码。

比如,hello.js 代码:

console.log("Hello World");

经过 qjsc 编译后,会生成一个这样的 hello.c 代码:

/* File generated automatically by the QuickJS compiler. */

#include "quickjs-libc.h"

const uint32_t hello_size = 87;

const uint8_t hello[87] = {
 0x01, 0x04, 0x0e, 0x63, 0x6f, 0x6e, 0x73, 0x6f,
 0x6c, 0x65, 0x06, 0x6c, 0x6f, 0x67, 0x16, 0x48,
 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57, 0x6f, 0x72,
 0x6c, 0x64, 0x22, 0x65, 0x78, 0x61, 0x6d, 0x70,
 0x6c, 0x65, 0x73, 0x2f, 0x68, 0x65, 0x6c, 0x6c,
 0x6f, 0x2e, 0x6a, 0x73, 0x0d, 0x00, 0x02, 0x00,
 0x9e, 0x01, 0x00, 0x01, 0x00, 0x03, 0x00, 0x00,
 0x14, 0x01, 0xa0, 0x01, 0x00, 0x00, 0x00, 0x38,
 0xc4, 0x00, 0x00, 0x00, 0x42, 0xc5, 0x00, 0x00,
 0x00, 0x04, 0xc6, 0x00, 0x00, 0x00, 0x27, 0x01,
 0x00, 0xd2, 0x2b, 0x8e, 0x03, 0x01, 0x00,
};

int main(int argc, char **argv)
{
  JSRuntime *rt;
  JSContext *ctx;
  rt = JS_NewRuntime();
  ctx = JS_NewContextRaw(rt);
  JS_AddIntrinsicBaseObjects(ctx);
  js_std_add_helpers(ctx, argc, argv);
  js_std_eval_binary(ctx, hello, hello_size, 0);
  js_std_loop(ctx);
  JS_FreeContext(ctx);
  JS_FreeRuntime(rt);
  return 0;
}

所有相关的 opcode 可以在 quickjs-opcode.h 里找到。最核心的 quickjs.c 有 47842 行。作为一个完整的 JS 引擎,代码已经是相当的精炼了,活生生的一本教科书。不过,现在的 QuickJS 在性能要求高的场景和 V8 还是没有可比性,希望这个东西不仅仅是大神闲暇之余的玩具,期待之后能带来更大的惊喜吧。

有人说, Fabrice 就是 10x Engineer,即工作效率相当于 10 个程序员。立马有人站出来反对,分明是 100x Engineer 嘛。这个世界就是这样,有些人比你聪明,比你有天赋,工作效率是你的 10 倍,同时还比你努力。当你在上班划水时,他在写代码,你在吃鸡落地成盒,他又调通了一个核心模块。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK