#序列化 #运行 #沙箱 #数据 #结构

wasmbox

使用 WebAssembly 序列化任意运行中的 Rust 代码

2 个版本

0.1.1 2022 年 6 月 5 日
0.1.0 2022 年 5 月 25 日

#633WebAssembly

MIT 许可证

16KB
203

WasmBox

GitHub Repo stars crates.io docs.rs Rust

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 工具,用于加载和与客户模块交互。它通过 stdinstdout 传递客户模块之间的消息。它只支持具有类型 <String, String> 的客户模块,因为 stdinstdout 处理字符串数据。

每行被视为一条独立的消息,并转发到客户端模块,除了两条特殊命令。代码!!snapshot对客户端模块进行快照并保存到磁盘,打印出结果文件的名称。!!restore <filename>从这些快照中恢复客户端模块的状态。

安全性

此模块大量使用unsafe,特别是在WASM代码中。主机在加载预编译模块时也使用unsafe,这可能导致任意代码执行。预编译模块只有在您可以确信它们是由wasmtime/cranelift创建的情况下才是安全的。

限制

  • 由于它使用WebAssembly,可能比本地代码慢。
  • 为了提供一个确定的运行环境,阻止对沙箱外部的访问。系统时钟被模拟以创建一个确定的(但单调递增的)时钟。随机熵不是随机的,而是来自一个基于种子的伪随机数生成器。
  • 为了避免不必要的重复,状态不包括程序模块本身;确保在恢复快照时运行创建快照的相同的WASM模块是调用者的责任。
  • 目前,WasmBoxHost环境拥有WebAssembly环境的所有内容,包括可以跨实例共享的内容。如果您想运行相同模块的多个实例,例如,这将是不高效的。
  • 可能还有很多其他事情。

依赖关系

~0.6-1.2MB
~29K SLoC