The Node smoke test expected shaper info logs after the default logger
became errors-only. Its failure also exposed config finalization calling
an unsupported passwd lookup on wasm and returning Unexpected before
normalization completed.
Skip shell defaults on wasm, matching upstream Zig. Assert successful
config finalization and window-width clamping in the Node test. Check
shaper output directly while verifying that info logs stay suppressed.
Document these behaviors in the wasm port notes.
Test Plan:
- cargo test -p ghostty-wasm --test node (passed)
- cargo test -p ghostty-config --lib (291 passed)
- cargo clippy --target wasm32-unknown-unknown -p ghostty-wasm -- -D warnings
- cargo clippy; cargo clippy --benches; cargo clippy --tests
- cargo +nightly fmt, followed by the native Clippy checks again
- git diff --check
Related-commit: 4fb9faa667
125 lines
7.7 KiB
Markdown
125 lines
7.7 KiB
Markdown
# WASM port (W1)
|
|
|
|
Port of Ghostty's wasm-specific Zig. Everything is behind
|
|
`cfg(target_arch = "wasm32")` (or target-gated Cargo deps); native behaviour is
|
|
unchanged.
|
|
|
|
## File map
|
|
| Zig | Rust |
|
|
|---|---|
|
|
| `main_wasm.zig` | `crates/ghostty-wasm` (new, `cdylib`; empty on native) |
|
|
| `config/Wasm.zig` | `ghostty-wasm/src/config.rs` |
|
|
| `os/wasm.zig`, `os/wasm/target.zig`, `os/wasm/log.zig` | `ghostty-os/src/wasm/{mod,target,log}.rs` |
|
|
| zig-js (`js.*` used by Atlas/web_canvas) | `ghostty-os/src/wasm/js.rs` + vendored zig-js host `ghostty-wasm/js/zig-js/` |
|
|
| `font/face/web_canvas.zig` | `ghostty-font/src/face/web_canvas.rs` (+ exports in `ghostty-wasm/src/face.rs`) |
|
|
| `font/shaper/web_canvas.zig` | `ghostty-font/src/shaper/web_canvas.rs` (+ `ghostty-wasm/src/shaper.rs`) |
|
|
| `font/DeferredFace.zig` (`wc` variant, `Wasm`) | `ghostty-font/src/deferred_face_web_canvas.rs` |
|
|
| `font/Atlas.zig` `Wasm` | `ghostty-wasm/src/atlas.rs` |
|
|
| `font/backend.zig` | `ghostty-font/src/backend.rs` (`OPTIONS`/`default_for_target` = `WebCanvas` on wasm32) |
|
|
| `renderer/WebGL.zig` (empty stub), `App.zig` `Wasm` (commented out upstream) | nothing to export; `ghostty-wasm/src/app.rs` documents it |
|
|
| `os/passwd.zig` (`@compileError` on wasm) | `ghostty-os/src/unsupported.rs` shims |
|
|
|
|
## Build gating
|
|
- `ghostty-os`: nix/rustix/zbus/gettext target-gated; POSIX modules (`env`,
|
|
`file`, `flatpak`, ...) are not compiled on wasm32. `unsupported.rs` gives
|
|
API-compatible "unavailable" stand-ins (`xdg`, `passwd`, `expand_home`,
|
|
`resources_dir`, `is_flatpak`) so `ghostty-config` compiles unchanged
|
|
(Zig never analyzes them on wasm). `unsafe` is `deny` (not `forbid`) for the
|
|
wasm import blocks only.
|
|
- `ghostty-config`: only `std::os::unix` bits changed (file-type names,
|
|
`OsStrExt`).
|
|
- `ghostty-terminal`: `onig`, `ghostty-onig`, `rustix` target-gated. Like Zig
|
|
with `oniguruma = false`, `StringMap::search_iterator`/`SearchIterator` do
|
|
not exist on wasm (match types stay). Kitty shared-memory transmission
|
|
returns `InvalidData` on wasm; `OsStrExt` shim for the file medium.
|
|
- `ghostty-font`: freetype/harfbuzz/fontconfig `-sys` target-gated; on wasm the
|
|
`collection`, `codepoint_resolver`, `shared_grid(_set)`, `library`,
|
|
`discovery`, `fontconfig`, `embedded`, `face::freetype`, harfbuzz shaper
|
|
and `shaper::{run,cache,noop}` modules are compiled out.
|
|
- Builds: `cargo build --target wasm32-unknown-unknown -p ghostty-font -p ghostty-config -p ghostty-terminal -p ghostty-vt -p ghostty-wasm` passes (debug + release); clippy `-D warnings` clean on both targets.
|
|
|
|
## JS protocol (zig-js, W1b)
|
|
The wasm module now speaks **exactly zig-js's ABI** (mitchellh/zig-js @
|
|
04db83c): import module `"zig-js"` with `valueGet/valueSet/valueObjectCreate/
|
|
valueStringCreate/valueStringLen/valueStringCopy/valueDeinit/valueNew/
|
|
funcApply`, 64-bit NaN-boxed refs (f64 for numbers; NaN head `0x7FF8_0000`,
|
|
bits 0-31 id, bits 32-34 type id; predefined ids nan/null/true/false/
|
|
undefined/global/runtime = 0..6) and the same get/set/apply/new/deinit
|
|
semantics. `ghostty-os/src/wasm/js.rs` is a Rust port of zig-js's Zig side:
|
|
`Ref`, `Type`, `Value` (low level), `Object` (`get::<T>`, `set`, `call::<T>`,
|
|
`new_instance`), `global()`, `runtime()`, `string()`, `Undefined`. `Object` has
|
|
`Drop` (= `deinit`); `Object::into_id` hands the ref id to the host (as
|
|
`atlas_debug_canvas` does). Result conversion mirrors `Object.convertValue`
|
|
(`Option<T>` for null/undefined, `()` = void, `Object/Value` retain,
|
|
others release). Deviation: a raw `Value` call argument is caller-owned.
|
|
The vendored host is zig-js's own `js/` (`crates/ghostty-wasm/js/zig-js/`,
|
|
unmodified TypeScript + MIT LICENSE; the lockfile, jest config are omitted).
|
|
The earlier invented `ghostty_js` ABI and `ghostty_host.js` are gone. `env.log`
|
|
(Zig's `os/wasm/log.zig`) is still a separate import.
|
|
|
|
`web_canvas` face and `atlas_debug_canvas` now call JS like the Zig code:
|
|
`document.createElement("canvas")`, `getContext`, `measureText`, `set("font")`,
|
|
`getImageData`, then `Uint8Array(runtime.memory.buffer, ptr).set(imageData.data)`
|
|
to copy pixels (no host-specific `copy_bytes`/`bytes`), and
|
|
`Uint8ClampedArray(memory.buffer, ptr, len)` (+ `.slice()` under
|
|
`wasm-shared`) + `ImageData` for the atlas. `face_debug_canvas` returns
|
|
`CanvasContainerNotFound` like Zig. `atlas_debug_canvas` returns the zig-js
|
|
ref id; free with `zigjs.deleteValue(id)`.
|
|
Remaining small deviation: `text_metrics` reads all five TextMetrics fields
|
|
instead of only the ones needed.
|
|
|
|
Host usage: `const zigjs = new ZigJS(); instantiate(bytes, {...zigjs.
|
|
importObject(), env: {log}}); zigjs.memory = instance.exports.memory;`
|
|
`globalThis` must provide `document`/`ImageData` (the host binds `globalThis`).
|
|
Allocation helpers `wasm_alloc/wasm_free` (host-owned buffers) replace the
|
|
stale `toHostOwned` (does not exist in current `os/wasm.zig`).
|
|
|
|
## Exports
|
|
Same names as Zig: `atlas_{new,free,reserve,set,grow,clear,debug_canvas}`,
|
|
`face_{new,free,render_glyph,debug_canvas}`, `deferred_face_{new,free,load}`,
|
|
`shaper_{new,free,test}`, `config_{new,free,load_string,finalize}`.
|
|
Differences/extras: `region_free`/`glyph_free` (Zig: "global free"),
|
|
`atlas_data/atlas_size`, `face_metrics`, `face_glyph_index`,
|
|
`shaper_{prepare,add_codepoint,shape}`, `config_format`,
|
|
`config_diagnostics_count`, `ghostty_wasm_init`, `wasm_alloc/free`.
|
|
`shaper_test` takes a `Face` (Zig takes a stale `GroupCache`) and prints into a
|
|
`ghostty-terminal` Terminal.
|
|
Config finalization skips desktop shell and home-directory lookup on wasm,
|
|
matching Zig, but still applies normalization such as minimum window dimensions.
|
|
The wasm logger forwards errors only, so `shaper_test` info records are suppressed.
|
|
|
|
## Deviations from the (stale) Zig web_canvas code
|
|
- Zig refers to `Atlas.Format.rgba` and a 7-field `Metrics` that no longer
|
|
exist; here Bgra atlases get the canvas RGBA bitmap with R/B swapped,
|
|
grayscale gets alpha, and `Metrics::init()` + face size = cell size.
|
|
- The `RunIterator` hook API is tied to the FreeType `Collection` (not built on
|
|
wasm), so `Shaper` exposes `prepare/add_codepoint/finalize/shape` directly and
|
|
takes the grapheme-index callback (`Face::grapheme_glyph_index`).
|
|
- `Collection`/`SharedGrid` are not generic over the face type in this port, so
|
|
they are unavailable on wasm (Zig's wasm exports never use them).
|
|
- `render_glyph` drops the unused `RenderOptions` parameter.
|
|
|
|
## Tests
|
|
- Native: pure parts of web_canvas (grapheme table, metrics math, presentation
|
|
detection, glyph sizing, bitmap/atlas conversion; 9 tests) and the shaper
|
|
clustering (6 tests) run natively; `backend::web_canvas_capabilities`.
|
|
- `crates/ghostty-wasm/tests/node.rs` builds the module (separate target dir
|
|
`target/wasm-test`) and runs `tests/node_test.js` under node with a fake
|
|
canvas: config parse/finalize/format, atlas, face metrics/glyph_index/
|
|
render, emoji presentation, shaper clustering, deferred face, debug canvas.
|
|
It checks that config finalization completes without errors and clamps window
|
|
dimensions, and that `shaper_test` prepares a run without emitting info logs.
|
|
Skipped if node or the wasm32 target is missing.
|
|
- Not ported: Zig tests in Atlas.Wasm (`toHostOwned`, stale).
|
|
|
|
## Known gaps
|
|
- `ghostty-os/tests/common/flatpak_mock.rs` (another agent's, untracked) fails
|
|
`clippy --all-targets -D warnings` (needless_pass_by_value); unrelated.
|
|
|
|
- Node test (`tests/node_test.js`) now loads zig-js's `ZigJS` straight from the
|
|
vendored `.ts` (needs node >= 24 type stripping; skipped otherwise), uses a
|
|
fake `document`/canvas on `globalThis`, asserts the import names are zig-js's,
|
|
and checks the host value table has no leaked entries at the end.
|
|
- Native clippy of ghostty-os currently fails in `our_hostname` and wasm builds
|
|
warn on `SEP` unused in `ghostty-os/src/lib.rs` (other agents' areas).
|