Kinoma XS6 是一款怎样的 JavaScript 引擎?

软件工程师、主攻高级编程语言虚拟机的设计与实现

62 👍 / 34 💬

问题描述

看起来很厉害:Kinoma | JavaScript 6th Edition

先做点笔记。

KinomaJS现在是Marvell在赞助开发。看来曾经也有华人参与开发?

KinomaJS的源码在Github:

Kinoma/kinomajs · GitHub

开源许可证为 Apache License v2。

其中JavaScript引擎的实现名叫XS6,源码在xs6目录:

kinomajs/xs6 at master · Kinoma/kinomajs · GitHub

整个引擎用C语言实现。是个很完整的JavaScript引擎实现。

“XS”自身没有版本号,后来的叫法是实现了什么版本的ECMAScript的XS就叫什么版本。所以XS6就是实现了ECMAScript 6的XS。

这里有少量开发文档:

kinomajs/notes.html at master · Kinoma/kinomajs · GitHub

粗略看下来,这个JavaScript引擎的核心部分的技术水平大概跟Brendan Eich 90年代中期在Netscape写的差不多吧…

就是因为实现方式还很简单所以加入各种ECMAScript新功能才会那么快。

感觉KinomaJS要说有什么精华的话应该不在这JavaScript引擎里,而在周边库的设计与实现里吧…这我就没看了也没啥兴趣看。

解释器状态的顶层结构:struct sxMachine / txMachine。常用变量名the。

kinomajs/xs6All.h at master · Kinoma/kinomajs · GitHub

可以看到它有独立的解释器栈(txMachine::stack)和作用域栈(txMachine::scope)。

它的Hosting API对解释器栈的使用方式跟Lua有相似之处:

kinomajs/xs6API.c at master · Kinoma/kinomajs · GitHub

Hosting API的文档:

kinomajs/Building a JS API with XS6.md at master · Kinoma/kinomajs · GitHub

文档里演示的xsCall2()宏实际上就是往解释器栈里压入参数然后调用:

#define xsCall2(_THIS,_ID,_SLOT0,_SLOT1) \
	(xsOverflow(-4), \
	fxPush(_SLOT0), \
	fxPush(_SLOT1), \
	fxInteger(the, --the->stack, 2), \
	fxPush(_THIS), \
	fxCallID(the, _ID), \
	fxPop())

词法分析器(lexer)是个典型的ad-hoc手写实现

kinomajs/xs6Lexical.c at master · Kinoma/kinomajs · GitHub

语法分析器(parser)是个典型的手写递归下降式的,没有在表达式层面用运算符优先级(operator precedence parsing)算法。生成AST。

kinomajs/xs6Syntaxical.c at master · Kinoma/kinomajs · GitHub

字节码生成就是典型的后序遍历AST生成线性字节码。字节码是基于栈的形式。

kinomajs/xs6Code.c at master · Kinoma/kinomajs · GitHub

XS6将字节码编译器与解释器分离开来,可以允许在资源受限的使用场景中,先在宿主机器上把JavaScript源码编译成字节码序列化起来,然后在实际设备上只运行字节码(实际设备上就不需要有这个字节码编译器了)。

这是比较能体现XS6为嵌入式优化的地方。

执行引擎是个字节码解释器,可选用switch-threaded或者token-threaded(基于GCC computed goto)。

在函数调用方面没啥优化,没有使用inline cache之类的基本优化技巧。

kinomajs/xs6Run.c at master · Kinoma/kinomajs · GitHub

话说这个解释器似乎在处理减法的时候有bug…我没有build来测试,但光读代码目测下面的代码会算出错误的结果:

var x = -0x7fffffff
var y = 0x7fffffff
var z = x - y // should be -4294967294

有人想测试一下不?

另外还有一个“高性能”版的解释器实现,在XS6目录之外,而在xslib里:

kinomajs/xsRun.c at master · Kinoma/kinomajs · GitHubkinomajs/xsAccelerator.c at master · Kinoma/kinomajs · GitHub

