iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0

前言:換一個載的人

昨天結語說「一顆檔案,誰載都一樣」,到昨天為止這句話只在瀏覽器裡驗過:載它的一直是同一行 instantiateStreaming。今天換一個載的人。把「跑 .wasm」這件事從瀏覽器抽出來、做成一支獨立程式,叫 runtime,名字有 Wasmtime、WasmEdge、Wasmer、wazero、WAMR,用在伺服器、邊緣、IoT、App 內嵌——序裡三個時代的第二個,從今天開始。今天只用 Wasmtime 做三件事:把前幾天那兩顆檔在終端跑起來;把那顆會喊 console.log 的檔也丟過去試試;最後寫一支會開檔案的程式,看宿主不給資料夾會怎樣。

原始碼在 GitHub 的 wasm-lab:https://github.com/liren0907/wasm-lab/tree/main/ithome-30days/day08

第一步:同一顆檔,換宿主

第一顆那天的五行 lib.rs 跟 import 表那天的 counter.wat,原封搬進 day08/,同一條 cargo build,編出來的 add.wasm 還是 392 B,跟那天那顆用 cmp 比過,一個 byte 都一樣。wasmtime 照官網裝,其他工具前幾天都裝過了。不開瀏覽器,直接在終端叫它:

$ wasmtime --invoke add add.wasm 7 35
warning: using `--invoke` with a function that takes arguments is experimental and may break in the future
warning: using `--invoke` with a function that returns values is experimental and may break in the future
42
$ wasmtime --invoke inc counter.wasm
1
$ wasmtime --invoke inc counter.wasm
1

那兩行 warning 是 wasmtime 說 --invoke 帶參數還在實驗,每次都會印,下面略掉。42 跟瀏覽器裡那個 42 是同一支函式算的,載它的人換了,檔案沒換。inc 連叫兩次都是 1:每一次 --invoke 都從檔案重新建一個 instance,global 從 0 起算——import 表那天按鈕按出來的 1、2、1,在這裡是 1、1,同一件事。狀態活在 instance,instance 是宿主建的,宿主想建幾個就幾個。

第二步:帶著 import 換宿主

import 表那天那三行 console.log 的 import 加回來,編出來叫 add_log.wasmwasm-tools print 最上面還是那一行 (import "console" "log" …)。丟給 wasmtime:

$ wasmtime --invoke add add_log.wasm 7 35
Error: failed to run main module `add_log.wasm`

Caused by:
    0: failed to instantiate "add_log.wasm"
    1: unknown import: `console::log` has not been defined

建實例那一刻就炸,跟那天瀏覽器裡給 {} 那次是同一個位置:它要的東西列在 import 表上,少給一個就起不來。差別在瀏覽器裡你隨手寫一支 log 塞進去就好,wasmtime 這裡沒有那個 {} 可以填——它認得的 import 是它自己那一套,要自己給得把它當函式庫嵌進自己的程式,那是之後的事。這就是換宿主的代價:檔案沒變,可是「它要的東西」誰來給、用什麼名字給,每個宿主各長各的樣。要讓同一顆檔在瀏覽器外面也到處能跑,import 表上的名字得是一套大家都認的——這套名字叫 WASI。

第三步:WASI,資料夾沒給就打不開

換一個 target:rustup target add wasm32-wasip2,寫一支會開檔案的小程式,notes/src/main.rs 全部就這幾行:

// 讀一個檔、印出來。哪些路徑存在不是這支程式說了算:宿主一條一條給進來。
fn main() {
    match std::fs::read_to_string("/notes.txt") {
        Ok(text) => print!("{text}"),
        Err(e) => {
            eprintln!("cannot open /notes.txt: {e}");
            std::process::exit(1);
        }
    }
}

cargo build --release --target wasm32-wasip2,得到 notes.wasmnotes/data/notes.txt 放兩行字。跑三次:

$ wasmtime run notes.wasm
cannot open /notes.txt: No such file or directory (os error 44)
$ wasmtime run --dir /tmp::/ notes.wasm
cannot open /notes.txt: No such file or directory (os error 44)
$ wasmtime run --dir notes/data::/ notes.wasm
buy milk
read the WASI spec

第一次沒給資料夾,打不開;第二次把 /tmp 掛成它的 /,還是打不開,訊息一個字都沒變;第三次把 data/ 掛成它的 /,才讀得到。看清楚它說的是什麼:不是「權限不足」,是「沒有這個檔案」。檔案明明在硬碟上,可是宿主沒遞進去的資料夾,在沙箱裡根本不存在,連「有這個檔」這件事都不洩漏。擋你的不是你的程式,是 runtime:--dir 是宿主遞一支插頭進去,遞幾支、插在哪,全是宿主決定。

它要什麼,不用跑也看得到:

$ wasm-tools component wit notes.wasm
package root:component;

world root {
  import wasi:io/poll@0.2.6;
  import wasi:io/error@0.2.6;
  import wasi:io/streams@0.2.6;
  import wasi:cli/environment@0.2.6;
  import wasi:cli/exit@0.2.6;
  import wasi:cli/stdin@0.2.6;
  import wasi:cli/stdout@0.2.6;
  import wasi:cli/stderr@0.2.6;
  import wasi:cli/terminal-input@0.2.6;
  import wasi:cli/terminal-output@0.2.6;
  import wasi:cli/terminal-stdin@0.2.6;
  import wasi:cli/terminal-stdout@0.2.6;
  import wasi:cli/terminal-stderr@0.2.6;
  import wasi:clocks/wall-clock@0.2.6;
  import wasi:filesystem/types@0.2.6;
  import wasi:filesystem/preopens@0.2.6;

  export wasi:cli/run@0.2.0;
}

後面幾百行是這些介面的定義,略。這就是它的 import 表,只是名字換成一套大家都認的:wasi:iowasi:cliwasi:clockswasi:filesystem 四包;WASI 0.2 一共七包,它沒要的另外三包 httprandomsockets,就是它碰不到的東西——不用執行就知道這支程式不會碰網路。wasi:filesystem/preopens 裡只有一支函式 get-directories,回傳宿主遞進來的資料夾清單,剛才三次跑的差別就是這張清單是空的還是有一筆。這顆檔比 add.wasm 多了一層——wasm-tools print 開頭是 (component 不是 (module——那一層之後講。

結語

一句帶走:換宿主不換檔;作業系統的資源是宿主一項項給進來的,沒給那條線就不存在,擋你的不是你的程式,是 runtime。


上一篇
Day 07|前端整合
下一篇
Day 09|同一支程式,兩代 WASI
系列文
Tool Use Is All You Need:30 天用 WebAssembly 試圖控制 Agent 的手腳9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言