diff --git a/runtime/src/delegate_invoke.rs b/runtime/src/delegate_invoke.rs new file mode 100644 index 0000000..2230ca1 --- /dev/null +++ b/runtime/src/delegate_invoke.rs @@ -0,0 +1,320 @@ +//! Typed `Invoke` for WinRT delegates implemented in JS. +//! +//! The JS delegate COM objects (the classic engine's `JsDelegate`, the napi engine's +//! `NapiDelegate`) share one vtable whose Invoke takes up to three pointer-sized arguments and +//! returns nothing. That covers event handlers (`(sender, args)`) but not a delegate such as +//! `Int64 F(Single, Single, Single, Single)`: on x64 and ARM64 floating-point arguments +//! travel in vector registers, which an integer-typed Invoke never reads, and a return value comes +//! back through a trailing out-pointer, which it never writes. +//! +//! For those delegates a libffi closure with the Invoke's exact signature is installed as the +//! delegate's Invoke instead: libffi collects each argument from wherever the ABI put it, and the +//! engine writes the JS function's result into the out-pointer. + +use std::ffi::c_void; + +use libffi::low::{self, ffi_closure, CodePtr}; +pub(crate) use libffi::low::ffi_cif; +use libffi::middle::{Cif, Type}; + +use crate::value::NativeType; + +/// What a delegate's Invoke returns. +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum DelegateReturn { + Void, + /// A number or boolean, written from the JS function's result. + Scalar(NativeType), + /// An object, string or struct: not marshalled; the result slot is set to zero. + Unsupported, +} + +/// A delegate Invoke's in-parameters and return, from WinRT metadata. +#[derive(Clone, Debug)] +pub(crate) struct DelegateSignature { + pub(crate) params: Vec, + pub(crate) ret: DelegateReturn, +} + +impl DelegateSignature { + pub(crate) fn new(params: Vec, ret: DelegateReturn) -> Self { + Self { params, ret } + } + + /// Whether Invoke has to go through [`TypedInvoke`]: a floating-point parameter, more + /// parameters than the shared vtable's Invoke reads, or a value to return. Parameters that + /// span more than one ABI slot (arrays) or are passed by value (structs) keep the shared + /// Invoke, as before. + pub(crate) fn needs_typed_invoke(&self) -> bool { + let single_slot = self.params.iter().all(|p| !matches!(p, NativeType::Buffer | NativeType::Struct(_))); + single_slot + && (self.params.len() > 3 + || self.params.iter().any(|p| matches!(p, NativeType::F32 | NativeType::F64)) + || matches!(self.ret, DelegateReturn::Scalar(_))) + } + + fn has_result_slot(&self) -> bool { + self.ret != DelegateReturn::Void + } +} + +/// Classifies an Invoke return signature. `resolve` maps a signature to its NativeType the way +/// delegate parameters are (named enums become `U32`). +pub(crate) fn delegate_return_for_signature(sig: &str, resolve: impl Fn(&str) -> NativeType) -> DelegateReturn { + if sig.is_empty() || sig == "Void" { + return DelegateReturn::Void; + } + match resolve(sig) { + NativeType::Void => DelegateReturn::Void, + NativeType::Pointer | NativeType::Buffer | NativeType::Function | NativeType::String | NativeType::Struct(_) => { + DelegateReturn::Unsupported + } + scalar => DelegateReturn::Scalar(scalar), + } +} + +/// One Invoke argument as read from its ABI slot. Integers and pointers keep the raw word the +/// shared Invoke has always handed to the per-type conversions. +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum DelegateArg { + Word(usize), + F32(f32), + F64(f64), +} + +/// The JS function's result, converted for the result slot. +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) enum ReturnValue { + Int(i64), + UInt(u64), + Float(f64), + Bool(bool), +} + +/// The callback libffi runs for a typed Invoke. `result` is libffi's integer-wide slot for the +/// HRESULT; `args[0]` points at the delegate (`this`). +pub(crate) type TypedInvokeCallback = unsafe extern "C" fn(cif: &ffi_cif, result: &mut u64, args: *const *const c_void, userdata: &()); + +/// A libffi closure implementing one delegate's Invoke with its real signature. Lives as long as +/// the delegate: its code pointer sits in the delegate's vtable. +pub(crate) struct TypedInvoke { + // The closure keeps a pointer to the CIF, so it must stay alive (and in place) with it. + _cif: Box, + closure: *mut ffi_closure, + code: CodePtr, +} + +impl TypedInvoke { + /// Builds `HRESULT Invoke(this, params..., [result*])` around `callback`. + pub(crate) fn new(signature: &DelegateSignature, callback: TypedInvokeCallback) -> Option { + let mut args = Vec::with_capacity(signature.params.len() + 2); + args.push(Type::pointer()); + for param in &signature.params { + args.push(Type::try_from(param.clone()).ok()?); + } + if signature.has_result_slot() { + args.push(Type::pointer()); + } + let cif = Box::new(Cif::new(args, Type::i32())); + let (closure, code) = low::closure_alloc(); + if closure.is_null() { + return None; + } + let prepared = unsafe { low::prep_closure(closure, cif.as_raw_ptr(), callback, std::ptr::null::<()>(), code) }; + if prepared.is_err() { + unsafe { low::closure_free(closure) }; + return None; + } + Some(Self { _cif: cif, closure, code }) + } + + /// The Invoke entry point to put in the delegate's vtable. + pub(crate) fn code_ptr(&self) -> *const c_void { + self.code.as_ptr() + } +} + +impl Drop for TypedInvoke { + fn drop(&mut self) { + unsafe { low::closure_free(self.closure) }; + } +} + +/// Reads a typed Invoke's `this`, arguments and result slot from libffi's argument array. +/// +/// # Safety +/// `args` must be the argument array libffi passed to a [`TypedInvoke`] callback built from +/// `signature`. +pub(crate) unsafe fn read_invoke_args(args: *const *const c_void, signature: &DelegateSignature) -> (*mut c_void, Vec, *mut c_void) { + let this = *(*args as *const *mut c_void); + let mut values = Vec::with_capacity(signature.params.len()); + for (i, ty) in signature.params.iter().enumerate() { + let slot = *args.add(i + 1); + values.push(match ty { + NativeType::F32 => DelegateArg::F32(*(slot as *const f32)), + NativeType::F64 => DelegateArg::F64(*(slot as *const f64)), + NativeType::Bool | NativeType::U8 => DelegateArg::Word(*(slot as *const u8) as usize), + NativeType::I8 => DelegateArg::Word(*(slot as *const i8) as isize as usize), + NativeType::U16 => DelegateArg::Word(*(slot as *const u16) as usize), + NativeType::I16 => DelegateArg::Word(*(slot as *const i16) as isize as usize), + NativeType::U32 => DelegateArg::Word(*(slot as *const u32) as usize), + NativeType::I32 => DelegateArg::Word(*(slot as *const i32) as isize as usize), + // 64-bit integers, pointers and handles fill the word. + _ => DelegateArg::Word(*(slot as *const usize)), + }); + } + let result = if signature.has_result_slot() { + *(*args.add(signature.params.len() + 1) as *const *mut c_void) + } else { + std::ptr::null_mut() + }; + (this, values, result) +} + +/// Writes the JS result into an Invoke's result slot; `None` (no result, or the function threw) +/// writes zero. +/// +/// # Safety +/// `slot` must be null or the result pointer of an Invoke whose return is `ret`. +pub(crate) unsafe fn write_return(slot: *mut c_void, ret: &DelegateReturn, value: Option) { + if slot.is_null() { + return; + } + let ty = match ret { + DelegateReturn::Void => return, + DelegateReturn::Unsupported => { + *(slot as *mut usize) = 0; + return; + } + DelegateReturn::Scalar(ty) => ty, + }; + let (int, float, boolean) = match value { + Some(ReturnValue::Int(v)) => (v, v as f64, v != 0), + Some(ReturnValue::UInt(v)) => (v as i64, v as f64, v != 0), + Some(ReturnValue::Float(v)) => (float_to_i64(v), v, v != 0.0 && !v.is_nan()), + Some(ReturnValue::Bool(v)) => (v as i64, v as u8 as f64, v), + None => (0, 0.0, false), + }; + match ty { + NativeType::F32 => *(slot as *mut f32) = float as f32, + NativeType::F64 => *(slot as *mut f64) = float, + NativeType::Bool => *(slot as *mut u8) = boolean as u8, + NativeType::U8 | NativeType::I8 => *(slot as *mut u8) = int as u8, + NativeType::U16 | NativeType::I16 => *(slot as *mut u16) = int as u16, + NativeType::U32 | NativeType::I32 => *(slot as *mut u32) = int as u32, + NativeType::U64 => *(slot as *mut u64) = match value { + Some(ReturnValue::UInt(v)) => v, + _ => int as u64, + }, + _ => *(slot as *mut i64) = int, + } +} + +// JS numbers convert to integers the way `ToInt64`-style casts do: truncate toward zero, NaN is 0. +fn float_to_i64(v: f64) -> i64 { + if v.is_nan() { + 0 + } else { + v.trunc() as i64 + } +} + +/// Stores `hr` in libffi's result slot for an `i32` return (integer returns narrower than a word +/// are written word-wide, sign-extended). +pub(crate) fn set_hresult(result: &mut u64, hr: i32) { + *result = hr as i64 as u64; +} + +#[cfg(test)] +mod tests { + use super::*; + + fn measure_signature() -> DelegateSignature { + DelegateSignature::new(vec![NativeType::F32; 4], DelegateReturn::Scalar(NativeType::I64)) + } + + #[test] + fn typed_invoke_is_only_for_signatures_the_shared_invoke_cannot_serve() { + assert!(!DelegateSignature::new(vec![NativeType::Pointer, NativeType::Pointer], DelegateReturn::Void).needs_typed_invoke()); + assert!(!DelegateSignature::new(vec![NativeType::Pointer], DelegateReturn::Unsupported).needs_typed_invoke()); + assert!(measure_signature().needs_typed_invoke()); + assert!(DelegateSignature::new(vec![NativeType::F64], DelegateReturn::Void).needs_typed_invoke()); + assert!(DelegateSignature::new(vec![NativeType::Pointer; 4], DelegateReturn::Void).needs_typed_invoke()); + assert!(DelegateSignature::new(vec![], DelegateReturn::Scalar(NativeType::Bool)).needs_typed_invoke()); + // Arrays and by-value structs keep the shared Invoke. + assert!(!DelegateSignature::new(vec![NativeType::Buffer, NativeType::F32], DelegateReturn::Void).needs_typed_invoke()); + } + + #[test] + fn return_signatures_classify() { + let resolve = crate::helpers::ffi_native_type_from_signature; + assert_eq!(delegate_return_for_signature("Void", resolve), DelegateReturn::Void); + assert_eq!(delegate_return_for_signature("Int64", resolve), DelegateReturn::Scalar(NativeType::I64)); + assert_eq!(delegate_return_for_signature("Single", resolve), DelegateReturn::Scalar(NativeType::F32)); + assert_eq!(delegate_return_for_signature("Boolean", resolve), DelegateReturn::Scalar(NativeType::Bool)); + assert_eq!(delegate_return_for_signature("Object", resolve), DelegateReturn::Unsupported); + assert_eq!(delegate_return_for_signature("String", resolve), DelegateReturn::Unsupported); + } + + struct Seen { + this: usize, + args: Vec, + } + + thread_local! { + static SEEN: std::cell::RefCell> = const { std::cell::RefCell::new(None) }; + } + + // Stands in for an engine: records what it read and returns the packed width/height. + unsafe extern "C" fn record(_cif: &ffi_cif, result: &mut u64, args: *const *const c_void, _userdata: &()) { + let signature = measure_signature(); + let (this, values, slot) = read_invoke_args(args, &signature); + let (w, h) = match (values[0], values[1]) { + (DelegateArg::F32(w), DelegateArg::F32(h)) => (w, h), + _ => (0.0, 0.0), + }; + let packed = ((w.to_bits() as u64) << 32) | h.to_bits() as u64; + write_return(slot, &signature.ret, Some(ReturnValue::UInt(packed))); + SEEN.with(|s| *s.borrow_mut() = Some(Seen { this: this as usize, args: values })); + set_hresult(result, 0); + } + + #[test] + fn typed_invoke_reads_float_arguments_and_writes_the_result() { + let invoke = TypedInvoke::new(&measure_signature(), record).expect("closure"); + // What a C++/WinRT caller of `Int64 F(Single, Single, Single, Single)` calls. + type MeasureInvoke = unsafe extern "system" fn(*mut c_void, f32, f32, f32, f32, *mut i64) -> i32; + let call: MeasureInvoke = unsafe { std::mem::transmute(invoke.code_ptr()) }; + let this = 0x1234usize as *mut c_void; + let mut out: i64 = 0; + let hr = unsafe { call(this, 120.5, f32::NAN, 300.0, -2.0, &mut out) }; + assert_eq!(hr, 0); + let seen = SEEN.with(|s| s.borrow_mut().take()).expect("callback ran"); + assert_eq!(seen.this, 0x1234); + assert!(matches!(seen.args[0], DelegateArg::F32(v) if v == 120.5)); + assert!(matches!(seen.args[1], DelegateArg::F32(v) if v.is_nan())); + assert_eq!(seen.args[2], DelegateArg::F32(300.0)); + assert_eq!(seen.args[3], DelegateArg::F32(-2.0)); + assert_eq!((out as u64 >> 32) as u32, 120.5f32.to_bits()); + assert_eq!(out as u64 as u32, f32::NAN.to_bits()); + } + + #[test] + fn results_convert_to_the_declared_type() { + let mut f: f32 = 0.0; + unsafe { write_return(&mut f as *mut f32 as *mut c_void, &DelegateReturn::Scalar(NativeType::F32), Some(ReturnValue::Float(1.5))) }; + assert_eq!(f, 1.5); + let mut b: u8 = 7; + unsafe { write_return(&mut b as *mut u8 as *mut c_void, &DelegateReturn::Scalar(NativeType::Bool), Some(ReturnValue::Bool(true))) }; + assert_eq!(b, 1); + let mut i: i32 = 7; + unsafe { write_return(&mut i as *mut i32 as *mut c_void, &DelegateReturn::Scalar(NativeType::I32), Some(ReturnValue::Float(-3.9))) }; + assert_eq!(i, -3); + let mut p: usize = 99; + unsafe { write_return(&mut p as *mut usize as *mut c_void, &DelegateReturn::Unsupported, None) }; + assert_eq!(p, 0); + let mut z: i64 = 9; + unsafe { write_return(&mut z as *mut i64 as *mut c_void, &DelegateReturn::Scalar(NativeType::I64), None) }; + assert_eq!(z, 0); + } +} diff --git a/runtime/src/interop_test.rs b/runtime/src/interop_test.rs index 79d11e7..1f7419b 100644 --- a/runtime/src/interop_test.rs +++ b/runtime/src/interop_test.rs @@ -2495,3 +2495,85 @@ fn perf_return_kind_dispatch() { println!("(perf_return_kind result files not written — skipping assertion)"); } } + +/// A JS function behind a delegate whose Invoke takes floats and returns a value +/// (`Int64 F(Single, Single, Single, Single)`), called the way native code calls it. +#[test] +fn js_delegate_with_float_params_and_int64_result() { + use crate::delegate_invoke::{DelegateReturn, DelegateSignature}; + use crate::value::NativeType; + use std::ffi::c_void; + use windows::core::{GUID, HRESULT}; + + let mut runtime = Box::new(Runtime::new(".")); + runtime.register_delegate_isolate_ptr(); + runtime.run_script( + r#" + globalThis.__measure = function (kw, kh, aw, ah) { + globalThis.__seen = [kw, kh, aw, ah].join(','); + if (aw < 0) throw new Error('negative width'); + // Two float32s packed as (widthBits << 32) | heightBits. + const view = new DataView(new ArrayBuffer(8)); + view.setFloat32(0, aw); + view.setFloat32(4, 40.5); + return view.getBigInt64(0); + }; + "#, + "typed_delegate.js", + ); + + let func = { + let context = runtime.global_context().clone(); + v8::scope!(scope, runtime.isolate_mut()); + let context = v8::Local::new(scope, &context); + let scope = &mut v8::ContextScope::new(scope, context); + let key = v8::String::new(scope, "__measure").unwrap(); + let value = context.global(scope).get(scope, key.into()).unwrap(); + v8::Global::new(scope, v8::Local::::try_from(value).unwrap()) + }; + let signature = DelegateSignature::new(vec![NativeType::F32; 4], DelegateReturn::Scalar(NativeType::I64)); + let delegate = crate::new_js_delegate(func, GUID::zeroed(), signature) as *mut crate::JsDelegate; + + type MeasureInvoke = unsafe extern "system" fn(*mut c_void, f32, f32, f32, f32, *mut i64) -> HRESULT; + let invoke: MeasureInvoke = unsafe { std::mem::transmute((*(*delegate).vtable).invoke) }; + + let mut out: i64 = 0; + let hr = unsafe { invoke(delegate as *mut c_void, f32::NAN, 12.25, 300.0, -2.0, &mut out) }; + assert_eq!(hr.0, 0); + assert_eq!(runtime.eval_script_to_string("globalThis.__seen").as_deref(), Some("NaN,12.25,300,-2")); + assert_eq!((out as u64 >> 32) as u32, 300.0f32.to_bits()); + assert_eq!(out as u64 as u32, 40.5f32.to_bits()); + + // A throwing function fails the call and leaves no stale result. + let mut out: i64 = 7; + let hr = unsafe { invoke(delegate as *mut c_void, 1.0, 2.0, -1.0, 4.0, &mut out) }; + assert_eq!(hr.0, 0x80004005u32 as i32); + assert_eq!(out, 0); + + unsafe { crate::js_delegate_release(delegate) }; +} + +/// Event-handler shaped delegates (pointer arguments, no result) keep the shared vtable. +#[test] +fn js_delegate_without_value_types_keeps_the_shared_invoke() { + use crate::delegate_invoke::{DelegateReturn, DelegateSignature}; + use crate::value::NativeType; + use windows::core::GUID; + + let mut runtime = Box::new(Runtime::new(".")); + runtime.register_delegate_isolate_ptr(); + runtime.run_script("globalThis.__handler = function (sender, args) {};", "handler.js"); + let func = { + let context = runtime.global_context().clone(); + v8::scope!(scope, runtime.isolate_mut()); + let context = v8::Local::new(scope, &context); + let scope = &mut v8::ContextScope::new(scope, context); + let key = v8::String::new(scope, "__handler").unwrap(); + let value = context.global(scope).get(scope, key.into()).unwrap(); + v8::Global::new(scope, v8::Local::::try_from(value).unwrap()) + }; + let signature = DelegateSignature::new(vec![NativeType::Pointer, NativeType::Pointer], DelegateReturn::Void); + let delegate = crate::new_js_delegate(func, GUID::zeroed(), signature) as *mut crate::JsDelegate; + assert!(std::ptr::eq(unsafe { (*delegate).vtable }, &crate::JS_DELEGATE_VTBL)); + unsafe { crate::js_delegate_release(delegate) }; +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index c7a49e0..671479c 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -5,6 +5,7 @@ static GLOBAL_ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc; mod class_helpers; mod collection_arg; +mod delegate_invoke; pub mod esm_http; pub(crate) mod dotnet; mod error; @@ -79,6 +80,9 @@ use crate::value::{ }; use crate::value::NativeType; #[cfg(feature = "classic")] +use crate::delegate_invoke::DelegateArg; +use crate::delegate_invoke::{DelegateReturn, DelegateSignature}; +#[cfg(feature = "classic")] use crate::value::NativeValue; use ahash::AHashSet; #[cfg(feature = "classic")] @@ -6001,21 +6005,11 @@ fn create_ns_ctor_object<'a>( None }; if let Some(func) = maybe_func { - if let Some((guid, param_types)) = - js_delegate_params_from_declaration(&*lock, kind) + if let Some((guid, signature)) = + delegate_signature_from_declaration(&*lock, kind) { let global_func = v8::Global::new(scope, func); - let data = Box::new(JsDelegateData { - js_func: global_func, - param_types, - }); - let delegate = Box::new(JsDelegate { - vtable: &JS_DELEGATE_VTBL as *const _, - ref_count: AtomicU32::new(1), - guid, - data: Box::into_raw(data), - }); - let raw = Box::into_raw(delegate) as *mut c_void; + let raw = crate::new_js_delegate(global_func, guid, signature); let result_obj = v8::Object::new(scope); if let Some(key) = v8::String::new(scope, "handle") { @@ -7751,7 +7745,42 @@ pub(crate) static JS_DELEGATE_VTBL: JsDelegateVtbl = JsDelegateVtbl { #[cfg(feature = "classic")] pub(crate) struct JsDelegateData { pub(crate) js_func: v8::Global, - pub(crate) param_types: Vec, + pub(crate) signature: DelegateSignature, + /// For a delegate whose Invoke the shared vtable can't implement (see [`delegate_invoke`]): + /// the typed Invoke and the delegate's own vtable, which points at it. + typed: Option<(delegate_invoke::TypedInvoke, Box)>, +} + +/// Wraps `js_func` as a COM delegate implementing `signature`; returns the IUnknown-compatible +/// pointer (refcount 1, owned by the caller). +#[cfg(feature = "classic")] +pub(crate) fn new_js_delegate(js_func: v8::Global, guid: GUID, signature: DelegateSignature) -> *mut c_void { + let typed = if signature.needs_typed_invoke() { + delegate_invoke::TypedInvoke::new(&signature, js_delegate_typed_invoke).map(|invoke| { + let vtable = Box::new(JsDelegateVtbl { + query_interface: js_delegate_query_interface, + add_ref: js_delegate_add_ref, + release: js_delegate_release, + // Called through the delegate's real signature, never through this type. + invoke: unsafe { std::mem::transmute::<*const c_void, unsafe extern "system" fn(*mut JsDelegate, usize, usize, usize, usize) -> HRESULT>(invoke.code_ptr()) }, + }); + (invoke, vtable) + }) + } else { + None + }; + let vtable: *const JsDelegateVtbl = match &typed { + Some((_, vtable)) => &**vtable, + None => &JS_DELEGATE_VTBL, + }; + let data = Box::new(JsDelegateData { js_func, signature, typed }); + let delegate = Box::new(JsDelegate { + vtable, + ref_count: AtomicU32::new(1), + guid, + data: Box::into_raw(data), + }); + Box::into_raw(delegate) as *mut c_void } #[cfg(feature = "classic")] @@ -7813,7 +7842,17 @@ unsafe extern "system" fn js_delegate_invoke( // Wrap everything in catch_unwind so Rust panics cannot propagate through // the WinRT C++ caller stack (which would be UB and cause CLR FailFast). let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - js_delegate_invoke_inner(this, p0, p1, p2) + js_delegate_invoke_inner( + this, + |signature| { + [p0, p1, p2] + .into_iter() + .take(signature.params.len().min(3)) + .map(DelegateArg::Word) + .collect() + }, + std::ptr::null_mut(), + ) })); match result { Ok(hr) => hr, @@ -7821,8 +7860,33 @@ unsafe extern "system" fn js_delegate_invoke( } } +/// Invoke for delegates with a typed Invoke (see [`delegate_invoke`]): libffi calls this with the +/// arguments read from wherever the ABI put them. #[cfg(feature = "classic")] -fn js_delegate_invoke_inner(this: *mut JsDelegate, p0: usize, p1: usize, p2: usize) -> HRESULT { +unsafe extern "C" fn js_delegate_typed_invoke( + _cif: &delegate_invoke::ffi_cif, + result: &mut u64, + args: *const *const c_void, + _userdata: &(), +) { + let hr = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let this = *(*args as *const *mut JsDelegate); + if this.is_null() || (*this).data.is_null() { + return HRESULT(0x80004005u32 as i32); + } + let (_, values, result_slot) = delegate_invoke::read_invoke_args(args, &(*(*this).data).signature); + js_delegate_invoke_inner(this, |_| values, result_slot) + })) + .unwrap_or(HRESULT(0x80004005u32 as i32)); + delegate_invoke::set_hresult(result, hr.0); +} + +#[cfg(feature = "classic")] +fn js_delegate_invoke_inner( + this: *mut JsDelegate, + args: impl FnOnce(&DelegateSignature) -> Vec, + result_slot: *mut c_void, +) -> HRESULT { if this.is_null() { return HRESULT(0x80004005u32 as i32); } @@ -7833,6 +7897,7 @@ fn js_delegate_invoke_inner(this: *mut JsDelegate, p0: usize, p1: usize, p2: usi } &*data_ptr }; + let args = args(&data.signature); let isolate_ptr = DELEGATE_ISOLATE_PTR.with(|c| c.get()); if isolate_ptr.is_null() { @@ -7872,7 +7937,7 @@ fn js_delegate_invoke_inner(this: *mut JsDelegate, p0: usize, p1: usize, p2: usi }; let context = v8::Local::new(base, &ctx_global); let scope = &mut v8::ContextScope::new(base, context); - js_delegate_run(data, scope, p0, p1, p2) + js_delegate_run(data, scope, &args, result_slot) } else { let isolate: &mut v8::Isolate = unsafe { &mut *isolate_ptr }; v8::scope!(base, isolate); @@ -7882,7 +7947,7 @@ fn js_delegate_invoke_inner(this: *mut JsDelegate, p0: usize, p1: usize, p2: usi }; let context = v8::Local::new(base, &ctx_global); let scope = &mut v8::ContextScope::new(base, context); - js_delegate_run(data, scope, p0, p1, p2) + js_delegate_run(data, scope, &args, result_slot) } } @@ -7892,9 +7957,8 @@ fn js_delegate_invoke_inner(this: *mut JsDelegate, p0: usize, p1: usize, p2: usi fn js_delegate_run( data: &JsDelegateData, scope: &mut v8::PinScope<'_, '_>, - p0: usize, - p1: usize, - p2: usize, + args: &[DelegateArg], + result_slot: *mut c_void, ) -> HRESULT { // TryCatch so JS exceptions don't escape into WinRT C++ frames. v8::tc_scope!(tc, scope); @@ -7902,13 +7966,21 @@ fn js_delegate_run( let func = v8::Local::new(tc, &data.js_func); let recv = v8::undefined(tc); - let params_raw = [p0, p1, p2]; - let n = data.param_types.len().min(3); - let mut js_args: Vec> = Vec::with_capacity(n); + let mut js_args: Vec> = Vec::with_capacity(args.len()); - for i in 0..n { - let raw = params_raw[i] as *mut c_void; - let val: v8::Local = match data.param_types[i] { + for (i, arg) in args.iter().enumerate() { + let raw = match *arg { + DelegateArg::Word(raw) => raw as *mut c_void, + DelegateArg::F32(v) => { + js_args.push(v8::Number::new(tc, v as f64).into()); + continue; + } + DelegateArg::F64(v) => { + js_args.push(v8::Number::new(tc, v).into()); + continue; + } + }; + let val: v8::Local = match data.signature.params[i] { NativeType::Pointer => { if raw.is_null() { v8::null(tc).into() @@ -7962,8 +8034,11 @@ fn js_delegate_run( js_args.push(val); } - let _ = func.call(tc, recv.into(), &js_args); - if tc.has_caught() { + let returned = func.call(tc, recv.into(), &js_args); + let threw = tc.has_caught(); + // Read before the microtask checkpoint below can run more JS. + let value = if threw { None } else { returned.and_then(|v| js_delegate_return_value(tc, v)) }; + if threw { if let Some(ex) = tc.exception() { let msg = ex.to_rust_string_lossy(tc); store_last_js_error(msg); @@ -7975,9 +8050,32 @@ fn js_delegate_run( if !defer_microtask_drain() { tc.perform_microtask_checkpoint(); } + if data.signature.ret != DelegateReturn::Void { + unsafe { delegate_invoke::write_return(result_slot, &data.signature.ret, value) }; + // A delegate the caller expects a value from reports the exception as a failure. + if threw { + return HRESULT(0x80004005u32 as i32); + } + } HRESULT(0) } +/// A JS delegate's result as a scalar: a BigInt keeps all 64 bits (`Int64` results such as a packed +/// width/height pair), numbers and booleans convert, anything else is no result. +#[cfg(feature = "classic")] +fn js_delegate_return_value(scope: &mut v8::PinScope<'_, '_>, value: v8::Local) -> Option { + if let Ok(big) = v8::Local::::try_from(value) { + return Some(delegate_invoke::ReturnValue::Int(big.i64_value().0)); + } + if value.is_boolean() { + return Some(delegate_invoke::ReturnValue::Bool(value.boolean_value(scope))); + } + if value.is_number() { + return value.number_value(scope).map(delegate_invoke::ReturnValue::Float); + } + None +} + /// Resolves the NativeType for a single delegate `Invoke` parameter signature. /// /// Unlike `ffi_native_type_from_signature`, this also resolves named WinRT enum types @@ -8144,6 +8242,78 @@ pub(crate) fn sealed_class_for_type_name( sealed.then(|| (Arc::from(type_name), decl)) } +/// The return of a delegate's `Invoke` (`type_args` substitutes `Var!N` placeholders). +fn delegate_return_from_invoke(method: &MethodDeclaration, type_args: &[String]) -> DelegateReturn { + let Some(meta) = method.metadata() else { + return DelegateReturn::Void; + }; + let sig = Signature::to_string(meta, &method.return_type()); + let placeholder = sig.strip_prefix("Var!").and_then(|n| n.parse::().ok()); + let sig = match placeholder.and_then(|n| type_args.get(n)) { + Some(arg) => arg.clone(), + None => sig, + }; + delegate_invoke::delegate_return_for_signature(&sig, ffi_type_for_delegate_param) +} + +/// The return of the `Invoke` of a delegate declaration. +pub(crate) fn delegate_return_from_declaration(lock: &dyn Declaration, kind: DeclarationKind) -> DelegateReturn { + let invoke = match kind { + DeclarationKind::Delegate => lock.as_any().downcast_ref::().map(|d| d.invoke_method()), + DeclarationKind::GenericDelegate => lock.as_any().downcast_ref::().map(|d| d.invoke_method()), + DeclarationKind::GenericDelegateInstance => lock + .as_any() + .downcast_ref::() + .map(|d| d.invoke_method()), + _ => None, + }; + invoke.map_or(DelegateReturn::Void, |method| delegate_return_from_invoke(method, &[])) +} + +/// The return of the `Invoke` of the delegate named `iid_name` (see [`delegate_info_from_type_sig`]). +pub(crate) fn delegate_return_from_type_sig(iid_name: &str) -> DelegateReturn { + if let Some(open_name) = iid_name.split_once('<').map(|(prefix, _)| prefix) { + let type_args = extract_generic_type_args(iid_name); + let Some(open_decl) = MetadataReader::find_by_name(open_name) else { + return DelegateReturn::Void; + }; + let lock = open_decl.read(); + return lock + .as_any() + .downcast_ref::() + .map_or(DelegateReturn::Void, |d| delegate_return_from_invoke(d.invoke_method(), &type_args)); + } + let Some(decl) = MetadataReader::find_by_name(iid_name) else { + return DelegateReturn::Void; + }; + let lock = decl.read(); + delegate_return_from_declaration(&*lock, lock.kind()) +} + +/// [`delegate_info_from_type_sig`] with the `Invoke` return: what a JS delegate needs to implement +/// the delegate's exact signature. +pub(crate) fn delegate_signature_from_type_sig(iid_name: &str) -> Option<(GUID, DelegateSignature)> { + let (guid, params) = delegate_info_from_type_sig(iid_name)?; + Some((guid, DelegateSignature::new(params, delegate_return_from_type_sig(iid_name)))) +} + +/// [`js_delegate_params_from_declaration`] with the `Invoke` return. +pub(crate) fn delegate_signature_from_declaration(lock: &dyn Declaration, kind: DeclarationKind) -> Option<(GUID, DelegateSignature)> { + let (guid, params) = js_delegate_params_from_declaration(lock, kind)?; + Some((guid, DelegateSignature::new(params, delegate_return_from_declaration(lock, kind)))) +} + +/// [`delegate_info_from_add_method`] with the `Invoke` return (WinRT event handlers return nothing, +/// so this is `Void` in practice). +pub(crate) fn delegate_signature_from_add_method(add_method: &MethodDeclaration) -> Option<(GUID, DelegateSignature)> { + let param = add_method.parameters().first()?; + let iid_name = Signature::to_iid_string(param.metadata()?, ¶m.type_()); + if iid_name.is_empty() { + return None; + } + delegate_signature_from_type_sig(&iid_name) +} + /// Derives the delegate (GUID, param_types) expected by a WinRT event's `add_*` /// method from the method's first parameter type. pub(crate) fn delegate_info_from_add_method( @@ -8190,7 +8360,7 @@ pub(crate) fn handle_as_delegate( return; }; - let Some((guid, param_types)) = delegate_info_from_type_sig(&type_name) else { + let Some((guid, signature)) = delegate_signature_from_type_sig(&type_name) else { throw_js_error( scope, &format!("{} is not a known WinRT delegate type", type_name), @@ -8198,17 +8368,7 @@ pub(crate) fn handle_as_delegate( return; }; - let data = Box::new(JsDelegateData { - js_func: v8::Global::new(scope, func), - param_types, - }); - let delegate = Box::new(JsDelegate { - vtable: &JS_DELEGATE_VTBL as *const _, - ref_count: AtomicU32::new(1), - guid, - data: Box::into_raw(data), - }); - let raw = Box::into_raw(delegate) as *mut c_void; + let raw = crate::new_js_delegate(v8::Global::new(scope, func), guid, signature); let result_obj = v8::Object::new(scope); if let Some(key) = v8::String::new(scope, "handle") { diff --git a/runtime/src/method_call.rs b/runtime/src/method_call.rs index ccc2772..6462494 100644 --- a/runtime/src/method_call.rs +++ b/runtime/src/method_call.rs @@ -55,8 +55,8 @@ pub(crate) enum PointerPlan { TypeName, /// Other struct parameter — serialize field-by-field (declaration pre-resolved). Struct(Arc>), - /// Delegate parameter — wrap a JS function with the precomputed (IID, invoke param types). - Delegate(GUID, Vec), + /// Delegate parameter — wrap a JS function with the precomputed (IID, Invoke signature). + Delegate(GUID, crate::delegate_invoke::DelegateSignature), /// Interface/class parameter — QI the argument to this IID. Interface(GUID), /// `IIterable`, `IVectorView` or `IVector` — a JS array becomes a native collection; @@ -104,10 +104,10 @@ impl PointerPlan { let delegate_info = parameter.metadata().and_then(|meta| { let raw_iid = Signature::to_iid_string(meta, ¶meter.type_()); let iid_name = crate::property_call::substitute_type_vars(&raw_iid, type_args); - crate::delegate_info_from_type_sig(&iid_name) + crate::delegate_signature_from_type_sig(&iid_name) }); match delegate_info { - Some((guid, param_types)) => PointerPlan::Delegate(guid, param_types), + Some((guid, signature)) => PointerPlan::Delegate(guid, signature), None => PointerPlan::Plain, } } @@ -1110,19 +1110,8 @@ impl MethodCall { } else if let Ok(func) = v8::Local::::try_from(value) { - use std::sync::atomic::AtomicU32; - let data = Box::new(crate::JsDelegateData { - js_func: v8::Global::new(scope, func), - param_types: delegate_param_types.clone(), - }); - let delegate = Box::new(crate::JsDelegate { - vtable: &crate::JS_DELEGATE_VTBL as *const _, - ref_count: AtomicU32::new(1), - guid: *guid, - data: Box::into_raw(data), - }); Ok(NativeValue { - pointer: Box::into_raw(delegate) as *mut c_void, + pointer: crate::new_js_delegate(v8::Global::new(scope, func), *guid, delegate_param_types.clone()), }) } else { ffi_parse_pointer_arg(scope, value) diff --git a/runtime/src/napi_engine/delegate.rs b/runtime/src/napi_engine/delegate.rs index 2ac19de..bb999d1 100644 --- a/runtime/src/napi_engine/delegate.rs +++ b/runtime/src/napi_engine/delegate.rs @@ -17,6 +17,7 @@ use std::sync::Arc; use napi::{sys, CallContext, Env, JsFunction, JsUnknown, NapiRaw, NapiValue, ValueType}; use windows::core::{IUnknown, Interface, GUID, HRESULT}; +use crate::delegate_invoke::{self, DelegateArg, DelegateReturn, DelegateSignature}; use crate::napi_engine::ns_proxy::Decl; use crate::value::NativeType; @@ -46,10 +47,13 @@ pub(crate) static NAPI_DELEGATE_VTBL: NapiDelegateVtbl = NapiDelegateVtbl { pub(crate) struct NapiDelegateData { env: sys::napi_env, func_ref: sys::napi_ref, - param_types: Vec, + signature: DelegateSignature, /// Aligned with `param_types`; `Some` for pointer parameters declared as a sealed class. /// Shorter than `param_types` (or empty) means "resolve at invoke time" for the rest. param_classes: Vec, + /// For a delegate whose Invoke the shared vtable can't implement (see [`delegate_invoke`]): + /// the typed Invoke and the delegate's own vtable, which points at it. + typed: Option<(delegate_invoke::TypedInvoke, Box)>, } impl Drop for NapiDelegateData { @@ -80,9 +84,9 @@ pub fn make_napi_delegate( env: &Env, func: &JsFunction, guid: GUID, - param_types: Vec, + signature: DelegateSignature, ) -> Option<*mut c_void> { - make_napi_delegate_typed(env, func, guid, param_types, Vec::new()) + make_napi_delegate_typed(env, func, guid, signature, Vec::new()) } /// The sealed-class declarations of a delegate's Invoke parameters, aligned with the @@ -111,7 +115,7 @@ pub(crate) fn make_napi_delegate_typed( env: &Env, func: &JsFunction, guid: GUID, - param_types: Vec, + signature: DelegateSignature, param_classes: Vec, ) -> Option<*mut c_void> { let mut func_ref: sys::napi_ref = std::ptr::null_mut(); @@ -120,14 +124,35 @@ pub(crate) fn make_napi_delegate_typed( if status != sys::Status::napi_ok || func_ref.is_null() { return None; } + let typed = if signature.needs_typed_invoke() { + delegate_invoke::TypedInvoke::new(&signature, napi_delegate_typed_invoke).map(|invoke| { + let vtable = Box::new(NapiDelegateVtbl { + query_interface: napi_delegate_query_interface, + add_ref: napi_delegate_add_ref, + release: napi_delegate_release, + // Called through the delegate's real signature, never through this type. + invoke: unsafe { + std::mem::transmute::<*const c_void, unsafe extern "system" fn(*mut NapiDelegate, usize, usize, usize, usize) -> HRESULT>(invoke.code_ptr()) + }, + }); + (invoke, vtable) + }) + } else { + None + }; + let vtable: *const NapiDelegateVtbl = match &typed { + Some((_, vtable)) => &**vtable, + None => &NAPI_DELEGATE_VTBL, + }; let data = Box::new(NapiDelegateData { env: env.raw(), func_ref, - param_types, + signature, param_classes, + typed, }); let delegate = Box::new(NapiDelegate { - vtable: &NAPI_DELEGATE_VTBL as *const _, + vtable, ref_count: AtomicU32::new(1), guid, data: Box::into_raw(data), @@ -194,7 +219,17 @@ unsafe extern "system" fn napi_delegate_invoke( ) -> HRESULT { // catch_unwind so Rust panics cannot cross the WinRT C++ caller stack (UB / CLR FailFast). let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - napi_delegate_invoke_inner(this, p0, p1, p2) + napi_delegate_invoke_inner( + this, + |signature| { + [p0, p1, p2] + .into_iter() + .take(signature.params.len().min(3)) + .map(DelegateArg::Word) + .collect() + }, + std::ptr::null_mut(), + ) })); match result { Ok(hr) => hr, @@ -202,7 +237,32 @@ unsafe extern "system" fn napi_delegate_invoke( } } -fn napi_delegate_invoke_inner(this: *mut NapiDelegate, p0: usize, p1: usize, p2: usize) -> HRESULT { +/// Invoke for delegates with a typed Invoke (see [`delegate_invoke`]): libffi calls this with the +/// arguments read from wherever the ABI put them. +unsafe extern "C" fn napi_delegate_typed_invoke( + _cif: &delegate_invoke::ffi_cif, + result: &mut u64, + args: *const *const c_void, + _userdata: &(), +) { + const E_FAIL: HRESULT = HRESULT(0x80004005u32 as i32); + let hr = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let this = *(*args as *const *mut NapiDelegate); + if this.is_null() || (*this).data.is_null() { + return E_FAIL; + } + let (_, values, result_slot) = delegate_invoke::read_invoke_args(args, &(*(*this).data).signature); + napi_delegate_invoke_inner(this, |_| values, result_slot) + })) + .unwrap_or(E_FAIL); + delegate_invoke::set_hresult(result, hr.0); +} + +fn napi_delegate_invoke_inner( + this: *mut NapiDelegate, + args: impl FnOnce(&DelegateSignature) -> Vec, + result_slot: *mut c_void, +) -> HRESULT { const E_FAIL: HRESULT = HRESULT(0x80004005u32 as i32); if this.is_null() { return E_FAIL; @@ -234,17 +294,21 @@ fn napi_delegate_invoke_inner(this: *mut NapiDelegate, p0: usize, p1: usize, p2: return E_FAIL; } - let params_raw = [p0, p1, p2]; - let n = data.param_types.len().min(3); - let mut js_args: [sys::napi_value; 4] = [std::ptr::null_mut(); 4]; - for i in 0..n { - let raw = params_raw[i]; - let class = data.param_classes.get(i).and_then(|c| c.as_ref()); - let val = match delegate_param_to_napi(env, raw, &data.param_types[i], class) { - Some(v) => v, - None => return E_FAIL, + let args = args(&data.signature); + let mut js_args: Vec = Vec::with_capacity(args.len()); + for (i, arg) in args.iter().enumerate() { + let val = match *arg { + DelegateArg::Word(raw) => { + let class = data.param_classes.get(i).and_then(|c| c.as_ref()); + delegate_param_to_napi(env, raw, &data.signature.params[i], class) + } + DelegateArg::F32(v) => napi_double(env, v as f64), + DelegateArg::F64(v) => napi_double(env, v), }; - js_args[i] = val; + match val { + Some(v) => js_args.push(v), + None => return E_FAIL, + } } let mut recv: sys::napi_value = std::ptr::null_mut(); @@ -254,10 +318,11 @@ fn napi_delegate_invoke_inner(this: *mut NapiDelegate, p0: usize, p1: usize, p2: env, recv, func, - n, + js_args.len(), js_args.as_ptr(), &mut call_result, ); + let threw = status != sys::Status::napi_ok; if status != sys::Status::napi_ok { // A JS exception must not escape into WinRT C++ frames: capture it into the // runtime's last-error slot (mirrors the TryCatch in the v8 original). @@ -270,6 +335,14 @@ fn napi_delegate_invoke_inner(this: *mut NapiDelegate, p0: usize, p1: usize, p2: } } } + if data.signature.ret != DelegateReturn::Void { + let value = if threw { None } else { napi_return_value(env, call_result) }; + delegate_invoke::write_return(result_slot, &data.signature.ret, value); + // A delegate the caller expects a value from reports the exception as a failure. + if threw { + return E_FAIL; + } + } HRESULT(0) })(); let _ = sys::napi_close_handle_scope(env, scope); @@ -277,6 +350,39 @@ fn napi_delegate_invoke_inner(this: *mut NapiDelegate, p0: usize, p1: usize, p2: } } +unsafe fn napi_double(env: sys::napi_env, value: f64) -> Option { + let mut out: sys::napi_value = std::ptr::null_mut(); + (sys::napi_create_double(env, value, &mut out) == sys::Status::napi_ok).then_some(out) +} + +/// A JS delegate's result as a scalar: a BigInt keeps all 64 bits (`Int64` results such as a packed +/// width/height pair), numbers and booleans convert, anything else is no result. +unsafe fn napi_return_value(env: sys::napi_env, value: sys::napi_value) -> Option { + if value.is_null() { + return None; + } + let mut ty = sys::ValueType::napi_undefined; + if sys::napi_typeof(env, value, &mut ty) != sys::Status::napi_ok { + return None; + } + match ty { + sys::ValueType::napi_bigint => { + let (mut v, mut lossless) = (0i64, false); + (sys::napi_get_value_bigint_int64(env, value, &mut v, &mut lossless) == sys::Status::napi_ok) + .then_some(delegate_invoke::ReturnValue::Int(v)) + } + sys::ValueType::napi_boolean => { + let mut v = false; + (sys::napi_get_value_bool(env, value, &mut v) == sys::Status::napi_ok).then_some(delegate_invoke::ReturnValue::Bool(v)) + } + sys::ValueType::napi_number => { + let mut v = 0f64; + (sys::napi_get_value_double(env, value, &mut v) == sys::Status::napi_ok).then_some(delegate_invoke::ReturnValue::Float(v)) + } + _ => None, + } +} + /// Convert one raw delegate Invoke parameter to a napi value per its NativeType — builds the /// JS-visible arguments the delegate callback is invoked with. /// @@ -401,14 +507,14 @@ fn native_as_delegate(ctx: &CallContext) -> napi::Result { let func: JsFunction = ctx.get(1)?; - let Some((guid, param_types)) = crate::delegate_info_from_type_sig(&type_name) else { + let Some((guid, signature)) = crate::delegate_signature_from_type_sig(&type_name) else { return Err(napi::Error::from_reason(format!( "__nsAsDelegate: unknown delegate type '{type_name}'" ))); }; - let param_classes = delegate_param_classes(&type_name, ¶m_types); - let Some(ptr) = make_napi_delegate_typed(&ctx.env, &func, guid, param_types, param_classes) + let param_classes = delegate_param_classes(&type_name, &signature.params); + let Some(ptr) = make_napi_delegate_typed(&ctx.env, &func, guid, signature, param_classes) else { return Err(napi::Error::from_reason( "__nsAsDelegate: failed to create native delegate".to_string(), diff --git a/runtime/src/napi_engine/ns_proxy.rs b/runtime/src/napi_engine/ns_proxy.rs index 02e34cc..89f148f 100644 --- a/runtime/src/napi_engine/ns_proxy.rs +++ b/runtime/src/napi_engine/ns_proxy.rs @@ -333,7 +333,7 @@ pub(crate) fn wire_winrt_event_napi( return None; } let func: JsFunction = unsafe { value.cast() }; - let (guid, param_types) = crate::delegate_info_from_add_method(add_method)?; + let (guid, signature) = crate::delegate_signature_from_add_method(add_method)?; // The delegate's IID-form name, derived exactly as `delegate_info_from_add_method` // derives it, keys the parameters' sealed-class declarations so the handler receives // typed arguments without a per-event runtime-class lookup. @@ -344,7 +344,7 @@ pub(crate) fn wire_winrt_event_napi( let iid_name = metadata::signature::Signature::to_iid_string(p.metadata()?, &p.type_()); (!iid_name.is_empty()).then(|| { - crate::napi_engine::delegate::delegate_param_classes(&iid_name, ¶m_types) + crate::napi_engine::delegate::delegate_param_classes(&iid_name, &signature.params) }) }) .unwrap_or_default(); @@ -352,7 +352,7 @@ pub(crate) fn wire_winrt_event_napi( env, &func, guid, - param_types, + signature, param_classes, ) }); diff --git a/runtime/src/ns_proxy.rs b/runtime/src/ns_proxy.rs index a6250b4..e8e857c 100644 --- a/runtime/src/ns_proxy.rs +++ b/runtime/src/ns_proxy.rs @@ -16,9 +16,8 @@ use crate::value::{ NativeValue, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, }; use crate::{ - class_activation_factory, delegate_info_from_add_method, js_delegate_params_from_declaration, - resolve_class_factory_from_parent, throw_js_error, DeclarationFFI, JsDelegate, JsDelegateData, - ReturnKind, JS_DELEGATE_VTBL, + class_activation_factory, resolve_class_factory_from_parent, throw_js_error, DeclarationFFI, + ReturnKind, }; use metadata::declarations::base_class_declaration::BaseClassDeclarationImpl; use metadata::declarations::class_declaration::ClassDeclaration; @@ -390,18 +389,8 @@ pub(crate) fn wire_winrt_event( }); let effective_ptr: Option<*mut c_void> = handle_ptr.or_else(|| { let func = v8::Local::::try_from(value).ok()?; - let (guid, param_types) = delegate_info_from_add_method(add_method)?; - let data = Box::new(JsDelegateData { - js_func: v8::Global::new(scope, func), - param_types, - }); - let delegate = Box::new(JsDelegate { - vtable: &JS_DELEGATE_VTBL as *const _, - ref_count: std::sync::atomic::AtomicU32::new(1), - guid, - data: Box::into_raw(data), - }); - Some(Box::into_raw(delegate) as *mut c_void) + let (guid, signature) = crate::delegate_signature_from_add_method(add_method)?; + Some(crate::new_js_delegate(v8::Global::new(scope, func), guid, signature)) }); if let Some(delegate_ptr) = effective_ptr { @@ -3376,21 +3365,11 @@ pub(crate) fn create_ns_ctor_object<'a>( None }; if let Some(func) = maybe_func { - if let Some((guid, param_types)) = - js_delegate_params_from_declaration(&*lock, kind) + if let Some((guid, signature)) = + crate::delegate_signature_from_declaration(&*lock, kind) { let global_func = v8::Global::new(scope, func); - let data = Box::new(JsDelegateData { - js_func: global_func, - param_types, - }); - let delegate = Box::new(JsDelegate { - vtable: &JS_DELEGATE_VTBL as *const _, - ref_count: std::sync::atomic::AtomicU32::new(1), - guid, - data: Box::into_raw(data), - }); - let raw = Box::into_raw(delegate) as *mut c_void; + let raw = crate::new_js_delegate(global_func, guid, signature); let result_obj = v8::Object::new(scope); if let Some(key) = v8::String::new(scope, "handle") { result_obj.set( diff --git a/runtime/src/property_call.rs b/runtime/src/property_call.rs index e090b0c..ebd7916 100644 --- a/runtime/src/property_call.rs +++ b/runtime/src/property_call.rs @@ -1305,19 +1305,8 @@ impl PropertyCall { if let Some(ptr) = handle_ptr { Ok(NativeValue { pointer: ptr }) } else if let Ok(func) = v8::Local::::try_from(value) { - use std::sync::atomic::AtomicU32; - let data = Box::new(crate::JsDelegateData { - js_func: v8::Global::new(scope, func), - param_types: delegate_param_types.clone(), - }); - let delegate = Box::new(crate::JsDelegate { - vtable: &crate::JS_DELEGATE_VTBL as *const _, - ref_count: AtomicU32::new(1), - guid: *guid, - data: Box::into_raw(data), - }); Ok(NativeValue { - pointer: Box::into_raw(delegate) as *mut c_void, + pointer: crate::new_js_delegate(v8::Global::new(scope, func), *guid, delegate_param_types.clone()), }) } else { ffi_parse_pointer_arg(scope, value)