跨端轻量JavaScript引擎的实现与探索
探讨跨端轻量JS引擎的实现方式,深入浅出的研究如何设计和构建一种高效、灵活且可移植的JS引擎,使其能够在不同的平台上运行JS应用程序。通过本次分享,您将了解到实现跨端JS引擎的关键技术和挑战。
【1.JavaScript语言】
JavaScript是ECMAScript的实现,由ECMA 39(欧洲计算机制造商协会39号技术委员会)负责制定ECMAScript标准。
ECMAScript发展史:
【2.JavaScript引擎】
JavaScript引擎是指用于处理以及执行JavaScript脚本的虚拟机。
常见的JavaScript引擎:
【3.JavaScript引擎工作原理】
a.V8引擎工作原理
b.Turbofan技术实例说明
function sum(a, b) {return a + b;}
这里a和b可以是任意类型数据,当执行sum函数时,Ignition解释器会检查a和b的数据类型,并相应地执行加法或者连接字符串的操作。
如果 sum函数被调用多次,每次执行时都要检查参数的数据类型是很浪费时间的。此时TurboFan就出场了。它会分析函数的执行信息,如果以前每次调用sum函数时传递的参数类型都是数字,那么TurboFan就预设sum的参数类型是数字类型,然后将其编译为机器码。
但是如果某一次的调用传入的参数不再是数字时,表示TurboFan的假设是错误的,此时优化编译生成的机器代码就不能再使用了,于是就需要进行回退到字节码的操作。
【1.QuickJS作者简介】
法布里斯·貝拉 (Fabrice Bellard)
【2.QuickJS简介】
QuickJS 是一个小型的嵌入式 Javascript 引擎。它支持 ES2023 规范,包括模块、异步生成器、代理和 BigInt。
它可以选择支持数学扩展,例如大十进制浮点数 (BigDecimal)、大二进制浮点数 (BigFloat) 和运算符重载。
•小且易于嵌入:只需几个 C 文件,无外部依赖项,一个简单的 hello world 程序的 210 KiB x86 代码。
•启动时间极短的快速解释器:在台式 PC 的单核上运行 ECMAScript 测试套件的 76000 次测试只需不到 2 分钟。运行时实例的完整生命周期在不到 300 微秒的时间内完成。
•几乎完整的 ES2023 支持,包括模块、异步生成器和完整的附录 B 支持(旧版 Web 兼容性)。
•通过了近 100% 的 ECMAScript 测试套件测试: Test262 Report(https://test262.fyi/#)。
•可以将 Javascript 源代码编译为可执行文件,无需外部依赖。
•使用引用计数(以减少内存使用并具有确定性行为)和循环删除的垃圾收集。
•数学扩展:BigDecimal、BigFloat、运算符重载、bigint 模式、数学模式。
•用 Javascript 实现的带有上下文着色的命令行解释器。
•带有 C 库包装器的小型内置标准库。
【3.QuickJS工程简介】
5.94MB quickjs├── 17.6kB cutils.c /// 辅助函数├── 7.58kB cutils.h /// 辅助函数├── 241kB libbf.c /// BigFloat相关├── 17.9kB libbf.h /// BigFloat相关├── 2.25kB libregexp-opcode.h /// 正则表达式操作符├── 82.3kB libregexp.c /// 正则表达式相关├── 3.26kB libregexp.h /// 正则表达式相关├── 3.09kB list.h /// 链表实现├── 16.7kB qjs.c /// QuickJS stand alone interpreter├── 22kB qjsc.c /// QuickJS command line compiler├── 73.1kB qjscalc.js /// 数学计算器├── 7.97kB quickjs-atom.h /// 定义了javascript中的关键字├── 114kB quickjs-libc.c├── 2.57kB quickjs-libc.h /// C API├── 15.9kB quickjs-opcode.h /// 字节码操作符定义├── 1.81MB quickjs.c├── 41.9kB quickjs.h /// QuickJS Engine├── 49.8kB repl.js /// REPL├── 218kB libunicode-table.h /// unicode相关├── 53kB libunicode.c /// unicode相关├── 3.86kB libunicode.h /// unicode相关├── 86.4kB unicode_gen.c /// unicode相关└── 6.99kB unicode_gen_def.h /// unicode相关
【4.QuickJS工作原理】
QuickJS的解释器是基于栈的。
QuickJS的对byte-code会优化两次,通过一个简单例子看看QuickJS的字节码与优化器的输出,以及执行过程。
function sum(a, b) {return a + b;}
•第一阶段(未经过优化的字节码)
;; function sum(a, b) {enter_scope 1;; return a + b;line_num 2scope_get_var a,1 ///通用的获取变量的指令scope_get_var b,1addreturn;; }
•第二阶段
;; function sum(a, b) {;; return a + b;line_num 2get_arg 0: a /// 获取参数列表中的变量get_arg 1: baddreturn;; }
•第三阶段
;; function sum(a, b) {;; return a + b;get_arg0 0: a /// 精简成获取参数列表中第0个参数get_arg1 1: baddreturn;; }
sum(1,2);通过上述简单的函数调用,观察sum函数调用过程中栈帧的变化,通过计算可知sum函数最栈帧大小为两个字节
【5.内存管理】
QuickJS通过引用计算来管理内存,在使用C API时需要根据不同API的说明手动增加或者减少引用计数器。
对于循环引用的对象,QuickJS通过临时减引用保存到临时数组中的方法来判断相互引用的对象是否可以回收。
【6.QuickJS简单使用】
从github上clone完最新的源码后,通过执行(macos 环境)以下代码即可在本地安装好qjs、qjsc、qjscalc几个命令行程序
sudo makesudo make install
•qjs: JavaScript代码解释器
•qjsc: JavaScript代码编译器
•qjscalc: 基于QuickJS的REPL计算器程序
通过使用qjs可以直接运行一个JavaScript源码,通过qsjc的如下命令,则可以输出一个带有byte-code源码的可直接运行的C源文件:
qjsc -e -o add.c examples/add.js#include "quickjs-libc.h"const uint32_t qjsc_add_size = 135;const uint8_t qjsc_add[135] = {0x02, 0x06, 0x06, 0x73, 0x75, 0x6d, 0x0e, 0x63,0x6f, 0x6e, 0x73, 0x6f, 0x6c, 0x65, 0x06, 0x6c,0x6f, 0x67, 0x1e, 0x65, 0x78, 0x61, 0x6d, 0x70,0x6c, 0x65, 0x73, 0x2f, 0x61, 0x64, 0x64, 0x2e,0x6a, 0x73, 0x02, 0x61, 0x02, 0x62, 0x0e, 0x00,0x06, 0x00, 0xa2, 0x01, 0x00, 0x01, 0x00, 0x05,0x00, 0x01, 0x25, 0x01, 0xa4, 0x01, 0x00, 0x00,0x00, 0x3f, 0xe3, 0x00, 0x00, 0x00, 0x40, 0xc2,0x00, 0x40, 0xe3, 0x00, 0x00, 0x00, 0x00, 0x38,0xe4, 0x00, 0x00, 0x00, 0x42, 0xe5, 0x00, 0x00,0x00, 0x38, 0xe3, 0x00, 0x00, 0x00, 0xb8, 0xb9,0xf2, 0x24, 0x01, 0x00, 0xcf, 0x28, 0xcc, 0x03,0x01, 0x04, 0x1f, 0x00, 0x08, 0x0a, 0x0e, 0x43,0x06, 0x00, 0xc6, 0x03, 0x02, 0x00, 0x02, 0x02,0x00, 0x00, 0x04, 0x02, 0xce, 0x03, 0x00, 0x01,0x00, 0xd0, 0x03, 0x00, 0x01, 0x00, 0xd3, 0xd4,0x9e, 0x28, 0xcc, 0x03, 0x01, 0x01, 0x03,};static JSContext *JS_NewCustomContext(JSRuntime *rt){JSContext *ctx = JS_NewContextRaw(rt);if (!ctx)return NULL;JS_AddIntrinsicBaseObjects(ctx);JS_AddIntrinsicDate(ctx);JS_AddIntrinsicEval(ctx);JS_AddIntrinsicStringNormalize(ctx);JS_AddIntrinsicRegExp(ctx);JS_AddIntrinsicJSON(ctx);JS_AddIntrinsicProxy(ctx);JS_AddIntrinsicMapSet(ctx);JS_AddIntrinsicTypedArrays(ctx);JS_AddIntrinsicPromise(ctx);JS_AddIntrinsicBigInt(ctx);return ctx;}int main(int argc, char **argv){JSRuntime *rt;JSContext *ctx;rt = JS_NewRuntime();js_std_set_worker_new_context_func(JS_NewCustomContext);js_std_init_handlers(rt);JS_SetModuleLoaderFunc(rt, NULL, js_module_loader, NULL);ctx = JS_NewCustomContext(rt);js_std_add_helpers(ctx, argc, argv);js_std_eval_binary(ctx, qjsc_add, qjsc_add_size, 0);js_std_loop(ctx);js_std_free_handlers(rt);JS_FreeContext(ctx);JS_FreeRuntime(rt);return 0;}
上面的这个C源文件,通过如下命令即可编译成可执行文件:
gcc add.c -o add_exec -I/usr/local/include quickjs-libc.c quickjs.c cutils.c libbf.c libregexp.c libunicode.c -DCONFIG_BIGNUM也可以直接使用如下命令,将JavaScript文件直接编译成可执行文件:
qjsc -o add_exec examples/add.js【7.给qjsc添加扩展】
QuickJS只实现了最基本的JavaScript能力,同时QuickJS也可以实现能力的扩展,比如给QuickJS添加打开文件并读取文件内容的内容,这样在JavaScript代码中即可通过js代码打开并读取到文件内容了。
通过一个例子来看看添加扩展都需要做哪些操作:
•编写一个C语言的扩展模块
#include "quickjs.h"#include "cutils.h"/// js中对应plus函数的C语言函数static JSValue plusNumbers(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {int a, b;if (JS_ToInt32(ctx, &a, argv[0]))return JS_EXCEPTION;if (JS_ToInt32(ctx, &b, argv[1]))return JS_EXCEPTION;return JS_NewInt32(ctx, a + b);}/// 模块需要导致的列表static const JSCFunctionListEntry js_my_module_funcs[] = {JS_CFUNC_DEF("plus", 2, plusNumbers),};/// 模块初始化函数,并将plus导出static int js_my_module_init(JSContext *ctx, JSModuleDef *m) {return JS_SetModuleExportList(ctx, m, js_my_module_funcs, countof(js_my_module_funcs));}JSModuleDef *js_init_module_my_module(JSContext *ctx, const char *module_name) {JSModuleDef *m;m = JS_NewCModule(ctx, module_name, js_my_module_init);if (!m)return NULL;JS_AddModuleExportList(ctx, m, js_my_module_funcs, countof(js_my_module_funcs));return m;}
•Makefile文件中添加my_module.c模块的编译
QJS_LIB_OBJS= ... $(OBJDIR)/my_module.o•在qjsc.c文件中注册模块
namelist_add(&cmodule_list,“my_module”,“my_module”,0); •编写一个my_module.js测试文件
import * as mm from 'my_module';const value = mm.plus(1, 2);console.log(`my_module.plus: ${value}`);
•重新编译
sudo make && sudo make installqjsc -m -o my_module examples/my_module.js /// 这里需要指定my_module模块
最终生成的my_module可执行文件,通过执行my_module输出:
my_module.plus: 3【8.使用C API】
在第5个步骤时,生成了add.c文件中实际上已经给出了一个简单的使用C API最基本的代码。当编写一下如下的js源码时,会发现当前的qjsc编译后的可执行文件或者qjs执行这段js代码与我们的预期不符:
function getName() {return new Promise((resolve, reject) => {setTimeout(() => {resolve("张三峰");}, 2000);});}console.log(`开始执行`);getName().then(name => console.log(`promise name: ${name}`));
上面的代码并不会按预期的效果输出结果,因为js环境下的loop只执行了一次,任务队列还没有来得急执行程序就结束了,稍微改动一下让程序可以正常输出,如下:
#include <stdio.h>#include <pthread.h>#include <stdlib.h>#include <uv.h>/* File generated automatically by the QuickJS compiler. */#include "quickjs-libc.h"#include <string.h>static JSContext *JS_NewCustomContext(JSRuntime *rt) {JSContext *ctx = JS_NewContextRaw(rt);if (!ctx)return NULL;JS_AddIntrinsicBaseObjects(ctx);JS_AddIntrinsicDate(ctx);JS_AddIntrinsicEval(ctx);JS_AddIntrinsicStringNormalize(ctx);JS_AddIntrinsicRegExp(ctx);JS_AddIntrinsicJSON(ctx);JS_AddIntrinsicProxy(ctx);JS_AddIntrinsicMapSet(ctx);JS_AddIntrinsicTypedArrays(ctx);JS_AddIntrinsicPromise(ctx);JS_AddIntrinsicBigInt(ctx);return ctx;}JSRuntime *rt = NULL;JSContext *ctx = NULL;void *run(void *args) {const char *file_path = "/Volumes/Work/分享/quickjs/code/quickjs/examples/promise.js";size_t pbuf_len = 0;js_std_set_worker_new_context_func(JS_NewCustomContext);js_std_init_handlers(rt);JS_SetModuleLoaderFunc(rt, NULL, js_module_loader, NULL);ctx = JS_NewCustomContext(rt);js_std_add_helpers(ctx, 0, NULL);js_init_module_os(ctx, "test");const uint8_t *code = js_load_file(ctx, &pbuf_len, file_path);JSValue js_ret_val = JS_Eval(ctx, (char *)code, pbuf_len, "add", JS_EVAL_TYPE_MODULE);if(JS_IsError(ctx, js_ret_val) || JS_IsException(js_ret_val)) {js_std_dump_error(ctx);}return NULL;}pthread_t quickjs_t;int main(int argc, char **argv) {rt = JS_NewRuntime();pthread_create(&quickjs_t, NULL, run, NULL);while (1) {if(ctx) js_std_loop(ctx);}js_std_free_handlers(rt);JS_FreeContext(ctx);JS_FreeRuntime(rt);return 0;}
int uv_run(uv_loop_t* loop, uv_run_mode mode) {...r = uv__loop_alive(loop);if (!r)uv__update_time(loop);while (r != 0 && loop->stop_flag == 0) {uv__update_time(loop);uv__run_timers(loop);ran_pending = uv__run_pending(loop);uv__run_idle(loop);uv__run_prepare(loop);...uv__io_poll(loop, timeout);uv__run_check(loop);uv__run_closing_handles(loop);...}}
static void timer_cb(uv_timer_t *handler) {printf("timer_cb exec.\r\n");}int main(int argc, const char * argv[]) {uv_loop_t *loop = uv_default_loop();uv_timer_t *timer = (uv_timer_t*)malloc(sizeof(uv_timer_t));uv_timer_init(loop, timer);uv_timer_start(timer, timer_cb, 2000, 0);uv_run(loop, UV_RUN_DEFAULT);}
console.log(`开始执行`);function getName() {return new Promise((resolve, reject) => {setTimeout(() => {resolve("张三峰");}, 2000);});}getName().then(name => console.log(`promise name: ${name}`));
#include <stdio.h>#include <pthread.h>#include <stdlib.h>#include <uv.h>/* File generated automatically by the QuickJS compiler. */#include "quickjs-libc.h"#include <string.h>typedef struct once_timer_data {JSValue func;JSValue this_val;JSContext *ctx;} once_timer_data;void once_timer_cb(uv_timer_t *once_timer) {once_timer_data *data = (once_timer_data *)once_timer->data;JSContext *ctx = data->ctx;JSValue js_ret_val = JS_Call(data->ctx, data->func, data->this_val, 0, NULL);if(JS_IsError(ctx, js_ret_val) || JS_IsException(js_ret_val)) {js_std_dump_error(ctx);}JS_FreeValue(data->ctx, js_ret_val);JS_FreeValue(data->ctx, data->func);JS_FreeValue(data->ctx, data->this_val);free(data);uv_timer_stop(once_timer);free(once_timer);}void check_cb(uv_check_t *check) {JSContext *ctx = (JSContext *)check->data;js_std_loop(ctx);}void idle_cb(uv_idle_t *idle) {}JSValue set_timeout(JSContext *ctx, JSValue this_val, int argc, JSValue *argv) {if(argc != 2) return JS_NULL;JSValue func_val = argv[0];JSValue delay_val = argv[1];int64_t delay = 0;int ret = JS_ToInt64(ctx, &delay, delay_val);if(ret < 0) js_std_dump_error(ctx);uv_timer_t *once_timer = (uv_timer_t *)malloc(sizeof(uv_timer_t));once_timer_data *data = (once_timer_data *)malloc(sizeof(once_timer_data));data->func = JS_DupValue(ctx, func_val);data->this_val = JS_DupValue(ctx, this_val);data->ctx = ctx;once_timer->data = data;uv_timer_init(uv_default_loop(), once_timer);uv_timer_start(once_timer, once_timer_cb, delay, 0);JSValue js_timer = JS_NewInt64(ctx, (uint64_t)once_timer);return js_timer;}static JSContext *JS_NewCustomContext(JSRuntime *rt) {JSContext *ctx = JS_NewContextRaw(rt);if (!ctx)return NULL;JS_AddIntrinsicBaseObjects(ctx);JS_AddIntrinsicDate(ctx);JS_AddIntrinsicEval(ctx);JS_AddIntrinsicStringNormalize(ctx);JS_AddIntrinsicRegExp(ctx);JS_AddIntrinsicJSON(ctx);JS_AddIntrinsicProxy(ctx);JS_AddIntrinsicMapSet(ctx);JS_AddIntrinsicTypedArrays(ctx);JS_AddIntrinsicPromise(ctx);JS_AddIntrinsicBigInt(ctx);return ctx;}void js_job(uv_timer_t *timer) {JSRuntime *rt = timer->data;const char *file_path = "/Volumes/Work/分享/quickjs/code/quickjs/examples/promise.js";size_t pbuf_len = 0;JSContext *ctx;js_std_set_worker_new_context_func(JS_NewCustomContext);js_std_init_handlers(rt);JS_SetModuleLoaderFunc(rt, NULL, js_module_loader, NULL);ctx = JS_NewCustomContext(rt);uv_check_t *check = (uv_check_t *)malloc(sizeof(uv_check_t));uv_check_init(uv_default_loop(), check);check->data = ctx;uv_check_start(check, check_cb);JSValue global = JS_GetGlobalObject(ctx);JSValue func_val = JS_NewCFunction(ctx, set_timeout, "setTimeout", 1);JS_SetPropertyStr(ctx, global, "setTimeout", func_val);JS_FreeValue(ctx, global);js_std_add_helpers(ctx, 0, NULL);js_init_module_os(ctx, "test");const uint8_t *code = js_load_file(ctx, &pbuf_len, file_path);JSValue js_ret_val = JS_Eval(ctx, (char *)code, pbuf_len, "add", JS_EVAL_TYPE_MODULE);if(JS_IsError(ctx, js_ret_val) || JS_IsException(js_ret_val)) {js_std_dump_error(ctx);}js_std_free_handlers(rt);JS_FreeContext(ctx);}int main(int argc, char **argv) {JSRuntime *rt = JS_NewRuntime();uv_loop_t *loop = uv_default_loop();uv_timer_t *timer = (uv_timer_t*)malloc(sizeof(uv_timer_t));timer->data = rt;uv_timer_init(loop, timer);uv_timer_start(timer, js_job, 0, 0);uv_idle_t *idle = (uv_idle_t *)malloc(sizeof(uv_idle_t));uv_idle_init(loop, idle);uv_idle_start(idle, idle_cb);uv_run(loop, UV_RUN_DEFAULT);JS_FreeRuntime(rt);return 0;}
- END -