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

@@ -319,31 +319,6 @@ GHOSTTY_API GhosttySgrAttributeTag ghostty_sgr_attribute_tag(GhosttySgrAttribute
GHOSTTY_API GhosttySgrAttributeValue* ghostty_sgr_attribute_value(
GhosttySgrAttribute* attr);
#ifdef __wasm__
/**
* Allocate memory for an SGR attribute (WebAssembly only).
*
* This is a convenience function for WebAssembly environments to allocate
* memory for an SGR attribute structure that can be passed to ghostty_sgr_next.
*
* @return Pointer to the allocated attribute structure
*
* @ingroup wasm
*/
GHOSTTY_API GhosttySgrAttribute* ghostty_wasm_alloc_sgr_attribute(void);
/**
* Free memory for an SGR attribute (WebAssembly only).
*
* Frees memory allocated by ghostty_wasm_alloc_sgr_attribute.
*
* @param attr Pointer to the attribute structure to free
*
* @ingroup wasm
*/
GHOSTTY_API void ghostty_wasm_free_sgr_attribute(GhosttySgrAttribute* attr);
#endif
#ifdef __cplusplus
}
#endif

View File

@@ -358,7 +358,8 @@ typedef struct {
* "schema": 1,
* "abi": {
* "target": "wasm32", "os": "freestanding", "environment": "none",
* "pointer_size": 4, "usize_size": 4, "endian": "little"
* "pointer_size": 4, "usize_size": 4, "max_alignment": 16,
* "endian": "little"
* },
* "types": {
* "GhosttyRenderStateData": {

View File

@@ -10,12 +10,12 @@
#ifdef __wasm__
#include <stddef.h>
#include <stdint.h>
#include <ghostty/vt/types.h>
/** @defgroup wasm WebAssembly Utilities
*
* Convenience functions for allocating various types in WebAssembly builds.
* Convenience functions for working with the low-level C ABI in WebAssembly
* builds.
* **These are only available the libghostty-vt wasm module.**
*
* Ghostty relies on pointers to various types for ABI compatibility, and
@@ -28,37 +28,64 @@
* your custom allocator. This is a very rare use case in the WebAssembly
* world so these are optimized for simplicity.
*
* Use ghostty_wasm_alloc() and ghostty_wasm_free() for host-owned scratch
* buffers and ABI values. Dynamic-language hosts can use ghostty_type_json()
* to discover pointer and size_t widths, maximum alignment, byte order, and
* the size and alignment of public C structs. Do not mix allocation families:
* buffers returned by libghostty-vt allocating APIs must still be released
* with ghostty_free(), and opaque handles must be released with their
* type-specific destructor.
*
* ## Memory growth
*
* An exported function may grow Wasm linear memory when it allocates. Numeric
* pointers and handles remain valid, but JavaScript ArrayBuffer, DataView, and
* typed-array objects created before the growth may no longer cover the live
* memory. Reacquire `exports.memory.buffer` immediately before every host-side
* memory access. A host that caches views should recreate them whenever either
* the buffer identity or its byte length changes.
*
* ## Example Usage
*
* Here's a simple example of using the Wasm utilities with the key encoder:
* Here's a simple example that creates a terminal, writes bytes, and safely
* handles memory growth:
*
* @code
* const { exports } = wasmInstance;
* const view = new DataView(wasmMemory.buffer);
* const memory = exports.memory;
* let cachedBuffer = null;
* let cachedLength = 0;
* let cachedBytes = null;
*
* // Create key encoder
* const encoderPtr = exports.ghostty_wasm_alloc_opaque();
* exports.ghostty_key_encoder_new(null, encoderPtr);
* const encoder = view.getUint32(encoder, true);
* function bytes() {
* const buffer = memory.buffer;
* if (buffer !== cachedBuffer || buffer.byteLength !== cachedLength) {
* cachedBuffer = buffer;
* cachedLength = buffer.byteLength;
* cachedBytes = new Uint8Array(buffer);
* }
* return cachedBytes;
* }
*
* // Configure encoder with Kitty protocol flags
* const flagsPtr = exports.ghostty_wasm_alloc_u8();
* view.setUint8(flagsPtr, 0x1F);
* exports.ghostty_key_encoder_setopt(encoder, 5, flagsPtr);
* function check(result) {
* if (result !== 0) throw new Error(`libghostty-vt error: ${result}`);
* }
*
* // Allocate output buffer and size pointer
* const bufferSize = 32;
* const bufPtr = exports.ghostty_wasm_alloc_u8_array(bufferSize);
* const writtenPtr = exports.ghostty_wasm_alloc_usize();
* // One slot can be reused for every opaque-handle constructor.
* const slot = exports.ghostty_wasm_alloc_opaque();
* if (slot === 0) throw new Error("out of memory");
* check(exports.ghostty_terminal_new(0, slot, 80, 24));
* const terminal = exports.ghostty_wasm_take_opaque(slot);
*
* // Encode the key event
* exports.ghostty_key_encoder_encode(
* encoder, eventPtr, bufPtr, bufferSize, writtenPtr
* );
* const input = new TextEncoder().encode("Hello, world!");
* const inputPtr = exports.ghostty_wasm_alloc(input.length);
* if (inputPtr === 0) throw new Error("out of memory");
* bytes().set(input, inputPtr); // Acquires the current memory after alloc.
* exports.ghostty_terminal_vt_write(terminal, inputPtr, input.length);
*
* // Read encoded output
* const bytesWritten = view.getUint32(writtenPtr, true);
* const encoded = new Uint8Array(wasmMemory.buffer, bufPtr, bytesWritten);
* exports.ghostty_wasm_free(inputPtr, input.length);
* exports.ghostty_terminal_free(terminal);
* exports.ghostty_wasm_free_opaque(slot);
* @endcode
*
* @remark The code above is pretty ugly! This is the lowest level interface
@@ -68,9 +95,36 @@
* @{
*/
/**
* Allocate caller-owned storage for a Wasm ABI value or scratch buffer.
*
* The returned address is aligned to the target's maximum C ABI alignment,
* reported as `abi.max_alignment` by ghostty_type_json(). The memory is
* uninitialized. A zero-length request returns NULL.
*
* The returned pointer must be released with ghostty_wasm_free() using the
* exact same length.
*
* @param len Number of bytes to allocate
* @return Pointer to allocated storage, or NULL if len is zero or allocation
* failed
* @ingroup wasm
*/
GHOSTTY_API void* ghostty_wasm_alloc(size_t len);
/**
* Free storage allocated by ghostty_wasm_alloc().
*
* @param ptr Pointer to free, or NULL (NULL is safely ignored)
* @param len Original allocation length passed to ghostty_wasm_alloc()
* @ingroup wasm
*/
GHOSTTY_API void ghostty_wasm_free(void *ptr, size_t len);
/**
* Allocate an opaque pointer. This can be used for any opaque pointer
* types such as GhosttyKeyEncoder, GhosttyKeyEvent, etc.
* types such as GhosttyKeyEncoder, GhosttyKeyEvent, etc. The allocated slot
* is initialized to NULL and may be reused across constructors.
*
* @return Pointer to allocated opaque pointer, or NULL if allocation failed
* @ingroup wasm
@@ -86,72 +140,18 @@ GHOSTTY_API void** ghostty_wasm_alloc_opaque(void);
GHOSTTY_API void ghostty_wasm_free_opaque(void **ptr);
/**
* Allocate an array of uint8_t values.
* Take an opaque handle from an out-parameter slot.
*
* @param len Number of uint8_t elements to allocate
* @return Pointer to allocated array, or NULL if allocation failed
* Returns the handle currently stored in @p slot and resets the slot to NULL.
* This function does not allocate, free the returned handle, or free the slot.
* Always check the GhosttyResult returned by the function that populated the
* slot before calling this function.
*
* @param slot Pointer to an opaque out-parameter slot, or NULL
* @return Stored opaque handle, or NULL if slot or its value is NULL
* @ingroup wasm
*/
GHOSTTY_API uint8_t* ghostty_wasm_alloc_u8_array(size_t len);
/**
* Free an array allocated by ghostty_wasm_alloc_u8_array().
*
* @param ptr Pointer to the array to free, or NULL (NULL is safely ignored)
* @param len Length of the array (must match the length passed to alloc)
* @ingroup wasm
*/
GHOSTTY_API void ghostty_wasm_free_u8_array(uint8_t *ptr, size_t len);
/**
* Allocate an array of uint16_t values.
*
* @param len Number of uint16_t elements to allocate
* @return Pointer to allocated array, or NULL if allocation failed
* @ingroup wasm
*/
GHOSTTY_API uint16_t* ghostty_wasm_alloc_u16_array(size_t len);
/**
* Free an array allocated by ghostty_wasm_alloc_u16_array().
*
* @param ptr Pointer to the array to free, or NULL (NULL is safely ignored)
* @param len Length of the array (must match the length passed to alloc)
* @ingroup wasm
*/
GHOSTTY_API void ghostty_wasm_free_u16_array(uint16_t *ptr, size_t len);
/**
* Allocate a single uint8_t value.
*
* @return Pointer to allocated uint8_t, or NULL if allocation failed
* @ingroup wasm
*/
GHOSTTY_API uint8_t* ghostty_wasm_alloc_u8(void);
/**
* Free a uint8_t allocated by ghostty_wasm_alloc_u8().
*
* @param ptr Pointer to free, or NULL (NULL is safely ignored)
* @ingroup wasm
*/
GHOSTTY_API void ghostty_wasm_free_u8(uint8_t *ptr);
/**
* Allocate a single size_t value.
*
* @return Pointer to allocated size_t, or NULL if allocation failed
* @ingroup wasm
*/
GHOSTTY_API size_t* ghostty_wasm_alloc_usize(void);
/**
* Free a size_t allocated by ghostty_wasm_alloc_usize().
*
* @param ptr Pointer to free, or NULL (NULL is safely ignored)
* @ingroup wasm
*/
GHOSTTY_API void ghostty_wasm_free_usize(size_t *ptr);
GHOSTTY_API void* ghostty_wasm_take_opaque(void **slot);
/** @} */