写点什么

使用 WebAssembly 打造定制 JS Runtime

作者:Java-fenn
  • 2022 年 9 月 25 日
    湖南
  • 本文字数:2162 字

    阅读完需:约 7 分钟

JavaScript WebAssembly 大 厂 技 术 坚 持 周 更 精 选 好 文本文为来自 教育-成人与创新-前端团队 成员的文章,已授权 ELab 发布。


背景这是一次简短的整活与折腾,起因是在 lightdm-webkit2-greeter 这个 lightdm 插件中看到了自定义 JS Runtime 的魔力,它支持在显示管理器中使用 web 技术去自定义登录界面,与操作系统的交互是通过 Runtime 中的一组 JS API 来实现登录、关机、睡眠等功能。


https://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.cduktape.hduk_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 APIIO 功能实现


/* 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 = emccCCOPTS = -s DISABLE_EXCEPTION_CATCHING=0 -s ALLOW_MEMORY_GROWTH=1 -O3 --bindCCOPTS += -s EXPORTED_RUNTIME_METHODS=["cwrap"]CCOPTS += -s EXPORTED_FUNCTIONS=['_runScript','_main']CCOPTS += -I./engine # for combined sourcesDEFINES =


BUILD = wasm/index.html


all: {CC} (CPPFLAGS) {BUILD} {CCOPTS} {CCLIBS}


run:cd wasm && python3 -m http.server 8080 简单测试 makemake 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: https://duktape.org/index.html


[2]Main — Emscripten 3.1.21-git (dev) documentation: https://emscripten.org/


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


  • END -

用户头像

Java-fenn

关注

需要Java资料或者咨询可加我v : Jimbye 2022.08.16 加入

还未添加个人简介

评论

发布
暂无评论
使用 WebAssembly 打造定制 JS Runtime_Java_Java-fenn_InfoQ写作社区