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:
@@ -733,6 +733,11 @@ jobs:
|
||||
echo "Verified ${artifact} requires simd128"
|
||||
done
|
||||
|
||||
- name: Test WASM allocator
|
||||
run: |
|
||||
nix develop -c node test/wasm-alloc.mjs ghostty-vt.wasm
|
||||
nix develop -c node test/wasm-alloc.mjs ghostty-vt-small.wasm
|
||||
|
||||
# Compile-only checks for the -Dvt-features flags so that future changes
|
||||
# don't regress any feature combination.
|
||||
build-libghostty-vt-features:
|
||||
|
||||
@@ -163,6 +163,7 @@
|
||||
let wasmMemory = null;
|
||||
let encoderPtr = null;
|
||||
let lastKeyEvent = null;
|
||||
let typeLayout = null;
|
||||
|
||||
async function loadWasm() {
|
||||
try {
|
||||
@@ -184,6 +185,12 @@
|
||||
|
||||
wasmInstance = wasmModule.instance;
|
||||
wasmMemory = wasmInstance.exports.memory;
|
||||
|
||||
const jsonPtr = wasmInstance.exports.ghostty_type_json();
|
||||
const jsonStr = new TextDecoder().decode(
|
||||
new Uint8Array(wasmMemory.buffer, jsonPtr, wasmMemory.buffer.byteLength - jsonPtr)
|
||||
).split('\0')[0];
|
||||
typeLayout = JSON.parse(jsonStr);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -199,6 +206,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');
|
||||
}
|
||||
}
|
||||
|
||||
function formatHex(bytes) {
|
||||
return Array.from(bytes)
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
@@ -415,16 +431,28 @@
|
||||
function encodeKeyEvent(event) {
|
||||
if (!encoderPtr) return null;
|
||||
|
||||
const usizeSize = typeLayout.abi.usize_size;
|
||||
let eventPtrPtr = 0;
|
||||
let eventPtr = 0;
|
||||
let utf8Ptr = 0;
|
||||
let utf8Length = 0;
|
||||
let requiredPtr = 0;
|
||||
let required = 0;
|
||||
let bufPtr = 0;
|
||||
let writtenPtr = 0;
|
||||
|
||||
try {
|
||||
// Create key event
|
||||
const eventPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
|
||||
eventPtrPtr = wasmInstance.exports.ghostty_wasm_alloc_opaque();
|
||||
const result = wasmInstance.exports.ghostty_key_event_new(0, eventPtrPtr);
|
||||
|
||||
if (result !== 0) {
|
||||
throw new Error(`ghostty_key_event_new failed with result ${result}`);
|
||||
}
|
||||
|
||||
const eventPtr = new DataView(getBuffer()).getUint32(eventPtrPtr, true);
|
||||
eventPtr = wasmInstance.exports.ghostty_wasm_take_opaque(eventPtrPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_opaque(eventPtrPtr);
|
||||
eventPtrPtr = 0;
|
||||
|
||||
// Get action from radio buttons
|
||||
const actionRadio = document.querySelector('input[name="action"]:checked');
|
||||
@@ -458,9 +486,10 @@
|
||||
// Set UTF-8 text from the key event (the actual character produced)
|
||||
if (event.key.length === 1) {
|
||||
const utf8Bytes = new TextEncoder().encode(event.key);
|
||||
const utf8Ptr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(utf8Bytes.length);
|
||||
utf8Length = utf8Bytes.length;
|
||||
utf8Ptr = wasmInstance.exports.ghostty_wasm_alloc(utf8Length);
|
||||
new Uint8Array(getBuffer()).set(utf8Bytes, utf8Ptr);
|
||||
wasmInstance.exports.ghostty_key_event_set_utf8(eventPtr, utf8Ptr, utf8Bytes.length);
|
||||
wasmInstance.exports.ghostty_key_event_set_utf8(eventPtr, utf8Ptr, utf8Length);
|
||||
}
|
||||
|
||||
// Set unshifted codepoint
|
||||
@@ -470,15 +499,15 @@
|
||||
}
|
||||
|
||||
// Encode the key event
|
||||
const requiredPtr = wasmInstance.exports.ghostty_wasm_alloc_usize();
|
||||
requiredPtr = wasmInstance.exports.ghostty_wasm_alloc(usizeSize);
|
||||
wasmInstance.exports.ghostty_key_encoder_encode(
|
||||
encoderPtr, eventPtr, 0, 0, requiredPtr
|
||||
);
|
||||
|
||||
const required = new DataView(getBuffer()).getUint32(requiredPtr, true);
|
||||
required = readUsize(requiredPtr);
|
||||
|
||||
const bufPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(required);
|
||||
const writtenPtr = wasmInstance.exports.ghostty_wasm_alloc_usize();
|
||||
bufPtr = wasmInstance.exports.ghostty_wasm_alloc(required);
|
||||
writtenPtr = wasmInstance.exports.ghostty_wasm_alloc(usizeSize);
|
||||
const encodeResult = wasmInstance.exports.ghostty_key_encoder_encode(
|
||||
encoderPtr, eventPtr, bufPtr, required, writtenPtr
|
||||
);
|
||||
@@ -487,7 +516,7 @@
|
||||
return null; // No encoding for this key
|
||||
}
|
||||
|
||||
const written = new DataView(getBuffer()).getUint32(writtenPtr, true);
|
||||
const written = readUsize(writtenPtr);
|
||||
const encoded = new Uint8Array(getBuffer()).slice(bufPtr, bufPtr + written);
|
||||
|
||||
return {
|
||||
@@ -498,6 +527,13 @@
|
||||
} catch (e) {
|
||||
console.error('Encoding error:', e);
|
||||
return null;
|
||||
} finally {
|
||||
wasmInstance.exports.ghostty_wasm_free(writtenPtr, usizeSize);
|
||||
wasmInstance.exports.ghostty_wasm_free(bufPtr, required);
|
||||
wasmInstance.exports.ghostty_wasm_free(requiredPtr, usizeSize);
|
||||
wasmInstance.exports.ghostty_wasm_free(utf8Ptr, utf8Length);
|
||||
wasmInstance.exports.ghostty_key_event_free(eventPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_opaque(eventPtrPtr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,13 +591,14 @@
|
||||
if (!encoderPtr) return;
|
||||
|
||||
const flags = getKittyFlags();
|
||||
const flagsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const flagsPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
new DataView(getBuffer()).setUint8(flagsPtr, flags);
|
||||
wasmInstance.exports.ghostty_key_encoder_setopt(
|
||||
encoderPtr,
|
||||
5, // GHOSTTY_KEY_ENCODER_OPT_KITTY_FLAGS
|
||||
flagsPtr
|
||||
);
|
||||
wasmInstance.exports.ghostty_wasm_free(flagsPtr, 1);
|
||||
|
||||
// Re-encode last key with new flags
|
||||
reencodeLastKey();
|
||||
@@ -649,7 +686,8 @@
|
||||
throw new Error(`ghostty_key_encoder_new failed with result ${result}`);
|
||||
}
|
||||
|
||||
encoderPtr = new DataView(getBuffer()).getUint32(encoderPtrPtr, true);
|
||||
encoderPtr = wasmInstance.exports.ghostty_wasm_take_opaque(encoderPtrPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_opaque(encoderPtrPtr);
|
||||
|
||||
// Set kitty flags based on checkboxes
|
||||
updateEncoderFlags();
|
||||
|
||||
+38
-24
@@ -106,6 +106,7 @@
|
||||
<script>
|
||||
let wasmInstance = null;
|
||||
let wasmMemory = null;
|
||||
let typeLayout = null;
|
||||
|
||||
async function loadWasm() {
|
||||
try {
|
||||
@@ -124,6 +125,12 @@
|
||||
|
||||
wasmInstance = wasmModule.instance;
|
||||
wasmMemory = wasmInstance.exports.memory;
|
||||
|
||||
const jsonPtr = wasmInstance.exports.ghostty_type_json();
|
||||
const jsonStr = new TextDecoder().decode(
|
||||
new Uint8Array(wasmMemory.buffer, jsonPtr, wasmMemory.buffer.byteLength - jsonPtr)
|
||||
).split('\0')[0];
|
||||
typeLayout = JSON.parse(jsonStr);
|
||||
|
||||
return true;
|
||||
} catch (e) {
|
||||
@@ -252,18 +259,22 @@
|
||||
throw new Error(`ghostty_sgr_new failed with result ${result}`);
|
||||
}
|
||||
|
||||
const parserPtr = new DataView(getBuffer()).getUint32(parserPtrPtr, true);
|
||||
const parserPtr = wasmInstance.exports.ghostty_wasm_take_opaque(parserPtrPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_opaque(parserPtrPtr);
|
||||
|
||||
// Allocate and set parameters
|
||||
const paramsPtr = wasmInstance.exports.ghostty_wasm_alloc_u16_array(params.length);
|
||||
const paramsByteLength = params.length * Uint16Array.BYTES_PER_ELEMENT;
|
||||
const paramsPtr = wasmInstance.exports.ghostty_wasm_alloc(paramsByteLength);
|
||||
const paramsView = new Uint16Array(getBuffer(), paramsPtr, params.length);
|
||||
params.forEach((p, i) => paramsView[i] = p);
|
||||
|
||||
// Allocate and set separators (or use null if empty)
|
||||
let sepsPtr = 0;
|
||||
const sepsByteLength = separators.length > 0 ? params.length : 0;
|
||||
if (separators.length > 0) {
|
||||
sepsPtr = wasmInstance.exports.ghostty_wasm_alloc_u8_array(separators.length);
|
||||
const sepsView = new Uint8Array(getBuffer(), sepsPtr, separators.length);
|
||||
sepsPtr = wasmInstance.exports.ghostty_wasm_alloc(sepsByteLength);
|
||||
const sepsView = new Uint8Array(getBuffer(), sepsPtr, sepsByteLength);
|
||||
sepsView.fill(0);
|
||||
separators.forEach((s, i) => sepsView[i] = s.charCodeAt(0));
|
||||
}
|
||||
|
||||
@@ -289,7 +300,8 @@
|
||||
output += 'm\n\n';
|
||||
|
||||
// Iterate through attributes
|
||||
const attrPtr = wasmInstance.exports.ghostty_wasm_alloc_sgr_attribute();
|
||||
const attrSize = typeLayout.types.GhosttySgrAttribute.size;
|
||||
const attrPtr = wasmInstance.exports.ghostty_wasm_alloc(attrSize);
|
||||
let count = 0;
|
||||
|
||||
while (wasmInstance.exports.ghostty_sgr_next(parserPtr, attrPtr)) {
|
||||
@@ -313,9 +325,9 @@
|
||||
|
||||
case SGR_ATTR_TAGS.DIRECT_COLOR_FG: {
|
||||
// Use ghostty_color_rgb_get to extract RGB components
|
||||
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const rPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
const gPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
const bPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
|
||||
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
|
||||
|
||||
@@ -325,17 +337,17 @@
|
||||
|
||||
output += `Foreground RGB = (${r}, ${g}, ${b})\n`;
|
||||
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free(rPtr, 1);
|
||||
wasmInstance.exports.ghostty_wasm_free(gPtr, 1);
|
||||
wasmInstance.exports.ghostty_wasm_free(bPtr, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
case SGR_ATTR_TAGS.DIRECT_COLOR_BG: {
|
||||
// Use ghostty_color_rgb_get to extract RGB components
|
||||
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const rPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
const gPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
const bPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
|
||||
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
|
||||
|
||||
@@ -345,17 +357,17 @@
|
||||
|
||||
output += `Background RGB = (${r}, ${g}, ${b})\n`;
|
||||
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free(rPtr, 1);
|
||||
wasmInstance.exports.ghostty_wasm_free(gPtr, 1);
|
||||
wasmInstance.exports.ghostty_wasm_free(bPtr, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
case SGR_ATTR_TAGS.UNDERLINE_COLOR: {
|
||||
// Use ghostty_color_rgb_get to extract RGB components
|
||||
const rPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const gPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const bPtr = wasmInstance.exports.ghostty_wasm_alloc_u8();
|
||||
const rPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
const gPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
const bPtr = wasmInstance.exports.ghostty_wasm_alloc(1);
|
||||
|
||||
wasmInstance.exports.ghostty_color_rgb_get(valuePtr, rPtr, gPtr, bPtr);
|
||||
|
||||
@@ -365,9 +377,9 @@
|
||||
|
||||
output += `Underline color RGB = (${r}, ${g}, ${b})\n`;
|
||||
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(rPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(gPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free_u8(bPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free(rPtr, 1);
|
||||
wasmInstance.exports.ghostty_wasm_free(gPtr, 1);
|
||||
wasmInstance.exports.ghostty_wasm_free(bPtr, 1);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -415,7 +427,9 @@
|
||||
outputDiv.textContent = output;
|
||||
|
||||
// Cleanup
|
||||
wasmInstance.exports.ghostty_wasm_free_sgr_attribute(attrPtr);
|
||||
wasmInstance.exports.ghostty_wasm_free(attrPtr, attrSize);
|
||||
wasmInstance.exports.ghostty_wasm_free(paramsPtr, paramsByteLength);
|
||||
wasmInstance.exports.ghostty_wasm_free(sepsPtr, sepsByteLength);
|
||||
wasmInstance.exports.ghostty_sgr_free(parserPtr);
|
||||
|
||||
} catch (e) {
|
||||
|
||||
+20
-10
@@ -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);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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": {
|
||||
|
||||
+87
-87
@@ -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);
|
||||
|
||||
/** @} */
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const testing = std.testing;
|
||||
|
||||
/// Convenience functions
|
||||
pub const convenience = @import("allocator/convenience.zig");
|
||||
/// Wasm-specific allocation helpers.
|
||||
pub const wasm = @import("allocator/wasm.zig");
|
||||
|
||||
/// Useful alias since they're required to create Zig allocators
|
||||
pub const ZigVTable = std.mem.Allocator.VTable;
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
//! This contains convenience functions for allocating various types.
|
||||
//!
|
||||
//! The primary use case for this is Wasm builds. Ghostty relies a lot on
|
||||
//! pointers to various types for ABI compatibility and creating those pointers
|
||||
//! in Wasm is tedious. This file contains a purely additive set of functions
|
||||
//! that can be exposed to the Wasm module without changing the API from the
|
||||
//! C library.
|
||||
//!
|
||||
//! Given these are convenience methods, they always use the default allocator.
|
||||
//! If a caller is using a custom allocator, they have the expertise to
|
||||
//! allocate these types manually using their custom allocator.
|
||||
|
||||
// Get our default allocator at comptime since it is known.
|
||||
const default = @import("../allocator.zig").default;
|
||||
const alloc = default(null);
|
||||
|
||||
pub const Opaque = *anyopaque;
|
||||
|
||||
pub fn allocOpaque() callconv(.c) ?*Opaque {
|
||||
return alloc.create(*anyopaque) catch return null;
|
||||
}
|
||||
|
||||
pub fn freeOpaque(ptr: ?*Opaque) callconv(.c) void {
|
||||
if (ptr) |p| alloc.destroy(p);
|
||||
}
|
||||
|
||||
pub fn allocU8Array(len: usize) callconv(.c) ?[*]u8 {
|
||||
const slice = alloc.alloc(u8, len) catch return null;
|
||||
return slice.ptr;
|
||||
}
|
||||
|
||||
pub fn freeU8Array(ptr: ?[*]u8, len: usize) callconv(.c) void {
|
||||
if (ptr) |p| alloc.free(p[0..len]);
|
||||
}
|
||||
|
||||
pub fn allocU16Array(len: usize) callconv(.c) ?[*]u16 {
|
||||
const slice = alloc.alloc(u16, len) catch return null;
|
||||
return slice.ptr;
|
||||
}
|
||||
|
||||
pub fn freeU16Array(ptr: ?[*]u16, len: usize) callconv(.c) void {
|
||||
if (ptr) |p| alloc.free(p[0..len]);
|
||||
}
|
||||
|
||||
pub fn allocU8() callconv(.c) ?*u8 {
|
||||
return alloc.create(u8) catch return null;
|
||||
}
|
||||
|
||||
pub fn freeU8(ptr: ?*u8) callconv(.c) void {
|
||||
if (ptr) |p| alloc.destroy(p);
|
||||
}
|
||||
|
||||
pub fn allocUsize() callconv(.c) ?*usize {
|
||||
return alloc.create(usize) catch return null;
|
||||
}
|
||||
|
||||
pub fn freeUsize(ptr: ?*usize) callconv(.c) void {
|
||||
if (ptr) |p| alloc.destroy(p);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
//! Wasm allocation conveniences for caller-owned storage and opaque out slots.
|
||||
//!
|
||||
//! The primary use case for this is Wasm builds. Ghostty relies a lot on
|
||||
//! pointers to various types for ABI compatibility and creating those pointers
|
||||
//! in Wasm is tedious. This file contains the small set of functions exposed by
|
||||
//! the Wasm module without changing the API from the C library.
|
||||
//!
|
||||
//! Given these are convenience methods, they always use the default allocator.
|
||||
//! If a caller is using a custom allocator, they have the expertise to
|
||||
//! allocate these types manually using their custom allocator.
|
||||
|
||||
const std = @import("std");
|
||||
const c_abi = @import("../c_abi.zig");
|
||||
|
||||
// Get our default allocator at comptime since it is known.
|
||||
const default = @import("../allocator.zig").default;
|
||||
const alloc = default(null);
|
||||
const wasm_alignment: std.mem.Alignment = .fromByteUnits(c_abi.max_alignment);
|
||||
|
||||
/// A nullable opaque C handle stored in a constructor out-parameter slot.
|
||||
pub const Opaque = ?*anyopaque;
|
||||
|
||||
/// Allocate `len` bytes of uninitialized, caller-owned Wasm ABI storage.
|
||||
///
|
||||
/// The returned pointer is aligned for any fundamental C ABI type and must be
|
||||
/// released with `freeBytes` using the same `len`. Returns null when `len` is
|
||||
/// zero or allocation fails.
|
||||
pub fn allocBytes(len: usize) callconv(.c) ?[*]u8 {
|
||||
if (len == 0) return null;
|
||||
return alloc.rawAlloc(len, wasm_alignment, @returnAddress());
|
||||
}
|
||||
|
||||
/// Release storage returned by `allocBytes`.
|
||||
///
|
||||
/// `len` must exactly match the allocation length. A null pointer is ignored.
|
||||
pub fn freeBytes(ptr: ?[*]u8, len: usize) callconv(.c) void {
|
||||
const p = ptr orelse return;
|
||||
if (len == 0) return;
|
||||
alloc.rawFree(p[0..len], wasm_alignment, @returnAddress());
|
||||
}
|
||||
|
||||
/// Allocate a null-initialized slot for an opaque constructor out-parameter.
|
||||
///
|
||||
/// The slot may be reused after each value is removed with `takeOpaque`. It
|
||||
/// must eventually be released with `freeOpaque`.
|
||||
pub fn allocOpaque() callconv(.c) ?*Opaque {
|
||||
const ptr = allocBytes(@sizeOf(Opaque)) orelse return null;
|
||||
const result: *Opaque = @ptrCast(@alignCast(ptr));
|
||||
result.* = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Release a slot returned by `allocOpaque` without freeing its stored handle.
|
||||
///
|
||||
/// Call the handle's type-specific destructor before freeing a populated slot.
|
||||
/// A null slot pointer is ignored.
|
||||
pub fn freeOpaque(ptr: ?*Opaque) callconv(.c) void {
|
||||
freeBytes(@ptrCast(ptr), @sizeOf(Opaque));
|
||||
}
|
||||
|
||||
/// Remove and return the handle in an opaque out-parameter slot.
|
||||
///
|
||||
/// The slot is reset to null so it can be safely reused for another
|
||||
/// constructor. This does not free either the handle or the slot. Returns null
|
||||
/// when the slot pointer is null or the slot is empty.
|
||||
pub fn takeOpaque(ptr: ?*Opaque) callconv(.c) Opaque {
|
||||
const p = ptr orelse return null;
|
||||
const result = p.*;
|
||||
p.* = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
test "Wasm allocation" {
|
||||
const ptr = allocBytes(1) orelse return error.OutOfMemory;
|
||||
defer freeBytes(ptr, 1);
|
||||
|
||||
try std.testing.expectEqual(
|
||||
@as(usize, 0),
|
||||
@intFromPtr(ptr) % c_abi.max_alignment,
|
||||
);
|
||||
ptr[0] = 42;
|
||||
try std.testing.expectEqual(@as(u8, 42), ptr[0]);
|
||||
|
||||
try std.testing.expect(allocBytes(0) == null);
|
||||
freeBytes(null, 0);
|
||||
}
|
||||
|
||||
test "opaque slots are initialized and reusable" {
|
||||
const slot = allocOpaque() orelse return error.OutOfMemory;
|
||||
defer freeOpaque(slot);
|
||||
|
||||
try std.testing.expect(takeOpaque(slot) == null);
|
||||
|
||||
var value: u8 = 42;
|
||||
slot.* = &value;
|
||||
try std.testing.expectEqual(@as(?*anyopaque, &value), takeOpaque(slot));
|
||||
try std.testing.expect(takeOpaque(slot) == null);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//! Memory-layout properties shared with C API callers.
|
||||
//!
|
||||
//! The C ABI defines how values are represented in memory so code written in
|
||||
//! different languages can safely exchange them. One part of that contract is
|
||||
//! alignment: some values must begin at an address divisible by 2, 4, 8, or
|
||||
//! another power of two. Reading a value from a less-aligned address can be
|
||||
//! slow on some CPUs and invalid on others.
|
||||
//!
|
||||
//! This module derives those properties from Zig's compile-time target data.
|
||||
//! It does not call or link libc. Keeping the calculation here gives allocators
|
||||
//! and ABI metadata one source of truth.
|
||||
|
||||
const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
|
||||
/// Largest address alignment required by a fundamental C ABI type, in bytes.
|
||||
///
|
||||
/// For example, a value of 16 means storage intended to hold an arbitrary C
|
||||
/// value must begin at an address evenly divisible by 16. The Wasm allocator
|
||||
/// uses this value for caller-owned ABI storage, and `ghostty_type_json`
|
||||
/// publishes it so hosts know the guarantee made by that allocator.
|
||||
pub const max_alignment: u16 = max: {
|
||||
var result: u16 = @alignOf(*anyopaque);
|
||||
for (std.enums.values(std.Target.CType)) |c_type| {
|
||||
result = @max(result, builtin.target.cTypeAlignment(c_type));
|
||||
}
|
||||
break :max result;
|
||||
};
|
||||
+4
-11
@@ -400,19 +400,12 @@ comptime {
|
||||
|
||||
// On Wasm we need to export our allocator convenience functions.
|
||||
if (builtin.target.cpu.arch.isWasm()) {
|
||||
const alloc = @import("lib/allocator/convenience.zig");
|
||||
const alloc = @import("lib/allocator/wasm.zig");
|
||||
@export(&alloc.allocBytes, .{ .name = "ghostty_wasm_alloc" });
|
||||
@export(&alloc.freeBytes, .{ .name = "ghostty_wasm_free" });
|
||||
@export(&alloc.allocOpaque, .{ .name = "ghostty_wasm_alloc_opaque" });
|
||||
@export(&alloc.freeOpaque, .{ .name = "ghostty_wasm_free_opaque" });
|
||||
@export(&alloc.allocU8Array, .{ .name = "ghostty_wasm_alloc_u8_array" });
|
||||
@export(&alloc.freeU8Array, .{ .name = "ghostty_wasm_free_u8_array" });
|
||||
@export(&alloc.allocU16Array, .{ .name = "ghostty_wasm_alloc_u16_array" });
|
||||
@export(&alloc.freeU16Array, .{ .name = "ghostty_wasm_free_u16_array" });
|
||||
@export(&alloc.allocU8, .{ .name = "ghostty_wasm_alloc_u8" });
|
||||
@export(&alloc.freeU8, .{ .name = "ghostty_wasm_free_u8" });
|
||||
@export(&alloc.allocUsize, .{ .name = "ghostty_wasm_alloc_usize" });
|
||||
@export(&alloc.freeUsize, .{ .name = "ghostty_wasm_free_usize" });
|
||||
@export(&c.wasm_alloc_sgr_attribute, .{ .name = "ghostty_wasm_alloc_sgr_attribute" });
|
||||
@export(&c.wasm_free_sgr_attribute, .{ .name = "ghostty_wasm_free_sgr_attribute" });
|
||||
@export(&alloc.takeOpaque, .{ .name = "ghostty_wasm_take_opaque" });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,8 +114,6 @@ pub const sgr_unknown_full = sgr.unknown_full;
|
||||
pub const sgr_unknown_partial = sgr.unknown_partial;
|
||||
pub const sgr_attribute_tag = sgr.attribute_tag;
|
||||
pub const sgr_attribute_value = sgr.attribute_value;
|
||||
pub const wasm_alloc_sgr_attribute = sgr.wasm_alloc_attribute;
|
||||
pub const wasm_free_sgr_attribute = sgr.wasm_free_attribute;
|
||||
|
||||
pub const key_event_new = key_event.new;
|
||||
pub const key_event_free = key_event.free;
|
||||
|
||||
@@ -126,17 +126,6 @@ pub fn attribute_value(
|
||||
return &attr.value;
|
||||
}
|
||||
|
||||
pub fn wasm_alloc_attribute() callconv(lib.calling_conv) *sgr.Attribute.C {
|
||||
const alloc = std.heap.wasm_allocator;
|
||||
const ptr = alloc.create(sgr.Attribute.C) catch @panic("out of memory");
|
||||
return ptr;
|
||||
}
|
||||
|
||||
pub fn wasm_free_attribute(attr: *sgr.Attribute.C) callconv(lib.calling_conv) void {
|
||||
const alloc = std.heap.wasm_allocator;
|
||||
alloc.destroy(attr);
|
||||
}
|
||||
|
||||
test "alloc" {
|
||||
var p: Parser = undefined;
|
||||
try testing.expectEqual(Result.success, new(
|
||||
|
||||
@@ -60,6 +60,7 @@
|
||||
"environment",
|
||||
"pointer_size",
|
||||
"usize_size",
|
||||
"max_alignment",
|
||||
"endian"
|
||||
],
|
||||
"properties": {
|
||||
@@ -81,6 +82,10 @@
|
||||
"usize_size": {
|
||||
"$ref": "#/$defs/positiveInteger"
|
||||
},
|
||||
"max_alignment": {
|
||||
"description": "Maximum fundamental C ABI alignment in bytes.",
|
||||
"$ref": "#/$defs/positiveInteger"
|
||||
},
|
||||
"endian": {
|
||||
"enum": ["little", "big"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ const std = @import("std");
|
||||
const builtin = @import("builtin");
|
||||
const build_options = @import("terminal_options");
|
||||
const lib = @import("../lib.zig");
|
||||
const c_abi = @import("../../lib/c_abi.zig");
|
||||
|
||||
const color = @import("../color.zig");
|
||||
const clipboard = @import("../clipboard.zig");
|
||||
@@ -377,6 +378,8 @@ const Json = struct {
|
||||
try jws.write(@sizeOf(*anyopaque));
|
||||
try jws.objectField("usize_size");
|
||||
try jws.write(@sizeOf(usize));
|
||||
try jws.objectField("max_alignment");
|
||||
try jws.write(c_abi.max_alignment);
|
||||
try jws.objectField("endian");
|
||||
try jws.write(@tagName(builtin.target.cpu.arch.endian()));
|
||||
try jws.endObject();
|
||||
@@ -843,6 +846,10 @@ test "manifest parses and is versioned" {
|
||||
const root = parsed.value.object;
|
||||
try std.testing.expectEqual(@as(i64, 1), root.get("schema").?.integer);
|
||||
try std.testing.expect(root.contains("abi"));
|
||||
try std.testing.expectEqual(
|
||||
@as(i64, c_abi.max_alignment),
|
||||
root.get("abi").?.object.get("max_alignment").?.integer,
|
||||
);
|
||||
try std.testing.expect(root.contains("library_version"));
|
||||
const manifest_types = root.get("types").?.object;
|
||||
try std.testing.expectEqual(type_decls.len, manifest_types.count());
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* Smoke-test the low-level allocation helpers exported by libghostty-vt Wasm.
|
||||
*
|
||||
* The test instantiates a release artifact, forces linear memory growth,
|
||||
* verifies generic allocation and reusable pointer slots, and checks that
|
||||
* public C structs can be allocated from the ABI manifest.
|
||||
*
|
||||
* Build and run locally with:
|
||||
*
|
||||
* zig build -Demit-lib-vt -Dtarget=wasm32-freestanding -Doptimize=ReleaseSmall
|
||||
* node test/wasm-alloc.mjs zig-out/bin/ghostty-vt.wasm
|
||||
*/
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs/promises";
|
||||
|
||||
const path = process.argv[2];
|
||||
if (path === undefined) {
|
||||
console.error("usage: node test/wasm-alloc.mjs <ghostty-vt.wasm>");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const module = await WebAssembly.compile(await fs.readFile(path));
|
||||
const instance = await WebAssembly.instantiate(module, {});
|
||||
const { exports } = instance;
|
||||
const memory = exports.memory;
|
||||
|
||||
let cachedBuffer = null;
|
||||
let cachedLength = 0;
|
||||
let cachedBytes = null;
|
||||
|
||||
/** Return a byte view over the current linear-memory buffer. */
|
||||
function memoryBytes() {
|
||||
const buffer = memory.buffer;
|
||||
if (buffer !== cachedBuffer || buffer.byteLength !== cachedLength) {
|
||||
cachedBuffer = buffer;
|
||||
cachedLength = buffer.byteLength;
|
||||
cachedBytes = new Uint8Array(buffer);
|
||||
}
|
||||
|
||||
return cachedBytes;
|
||||
}
|
||||
|
||||
// Read target-specific sizes, alignment, and result values from the ABI
|
||||
// manifest so this test does not duplicate properties of the Wasm target.
|
||||
const typeJsonPtr = exports.ghostty_type_json();
|
||||
const typeBytes = memoryBytes();
|
||||
const typeJsonEnd = typeBytes.indexOf(0, typeJsonPtr);
|
||||
assert.notEqual(typeJsonEnd, -1);
|
||||
const typeLayout = JSON.parse(
|
||||
new TextDecoder().decode(typeBytes.subarray(typeJsonPtr, typeJsonEnd)),
|
||||
);
|
||||
const allocationAlignment = typeLayout.abi.max_alignment;
|
||||
const resultValues = typeLayout.types.GhosttyResult.values;
|
||||
|
||||
function check(result) {
|
||||
assert.equal(
|
||||
result,
|
||||
resultValues.SUCCESS,
|
||||
`libghostty-vt call failed with ${result}`,
|
||||
);
|
||||
}
|
||||
|
||||
// A block larger than the current memory guarantees allocator-driven growth.
|
||||
// Verify that the documented lazy view-refresh pattern observes the new buffer.
|
||||
const oldBuffer = memory.buffer;
|
||||
const allocationLength = oldBuffer.byteLength + 1;
|
||||
const allocation = exports.ghostty_wasm_alloc(allocationLength);
|
||||
assert.notEqual(allocation, 0);
|
||||
assert.equal(allocation % allocationAlignment, 0);
|
||||
assert.notEqual(memory.buffer, oldBuffer);
|
||||
|
||||
// Distinct arbitrary sentinels verify that both ends of the allocation are
|
||||
// writable after refreshing the linear-memory view.
|
||||
const firstSentinel = 0x12;
|
||||
const lastSentinel = 0x34;
|
||||
const bytes = memoryBytes();
|
||||
bytes[allocation] = firstSentinel;
|
||||
bytes[allocation + allocationLength - 1] = lastSentinel;
|
||||
assert.equal(bytes[allocation], firstSentinel);
|
||||
assert.equal(bytes[allocation + allocationLength - 1], lastSentinel);
|
||||
exports.ghostty_wasm_free(allocation, allocationLength);
|
||||
assert.equal(exports.ghostty_wasm_alloc(0), 0);
|
||||
|
||||
// A pointer slot starts cleared, remains cleared after a failed constructor,
|
||||
// and can be reused across successful constructors without a DataView read.
|
||||
const slot = exports.ghostty_wasm_alloc_opaque();
|
||||
assert.notEqual(slot, 0);
|
||||
assert.equal(exports.ghostty_wasm_take_opaque(slot), 0);
|
||||
|
||||
const terminalColumns = 80;
|
||||
const terminalRows = 24;
|
||||
assert.equal(
|
||||
exports.ghostty_terminal_new(0, slot, 0, terminalRows),
|
||||
resultValues.INVALID_VALUE,
|
||||
);
|
||||
assert.equal(exports.ghostty_wasm_take_opaque(slot), 0);
|
||||
|
||||
check(exports.ghostty_terminal_new(0, slot, terminalColumns, terminalRows));
|
||||
const terminal = exports.ghostty_wasm_take_opaque(slot);
|
||||
assert.notEqual(terminal, 0);
|
||||
|
||||
check(exports.ghostty_render_state_new(0, slot));
|
||||
const renderState = exports.ghostty_wasm_take_opaque(slot);
|
||||
assert.notEqual(renderState, 0);
|
||||
|
||||
check(exports.ghostty_render_state_row_iterator_new(0, slot));
|
||||
const rowIterator = exports.ghostty_wasm_take_opaque(slot);
|
||||
assert.notEqual(rowIterator, 0);
|
||||
|
||||
check(exports.ghostty_render_state_row_cells_new(0, slot));
|
||||
const rowCells = exports.ghostty_wasm_take_opaque(slot);
|
||||
assert.notEqual(rowCells, 0);
|
||||
assert.equal(exports.ghostty_wasm_take_opaque(slot), 0);
|
||||
|
||||
// Public struct storage uses the generic allocator and exported type layout.
|
||||
assert.equal(
|
||||
typeLayout.abi.pointer_size,
|
||||
typeLayout.types.GhosttyBuffer.fields.ptr.size,
|
||||
);
|
||||
assert.equal(
|
||||
typeLayout.abi.usize_size,
|
||||
typeLayout.types.GhosttyBuffer.fields.len.size,
|
||||
);
|
||||
const sgrAttributeSize = typeLayout.types.GhosttySgrAttribute.size;
|
||||
const sgrAttribute = exports.ghostty_wasm_alloc(sgrAttributeSize);
|
||||
assert.notEqual(sgrAttribute, 0);
|
||||
assert.equal(sgrAttribute % allocationAlignment, 0);
|
||||
exports.ghostty_wasm_free(sgrAttribute, sgrAttributeSize);
|
||||
|
||||
exports.ghostty_render_state_row_cells_free(rowCells);
|
||||
exports.ghostty_render_state_row_iterator_free(rowIterator);
|
||||
exports.ghostty_render_state_free(renderState);
|
||||
exports.ghostty_terminal_free(terminal);
|
||||
exports.ghostty_wasm_free_opaque(slot);
|
||||
|
||||
console.log(`Wasm allocator smoke test passed: ${path}`);
|
||||
Reference in New Issue
Block a user