feat(wasm): use zig-js's exact JS interop ABI
Ghostty's wasm code talks to JavaScript through the zig-js library (mitchellh/zig-js, pinned in build.zig.zon). The first port invented its own import ABI because the dependency wasn't available; now: - ghostty-os/src/wasm/js.rs is a Rust port of zig-js's Zig side (Ref, Value, Object get/set/call/new, global, runtime, string) using its "zig-js" import module, function names and NaN-boxed value refs. - web_canvas face and the atlas debug canvas make the same JS calls as Ghostty's Zig code. - zig-js's unmodified JS host (+ LICENSE) is vendored under crates/ghostty-wasm/js/zig-js and drives the node test. Test Plan: - cargo test -p ghostty-wasm (node >= 24; asserts zig-js imports and no leaked host values)
This commit is contained in:
1 parent
28f55ff2dd
commit
a07b224869
16 files changed
+1532
-479
No files matched your search
@@ -1,7 +1,7 @@
|
||||
//! `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 ghostty_os::wasm::js::{self, Object};
|
||||
|
||||
use crate::{ensure_init, host_bytes};
|
||||
|
||||
@@ -120,40 +120,42 @@ pub extern "C" fn atlas_size(atlas: *const Atlas) -> u32 {
|
||||
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.
|
||||
/// This creates a Canvas element identified by the id returned that the
|
||||
/// caller can draw into the DOM to visualize the atlas. The returned ID must
|
||||
/// be freed from the JS runtime by calling `zigjs.deleteValue`. Returns 0 on
|
||||
/// error (that is also never a valid id: 0 is the predefined `NaN`).
|
||||
#[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(),
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
log::warn!("error dumping atlas canvas err={err}");
|
||||
log::warn!("error dumping atlas canvas err={err:?}");
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn debug_canvas(atlas: &Atlas) -> Result<Object, js::Error> {
|
||||
fn debug_canvas(atlas: &Atlas) -> Result<u32, DebugCanvasError> {
|
||||
// Create our canvas
|
||||
let doc = js::global()?.get_object("document")?;
|
||||
let canvas = doc.call("createElement", &[Arg::Str("canvas")])?;
|
||||
let doc = js::global().get::<Object>("document")?;
|
||||
let canvas = doc.call::<Object>("createElement", &[&js::string("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))?;
|
||||
canvas.set("width", atlas.size)?;
|
||||
canvas.set("height", atlas.size)?;
|
||||
|
||||
let width_str = format!("{}px", atlas.size);
|
||||
let style = canvas.get::<Object>("style")?;
|
||||
style.set("width", js::string(&width_str))?;
|
||||
style.set("height", js::string(&width_str))?;
|
||||
}
|
||||
|
||||
// This will return the same context on subsequent calls.
|
||||
let ctx = canvas.call("getContext", &[Arg::Str("2d")])?;
|
||||
// This will return the same context on subsequent calls so it is
|
||||
// important to reset it.
|
||||
let ctx = canvas.call::<Object>("getContext", &[&js::string("2d")])?;
|
||||
|
||||
// We need to draw pixels so this is format dependent.
|
||||
let buf: Vec<u8> = match atlas.format {
|
||||
@@ -173,24 +175,51 @@ fn debug_canvas(atlas: &Atlas) -> Result<Object, js::Error> {
|
||||
}
|
||||
buf
|
||||
}
|
||||
Format::Bgr => {
|
||||
log::warn!("error dumping atlas canvas err=UnsupportedAtlasFormat");
|
||||
return Err(js::Error::Js);
|
||||
}
|
||||
Format::Bgr => return Err(DebugCanvasError::UnsupportedAtlasFormat),
|
||||
};
|
||||
|
||||
// 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)],
|
||||
)?;
|
||||
let image_data = {
|
||||
// Get our runtime memory
|
||||
let mem = js::runtime().get::<Object>("memory")?;
|
||||
let mem_buf = mem.get::<Object>("buffer")?;
|
||||
|
||||
Ok(canvas)
|
||||
// Create an array that points to our buffer
|
||||
let arr = {
|
||||
let uint8_clamped_array = js::global().get::<Object>("Uint8ClampedArray")?;
|
||||
let arr = uint8_clamped_array.new_instance(&[
|
||||
&mem_buf,
|
||||
&(buf.as_ptr() as usize),
|
||||
&buf.len(),
|
||||
])?;
|
||||
if ghostty_os::wasm::SHARED_MEM {
|
||||
// If we're sharing memory then we have to copy the data since
|
||||
// we can't set ImageData directly using a SharedArrayBuffer.
|
||||
arr.call::<Object>("slice", &[])?
|
||||
} else {
|
||||
arr
|
||||
}
|
||||
};
|
||||
|
||||
// Create the image data from our array
|
||||
let image_data = js::global().get::<Object>("ImageData")?;
|
||||
image_data.new_instance(&[&arr, &atlas.size, &atlas.size])?
|
||||
};
|
||||
|
||||
// Draw it
|
||||
ctx.call::<()>("putImageData", &[&image_data, &0, &0])?;
|
||||
|
||||
Ok(canvas.into_id())
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DebugCanvasError {
|
||||
Js(js::Error),
|
||||
UnsupportedAtlasFormat,
|
||||
}
|
||||
|
||||
impl From<js::Error> for DebugCanvasError {
|
||||
fn from(e: js::Error) -> Self {
|
||||
DebugCanvasError::Js(e)
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,10 @@ pub extern "C" fn face_new(ptr: *const u8, len: usize, pts: u16, p: u16) -> *mut
|
||||
};
|
||||
match Face::init_named(raw, DesiredSize::new(f32::from(pts)), p) {
|
||||
Ok(face) => Box::into_raw(Box::new(face)),
|
||||
Err(_) => std::ptr::null_mut(),
|
||||
Err(err) => {
|
||||
log::warn!("error creating face err={err}");
|
||||
std::ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
//! - module `config`, `config/Wasm.zig` (`config_*`)
|
||||
//! - module `app`, `App.zig` `Wasm` (commented out upstream, nothing exported)
|
||||
//!
|
||||
//! The host provides the `ghostty_js` imports (see
|
||||
//! The host provides the `zig-js` imports (see
|
||||
//! `ghostty_os::wasm::js`) and `env.log`. Strings are passed as
|
||||
//! pointer+length pairs into wasm memory allocated by the host with
|
||||
//! [`wasm_alloc`] / released with [`wasm_free`].
|
||||
|
||||
Reference in new issue
Block a user