在 WebAssembly 裡面跑 JavaScript 引擎
我相信很多人看到這個標題會覺得我在想什麼,然後我也是這麼覺得的 = =
反正就是無聊腦洞大開想到可不可以在 WebAssembly 中跑個小型的 JS 引擎,然後腦一熱就做下去了。不過好處是有學到怎麼用 Emscripten。
目前弄出的專案是這個: wasm-jseval ,有 Duktape 和 QuickJS 兩種引擎可以選擇使用。
至於那個的使用方法其實看 README 就知道怎麼用了,所以這篇文章只是想記錄我是怎麼弄的而已。
環境設置
我是在 WSL 的 Ubuntu 18.04 下面弄的,基本上只需要有 make
和 Emscripten 的 toolchain 就好了
Emscripten 的安裝方法其實參考官方的教學就好了: https://emscripten.org/docs/getting_started/downloads.html 不過為了方便我還是會在下面放我安裝的指令
1 | git clone https://github.com/emscripten-core/emsdk.git |
Duktape 的 eval
下載
到 Duktape 的下載頁 選個想要的版本,然後下載後解壓縮到你想要的地方。例如我是用 2.5.0 版本的。
1 | wget https://duktape.org/duktape-2.5.0.tar.xz |
寫個簡單的 eval binding
我的目的是弄出一個簡單的 eval
函數可以在 js
端呼叫,所以我們需要用 C 寫個簡單的 binding。至於怎麼寫就自己看 Duktape
網站上的教學和 Document 了。
1 |
|
編譯
其實因為 Duktape 本身編譯就很簡單,而 emcc 的用法也和 gcc 很像,所以最簡單就像這樣就能編譯了。輸出格式用 html 是為了方便測試。
1 | # eval.c 是你上面那個 binding 檔案的名稱 |
然後用個簡單的 webserver 如 httpsrv 或是 python
內建的在當前目錄建立 server,然後用瀏覽器瀏覽伺服器上的
eval.html
。 然後打開瀏覽器的 devtool console,然後輸入看看
Module
就代表成功了。
不過目前還沒有 export 出需要的 function,所以要加上需要的參數把它 export 給 js 使用。
1 | emcc eval.c ./src/duktape.c -o eval.html -s EXPORTED_FUNCTIONS='["_eval"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' |
然後重整頁面再回到 console,輸入看看
Module.cwrap('eval', 'string', ['string'])('1+1')
,這樣應該會回傳一個
"2"
的 string,這就代表已經成功了。 那個 cwrap
是個方便的函數能讓你直接從 js 呼叫 C 的函數,並且幫你處裡型別轉換。
不過這樣弄出來是兩個檔案 eval.js
和
eval.wasm
,在作為 library 時比較不方便。不過其實 Emscripten
有支援幫你把它轉 base64 塞入 js 的功能
-s SINGLE_FILE=1
,不過這樣會讓檔案變很大。
所以要順便加上一些最佳化參數給它,這樣會比較好。如
-Oz
。
下面這行是我最終的編譯指令,它只會產生一個
eval.js
,而且是 UMD 格式的,可以用 node.js 去
import。至於如何包裝成 library 就自己看 code 了。
1 | emcc -o eval.js eval.c ../duktape/src/duktape.c -lm -Oz --closure 1 -s WASM=1 -s SINGLE_FILE=1 -s AGGRESSIVE_VARIABLE_ELIMINATION -s MODULARIZE=1 -s EXPORTED_FUNCTIONS='["_eval"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' |
src/index.js
頂端的那個 EVALJS
是會給 bundler
自動替換的,把它想成是剛剛編譯出來的 eval.js
就好了。
QuickJS 的 eval
下載
這個就不多說了,在這邊下載。我使用的版本是 2019-12-21。
1 | wget https://bellard.org/quickjs/quickjs-2019-12-21.tar.xz |
寫個簡單的 eval binding
這個因為它的 Document 比較少,很多函數要去它給的
quickjs.h
裡面翻了,不過其實比 Duktape 還容易使用。
1 |
|
編譯
這個因為它不是一個檔案的,當你發現找不到 symbol
時就搜尋一下它在哪個檔案,然後都加進編譯指令中就可以編譯了。
有個要特別注意的是你需要自己定義 CONFIG_VERSION
這個
macro,不然編譯不給過,因為它本身有用到。
1 | emcc -o eval.html eval.c ./quickjs.c ./cutils.c ./libregexp.c ./libbf.c ./libunicode.c -DCONFIG_VERSION="\"1.0.0\"" -lm -s EXPORTED_FUNCTIONS='["_eval"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' |
然後一樣是開伺服器,用瀏覽器到 eval.html
中試試看
Module.cwrap('eval', 'string', ['string'])('1+1')
,回傳值一樣是
"2"
。
再來弄成一個檔案,最佳化等等都和上面一樣。最終編譯指令長這樣:
1 | emcc -o eval.js eval.c ./quickjs.c ./cutils.c ./libregexp.c ./libbf.c ./libunicode.c -DCONFIG_VERSION="\"1.0.0\"" -s WASM=1 -s SINGLE_FILE -s MODULARIZE=1 -lm -Oz -s EXPORTED_FUNCTIONS='["_eval"]' -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' --llvm-lto 3 -s AGGRESSIVE_VARIABLE_ELIMINATION=1 --closure 1 |
然後把它包裝成 library 的 code 其實和上面包裝 Duktape 版本的 code 是一模一樣的,因為我寫的 binding 介面設計的完全一樣。