Skip to main content
Aotokitsuruya
Aotokitsuruya
Senior Software Developer
Published at
This article is translated by AI, if have any corrections please let me know.
This article is part of Kobako series.

Having sorted out the basic conditions for running 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.

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 isn’t complicated in itself; we only need a few lines of code to put the corresponding design in place.

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

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.