目录

Elk JSEngine 学习

一直想学习一下如何写一个 VM,但是苦于各种成熟 VM 代码量庞大,阅读困难,无从学起

即使 lua 的古老版本的代码,对于我这种没有任何编译背景和语言设计背景的人来说,很难理解源码在写什么、在处理什么问题

所幸,天无绝人之路,终于让我发现了两个十分小巧(1-2k行代码),却又可以满足基本使用(可以执行基础的运算即可)的 JS Engine 和 Java VM

今天就来看一下 Elk 这个 Tiny JS Engine

JS Engine 需要考虑什么

最基础的

  • 语法相关,支持解析和运行 ES6 语句
  • 内存相关,需要自动回收对应的内存

像 quickjs 会更加复杂,要支持 ES6 全集的语法,要留下 native api 的拓展功能,包括 debug 工具等等

像 V8 甚至要有 JIT/AOT 编译等等更加复杂和高端的技术点

这里 Elk 只实现了 ES6 的子集,只有一个自己的解释器,所以十分简单

Elk 语法解析

一个完整版的 parser 还是十分困难的,即使 elk 只是实现了很小很小的子集,也没有闭包的概念,但是其大部分的代码还是在处理 parser 的逻辑

因为也没有仔细阅读过 quickjs parser,所以也无法对比,也不知道真正的完美支持 ES6 语法的 parser 有哪些要注意的点和难点

从 Elk 的 parser 源码阅读中学到了一些知识点

  1. On 64-bit platforms, pointers are really 48 bit only
    这个我搜了下,其实是看 cpu 如何实现,大部分都是 48 bit。所以空余的 16 bit 可以用做其他用途
    Elk 这里就用来当做一个 tag pointer 使用。通过前 16 位判定到底是数值类型还是引用类型
    引用类型,那就直接读取后 48 bit 地址,指针获取对应内存
    数值类型使用浮点数存储,读取整个 64 bit

这里你可能会有疑问,既然 double 读取了 64 bit,而前面有几个 bit 位用于判定类型,不会冲突么?比如,Elk 使用 11111111111 开头表示引用类型,如果 double 类型的值刚好是 11111111111 开头怎么办?
所幸的是,这个事情不会发生!IEEE 754,7ff 具有特殊意义,表示无穷,后续数字无意义,double 中不会出现。

  1. union 结构体
    js value 可以是多种类型的,如果用一个 struct 内部包含所有类型来表示 js value 的内存结构,无疑是浪费内存空间的。而使用 union 就可以很好的避免这个问题。

Elk 与 native api 拓展

js engine 只是一个简单的执行引擎,假如想让 js 可以读取文件,我们应该怎么做呢?这就涉及到 native api 拓展,我们可以让 js 调用 fopen 之类的系统方法,来实现读文件。

那 js 如何调用系统方法呢?其实并没有什么高科技。

Elk 的实现,就是将 c 方法的方法指针,存到了 props 中,方法名为 key,方法指针为 value。函数的参数类型只支持基本类型,也不需要做转换。当调用对应 native 方法时,寻找 props 中的函数,并使用函数指针调用即可。

Elk 内存管理

Elk 的内存管理十分简单,预先分配一块固定大小的内存区间,如果使用超出了这块内存空间,则会直接 OOM

所有的数据类型全部存储为一个 entity 概念,都会调用到 mkentity 方法中,如果是字符串还有 NULL 处理。

对于 gc,Elk 初始化时会有一个 gc 阈值(默认为初始化时总大小的一半)。每次执行 stmt 时都会判断一下是否需要进行 gc

gc 对象的判定很简单。Elk 用 scope 来表示,scope 的值存储的是上一个 scope 地址,那么一旦 scope 结束,就读出上一个 scope 相对于整片内存空间的 offset,然后清理上一个 offset 到当前 offset 的内存空间