写点什么

跨端轻量 JavaScript 引擎的实现与探索

  • 2024-02-28
    北京
  • 本文字数:8344 字

    阅读完需:约 27 分钟

导言:探讨跨端轻量 JS 引擎的实现方式,深入浅出的研究如何设计和构建一种高效、灵活且可移植的 JS 引擎,使其能够在不同的平台上运行 JS 应用程序。通过本次分享,您将了解到实现跨端 JS 引擎的关键技术和挑战。通过本次分享,您将了解到实现跨端 JavaScript 引擎的关键技术和挑战。

一、JavaScript

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;}
复制代码


这里ab可以是任意类型数据,当执行sum函数时,Ignition解释器会检查ab的数据类型,并相应地执行加法或者连接字符串的操作。


如果 sum函数被调用多次,每次执行时都要检查参数的数据类型是很浪费时间的。此时TurboFan就出场了。它会分析函数的执行信息,如果以前每次调用sum函数时传递的参数类型都是数字,那么TurboFan就预设sum的参数类型是数字类型,然后将其编译为机器码。


但是如果某一次的调用传入的参数不再是数字时,表示TurboFan的假设是错误的,此时优化编译生成的机器代码就不能再使用了,于是就需要进行回退到字节码的操作。

三、QuickJS

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 2 scope_get_var a,1 ///通用的获取变量的指令 scope_get_var b,1 add return
;; }
复制代码


•第二阶段


;; function sum(a, b) {;;     return a + b;
line_num 2 get_arg 0: a /// 获取参数列表中的变量 get_arg 1: b add return;; }
复制代码


•第三阶段


;; function sum(a, b) {;;     return a + b;
get_arg0 0: a /// 精简成获取参数列表中第0个参数 get_arg1 1: b add return
;; }
复制代码


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;}
复制代码


这样的操作只适合用于测试一下功能,实际生产中使用需要一个即可以在必要的时候调用 loop 又可以做到不抢占过多的 CPU 或者只抢占较少的 CPU 时间片。

四、libuv

1.libuv 简价

libuv 是一个使用 C 语言编写的多平台支持库,专注于异步 I/O。 它主要是为 Node.js 使用而开发的,但 Luvit、Julia、uvloop 等也使用它。


功能亮点


•由 epoll、kqueue、IOCP、事件端口支持的全功能事件循环。


•异步 TCP 和 UDP 套接字


•异步 DNS 解析


•异步文件和文件系统操作


•文件系统事件


•ANSI 转义码控制的 TTY


•具有套接字共享的 IPC,使用 Unix 域套接字或命名管道 (Windows)


•子进程


•线程池


•信号处理


•高分辨率时钟


•线程和同步原语

2.libuv 运行原理


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);    ...  }}
复制代码

3.简单使用

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);}
复制代码

五、QuickJS + libuv

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;}
复制代码


作者:京东零售客服研发 游小彬

来源:京东零售技术 转载请注明来源

发布于: 刚刚阅读数: 2
用户头像

还未添加个人签名 2024-01-12 加入

京东零售那些事,有品、有调又有料的研发资讯,带你深入了解程序猿的生活和工作。

评论

发布
暂无评论
跨端轻量JavaScript引擎的实现与探索_JavaScript_京东零售技术_InfoQ写作社区