---
title: "Kobako: Standard Output"
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","Experience","Ruby","WebAssembly","Gem","mruby"]
series: "kobako"
toc: true
aiTranslated: true
permalink: "https://blog.aotoki.me/en/posts/2026/09/16/kobako-standard-output/"
language: "en"
---


Having sorted out the [basic conditions for running](https://blog.aotoki.me/en/posts/2026/09/09/kobako-resource-limits/) mruby on WebAssembly, it's time to start thinking about more practical uses. At the very least, a script should be able to run and print a message like `Hello World`, but mruby itself wasn't designed with the WASI (WebAssembly System Interface) environment in mind, so many Gems simply can't be used.

<!--more-->

## mruby-io

In mruby's design, the core capability is parsing and running Ruby syntax. Unlike the CRuby we're familiar with, many basic or standard libraries don't exist, and that includes methods like `puts`.

Configuring things by hand to get close to CRuby every time would be inconvenient, so mruby ships a default Gem Box (a pre-selected set of Gems), which includes `mruby-io`, the standard library Gem that provides features like `IO` and `File`. Standard output (`stdout`) actually points to `/dev/fd/1`, which means that if I want to print a `Hello World` message, what I need to do is write the message to `fd 1` (file descriptor 1).

This is also why `mruby-io` defines the `print` and `puts` methods alongside `IO` and `File`, because for `puts`, the meaning is almost equivalent to `IO.new(1, "w+").write("Hello World\n")`.

## POSIX

The concept of WASI is similar to POSIX (Portable Operating System Interface), but the problem is that WASI Preview 1 doesn't follow POSIX conventions, while most Unix software is designed around POSIX, and mruby is no exception.

If we want to output a message, we'd use `mruby-io` to do it, but `mruby-io` depends on the POSIX standard under the hood, so it won't compile for a WebAssembly environment that uses WASI, which leaves us unable to use `mruby-io` to output messages.

But this isn't a bad thing. `mruby-io` lets users touch the file system, which is risky for a sandbox designed for AI to use. On top of that, WASI Preview 1's design requires defining who receives `stdout` at startup, and the directories where files can be opened also have to be opened up at startup (preopened). At least none of these limitations are something Kobako has to break through, so we might as well handle it simply ourselves, which is actually safer.

## MemoryOutputPipe

Writing to `stdout` in [Wasmtime](https://wasmtime.dev/) isn't complicated in itself; we only need a few lines of code to put the corresponding design in place.

```rust
let stdout = MemoryOutputPipe::new(limit + 1); // the extra 1 byte tells us whether the limit was exceeded
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");
    }
});
```

In Wasmtime, we use `MemoryOutputPipe` to allocate a block of memory for holding `stdout`, and have the running Wasmtime write `fd 1` to that location. On the Ruby side, we define the `Kernel.puts` method, and writing to `fd 1` as usual is enough for Wasmtime's `stdout` to capture it.

There are other options like `AsyncStdoutStream` or `OutputFile`, but for Kobako, keeping the output in memory makes the most sense, because Kobako is expected to be created and destroyed in large numbers, and mruby itself doesn't have much asynchronous design, so memory is the best fit.

At this stage, Kobako finally has the basic ability to start, run, and output, and we're gradually getting closer to a more complete and polished sandbox gem.

