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:
1 parent
75252a7651
commit
42f05c926d
33 files changed
+2866
-30
No files matched your search
@@ -6,16 +6,51 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ghostty-vt.workspace = true
|
||||
gettext-rs = { version = "0.8.0", features = ["gettext-system"] }
|
||||
log = "0.4.34"
|
||||
thiserror = "2.0.21"
|
||||
|
||||
# Zig's wasm build only compiles `os/wasm.zig`; the POSIX / D-Bus / gettext
|
||||
# dependencies are unavailable on wasm32.
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
gettext-rs = { version = "0.8.0", features = ["gettext-system"] }
|
||||
nix = { version = "0.31.3", features = ["user"] }
|
||||
rustix = { version = "1.1.5", features = ["fs", "process", "system", "pipe", "net", "time", "rand", "pty"] }
|
||||
thiserror = "2.0.21"
|
||||
zbus = "5.19.0"
|
||||
|
||||
[features]
|
||||
# Mirrors Zig's `wasm_shared` build option (SharedArrayBuffer-backed memory).
|
||||
wasm-shared = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
# Same as `[workspace.lints]` except `unsafe_code` is denied instead of
|
||||
# forbidden: the wasm JS import blocks (`src/wasm`) need `unsafe`.
|
||||
[lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
[lints.clippy]
|
||||
pedantic = { level = "warn", priority = -1 }
|
||||
todo = "warn"
|
||||
unwrap_used = "warn"
|
||||
cast_possible_truncation = "allow"
|
||||
cast_possible_wrap = "allow"
|
||||
cast_sign_loss = "allow"
|
||||
cast_precision_loss = "allow"
|
||||
cast_lossless = "allow"
|
||||
module_name_repetitions = "allow"
|
||||
too_many_lines = "allow"
|
||||
missing_errors_doc = "allow"
|
||||
missing_panics_doc = "allow"
|
||||
similar_names = "allow"
|
||||
struct_excessive_bools = "allow"
|
||||
fn_params_excessive_bools = "allow"
|
||||
match_same_arms = "allow"
|
||||
unreadable_literal = "allow"
|
||||
many_single_char_names = "allow"
|
||||
doc_markdown = "allow"
|
||||
must_use_candidate = "allow"
|
||||
return_self_not_must_use = "allow"
|
||||
used_underscore_binding = "allow"
|
||||
large_stack_arrays = "allow"
|
||||
inline_always = "allow"
|
||||
|
||||
[dev-dependencies]
|
||||
temp-env = "0.3.6"
|
||||
@@ -15,38 +15,69 @@
|
||||
)]
|
||||
|
||||
pub mod args;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod cgroup;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod dbus;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod desktop;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod env;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod file;
|
||||
pub mod file_type;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod flatpak;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod homedir;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod hostname;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod i18n;
|
||||
pub mod i18n_locales;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod kernel_info;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod locale;
|
||||
pub mod mouse;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod open;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod passwd;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod path;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod pipe;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod resourcesdir;
|
||||
pub mod shell;
|
||||
pub mod string_encoding;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod systemd;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod temp_dir;
|
||||
pub mod uri;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub mod wasm;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod unsupported;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use unsupported::homedir::expand_home;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub use unsupported::{ResourcesDir, homedir, is_flatpak, passwd, resources_dir, xdg};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod xdg;
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod testutil;
|
||||
|
||||
// Functions and types (mirrors the flat exports of `os/main.zig`).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use dbus::launched_by_dbus_activation;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use desktop::{DesktopEnvironment, desktop_environment, launched_from_desktop};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use env::{
|
||||
EnvMap,
|
||||
append_env,
|
||||
@@ -59,21 +90,32 @@ pub use env::{
|
||||
prepend_env,
|
||||
prepend_env_os,
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use file::{Rlimit, alloc_tmp_dir, fix_max_files, restore_max_files};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use flatpak::{FlatpakHostCommand, is_flatpak};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use homedir::{expand_home, home};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use kernel_info::get_kernel_info;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use locale::ensure_locale;
|
||||
pub use mouse::click_interval;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use open::open;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use pipe::pipe;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use resourcesdir::{ResourcesDir, resources_dir};
|
||||
pub use shell::ShellEscapeWriter;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use systemd::launched_by_systemd;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub use temp_dir::TempDir;
|
||||
|
||||
/// Join path components like `std.fs.path.join` (POSIX): empty components
|
||||
/// are skipped and exactly one separator is placed between components.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn path_join(parts: &[&str]) -> String {
|
||||
let mut out = String::new();
|
||||
for p in parts {
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
//! API-compatible stand-ins for OS facilities that do not exist on wasm32,
|
||||
//! so crates that are also built for wasm (`ghostty-config`) compile
|
||||
//! unchanged. In Zig these are simply never analyzed on wasm
|
||||
//! (`os/passwd.zig` even `@compileError`s); here each reports "unavailable"
|
||||
//! the way the Zig callers' error paths expect.
|
||||
|
||||
use std::io;
|
||||
|
||||
/// `homedir.zig` is unavailable: nothing can be expanded.
|
||||
pub mod homedir {
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum ExpandError {
|
||||
#[error("home detection failed")]
|
||||
HomeDetectionFailed,
|
||||
#[error("buffer too small")]
|
||||
BufferTooSmall,
|
||||
}
|
||||
|
||||
/// Paths without a leading `~/` pass through; others fail.
|
||||
pub fn expand_home(path: &str) -> Result<String, ExpandError> {
|
||||
if path.starts_with("~/") {
|
||||
Err(ExpandError::HomeDetectionFailed)
|
||||
} else {
|
||||
Ok(path.to_owned())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod passwd {
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct Entry {
|
||||
pub shell: Option<String>,
|
||||
pub home: Option<String>,
|
||||
pub name: Option<String>,
|
||||
}
|
||||
|
||||
/// Zig: `@compileError("passwd is not available for wasm")`.
|
||||
pub fn get() -> std::io::Result<Entry> {
|
||||
Err(std::io::Error::new(
|
||||
std::io::ErrorKind::Unsupported,
|
||||
"passwd is not available for wasm",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub mod xdg {
|
||||
use std::ffi::CStr;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct Options<'a> {
|
||||
pub subdir: Option<&'a str>,
|
||||
pub home: Option<&'a str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
#[error("xdg directories are not available on wasm")]
|
||||
Unavailable,
|
||||
}
|
||||
|
||||
pub fn config(_: &Options<'_>) -> Result<String, Error> {
|
||||
Err(Error::Unavailable)
|
||||
}
|
||||
|
||||
pub fn cache(_: &Options<'_>) -> Result<String, Error> {
|
||||
Err(Error::Unavailable)
|
||||
}
|
||||
|
||||
pub fn state(_: &Options<'_>) -> Result<String, Error> {
|
||||
Err(Error::Unavailable)
|
||||
}
|
||||
|
||||
/// No `xdg-terminal-exec` mode on wasm.
|
||||
pub fn parse_terminal_exec<'a>(_: &'a [&'a CStr]) -> Option<&'a [&'a CStr]> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct ResourcesDir {
|
||||
pub app_path: Option<String>,
|
||||
pub host_path: Option<String>,
|
||||
}
|
||||
|
||||
impl ResourcesDir {
|
||||
pub fn app(&self) -> Option<&str> {
|
||||
self.app_path.as_deref()
|
||||
}
|
||||
|
||||
pub fn host(&self) -> Option<&str> {
|
||||
self.host_path.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resources_dir() -> io::Result<ResourcesDir> {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Unsupported,
|
||||
"resources dir is not available on wasm",
|
||||
))
|
||||
}
|
||||
|
||||
pub fn is_flatpak() -> bool {
|
||||
false
|
||||
}
|
||||
@@ -0,0 +1,307 @@
|
||||
//! JS interop for wasm. Stands in for the `zig-js` package
|
||||
//! (`mitchellh/zig-js`) that Ghostty's Zig wasm code (`font/face/web_canvas`,
|
||||
//! `font/Atlas`) imports as `js`.
|
||||
//!
|
||||
//! The `zig-js` source is not part of the Ghostty tree, so its exact import
|
||||
//! names cannot be mirrored; this module defines an equivalent handle based
|
||||
//! protocol in the import module `ghostty_js` and keeps the *Rust-side API*
|
||||
//! shaped like `zig-js` (`Object`, `get`, `set`, `call`, `new`, string and
|
||||
//! number arguments, global object lookup). The host (JS) keeps a table of
|
||||
//! values; wasm only holds `u32` handles. Handle `0` is "invalid" and is
|
||||
//! what every import returns when the JS operation threw.
|
||||
//!
|
||||
//! Imports (module `ghostty_js`):
|
||||
//!
|
||||
//! | import | signature |
|
||||
//! |---|---|
|
||||
//! | `global` | `() -> handle` of `globalThis` |
|
||||
//! | `get` | `(obj, name_ptr, name_len) -> handle` |
|
||||
//! | `set` | `(obj, name_ptr, name_len, value) -> i32` (1 = ok) |
|
||||
//! | `call` | `(obj, name_ptr, name_len, args_ptr, args_len) -> handle` |
|
||||
//! | `new` | `(ctor, args_ptr, args_len) -> handle` |
|
||||
//! | `string` | `(ptr, len) -> handle` (UTF-8 -> JS string) |
|
||||
//! | `number` | `(f64) -> handle` |
|
||||
//! | `boolean` | `(i32) -> handle` |
|
||||
//! | `to_f64` | `(handle) -> f64` |
|
||||
//! | `string_len` | `(handle) -> usize` (UTF-8 byte length) |
|
||||
//! | `string_copy` | `(handle, ptr, cap) -> usize` (writes UTF-8) |
|
||||
//! | `is_nullish` | `(handle) -> i32` |
|
||||
//! | `bytes` | `(ptr, len) -> handle` (a `Uint8ClampedArray` copy of wasm memory) |
|
||||
//! | `copy_bytes` | `(array_handle, dst_ptr, len) -> i32` (copies a `Uint8Array`-like into wasm memory) |
|
||||
//! | `release` | `(handle)` |
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::fmt;
|
||||
|
||||
mod ffi {
|
||||
#[link(wasm_import_module = "ghostty_js")]
|
||||
unsafe extern "C" {
|
||||
pub fn global() -> u32;
|
||||
pub fn get(obj: u32, name: *const u8, len: usize) -> u32;
|
||||
pub fn set(obj: u32, name: *const u8, len: usize, value: u32) -> i32;
|
||||
pub fn call(obj: u32, name: *const u8, len: usize, args: *const u32, argc: usize) -> u32;
|
||||
pub fn new(ctor: u32, args: *const u32, argc: usize) -> u32;
|
||||
pub fn string(ptr: *const u8, len: usize) -> u32;
|
||||
pub fn number(v: f64) -> u32;
|
||||
pub fn boolean(v: i32) -> u32;
|
||||
pub fn to_f64(h: u32) -> f64;
|
||||
pub fn string_len(h: u32) -> usize;
|
||||
pub fn string_copy(h: u32, ptr: *mut u8, cap: usize) -> usize;
|
||||
pub fn is_nullish(h: u32) -> i32;
|
||||
pub fn bytes(ptr: *const u8, len: usize) -> u32;
|
||||
pub fn copy_bytes(h: u32, dst: *mut u8, len: usize) -> i32;
|
||||
pub fn release(h: u32);
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors from JS interop (`zig-js` `error.JSError` and friends).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// The JS operation threw (or the handle was invalid).
|
||||
Js,
|
||||
/// The value is `null`/`undefined` where an object was required.
|
||||
Nullish,
|
||||
/// A returned string was not valid UTF-8.
|
||||
Utf8,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Error::Js => f.write_str("javascript error"),
|
||||
Error::Nullish => f.write_str("value is null or undefined"),
|
||||
Error::Utf8 => f.write_str("invalid utf-8 from javascript"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// A reference to a JS value. Released on drop (zig-js `Object.deinit`).
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Object(u32);
|
||||
|
||||
/// An argument for [`Object::call`] / [`Object::new_instance`].
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Arg<'a> {
|
||||
Object(&'a Object),
|
||||
Str(&'a str),
|
||||
Num(f64),
|
||||
Bool(bool),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Object> for Arg<'a> {
|
||||
fn from(v: &'a Object) -> Self {
|
||||
Arg::Object(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Arg<'a> {
|
||||
fn from(v: &'a str) -> Self {
|
||||
Arg::Str(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Arg<'_> {
|
||||
fn from(v: f64) -> Self {
|
||||
Arg::Num(v)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for Arg<'_> {
|
||||
fn from(v: f32) -> Self {
|
||||
Arg::Num(f64::from(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Arg<'_> {
|
||||
fn from(v: u32) -> Self {
|
||||
Arg::Num(f64::from(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Arg<'_> {
|
||||
fn from(v: bool) -> Self {
|
||||
Arg::Bool(v)
|
||||
}
|
||||
}
|
||||
|
||||
fn check(h: u32) -> Result<Object> {
|
||||
if h == 0 {
|
||||
Err(Error::Js)
|
||||
} else {
|
||||
Ok(Object(h))
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts arguments to handles. Temporaries are released by [`Args::drop`].
|
||||
struct Args {
|
||||
handles: Vec<u32>,
|
||||
owned: Vec<u32>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn new(args: &[Arg<'_>]) -> Result<Args> {
|
||||
let mut out = Args {
|
||||
handles: Vec::with_capacity(args.len()),
|
||||
owned: Vec::new(),
|
||||
};
|
||||
for a in args {
|
||||
let h = match a {
|
||||
Arg::Object(o) => o.0,
|
||||
// SAFETY: pointer/length of a live &str; host copies it.
|
||||
Arg::Str(s) => unsafe { ffi::string(s.as_ptr(), s.len()) },
|
||||
// SAFETY: plain value call.
|
||||
Arg::Num(n) => unsafe { ffi::number(*n) },
|
||||
// SAFETY: plain value call.
|
||||
Arg::Bool(b) => unsafe { ffi::boolean(i32::from(*b)) },
|
||||
};
|
||||
if h == 0 {
|
||||
return Err(Error::Js);
|
||||
}
|
||||
if !matches!(a, Arg::Object(_)) {
|
||||
out.owned.push(h);
|
||||
}
|
||||
out.handles.push(h);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Args {
|
||||
fn drop(&mut self) {
|
||||
for h in &self.owned {
|
||||
// SAFETY: handle was created by us and not yet released.
|
||||
unsafe { ffi::release(*h) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `js.global`: the `globalThis` object.
|
||||
pub fn global() -> Result<Object> {
|
||||
// SAFETY: plain call into the host.
|
||||
check(unsafe { ffi::global() })
|
||||
}
|
||||
|
||||
impl Object {
|
||||
/// Creates a JS string value.
|
||||
pub fn string(s: &str) -> Result<Object> {
|
||||
// SAFETY: pointer/length of a live &str; host copies it.
|
||||
check(unsafe { ffi::string(s.as_ptr(), s.len()) })
|
||||
}
|
||||
|
||||
/// Creates a `Uint8ClampedArray` holding a copy of `data` (what zig-js
|
||||
/// callers built by viewing wasm memory with `new Uint8ClampedArray(
|
||||
/// memory.buffer, ptr, len)`; copying also works for shared memory).
|
||||
pub fn clamped_array(data: &[u8]) -> Result<Object> {
|
||||
// SAFETY: pointer/length of a live slice; host copies it.
|
||||
check(unsafe { ffi::bytes(data.as_ptr(), data.len()) })
|
||||
}
|
||||
|
||||
/// Gives up ownership of the handle without releasing it (the host is
|
||||
/// expected to release it, like zig-js `deleteValue`).
|
||||
pub fn into_raw(self) -> u32 {
|
||||
let h = self.0;
|
||||
std::mem::forget(self);
|
||||
h
|
||||
}
|
||||
|
||||
/// Gets a property as an object reference (also used for primitives,
|
||||
/// which are boxed on the host side).
|
||||
pub fn get(&self, name: &str) -> Result<Object> {
|
||||
// SAFETY: pointer/length of a live &str; host copies it.
|
||||
check(unsafe { ffi::get(self.0, name.as_ptr(), name.len()) })
|
||||
}
|
||||
|
||||
/// Gets a property that must be an object (not null/undefined).
|
||||
pub fn get_object(&self, name: &str) -> Result<Object> {
|
||||
let v = self.get(name)?;
|
||||
if v.is_nullish() {
|
||||
return Err(Error::Nullish);
|
||||
}
|
||||
Ok(v)
|
||||
}
|
||||
|
||||
/// Gets a numeric property.
|
||||
pub fn get_f64(&self, name: &str) -> Result<f64> {
|
||||
Ok(self.get(name)?.as_f64())
|
||||
}
|
||||
|
||||
/// Gets a numeric property as `f32` (zig `get(f32, ...)`).
|
||||
pub fn get_f32(&self, name: &str) -> Result<f32> {
|
||||
Ok(self.get_f64(name)? as f32)
|
||||
}
|
||||
|
||||
/// Gets a string property.
|
||||
pub fn get_string(&self, name: &str) -> Result<String> {
|
||||
self.get(name)?.to_string_value()
|
||||
}
|
||||
|
||||
/// Sets a property.
|
||||
pub fn set(&self, name: &str, value: Arg<'_>) -> Result<()> {
|
||||
let args = Args::new(&[value])?;
|
||||
// SAFETY: pointer/length of a live &str; handle is valid.
|
||||
let ok = unsafe { ffi::set(self.0, name.as_ptr(), name.len(), args.handles[0]) };
|
||||
if ok == 1 { Ok(()) } else { Err(Error::Js) }
|
||||
}
|
||||
|
||||
/// Calls a method and returns the result (which may be nullish).
|
||||
pub fn call(&self, name: &str, args: &[Arg<'_>]) -> Result<Object> {
|
||||
let args = Args::new(args)?;
|
||||
// SAFETY: name and args pointers are live for the call.
|
||||
check(unsafe {
|
||||
ffi::call(
|
||||
self.0,
|
||||
name.as_ptr(),
|
||||
name.len(),
|
||||
args.handles.as_ptr(),
|
||||
args.handles.len(),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// `new this(...args)` (zig-js `Object.new`).
|
||||
pub fn new_instance(&self, args: &[Arg<'_>]) -> Result<Object> {
|
||||
let args = Args::new(args)?;
|
||||
// SAFETY: args pointer is live for the call.
|
||||
check(unsafe { ffi::new(self.0, args.handles.as_ptr(), args.handles.len()) })
|
||||
}
|
||||
|
||||
pub fn is_nullish(&self) -> bool {
|
||||
// SAFETY: plain handle call.
|
||||
unsafe { ffi::is_nullish(self.0) != 0 }
|
||||
}
|
||||
|
||||
/// Numeric value of this object (`Number(v)` on the host).
|
||||
pub fn as_f64(&self) -> f64 {
|
||||
// SAFETY: plain handle call.
|
||||
unsafe { ffi::to_f64(self.0) }
|
||||
}
|
||||
|
||||
/// String value of this object (`String(v)` on the host).
|
||||
pub fn to_string_value(&self) -> Result<String> {
|
||||
// SAFETY: plain handle call.
|
||||
let len = unsafe { ffi::string_len(self.0) };
|
||||
let mut buf = vec![0u8; len];
|
||||
// SAFETY: `buf` has `len` writable bytes.
|
||||
let n = unsafe { ffi::string_copy(self.0, buf.as_mut_ptr(), buf.len()) };
|
||||
buf.truncate(n);
|
||||
String::from_utf8(buf).map_err(|_| Error::Utf8)
|
||||
}
|
||||
|
||||
/// Copies this (`Uint8Array`/`Uint8ClampedArray`) object into `dst`.
|
||||
pub fn copy_bytes_to(&self, dst: &mut [u8]) -> Result<()> {
|
||||
// SAFETY: `dst` is `dst.len()` writable bytes in wasm memory.
|
||||
let ok = unsafe { ffi::copy_bytes(self.0, dst.as_mut_ptr(), dst.len()) };
|
||||
if ok == 1 { Ok(()) } else { Err(Error::Js) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Object {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: we own this handle.
|
||||
unsafe { ffi::release(self.0) };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
//! Freestanding log sink. Port of `ZIG/src/os/wasm/log.zig`.
|
||||
//!
|
||||
//! Formats `"<level>(<scope>): <message>"` (or `"<level>: <message>"` for
|
||||
//! the default scope) and hands it to the host through the `env.log` import,
|
||||
//! exactly like Ghostty's `Freestanding.log`.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use ::log::{Level, LevelFilter, Log, Metadata, Record};
|
||||
|
||||
mod ffi {
|
||||
#[link(wasm_import_module = "env")]
|
||||
unsafe extern "C" {
|
||||
/// `extern "env" fn log(ptr: [*]const u8, len: usize) void`
|
||||
pub fn log(ptr: *const u8, len: usize);
|
||||
}
|
||||
}
|
||||
|
||||
/// Zig `std.log.Level.asText()`.
|
||||
fn level_text(level: Level) -> &'static str {
|
||||
match level {
|
||||
Level::Error => "error",
|
||||
Level::Warn => "warning",
|
||||
Level::Info => "info",
|
||||
Level::Debug | Level::Trace => "debug",
|
||||
}
|
||||
}
|
||||
|
||||
/// Formats a record like the Zig log function does.
|
||||
pub fn format_record(level: Level, scope: &str, args: &std::fmt::Arguments<'_>) -> String {
|
||||
let prefix = if scope.is_empty() || scope == "default" {
|
||||
": ".to_owned()
|
||||
} else {
|
||||
format!("({scope}): ")
|
||||
};
|
||||
format!("{}{prefix}{args}", level_text(level))
|
||||
}
|
||||
|
||||
/// Sends a string to the host.
|
||||
pub fn host_log(s: &str) {
|
||||
// SAFETY: the pointer/length pair describes a live UTF-8 buffer and the
|
||||
// host only reads it for the duration of the call.
|
||||
unsafe { ffi::log(s.as_ptr(), s.len()) }
|
||||
}
|
||||
|
||||
struct Logger;
|
||||
|
||||
impl Log for Logger {
|
||||
fn enabled(&self, _: &Metadata<'_>) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn log(&self, record: &Record<'_>) {
|
||||
host_log(&format_record(
|
||||
record.level(),
|
||||
record.target(),
|
||||
record.args(),
|
||||
));
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
static LOGGER: Logger = Logger;
|
||||
|
||||
/// Installs the logger. Zig's level: debug in Debug, warn in ReleaseSmall,
|
||||
/// info otherwise; here debug with `debug_assertions`, info otherwise.
|
||||
pub fn init() {
|
||||
let level = if cfg!(debug_assertions) {
|
||||
LevelFilter::Debug
|
||||
} else {
|
||||
LevelFilter::Info
|
||||
};
|
||||
if ::log::set_logger(&LOGGER).is_ok() {
|
||||
::log::set_max_level(level);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//! Helpers for wasm compilation. Port of `ZIG/src/os/wasm.zig` and
|
||||
//! `ZIG/src/os/wasm/*.zig`.
|
||||
//!
|
||||
//! Only compiled for `wasm32` (Zig: `@compileError` otherwise). The module is
|
||||
//! organised like the Zig directory:
|
||||
//!
|
||||
//! - [`log`]: the freestanding log sink (`os/wasm/log.zig`).
|
||||
//! - [`target`]: the wasm target platform (`os/wasm/target.zig`).
|
||||
//! - [`js`]: the JS interop layer that stands in for the `zig-js` package
|
||||
//! used by the Zig font code (see that module for the import ABI).
|
||||
|
||||
pub mod js;
|
||||
pub mod log;
|
||||
pub mod target;
|
||||
|
||||
/// True if we're in shared memory mode. If true, then the memory buffer
|
||||
/// in JS will be backed by a SharedArrayBuffer and some behaviors change.
|
||||
/// (Zig: `options.wasm_shared`, here the `wasm-shared` cargo feature.)
|
||||
pub const SHARED_MEM: bool = cfg!(feature = "wasm-shared");
|
||||
|
||||
// The Zig `alloc` (`std.heap.wasm_allocator`) is the Rust global allocator on
|
||||
// wasm32 (`std::alloc::System` is the wasm allocator); nothing to port.
|
||||
@@ -0,0 +1,14 @@
|
||||
//! The wasm target platform. Port of `ZIG/src/os/wasm/target.zig`.
|
||||
//!
|
||||
//! This is used to toggle certain features on and off since the standard
|
||||
//! triple target is often not specific enough (i.e. we can't tell
|
||||
//! wasm32-freestanding is for browser or not).
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Target {
|
||||
Browser,
|
||||
}
|
||||
|
||||
/// Our specific target platform. `None` on non-wasm targets (Zig); this
|
||||
/// module only exists on wasm32 so it is always the browser.
|
||||
pub const TARGET: Option<Target> = Some(Target::Browser);
|
||||
Reference in new issue
Block a user