1. 缘起:我怀念 java -jar 的那种分发体验
我一直喜欢 Java 那种分发体验:java -jar app.jar 参数...,编译一次,把 jar 丢到任何装了 JRE 的机器上就能跑,不用管操作系统和 CPU 架构。但我不想碰 JVM 底层的 C++,想在 2026 年用 Rust 重造这种感觉。
转机出现在 WebAssembly 的 WASI 上。我在 Mac(arm64)上装了 wasmtime、wabt、wasm-tools 三个工具,补上 Rust 的 wasm32-wasip1 编译目标,然后发现:wasmtime run app.wasm 参数... 就是 java -jar app.jar 参数... 在 wasm 世界的对应物------前置命令加字节码文件加参数,仪式感一模一样。
这篇文章是给自己回顾用的,记录我把 JVM 的 CLI 体验整体迁移到 wasmtime 的过程、踩过的认知误区,以及实测验证。
2. 谬误溯源:动手前的四个错误认知
动手前我有几个错误认知,都来自把浏览器的 wasm 限制当成了 wasm 本身的限制。
2.1 以为 wasm 只能在浏览器里跑
错。WASI 让 wasm 在终端和服务器上原生运行,wasmtime 就是干这个的参考实现,整个栈用 Rust 写的。
2.2 以为 wasm 要像 Rust、Go 那样按平台分别编译
错。wasm 字节码平台无关,Mac 上编出来的 .wasm 直接丢 Linux x86_64、Windows 上跑,和 jar 一样。
2.3 以为 wasm 需要 node、python 那种环境,装一堆依赖
错。依赖在编译期就静态打包进单个 .wasm,运行时只需要一个 wasmtime,像 JRE 一样装一次、共享所有程序。
2.4 以为 wasm 没有网络能力
半错。WASI preview1 确实没网络,但 preview2 已经引入 wasi:sockets 和 wasi:http,服务器场景已可用。错误点在于把宿主能给什么当成了 wasm 能干什么。wasm 本身是一台纯计算虚拟机,手脚全靠宿主注入------这恰恰是它可以像 JVM 一样分层的根本原因。
3. 源码验证:四步走通全链路
理论说再多不如跑一遍。下面用一段真实代码,从装工具链到带参数运行,全程实测走通 wasmtime 的 CLI 分发链路。
3.1 第一步:安装工具链
Homebrew 一条命令装齐三个工具,再补上 Rust 的 wasm32-wasip1 编译目标:
bash
brew install wasmtime wabt wasm-tools
rustup target add wasm32-wasip1
3.2 第二步:写一个吃参数的 CLI 程序
这个程序读标准 argv 分发子命令,支持 hello、add、version 三个命令,未知命令走错误分支:
rust
use std::env;
use std::process::exit;
fn main() {
let args: Vec<String> = env::args().collect();
match args.get(1).map(|s| s.as_str()).unwrap_or("help") {
"hello" => println!("Hello, {}!", args.get(2).map(String::as_str).unwrap_or("world")),
"add" => { let a: i32 = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
let b: i32 = args.get(3).and_then(|s| s.parse().ok()).unwrap_or(0);
println!("{} + {} = {}", a, b, a + b); }
"version"=> println!("cli-demo 0.1.0 (wasm32-wasip1)"),
_ => { eprintln!("error: unknown command"); exit(1); }
}
}
边界条件:add 缺参数时用 unwrap_or(0) 兜底,hello 缺名字默认 world。
3.3 第三步:编译到 wasm
编译到 wasm32-wasip1 目标,产物是单个 .wasm 文件:
bash
cargo build --release --target wasm32-wasip1
3.4 第四步:带参数运行
编译完成后,用 wasmtime 直接跑,参数照常传:
bash
wasmtime run cli-demo.wasm --help
wasmtime run cli-demo.wasm hello Alice # Hello, Alice!
wasmtime run cli-demo.wasm add 10 32 # 10 + 32 = 42
wasmtime run cli-demo.wasm 999 # exit 1
4. 三个实测要点
4.1 退出码传播
guest 里 exit(1),wasmtime 进程也返回 1,宿主 shell 能看到 $?,CLI 才能进脚本流程。
4.2 依赖打包
带 serde_json、chrono 的程序编译后单文件仅 129KB,Cargo 锁了 44 个 crate,全在文件里。
4.3 编译快
无第三方依赖的 CLI 编译到 wasm 只要 0.37 秒。
5. 落地结论:把 Java 世界观映射到 wasm 世界观
把 Java 的 CLI 体验整体搬到 wasm 世界,一张表就能说清对应关系:
| Java 世界 | wasm 世界 |
|---|---|
java -jar app.jar 参数... |
wasmtime run app.wasm 参数... |
.class(单个编译单元) |
.wasm 模块 |
.jar(打包加入口声明) |
component 组件模型,可内嵌多模块 |
| jar 的 Main-Class 入口 | wasm 的 _start 导出或 wasi:cli/run 接口 |
| classpath 挂多个 jar | --preload 预加载依赖模块 |
JVM 启动参数(-Xmx 等) |
wasmtime 的 -O、-C、-W、-S 配置组 |
适用范围:需要一次编译到处分发的 CLI 工具、跨平台小工具、服务端程序。
不适用:高并发网络服务------wasm 的线程模型还在演进,远不如 JVM 虚拟线程成熟。
设计启示:如果你也想做类 JVM,用 Rust 写引擎内核是顺路的选择。wasmtime 整个栈就是 Rust 写的(wasmtime、Cranelift、wasm-tools),既有编译期内存安全,又是性能天花板。
6. 速查手册
6.1 人类开发者速查
记住一个前缀就行:wasmtime run。把 app.wasm 当作 app.jar,参数照传。自查命令:
bash
wasmtime run app.wasm --help
wasmtime --version
wasm-tools print app.wasm
wasmtime --version 看运行时版本,wasm-tools print app.wasm 反汇编看指令。
6.2 对话式 AI 速查
一句话问法:wasm 里怎么实现类似 java -jar 的 CLI?回答要点:wasmtime run 模块.wasm 参数;WASI argv 即参数;依赖编译期打包;运行时只需一个 wasmtime。
6.3 代码 Agent 速查
可复制执行:
bash
brew install wasmtime wabt wasm-tools
rustup target add wasm32-wasip1
cargo build --release --target wasm32-wasip1
wasmtime run target/wasm32-wasip1/release/app.wasm 参数
校验:正常跑完 echo $? 应为 0,非法参数应返回非零退出码。