强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

WebAssembly 入门教程 / 09 - WASI 系统接口

09 - WASI 系统接口

WASI 赋予了 WebAssembly “离开浏览器” 的能力,同时保持沙箱安全性。


9.1 WASI 概述

WASI(WebAssembly System Interface)是一组标准化的系统调用接口,使得 Wasm 模块可以在浏览器之外(服务器、CLI、边缘节点等)安全运行。

WASI 设计原则

原则说明
Capability-based Security基于能力的安全模型,而非传统的权限模型
可移植同一二进制可在任何支持 WASI 的运行时运行
可组合模块可以通过 Component Model 组合
最小权限默认无权限,需要显式授予

WASI vs POSIX

POSIX 模型:                    WASI 模型:
┌─────────────┐                ┌─────────────┐
│  应用程序    │                │  Wasm 模块   │
├─────────────┤                ├─────────────┤
│ 系统调用     │                │ WASI 接口    │
│ (syscall)    │                │ (fd, args,   │
│              │                │  clock, etc) │
├─────────────┤                ├─────────────┤
│ 操作系统     │                │ 宿主运行时    │
│              │                │ (沙箱)       │
│ 全部权限     │                │ 受控权限      │
└─────────────┘                └─────────────┘

WASI Preview 1 vs Preview 2

特性Preview 1Preview 2
接口描述固定接口WIT (WebAssembly Interface Types)
文件系统wasi_snapshot_preview1wasi:filesystem
网络有限wasi:sockets, wasi:http
时钟wasi_snapshot_preview1.clock_*wasi:clocks
随机数wasi_snapshot_preview1.random_*wasi:random
组件模型不支持支持
跨语言组合有限完整支持

9.2 文件系统操作

C + WASI SDK 示例

// file_ops.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>

int main(int argc, char *argv[]) {
    // 读取文件
    FILE *fp = fopen("/input/data.txt", "r");
    if (!fp) {
        fprintf(stderr, "Cannot open file\n");
        return 1;
    }
    
    char buffer[1024];
    size_t bytes_read = fread(buffer, 1, sizeof(buffer) - 1, fp);
    buffer[bytes_read] = '\0';
    fclose(fp);
    
    printf("Read %zu bytes: %s\n", bytes_read, buffer);
    
    // 写入文件
    fp = fopen("/output/result.txt", "w");
    if (fp) {
        fprintf(fp, "Processed: %s\n", buffer);
        fclose(fp);
    }
    
    // 获取文件信息
    struct stat st;
    if (stat("/input/data.txt", &st) == 0) {
        printf("File size: %ld bytes\n", st.st_size);
    }
    
    return 0;
}
# 编译
${WASI_SDK_PATH}/bin/clang --target=wasm32-wasi \
  --sysroot=${WASI_SDK_PATH}/share/wasi-sysroot \
  -o file_ops.wasm file_ops.c

# 运行(授予目录权限)
wasmtime run \
  --dir /input::./input \
  --dir /output::./output \
  file_ops.wasm

Rust + WASI 示例

use std::fs;
use std::io::{self, Read, Write, BufRead, BufReader};

fn main() -> io::Result<()> {
    // 读取整个文件到字符串
    let content = fs::read_to_string("/input/config.toml")?;
    println!("Config: {}", content);
    
    // 逐行读取
    let file = fs::File::open("/input/data.csv")?;
    let reader = BufReader::new(file);
    for (i, line) in reader.lines().enumerate() {
        println!("Line {}: {}", i, line?);
    }
    
    // 写入文件
    let mut output = fs::File::create("/output/report.txt")?;
    writeln!(output, "Report generated at ...")?;
    
    // 目录操作
    fs::create_dir_all("/output/reports")?;
    
    // 列出目录
    for entry in fs::read_dir("/input")? {
        let entry = entry?;
        println!("{}: {} bytes",
            entry.file_name().to_string_lossy(),
            entry.metadata()?.len()
        );
    }
    
    // 文件元数据
    let metadata = fs::metadata("/input/data.csv")?;
    println!("Size: {}, Modified: {:?}",
        metadata.len(),
        metadata.modified()
    );
    
    Ok(())
}

