Playground: add "How It Works" technical documentation page

Explains the architecture (xterm.js, JS shell, WASI shim, WASM binary),
command lifecycle, pipeline execution, WASM loading, command parsing,
and the Rust side (feat_wasm, multicall binary, WASI adaptations,
locale embedding). Also adds Weblate link for missing translations.
This commit is contained in:
Sylvestre Ledru
2026-04-04 11:58:21 +02:00
parent 17e38beade
commit ddd1a9237d
2 changed files with 259 additions and 0 deletions
+256
View File
@@ -0,0 +1,256 @@
+++
title = "Playground: How It Works"
template = "page.html"
+++
<script src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
<script>mermaid.initialize({ startOnLoad: true, theme: 'dark' });</script>
The [uutils playground](/playground) lets you run real Rust coreutils directly in your browser, with no server, no installation, and no network round-trips after the initial page load. This page explains the architecture behind it.
## High-Level Overview
<pre class="mermaid">
flowchart LR
A["Browser"] -->|"1. Load page"| B["Zola static site"]
B -->|"2. Fetch WASM binary"| C["uutils.wasm<br/>(~10 MB)"]
A -->|"3. User types command"| D["JavaScript shell"]
D -->|"4. Execute via WASI"| E["WebAssembly runtime"]
E -->|"5. Output"| A
</pre>
Everything runs **client-side**. Once the WASM binary is downloaded, the playground works entirely offline.
## Architecture
<pre class="mermaid">
flowchart TB
subgraph Browser
direction TB
UI["xterm.js terminal emulator"]
Shell["JavaScript shell layer<br/>(parseCommandLine, builtins)"]
WASI["WASI shim<br/>(@bjorn3/browser_wasi_shim)"]
WASM["uutils coreutils<br/>compiled to WebAssembly"]
VFS["Virtual filesystem<br/>(in-memory)"]
UI -->|"user input"| Shell
Shell -->|"argv + stdin"| WASI
WASI -->|"WASI syscalls"| WASM
WASM -->|"fd_write stdout/stderr"| WASI
WASI -->|"output text"| UI
WASM <-->|"fd_read / fd_write"| VFS
Shell <-->|"builtins: cd, help, clear, locale"| UI
end
</pre>
### Components
| Component | Role |
|-----------|------|
| **[xterm.js](https://xtermjs.org/)** | Terminal emulator rendered in the browser. Handles cursor, colors, input, scrollback. |
| **JavaScript shell** | Parses command lines, manages pipes, handles builtins (`help`, `clear`, `cd`, `locale`), and dispatches to WASM. |
| **[browser_wasi_shim](https://github.com/bjorn3/browser_wasi_shim)** | Implements the WASI (WebAssembly System Interface) in JavaScript so that uutils can perform I/O operations. |
| **uutils.wasm** | The actual Rust coreutils, compiled with the `feat_wasm` feature to a single WASM binary containing 60+ commands. |
| **Virtual filesystem** | An in-memory filesystem backed by WASI shim `PreopenDirectory`, pre-populated with sample files. Persists across commands within a session. |
## Lifecycle of a Command
Here's what happens when you type `sort fruits.txt | uniq -c | sort -rn` and press Enter:
<pre class="mermaid">
sequenceDiagram
participant User
participant Terminal as xterm.js
participant Shell as JS Shell
participant WASI as WASI Shim
participant WASM as uutils.wasm
participant VFS as Virtual FS
User->>Terminal: types "sort fruits.txt | uniq -c | sort -rn" + Enter
Terminal->>Shell: raw input string
Note over Shell: parseCommandLine() splits<br/>into 3 pipeline stages
Shell->>WASI: argv=["coreutils","sort","fruits.txt"], stdin=""
WASI->>WASM: instantiate + start()
WASM->>VFS: fd_read("fruits.txt")
VFS-->>WASM: file contents
WASM->>WASI: fd_write(stdout, sorted data)
WASI-->>Shell: stdout captured
Shell->>WASI: argv=["coreutils","uniq","-c"], stdin=sorted data
WASI->>WASM: instantiate + start()
WASM->>WASI: fd_write(stdout, counted data)
WASI-->>Shell: stdout captured
Shell->>WASI: argv=["coreutils","sort","-rn"], stdin=counted data
WASI->>WASM: instantiate + start()
WASM->>WASI: fd_write(stdout, final result)
WASI-->>Shell: stdout captured
Shell->>Terminal: display final output
Terminal->>User: rendered result
</pre>
Key details:
- **Pipeline execution**: each pipe stage is a fresh WASM instantiation. The stdout of one stage becomes the stdin of the next.
- **Command dispatch**: every command goes through `["coreutils", command, ...args]` — the WASM binary is a multicall binary, similar to BusyBox.
- **Path resolution**: relative paths are resolved against a virtual `cwd` maintained by the JS shell.
## WASM Loading & Initialization
<pre class="mermaid">
flowchart TB
A["Page load"] --> B{"SharedArrayBuffer<br/>available?"}
B -->|No| C["Install polyfill stub"]
B -->|Yes| D["Continue"]
C --> D
D --> E["Load xterm.js + fit addon<br/>(from CDN with SRI)"]
E --> F["Render terminal + banner"]
F --> G["Parallel fetch"]
G --> H["browser_wasi_shim<br/>(dynamic import)"]
G --> I["uutils.wasm<br/>(compileStreaming)"]
H --> J["Ready"]
I --> J
J --> K{"?cmd= parameter?"}
K -->|Yes| L["Auto-run commands"]
K -->|No| M["Show prompt"]
</pre>
- The WASM binary is compiled with `WebAssembly.compileStreaming()` for best performance, with a fallback to `arrayBuffer()` if the server doesn't set `application/wasm` content-type.
- While WASM loads, a JavaScript fallback provides basic implementations of `echo`, `cat`, `seq`, `factor`, `wc`, `ls`, and `basename`.
- The `SharedArrayBuffer` polyfill stub prevents `ReferenceError` in browsers without cross-origin isolation headers.
## Command Parsing & Pipes
The shell implements a simple but functional parser:
<pre class="mermaid">
flowchart LR
Input["sort fruits.txt | uniq -c"] --> Tokenizer
Tokenizer --> |"handles quotes, escapes,<br/>pipes, redirections"| Pipeline
subgraph Pipeline
direction LR
S1["Stage 1<br/>['sort', 'fruits.txt']"]
S2["Stage 2<br/>['uniq', '-c']"]
S1 --> |stdout → stdin| S2
end
</pre>
Supported shell features:
- **Pipes** (`|`) — chain commands together
- **Redirections** (`>`, `>>`, `<`) — write output to files, append, or read input from files
- **Single quotes** (`'...'`) — literal strings, no escaping
- **Double quotes** (`"..."`) — literal strings with backslash escaping
- **Backslash escaping** (`\|`, `\ `) — escape special characters
- **Tab completion** — commands and filenames
- **Keyboard shortcuts** — Ctrl+C (cancel), Ctrl+L (clear), Ctrl+U (clear line), arrows (history/cursor)
**Not** supported (by design, to keep it simple): variables (`$VAR`), subshells, `&&`/`||`, globbing.
## The Rust Side: Building Coreutils for WebAssembly
### Compilation Target
The uutils coreutils are compiled to **`wasm32-wasip1`** (WebAssembly System Interface Preview 1) using the standard Rust toolchain:
```bash
cargo build --target wasm32-wasip1 --features feat_wasm
```
This produces a single `uutils.wasm` binary — a **multicall binary** similar to BusyBox, where all 60+ utilities are bundled into one executable.
### The `feat_wasm` Feature Gate
Not every coreutil can run in a WASM sandbox. The [`feat_wasm` feature](https://github.com/uutils/coreutils/blob/main/Cargo.toml) in `Cargo.toml` defines the curated set of utilities that are compatible with WASI:
<pre class="mermaid">
flowchart LR
subgraph "feat_wasm (included)"
direction LR
A["Text: cat, head, tail,<br/>sort, uniq, cut, tr, wc, fmt"]
B["Files: cp, mv, rm, mkdir,<br/>touch, link, ln, ls"]
C["Math: seq, factor, shuf,<br/>numfmt, expr"]
D["Checksum: md5sum, sha*sum,<br/>b2sum, cksum"]
E["Encoding: base32, base64,<br/>basenc"]
F["Other: date, uname, arch,<br/>nproc, sleep, echo, printf"]
end
subgraph "Excluded from WASM"
direction LR
X["dd, df, du, env, mktemp,<br/>more, tac, test, stty,<br/>chcon, runcon, chown, kill..."]
end
</pre>
Utilities are excluded when they depend on OS-level syscalls not available in WASI — for example, `df` needs filesystem stats, `du` needs directory traversal with metadata, and `chown`/`chcon` need permission and SELinux APIs.
### Multicall Binary: How Command Dispatch Works
<pre class="mermaid">
flowchart TB
A["WASI runtime calls _start()"] --> B["main() in coreutils.rs"]
B --> C["Read argv[0] = 'coreutils'"]
C --> D["Read argv[1] = utility name<br/>e.g. 'sort'"]
D --> E["Look up in util_map()"]
E --> F["Call sort::uumain(args)"]
F --> G["Return exit code"]
</pre>
At **build time**, `build.rs` scans all enabled Cargo features and generates a `uutils_map.rs` file containing a [PHF](https://crates.io/crates/phf) (perfect hash function) map:
```rust
// Auto-generated at build time
type UtilityMap<T> = phf::OrderedMap<
&'static str,
(fn(T) -> i32, fn() -> Command)
>;
```
Each entry maps a utility name (e.g. `"sort"`) to a pair of functions:
- **`uumain`** — the utility's entry point, taking argument iterators and returning an exit code
- **`uu_app`** — returns the `clap::Command` definition for argument parsing and help
At **runtime**, the multicall binary reads `argv` to determine which utility to invoke. In the browser, the JavaScript shell calls the WASM binary as `["coreutils", "sort", "-rn"]`, so `argv[1]` becomes the dispatch key.
### WASI Platform Adaptations
Individual utilities use conditional compilation to handle WASI's limitations:
```rust
// In cp: symlinks are not supported on WASI
#[cfg(target_os = "wasi")]
return Err(Error::Unsupported);
// In tail: no inotify/kqueue file watching
#[cfg(target_os = "wasi")]
fn follow() { /* no-op stub */ }
// In ls: hostname crate excluded for WASI
#[cfg(not(target_os = "wasi"))]
use hostname::get;
```
These stubs mean the utilities gracefully degrade rather than crash — `tail -f` simply won't follow, `cp` won't create symlinks, and `ls` won't show hostname information.
### Localization: Embedding All Translations
A key difference for the WASM build is how locale files are handled:
<pre class="mermaid">
flowchart TB
subgraph "Normal build (Linux, macOS...)"
N1["build.rs detects user's LANG"]
N1 --> N2["Embed only matching .ftl file<br/>+ English fallback"]
end
subgraph "WASI build"
W1["build.rs detects target_os = wasi"]
W1 --> W2["Embed ALL .ftl locale files<br/>(30+ languages)"]
W2 --> W3["Runtime locale switching<br/>via LANG env variable"]
end
</pre>
On native platforms, `uucore`'s build script embeds only the [Fluent](https://projectfluent.org/) (`.ftl`) translation files matching the user's `LANG` environment variable, to keep the binary small. For WASI builds, **all locale files are embedded**, because the target locale isn't known at compile time — the playground user can switch languages at runtime via the locale dropdown or the `locale` command.
+3
View File
@@ -10,6 +10,7 @@ template = "page.html"
<select id="locale-select" onchange="setLocale(this.value)">
<option value="en-US">English (en-US)</option>
</select>
<span class="locale-help">Missing a translation or your language? <a href="https://hosted.weblate.org/projects/rust-coreutils/">Help translate on Weblate</a></span>
</div>
<div id="wasm-playground"></div>
@@ -110,3 +111,5 @@ Some commands (ex: `chcon`, `runcon`, etc) are not yet available in the WASM bui
depend on platform-specific syscalls not fully supported by WebAssembly/WASI yet.<br />
We are actively working on adding more commands as we improve WASI compatibility in uutils coreutils.
Curious about how this all works under the hood? Read the [technical deep-dive on the playground architecture](/playground-how-it-works).