使用 WebAssembly 打造定製 JS Runtime

語言: CN / TW / HK

       

本文為來自 教育-成人與創新-前端團隊 成員的文章,已授權 ELab 釋出。

背景

這是一次簡短的整活與折騰,起因是在 lightdm-webkit2-greeter 這個 lightdm 外掛中看到了自定義 JS Runtime的魔力,它支援在顯示管理器中使用 web 技術去自定義登入介面,與作業系統的互動是通過 Runtime 中的一組 JS API來實現登入、關機、睡眠等功能。

http://doclets.io/Antergos/web-greeter/stable

把 webkit 搬過來渲染系統介面,然後通過定製的 JS Runtime 與作業系統互動,相當於對瀏覽器本身進行了改造,關鍵的實現點是把系統呼叫封裝成了Native函式,並在JS Runtime中進行繫結,以實現瀏覽器介面控制作業系統。

這種方式和 Electron 的本質區別在於,無需讓瀏覽器與另外一個程序通訊,它直接拓展了 JS 的執行時環境,與 Node 的做法十分相像,不過這次我們越過中間商賺差價,自己實現 Runtime ,可以做的更小巧和定製化。

image.png

思考

直接在瀏覽器上去定製 Runtime 這個想法確實很酷,但顯然難度屬於地獄級,這相當於我們直接去爆改 V8、JavaScriptCore 這種成熟穩定又複雜的JS引擎來是實現 JS API層面的嵌入和拓展,但 JS 引擎並不只是瀏覽器獨有,真要改的話,可以找一個輕量、好改、好移植的。

很好,但是OS binding怎麼辦?總不能直接把瀏覽器裡的JS引擎整個替換成這個不復雜,又好改,又好移植的吧?確實這裡是一個坎,卡在這,活就整下去了,暫且先不做 OS binding,改做 Web binding,讓Web Assembly來跑 Runtime,然後在 Runtime 裡再跑JS,有點套娃了,但它依舊有一些應用的場景。

DEMO

起一個 JS 引擎

  • 要方便移植,要好改,方便我們快速的定製

  • Native 與 JS 的互動足夠簡單(包括資料型別的轉換,通訊的實現,事件迴圈等)

  • 因為是編譯到 WebAssembly 在 Web上跑,所以傳輸體積越小越好,同時執行時記憶體佔用也最好不要太大。

這裡選擇了 Figma 曾經的方案 - Duktape

  • duktape.c
    duktape.h
    duk_config.h
    
    • 完整的 ES5 支援

    • 支援垃圾回收

    • 位元組碼快取

    • 支援除錯功能

簡單寫一個函式,來實現JS的執行

extern "C" char* runScript(char* script){
duk_context *ctx = duk_create_heap_default();


duk_eval_string(ctx, script);
duk_pop(ctx); /* pop eval result */

duk_destroy_heap(ctx);

return "ok";
}

拓展一些 Runtime API

  • IO 功能實現

/* Being an embeddable engine, Duktape doesn't provide I/O
* bindings by default. Here'
s a simple one argument print()
* function.
*/
static duk_ret_t native_print(duk_context *ctx) {
duk_push_string(ctx, " ");
duk_insert(ctx, 0);
duk_join(ctx, duk_get_top(ctx) - 1);
printf("%s\n", duk_safe_to_string(ctx, -1));
return 0;
}
  • 繫結到 Runtime

duk_push_c_function(ctx, native_print, DUK_VARARGS);
duk_put_global_string(ctx, "print");
  • 這裡涉及到一些堆疊的基本概念,本文不做贅述,它在 Duktape 中的實現模型如下圖所示

至此,我們實現了一個基本的JS引擎,它可以完成 ES5 程式碼的解析和執行,我們在全域性物件上注入了一個 print 方法,它是一個 Native的實現,通過引擎內部的堆疊與 JS 互動,最後 使用Duktape提供的註冊方式暴露到 JS Runtime中

編譯成 WASM

這裡編譯器的實現選用 emscripten,用它直接生成相應的 WebAssembly 檔案和相應的 JS 膠水程式碼。

  • 把剛剛實現的 JS 執行函式暴露到 宿主環境中(另一個JS Runtime)


int main() {
EM_ASM("console.log('wasm js runtime is ready!')");
EM_ASM("window.runScript = Module.cwrap('runScript', 'string', ['string'])");
return 0;
}
  • 在編譯的時候,指定匯出函式

CCOPTS += -s EXPORTED_FUNCTIONS=['_runScript','_main']
  • 完整的Makefile 如下

DUKTAPE_SOURCES = ./engine/duktape.c

CC = emcc
CCOPTS = -s DISABLE_EXCEPTION_CATCHING=0 -s ALLOW_MEMORY_GROWTH=1 -O3 --bind
CCOPTS += -s EXPORTED_RUNTIME_METHODS=["cwrap"]
CCOPTS += -s EXPORTED_FUNCTIONS=['_runScript','_main']
CCOPTS += -I./engine # for combined sources
DEFINES =

BUILD = wasm/index.html

all: $(DUKTAPE_SOURCES) main.cpp
${CC} $(CFLAGS) $(CPPFLAGS) ${LDFLAGS} -o ${BUILD} ${DEFINES} ${CCOPTS} ${DUKTAPE_SOURCES} main.cpp ${CCLIBS}

run:
cd wasm && python3 -m http.server 8080

簡單測試

make
make run

看一下 WASM 體積,膠水程式碼+ WASM本體不 600KB 出頭,基本在一張大圖的範圍內,可以接受

藉助這兩個專案,至此我們完成了一整個 JS Runtime 定製的流程,目前看起來它完全是可用的:

  • 它足夠小巧,隨取隨用

  • 它與宿主 JS Runtime 完全隔離,足夠安全

  • WASM 實現相對來說在Web上是效能較好的,不會影響瀏覽器中JS執行緒

應用場景

  • JS 沙箱

  • 打造外掛系統

  • 把WASM 產物一移植到 WASI 以實現真正的 OS Binding

參考

  • Duktape [1]

  • Main — Emscripten 3.1.21-git (dev) documentation [2]

  • How to build a plugin system on the web and also sleep well at night [3]

參考資料

[1]

Duktape: http://duktape.org/index.html

[2]

Main — Emscripten 3.1.21-git (dev) documentation: http://emscripten.org/

[3]

How to build a plugin system on the web and also sleep well at night: http://www.figma.com/blog/how-we-built-the-figma-plugin-system/

- END -

:heart: 謝謝支援

以上便是本次分享的全部內容,希望對你有所幫助^_^

喜歡的話別忘了 分享、點贊、收藏 三連哦~。

歡迎關注公眾號 ELab團隊 收貨大廠一手好文章

位元組 / :   YCE7SSZ

:   http://job.toutiao.com/s/6QatD8H