Chapter 08

WASI:WebAssembly 系统接口

了解 WASI 规范,使用 wasmtime/wasmer 运行时在服务端安全执行 Wasm 模块

WASI:WebAssembly 系统接口

为什么需要 WASI?

在浏览器中,WebAssembly 通过 JavaScript API 与系统交互(DOM、Fetch、Web Storage)。但在浏览器之外(服务器、CLI 工具、嵌入式设备),Wasm 需要访问文件系统、网络、时钟等系统资源。

WASI(WebAssembly System Interface)是 Bytecode Alliance 制定的标准接口规范,为 Wasm 提供了一套类 POSIX 的系统调用接口。它的设计原则是基于能力的安全模型(Capability-Based Security):默认零权限,只有宿主显式授予的能力才能使用。

WASI Preview 1
第一版 WASI(2019),定义了文件 I/O、目录遍历、时钟、随机数等基础接口。大多数工具链和运行时实现了这一版。
WASI 0.2(2024)
基于组件模型重新设计,引入 WASIp2。新增 HTTP(wasi:http)、套接字(wasi:sockets)等接口,是未来的主要版本。

wasmtime:生产级 Wasm 运行时

# 安装 wasmtime
curl https://wasmtime.dev/install.sh -sSf | bash

# 编译 Rust 程序到 WASI 目标
rustup target add wasm32-wasip1
cargo build --target wasm32-wasip1 --release

# 运行(默认允许标准输入输出)
wasmtime target/wasm32-wasip1/release/hello.wasm

# 授予文件系统权限(能力授予)
wasmtime --dir /tmp hello.wasm

# 映射目录(沙箱内路径 → 宿主路径)
wasmtime --dir /sandbox::/host/path hello.wasm

Rust WASI 程序示例

// src/main.rs:编译目标 wasm32-wasip1
use std::io::{self, BufRead};
use std::fs;

fn main() -> io::Result<()> {
    // 读取命令行参数
    let args: Vec<String> = std::env::args().collect();

    if args.len() > 1 {
        // 读取文件(需要 wasmtime --dir 授权)
        let content = fs::read_to_string(&args[1])?;
        println!("文件内容:{}", content);
    } else {
        // 从 stdin 读取
        let stdin = io::stdin();
        for line in stdin.lock().lines() {
            println!("读到:{}", line?);
        }
    }
    Ok(())
}

Spin:WASI 微服务框架

# 安装 Spin CLI
curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash

# 创建新的 HTTP 处理器
spin new http-rust my-service
cd my-service
spin build
spin up
// src/lib.rs:Spin HTTP 处理器
use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_component;

#[http_component]
fn handle_request(req: Request) -> Response {
    println!("请求路径: {}", req.uri().path());
    Response::new(200, "Hello from Wasm!")
}

WASI 能力模型深度解析

什么是基于能力的安全模型?

传统的 POSIX 程序以 UID/GID 作为权限控制——进程可以访问该用户有权访问的所有资源。这是环境权限(Ambient Authority)模型,权限是隐式的。

WASI 使用基于能力的安全模型(Capability-Based Security):程序默认没有任何权限,宿主(运行时)必须显式传递文件描述符、目录句柄等「能力令牌」给程序。没有获得能力令牌的程序无法访问任何资源——即使是同一系统上的其他文件。

传统 POSIX 程序 WASI 程序 ┌─────────────────┐ ┌─────────────────┐ │ Process │ │ Wasm Module │ │ UID: 1000 │ │ 默认权限:零 │ │ │ │ │ │ 可访问: │ │ 只能访问: │ │ ~/anything │ │ 宿主显式给予的 │ │ /tmp/anything │ │ 文件描述符 │ │ /etc/passwd │ │ (fd 0=stdin, │ │ 所有网络端口 │ │ fd 1=stdout, │ └─────────────────┘ │ fd 3=/sandbox │ │ 仅此而已!) │ └─────────────────┘

WASI 接口层次结构

wasi:clocks(时钟)
提供 monotonic_clock(单调时钟,用于计时)和 wall_clock(挂钟时间,不保证单调)。程序需要显式导入时钟能力才能使用时间相关功能。
wasi:filesystem(文件系统)
基于「预开放目录(preopened directory)」的文件访问。宿主将某些目录的句柄传递给程序,程序只能在这些目录树内操作,不能「越狱」到上层目录。
wasi:sockets(网络,WASI 0.2 新增)
TCP/UDP 套接字接口,需要宿主显式授予网络访问能力。不同于 POSIX,默认没有任何网络访问权限。
wasi:random(随机数)
提供密码学安全的随机数生成器。在 WASI 中,即使生成随机数也需要显式能力,防止侧信道攻击。
wasi:http(HTTP,WASI 0.2 新增)
HTTP 请求/响应接口,让 Wasm 模块可以处理 HTTP 请求(作为服务器)或发出 HTTP 请求(作为客户端)。

Rust WASI 程序开发完整示例

// Cargo.toml
[package]
name = "wasi-tool"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
// src/main.rs — 一个 WASI 命令行工具:JSON 格式化器
use std::io::{self, Read, Write};

