Files
ghostty-rs/crates/ghostty-vt-capi/src/wasm.rs
T
ddidderr 0915f7b935 feat(vt-capi): port the libghostty-vt C API
Ports ZIG/src/lib_vt.zig and terminal/c/* as crates/ghostty-vt-capi
(cdylib + staticlib + rlib, soname libghostty-vt.so) implementing the
unchanged include/ghostty/vt.h + vt/*.h ABI: OSC parser, SGR parser,
key events and key encoding, paste safety, colors, result codes - all
38 native symbols, plus the ghostty_wasm_* helpers on wasm32.

Verification:
- tests/abi.rs generates a C probe from the headers, compiled with cc,
  that checks sizeof/alignof/offsetof of every struct/union, every
  enumerator and #define against Rust, and links every declared symbol;
  nm -D exports equal the declared functions.
- Ghostty's example/c-vt* programs compile unchanged against the Rust
  library (shared and static) and produce the expected output.
- The wasm32 build runs Ghostty's wasm-key-encode/wasm-sgr example
  scripts unmodified under node (upstream's wasm-sgr tag table is stale
  vs sgr.h; a regenerated copy checks full output).

Test Plan:
- cargo test -p ghostty-vt-capi (22 unit + 2 ABI + 5 C + 3 wasm tests)
2026-10-12 05:16:41 +02:00

150 lines
5.1 KiB
Rust

//! Wasm allocation helpers (ZIG/src/lib/allocator/convenience.zig and
//! `wasm_alloc_attribute` in ZIG/src/terminal/c/sgr.zig,
//! `include/ghostty/vt/wasm.h`).
//!
//! JavaScript hosts cannot easily create the pointers the C API wants, so
//! upstream exports these helpers on wasm. They always use the default
//! allocator. They are exported (`no_mangle`) only when compiling for wasm.
#![allow(unsafe_code)]
use std::{alloc::Layout, ffi::c_void};
use crate::sgr::SgrAttribute;
fn alloc_layout(layout: Layout) -> *mut u8 {
if layout.size() == 0 {
return layout.align() as *mut u8;
}
// SAFETY: non-zero size.
unsafe { std::alloc::alloc(layout) }
}
/// # Safety
/// `ptr` must be null or come from `alloc_layout` with the same `layout`.
unsafe fn free_layout(ptr: *mut u8, layout: Layout) {
if !ptr.is_null() && layout.size() != 0 {
// SAFETY: per the contract.
unsafe { std::alloc::dealloc(ptr, layout) };
}
}
/// Allocate one `void *` (e.g. an out-parameter for `*_new`).
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_opaque() -> *mut *mut c_void {
alloc_layout(Layout::new::<*mut c_void>()).cast()
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_opaque`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_opaque(ptr: *mut *mut c_void) {
// SAFETY: per the contract.
unsafe { free_layout(ptr.cast(), Layout::new::<*mut c_void>()) };
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_u8_array(len: usize) -> *mut u8 {
Layout::array::<u8>(len).map_or(std::ptr::null_mut(), alloc_layout)
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_u8_array(len)`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_u8_array(ptr: *mut u8, len: usize) {
if let Ok(l) = Layout::array::<u8>(len) {
// SAFETY: per the contract.
unsafe { free_layout(ptr, l) };
}
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_u16_array(len: usize) -> *mut u16 {
Layout::array::<u16>(len).map_or(std::ptr::null_mut(), |l| alloc_layout(l).cast())
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_u16_array(len)`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_u16_array(ptr: *mut u16, len: usize) {
if let Ok(l) = Layout::array::<u16>(len) {
// SAFETY: per the contract.
unsafe { free_layout(ptr.cast(), l) };
}
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_u8() -> *mut u8 {
alloc_layout(Layout::new::<u8>())
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_u8`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_u8(ptr: *mut u8) {
// SAFETY: per the contract.
unsafe { free_layout(ptr, Layout::new::<u8>()) };
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_usize() -> *mut usize {
alloc_layout(Layout::new::<usize>()).cast()
}
/// # Safety
/// `ptr` must be null or from `ghostty_wasm_alloc_usize`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_usize(ptr: *mut usize) {
// SAFETY: per the contract.
unsafe { free_layout(ptr.cast(), Layout::new::<usize>()) };
}
/// Allocate a `GhosttySgrAttribute` (zero-initialized).
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn ghostty_wasm_alloc_sgr_attribute() -> *mut SgrAttribute {
let p = alloc_layout(Layout::new::<SgrAttribute>()).cast::<SgrAttribute>();
if !p.is_null() {
// SAFETY: freshly allocated, correctly sized; all-zero is a valid value.
unsafe { p.write_bytes(0, 1) };
}
p
}
/// # Safety
/// `attr` must be null or from `ghostty_wasm_alloc_sgr_attribute`.
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn ghostty_wasm_free_sgr_attribute(attr: *mut SgrAttribute) {
// SAFETY: per the contract.
unsafe { free_layout(attr.cast(), Layout::new::<SgrAttribute>()) };
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn alloc_and_free_round_trip() {
// SAFETY: pointers come from the matching allocators.
unsafe {
let p = ghostty_wasm_alloc_u8_array(16);
assert!(!p.is_null());
p.write_bytes(7, 16);
ghostty_wasm_free_u8_array(p, 16);
let z = ghostty_wasm_alloc_u8_array(0);
ghostty_wasm_free_u8_array(z, 0);
let u = ghostty_wasm_alloc_u16_array(5);
ghostty_wasm_free_u16_array(u, 5);
let o = ghostty_wasm_alloc_opaque();
ghostty_wasm_free_opaque(o);
let b = ghostty_wasm_alloc_u8();
ghostty_wasm_free_u8(b);
let s = ghostty_wasm_alloc_usize();
ghostty_wasm_free_usize(s);
let a = ghostty_wasm_alloc_sgr_attribute();
assert_eq!((*a).tag, 0);
ghostty_wasm_free_sgr_attribute(a);
}
}
}