From a338cd87ebd5b0b9b4ab7659fd6f4965d14696a5 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Fri, 6 Nov 2026 03:07:35 -0400 Subject: [PATCH] feat(runtime): primitive arrays, interop.Array.create, boxed delegates Arrays: every primitive WinRT array parameter (Int32[], Double[], Char16[], Boolean[], ...) now marshals, both arrays the callee reads and arrays it fills; only byte[] did before. A typed array of the element's kind, an ArrayBuffer or DataView is passed in place; a plain JS array is copied, and refused for a fill array since the callee's writes would be lost. interop.Array.create(type, count) allocates the matching typed array up front, e.g. reader.ReadBytes(interop.Array.create('UInt8', n)). Delegates: a JS delegate passed where WinRT expects an Object now also answers IInspectable and IReference, the way a projected delegate is boxed. UIElement.AddHandler unboxes its handler that way and failed with E_INVALIDARG; AddHandler(..., handledEventsToo) and RemoveHandler work on both engines. Errors: an argument that can't be marshaled reports what was expected ("Expected an Int32Array, an ArrayBuffer, a plain array or null for parameter type Int32[]") instead of "Unspecified error (HRESULT 0x80004005)". --- .../src/declarations/parameter_declaration.rs | 11 +- metadata/src/generic_instance_id_builder.rs | 30 ++ runtime/src/delegate_box.rs | 141 +++++++++ runtime/src/error.rs | 34 +++ runtime/src/global_fns.rs | 2 +- runtime/src/helpers.rs | 281 +++++++++++++++++- runtime/src/interop_test.rs | 6 +- runtime/src/js/interop_array.js | 31 ++ runtime/src/lib.rs | 66 +++- runtime/src/method_call.rs | 109 +++++-- runtime/src/napi_engine/delegate.rs | 14 +- runtime/src/napi_engine/interop.rs | 4 +- runtime/src/napi_engine/invoke.rs | 5 + runtime/src/napi_engine/value.rs | 88 +++++- runtime/src/property_call.rs | 112 +++++-- runtime/src/value.rs | 76 ++++- typings/nswinrt-helpers.d.ts | 23 ++ 17 files changed, 957 insertions(+), 76 deletions(-) create mode 100644 runtime/src/delegate_box.rs create mode 100644 runtime/src/js/interop_array.js diff --git a/metadata/src/declarations/parameter_declaration.rs b/metadata/src/declarations/parameter_declaration.rs index 01f3b1c..3b9fcb7 100644 --- a/metadata/src/declarations/parameter_declaration.rs +++ b/metadata/src/declarations/parameter_declaration.rs @@ -12,6 +12,7 @@ pub struct ParameterDeclaration { token: CorTokenType, parameter_type: PCCOR_SIGNATURE, full_name: String, + attributes: u32, } impl ParameterDeclaration { @@ -24,6 +25,7 @@ impl ParameterDeclaration { assert_eq!(type_from_token(token), mdtParamDef.0); assert_ne!(token.0, 0); + let mut attributes = 0u32; let full_name = match metadata { None => String::new(), Some(metadata) => { @@ -37,7 +39,7 @@ impl ParameterDeclaration { 0 as _, Some(&mut full_name_data), &mut length, - 0 as _, + &mut attributes, 0 as _, 0 as _, 0 as _, @@ -55,9 +57,16 @@ impl ParameterDeclaration { token, parameter_type: sig_type, full_name, + attributes, } } + /// Marked `[out]` without being ByRef: a caller-allocated array the callee fills. + pub fn is_fill_array(&self) -> bool { + const PD_OUT: u32 = 0x2; + self.attributes & PD_OUT != 0 && !self.is_out() + } + pub fn is_out(&self) -> bool { let mut parameter_type = self.parameter_type.clone(); cor_sig_uncompress_token(&mut parameter_type) == ELEMENT_TYPE_BYREF.0 as u32 diff --git a/metadata/src/generic_instance_id_builder.rs b/metadata/src/generic_instance_id_builder.rs index e262b67..8fa89fe 100644 --- a/metadata/src/generic_instance_id_builder.rs +++ b/metadata/src/generic_instance_id_builder.rs @@ -265,6 +265,15 @@ impl GenericInstanceIdBuilder { }; compute_winrt_piid(&signature) } + + /// The IID of `IReference` for the non-generic delegate type `D` whose IID is + /// `delegate_iid`: the interface a delegate boxed as an `Object` answers. + pub fn ireference_of_delegate(delegate_iid: &GUID) -> GUID { + compute_winrt_piid(&format!( + "pinterface({{61c17706-2d65-11e0-9ae8-d48564015472}};delegate({}))", + format_guid(delegate_iid) + )) + } } /// Recursively computes the canonical WinRT wire-format signature for a type @@ -467,4 +476,25 @@ mod tests { ); assert_eq!(actual, expected); } + + #[test] + fn ireference_of_delegate_matches_the_os() { + use windows::core::Interface; + use windows::Foundation::DeferralCompletedHandler; + let names = [ + HSTRING::from("Windows.Foundation.IReference`1"), + HSTRING::from("Windows.Foundation.DeferralCompletedHandler"), + ]; + let elements: Vec = names.iter().map(|n| PCWSTR(n.as_ptr())).collect(); + let locator = IRoMetaDataLocatorImpl; + let locator = IRoMetaDataLocator::new(&locator); + let mut expected = GUID::zeroed(); + unsafe { + RoGetParameterizedTypeInstanceIID(&elements, &*locator, &mut expected, None).unwrap(); + } + assert_eq!( + GenericInstanceIdBuilder::ireference_of_delegate(&DeferralCompletedHandler::IID), + expected, + ); + } } diff --git a/runtime/src/delegate_box.rs b/runtime/src/delegate_box.rs new file mode 100644 index 0000000..29fdc6f --- /dev/null +++ b/runtime/src/delegate_box.rs @@ -0,0 +1,141 @@ +//! A JS delegate passed where WinRT expects an `Object` must arrive boxed as `IReference`: +//! `UIElement.AddHandler` unboxes its handler that way. Each JS delegate embeds a `DelegateBox` +//! tear-off that answers `IInspectable` and `IReference` and returns the delegate from +//! `get_Value`. Its IUnknown methods forward to the delegate, so COM identity is the delegate's. + +use std::ffi::c_void; +use std::sync::OnceLock; + +use metadata::generic_instance_id_builder::GenericInstanceIdBuilder; +use windows::core::{IInspectable, Interface, GUID, HRESULT}; + +const E_NOINTERFACE: HRESULT = HRESULT(0x8000_4002u32 as i32); +const E_POINTER: HRESULT = HRESULT(0x8000_4003u32 as i32); + +#[repr(C)] +struct DelegateBoxVtbl { + query_interface: + unsafe extern "system" fn(*mut DelegateBox, *const GUID, *mut *mut c_void) -> HRESULT, + add_ref: unsafe extern "system" fn(*mut DelegateBox) -> u32, + release: unsafe extern "system" fn(*mut DelegateBox) -> u32, + get_iids: unsafe extern "system" fn(*mut DelegateBox, *mut u32, *mut *mut GUID) -> HRESULT, + get_runtime_class_name: unsafe extern "system" fn(*mut DelegateBox, *mut *mut c_void) -> HRESULT, + get_trust_level: unsafe extern "system" fn(*mut DelegateBox, *mut i32) -> HRESULT, + get_value: unsafe extern "system" fn(*mut DelegateBox, *mut *mut c_void) -> HRESULT, +} + +static DELEGATE_BOX_VTBL: DelegateBoxVtbl = DelegateBoxVtbl { + query_interface: box_query_interface, + add_ref: box_add_ref, + release: box_release, + get_iids: box_get_iids, + get_runtime_class_name: box_get_runtime_class_name, + get_trust_level: box_get_trust_level, + get_value: box_get_value, +}; + +#[repr(C)] +pub(crate) struct DelegateBox { + vtable: *const DelegateBoxVtbl, + /// The delegate this box is part of; set once the delegate has its final address. + owner: *mut c_void, + delegate_iid: GUID, + reference_iid: OnceLock, +} + +impl DelegateBox { + pub(crate) fn new(delegate_iid: GUID) -> Self { + Self { + vtable: &DELEGATE_BOX_VTBL, + owner: std::ptr::null_mut(), + delegate_iid, + reference_iid: OnceLock::new(), + } + } + + /// Ties the box to the delegate that contains it. + pub(crate) fn set_owner(&mut self, owner: *mut c_void) { + self.owner = owner; + } + + /// The box's interface pointer when the owning delegate is asked for `iid` and the box + /// implements it. The caller AddRefs the delegate. + pub(crate) fn query(&self, iid: &GUID) -> Option<*mut c_void> { + let reference_iid = self + .reference_iid + .get_or_init(|| GenericInstanceIdBuilder::ireference_of_delegate(&self.delegate_iid)); + (*iid == IInspectable::IID || iid == reference_iid) + .then(|| self as *const Self as *mut c_void) + } +} + +/// The owning delegate's IUnknown vtable. +unsafe fn owner_vtable(this: *mut DelegateBox) -> &'static windows::core::IUnknown_Vtbl { + &**((*this).owner as *const *const windows::core::IUnknown_Vtbl) +} + +unsafe extern "system" fn box_query_interface( + this: *mut DelegateBox, + iid: *const GUID, + out: *mut *mut c_void, +) -> HRESULT { + if out.is_null() { + return E_POINTER; + } + if iid.is_null() { + *out = std::ptr::null_mut(); + return E_NOINTERFACE; + } + (owner_vtable(this).QueryInterface)((*this).owner, iid, out) +} + +unsafe extern "system" fn box_add_ref(this: *mut DelegateBox) -> u32 { + (owner_vtable(this).AddRef)((*this).owner) +} + +unsafe extern "system" fn box_release(this: *mut DelegateBox) -> u32 { + (owner_vtable(this).Release)((*this).owner) +} + +unsafe extern "system" fn box_get_iids( + _this: *mut DelegateBox, + count: *mut u32, + iids: *mut *mut GUID, +) -> HRESULT { + if count.is_null() || iids.is_null() { + return E_POINTER; + } + *count = 0; + *iids = std::ptr::null_mut(); + HRESULT(0) +} + +unsafe extern "system" fn box_get_runtime_class_name( + _this: *mut DelegateBox, + name: *mut *mut c_void, +) -> HRESULT { + if name.is_null() { + return E_POINTER; + } + // The empty HSTRING. + *name = std::ptr::null_mut(); + HRESULT(0) +} + +unsafe extern "system" fn box_get_trust_level(_this: *mut DelegateBox, level: *mut i32) -> HRESULT { + if level.is_null() { + return E_POINTER; + } + *level = 0; + HRESULT(0) +} + +unsafe extern "system" fn box_get_value(this: *mut DelegateBox, value: *mut *mut c_void) -> HRESULT { + if value.is_null() { + return E_POINTER; + } + let owner = (*this).owner; + (owner_vtable(this).AddRef)(owner); + *value = owner; + HRESULT(0) +} diff --git a/runtime/src/error.rs b/runtime/src/error.rs index 28785e5..fe3ffcc 100644 --- a/runtime/src/error.rs +++ b/runtime/src/error.rs @@ -46,9 +46,43 @@ pub fn type_error(message: impl Into>) -> Error { custom_error("TypeError", message) } +/// The HRESULT a call reports when one of its JS arguments could not be marshaled. +pub(crate) const ARGUMENT_FAILURE: windows::core::HRESULT = + windows::core::HRESULT(0x8000_4005u32 as i32); + +thread_local! { + static ARGUMENT_ERROR: std::cell::RefCell> = + const { std::cell::RefCell::new(None) }; +} + +/// Records why an argument could not be marshaled; the call then fails with +/// `ARGUMENT_FAILURE`, and `format_hresult_message` reports this text instead. +pub(crate) fn set_argument_error(message: impl Display) { + ARGUMENT_ERROR.with(|e| *e.borrow_mut() = Some(message.to_string())); +} + +/// Drops a recorded argument error that no caller reported. +#[inline] +pub(crate) fn clear_argument_error() { + ARGUMENT_ERROR.with(|e| { + if e.borrow().is_some() { + *e.borrow_mut() = None; + } + }); +} + +pub(crate) fn take_argument_error() -> Option { + ARGUMENT_ERROR.with(|e| e.borrow_mut().take()) +} + /// Format an HRESULT into a human-readable message using the runtime's /// provided message text when available. Falls back to a hex HRESULT string. pub fn format_hresult_message(hr: windows::core::HRESULT) -> String { + if hr == ARGUMENT_FAILURE { + if let Some(message) = take_argument_error() { + return message; + } + } let s = hr.message(); if s.is_empty() { format!("HRESULT 0x{:08X}", hr.0 as u32) diff --git a/runtime/src/global_fns.rs b/runtime/src/global_fns.rs index ded636b..2f9298f 100644 --- a/runtime/src/global_fns.rs +++ b/runtime/src/global_fns.rs @@ -1931,7 +1931,7 @@ const HELPER_SOURCE: &str = concat!(r#" globalThis.NSWinRT = globalThis.NSWinRT || {}; globalThis.NSWinRT.interop = globalThis.NSWinRT.interop || globalThis.interop; -"#, include_str!("js/dotnet_subclass.js"), r#" +"#, include_str!("js/interop_array.js"), "\n", include_str!("js/dotnet_subclass.js"), r#" function asDelegate(handler) { if (typeof handler === 'function') { diff --git a/runtime/src/helpers.rs b/runtime/src/helpers.rs index 5055b54..f4f26ac 100644 --- a/runtime/src/helpers.rs +++ b/runtime/src/helpers.rs @@ -109,10 +109,226 @@ pub fn ireference_inner_type(sig: &str) -> Option<&str> { pub(crate) fn parameter_layout_supported(signature: &str) -> bool { match signature.trim().strip_suffix("[]") { None => true, - Some(element) => matches!(element, "UInt8" | "Uint8" | "Int8" | "Byte" | "SByte"), + Some(element) => ArrayElement::from_name(element).is_some(), } } +/// Element type of a WinRT array parameter (`Int32[]`) the call layout carries as +/// `(count, pointer)` over JS-owned memory: a typed array of the matching kind, an +/// ArrayBuffer, a DataView or a plain array (copied). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum ArrayElement { + U8, + I8, + U16, + I16, + U32, + I32, + U64, + I64, + F32, + F64, + Bool, + Char16, +} + +impl ArrayElement { + pub(crate) fn from_name(element: &str) -> Option { + Some(match element { + "UInt8" | "Uint8" | "Byte" => Self::U8, + "Int8" | "SByte" => Self::I8, + "UInt16" => Self::U16, + "Int16" => Self::I16, + "UInt32" => Self::U32, + "Int32" => Self::I32, + "UInt64" => Self::U64, + "Int64" => Self::I64, + "Single" => Self::F32, + "Double" => Self::F64, + "Boolean" => Self::Bool, + "Char16" => Self::Char16, + _ => return None, + }) + } + + /// The element type of a pass or fill array signature; `None` for anything else, + /// including received (`ByRef`) arrays. + pub(crate) fn of_signature(signature: &str) -> Option { + signature.trim().strip_suffix("[]").and_then(Self::from_name) + } + + pub(crate) fn size(self) -> usize { + match self { + Self::U8 | Self::I8 | Self::Bool => 1, + Self::U16 | Self::I16 | Self::Char16 => 2, + Self::U32 | Self::I32 | Self::F32 => 4, + Self::U64 | Self::I64 | Self::F64 => 8, + } + } + + pub(crate) fn winrt_name(self) -> &'static str { + match self { + Self::U8 => "UInt8", + Self::I8 => "Int8", + Self::U16 => "UInt16", + Self::I16 => "Int16", + Self::U32 => "UInt32", + Self::I32 => "Int32", + Self::U64 => "UInt64", + Self::I64 => "Int64", + Self::F32 => "Single", + Self::F64 => "Double", + Self::Bool => "Boolean", + Self::Char16 => "Char16", + } + } + + /// The error for a value that can't back an array of this element. + pub(crate) fn mismatch(self) -> crate::error::AnyError { + crate::error::type_error(format!( + "Expected {}, an ArrayBuffer, a plain array or null for parameter type {}[]", + self.typed_array_with_article(), + self.winrt_name() + )) + } + + /// The error for a plain array passed where the callee fills the array. + pub(crate) fn fill_mismatch(self) -> crate::error::AnyError { + crate::error::type_error(format!( + "The {}[] parameter is filled by the callee: pass {} (interop.Array.create('{}', n))", + self.winrt_name(), + self.typed_array_with_article(), + self.winrt_name() + )) + } + + fn typed_array_with_article(self) -> String { + let name = self.typed_array_name(); + let article = if name.starts_with('I') { "an" } else { "a" }; + format!("{article} {name}") + } + + /// The typed array JS uses for this element. + pub(crate) fn typed_array_name(self) -> &'static str { + match self { + Self::U8 | Self::Bool => "Uint8Array", + Self::I8 => "Int8Array", + Self::U16 | Self::Char16 => "Uint16Array", + Self::I16 => "Int16Array", + Self::U32 => "Uint32Array", + Self::I32 => "Int32Array", + Self::U64 => "BigUint64Array", + Self::I64 => "BigInt64Array", + Self::F32 => "Float32Array", + Self::F64 => "Float64Array", + } + } + + /// Whether a typed array of `kind` may back an array of this element: the matching kind, + /// or any byte-sized view for a byte-sized element. + pub(crate) fn accepts_view(self, kind: TypedArrayKind) -> bool { + use TypedArrayKind as K; + match self { + Self::U8 | Self::I8 | Self::Bool => matches!(kind, K::U8 | K::U8Clamped | K::I8), + Self::U16 | Self::Char16 => kind == K::U16, + Self::I16 => kind == K::I16, + Self::U32 => kind == K::U32, + Self::I32 => kind == K::I32, + Self::U64 => kind == K::U64, + Self::I64 => kind == K::I64, + Self::F32 => kind == K::F32, + Self::F64 => kind == K::F64, + } + } + + /// Appends one element converted from a JS number, as the matching typed array would + /// store it. + pub(crate) fn push_number(self, out: &mut Vec, number: f64) { + let integer = |bits: u32| -> u64 { + if !number.is_finite() { + return 0; + } + let modulus = 2f64.powi(bits as i32); + let wrapped = number.trunc() % modulus; + let wrapped = if wrapped < 0.0 { wrapped + modulus } else { wrapped }; + wrapped as u64 + }; + match self { + Self::U8 | Self::I8 => out.push(integer(8) as u8), + Self::Bool => out.push((number != 0.0 && !number.is_nan()) as u8), + Self::U16 | Self::I16 | Self::Char16 => { + out.extend_from_slice(&(integer(16) as u16).to_le_bytes()) + } + Self::U32 | Self::I32 => out.extend_from_slice(&(integer(32) as u32).to_le_bytes()), + Self::U64 | Self::I64 => out.extend_from_slice(&(number as i64).to_le_bytes()), + Self::F32 => out.extend_from_slice(&(number as f32).to_le_bytes()), + Self::F64 => out.extend_from_slice(&number.to_le_bytes()), + } + } + + /// Appends one 64-bit element given as a BigInt. + pub(crate) fn push_bigint(self, out: &mut Vec, value: u64) { + match self { + Self::U64 | Self::I64 => out.extend_from_slice(&value.to_le_bytes()), + _ => self.push_number(out, value as i64 as f64), + } + } +} + +/// A JS value marshaled for a `T[]` parameter: JS-owned memory the callee reads or fills in +/// place, or a copy of a plain array. +pub(crate) struct ArrayArg { + pub(crate) pointer: *mut std::ffi::c_void, + pub(crate) count: u32, + pub(crate) byte_length: u32, + pub(crate) copy: Option>, +} + +impl ArrayArg { + /// Borrowed memory of `byte_length` bytes, which must hold whole elements. + pub(crate) fn borrowed( + pointer: *mut std::ffi::c_void, + byte_length: u32, + element: ArrayElement, + ) -> Result { + if byte_length as usize % element.size() != 0 { + return Err(element.mismatch()); + } + Ok(Self { + pointer, + count: byte_length / element.size() as u32, + byte_length, + copy: None, + }) + } + + pub(crate) fn copied(bytes: Vec, element: ArrayElement) -> Self { + Self { + pointer: bytes.as_ptr() as *mut std::ffi::c_void, + count: (bytes.len() / element.size()) as u32, + byte_length: bytes.len() as u32, + copy: Some(bytes), + } + } +} + +/// Typed array kinds, shared by both engines' checks. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum TypedArrayKind { + I8, + U8, + U8Clamped, + I16, + U16, + I32, + U32, + F32, + F64, + I64, + U64, + Other, +} + /// Size of the scratch buffer a call writes its value-type return into. pub(crate) const RETURN_BUFFER_SIZE: usize = 128; @@ -137,9 +353,9 @@ pub(crate) fn ffi_native_type_from_signature(signature: &str) -> NativeType { let by_ref_inner = signature.strip_prefix("ByRef ").unwrap_or(signature); if let Some(element) = by_ref_inner.strip_suffix("[]") { - return match element { - "UInt8" | "Uint8" | "Int8" | "Byte" | "SByte" => NativeType::Buffer, - _ => NativeType::Pointer, + return match ArrayElement::from_name(element) { + Some(_) => NativeType::Buffer, + None => NativeType::Pointer, }; } @@ -175,3 +391,60 @@ pub(crate) fn ffi_native_type_from_signature(signature: &str) -> NativeType { _ => NativeType::Pointer, } } + +#[cfg(test)] +mod array_element_tests { + use super::{parameter_layout_supported, ArrayArg, ArrayElement, TypedArrayKind}; + + fn bytes(element: ArrayElement, numbers: &[f64]) -> Vec { + let mut out = Vec::new(); + for &n in numbers { + element.push_number(&mut out, n); + } + out + } + + #[test] + fn signatures() { + assert_eq!(ArrayElement::of_signature("Int32[]"), Some(ArrayElement::I32)); + assert_eq!(ArrayElement::of_signature("Char16[]"), Some(ArrayElement::Char16)); + assert_eq!(ArrayElement::of_signature("ByRef Int32[]"), None); + assert_eq!(ArrayElement::of_signature("String[]"), None); + assert!(parameter_layout_supported("Double[]")); + assert!(!parameter_layout_supported("Windows.Foundation.Uri[]")); + } + + #[test] + fn numbers_convert_like_typed_arrays() { + assert_eq!(bytes(ArrayElement::U8, &[257.0, -1.0, 3.9, f64::NAN]), [1, 255, 3, 0]); + assert_eq!(bytes(ArrayElement::I32, &[-1.0]), (-1i32).to_le_bytes()); + assert_eq!(bytes(ArrayElement::U32, &[4294967297.0]), 1u32.to_le_bytes()); + assert_eq!(bytes(ArrayElement::I16, &[-2.0]), (-2i16).to_le_bytes()); + assert_eq!(bytes(ArrayElement::Bool, &[2.0, 0.0]), [1, 0]); + assert_eq!(bytes(ArrayElement::F32, &[0.5]), 0.5f32.to_le_bytes()); + assert_eq!(bytes(ArrayElement::I64, &[-5.0]), (-5i64).to_le_bytes()); + let mut out = Vec::new(); + ArrayElement::I64.push_bigint(&mut out, (-7i64) as u64); + assert_eq!(out, (-7i64).to_le_bytes()); + } + + #[test] + fn counts_are_elements_not_bytes() { + let doubles = bytes(ArrayElement::F64, &[1.0, 2.0, 3.0]); + let copied = ArrayArg::copied(doubles, ArrayElement::F64); + assert_eq!((copied.count, copied.byte_length), (3, 24)); + let borrowed = ArrayArg::borrowed(std::ptr::null_mut(), 12, ArrayElement::I32); + let borrowed = borrowed.ok().unwrap(); + assert_eq!(borrowed.count, 3); + assert!(ArrayArg::borrowed(std::ptr::null_mut(), 6, ArrayElement::I32).is_err()); + } + + #[test] + fn views_must_match_the_element() { + assert!(ArrayElement::I32.accepts_view(TypedArrayKind::I32)); + assert!(!ArrayElement::I32.accepts_view(TypedArrayKind::F64)); + assert!(ArrayElement::U8.accepts_view(TypedArrayKind::I8)); + assert!(ArrayElement::Char16.accepts_view(TypedArrayKind::U16)); + assert!(!ArrayElement::U64.accepts_view(TypedArrayKind::I64)); + } +} diff --git a/runtime/src/interop_test.rs b/runtime/src/interop_test.rs index 18a1e3d..a7fb6e4 100644 --- a/runtime/src/interop_test.rs +++ b/runtime/src/interop_test.rs @@ -526,7 +526,7 @@ fn native_wrong_type_throws() { // If none of the WinRT types are present, skip (pass) the test. if (!PropertySetCtor && !CryptBuf && !UriCtor) return; - const hrex = /HRESULT 0x[0-9A-Fa-f]{8}/; + const hrex = /HRESULT 0x[0-9A-Fa-f]{8}|^Error: Expected /; if (PropertySetCtor) { try { @@ -545,7 +545,7 @@ fn native_wrong_type_throws() { try { CryptBuf.CreateFromByteArray('not-an-array'); } catch (e) { - if (hrex.test(String(e)) || /HRESULT/.test(String(e))) return; + if (hrex.test(String(e))) return; throw new Error('CryptographicBuffer.CreateFromByteArray threw without HRESULT: ' + String(e)); } } @@ -643,7 +643,7 @@ fn propertyset_marshalling_broad_validation() { } catch (e) { threw = true; const s = String(e || ''); - if (!hrex.test(s) && !/HRESULT/.test(s)) { + if (!hrex.test(s) && !/HRESULT|^Error: Expected /.test(s)) { throw new Error('Assignment threw without HRESULT for key ' + c.k + ': ' + s); } } diff --git a/runtime/src/js/interop_array.js b/runtime/src/js/interop_array.js new file mode 100644 index 0000000..41c859e --- /dev/null +++ b/runtime/src/js/interop_array.js @@ -0,0 +1,31 @@ +// interop.Array.create(type, count): a zero-filled array of a primitive WinRT element type, +// backed by JS memory a `T[]` parameter reads, or a callee fills, in place. +(function () { + var interop = globalThis.interop || (globalThis.NSWinRT && globalThis.NSWinRT.interop); + if (!interop || (interop.Array && typeof interop.Array.create === 'function')) return; + + var kinds = { + UInt8: 'Uint8Array', Byte: 'Uint8Array', Boolean: 'Uint8Array', + Int8: 'Int8Array', SByte: 'Int8Array', + UInt16: 'Uint16Array', Char16: 'Uint16Array', Int16: 'Int16Array', + UInt32: 'Uint32Array', Int32: 'Int32Array', + UInt64: 'BigUint64Array', Int64: 'BigInt64Array', + Single: 'Float32Array', Double: 'Float64Array' + }; + + function create(type, count) { + var name = typeof type === 'function' && type.BYTES_PER_ELEMENT ? type.name : String(type); + var kind = Object.prototype.hasOwnProperty.call(kinds, name) ? kinds[name] : name; + var ctor = globalThis[kind]; + if (!/Array$/.test(kind) || typeof ctor !== 'function' || !ctor.BYTES_PER_ELEMENT) { + throw new TypeError('interop.Array.create: unsupported element type ' + name + + '; expected one of ' + Object.keys(kinds).join(', ')); + } + if (typeof count !== 'number' || !(count >= 0) || count % 1 !== 0 || count > 4294967295) { + throw new RangeError('interop.Array.create: count must be an integer from 0 to 4294967295'); + } + return new ctor(count); + } + + interop.Array = { create: create }; +})(); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 95b8b3b..dfb22d0 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_box; mod delegate_invoke; pub mod esm_http; pub(crate) mod dotnet; @@ -7699,13 +7700,15 @@ pub(crate) fn new_js_delegate(js_func: v8::Global, guid: GUID, sig }; let owner_isolate = DELEGATE_ISOLATE_PTR.with(|c| c.get()); let data = Box::new(JsDelegateData { js_func, signature, typed, owner_isolate }); - let delegate = Box::new(JsDelegate { + let delegate = Box::into_raw(Box::new(JsDelegate { vtable, ref_count: AtomicU32::new(1), guid, data: Box::into_raw(data), - }); - Box::into_raw(delegate) as *mut c_void + boxed: crate::delegate_box::DelegateBox::new(guid), + })); + unsafe { (*delegate).boxed.set_owner(delegate as *mut c_void) }; + delegate as *mut c_void } #[cfg(feature = "classic")] @@ -7715,6 +7718,8 @@ pub(crate) struct JsDelegate { pub(crate) ref_count: AtomicU32, pub(crate) guid: GUID, pub(crate) data: *mut JsDelegateData, + /// Answers `IReference` for a delegate passed as an `Object`. + pub(crate) boxed: crate::delegate_box::DelegateBox, } #[cfg(feature = "classic")] @@ -7752,6 +7757,10 @@ unsafe extern "system" fn js_delegate_query_interface( *out = this as *mut c_void; js_delegate_add_ref(this); HRESULT(0) + } else if let Some(boxed) = d.boxed.query(&*iid) { + *out = boxed; + js_delegate_add_ref(this); + HRESULT(0) } else { *out = std::ptr::null_mut(); HRESULT(0x80004002u32 as i32) // E_NOINTERFACE @@ -9158,19 +9167,22 @@ mod js_delegate_tests { JS_DELEGATE_VTBL, }; use std::ffi::c_void; - use std::sync::atomic::AtomicU32; + use std::sync::atomic::{AtomicU32, Ordering as AtomicOrdering}; use windows::core::{IUnknown, Interface, GUID, HRESULT}; /// Build a JsDelegate with a null data pointer for reference-count-only tests. /// Callers must ensure the delegate's ref_count never reaches 0 (which would /// try to free the null data pointer). unsafe fn make_test_delegate(guid: GUID) -> *mut JsDelegate { - Box::into_raw(Box::new(JsDelegate { + let ptr = Box::into_raw(Box::new(JsDelegate { vtable: &JS_DELEGATE_VTBL as *const _, ref_count: AtomicU32::new(1), guid, data: std::ptr::null_mut(), - })) + boxed: crate::delegate_box::DelegateBox::new(guid), + })); + (*ptr).boxed.set_owner(ptr as *mut c_void); + ptr } #[test] @@ -9261,4 +9273,46 @@ mod js_delegate_tests { let _ = Box::from_raw(ptr); } } + + #[test] + fn js_delegate_boxes_itself_as_ireference() { + use windows::Foundation::{DeferralCompletedHandler, IReference}; + let reference_iid = + metadata::generic_instance_id_builder::GenericInstanceIdBuilder::ireference_of_delegate( + &DeferralCompletedHandler::IID, + ); + unsafe { + let ptr = make_test_delegate(DeferralCompletedHandler::IID); + let mut boxed: *mut c_void = std::ptr::null_mut(); + let hr = js_delegate_query_interface(ptr, &reference_iid, &mut boxed); + assert_eq!(hr, HRESULT(0), "QI for IReference should return S_OK"); + assert_ne!(boxed, ptr as *mut c_void, "the box is a tear-off"); + assert_eq!((*ptr).ref_count.load(AtomicOrdering::Relaxed), 2); + + // get_Value hands back the delegate itself, AddRef'd. + let reference = std::mem::ManuallyDrop::new( + IReference::::from_raw(boxed), + ); + let mut value: *mut c_void = std::ptr::null_mut(); + let hr = (Interface::vtable(&*reference).Value)(boxed, &mut value as *mut _ as *mut _); + assert_eq!(hr, HRESULT(0)); + assert_eq!(value, ptr as *mut c_void); + assert_eq!((*ptr).ref_count.load(AtomicOrdering::Relaxed), 3); + + // The box's IUnknown is the delegate's: COM identity holds across the tear-off. + let mut identity: *mut c_void = std::ptr::null_mut(); + let unknown = std::mem::ManuallyDrop::new(IUnknown::from_raw(boxed)); + let query = Interface::vtable(&*unknown).QueryInterface; + let hr = query(boxed, &IUnknown::IID, &mut identity); + assert_eq!(hr, HRESULT(0)); + assert_eq!(identity, ptr as *mut c_void); + + // Box release forwards to the delegate. + (Interface::vtable(&*unknown).Release)(boxed); + js_delegate_release(ptr); + js_delegate_release(ptr); + assert_eq!((*ptr).ref_count.load(AtomicOrdering::Relaxed), 1); + let _ = Box::from_raw(ptr); + } + } } diff --git a/runtime/src/method_call.rs b/runtime/src/method_call.rs index d6b973d..429f448 100644 --- a/runtime/src/method_call.rs +++ b/runtime/src/method_call.rs @@ -2,7 +2,7 @@ use crate::error::AnyError; use crate::helpers::ffi_native_type_from_signature; #[cfg(feature = "classic")] use crate::value::{ - ffi_parse_bool_arg, ffi_parse_buffer_arg_with_length, ffi_parse_f32_arg, ffi_parse_f64_arg, + ffi_parse_bool_arg, ffi_parse_f32_arg, ffi_parse_f64_arg, ffi_parse_function_arg, ffi_parse_i16_arg, ffi_parse_i32_arg, ffi_parse_i64_arg, ffi_parse_i8_arg, ffi_parse_isize_arg, ffi_parse_pointer_arg, ffi_parse_query_interface_arg, ffi_parse_string_arg, ffi_parse_struct_arg, ffi_parse_u16_arg, ffi_parse_u32_arg, @@ -932,6 +932,7 @@ impl MethodCall { let is_string_return = self.si.is_string_return; self.reset_args(); + crate::error::clear_argument_error(); let mut queried_interfaces: SmallVec<[IUnknown; 2]> = SmallVec::new(); let mut struct_scratch: Vec> = Vec::new(); // Track out-parameter slots: (argument_buf index, parse_native_type, param_index, wrapper) @@ -985,7 +986,10 @@ impl MethodCall { if !init_val.is_undefined() && !init_val.is_null() { match write_v8_value_to_ptr(scope, init_val, ptr, native_type) { Ok(_) => {} - Err(_) => return (call_failure(), std::ptr::null_mut(), Vec::new()), + Err(e) => { + crate::error::set_argument_error(e); + return (call_failure(), std::ptr::null_mut(), Vec::new()); + } } } } @@ -1192,18 +1196,35 @@ impl MethodCall { } } NativeType::Buffer => { - let parsed = ffi_parse_buffer_arg_with_length(scope, value); - let (buffer_value, byte_length) = match parsed { - Ok(value) => value, - Err(_) => return (call_failure(), std::ptr::null_mut(), Vec::new()), + let element = crate::helpers::ArrayElement::of_signature(param_sig) + .unwrap_or(crate::helpers::ArrayElement::U8); + let array = crate::value::ffi_parse_array_arg(scope, value, element); + let array = array.and_then(|array| { + // A copy can't carry back what the callee writes into a fill array. + if array.copy.is_some() && parameter.is_fill_array() { + Err(element.fill_mismatch()) + } else { + Ok(array) + } + }); + let array = match array { + Ok(array) => array, + Err(e) => { + crate::error::set_argument_error(e); + return (call_failure(), std::ptr::null_mut(), Vec::new()); + } }; - buffer_args.push((value, unsafe { buffer_value.pointer }, byte_length)); - + match array.copy { + Some(bytes) => struct_scratch.push(bytes), + None => buffer_args.push((value, array.pointer, array.byte_length)), + } self.argument_buf.push(NativeValue { - u32_value: byte_length, + u32_value: array.count, }); self.argument_parse_types.push(Some(native_type.clone())); - self.argument_buf.push(buffer_value); + self.argument_buf.push(NativeValue { + pointer: array.pointer, + }); self.argument_parse_types.push(Some(native_type.clone())); continue; } @@ -1214,7 +1235,10 @@ impl MethodCall { let value = match value { Ok(value) => value, - Err(_) => return (call_failure(), std::ptr::null_mut(), Vec::new()), + Err(e) => { + crate::error::set_argument_error(e); + return (call_failure(), std::ptr::null_mut(), Vec::new()); + } }; self.argument_buf.push(value); @@ -1263,6 +1287,9 @@ impl MethodCall { } if !crate::value::buffers_unchanged(scope, &buffer_args) { + crate::error::set_argument_error( + "An array argument was detached or resized while the arguments were read", + ); return (call_failure(), std::ptr::null_mut(), Vec::new()); } @@ -1272,7 +1299,10 @@ impl MethodCall { &self.argument_parse_types, ) { Ok(value) => value, - Err(_) => return (call_failure(), std::ptr::null_mut(), Vec::new()), + Err(e) => { + crate::error::set_argument_error(e); + return (call_failure(), std::ptr::null_mut(), Vec::new()); + } }; let func_to_call = self.func; @@ -1454,6 +1484,7 @@ impl MethodCall { let is_void = self.si.is_void; self.reset_args(); + crate::error::clear_argument_error(); self.argument_buf.push(NativeValue { pointer: self.interface.as_raw() as *mut c_void, }); @@ -1508,6 +1539,7 @@ impl MethodCall { } self.reset_args(); + crate::error::clear_argument_error(); self.argument_buf.push(NativeValue { pointer: self.interface.as_raw() as *mut c_void, }); @@ -1600,6 +1632,7 @@ impl MethodCall { let is_string_return = self.si.is_string_return; self.reset_args(); + crate::error::clear_argument_error(); let mut queried_interfaces: SmallVec<[IUnknown; 2]> = SmallVec::new(); let mut struct_scratch: Vec> = Vec::new(); // (argument_buf index, parse_native_type, param_index, out-wrapper object) @@ -1646,7 +1679,10 @@ impl MethodCall { if vt != ValueType::Undefined && vt != ValueType::Null { match nv::write_js_value_to_ptr(env, &init_val, ptr, native_type) { Ok(_) => {} - Err(_) => return fail3(), + Err(e) => { + crate::error::set_argument_error(e); + return fail3(); + } } } } @@ -1843,17 +1879,37 @@ impl MethodCall { } } NativeType::Buffer => { - let parsed = nv::napi_parse_buffer_with_length(env, &value); - let (buffer_value, byte_length) = match parsed { - Ok(value) => value, - Err(_) => return fail3(), + let element = crate::helpers::ArrayElement::of_signature(param_sig) + .unwrap_or(crate::helpers::ArrayElement::U8); + let array = nv::napi_parse_array_arg(env, &value, element).and_then(|array| { + // A copy can't carry back what the callee writes into a fill array. + if array.copy.is_some() && parameter.is_fill_array() { + Err(element.fill_mismatch()) + } else { + Ok(array) + } + }); + let array = match array { + Ok(array) => array, + Err(e) => { + crate::error::set_argument_error(e); + return fail3(); + } }; - buffer_args.push((unsafe { napi::NapiRaw::raw(&value) }, unsafe { buffer_value.pointer }, byte_length)); + match array.copy { + Some(bytes) => struct_scratch.push(bytes), + None => { + let raw = unsafe { napi::NapiRaw::raw(&value) }; + buffer_args.push((raw, array.pointer, array.byte_length)) + } + } self.argument_buf.push(NativeValue { - u32_value: byte_length, + u32_value: array.count, }); self.argument_parse_types.push(Some(native_type.clone())); - self.argument_buf.push(buffer_value); + self.argument_buf.push(NativeValue { + pointer: array.pointer, + }); self.argument_parse_types.push(Some(native_type.clone())); continue; } @@ -1864,7 +1920,10 @@ impl MethodCall { let value = match value { Ok(value) => value, - Err(_) => return fail3(), + Err(e) => { + crate::error::set_argument_error(e); + return fail3(); + } }; self.argument_buf.push(value); @@ -1905,6 +1964,9 @@ impl MethodCall { } if !nv::buffers_unchanged(env, &buffer_args) { + crate::error::set_argument_error( + "An array argument was detached or resized while the arguments were read", + ); return fail3(); } @@ -1914,7 +1976,10 @@ impl MethodCall { &self.argument_parse_types, ) { Ok(value) => value, - Err(_) => return fail3(), + Err(e) => { + crate::error::set_argument_error(e); + return fail3(); + } }; let func_to_call = self.func; diff --git a/runtime/src/napi_engine/delegate.rs b/runtime/src/napi_engine/delegate.rs index cae18aa..793deeb 100644 --- a/runtime/src/napi_engine/delegate.rs +++ b/runtime/src/napi_engine/delegate.rs @@ -75,6 +75,8 @@ pub(crate) struct NapiDelegate { ref_count: AtomicU32, guid: GUID, data: *mut NapiDelegateData, + /// Answers `IReference` for a delegate passed as an `Object`. + boxed: crate::delegate_box::DelegateBox, } unsafe impl Send for NapiDelegate {} @@ -161,13 +163,15 @@ pub(crate) fn make_napi_delegate_typed( typed, owner_thread: std::thread::current().id(), }); - let delegate = Box::new(NapiDelegate { + let delegate = Box::into_raw(Box::new(NapiDelegate { vtable, ref_count: AtomicU32::new(1), guid, data: Box::into_raw(data), - }); - Some(Box::into_raw(delegate) as *mut c_void) + boxed: crate::delegate_box::DelegateBox::new(guid), + })); + unsafe { (*delegate).boxed.set_owner(delegate as *mut c_void) }; + Some(delegate as *mut c_void) } /// Invoke a NapiDelegate COM pointer through its vtable — what a WinRT event source does when @@ -218,6 +222,10 @@ unsafe extern "system" fn napi_delegate_query_interface( *out = this as *mut c_void; napi_delegate_add_ref(this); HRESULT(0) + } else if let Some(boxed) = d.boxed.query(&*iid) { + *out = boxed; + napi_delegate_add_ref(this); + HRESULT(0) } else { *out = std::ptr::null_mut(); HRESULT(0x80004002u32 as i32) diff --git a/runtime/src/napi_engine/interop.rs b/runtime/src/napi_engine/interop.rs index b91d615..37caf36 100644 --- a/runtime/src/napi_engine/interop.rs +++ b/runtime/src/napi_engine/interop.rs @@ -359,7 +359,7 @@ pub fn install_interop(env: &Env) -> napi::Result<()> { /// The JS half of `interop.*`, mirroring the classic runtime's `NSWinRT.interop` block in /// `global_fns.rs` HELPER_SOURCE — keep the two in sync. No BigInt literals or modern syntax /// so it runs on every engine (the DateTime tick helpers fall back to Number without BigInt). -const INTEROP_HELPERS_JS: &str = r#" +const INTEROP_HELPERS_JS: &str = concat!(r#" 'use strict'; var g = globalThis; if (g.NSWinRT && g.NSWinRT.interop) { return; } @@ -676,4 +676,4 @@ g.NSWinRT.interop = interop; if (g.interop == null) { g.interop = interop; } -"#; +"#, include_str!("../js/interop_array.js")); diff --git a/runtime/src/napi_engine/invoke.rs b/runtime/src/napi_engine/invoke.rs index 2fb8b47..f27b0c9 100644 --- a/runtime/src/napi_engine/invoke.rs +++ b/runtime/src/napi_engine/invoke.rs @@ -68,6 +68,11 @@ pub(crate) fn convert_call_result( result: *mut c_void, ) -> Result { if hr.is_err() { + if hr == crate::error::ARGUMENT_FAILURE { + if let Some(message) = crate::error::take_argument_error() { + return Err(type_error(message)); + } + } let detail = crate::error::format_hresult_message(hr); return Err(generic_error(format!("WinRT call failed: {detail}"))); } diff --git a/runtime/src/napi_engine/value.rs b/runtime/src/napi_engine/value.rs index d85b819..e4d5497 100644 --- a/runtime/src/napi_engine/value.rs +++ b/runtime/src/napi_engine/value.rs @@ -975,13 +975,13 @@ pub fn napi_parse_pointer(env: &Env, arg: &JsUnknown) -> Result Result { + use crate::helpers::{ArrayArg, ArrayElement, TypedArrayKind}; + let raw_env = env.raw(); + let raw = unsafe { arg.raw() }; + let mut is_array = false; + let status = unsafe { sys::napi_is_array(raw_env, raw, &mut is_array) }; + if status == sys::Status::napi_ok && is_array { + let array: JsObject = unsafe { arg.cast() }; + let length = array.get_array_length().map_err(|_| element.mismatch())?; + let mut bytes = Vec::with_capacity(length as usize * element.size()); + for index in 0..length { + let item: JsUnknown = array.get_element(index).map_err(|_| element.mismatch())?; + match item.get_type().map_err(|_| element.mismatch())? { + ValueType::BigInt => { + let (mut value, mut lossless) = (0u64, false); + unsafe { + let item = item.raw(); + sys::napi_get_value_bigint_uint64(raw_env, item, &mut value, &mut lossless) + }; + element.push_bigint(&mut bytes, value); + } + _ if element == ArrayElement::Bool => { + let flag = item + .coerce_to_bool() + .and_then(|b| b.get_value()) + .map_err(|_| element.mismatch())?; + element.push_number(&mut bytes, flag as u8 as f64); + } + _ => { + let number = item + .coerce_to_number() + .and_then(|n| n.get_double()) + .map_err(|_| element.mismatch())?; + element.push_number(&mut bytes, number); + } + } + } + return Ok(ArrayArg::copied(bytes, element)); + } + let mut is_typed = false; + let status = unsafe { sys::napi_is_typedarray(raw_env, raw, &mut is_typed) }; + if status == sys::Status::napi_ok && is_typed { + let mut kind: sys::napi_typedarray_type = -1; + let mut length = 0usize; + let mut data: *mut c_void = std::ptr::null_mut(); + let mut buffer: sys::napi_value = std::ptr::null_mut(); + let mut offset = 0usize; + unsafe { + sys::napi_get_typedarray_info( + raw_env, raw, &mut kind, &mut length, &mut data, &mut buffer, &mut offset, + ) + }; + let kind = match kind { + 0 => TypedArrayKind::I8, + 1 => TypedArrayKind::U8, + 2 => TypedArrayKind::U8Clamped, + 3 => TypedArrayKind::I16, + 4 => TypedArrayKind::U16, + 5 => TypedArrayKind::I32, + 6 => TypedArrayKind::U32, + 7 => TypedArrayKind::F32, + 8 => TypedArrayKind::F64, + 9 => TypedArrayKind::I64, + 10 => TypedArrayKind::U64, + _ => TypedArrayKind::Other, + }; + if !element.accepts_view(kind) { + return Err(element.mismatch()); + } + } + let (value, byte_length) = + napi_parse_buffer_with_length(env, arg).map_err(|_| element.mismatch())?; + ArrayArg::borrowed(unsafe { value.pointer }, byte_length, element) +} + pub fn napi_parse_struct( env: &Env, arg: &JsUnknown, diff --git a/runtime/src/property_call.rs b/runtime/src/property_call.rs index 1c19654..91c0da6 100644 --- a/runtime/src/property_call.rs +++ b/runtime/src/property_call.rs @@ -3,7 +3,7 @@ use crate::helpers::{ffi_native_type_from_signature, strip_generic_suffix}; use crate::method_call::{is_scalar_return_type, PointerPlan, QiCache}; #[cfg(feature = "classic")] use crate::value::{ - append_struct_field_bytes, ffi_parse_bool_arg, ffi_parse_buffer_arg_with_length, + append_struct_field_bytes, ffi_parse_bool_arg, ffi_parse_f32_arg, ffi_parse_f64_arg, ffi_parse_function_arg, ffi_parse_i16_arg, ffi_parse_i32_arg, ffi_parse_i64_arg, ffi_parse_i8_arg, ffi_parse_isize_arg, ffi_parse_pointer_arg, ffi_parse_query_interface_arg, ffi_parse_string_arg, @@ -1238,6 +1238,7 @@ impl PropertyCall { let is_string_return = self.si.is_string_return; self.reset_args(); + crate::error::clear_argument_error(); let mut queried_interfaces: SmallVec<[IUnknown; 2]> = SmallVec::new(); let mut struct_scratch: Vec> = Vec::new(); // param_index (usize) replaces Option — references self.param_sigs[i] directly, @@ -1287,7 +1288,10 @@ impl PropertyCall { if !init_val.is_undefined() && !init_val.is_null() { match write_v8_value_to_ptr(scope, init_val, ptr, native_type) { Ok(_) => {} - Err(_) => return (call_failure(), std::ptr::null_mut(), Vec::new()), + Err(e) => { + crate::error::set_argument_error(e); + return (call_failure(), std::ptr::null_mut(), Vec::new()); + } } } } @@ -1424,18 +1428,35 @@ impl PropertyCall { } } NativeType::Buffer => { - let parsed = ffi_parse_buffer_arg_with_length(scope, value); - let (buffer_value, byte_length) = match parsed { - Ok(value) => value, - Err(_) => return (call_failure(), std::ptr::null_mut(), Vec::new()), + let element = crate::helpers::ArrayElement::of_signature(param_sig) + .unwrap_or(crate::helpers::ArrayElement::U8); + let array = crate::value::ffi_parse_array_arg(scope, value, element); + let array = array.and_then(|array| { + // A copy can't carry back what the callee writes into a fill array. + if array.copy.is_some() && parameter.is_fill_array() { + Err(element.fill_mismatch()) + } else { + Ok(array) + } + }); + let array = match array { + Ok(array) => array, + Err(e) => { + crate::error::set_argument_error(e); + return (call_failure(), std::ptr::null_mut(), Vec::new()); + } }; - buffer_args.push((value, unsafe { buffer_value.pointer }, byte_length)); - + match array.copy { + Some(bytes) => struct_scratch.push(bytes), + None => buffer_args.push((value, array.pointer, array.byte_length)), + } self.argument_buf.push(NativeValue { - u32_value: byte_length, + u32_value: array.count, }); self.argument_parse_types.push(Some(native_type.clone())); - self.argument_buf.push(buffer_value); + self.argument_buf.push(NativeValue { + pointer: array.pointer, + }); self.argument_parse_types.push(Some(native_type.clone())); continue; } @@ -1486,7 +1507,10 @@ impl PropertyCall { let value = match value { Ok(value) => value, - Err(_) => return (call_failure(), std::ptr::null_mut(), Vec::new()), + Err(e) => { + crate::error::set_argument_error(e); + return (call_failure(), std::ptr::null_mut(), Vec::new()); + } }; self.argument_buf.push(value); @@ -1509,6 +1533,9 @@ impl PropertyCall { } if !crate::value::buffers_unchanged(scope, &buffer_args) { + crate::error::set_argument_error( + "An array argument was detached or resized while the arguments were read", + ); return (call_failure(), std::ptr::null_mut(), Vec::new()); } @@ -1518,7 +1545,10 @@ impl PropertyCall { &self.argument_parse_types, ) { Ok(prep) => prep, - Err(_) => return (call_failure(), std::ptr::null_mut(), Vec::new()), + Err(e) => { + crate::error::set_argument_error(e); + return (call_failure(), std::ptr::null_mut(), Vec::new()); + } }; let call_args = crate::ffi::build_call_args(&prep, &self.argument_buf, &self.si.parameter_types); @@ -1665,6 +1695,7 @@ impl PropertyCall { let is_string_return = self.si.is_string_return; self.reset_args(); + crate::error::clear_argument_error(); let mut queried_interfaces: SmallVec<[IUnknown; 2]> = SmallVec::new(); let mut struct_scratch: Vec> = Vec::new(); let mut out_slots: Vec<(usize, NativeType, usize, Option)> = Vec::new(); @@ -1680,7 +1711,10 @@ impl PropertyCall { Some(v) => dup_arg(env, v), None => match env.get_undefined() { Ok(u) => unsafe { JsUnknown::from_raw_unchecked(env.raw(), u.raw()) }, - Err(_) => return fail3(), + Err(e) => { + crate::error::set_argument_error(e); + return fail3(); + } }, }; let vt = value.get_type().unwrap_or(ValueType::Undefined); @@ -1714,7 +1748,10 @@ impl PropertyCall { if ivt != ValueType::Undefined && ivt != ValueType::Null { match nv::write_js_value_to_ptr(env, &init_val, ptr, native_type) { Ok(_) => {} - Err(_) => return fail3(), + Err(e) => { + crate::error::set_argument_error(e); + return fail3(); + } } } } @@ -1849,17 +1886,37 @@ impl PropertyCall { } } NativeType::Buffer => { - let parsed = nv::napi_parse_buffer_with_length(env, &value); - let (buffer_value, byte_length) = match parsed { - Ok(value) => value, - Err(_) => return fail3(), + let element = crate::helpers::ArrayElement::of_signature(param_sig) + .unwrap_or(crate::helpers::ArrayElement::U8); + let array = nv::napi_parse_array_arg(env, &value, element).and_then(|array| { + // A copy can't carry back what the callee writes into a fill array. + if array.copy.is_some() && parameter.is_fill_array() { + Err(element.fill_mismatch()) + } else { + Ok(array) + } + }); + let array = match array { + Ok(array) => array, + Err(e) => { + crate::error::set_argument_error(e); + return fail3(); + } }; - buffer_args.push((unsafe { napi::NapiRaw::raw(&value) }, unsafe { buffer_value.pointer }, byte_length)); + match array.copy { + Some(bytes) => struct_scratch.push(bytes), + None => { + let raw = unsafe { napi::NapiRaw::raw(&value) }; + buffer_args.push((raw, array.pointer, array.byte_length)) + } + } self.argument_buf.push(NativeValue { - u32_value: byte_length, + u32_value: array.count, }); self.argument_parse_types.push(Some(native_type.clone())); - self.argument_buf.push(buffer_value); + self.argument_buf.push(NativeValue { + pointer: array.pointer, + }); self.argument_parse_types.push(Some(native_type.clone())); continue; } @@ -1906,7 +1963,10 @@ impl PropertyCall { let value = match value { Ok(value) => value, - Err(_) => return fail3(), + Err(e) => { + crate::error::set_argument_error(e); + return fail3(); + } }; self.argument_buf.push(value); @@ -1929,6 +1989,9 @@ impl PropertyCall { } if !nv::buffers_unchanged(env, &buffer_args) { + crate::error::set_argument_error( + "An array argument was detached or resized while the arguments were read", + ); return fail3(); } @@ -1938,7 +2001,10 @@ impl PropertyCall { &self.argument_parse_types, ) { Ok(prep) => prep, - Err(_) => return fail3(), + Err(e) => { + crate::error::set_argument_error(e); + return fail3(); + } }; let call_args = crate::ffi::build_call_args(&prep, &self.argument_buf, &self.si.parameter_types); diff --git a/runtime/src/value.rs b/runtime/src/value.rs index 3711c39..7839590 100644 --- a/runtime/src/value.rs +++ b/runtime/src/value.rs @@ -143,13 +143,12 @@ impl TryFrom<&str> for NativeType { let signature = native_type.trim(); let by_ref_inner = signature.strip_prefix("ByRef ").unwrap_or(signature); - // Array signatures are represented as "T[]" by metadata::Signature. - // Byte arrays are best treated as buffer sources in JS; all other arrays - // are opaque pointers in this bridge. + // Arrays of primitives travel as (count, pointer) over JS memory; other arrays are + // opaque pointers. if let Some(element) = by_ref_inner.strip_suffix("[]") { - return Ok(match element { - "UInt8" | "Uint8" | "Int8" | "Byte" | "SByte" => NativeType::Buffer, - _ => NativeType::Pointer, + return Ok(match crate::helpers::ArrayElement::from_name(element) { + Some(_) => NativeType::Buffer, + None => NativeType::Pointer, }); } @@ -1226,7 +1225,7 @@ pub fn ffi_parse_pointer_arg( std::ptr::null_mut() } else { return Err(type_error( - "Invalid FFI pointer type, expected null, External, or { handle: External|null }", + "Expected a WinRT object or null", )); }; Ok(NativeValue { pointer }) @@ -1330,12 +1329,73 @@ pub fn ffi_parse_buffer_arg_with_length( (std::ptr::null_mut(), 0) } else { return Err(type_error( - "Invalid FFI buffer type, expected null, ArrayBuffer, or ArrayBufferView", + "Expected an ArrayBuffer, a typed array or null", )); }; Ok((NativeValue { pointer }, byte_length)) } +/// The value passed for a `T[]` parameter of primitive `element`: a typed array of the +/// element's kind, an ArrayBuffer or DataView, a plain array (copied, each element converted the +/// way the matching typed array stores it) or null. +#[cfg(feature = "classic")] +pub(crate) fn ffi_parse_array_arg( + scope: &mut v8::PinScope<'_, '_>, + arg: v8::Local, + element: crate::helpers::ArrayElement, +) -> std::result::Result { + use crate::helpers::{ArrayArg, ArrayElement, TypedArrayKind}; + if let Ok(array) = v8::Local::::try_from(arg) { + let length = array.length(); + let mut bytes = Vec::with_capacity(length as usize * element.size()); + for index in 0..length { + let item = array.get_index(scope, index).ok_or_else(|| element.mismatch())?; + if let Ok(big) = v8::Local::::try_from(item) { + element.push_bigint(&mut bytes, big.u64_value().0); + } else if element == ArrayElement::Bool { + element.push_number(&mut bytes, item.boolean_value(scope) as u8 as f64); + } else { + let number = item.number_value(scope).ok_or_else(|| element.mismatch())?; + element.push_number(&mut bytes, number); + } + } + return Ok(ArrayArg::copied(bytes, element)); + } + if arg.is_typed_array() { + let kind = if arg.is_uint8_array() { + TypedArrayKind::U8 + } else if arg.is_uint8_clamped_array() { + TypedArrayKind::U8Clamped + } else if arg.is_int8_array() { + TypedArrayKind::I8 + } else if arg.is_uint16_array() { + TypedArrayKind::U16 + } else if arg.is_int16_array() { + TypedArrayKind::I16 + } else if arg.is_uint32_array() { + TypedArrayKind::U32 + } else if arg.is_int32_array() { + TypedArrayKind::I32 + } else if arg.is_float32_array() { + TypedArrayKind::F32 + } else if arg.is_float64_array() { + TypedArrayKind::F64 + } else if arg.is_big_int64_array() { + TypedArrayKind::I64 + } else if arg.is_big_uint64_array() { + TypedArrayKind::U64 + } else { + TypedArrayKind::Other + }; + if !element.accepts_view(kind) { + return Err(element.mismatch()); + } + } + let (value, byte_length) = + ffi_parse_buffer_arg_with_length(scope, arg).map_err(|_| element.mismatch())?; + ArrayArg::borrowed(unsafe { value.pointer }, byte_length, element) +} + #[cfg(feature = "classic")] #[inline] pub fn ffi_parse_struct_arg( diff --git a/typings/nswinrt-helpers.d.ts b/typings/nswinrt-helpers.d.ts index 4be1290..b65e2ae 100644 --- a/typings/nswinrt-helpers.d.ts +++ b/typings/nswinrt-helpers.d.ts @@ -40,6 +40,28 @@ declare global { ): new (...args: any[]) => Omit; } + interface NSWinRTArrayTypes { + UInt8: Uint8Array; + Byte: Uint8Array; + Boolean: Uint8Array; + Int8: Int8Array; + SByte: Int8Array; + UInt16: Uint16Array; + Char16: Uint16Array; + Int16: Int16Array; + UInt32: Uint32Array; + Int32: Int32Array; + UInt64: BigUint64Array; + Int64: BigInt64Array; + Single: Float32Array; + Double: Float64Array; + } + + interface NSWinRTArray { + /** A zero-filled array of a primitive WinRT element type for `T[]` parameters. */ + create(type: K, count: number): NSWinRTArrayTypes[K]; + } + interface NSWinRTPointer { handle: any | null; isNull(): boolean; @@ -49,6 +71,7 @@ declare global { interface NSWinRTInterop { Pointer: { new(handle?: any | null): NSWinRTPointer }; + Array: NSWinRTArray; pointer(value: any): NSWinRTPointer; isPointer(value: any): boolean; handleOf(value: any): any;