Files
ghostty-rs/crates/ghostty-wasm/js/ghostty_host.js
T
ddidderr d89691b0ce feat(wasm): port Ghostty's wasm32 build
Ports main_wasm.zig as crates/ghostty-wasm (cdylib exporting the Zig
names: atlas_*, face_*, deferred_face_*, shaper_*, config_*), the
web_canvas font face and shaper (backend selected on wasm32), os/wasm
(logging, target, JS interop) and config/Wasm.zig.

C-library dependencies (freetype, harfbuzz, fontconfig, onig, gettext,
D-Bus, rustix) are target-gated. Like Zig's wasm build, link regex
search (StringMap::search_iterator) is unavailable without Oniguruma;
Collection/SharedGrid/harfbuzz are compiled out on wasm32. ghostty-os
switches from forbid to deny unsafe_code for the JS import blocks.

The JS import ABI is currently a documented handle-based interface with
a reference host (js/ghostty_host.js); aligning it with zig-js's exact
import names follows separately.

Test Plan:
- cargo build --target wasm32-unknown-unknown -p ghostty-font \
  -p ghostty-config -p ghostty-terminal -p ghostty-vt -p ghostty-wasm
- cargo test -p ghostty-wasm (node runs the module against a fake canvas)
2026-10-12 05:21:14 +02:00

107 lines
3.4 KiB
JavaScript

// Host-side glue for the `ghostty_js` imports (see
// crates/ghostty-os/src/wasm/js.rs for the protocol) and `env.log`.
// Usable from a browser (ES module) or node (CommonJS).
//
// const host = createHost({ globalThis, log });
// const { instance } = await WebAssembly.instantiate(bytes, host.imports);
// host.setInstance(instance);
function createHost({ global = globalThis, log = console.log } = {}) {
const dec = new TextDecoder();
const enc = new TextEncoder();
// Handle table. Handle 0 is reserved for "invalid / threw".
const table = [undefined];
const free = [];
let instance = null;
const put = (v) => {
if (free.length > 0) {
const h = free.pop();
table[h] = v;
return h;
}
table.push(v);
return table.length - 1;
};
const mem = () => new Uint8Array(instance.exports.memory.buffer);
const str = (ptr, len) => dec.decode(mem().subarray(ptr, ptr + len));
const guard = (f) => {
try {
return f();
} catch (e) {
host.lastError = e;
return 0;
}
};
const ghostty_js = {
global: () => put(global),
get: (obj, ptr, len) => guard(() => put(table[obj][str(ptr, len)])),
set: (obj, ptr, len, val) =>
guard(() => {
table[obj][str(ptr, len)] = table[val];
return 1;
}),
call: (obj, ptr, len, argsPtr, argc) =>
guard(() => {
const args = Array.from(new Uint32Array(instance.exports.memory.buffer, argsPtr, argc), (h) => table[h]);
const o = table[obj];
return put(o[str(ptr, len)](...args));
}),
new: (ctor, argsPtr, argc) =>
guard(() => {
const args = Array.from(new Uint32Array(instance.exports.memory.buffer, argsPtr, argc), (h) => table[h]);
return put(new table[ctor](...args));
}),
string: (ptr, len) => put(str(ptr, len)),
number: (v) => put(v),
boolean: (v) => put(v !== 0),
to_f64: (h) => Number(table[h]),
string_len: (h) => enc.encode(String(table[h])).length,
string_copy: (h, ptr, cap) => {
const bytes = enc.encode(String(table[h]));
const n = Math.min(bytes.length, cap);
mem().set(bytes.subarray(0, n), ptr);
return n;
},
is_nullish: (h) => (table[h] === undefined || table[h] === null ? 1 : 0),
bytes: (ptr, len) => put(new Uint8ClampedArray(mem().slice(ptr, ptr + len).buffer)),
copy_bytes: (h, dst, len) =>
guard(() => {
const src = table[h];
if (src.length !== len) throw new Error(`length mismatch ${src.length} != ${len}`);
mem().set(new Uint8Array(src.buffer, src.byteOffset, len), dst);
return 1;
}),
release: (h) => {
if (h > 0) {
table[h] = undefined;
free.push(h);
}
},
};
const host = {
lastError: null,
imports: { ghostty_js, env: { log: (ptr, len) => log(str(ptr, len)) } },
setInstance(i) {
instance = i;
},
// Helpers for callers of the exports.
writeString(s) {
const bytes = enc.encode(s);
const ptr = instance.exports.wasm_alloc(bytes.length);
mem().set(bytes, ptr);
return { ptr, len: bytes.length, free: () => instance.exports.wasm_free(ptr, bytes.length) };
},
readString(ptr, len) {
return str(ptr, len);
},
value: (h) => table[h],
handleCount: () => table.length - 1 - free.length,
};
return host;
}
if (typeof module !== 'undefined') module.exports = { createHost };