文件描述符映射

宿主文件系统                    Wasm 虚拟文件系统
┌──────────────────┐           ┌──────────────────┐
│ /home/user/      │           │                  │
│   project/       │           │                  │
│     input/       │ ──映射──► │ /input/          │
│       data.txt   │           │   data.txt       │
│     output/      │ ──映射──► │ /output/         │
│                  │           │                  │
└──────────────────┘           └──────────────────┘

# Wasm 模块只能访问被显式映射的目录
# 即使代码中写了 "/etc/passwd",也无法访问

9.3 环境变量

C 示例

#include <stdio.h>
#include <stdlib.h>

int main() {
    // 读取环境变量
    const char *home = getenv("HOME");
    const char *path = getenv("PATH");
    const char *custom = getenv("MY_VAR");
    
    printf("HOME: %s\n", home ? home : "(null)");
    printf("PATH: %s\n", path ? path : "(null)");
    printf("MY_VAR: %s\n", custom ? custom : "(null)");
    
    return 0;
}
# 传入环境变量
wasmtime run --env MY_VAR=hello --env DEBUG=1 env_demo.wasm

Rust 示例

use std::env;

fn main() {
    // 读取单个环境变量
    match env::var("DATABASE_URL") {
        Ok(url) => println!("DB: {}", url),
        Err(_) => println!("DATABASE_URL not set"),
    }
    
    // 遍历所有环境变量
    for (key, value) in env::vars() {
        println!("{}={}", key, value);
    }
    
    // 命令行参数
    let args: Vec<String> = env::args().collect();
    println!("Args: {:?}", args);
}
# 传入环境变量和参数
wasmtime run \
  --env DATABASE_URL=postgres://localhost/mydb \
  --env RUST_LOG=debug \
  my_app.wasm -- --input data.csv --output result.json

9.4 命令行参数

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Program: %s\n", argv[0]);
    printf("Arguments (%d):\n", argc - 1);
    
    for (int i = 1; i < argc; i++) {
        printf("  [%d]: %s\n", i, argv[i]);
    }
    
    return 0;
}
wasmtime run args_demo.wasm -- hello world --verbose
# 输出:
# Program: args_demo.wasm
# Arguments (3):
#   [1]: hello
#   [2]: world
#   [3]: --verbose

9.5 时钟(Clocks)

实时时钟

use std::time::{SystemTime, UNIX_EPOCH, Instant, Duration};

fn main() {
    // 系统时间
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap();
    println!("Timestamp: {}.{:09}s", now.as_secs(), now.subsec_nanos());
    
    // 单调时钟(用于测量间隔)
    let start = Instant::now();
    
    // ... 一些操作 ...
    std::thread::sleep(Duration::from_millis(100));
    
    let elapsed = start.elapsed();
    println!("Elapsed: {:?}", elapsed);
}

WASI 时钟接口

WASI Preview 1 时钟接口:
├── clock_time_get(clock_id, precision) → timestamp
│   ├── clock_id = 0: REALTIME
│   └── clock_id = 1: MONOTONIC
│
└── 返回值: nanoseconds since epoch (REALTIME)
             or nanoseconds since arbitrary point (MONOTONIC)

9.6 随机数

use std::io;

fn main() {
    // 方法 1:标准库
    let random_bytes: Vec<u8> = (0..32).map(|_| {
        // WASI 会提供安全的随机源
        let mut buf = [0u8; 1];
        getrandom::getrandom(&mut buf).unwrap();
        buf[0]
    }).collect();
    
    println!("Random bytes: {:?}", random_bytes);
}

// 使用 getrandom crate(自动利用 WASI)
// Cargo.toml:
// [dependencies]
// getrandom = { version = "0.2", features = ["js"] }

C 示例

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

// WASI 提供了 arc4random
uint32_t get_random_u32() {
    return arc4random();
}

int main() {
    // 生成随机数
    for (int i = 0; i < 5; i++) {
        printf("Random: %u\n", get_random_u32());
    }
    return 0;
}

9.7 权限沙箱

Capability-based Security

