From e4fc6f8700edde0dcb9cb266ffe4a7e1bd021efa Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Mon, 28 Sep 2026 07:18:19 -0400 Subject: [PATCH] feat: JS delegates with value-type parameters and return values JS delegates shared one vtable whose Invoke takes up to three pointer-sized arguments and returns nothing. That covers event handlers, but a delegate such as Int64 F(Single, Single, Single, Single) got garbage arguments (floats travel in vector registers on x64 and ARM64) and its result was never written back. Delegates whose Invoke takes floating-point values or returns a value now get their own vtable, whose Invoke is a libffi closure with the exact signature from metadata. The JS function's result is converted to the declared return type, and a JS exception is reported as E_FAIL. Other delegates keep the shared Invoke. Both the classic and the Node-API engines use it. --- runtime/src/delegate_invoke.rs | 320 ++++++++++++++++++++++++++++ runtime/src/interop_test.rs | 82 +++++++ runtime/src/lib.rs | 242 +++++++++++++++++---- runtime/src/method_call.rs | 21 +- runtime/src/napi_engine/delegate.rs | 150 +++++++++++-- runtime/src/napi_engine/ns_proxy.rs | 6 +- runtime/src/ns_proxy.rs | 35 +-- runtime/src/property_call.rs | 13 +- 8 files changed, 747 insertions(+), 122 deletions(-) create mode 100644 runtime/src/delegate_invoke.rs 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)