从 "wasm 模块" 到 "wasm 组件"

传统 WebAssembly 的痛点在于接口:模块之间只能传递 i32/i64/f32/f64 四种数值,字符串、结构体都要靠手工管理线性内存和偏移量,跨语言调用极其痛苦。

组件模型(Component Model) 用 WIT(Wasm Interface Type) 解决这件事:用 IDL 描述接口,工具链自动生成绑定代码,让不同语言写的组件可以像函数调用一样互相调用。

第一个组件:用 Rust 编写

先定义接口:

// wit/calculator.wit
package example:calculator@0.1.0;

interface ops {
  add(a: s32, b: s32) -> s32;
  // 字符串、列表、record 都是一等类型
  format(value: f64, unit: string) -> string;
  stats(values: list<f64>) -> tuple<f64, f64>;
}

world calculator {
  export ops;
}

Rust 侧实现:

// src/lib.rs
wit_bindgen::generate!({
    world: "calculator",
    exports: { "example:calculator/ops": Ops },
});

struct Ops;

impl exports::example::calculator::ops::Guest for Ops {
    fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    fn format(value: f64, unit: String) -> String {
        format!("{value:.2}{unit}")
    }

    fn stats(values: Vec<f64>) -> (f64, f64) {
        let sum: f64 = values.iter().sum();
        let avg = if values.is_empty() { 0.0 } else { sum / values.len() as f64 };
        (sum, avg)
    }
}
# 安装组件工具链
cargo install cargo-component

# 编译为 wasm 组件
cargo component build --release
# 产物:target/wasm32-wasip2/release/calculator.wasm

注意目标三元组是 wasm32-wasip2(WASI 0.2),而不是旧的 wasm32-wasi。

在宿主中调用(JavaScript)

npm i @bytecodealliance/jco
// 生成 JS 绑定
npx jco transpile calculator.wasm -o dist
import { calculator } from "./dist/calculator.js";

const { ops } = calculator;
console.log(ops.add(2, 3));                  // 5
console.log(ops.format(12.3456, "ms"));      // "12.35ms"
console.log(ops.stats([1, 2, 3, 4]));        // [10, 2.5]

字符串和列表自动完成编解码,宿主完全不需要碰线性内存。

WASI 0.2:标准化的系统接口

WASI Preview 2 把系统能力拆成一组接口,并引入异步支持:

use wasip2::*;

#[tokio::main(flavor = "current_thread")]
async fn main() {
    // 标准输出
    println!("start");

    // 异步 HTTP 出站请求(wasi:http/outgoing-handler)
    let req = http::types::OutgoingRequest::new(http::types::Headers::new());
    req.set_method(&http::types::Method::Get).unwrap();
    req.set_scheme(Some(&http::types::Scheme::Https)).unwrap();
    req.set_authority(Some("api.github.com")).unwrap();
    req.set_path_with_query(Some("/")).unwrap();

    let fut = http::outgoing_handler::handle(req, None).unwrap();
    let resp = futures::executor::block_on(fut).unwrap();
    println!("status: {:?}", resp.status());

    // 单调时钟
    let now = clocks::monotonic_clock::now();
    println!("uptime ns: {now}");
}

这意味着 wasm 不再只能做纯计算,它可以安全地进行 IO、拥有文件系统沙箱、被授予精确的能力清单。

典型应用场景

  1. 插件系统:为主程序提供多语言插件能力,且有内存与系统调用沙箱(Figma、Shopify、Envoy 都在用)。
  2. 边缘计算:wasm 冷启动在毫秒级,镜像体积几百 KB,比容器更适合 Serverless。
  3. AI 推理与数据处理:把 Python/Rust 写的算子编译成组件,在任意宿主里复用。

生态现状与建议

# 运行时选型
wasmtime --version   # Bytecode Alliance,组件模型支持最完整
wasmtime serve calculator.wasm
  • 组件模型规范已接近稳定,WASI 0.2 在 Wasmtime 中默认可用;
  • 尚未进入浏览器原生支持(需 jco polyfill);
  • 建议先从“内部插件”这类可控场景切入,而不是直接替换核心服务。

小结

组件模型补齐了 WebAssembly 最后一块拼图——跨语言的高层接口。如果说过去的 wasm 是“能在任何地方跑的 CPU”,那么现在的 wasm 组件就是“能在任何地方调用的软件模块”。