2 个版本
0.1.1 | 2022 年 6 月 5 日 |
---|---|
0.1.0 | 2022 年 5 月 25 日 |
#633 在 WebAssembly
16KB
203 行
WasmBox
WasmBox 将运行中的 Rust 代码转换为可序列化的数据结构。
它是通过将其编译为 WebAssembly 并在沙箱中运行来实现的。为了快照运行中的代码,它序列化沙箱的线性内存,该内存包含程序的整个堆。
WasmBox 是新功能和实验性的。 在生产代码中依赖它之前,请随意提交问题,我们可以讨论 :)。
接口
WasmBox 有两个组件:宿主环境和客座模块。宿主环境是与 WasmBox 外部交互的程序。客座模块是运行在 内部 的程序。客座模块是一个独立的 Rust 编译器工件,编译为 wasm32-wasi
目标。
两个组件通过 WasmBox 提供的双向、类型化通信进行交互。为开发客座模块提供了同步和异步接口。
要使用异步接口,创建一个具有以下签名的函数:async fn run(ctx: WasmBoxContext<String, String>
,并用 #[wasmbox]
注解装饰。
以下示例实现了一个简单的有状态 WasmBox 客座模块,它内部存储计数器状态。它等待来自宿主环境的输入。当它从宿主环境接收到 "up"
或 "down"
输入时,它会在内部修改计数器状态并将其发布回宿主环境。
use wasmbox::prelude::*;
#[wasmbox]
async fn run(ctx: WasmBoxContext<String, String>) {
let mut c = 0;
loop {
let message = ctx.next().await;
match message.as_ref() {
"up" => c += 1,
"down" => c -= 1,
_ => continue,
}
ctx.send(format!("value={}", c));
}
}
WasmBoxContext 的 <String, String>
属性分别表示进入和离开 WasmBox 的数据类型。 ctx.next()
返回第一种类型的数据,而 ctx.send()
则期望第二种类型的数据。如果您正在编写自己的宿主环境,可以在这里使用任何 (de)serializable 类型,只要宿主环境和客户模块中的类型对相同即可。由于客户模块是在运行时动态加载的,这不能由编译器强制执行,因此这取决于您来确保。
由 wasmbox-cli
提供的演示宿主环境仅支持 <String, String>
,因此我们在这里使用它。
编译客户模块
客户模块的 Cargo.toml
应包含以下内容
[lib]
crate-type = ["cdylib", "rlib"]
它们应该使用目标 wasm32-wasi
编译,如下所示
cargo build --release --target=wasm32-wasi
您可能需要安装 wasm32-wasi
目标(例如使用 rustup
)。
在 target/wasm32-wasi/release
下寻找 .wasm
文件。
宿主环境
宿主环境始终是相同的(同步)接口,无论客户模块是使用异步还是同步接口。
构建宿主环境(WasmBoxHost
)需要两个东西:要加载的模块以及用于从客户模块接收消息的回调。模块可以是 .wasm
文件或预编译模块的形式传递。
请参阅 wasmbox-cli
以了解实现宿主环境的示例。
use wasmbox_host::WasmBoxHost;
use anyhow::Result;
fn main() -> Result<()> {
let mut mybox = WasmBoxHost::from_wasm_file("path/to/some/module.wasm",
|st: String| println!("guest module says: {}", st))?;
// Send some messages into the box:
mybox.message("The guest module will receive this message.");
mybox.message("And this one.");
// Turn the state into a serializable object.
let state = mybox.snapshot_state()?;
// Or, serialize directly to disk:
mybox.snapshot_to_file("snapshot.bin")?;
// We can interact more with the box:
mybox.message("Pretend this message has a side-effect on the box's state.");
// And then restore the state, undoing the last side-effect.
mybox.restore_snapshot(&state)?;
// Or, restore directly from disk:
mybox.restore_snapshot_from_file("snapshot.bin")?;
Ok(())
}
同步客户接口
而不是编写异步函数来实现客户,您可以通过实现一个 trait
并使用 #[wasmbox_sync]
宏来实现。
每个 WasmBox 都通过调用 init
来构建。来自宿主的消息通过调用 trait 的 message
函数传递。为了将消息传递回宿主,init
中提供了一个用于接收客户模块消息的boxed callback
函数。
允许 init
函数和 message
函数调用回调,并且可以多次调用。要从 message
调用回调,您可以将它存储在类型本身中。
use wasmbox::prelude::*;
#[wasmbox_sync]
struct Counter {
count: u32,
callback: Box<dyn Fn(String) + Send + Sync>,
}
impl WasmBox for Counter {
type Input = String;
type Output = String;
fn init(callback: Box<dyn Fn(Self::Output) + Send + Sync>) -> Self
where
Self: Sized,
{
Counter { count: 0, callback }
}
fn message(&mut self, input: Self::Input) {
match input.as_ref() {
"up" => self.count += 1,
"down" => self.count -= 1,
_ => return
}
(self.callback)(format!("value={}", self.count));
}
}
CLI 工具
提供了一个 CLI 工具,用于加载和与客户模块交互。它通过 stdin
和 stdout
传递客户模块之间的消息。它只支持具有类型 <String, String>
的客户模块,因为 stdin
和 stdout
处理字符串数据。
每行被视为一条独立的消息,并转发到客户端模块,除了两条特殊命令。代码!!snapshot
对客户端模块进行快照并保存到磁盘,打印出结果文件的名称。!!restore <filename>
从这些快照中恢复客户端模块的状态。
安全性
此模块大量使用unsafe
,特别是在WASM代码中。主机在加载预编译模块时也使用unsafe,这可能导致任意代码执行。预编译模块只有在您可以确信它们是由wasmtime/cranelift创建的情况下才是安全的。
限制
- 由于它使用WebAssembly,可能比本地代码慢。
- 为了提供一个确定的运行环境,阻止对沙箱外部的访问。系统时钟被模拟以创建一个确定的(但单调递增的)时钟。随机熵不是随机的,而是来自一个基于种子的伪随机数生成器。
- 为了避免不必要的重复,状态不包括程序模块本身;确保在恢复快照时运行创建快照的相同的WASM模块是调用者的责任。
- 目前,
WasmBoxHost
环境拥有WebAssembly环境的所有内容,包括可以跨实例共享的内容。如果您想运行相同模块的多个实例,例如,这将是不高效的。 - 可能还有很多其他事情。
依赖关系
~0.6-1.2MB
~29K SLoC