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 1 | Preview 2 |
|---|---|---|
| 接口描述 | 固定接口 | WIT (WebAssembly Interface Types) |
| 文件系统 | wasi_snapshot_preview1 | wasi: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 1 | Preview 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 的能力。