example/wasm-vt: use ghostty_type_json for struct layouts

Replace hardcoded byte offsets and struct sizes with dynamic lookups
from the ghostty_type_json API. On WASM load, the type layout JSON
is fetched once and parsed into a lookup table. Two helpers,
fieldInfo and setField, use this metadata to write struct fields at
the correct offsets with the correct types.

This removes the need to manually maintain wasm32 struct layout
comments and magic numbers for GhosttyTerminalOptions and
GhosttyFormatterTerminalOptions, so the example stays correct if
the struct layouts change.
This commit is contained in:
Mitchell Hashimoto
2026-03-30 10:09:26 -07:00
parent 2e827cc39d
commit 6479d90ca5

View File

@@ -117,6 +117,7 @@
<script>
let wasmInstance = null;
let wasmMemory = null;
let typeLayout = null;
async function loadWasm() {
try {
@@ -136,6 +137,14 @@
wasmInstance = wasmModule.instance;
wasmMemory = wasmInstance.exports.memory;
// Load the type layout JSON from the library
const lenPtr = wasmInstance.exports.ghostty_wasm_alloc_usize();
const jsonPtr = wasmInstance.exports.ghostty_type_json(lenPtr);
const jsonLen = new DataView(wasmMemory.buffer).getUint32(lenPtr, true);
wasmInstance.exports.ghostty_wasm_free_usize(lenPtr);
const jsonBytes = new Uint8Array(wasmMemory.buffer, jsonPtr, jsonLen);
typeLayout = JSON.parse(new TextDecoder().decode(jsonBytes));
return true;
} catch (e) {
console.error('Failed to load WASM:', e);
@@ -146,6 +155,24 @@
}
}
// Look up a field's offset and DataView setter from the type layout JSON.
function fieldInfo(structName, fieldName) {
const field = typeLayout[structName].fields[fieldName];
return field;
}
// Set a field in a DataView using the type layout JSON metadata.
function setField(view, structName, fieldName, value) {
const field = fieldInfo(structName, fieldName);
switch (field.type) {
case 'u8': case 'bool': view.setUint8(field.offset, value); break;
case 'u16': view.setUint16(field.offset, value, true); break;
case 'u32': case 'enum': view.setUint32(field.offset, value, true); break;
case 'u64': view.setBigUint64(field.offset, BigInt(value), true); break;
default: throw new Error(`Unsupported field type: ${field.type}`);
}
}
function getBuffer() {
return wasmMemory.buffer;
}
@@ -173,14 +200,13 @@
const rows = parseInt(document.getElementById('rows').value, 10);
const vtText = parseEscapes(document.getElementById('vtInput').value);
// GhosttyTerminalOptions: { u16 cols, u16 rows, u32(wasm) max_scrollback }
// On wasm32, size_t is u32. Struct layout: cols(u16) + rows(u16) + max_scrollback(u32) = 8 bytes.
const TERM_OPTS_SIZE = 8;
const TERM_OPTS_SIZE = typeLayout['GhosttyTerminalOptions'].size;
const optsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(TERM_OPTS_SIZE);
new Uint8Array(getBuffer(), optsPtr, TERM_OPTS_SIZE).fill(0);
const optsView = new DataView(getBuffer(), optsPtr, TERM_OPTS_SIZE);
optsView.setUint16(0, cols, true);
optsView.setUint16(2, rows, true);
optsView.setUint32(4, 0, true); // max_scrollback = 0
setField(optsView, 'GhosttyTerminalOptions', 'cols', cols);
setField(optsView, 'GhosttyTerminalOptions', 'rows', rows);
setField(optsView, 'GhosttyTerminalOptions', 'max_scrollback', 0);
// Allocate pointer to receive the terminal handle
const termPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
@@ -204,26 +230,25 @@
wasmInstance.exports.ghostty_wasm_free_u8_array(dataPtr, vtBytes.length);
// Create a plain-text formatter
// GhosttyFormatterTerminalOptions layout on wasm32 (extern struct):
// size: u32 @ 0 (= sizeof struct = 36)
// emit: u32 @ 4 (GhosttyFormatterFormat enum)
// unwrap: u8 @ 8
// trim: u8 @ 9
// pad: 2 bytes
// extra: GhosttyFormatterTerminalExtra @ 12 (24 bytes, all zeros)
// Total: 36 bytes
const FMT_OPTS_SIZE = 36;
const FMT_OPTS_SIZE = typeLayout['GhosttyFormatterTerminalOptions'].size;
const fmtOptsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(FMT_OPTS_SIZE);
new Uint8Array(getBuffer(), fmtOptsPtr, FMT_OPTS_SIZE).fill(0);
const fmtOptsView = new DataView(getBuffer(), fmtOptsPtr, FMT_OPTS_SIZE);
fmtOptsView.setUint32(0, FMT_OPTS_SIZE, true); // size
fmtOptsView.setUint32(4, GHOSTTY_FORMATTER_FORMAT_PLAIN, true); // emit
fmtOptsView.setUint8(8, 0); // unwrap = false
fmtOptsView.setUint8(9, 1); // trim = true
// extra.size @ offset 12 (GhosttyFormatterTerminalExtra = 24 bytes)
fmtOptsView.setUint32(12, 24, true);
// extra.screen.size @ offset 24 (GhosttyFormatterScreenExtra = 12 bytes)
fmtOptsView.setUint32(24, 12, true);
setField(fmtOptsView, 'GhosttyFormatterTerminalOptions', 'size', FMT_OPTS_SIZE);
setField(fmtOptsView, 'GhosttyFormatterTerminalOptions', 'emit', GHOSTTY_FORMATTER_FORMAT_PLAIN);
setField(fmtOptsView, 'GhosttyFormatterTerminalOptions', 'unwrap', 0);
setField(fmtOptsView, 'GhosttyFormatterTerminalOptions', 'trim', 1);
// Set the nested sized-struct `size` fields for extra and extra.screen
const extraOffset = fieldInfo('GhosttyFormatterTerminalOptions', 'extra').offset;
const extraSize = typeLayout['GhosttyFormatterTerminalExtra'].size;
const extraSizeField = fieldInfo('GhosttyFormatterTerminalExtra', 'size');
fmtOptsView.setUint32(extraOffset + extraSizeField.offset, extraSize, true);
const screenOffset = fieldInfo('GhosttyFormatterTerminalExtra', 'screen').offset;
const screenSize = typeLayout['GhosttyFormatterScreenExtra'].size;
const screenSizeField = fieldInfo('GhosttyFormatterScreenExtra', 'size');
fmtOptsView.setUint32(extraOffset + screenOffset + screenSizeField.offset, screenSize, true);
const fmtPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
const fmtResult = wasmInstance.exports.ghostty_formatter_terminal_new(