这个所谓“run loop accelerator”并不是XS6的组成部分,而是老的XS(XS5?)的实现。也就是说XS的解释器曾经有比较优化的实现,但新的XS6重写了字节码编译器和解释器之后,还没有把以前的优化移植过来。

内存管理,GC堆的实现是典型的分段式(chunked),回收算法是典型的mark-sweep。

这里说的每一段叫做一个heap,是一个txSlot数组,直接malloc而来;heap之间形成单向链表,通过每个heap最开头的txSlot的next字段串起来。

kinomajs/xs6Memory.c at master · Kinoma/kinomajs · GitHub

值的表现方式(value representation)是很肥的discriminated union,struct sxSlot / txSlot是也。

txSlot里的kind字段记录了tag,然后value字段记录了对应的值。

然而这个txSlot结构非常肥——它不但可以用来表示值,还可以用来表示其它各种东西,包括引用与对象自身。

所有值(txSlot)通过next字段串成一个单向链表。这个灵活的结构有多种用途。

txMachine::firstHeap字段记录着当前GC堆的起点。它本质上是一个txSlot数组,所有元素都是相同大小的,便于管理,完全避开了heap parsibility的问题。其它像是Array或String内容的chunk之类都必须从某些txSlot可以引用到,所以marker/sweeper只要能找到那些txSlot就可以正确处理挂在下面的chunk了。

txMachine::freeHeap字段通过next链将当前free的txSlot串在一起构成freelist。

kinomajs/xs6All.h at master · Kinoma/kinomajs · GitHub

对值的操作有不少实现在xs6API里:

kinomajs/xs6API.c at master · Kinoma/kinomajs · GitHub

对象布局/对象属性的实现

kinomajs/xs6Object.c at master · Kinoma/kinomajs · GitHubkinomajs/xsProperty.c at master · Kinoma/kinomajs · GitHub

创建对象是这样实现的:

kinomajs/xs6Object.c at master · Kinoma/kinomajs · GitHub
txSlot* fxNewObjectInstance(txMachine* the)
{
	txSlot* instance;
	instance = fxNewSlot(the);
	instance->kind = XS_INSTANCE_KIND;
	instance->value.instance.garbage = C_NULL;
	instance->value.instance.prototype = the->stack->value.reference;
	the->stack->value.reference = instance;
	the->stack->kind = XS_REFERENCE_KIND;
	return instance;
}

也就是从txMachine::freeHeap一个新的txSlot出来,初始化到XS_INSTANCE_KIND以及栈上指定的prototype,然后的栈定的引用指向这个新的txSlot并设置其类别为引用XS_REFERENCE_KIND。

fxToInstance()函数可以从一个引用找出其指向的对象的txSlot*:

kinomajs/xs6Type.c at master · Kinoma/kinomajs · GitHub

换句话说,对这样的JavaScript对象:

o = { x: 1, y: 2 }

在内存里实际上是:

  txSlot: o
[ reference ]    txSlot
  next ->     [ instance ]  txSlot
                next ->    [ x: 2 ]   txSlot
                             next -> [ y: 2 ]
                                       next -> NULL

数组采用紧凑布局,数组元素放在一段连续的内存里(Chunk):

kinomajs/xsProperty.c at master · Kinoma/kinomajs · GitHub

数组对象标准库的实现:

kinomajs/xs6Array.c at master · Kinoma/kinomajs · GitHub

XS6内部的符号驻留(symbol interning),用16位带符号整数类型txID为索引。

struct sxSymbol {
	txSymbol* next;
	txID ID;
	txInteger length;
	txString string;
	txSize sum;
	txInteger usage;
};

有若干内建的ID是这样处理的,例如mxID(the, _configurable):

#define mxIDs the->stackTop[-1 - mxIDsStackIndex]
#define mxID(_ID) (((txID*)(mxIDs.value.code))[_ID])

剩下的JavaScript核心库的实现也都挺常规的…

正则表达式用的是

PCRE

库。

XS6内建一个简单的“profiler”,作为一个小JavaScript引擎这倒是不错。

kinomajs/xs6Profile.c at master · Kinoma/kinomajs · GitHub