libghostty: simplify Wasm allocation API

Replace a bunch of type-specific Wasm allocation functions with a generic
byte allocator and reusable opaque out-parameters for pointers. This
makes it a lot more ergonomic (relatively) to use the Wasm interface
and removes a dozen or so exports.

This also updates the `ghostty_type_json` `abi` field with a maximum
alignment value that host sides can use to keep every allocation aligned
properly, easily, without hardcoding numbers.

This adds a test to verify this all works as intended and runs in CI.
This commit is contained in:
Mitchell Hashimoto
2026-08-16 12:37:16 -07:00
parent 0ba6250388
commit a8e9b413f1
17 changed files with 481 additions and 243 deletions

View File

@@ -183,6 +183,15 @@
return wasmMemory.buffer;
}
function readUsize(ptr) {
const view = new DataView(getBuffer());
switch (typeLayout.abi.usize_size) {
case 4: return view.getUint32(ptr, true);
case 8: return Number(view.getBigUint64(ptr, true));
default: throw new Error('unsupported size_t width');
}
}
// Parse escape sequences in the input string (e.g. \x1b, \r, \n)
function parseEscapes(str) {
return str
@@ -204,6 +213,7 @@
const cols = parseInt(document.getElementById('cols').value, 10);
const rows = parseInt(document.getElementById('rows').value, 10);
const vtText = parseEscapes(document.getElementById('vtInput').value);
const usizeSize = typeLayout.abi.usize_size;
// Allocate pointer to receive the terminal handle
const termPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
@@ -219,19 +229,19 @@
throw new Error(`ghostty_terminal_new failed with result ${newResult}`);
}
const termPtr = new DataView(getBuffer()).getUint32(termPtrPtr, true);
const termPtr = wasmInstance.exports.ghostty_wasm_take_opaque(termPtrPtr);
wasmInstance.exports.ghostty_wasm_free_opaque(termPtrPtr);
// Write VT data to the terminal
const vtBytes = new TextEncoder().encode(vtText);
const dataPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(vtBytes.length);
const dataPtr = wasmInstance.exports.ghostty_wasm_alloc(vtBytes.length);
new Uint8Array(getBuffer()).set(vtBytes, dataPtr);
wasmInstance.exports.ghostty_terminal_vt_write(termPtr, dataPtr, vtBytes.length);
wasmInstance.exports.ghostty_wasm_free_u8_array(dataPtr, vtBytes.length);
wasmInstance.exports.ghostty_wasm_free(dataPtr, vtBytes.length);
// Create a plain-text formatter
const FMT_OPTS_SIZE = typeLayout.types.GhosttyFormatterTerminalOptions.size;
const fmtOptsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(FMT_OPTS_SIZE);
const fmtOptsPtr = wasmInstance.exports.ghostty_wasm_alloc(FMT_OPTS_SIZE);
new Uint8Array(getBuffer(), fmtOptsPtr, FMT_OPTS_SIZE).fill(0);
const fmtOptsView = new DataView(getBuffer(), fmtOptsPtr, FMT_OPTS_SIZE);
setField(fmtOptsView, 'GhosttyFormatterTerminalOptions', 'size', FMT_OPTS_SIZE);
@@ -254,19 +264,19 @@
const fmtResult = wasmInstance.exports.ghostty_formatter_terminal_new(
0, fmtPtrPtr, termPtr, fmtOptsPtr
);
wasmInstance.exports.ghostty_wasm_free_u8_array(fmtOptsPtr, FMT_OPTS_SIZE);
wasmInstance.exports.ghostty_wasm_free(fmtOptsPtr, FMT_OPTS_SIZE);
if (fmtResult !== GHOSTTY_SUCCESS) {
wasmInstance.exports.ghostty_terminal_free(termPtr);
throw new Error(`ghostty_formatter_terminal_new failed with result ${fmtResult}`);
}
const fmtPtr = new DataView(getBuffer()).getUint32(fmtPtrPtr, true);
const fmtPtr = wasmInstance.exports.ghostty_wasm_take_opaque(fmtPtrPtr);
wasmInstance.exports.ghostty_wasm_free_opaque(fmtPtrPtr);
// Format with alloc
const outPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
const outLenPtr = wasmInstance.exports.ghostty_wasm_alloc_usize();
const outLenPtr = wasmInstance.exports.ghostty_wasm_alloc(usizeSize);
const formatResult = wasmInstance.exports.ghostty_formatter_format_alloc(
fmtPtr, 0, outPtrPtr, outLenPtr
);
@@ -277,8 +287,8 @@
throw new Error(`ghostty_formatter_format_alloc failed with result ${formatResult}`);
}
const outPtr = new DataView(getBuffer()).getUint32(outPtrPtr, true);
const outLen = new DataView(getBuffer()).getUint32(outLenPtr, true);
const outPtr = wasmInstance.exports.ghostty_wasm_take_opaque(outPtrPtr);
const outLen = readUsize(outLenPtr);
const outBytes = new Uint8Array(getBuffer(), outPtr, outLen);
const outText = new TextDecoder().decode(outBytes);
@@ -294,7 +304,7 @@
// Clean up
wasmInstance.exports.ghostty_free(0, outPtr, outLen);
wasmInstance.exports.ghostty_wasm_free_opaque(outPtrPtr);
wasmInstance.exports.ghostty_wasm_free_usize(outLenPtr);
wasmInstance.exports.ghostty_wasm_free(outLenPtr, usizeSize);
wasmInstance.exports.ghostty_formatter_free(fmtPtr);
wasmInstance.exports.ghostty_terminal_free(termPtr);