跨端輕量JavaScript引擎的實現與探索
一、JavaScript
1.JavaScript語言
JavaScript是ECMAScript的實現,由ECMA 39(歐洲計算機制造商協會39號技術委員會)負責制定ECMAScript標準。
ECMAScript發展史:
時間 | 版本 | 說明 |
1997年7月 | ES1.0 發布 | 當年7月,ECMA262 標準出臺 |
1998年6月 | ES2.0 發布 | 該版本修改完全符合ISO/IEC 16262國際標準。 |
1999年12月 | ES3.0 發布 | 成為 JavaScript 的通行標準,得到了廣泛支持 |
2007年10月 | ES4.0草案發布 | 各大廠商意見分歧,該方案未通過 |
2008年7月 | 發布ES3.1,并改名為ECMAScript 5 | 廢除ECMAScript 4.0,所以4.0版本不存在 |
2009年12月 | ESt 5.0 正式發布 | |
2011年6月 | ES5.1 發布 | 該版本成為了 ISO 國際標準(ISO/IEC 16262:2011) |
2013年12月 | ES6 草案發布 | |
2015年6月 | ES6 正式發布,并且更名為“ECMAScript 2015” | TC39委員會決定每年發布一個ECMAScript 的版本 |
2.JavaScript引擎
JavaScript引擎是指用于處理以及執行JavaScript腳本的虛擬機。
常見的JavaScript引擎:
引擎 | 所屬機構/個人 | 瀏覽器 | 說明 |
SpiderMonkey | Mozilla | Firefox | 第一款JavaScript引擎,早期用于 Netscape Navigator,現時用于 Mozilla Firefox。是用C語言實現的,還有一個Java版本叫Rhino;Rhino引擎由Mozilla基金會管理,開放源代碼,完全以Java編寫,用于 HTMLUnit;而后TraceMonkey引擎是基于實時編譯的引擎,用于Mozilla Firefox 3.5~3.6版本;JaegerMonkey:結合追蹤和組合碼技術大幅提高性能,用于Mozilla Firefox 4.0以上版本 |
JavaScriptCore | Apple | Safari | 簡稱JSC,開源,用于webkit內核瀏覽器,如 Safari ,2008 年實現了編譯器和字節碼解釋器,升級為了SquirrelFish。蘋果內部代號為Nitro的 JavaScript 引擎也是基于 JSC引擎的。至于具體時間,JSC是WebKit默認內嵌的JS引擎,而WebKit誕生于1998年,Nitro是為Safari 4編寫,Safari 4是2009年6月發布。 |
V8 | Chrome | 2008年9月,Google的V8引擎第一個版本隨著Chrome的第一個版本發布。V8引擎用 C++編寫,由 Google 丹麥開發,開源。除了Chrome,還被運用于Node.js以及運用于Android操作系統等 | |
Chakra | Microsoft | Edge、IE | 譯名查克拉,用于IE9、10、11和Microsoft Edge,IE9發布時間2011年3月 |
JerryScript | 三星 | 三星推出的適用于嵌入式設備的小型 JavaScript 引擎,2015年開源 | |
Nashorn | Oracale | 從 JDK 1.8 開始,Nashorn取代Rhino(JDK 1.6, JDK1.7) 成為 Java 的嵌入式 JavaScript 引擎,JDK1.8發布于2014年 | |
QuickJS | Fabrice Bellard | QuickJS 是一個小型的嵌入式 Javascript 引擎。 它支持 ES2023 規范,包括模塊、異步生成器、代理和 BigInt。 它可以選擇支持數學擴展,例如大十進制浮點數 (BigDecimal)、大二進制浮點數 (BigFloat) 和運算符重載。 | |
Hermes | 引擎,Facebook在Chain React 2019 大會上發布的一個嶄新JavaScript引擎,用于移動端React Native應用的集成,開源 |
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的假設是錯誤的,此時優化編譯生成的機器代碼就不能再使用了,于是就需要進行回退到字節碼的操作。
三、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函數最棧幀大小為兩個字節
get_arg0 | get_arg1 | add | return |
1 | 2 | 3 | 將棧頂的數據3返回 |
1 |
5.內存管理
QuickJS通過引用計算來管理內存,在使用C API時需要根據不同API的說明手動增加或者減少引用計數器。
對于循環引用的對象,QuickJS通過臨時減引用保存到臨時數組中的方法來判斷相互引用的對象是否可以回收。
6.QuickJS簡單使用
從github上clone完最新的源碼后,通過執行(macos 環境)以下代碼即可在本地安裝好qjs、qjsc、qjscalc幾個命令行程序
sudo make
sudo 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 install
qjsc -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;
}