解決 mruby 在 WebAssembly 執行的基本條件後,是時候開始考慮能夠有更多實際的應用方式,至少要讓一個腳本可以執行,然後順利看到 Hello World 這樣的訊息,但是 mruby 本身的設計並沒有考慮 WASI(WebAssembly System Interface)環境,因此有很多 Gem 是無法使用的。
mruby-io
在 mruby 的設計中,核心能力是解析 Ruby 語法並且執行,跟我們熟悉的 CRuby 不同,很多基本或者標準的函式庫都不存在,這也包括像是 puts 這樣的方法。
如果每次都要手動設定到接近 CRuby 的狀態也很不方便,因此 mruby 預設提供一組 Gem Box(預先選好的 Gem 組合),裡面就包含 mruby-io 這個標準函式庫的 Gem 負責提供 IO、File 等功能,而標準輸出(Standard Output,或稱 stdout)實際上就是指向 /dev/fd/1 的位置,也就是說我想印出 Hello World 的訊息,要做的是對 fd 1(檔案描述符,File Descriptor)這個位置寫入訊息。
這也是為什麼 mruby-io 除了 IO 和 File 之外,還把 print、puts 方法一起定義,因為對 puts 來說,意思跟 IO.new(1, "w+").write("Hello World\n") 幾乎是等價的。
POSIX
WASI 的概念跟 POSIX(Portable Operating System Interface)是類似的,但問題在於 WASI Preview 1 並沒有符合 POSIX 的約定,而大多數 Unix 的軟體都會遵守 POSIX 來設計,對 mruby 來說也不意外。
如果我們想要輸出訊息,就會使用 mruby-io 來實現,但是 mruby-io 底層依賴了 POSIX 標準,對採用 WASI 的 WebAssembly 環境就變成無法編譯的狀態,也讓我們無法使用 mruby-io 來輸出訊息。
但這並不是一件壞事,因為 mruby-io 是允許使用者接觸檔案系統的,如果是要設計給 AI 使用的沙盒(Sandbox),風險比較高,而且 WASI Preview 1 的設計上必須在啟動時就先定義好 stdout 由誰承接,能開啟檔案的目錄也要在啟動時先開放(Preopen),至少這些限制都不是 Kobako 一定要突破的,倒不如自己做一個簡單的處理,反而更安全。
MemoryOutputPipe
Wasmtime 要輸出到 stdout 本身並不複雜,我們只需要幾行程式碼就可以完成相對應的設計。
1let stdout = MemoryOutputPipe::new(limit + 1); // 多 1 byte 用來判斷是否超出上限
2let wasi = WasiCtxBuilder::new().stdout(stdout.clone()).build_p1();
3p1::add_to_linker_sync(&mut linker, |state| &mut state.wasi);
4
5kernel.define_method(mrb, "puts", |mrb, args| {
6 for arg in args {
7 write(1, arg.to_s(mrb) + "\n");
8 }
9});在 Wasmtime 中,我們會利用 MemoryOutputPipe 在記憶體中配置一個用來保存 stdout 的區塊,讓目前正在執行的 Wasmtime 把 fd 1 寫入到那個位置,在 Ruby 中則定義 Kernel.puts 方法,照常寫入到 fd 1 就能被 Wasmtime 的 stdout 保存下來。
雖然還有其他像是 AsyncStdoutStream 或者 OutputFile 的選項,但是對 Kobako 來說,使用記憶體保存輸出是最合理的,因為 Kobako 本身就被預期大量產生、消滅,而且 mruby 本身也沒有太多非同步的設計,因此最適合的就是放到記憶體。
到這個階段,Kobako 才算是初步的具備啟動、執行、輸出的能力,我們正在逐步地接近一個更加完整、完善的沙盒套件。
喜歡這篇文章?請我喝杯奶茶 🧋