近期的工作是改造为WebAssembly加入一组新的API来实现相应功能,翻找了一些资料和类似代码感觉这方面的内容还是比较少,可能大家关注的焦点还是更多在于怎么把某些代码编译到WASM上去或者怎么嵌入WASM来实现轻量级的Out-of-box Sandbox。所以在这里记录了一点自己的笔记和参考资料以备查阅。
WASM or WASI
WebAssembly ,顾名思义,是在Web上执行Assembly的技术。这项技术的初衷就是避开JavaScript在技术密集型应用上的先天不足,尝试在保证安全性满足Web标准要求的前提下允许浏览器直接运行类汇编代码。在Web平台中运行时,浏览器通过WebAssembly运行时向WASM应用暴露了一些API可供调用,后来WASM被广泛移植到其他非浏览器的运行时(如wasmtime)后,WASI标准为WASM应用提供了一组类似POSIX Syscall的API,使得大部分满足POSIX标准的控制台应用和第三方库可以移植到WASM平台运行。在实现了WASI标准后,WASM运行时可以视作一个具有安全性和隔离性的极其轻量级的应用层沙箱,如编辑器Lapce就使用WASI来运行插件并使用 wasi-experimental-http 提案来调用RPC拓展编辑器功能。
理论上来讲,我们只需要仿照WASI的标准来实现一组类似的API,即可拓展运行时的功能为WASM应用提供新的Syscall。这也是大部分WASM相关特性提案的标准实现流程,但是在实践层面上,由于本人的缺少经验和菜,还是趟了一些坑,下面做一个记录和笔记。我们采用的运行时是wasmtime,WASM应用使用Rust编写。
一个提案的诞生
像大多数前端组织的委员会提案类似,WASM的特性提案要经过5个甚至更多的阶段,你可以在这里看到一堆在流程中的提案。流程1 Feature Proposal到 Proposed Spec Text Available会提出一些约定或文档,流程3 Implementation Phase 开始则基于主流运行时产出API的实现。
在许多所有者为WebAssembly组织的提案仓库中,会发现并没有太多代码。因为这些仓库的作用是公布接口定义规范(类似API Document),你可能会在phases
或类似的子目录里发现一些.wit
或.witx
文件。大部分提案的接口定义规范分为ephemeral(正在开发版本)/snapshot(冻结可供使用版本)/old(废弃版本)
三个生命周期,一般的实现仅会实现snapshot版本的接口规范。这些witx
文档定义了接口中要暴露出的实现和数据结构,要注意witx
并不是一个很有表现力的DSL,你只能使用一些较为基础的可序列化的数据结构和他们的组合,这保证了WASM的语言无关特性。一般来讲,witx或者wit文档是需要手动编写的(在这里有WITX的语法文档),而另有一些自动化Code Generator帮助你通过witx生成其他代码和接口。
完成API Docs之后,要想对Feature进行实现,还需要完成两处代码的编写:
- 按照API文档生成WASM应用所使用的SDK,WASM应用通过调用SDK提供的函数来发出Syscall.你可以使用
witx-codegen
或witx-bindgen
(Rust)自动生成SDK的绑定. - 按照API文档生成WASM Runtime侧的实际调用函数,它接受Syscall并完成真正的工作并返回数据.可以使用
wiggle
(Rust)自动生成Host侧的一个接口定义,通过实现或继承这个接口来补全所有实际调用函数.
就我个人的体验而言,witx
自身似乎是缩进不敏感的,且witx-bindgen
能较好的兼容大部分的witx语法。但是witx-codegen
的兼容性并没有那么好,有几个容易忽略的语法点(考虑到这个项目已经两年没有更新了,可能有更多奇奇怪怪的问题):
- 函数的result只能使用
typename
定义出的Type Alias. - Type Alias不能定义 (@witx pointer u8)和(@witx const_pointer u8),会提示
未实现
- 无法 import memory
(import "memory" (memory))
(看上去似乎是个Bug) - 使用$handle之前必须定义一个resource(witx-bindgen不需要resource定义)
总之,如果你可以成功生成两边的定义接口代码,那么你几乎要完成整个设计工作了。
Host Impl
实现一个功能最重要的部分就是完成WASM Host运行时对功能相关接口的具体实现。一般我们会去更改Runtime的代码,对于Wasmtime而言,如果你的功能比较简单,不依赖于其他提案接口(如WASI),只需要简单创建一个Context,利用Wiggle根据之前定义的接口文档自动实现一个Trait(这个Trait包含了接口文档中所有的接口和类型,通过这个Trait约束了你实现函数的传参和返回值),最后使Context Impl此Trait。这一部分似乎没有什么值得说的, A Lot of Dirty Work.
这里给出一个最小实现,我们假设witx中module命名为$wasi_ephemeral_x
,那么生成的Trait名称为WasiEphemeralX,同时我们创建一个Struct命名为WasiXCtx
,作为这个Trait的实现,同时我们在witx中创建了自定义的错误枚举类x_error(XError in Rust),如下所示,success代表无错误。
(typename $x_error
(enum (@witx tag u16)
$success
$runtime_error
$io_error
)
)
为了储存Handle类型的对象,我们在Ctx中先创建一个Table类型,这里完全参考WASI项目中的实现.一个简单的Starter-Template如下所示,编译项目,rustc或你IDE中的rust-analyzer就会提示你WasiRdmaCtx
应该实现那些方法啦!
wiggle::from_witx!({
witx: ["$WASI_ROOT/witx/wasi_ephemeral_x.witx"],
});
//记得在build.rs里面指定$WASI_ROOT的路径! Like below:
//let wasi_root = std::env::current_dir().unwrap();
//println!("cargo:rustc-env=WASI_ROOT={}", wasi_root.display());
impl wiggle::GuestErrorType for types::XError {
fn success() -> Self {
Self::Success
}
}
pub struct WasiXCtx {
table: table::Table,
}
impl WasiEphemeralX for WasiRdmaCtx {}
Handle是什么?
但是有一点要特意提到,Witx简陋的抽象描述能力远远不足以描述复杂的底层架构,因此通常我们会使用Handle类型隔离底层的复杂类型。Witx对Handle的定义是Handles are opaque references to objects managed by the host.
——如同Witx对其他东西的定义一样抽象且莫名其妙。但是实际上,如果你阅读WASI的实现代码或者利用宏展开wiggle::from_witx
宏代码,就会发现这句话之后的真正含义:
type handle(u32)
是的,Handle在实际传输中就是一个无符号整数,而在Host侧,开发者需要自行定义一个映射机制(建议参考WASI利用Hashmap的实现),将真正的底层结构体(或他们的指针)妥帖存放好,Handle实际上只是个Key,需要使用结构体时凭借Key从映射表中获得并进行操作。因此所有返回Handle的函数都应当将其Insert到一个全局映射表,并返回其Key,所有传参为Handle的函数也应当首先查询映射表。
宏 Expand
Wiggle的核心功能入口就是wiggle::from_witx
宏,每次编译时Wiggle会读取本地Witx文件,更新生成Trait和必须的Types等。但是Rust的宏的调试和开发体验确实不尽如人意,特别是对于Clion这种比较重量级的IDE,时常会因为宏的缓存导致语法提示或代码生成提示等功能不正常。
如果想手动查看宏生成的代码内容,可以在调用宏的代码所在的crate中使用 cargo expand
命令(需要安装cargo-expand
包),注意它其实调用了cargo rustc --profile=check -- -Zunpretty=expanded
命令,因此必须安装+nightly toolchain.
如果因为宏缓存没有更新导致Clion代码提示出现问题,建议删除缓存等他重建,插件的宏缓存在~/.cache/JetBrains/<IDE产品名称>/<Project名称或路径>/intellij-rust/macros/cache/
,删除并重启Clion即可。
Wasm Impl
我们另外要做的事情就是生成WASM侧的代码,类比于RPC中的Stub
,这些代码要做的事情也无非是将各语言的数据结构和类型转换为C中的类型,然后发出系统调用,一个简单的实例如下(由witx-bindgen生成):
pub unsafe fn x_init(
node: &str,
service: &str,
hints: XStruct,
cap: YStruct,
is_server: u8,
) -> Result<ZHandle, XError> {
let mut rp0 = MaybeUninit::<ZHandle>::uninit();
let ret = wasi_ephemeral_x::x_init(
node.as_ptr() as i32,
node.len() as i32,
service.as_ptr() as i32,
service.len() as i32,
&hints as *const _ as i32,
&cap as *const _ as i32,
is_server as i32,
rp0.as_mut_ptr() as i32,
);
match ret {
0 => Ok(core::ptr::read(rp0.as_mut_ptr() as i32 as *const ZHandle)),
_ => Err(XError(ret as u16)),
}
}
pub mod wasi_ephemeral_x {
#[link(wasm_import_module = "wasi_ephemeral_x")]
extern "C" {
pub fn x_init(
arg0: i32,
arg1: i32,
arg2: i32,
arg3: i32,
arg4: i32,
arg5: i32,
arg6: i32,
arg7: i32,
) -> i32;}
当然,这些枯燥的工作肯定不需要我们手动完成,如果你的WASM侧的语言是Rust,那我推荐witx-bindgen
,如果你的语言是Zig/assemblyscript
,也许可以去踩下witx-codegen 的坑,不过这个项目也已经很久没啥更新了,文档也写的奇奇怪怪。
另外,Wasmtime和许多组织声称要讲API描述语言由 witx
换成wit
,语法逻辑的介绍可以参考wit-bindgen
项目的README,感觉复杂度上升了很多,但是带来的好处也多多:原生支持了C++/Go/Rust等的Bind,有了很多新的类型,引入了所有权机制等。
受上面因素影响,witx-bindgen被移除了wasmtime项目的crates,你现在只能在这里找到他.
学习了