feat(wasm): port Ghostty's wasm32 build

Ports main_wasm.zig as crates/ghostty-wasm (cdylib exporting the Zig
names: atlas_*, face_*, deferred_face_*, shaper_*, config_*), the
web_canvas font face and shaper (backend selected on wasm32), os/wasm
(logging, target, JS interop) and config/Wasm.zig.

C-library dependencies (freetype, harfbuzz, fontconfig, onig, gettext,
D-Bus, rustix) are target-gated. Like Zig's wasm build, link regex
search (StringMap::search_iterator) is unavailable without Oniguruma;
Collection/SharedGrid/harfbuzz are compiled out on wasm32. ghostty-os
switches from forbid to deny unsafe_code for the JS import blocks.

The JS import ABI is currently a documented handle-based interface with
a reference host (js/ghostty_host.js); aligning it with zig-js's exact
import names follows separately.

Test Plan:
- cargo build --target wasm32-unknown-unknown -p ghostty-font \
  -p ghostty-config -p ghostty-terminal -p ghostty-vt -p ghostty-wasm
- cargo test -p ghostty-wasm (node runs the module against a fake canvas)
This commit is contained in:
ddidderr committed 2026-10-12 05:21:14 +02:00
1 parent 0915f7b935
commit d89691b0ce
33 files changed
+2866 -30

No files matched your search

