lib-vt: wasm convenience functions and a simple example (#9309)

This adds a set of Wasm convenience functions to ease memory management.
These are all prefixed with `ghostty_wasm` and are documented as part of
the standard Doxygen docs.

I also added a very simple single-page HTML example that demonstrates
how to use the Wasm module for key encoding.

This also adds a bunch of safety checks to the C API to verify that
valid values are actually passed to the function. This is an easy to hit
bug.

**AI disclosure:** The example is AI-written with Amp. I read through
all the code and understand it but I can't claim there isn't a better
way, I'm far from a JS expert. It is simple and works currently though.
Happy to see improvements if anyone wants to contribute.
This commit is contained in:
Mitchell Hashimoto
2025-10-22 14:25:52 -07:00
committed by GitHub
parent 9dc2e5978f
commit c133fac7e7
13 changed files with 1027 additions and 2 deletions

View File

@@ -77,7 +77,7 @@ pub fn encode(
event: key.KeyEvent,
opts: Options,
) std.Io.Writer.Error!void {
//std.log.warn("KEYENCODER event={} opts={}", .{ event, opts });
std.log.warn("KEYENCODER event={} opts={}", .{ event, opts });
return if (opts.kitty_flags.int() != 0) try kitty(
writer,
event,

View File

@@ -2,6 +2,9 @@ const std = @import("std");
const builtin = @import("builtin");
const testing = std.testing;
/// Convenience functions
pub const convenience = @import("allocator/convenience.zig");
/// Useful alias since they're required to create Zig allocators
pub const ZigVTable = std.mem.Allocator.VTable;

View File

@@ -0,0 +1,50 @@
//! 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 allocBuffer(len: usize) callconv(.c) ?[*]u8 {
const slice = alloc.alloc(u8, len) catch return null;
return slice.ptr;
}
pub fn freeBuffer(ptr: ?[*]u8, 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);
}

View File

@@ -126,6 +126,19 @@ comptime {
@export(&c.key_encoder_setopt, .{ .name = "ghostty_key_encoder_setopt" });
@export(&c.key_encoder_encode, .{ .name = "ghostty_key_encoder_encode" });
@export(&c.paste_is_safe, .{ .name = "ghostty_paste_is_safe" });
// On Wasm we need to export our allocator convenience functions.
if (builtin.target.cpu.arch.isWasm()) {
const alloc = @import("lib/allocator/convenience.zig");
@export(&alloc.allocOpaque, .{ .name = "ghostty_wasm_alloc_opaque" });
@export(&alloc.freeOpaque, .{ .name = "ghostty_wasm_free_opaque" });
@export(&alloc.allocBuffer, .{ .name = "ghostty_wasm_alloc_buffer" });
@export(&alloc.freeBuffer, .{ .name = "ghostty_wasm_free_buffer" });
@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" });
}
}
}

View File

@@ -10,6 +10,8 @@ const OptionAsAlt = @import("../../input/config.zig").OptionAsAlt;
const Result = @import("result.zig").Result;
const KeyEvent = @import("key_event.zig").Event;
const log = std.log.scoped(.key_encode);
/// Wrapper around key encoding options that tracks the allocator for C API usage.
const KeyEncoderWrapper = struct {
opts: key_encode.Options,
@@ -70,6 +72,13 @@ pub fn setopt(
option: Option,
value: ?*const anyopaque,
) callconv(.c) void {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(Option, @intFromEnum(option)) catch {
log.warn("setopt invalid option value={d}", .{@intFromEnum(option)});
return;
};
}
return switch (option) {
inline else => |comptime_option| setoptTyped(
encoder_,
@@ -95,7 +104,15 @@ fn setoptTyped(
const bits: u5 = @truncate(value.*);
break :flags @bitCast(bits);
},
.macos_option_as_alt => opts.macos_option_as_alt = value.*,
.macos_option_as_alt => {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(OptionAsAlt, @intFromEnum(value.*)) catch {
log.warn("setopt invalid OptionAsAlt value={d}", .{@intFromEnum(value.*)});
return;
};
}
opts.macos_option_as_alt = value.*;
},
}
}

View File

@@ -6,6 +6,8 @@ const CAllocator = lib_alloc.Allocator;
const key = @import("../../input/key.zig");
const Result = @import("result.zig").Result;
const log = std.log.scoped(.key_event);
/// Wrapper around KeyEvent that tracks the allocator for C API usage.
/// The UTF-8 text is not owned by this wrapper - the caller is responsible
/// for ensuring the lifetime of any UTF-8 text set via set_utf8.
@@ -36,6 +38,13 @@ pub fn free(event_: Event) callconv(.c) void {
}
pub fn set_action(event_: Event, action: key.Action) callconv(.c) void {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(key.Action, @intFromEnum(action)) catch {
log.warn("set_action invalid action value={d}", .{@intFromEnum(action)});
return;
};
}
const event: *key.KeyEvent = &event_.?.event;
event.action = action;
}
@@ -46,6 +55,13 @@ pub fn get_action(event_: Event) callconv(.c) key.Action {
}
pub fn set_key(event_: Event, k: key.Key) callconv(.c) void {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(key.Key, @intFromEnum(k)) catch {
log.warn("set_key invalid key value={d}", .{@intFromEnum(k)});
return;
};
}
const event: *key.KeyEvent = &event_.?.event;
event.key = k;
}

View File

@@ -6,6 +6,8 @@ const CAllocator = lib_alloc.Allocator;
const osc = @import("../osc.zig");
const Result = @import("result.zig").Result;
const log = std.log.scoped(.osc);
/// C: GhosttyOscParser
pub const Parser = ?*osc.Parser;
@@ -68,6 +70,13 @@ pub fn commandData(
data: CommandData,
out: ?*anyopaque,
) callconv(.c) bool {
if (comptime std.debug.runtime_safety) {
_ = std.meta.intToEnum(CommandData, @intFromEnum(data)) catch {
log.warn("commandData invalid data value={d}", .{@intFromEnum(data)});
return false;
};
}
return switch (data) {
inline else => |comptime_data| commandDataTyped(
command_,