# 只读挂载输入目录
wasmtime run --dir /input::./data:readonly app.wasm

# 可读写挂载输出目录
wasmtime run --dir /output::./results app.wasm

# 授予特定环境变量
wasmtime run --env API_KEY=xxx --env TIMEOUT=30 app.wasm

# 不授予任何文件系统访问
wasmtime run isolated.wasm

权限配置示例

# 完整的权限控制示例
wasmtime run \
  --dir /data::./data:readonly \
  --dir /output::./output \
  --env NODE_ENV=production \
  --env LOG_LEVEL=info \
  --wasi threads \
  app.wasm -- --config /data/config.json --output /output/result

预览 2 的细粒度权限

# Wasmtime 配置文件 (wasmtime.toml)
[[component]]
name = "my-app"

[component.resources]
filesystem = [
  { path = "./data", readonly = true },
  { path = "./output", readonly = false }
]
environment = ["NODE_ENV", "LOG_LEVEL"]
clocks = ["realtime", "monotonic"]
random = ["secure"]
sockets = ["tcp", "udp"]

9.8 网络(Preview 1 有限支持)

// ⚠️ WASI Preview 1 不原生支持网络
// 需要使用 Preview 2 或特定运行时扩展

// Preview 2 — HTTP 请求示例
use wasi::http::outgoing_handler;
use wasi::http::types::*;

fn make_request() -> Result<(), Box<dyn std::error::Error>> {
    let request = OutgoingRequest::new(Headers::new());
    request.set_scheme(Some(&Scheme::Https))?;
    request.set_authority(Some(&Authority::new("api.example.com")?))?;
    request.set_path_with_query(Some("/data"))?;
    
    let response = outgoing_handler::handle(request, None)?;
    // ... handle response
    Ok(())
}
// 使用 reqwest + wasi 兼容版本
// Cargo.toml:
// [dependencies]
// reqwest = { version = "0.11", features = ["blocking"] }

use reqwest;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let resp = reqwest::blocking::get("https://httpbin.org/get")?;
    println!("Status: {}", resp.status());
    println!("Body: {}", resp.text()?);
    Ok(())
}

9.9 WASI 运行时对比

运行时Preview 1Preview 2性能特性
Wasmtime⭐⭐⭐⭐Component Model, 完整 WASI
Wasmer部分⭐⭐⭐⭐多后端(Cranelift/LLVM/Singlepass)
WasmEdge⭐⭐⭐⭐⭐AI 推理、TensorFlow 集成
Wazero部分⭐⭐⭐纯 Go 实现,零依赖
wasm3部分⭐⭐⭐解释器,极小体积,适合 IoT

9.10 WIT(WebAssembly Interface Types)

Preview 2 使用 WIT 文件描述接口:

// world.wit
package example:greeter;

interface greet {
    greet: func(name: string) -> string;
    get-time: func() -> string;
}

world greeter-world {
    export greet;
    import wasi:filesystem/types;
    import wasi:clocks/wall-clock;
}
// Rust 实现 WIT 导出
wit_bindgen::generate!({
    world: "greeter-world",
});

struct Greeter;

impl Guest for Greeter {
    fn greet(name: String) -> String {
        format!("Hello, {}!", name)
    }
    
    fn get_time() -> String {
        // 使用导入的 WASI 时钟
        let now = wasi::clocks::wall_clock::now();
        format!("{}:{}", now.seconds, now.nanoseconds)
    }
}

export!(Greeter);

9.11 注意事项

⚠️ 路径限制:Wasm 模块无法访问被映射目录之外的路径(capability-based security)。../ 遍历也不会逃出沙箱。

⚠️ 符号链接:WASI Preview 1 对符号链接的支持有限。follow_symlinks 标志控制是否跟踪符号链接。

⚠️ 文件锁定:WASI Preview 1 不提供文件锁定机制。并发写入需要应用层实现协调。

⚠️ 网络能力:Preview 1 不包含网络接口。需要网络功能应使用 Preview 2 或特定运行时扩展。


9.12 扩展阅读


下一章10 - 浏览器应用 — 在浏览器中充分利用 WebAssembly 的能力。