---
title: "Kobako：標準輸出"
date: 2026-09-16T00:00:00+08:00
publishDate: 2026-09-16T00:00:00+08:00
lastmod: 2026-09-14T21:53:28+08:00
tags: ["LLM","AI","經驗","Ruby","WebAssembly","Gem","mruby"]
series: "kobako"
toc: true
permalink: "https://blog.aotoki.me/posts/2026/09/16/kobako-standard-output/"
language: "zh-tw"
---


解決 mruby 在 WebAssembly [執行的基本條件](https://blog.aotoki.me/posts/2026/09/09/kobako-resource-limits/)後，是時候開始考慮能夠有更多實際的應用方式，至少要讓一個腳本可以執行，然後順利看到 `Hello World` 這樣的訊息，但是 mruby 本身的設計並沒有考慮 WASI（WebAssembly System Interface）環境，因此有很多 Gem 是無法使用的。

<!--more-->

## 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](https://wasmtime.dev/) 要輸出到 `stdout` 本身並不複雜，我們只需要幾行程式碼就可以完成相對應的設計。

```rust
let stdout = MemoryOutputPipe::new(limit + 1); // 多 1 byte 用來判斷是否超出上限
let wasi = WasiCtxBuilder::new().stdout(stdout.clone()).build_p1();
p1::add_to_linker_sync(&mut linker, |state| &mut state.wasi);

kernel.define_method(mrb, "puts", |mrb, args| {
    for arg in args {
        write(1, arg.to_s(mrb) + "\n");
    }
});
```

在 Wasmtime 中，我們會利用 `MemoryOutputPipe` 在記憶體中配置一個用來保存 `stdout` 的區塊，讓目前正在執行的 Wasmtime 把 `fd 1` 寫入到那個位置，在 Ruby 中則定義 `Kernel.puts` 方法，照常寫入到 `fd 1` 就能被 Wasmtime 的 `stdout` 保存下來。

雖然還有其他像是 `AsyncStdoutStream` 或者 `OutputFile` 的選項，但是對 Kobako 來說，使用記憶體保存輸出是最合理的，因為 Kobako 本身就被預期大量產生、消滅，而且 mruby 本身也沒有太多非同步的設計，因此最適合的就是放到記憶體。

到這個階段，Kobako 才算是初步的具備啟動、執行、輸出的能力，我們正在逐步地接近一個更加完整、完善的沙盒套件。
