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
+599
-242
@@ -1,77 +1,77 @@
|
||||
//! 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`.
|
||||
//! JS interop for wasm. A Rust port of the `zig-js` package
|
||||
//! (`mitchellh/zig-js`, `src/{main,ref,value,Object,extern}.zig`) which
|
||||
//! Ghostty's Zig wasm code 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.
|
||||
//! This speaks exactly zig-js's ABI so the module runs with zig-js's own JS
|
||||
//! host (`ZigJS` from `zig-js-glue`; vendored under
|
||||
//! `crates/ghostty-wasm/js/zig-js`):
|
||||
//!
|
||||
//! Imports (module `ghostty_js`):
|
||||
//! - import module `"zig-js"`, functions `valueGet`, `valueSet`,
|
||||
//! `valueObjectCreate`, `valueStringCreate`, `valueStringLen`,
|
||||
//! `valueStringCopy`, `valueDeinit`, `valueNew`, `funcApply` (see [`ffi`]);
|
||||
//! - a JS value crosses the boundary as a 64-bit [`Ref`]: a plain `f64` for
|
||||
//! numbers, otherwise a NaN whose low 32 bits are an id into the host's
|
||||
//! value table and bits 32-34 a type id (see [`Ref`]);
|
||||
//! - the same semantics for [`Value`] (low level) and [`Object`] (high level):
|
||||
//! `get`/`set` only on object refs, `apply`/`new` only on function refs,
|
||||
//! arguments that are not already an [`Object`] are created for the call and
|
||||
//! released after it, results are converted per requested type and released
|
||||
//! unless the type retains the JS value (`Object`/`Value`).
|
||||
//!
|
||||
//! | 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)` |
|
||||
//! The wasm host also needs `memory` assigned on the `ZigJS` instance (that is
|
||||
//! what [`runtime`]`.get::<Object>("memory")` reads).
|
||||
//!
|
||||
//! Differences from the Zig API are only those forced by Rust: generics and
|
||||
//! traits ([`Get`], [`IntoValue`]) instead of `comptime T`, `Drop` instead of
|
||||
//! explicit `deinit()` (use [`Object::into_id`] to hand ownership to the host
|
||||
//! like Zig's `atlas_debug_canvas` does), and a `&[&dyn IntoValue]` argument
|
||||
//! list instead of an anonymous tuple.
|
||||
#![allow(unsafe_code)]
|
||||
|
||||
use std::fmt;
|
||||
|
||||
/// Raw imports, module `"zig-js"` (`extern.zig`).
|
||||
mod ffi {
|
||||
#[link(wasm_import_module = "ghostty_js")]
|
||||
#[link(wasm_import_module = "zig-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);
|
||||
#[link_name = "valueGet"]
|
||||
pub fn value_get(out: *mut u64, id: u32, addr: *const u8, len: usize);
|
||||
#[link_name = "valueSet"]
|
||||
pub fn value_set(id: u32, addr: *const u8, len: usize, ref_ptr: *const u64);
|
||||
#[link_name = "valueObjectCreate"]
|
||||
pub fn value_object_create(out: *mut u64);
|
||||
#[link_name = "valueStringCreate"]
|
||||
pub fn value_string_create(out: *mut u64, addr: *const u8, len: usize);
|
||||
#[link_name = "valueStringLen"]
|
||||
pub fn value_string_len(id: u32) -> u32;
|
||||
#[link_name = "valueStringCopy"]
|
||||
pub fn value_string_copy(id: u32, addr: *mut u8, max: usize);
|
||||
#[link_name = "valueDeinit"]
|
||||
pub fn value_deinit(id: u32);
|
||||
#[link_name = "valueNew"]
|
||||
pub fn value_new(out: *mut u64, id: u32, args_ptr: *const u64, args_len: usize);
|
||||
#[link_name = "funcApply"]
|
||||
pub fn func_apply(
|
||||
out: *mut u64,
|
||||
func: u32,
|
||||
this_ptr: *const u64,
|
||||
args_ptr: *const u64,
|
||||
args_len: usize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Errors from JS interop (`zig-js` `error.JSError` and friends).
|
||||
/// Errors that can occur in this package (`js.Error`).
|
||||
#[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,
|
||||
/// The wrong type for the receiver or the result type can't be assigned
|
||||
/// from the JS type when using the higher-level APIs like [`Object`].
|
||||
InvalidType,
|
||||
}
|
||||
|
||||
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"),
|
||||
}
|
||||
f.write_str("InvalidType")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,229 +79,586 @@ 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);
|
||||
/// Possible types of values (`js.Type`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Type {
|
||||
Undefined,
|
||||
Null,
|
||||
Boolean,
|
||||
Number,
|
||||
Object,
|
||||
String,
|
||||
Symbol,
|
||||
Function,
|
||||
}
|
||||
|
||||
/// An argument for [`Object::call`] / [`Object::new_instance`].
|
||||
/// Ref uniquely identifies a JS value (`ref.zig`). A JS value can't be
|
||||
/// physically copied to the WASM environment so we represent it using a
|
||||
/// 64-bit number into a table that is maintained on the JS side.
|
||||
///
|
||||
/// For a JS number (float64), the ref is literally the float64 (bitcasted).
|
||||
/// For other types, we utilize the unused bits of the IEEE 754 NaN
|
||||
/// representation to encode additional data into the ref. Bits 0-31 are used
|
||||
/// as an ID and bits 32-34 are used as a type ID.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Ref(pub u64);
|
||||
|
||||
/// NaN in IEEE-754 is `0b0111_1111_1111_<anything other than all zeroes>`. We
|
||||
/// always force NaN to have a 1-bit set in the 4th byte from the MSB so that
|
||||
/// we can use the lower 51 bits. This is the top 29 bits of the ref.
|
||||
const NAN_HEAD: u64 = 0x7FF8_0000 >> 3;
|
||||
|
||||
/// The `type_id` field (bits 32-34).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u64)]
|
||||
enum TypeId {
|
||||
Inferred = 0,
|
||||
Object = 1,
|
||||
String = 2,
|
||||
Symbol = 3,
|
||||
Function = 4,
|
||||
}
|
||||
|
||||
impl Ref {
|
||||
/// Predefined refs.
|
||||
pub const NAN: Ref = Ref::predefined(0);
|
||||
pub const NULL: Ref = Ref::predefined(1);
|
||||
pub const TRUE: Ref = Ref::predefined(2);
|
||||
pub const FALSE: Ref = Ref::predefined(3);
|
||||
pub const UNDEFINED: Ref = Ref::predefined(4);
|
||||
pub const GLOBAL: Ref = Ref::predefined(5);
|
||||
pub const RUNTIME: Ref = Ref::predefined(6);
|
||||
|
||||
const fn predefined(id: u32) -> Ref {
|
||||
Ref::new(id, TypeId::Inferred)
|
||||
}
|
||||
|
||||
const fn new(id: u32, type_id: TypeId) -> Ref {
|
||||
Ref((NAN_HEAD << 35) | ((type_id as u64) << 32) | id as u64)
|
||||
}
|
||||
|
||||
/// Bits 0-31.
|
||||
pub const fn id(self) -> u32 {
|
||||
self.0 as u32
|
||||
}
|
||||
|
||||
const fn head(self) -> u64 {
|
||||
self.0 >> 35
|
||||
}
|
||||
|
||||
/// Bits 32-34, `None` for the unused values 5-7.
|
||||
const fn type_id(self) -> Option<TypeId> {
|
||||
match (self.0 >> 32) & 0b111 {
|
||||
0 => Some(TypeId::Inferred),
|
||||
1 => Some(TypeId::Object),
|
||||
2 => Some(TypeId::String),
|
||||
3 => Some(TypeId::Symbol),
|
||||
4 => Some(TypeId::Function),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the type of a ref.
|
||||
pub fn type_of(self) -> Type {
|
||||
// If we aren't a NaN then we have to be a number
|
||||
if self.head() != NAN_HEAD {
|
||||
return Type::Number;
|
||||
}
|
||||
|
||||
match self.type_id() {
|
||||
Some(TypeId::Object) => Type::Object,
|
||||
Some(TypeId::String) => Type::String,
|
||||
Some(TypeId::Symbol) => Type::Symbol,
|
||||
Some(TypeId::Function) => Type::Function,
|
||||
Some(TypeId::Inferred) => match self.id() {
|
||||
0 => Type::Number,
|
||||
1 => Type::Null,
|
||||
2 | 3 => Type::Boolean,
|
||||
4 => Type::Undefined,
|
||||
5 | 6 => Type::Object,
|
||||
// Zig: `unreachable`.
|
||||
_ => Type::Undefined,
|
||||
},
|
||||
None => Type::Undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the ref should be released.
|
||||
///
|
||||
/// We don't need to release raw numbers because they're copied directly
|
||||
/// into our environment. And we don't need to release the predefined
|
||||
/// values because they're always retained.
|
||||
pub fn is_releasable(self) -> bool {
|
||||
self.head() == NAN_HEAD && self.type_id() != Some(TypeId::Inferred)
|
||||
}
|
||||
|
||||
pub fn to_f64(self) -> f64 {
|
||||
debug_assert_eq!(self.type_of(), Type::Number);
|
||||
f64::from_bits(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Only used with [`Value::string`]/[`IntoValue`] to denote a string type
|
||||
/// (`js.String`; named `JsString` to not clash with `std::string::String`).
|
||||
///
|
||||
/// This is NOT a JS string. This is just a sentinel type so that we can
|
||||
/// differentiate a byte slice and a "string" when trying to convert a Rust
|
||||
/// value into a JS value.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Arg<'a> {
|
||||
Object(&'a Object),
|
||||
Str(&'a str),
|
||||
Num(f64),
|
||||
Bool(bool),
|
||||
}
|
||||
pub struct JsString<'a>(&'a [u8]);
|
||||
|
||||
impl<'a> From<&'a Object> for Arg<'a> {
|
||||
fn from(v: &'a Object) -> Self {
|
||||
Arg::Object(v)
|
||||
impl<'a> JsString<'a> {
|
||||
/// `js.String.init`.
|
||||
pub fn init(x: &'a (impl AsRef<[u8]> + ?Sized)) -> Self {
|
||||
JsString(x.as_ref())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for Arg<'a> {
|
||||
fn from(v: &'a str) -> Self {
|
||||
Arg::Str(v)
|
||||
/// Shortcut for [`JsString::init`] since this is a common operation
|
||||
/// (`js.string`).
|
||||
pub fn string(v: &(impl AsRef<[u8]> + ?Sized)) -> JsString<'_> {
|
||||
JsString::init(v)
|
||||
}
|
||||
|
||||
/// Only used to denote an undefined value (`js.Undefined`).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Undefined;
|
||||
|
||||
/// Denotes creating a new empty object (`js.Object{ .value = undefined }`
|
||||
/// passed to `Value.init`).
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct NewObject;
|
||||
|
||||
/// A value represents a JS value (`js.Value`). This is the low-level
|
||||
/// "untyped" interface to any generic JS value. It is more ergonomic to use
|
||||
/// the higher level wrappers such as [`Object`]. It is `Copy` and does not
|
||||
/// release itself; call [`Value::deinit`].
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
pub struct Value(pub u64);
|
||||
|
||||
impl Value {
|
||||
pub const NAN: Value = Value(Ref::NAN.0);
|
||||
pub const NULL: Value = Value(Ref::NULL.0);
|
||||
pub const TRUE: Value = Value(Ref::TRUE.0);
|
||||
pub const FALSE: Value = Value(Ref::FALSE.0);
|
||||
pub const UNDEFINED: Value = Value(Ref::UNDEFINED.0);
|
||||
pub const GLOBAL: Value = Value(Ref::GLOBAL.0);
|
||||
/// Gives access to the "this" value for the ZigJS JS class. This makes it
|
||||
/// possible to access the memory for example.
|
||||
pub const RUNTIME: Value = Value(Ref::RUNTIME.0);
|
||||
|
||||
/// `Value.init` for floats (any numeric type goes through `f64`).
|
||||
pub fn number(x: f64) -> Value {
|
||||
if x.is_nan() {
|
||||
return Value::NAN;
|
||||
}
|
||||
Value(x.to_bits())
|
||||
}
|
||||
|
||||
/// `Value.init` for bools.
|
||||
pub fn boolean(x: bool) -> Value {
|
||||
if x { Value::TRUE } else { Value::FALSE }
|
||||
}
|
||||
|
||||
/// `Value.init(Object{})`: creates a new empty object.
|
||||
pub fn object() -> Value {
|
||||
let mut result = 0u64;
|
||||
// SAFETY: `result` is a valid out pointer.
|
||||
unsafe { ffi::value_object_create(&raw mut result) };
|
||||
Value(result)
|
||||
}
|
||||
|
||||
/// `Value.init(String)`. The bytes are copied to the JS side.
|
||||
pub fn string(s: JsString<'_>) -> Value {
|
||||
let mut result = 0u64;
|
||||
// SAFETY: `s.0` is a live slice (host copies it); `result` is valid.
|
||||
unsafe { ffi::value_string_create(&raw mut result, s.0.as_ptr(), s.0.len()) };
|
||||
Value(result)
|
||||
}
|
||||
|
||||
fn as_ref(self) -> Ref {
|
||||
Ref(self.0)
|
||||
}
|
||||
|
||||
/// Deinitializes the value, allowing the JS environment to GC the value.
|
||||
pub fn deinit(self) {
|
||||
// We avoid releasing values that aren't releasable. This avoids
|
||||
// crossing the js/wasm boundary for a bit of performance.
|
||||
if self.as_ref().is_releasable() {
|
||||
// SAFETY: plain call.
|
||||
unsafe { ffi::value_deinit(self.as_ref().id()) };
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the value of a property of an object.
|
||||
pub fn get(self, n: &str) -> Result<Value> {
|
||||
if self.type_of() != Type::Object {
|
||||
return Err(Error::InvalidType);
|
||||
}
|
||||
let mut result = 0u64;
|
||||
// SAFETY: `n` is live for the call; `result` is a valid out pointer.
|
||||
unsafe { ffi::value_get(&raw mut result, self.as_ref().id(), n.as_ptr(), n.len()) };
|
||||
Ok(Value(result))
|
||||
}
|
||||
|
||||
/// Set the value of a property on an object.
|
||||
pub fn set(self, n: &str, v: Value) -> Result<()> {
|
||||
if self.type_of() != Type::Object {
|
||||
return Err(Error::InvalidType);
|
||||
}
|
||||
// SAFETY: `n` and `v` are live for the call.
|
||||
unsafe { ffi::value_set(self.as_ref().id(), n.as_ptr(), n.len(), &raw const v.0) };
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Call this value as a function.
|
||||
pub fn apply(self, this: Value, args: &[Value]) -> Result<Value> {
|
||||
if self.type_of() != Type::Function {
|
||||
return Err(Error::InvalidType);
|
||||
}
|
||||
let mut result = 0u64;
|
||||
// SAFETY: `Value` is `repr(transparent)` over u64; the slice is live
|
||||
// for the call.
|
||||
unsafe {
|
||||
ffi::func_apply(
|
||||
&raw mut result,
|
||||
self.as_ref().id(),
|
||||
&raw const this.0,
|
||||
args.as_ptr().cast::<u64>(),
|
||||
args.len(),
|
||||
);
|
||||
}
|
||||
Ok(Value(result))
|
||||
}
|
||||
|
||||
/// Call "new" on this value like a constructor.
|
||||
pub fn new_instance(self, args: &[Value]) -> Result<Value> {
|
||||
if self.type_of() != Type::Function {
|
||||
return Err(Error::InvalidType);
|
||||
}
|
||||
let mut result = 0u64;
|
||||
// SAFETY: as in `apply`.
|
||||
unsafe {
|
||||
ffi::value_new(
|
||||
&raw mut result,
|
||||
self.as_ref().id(),
|
||||
args.as_ptr().cast::<u64>(),
|
||||
args.len(),
|
||||
);
|
||||
}
|
||||
Ok(Value(result))
|
||||
}
|
||||
|
||||
/// Returns the bool value if this is a boolean.
|
||||
pub fn as_bool(self) -> Result<bool> {
|
||||
if self.type_of() != Type::Boolean {
|
||||
return Err(Error::InvalidType);
|
||||
}
|
||||
Ok(self == Value::TRUE)
|
||||
}
|
||||
|
||||
/// Returns the float value if this is a number.
|
||||
pub fn float(self) -> Result<f64> {
|
||||
if self.type_of() != Type::Number {
|
||||
return Err(Error::InvalidType);
|
||||
}
|
||||
Ok(f64::from_bits(self.0))
|
||||
}
|
||||
|
||||
/// Returns the UTF-8 encoded string value.
|
||||
pub fn to_bytes(self) -> Result<Vec<u8>> {
|
||||
if self.type_of() != Type::String {
|
||||
return Err(Error::InvalidType);
|
||||
}
|
||||
|
||||
// Get the length and allocate our buffer
|
||||
let id = self.as_ref().id();
|
||||
// SAFETY: plain call.
|
||||
let len = unsafe { ffi::value_string_len(id) } as usize;
|
||||
let mut buf = vec![0u8; len];
|
||||
|
||||
// Copy the string into the buffer
|
||||
// SAFETY: `buf` has `len` writable bytes.
|
||||
unsafe { ffi::value_string_copy(id, buf.as_mut_ptr(), buf.len()) };
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Returns the type of this value.
|
||||
pub fn type_of(self) -> Type {
|
||||
self.as_ref().type_of()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Arg<'_> {
|
||||
fn from(v: f64) -> Self {
|
||||
Arg::Num(v)
|
||||
/// Anything `Value.init` accepts. [`IntoValue::owned`] says whether the
|
||||
/// resulting value is created for the call (and so must be released after it,
|
||||
/// like Zig's `Object.call` does for every argument that isn't a `js.Object`).
|
||||
pub trait IntoValue {
|
||||
fn to_value(&self) -> Value;
|
||||
|
||||
fn owned(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
macro_rules! into_value_number {
|
||||
($($t:ty),*) => {$(
|
||||
impl IntoValue for $t {
|
||||
fn to_value(&self) -> Value {
|
||||
#[allow(clippy::cast_lossless, clippy::unnecessary_cast)]
|
||||
Value::number(*self as f64)
|
||||
}
|
||||
if !matches!(a, Arg::Object(_)) {
|
||||
out.owned.push(h);
|
||||
}
|
||||
out.handles.push(h);
|
||||
}
|
||||
Ok(out)
|
||||
)*};
|
||||
}
|
||||
// Ints, floats and (via `usize`) pointers: all JS numbers are f64.
|
||||
into_value_number!(f64, f32, u8, u16, u32, u64, usize, i8, i16, i32, i64, isize);
|
||||
|
||||
impl IntoValue for bool {
|
||||
fn to_value(&self) -> Value {
|
||||
Value::boolean(*self)
|
||||
}
|
||||
}
|
||||
impl IntoValue for Undefined {
|
||||
fn to_value(&self) -> Value {
|
||||
Value::UNDEFINED
|
||||
}
|
||||
}
|
||||
impl IntoValue for NewObject {
|
||||
fn to_value(&self) -> Value {
|
||||
Value::object()
|
||||
}
|
||||
}
|
||||
impl IntoValue for JsString<'_> {
|
||||
fn to_value(&self) -> Value {
|
||||
Value::string(*self)
|
||||
}
|
||||
}
|
||||
impl IntoValue for Value {
|
||||
fn to_value(&self) -> Value {
|
||||
*self
|
||||
}
|
||||
|
||||
// Deviation: Zig releases a plain `js.Value` argument of `call`/`new`
|
||||
// but not in `set`; we treat a raw `Value` as caller-owned.
|
||||
fn owned(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
impl IntoValue for Object {
|
||||
fn to_value(&self) -> Value {
|
||||
self.value
|
||||
}
|
||||
|
||||
fn owned(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
impl<T: IntoValue + ?Sized> IntoValue for &T {
|
||||
fn to_value(&self) -> Value {
|
||||
(**self).to_value()
|
||||
}
|
||||
|
||||
fn owned(&self) -> bool {
|
||||
(**self).owned()
|
||||
}
|
||||
}
|
||||
|
||||
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) };
|
||||
}
|
||||
/// Result types for [`Object::get`]/[`Object::call`] (zig `Get(T)`).
|
||||
pub trait Get: Sized {
|
||||
/// True if we want to retain the js value.
|
||||
const RETAINS: bool = false;
|
||||
|
||||
/// Convert a non-null/undefined value.
|
||||
fn from_value(v: Value) -> Result<Self>;
|
||||
|
||||
/// What a null/undefined value converts to (`InvalidType` unless the
|
||||
/// type is optional or void).
|
||||
fn from_nullish() -> Result<Self> {
|
||||
Err(Error::InvalidType)
|
||||
}
|
||||
}
|
||||
|
||||
/// `js.global`: the `globalThis` object.
|
||||
pub fn global() -> Result<Object> {
|
||||
// SAFETY: plain call into the host.
|
||||
check(unsafe { ffi::global() })
|
||||
impl Get for () {
|
||||
fn from_value(_: Value) -> Result<Self> {
|
||||
Err(Error::InvalidType)
|
||||
}
|
||||
|
||||
fn from_nullish() -> Result<Self> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl Get for Value {
|
||||
const RETAINS: bool = true;
|
||||
|
||||
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);
|
||||
}
|
||||
fn from_value(v: Value) -> Result<Self> {
|
||||
Ok(v)
|
||||
}
|
||||
}
|
||||
impl Get for Object {
|
||||
const RETAINS: bool = true;
|
||||
|
||||
/// Gets a numeric property.
|
||||
pub fn get_f64(&self, name: &str) -> Result<f64> {
|
||||
Ok(self.get(name)?.as_f64())
|
||||
fn from_value(v: Value) -> Result<Self> {
|
||||
match v.type_of() {
|
||||
Type::Object | Type::Function => Ok(Object { value: v }),
|
||||
_ => Err(Error::InvalidType),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Get for bool {
|
||||
fn from_value(v: Value) -> Result<Self> {
|
||||
v.as_bool()
|
||||
}
|
||||
}
|
||||
impl Get for String {
|
||||
/// zig `js.String` => `[]u8` (here lossily decoded UTF-8).
|
||||
fn from_value(v: Value) -> Result<Self> {
|
||||
Ok(String::from_utf8_lossy(&v.to_bytes()?).into_owned())
|
||||
}
|
||||
}
|
||||
macro_rules! get_number {
|
||||
($($t:ty),*) => {$(
|
||||
impl Get for $t {
|
||||
fn from_value(v: Value) -> Result<Self> {
|
||||
// Zig: `@floatCast` / `@intFromFloat`.
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
Ok(v.float()? as $t)
|
||||
}
|
||||
}
|
||||
)*};
|
||||
}
|
||||
get_number!(f32, f64, u8, u16, u32, u64, usize, i8, i16, i32, i64, isize);
|
||||
|
||||
/// An optional result: null or undefined become `None`.
|
||||
impl<T: Get> Get for Option<T> {
|
||||
const RETAINS: bool = T::RETAINS;
|
||||
|
||||
fn from_value(v: Value) -> Result<Self> {
|
||||
T::from_value(v).map(Some)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
fn from_nullish() -> Result<Self> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets a string property.
|
||||
pub fn get_string(&self, name: &str) -> Result<String> {
|
||||
self.get(name)?.to_string_value()
|
||||
/// Zig `Object.convertValue`.
|
||||
fn convert_value<T: Get>(v: Value) -> Result<T> {
|
||||
let r = match v.type_of() {
|
||||
Type::Null | Type::Undefined => T::from_nullish(),
|
||||
_ => T::from_value(v),
|
||||
};
|
||||
// Release the value unless the result retains it (and conversion worked;
|
||||
// Zig's `errdefer` releases it on error).
|
||||
if !T::RETAINS || r.is_err() {
|
||||
v.deinit();
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// 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) }
|
||||
/// Object is a JS object (`js.Object`). This is a higher-level abstraction
|
||||
/// over [`Value`] and is the recommended interface. It releases the JS value
|
||||
/// on drop (Zig: `deinit()`).
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Object {
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
/// The global "this" value as the high-level Object API (`js.global`).
|
||||
pub fn global() -> Object {
|
||||
Object {
|
||||
value: Value::GLOBAL,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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) }
|
||||
/// The runtime value is the "this" value for the ZigJS class in JS. This is
|
||||
/// useful to directly accessing the `WebAssembly.Memory` property to
|
||||
/// implement things like JS-to-WASM memcpy (`js.runtime`).
|
||||
pub fn runtime() -> Object {
|
||||
Object {
|
||||
value: Value::RUNTIME,
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Object {
|
||||
fn drop(&mut self) {
|
||||
// SAFETY: we own this handle.
|
||||
unsafe { ffi::release(self.0) };
|
||||
self.value.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
impl Object {
|
||||
/// Gives up ownership without releasing, returning the host ref id (Zig
|
||||
/// code does this by reading `ref.id` and skipping `deinit`). The host
|
||||
/// releases it with `ZigJS.deleteValue(id)`.
|
||||
pub fn into_id(self) -> u32 {
|
||||
let id = Ref(self.value.0).id();
|
||||
std::mem::forget(self);
|
||||
id
|
||||
}
|
||||
|
||||
/// Get a property value of type `T` (`get` / `getAlloc`).
|
||||
///
|
||||
/// If the type is `Option`, then null or undefined JS values will return
|
||||
/// `None`. If it is not, null or undefined will return an error.
|
||||
pub fn get<T: Get>(&self, n: &str) -> Result<T> {
|
||||
let v = self.value.get(n)?;
|
||||
convert_value(v)
|
||||
}
|
||||
|
||||
/// Set a value of a property. The type of `v` must be able to turn into
|
||||
/// a value (see [`IntoValue`]).
|
||||
pub fn set(&self, n: &str, v: impl IntoValue) -> Result<()> {
|
||||
let js_value = v.to_value();
|
||||
let r = self.value.set(n, js_value);
|
||||
if v.owned() {
|
||||
js_value.deinit();
|
||||
}
|
||||
r
|
||||
}
|
||||
|
||||
/// Call a function on a object. This will set the "this" parameter to
|
||||
/// the object properly (`call` / `callAlloc`).
|
||||
pub fn call<T: Get>(&self, n: &str, args: &[&dyn IntoValue]) -> Result<T> {
|
||||
// Build our arguments.
|
||||
let js_args = Args::new(args);
|
||||
|
||||
// Invoke
|
||||
let f = self.value.get(n)?;
|
||||
let r = f.apply(self.value, &js_args.values);
|
||||
f.deinit();
|
||||
convert_value(r?)
|
||||
}
|
||||
|
||||
/// Construct an object from this value.
|
||||
pub fn new_instance(&self, args: &[&dyn IntoValue]) -> Result<Object> {
|
||||
let js_args = Args::new(args);
|
||||
let v = self.value.new_instance(&js_args.values)?;
|
||||
Ok(Object { value: v })
|
||||
}
|
||||
}
|
||||
|
||||
/// Converted call arguments; the ones created for the call are released when
|
||||
/// this drops, those already objects are left to the caller.
|
||||
struct Args<'a> {
|
||||
values: Vec<Value>,
|
||||
src: &'a [&'a dyn IntoValue],
|
||||
}
|
||||
|
||||
impl<'a> Args<'a> {
|
||||
fn new(src: &'a [&'a dyn IntoValue]) -> Self {
|
||||
Args {
|
||||
values: src.iter().map(|a| IntoValue::to_value(*a)).collect(),
|
||||
src,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Args<'_> {
|
||||
fn drop(&mut self) {
|
||||
for (v, a) in self.values.iter().zip(self.src) {
|
||||
if a.owned() {
|
||||
v.deinit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user