本节摘要:WASI(WebAssembly System Interface)回答"字节出了浏览器怎么活":把文件、时钟、随机数、网络这些系统能力抽象成显式授予的接口。本节走完一条从编译到运行的完整服务端会话,剖析能力导向安全模型的哲学,并理清预览版本之间的兼容性地形。
某数据团队要把一段 Rust 写的报表解析器部署到边缘节点上:代码要读配置文件、读环境变量、向标准输出写结果。放到 Wasm 的世界里,这三件事分别需要文件、环境、终端三类系统能力——而 MVP 规范对它们的回答是"没有这回事"。WASI 就是补上这一层的标准:它不模拟任何具体操作系统,而是定义一套能力接口,让运行时按需授予。
传统进程的安全模型是"身份加全局资源":进程以用户身份运行,能用该用户权限访问文件系统里的任何路径。WASI 的模型是"显式授予加最小可见":模块在实例化时拿到一张能力清单——目录句柄、时钟精度、随机源,清单之外的东西在模块的视野里根本不存在。
两套模型的差异用一个细节即可看清:WASI 的文件访问不提供"按路径打开任意文件"的自由——模块要先持有某个目录的句柄(宿主在实例化时预开放给它的),路径访问都发生在句柄之下。未被授予数据目录的模块,连那个目录的存在都无从得知,更谈不上试探。这把最小权限原则从"道德呼吁"变成了"结构事实":多授一份能力需要宿主显式动作,日志里留下痕迹。
能力模型还有一层工程红利常被忽视:测试与演练变得异常干净。给模块递一个指向临时目录的句柄,测试就天然隔离;递一个假时钟、确定性随机源,时间相关的逻辑也能做确定性回归——能力即注入点,这在第八章的工程实践里会再兑现。
把能力模型落到命令行。先写一个读文件、取环境变量、向标准输出写结果的 Rust 小工具:
use std::env; use std::fs; use std::io::{self, Write}; fn main() -> io::Result<()> { let path = env::var("REPORT_PATH").unwrap_or_else(|_| "report.txt".into()); let body = fs::read_to_string(&path)?; let stdout = io::stdout(); let mut out = stdout.lock(); writeln!(out, "parsed {} bytes from {}", body.len(), path)?; Ok(()) }
编译成 WASI 目标并运行——注意运行时如何显式授予能力:
$ rustup target add wasm32-wasip1 $ cargo build --target wasm32-wasip1 --release $ wasmtime run \ --env REPORT_PATH=report.txt \ # 授予环境变量 --dir .::/data \ # 把宿主当前目录挂为模块可见的 /data target/wasm32-wasip1/release/report.wasm parsed 4096 bytes from report.txt
这行命令就是能力模型的具象:--env 只递清单里点名的那几个变量,--dir 只挂点名的目录,运行时还能限制为只读。删掉任何一项,模块对应的系统调用会得到"能力不存在"的应答,而不是四处乱撞。顺带能看出 WASI 的接口习惯——标准输入输出与错误流是默认授予的基础能力,其余一切都要点名。

WASI 自身仍在快速演进,版本地形值得专门交底。第一代快照(wasi_snapshot_preview1,目标三元组里的 wasip1)以平铺函数为特征——fd_read、path_open 这类 C 风格接口,生态最成熟,服务端工具链全面支持,是当下的安全选择。第二代预览(wasi-preview2,三元组 wasip2)按组件模型的接口思想重构:能力被组织成命名空间化的接口包(文件系统、时钟、随机数、输入输出流各自成族),接口即组件,天生与第六章第五节的组件模型接轨。
两者的关系不是"新版替换旧版"而是"新地基接替旧地基":preview2 建立在组件模型之上,接口形态完全不同,旧模块要经适配层运行。工程上的务实策略由此而定:当下产线统一 wasip1,新项目尝试 wasip2 但锁定所用运行时的版本,跨版本迁移等组件模型生态定型后再排期——这与本章支柱页的分层采用策略完全一致。
网络是另一个需要点名的边界:套接字接口长期是最难标准化的部分(能力粒度、异步模型都无共识),各家运行时以私有扩展先行。需要网络能力的项目,要么用运行时私有接口并承担锁定,要么把网络留在宿主侧、模块只做纯计算——后者与本章主张的"事件与数据分流"架构天然吻合。
命令行工具是能力模型最低成本的练兵场:参数、标准流、退出码都是默认授予的基础能力,用它们先跑通"编译到运行"的全流程,再逐项追加文件与时钟,能力清单的增长轨迹一目了然:
fn main() { let args: Vec<String> = std::env::args().collect(); println!("收到 {} 个参数,首个是 {}", args.len() - 1, args.get(1).unwrap_or(&"-".into())); }
$ wasmtime run tool.wasm hello 收到 1 个参数,首个是 hello
从这段最小的会话出发,把 --env、--dir 一项项加回去,团队对"能力边界在哪里"的理解就从文档知识变成了手感。
系统能力有了标准答案,下一节看互操作的终极形态:组件模型如何让不同语言按契约拼箱。