前言
现在 MoonBit 因其众多的优势正在被全球的用户使用,不久前 MoonBit 收到美国客户的一封来信:
我们希望生成一些小适配器 WASM 组件作为我们工具的一部分。由于 WASM 是一种较低级的程序语言,直接生成 WASM 太痛苦了,并且生成大量 Rust 代码是有问题的,因为我们的 JS 用户在安装 Rust 工具链上有困难。由于 MoonBit 可以编译成 WASM,将这些适配器作为 MoonBit 代码生成,并从我们的 CLI 工具中通过 WASM 版的 MoonBit 编译器进行编译将是一个理想的解决方案。
在收到来信后,MoonBit 给美国客户提供了 WASM 版编译器的文件,以及一套使用 Rusty V8 实现用于运行WASM 版编译器的系统 API,帮助他们解决这个困扰良久的问题。
为什么选择使用 MoonBit?
生成 WASM 的可选项虽然还是有一些,如 GO、C#、Rust、C++ 和 C 等编程语言,但是 MoonBit 在这些中具有显著优势,生成 WASM 代码的质量更高,而且 Moo nBit 可以生成比 Rust 更紧凑的 WASM 文件,编译速度也更快 。
美国客户希望动态使用 MoonBit 编译器生成 WASM,但又不想每个平台都做一份,MoonBit 编译器本身可以编译成 wasm,配合 rusty_v8 便可跨平台运行,以下是详细的方案。
解决方案之使用 rusty_v8 运行
moonbit-compiler仓库里的wasm/js文件均使用 wasm_of_ocaml-6.0.1 构建, 最新版 wasm_of_ocaml 的API列表已经有所区别。以下统一将 wasm_of_ocaml 缩写为wasmoo。
虽然 wasmoo 官方没有提供 node以外的运行方式,但是只要实现了wasmoo的wasm加载脚本import object列表中的相应函数,使用其他wasm运行时也可以运行wasm版的MoonBit编译器。
rusty_v8 是一个为 Rust 语言提供的高质量绑定库,用于直接调用 Google 的 V8 JavaScript 引擎功能。
MoonBit 官方运行wasm的工具 moonrun 基于 rusty_v8 编写,故此处以 rusty_v8 为例演示如何实现wasmoo所需的外部API。
System API 概览
wasmoo所需的外部API一部分是js语言自带的,另一些则需要js运行时提供。要使用 rusty_v8 运行wasmoo编译出的wasm文件, 首先要做的事情是把API列表中需要js运行时实现的函数全部用rust实现并接入v8。
阅读wasmoo随wasm文件一同输出的js脚本可得以下API列表:
getenv : JSString -> JSString system : JSString -> Number log : JSString -> undefined is_file : JSString -> Number(1 | 0) is_directory : JSString -> Number(1 | 0) file_exists : JSString -> Number(1 | 0) // actually mains path_exists chmod : JSString, u32 as PermissionMode -> undefined truncate: JSString, u64 as Length -> undefined open : JSString as Path, i32 as Flags, Numberas PermissionMode -> i32 as FileDescriptor close : i32 as FileDescriptor -> undefined access : JSString as Path, i32 as Mode -> undefined write: i32 as FileDescriptor, UInt8Array as Buffer, i32 as Offset, i32 as Length, nullas Position -> Number read: i32 as FileDescriptor, UInt8Array as Buffer, i32 as Offset, i32 as Length, nullas Position -> Number fsync: i32 as FileDescriptor -> undefined file_size: i32 as FileDescriptor -> BigInt utimes: JSString as Path, F64 as AccessTime, F64 as ModifyTime -> undefined exit: i32 -> undefined isatty: i32 as FileDescriptor -> Number(1 | 0) getcwd: () -> JSString chdir: JSString -> undefined mkdir: JSString as Path, i32 as Mode -> undefined rmdir: JSString as Path -> undefined unlink: JSString as Path -> undefined readdir: JSString as Path -> Array
stat : JSString as Path -> Object lstat : JSString as Path -> Object fstat: i32 as FileDescriptor -> Object fchmod: i32 as FileDescriptor, i32 as Mode -> undefined ftruncate: i32 as FileDescriptor, u64 as Length -> undefined rename: JSString as OldPath, JSString as NewPath -> undefined decode_utf8: Uint8Array -> JSString encode_into: JSString, Uint8Array -> Object { read, written }
以上是这些 API 的「 伪类型」标注 ,但相信对熟悉 JavaScript 与 Rust 的开发者来说不难看懂。实现这些API时比较棘手的工作包括:
正确处理 wasmoo 常量到系统 API 选项的翻译
在v8和Rust之间高效交换数据(多见于处理UInt8Array时)
如何实现 API 原文链接:
https://www.moonbitlang.com/blog/moonbit-wasm-compiler
将 API 导入 Js 环境
这一步骤通过init_wasmoo函数完成
pub fn init_wasmoo<'s>( obj: v8::Local<'s, v8::Object>, scope: &mut v8::HandleScope<'s>, ) -> v8::Local<'s, v8::Object> { let getenv = v8::FunctionTemplate::new(scope, getenv); let getenv = getenv.get_function(scope).unwrap(); let ident = v8::String::new(scope, "getenv").unwrap(); obj.set(scope, ident.into(), getenv.into()); let system = v8::FunctionTemplate::new(scope, system); let system = system.get_function(scope).unwrap(); let ident = v8::String::new(scope, "system").unwrap(); obj.set(scope, ident.into(), system.into()); ... obj } 完整实现代码请见:
https://github.com/moonbitlang/moonc_wasm/blob/main/src/wasmoo_extern.rs
启动 v8 并加载 wasm 文件
在实现了wasmoo所需的外部API后,下一步是以合适的方式启动v8并通过js脚本实例化wasm。这一步通过函数run_wasmoo实现。
fn run_wasmoo(module_name: &str, argv: Vec ) -> anyhow::Result<()> { ... }不过在此之前,还有两件事要做:加载 wasm 文件和初始化 v8.
加载 wasm 文件通过函数 load_wasm_file 实现,为了避免重复 IO(也为了方便打包),在函数 load_wasm_file 中使用 Rust 宏 include_bytes! 将 wasm 文件直接作为 bytes 嵌入源码。该函数在 init_wasmoo 中被添加到js环境。
fn load_wasm_file( scope: &mut v8::HandleScope, _args: v8::FunctionCallbackArguments, mut ret: v8::ReturnValue, ) { let contents = Vec::from(include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/moonc/moonc.wasm"))); let len = contents.len(); let array_buffer = v8::ArrayBuffer::with_backing_store( scope, &v8::ArrayBuffer::new_backing_store_from_bytes(contents).make_shared(), ); let uint8_array = v8::Uint8Array::new(scope, array_buffer, 0, len).unwrap(); ret.set(uint8_array.into()); }初始化v8时,需要手动通过 flag 将栈空间开大一些。
fn initialize_v8() -> anyhow::Result<()> { v8::V8::set_flags_from_string(&format!("--stack-size={}", 102400)); v8::V8::set_flags_from_string("--experimental-wasm-exnref"); v8::V8::set_flags_from_string("--experimental-wasm-imported-strings"); let platform = v8::new_default_platform(0, false).make_shared(); v8::V8::initialize_platform(platform); v8::V8::initialize(); Ok(()) }run_wasmoo里需要初始化文件描述符相关 API 需要的映射表
let context = v8::Context::new(scope, Default::default()); // setup file descriptor table context.set_slot(wasmoo_extern::FdTable::new()); let scope = &mut v8::ContextScope::new(scope, context);最后, run_wasmoo 还需要正确地处理传给 wasm 文件的命令行参数数组, 在此处所建立的 v8 环境中,命令行参数数组对应一个全局变量 process_argv 。
实际上wasmoo传递命令行参数数组的方式就是将它放在wasm的import list里。这一点在稍后解析wasm的引导脚本时即可看出。
// setup argvlet process_argv = v8::Array::new(scope, argv.len() as i32); for (i, s) in argv.iter().enumerate() { let s = v8::String::new(scope, s).unwrap(); process_argv.set_index(scope, i as u32, s.into()); } let ident = v8::String::new(scope, "process_argv").unwrap(); global_proxy.set(scope, ident.into(), process_argv.into()); 可以看出, run_wasmoo 的任务只是将需要运行时传递的参数塞到 js 脚本和 v8 环境中。加载 wasm 并最终执行任务的细节在脚本 js_glue_for_wasmoo.js 中:
https://github.com/moonbitlang/moonc_wasm/blob/main/src/moonc/js_glue_for_wasmoo.js
该脚本有一个非常复杂的导入列表
const bindings = { ...... getcwd, chdir, mkdir, rmdir, unlink, argv: () => process_argv, readdir: (p) => read_dir(p), stat: (p, l) => alloc_stat(stat(p), l), lstat: (p, l) => alloc_stat(lstat(p), l), fstat: (fd, l) => alloc_stat(fstat(fd), l), ...... } const importObject = { Math: math, bindings, js, "wasm:js-string": string_ops, "wasm:text-decoder": string_ops, "wasm:text-encoder": string_ops, env: {}, }; importObject.OCaml = {};这个导入列表的复杂之处在于它里面的许多函数递归调用了从 wasm 中导出的函数(除此之外, importObject.OCaml 也需要用导出列表重新赋值。)
最终启动 wasm 需要将导出列表中的函数/对象引入 js 环境,然后调用从 wasm 中导出的 _initialize 函数。
let bytes = load_wasm_file(); let wasm_module = new WebAssembly.Module(bytes, options); let instance = new WebAssembly.Instance(wasm_module, importObject); Object.assign( importObject.OCaml, instance.exports, ); var { caml_callback, caml_alloc_times, caml_alloc_tm, caml_alloc_stat, caml_start_fiber, caml_handle_uncaught_exception, caml_buffer, caml_extract_bytes, string_get, string_set, _initialize, } = instance.exports; start_fiber = caml_start_fiber var buffer = caml_buffer?.buffer; var out_buffer = buffer && newUint8Array(buffer, 0, buffer.length); await _initialize(); 完整代码请见:
https://github.com/moonbitlang/moonc_wasm
总结
文章探讨了编程语言工具链在多平台分发时面临的挑战,包括 CPU 架构差异、API 兼容性问题等,并指出容器化方案体积庞大、Nix/Guix 学习成本高的局限性。MoonBit 创新性地通过 wasm_of_ocaml 将 OCaml 编写的编译器工具链(moonc/moonfmt等)编译为 WASM,实现了高性能、跨平台和嵌入式支持。
核心方案 Rusty_v8 扩展:针对非 Node 环境,通过实现 WASM 所需的系统 API (如文件操作is_file、chmod等),利用 Rusty_v8 引擎自定义运行时,使工具链可嵌入其他系统。
技术亮点:WASM版本保持编译性能,同时支持灵活调用;通过抽象系统 API 层,解耦运行时依赖,增强可移植性.此方案为工具链分发提供了轻量化、低侵入性的新思路,尤其适合需要跨平台或嵌入第三方工具的场景。
尽管没有尝试过,但理论上来说只要提供一套行为上兼容度足够的API,wasm版 M oonBit 工具链也可以在其他环境里运行(例如使用虚拟文件系统的浏览器环境)。期待看到有人想出更有创意的使用方式!
原标题: Running WebAssembly-based MoonBit compiler
MoonBit 介绍:
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.