+196
View File
@@ -0,0 +1,196 @@
//! `font/Atlas.zig` `Wasm`: the wasm-compatible atlas API.
use ghostty_font::atlas::{Atlas, Format, Region};
use ghostty_os::wasm::js::{self, Arg, Object};
use crate::{ensure_init, host_bytes};
/// C layout of `Atlas.Region`.
#[repr(C)]
pub struct RegionC {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl From<RegionC> for Region {
fn from(r: RegionC) -> Region {
Region {
x: r.x,
y: r.y,
width: r.width,
height: r.height,
}
}
}
fn format_from(v: u8) -> Option<Format> {
match v {
0 => Some(Format::Grayscale),
1 => Some(Format::Bgr),
2 => Some(Format::Bgra),
_ => None,
}
}
#[unsafe(no_mangle)]
pub extern "C" fn atlas_new(size: u32, format: u8) -> *mut Atlas {
ensure_init();
let Some(format) = format_from(format) else {
return std::ptr::null_mut();
};
Box::into_raw(Box::new(Atlas::new(size, format)))
}
#[unsafe(no_mangle)]
pub extern "C" fn atlas_free(ptr: *mut Atlas) {
if !ptr.is_null() {
// SAFETY: pointer came from `atlas_new` and is freed once.
drop(unsafe { Box::from_raw(ptr) });
}
}
/// The return value must be freed by the caller with [`region_free`].
#[unsafe(no_mangle)]
pub extern "C" fn atlas_reserve(atlas: *mut Atlas, width: u32, height: u32) -> *mut RegionC {
// SAFETY: the host passes a live pointer from `atlas_new`.
let atlas = unsafe { &mut *atlas };
match atlas.reserve(width, height) {
Ok(r) => Box::into_raw(Box::new(RegionC {
x: r.x,
y: r.y,
width: r.width,
height: r.height,
})),
Err(_) => std::ptr::null_mut(),
}
}
#[unsafe(no_mangle)]
pub extern "C" fn region_free(ptr: *mut RegionC) {
if !ptr.is_null() {
// SAFETY: pointer came from `atlas_reserve` and is freed once.
drop(unsafe { Box::from_raw(ptr) });
}
}
#[unsafe(no_mangle)]
pub extern "C" fn atlas_set(atlas: *mut Atlas, reg: *const RegionC, data: *const u8, len: usize) {
// SAFETY: live pointers from the host; `data` is valid for `len` bytes.
let (atlas, reg, data) = unsafe { (&mut *atlas, &*reg, host_bytes(data, len)) };
atlas.set(
Region {
x: reg.x,
y: reg.y,
width: reg.width,
height: reg.height,
},
data,
);
}
#[unsafe(no_mangle)]
pub extern "C" fn atlas_grow(atlas: *mut Atlas, size_new: u32) -> bool {
// SAFETY: live pointer from `atlas_new`.
unsafe { &mut *atlas }.grow(size_new);
true
}
#[unsafe(no_mangle)]
pub extern "C" fn atlas_clear(atlas: *mut Atlas) {
// SAFETY: live pointer from `atlas_new`.
unsafe { &mut *atlas }.clear();
}
/// Returns a pointer to the atlas pixel data and its length through `len`
/// (host convenience; Zig hosts read `atlas.data` through the struct layout).
#[unsafe(no_mangle)]
pub extern "C" fn atlas_data(atlas: *const Atlas, len: *mut usize) -> *const u8 {
// SAFETY: live pointers from the host.
let (atlas, len) = unsafe { (&*atlas, &mut *len) };
*len = atlas.data.len();
atlas.data.as_ptr()
}
/// The atlas edge length in pixels.
#[unsafe(no_mangle)]
pub extern "C" fn atlas_size(atlas: *const Atlas) -> u32 {
// SAFETY: live pointer from `atlas_new`.
unsafe { &*atlas }.size
}
/// This creates a Canvas element identified by the handle returned that the
/// caller can draw into the DOM to visualize the atlas. The returned handle
/// must be released from the JS runtime (`ghostty_js` release / zigjs
/// `deleteValue`). Returns 0 on error.
#[unsafe(no_mangle)]
pub extern "C" fn atlas_debug_canvas(atlas: *const Atlas) -> u32 {
// SAFETY: live pointer from `atlas_new`.
let atlas = unsafe { &*atlas };
match debug_canvas(atlas) {
Ok(c) => c.into_raw(),
Err(err) => {
log::warn!("error dumping atlas canvas err={err}");
0
}
}
}
fn debug_canvas(atlas: &Atlas) -> Result<Object, js::Error> {
// Create our canvas
let doc = js::global()?.get_object("document")?;
let canvas = doc.call("createElement", &[Arg::Str("canvas")])?;
// Setup our canvas size
{
canvas.set("width", Arg::from(atlas.size))?;
canvas.set("height", Arg::from(atlas.size))?;
let style = canvas.get_object("style")?;
let px = format!("{}px", atlas.size);
style.set("width", Arg::Str(&px))?;
style.set("height", Arg::Str(&px))?;
}
// This will return the same context on subsequent calls.
let ctx = canvas.call("getContext", &[Arg::Str("2d")])?;
// We need to draw pixels so this is format dependent.
let buf: Vec<u8> = match atlas.format {
// Convert from BGRA to RGBA by swapping every R and B.
Format::Bgra => {
let mut buf = atlas.data.clone();
for px in buf.as_chunks_mut::<4>().0 {
px.swap(0, 2);
}
buf
}
// Convert from A8 to RGBA so every 4th byte is set to a value.
Format::Grayscale => {
let mut buf = vec![0u8; atlas.data.len() * 4];
for (i, v) in atlas.data.iter().enumerate() {
buf[i * 4 + 3] = *v;
}
buf
}
Format::Bgr => {
log::warn!("error dumping atlas canvas err=UnsupportedAtlasFormat");
return Err(js::Error::Js);
}
};
// Create an ImageData from our buffer and then write it to the canvas
let arr = Object::clamped_array(&buf)?;
let image_data_ctor = js::global()?.get_object("ImageData")?;
let image_data = image_data_ctor.new_instance(&[
Arg::Object(&arr),
Arg::from(atlas.size),
Arg::from(atlas.size),
])?;
ctx.call(
"putImageData",
&[Arg::Object(&image_data), Arg::Num(0.0), Arg::Num(0.0)],
)?;
Ok(canvas)
}