fn main() -> io::Result<()> {
    let args: Vec<String> = std::env::args().collect();

    // 从 stdin 读取 JSON 输入
    let mut input = String::new();
    io::stdin().read_to_string(&mut input)?;

    // 解析并格式化 JSON
    let value: serde_json::Value = serde_json::from_str(&input)
        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;

    let indent = if args.len() > 1 {
        args[1].parse::<usize>().unwrap_or(2)
    } else { 2 };

    // 输出格式化后的 JSON 到 stdout
    let formatter = serde_json::ser::PrettyFormatter::with_indent(
        b" ".repeat(indent).as_bytes()
    );
    let formatted = serde_json::to_string_pretty(&value)?;
    io::stdout().write_all(formatted.as_bytes())?;
    println!();
    Ok(())
}
# 编译为 WASI 目标
cargo build --target wasm32-wasip1 --release

# 运行(wasmtime)
echo '{"name":"Alice","age":30}' | wasmtime target/wasm32-wasip1/release/wasi-tool.wasm
# 输出:
# {
#   "name": "Alice",
#   "age": 30
# }

# 在 Node.js 22+ 中运行(原生 WASI 支持)
node --experimental-wasm-modules wasi-runner.mjs

Node.js 中的 WASI API

// wasi-runner.mjs — 在 Node.js 中运行 WASI 程序
import { WASI } from 'wasi';
import { readFileSync } from 'fs';
import { argv, env } from 'process';

// 配置 WASI 能力
const wasi = new WASI({
  args: argv,          // 传递命令行参数
  env,                 // 传递环境变量
  preopens: {
    // 将宿主目录 '/data' 映射为 Wasm 内的 '/sandbox'
    '/sandbox': '/data',
    // Wasm 程序只能访问这个映射的目录
  },
  returnOnExit: true,  // 程序调用 exit() 时返回而不是抛出
});

// 读取并实例化 Wasm 模块
const wasm = readFileSync('./wasi-tool.wasm');
const module = await WebAssembly.compile(wasm);
const instance = await WebAssembly.instantiate(module, {
  // wasi.getImportObject() 提供所有 WASI 系统调用的实现
  wasi_snapshot_preview1: wasi.wasiImport,
});

// 启动程序(调用 _start = main 函数)
wasi.start(instance);

WASI 0.2 与旧版 Preview 1 的区别

wasm32-wasip1(Preview 1)
Rust 的稳定编译目标(2024年稳定)。对应 WASI Preview 1 接口规范。大多数运行时(wasmtime、Node.js WASI API、Docker+WASI)支持此版本。产生的是普通 Core Wasm 模块,以特定约定导入 WASI 函数(模块名 "wasi_snapshot_preview1")。
wasm32-wasip2(Preview 2 / WASI 0.2)
基于组件模型的新版 WASI,Rust 1.82+ 支持(仍是 tier 2 目标)。产生的是 Wasm 组件格式(不是普通模块),需要支持组件模型的运行时(wasmtime 14+)。新增 wasi:http/incoming-handler(HTTP 服务器接口)和 wasi:sockets(TCP/UDP)。
wasip2 的 HTTP 服务器示例
用 Spin 框架 + WASI 0.2,可以用 Rust 写出完全符合 WASI HTTP 接口的 HTTP 服务器,部署到任何支持 WASI 0.2 的运行时或平台(Fermyon Cloud、Azure Container Apps WASI 版等),无需改动代码。
// 使用 WASI 0.2 HTTP 接口(需要 wasm32-wasip2 目标)
// Cargo.toml:
// [dependencies]
// wit-bindgen = "0.28"
// wasi = "0.13"

// wit-bindgen 为 wasi:http 自动生成 Rust 绑定
wit_bindgen::generate!({
    world: "wasi:http/proxy",
    path: "wit",
});

struct HttpHandler;

impl exports::wasi::http::incoming_handler::Guest for HttpHandler {
    fn handle(request: IncomingRequest, response_out: ResponseOutparam) {
        // 读取请求路径
        let path = request.path_with_query()
            .unwrap_or("/".to_string());

        // 构造 HTTP 响应
        let response = OutgoingResponse::new(
            Fields::from_list(&[(
                "content-type".to_string(),
                b"text/plain".to_vec(),
            )]).unwrap()
        );
        response.set_status_code(200).unwrap();

        let body = response.body().unwrap();
        let output = body.write().unwrap();
        output.blocking_write_and_flush(
            format!("Hello from WASI 0.2! Path: {}", path).as_bytes()
        ).unwrap();

        ResponseOutparam::set(response_out, Ok(response));
    }
}

export!(HttpHandler);
wasip1 程序无法直接在 wasip2 运行时运行

WASI Preview 1 和 WASI 0.2 是不同格式:Preview 1 是普通 Core Wasm 模块,WASI 0.2 是 Wasm 组件格式。两者互不兼容。wasmtime 14+ 提供了"适配器"(adapter),可以将 wasip1 模块提升为 wasip2 组件:wasm-tools component new my_prog.wasm --adapt wasi_snapshot_preview1=adapter.wasm -o my_component.wasm。这是在 WASI 生态过渡期的重要工具。

wasmtime CLI 高级用法

# 查看 Wasm 模块的导入需求
wasmtime inspect module.wasm

# 启用性能计数器
wasmtime --profile=vtune module.wasm

# 限制内存使用
wasmtime --max-wasm-stack 1MiB module.wasm

# 启用 WASI 网络(需要 WASI 0.2)
wasmtime --wasi-modules=experimental-wasi-nn module.wasm

# 运行时 AOT 编译(Ahead-of-Time,加快启动速度)
wasmtime compile module.wasm -o module.cwasm  # 预编译
wasmtime run module.cwasm                     # 直接运行编译结果

本章小结

本章核心要点