From c8f20469d9b6539895046f6b2c8543f888bcbf9e Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Fri, 6 Nov 2026 03:54:07 -0400 Subject: [PATCH 1/3] feat(runtime): arrays WinRT hands back WinRT-allocated arrays now convert on both engines, where they were refused when the call was built: - T[] return values and received (out T[]) parameters. The callee's (length, data) pair is converted and the CoTaskMem array freed: primitive elements become a typed array of the matching kind (as interop.Array.create allocates), strings and objects a JS array, and structs an array of struct objects. - Boxed arrays (IReferenceArray, e.g. from PropertyValue.Create*Array or a PropertySet value) unbox the same way boxed scalars already do. In the other direction, a typed array, or a plain array of strings, numbers, booleans or WinRT objects, passed for an Object parameter is boxed with PropertyValue, so a returned array can be passed back. Examples: CryptographicBuffer.CopyToByteArray(buffer, out) gives a Uint8Array; DisplayServices.FindAll() gives an array of DisplayId; ListBoxAutomationPeer.GetSelection() gives an array of wrappers. Arrays of strings or objects passed in (String[] parameters) are still unsupported. --- runtime/src/helpers.rs | 168 +++++++++++++++++++++++++- runtime/src/lib.rs | 110 +++++++++++++++++ runtime/src/method_call.rs | 129 +++++++++++++++++++- runtime/src/napi_engine/invoke.rs | 76 ++++++++++++ runtime/src/napi_engine/value.rs | 189 +++++++++++++++++++++++++----- runtime/src/ns_proxy.rs | 18 ++- runtime/src/value.rs | 107 +++++++++++++++-- 7 files changed, 749 insertions(+), 48 deletions(-) diff --git a/runtime/src/helpers.rs b/runtime/src/helpers.rs index f4f26ac..a9a9db9 100644 --- a/runtime/src/helpers.rs +++ b/runtime/src/helpers.rs @@ -107,12 +107,66 @@ pub fn ireference_inner_type(sig: &str) -> Option<&str> { /// `(count, pointer)`, or pointers to both for a received (`ByRef`) array. Only byte arrays get /// that pair here, and any other array would shift every later argument. pub(crate) fn parameter_layout_supported(signature: &str) -> bool { - match signature.trim().strip_suffix("[]") { + let signature = signature.trim(); + if let Some(received) = signature.strip_prefix("ByRef ").and_then(|s| s.strip_suffix("[]")) { + return crate::classify_array(received).is_some(); + } + match signature.strip_suffix("[]") { None => true, Some(element) => ArrayElement::from_name(element).is_some(), } } +/// The array a boxed `IReferenceArray` holds: its conversion, length and WinRT-allocated +/// data, which the caller converts and frees. `None` for a scalar box. +pub(crate) fn boxed_array( + value: &windows::Foundation::IPropertyValue, + kind: windows::Foundation::PropertyType, +) -> Option<(crate::ReturnKind, u32, *mut std::ffi::c_void)> { + use windows::core::Interface; + use windows::Foundation::PropertyType as P; + type GetArray = unsafe extern "system" fn( + *mut std::ffi::c_void, + *mut u32, + *mut *mut std::ffi::c_void, + ) -> windows::core::HRESULT; + let v = value.vtable(); + let (element, get) = match kind { + P::UInt8Array => ("UInt8", v.GetUInt8Array as usize), + P::Int16Array => ("Int16", v.GetInt16Array as usize), + P::UInt16Array => ("UInt16", v.GetUInt16Array as usize), + P::Int32Array => ("Int32", v.GetInt32Array as usize), + P::UInt32Array => ("UInt32", v.GetUInt32Array as usize), + P::Int64Array => ("Int64", v.GetInt64Array as usize), + P::UInt64Array => ("UInt64", v.GetUInt64Array as usize), + P::SingleArray => ("Single", v.GetSingleArray as usize), + P::DoubleArray => ("Double", v.GetDoubleArray as usize), + P::Char16Array => ("Char16", v.GetChar16Array as usize), + P::BooleanArray => ("Boolean", v.GetBooleanArray as usize), + P::StringArray => ("String", v.GetStringArray as usize), + P::InspectableArray => ("Object", v.GetInspectableArray as usize), + P::GuidArray => ("Guid", v.GetGuidArray as usize), + P::DateTimeArray => ("Windows.Foundation.DateTime", v.GetDateTimeArray as usize), + P::TimeSpanArray => ("Windows.Foundation.TimeSpan", v.GetTimeSpanArray as usize), + P::PointArray => ("Windows.Foundation.Point", v.GetPointArray as usize), + P::SizeArray => ("Windows.Foundation.Size", v.GetSizeArray as usize), + P::RectArray => ("Windows.Foundation.Rect", v.GetRectArray as usize), + _ => return None, + }; + let kind = crate::classify_array(element)?; + // Every Get*Array slot has the same (length, data) out-parameter ABI. + let get = unsafe { std::mem::transmute::(get) }; + let mut length = 0u32; + let mut data: *mut std::ffi::c_void = std::ptr::null_mut(); + unsafe { get(value.as_raw(), &mut length, &mut data) }.ok().ok()?; + Some((kind, length, data)) +} + +/// The element of a received array parameter (`ByRef T[]`), which takes two ABI slots. +pub(crate) fn received_array_element(signature: &str) -> Option<&str> { + signature.trim().strip_prefix("ByRef ")?.strip_suffix("[]") +} + /// 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). @@ -151,6 +205,23 @@ impl ArrayElement { }) } + pub(crate) fn from_native(native_type: &NativeType) -> Option { + Some(match native_type { + NativeType::U8 => Self::U8, + NativeType::I8 => Self::I8, + NativeType::U16 => Self::U16, + NativeType::I16 => Self::I16, + NativeType::U32 => Self::U32, + NativeType::I32 => Self::I32, + NativeType::U64 => Self::U64, + NativeType::I64 => Self::I64, + NativeType::F32 => Self::F32, + NativeType::F64 => Self::F64, + NativeType::Bool => Self::Bool, + _ => 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 { @@ -312,6 +383,52 @@ impl ArrayArg { } } +/// The values of a JS array or typed array passed for an `Object` parameter. +pub(crate) enum ArrayValues { + Typed(TypedArrayKind, Vec), + Strings(Vec), + Numbers(Vec), + Booleans(Vec), + Objects(Vec>), +} + +/// `values` boxed as the `IReferenceArray` that `PropertyValue` creates, the inverse of +/// unboxing a returned array. `None` for Int8 and Float16 views, which it has no array for. +pub(crate) fn box_array(values: ArrayValues) -> Option { + use windows::Foundation::PropertyValue as P; + use TypedArrayKind as K; + fn read(bytes: &[u8], from: fn([u8; N]) -> T) -> Vec { + bytes.chunks_exact(N).map(|c| from(c.try_into().unwrap())).collect() + } + let boxed = match values { + ArrayValues::Typed(kind, bytes) => match kind { + K::U8 | K::U8Clamped => P::CreateUInt8Array(&bytes), + K::I16 => P::CreateInt16Array(&read(&bytes, i16::from_le_bytes)), + K::U16 => P::CreateUInt16Array(&read(&bytes, u16::from_le_bytes)), + K::I32 => P::CreateInt32Array(&read(&bytes, i32::from_le_bytes)), + K::U32 => P::CreateUInt32Array(&read(&bytes, u32::from_le_bytes)), + K::I64 => P::CreateInt64Array(&read(&bytes, i64::from_le_bytes)), + K::U64 => P::CreateUInt64Array(&read(&bytes, u64::from_le_bytes)), + K::F32 => P::CreateSingleArray(&read(&bytes, f32::from_le_bytes)), + K::F64 => P::CreateDoubleArray(&read(&bytes, f64::from_le_bytes)), + K::I8 | K::Other => return None, + }, + ArrayValues::Strings(strings) => P::CreateStringArray(&strings), + ArrayValues::Numbers(numbers) => { + let int32 = + |n: &f64| n.trunc() == *n && (i32::MIN as f64..=i32::MAX as f64).contains(n); + if numbers.iter().all(int32) { + P::CreateInt32Array(&numbers.iter().map(|&n| n as i32).collect::>()) + } else { + P::CreateDoubleArray(&numbers) + } + } + ArrayValues::Booleans(flags) => P::CreateBooleanArray(&flags), + ArrayValues::Objects(objects) => P::CreateInspectableArray(&objects), + }; + boxed.ok() +} + /// Typed array kinds, shared by both engines' checks. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum TypedArrayKind { @@ -439,6 +556,55 @@ mod array_element_tests { assert!(ArrayArg::borrowed(std::ptr::null_mut(), 6, ArrayElement::I32).is_err()); } + #[test] + fn received_arrays_classify_by_element() { + use crate::ReturnKind; + let size = |element: &str| match crate::classify_array(element) { + Some(ReturnKind::Array { size, .. }) => Some(size), + _ => None, + }; + assert_eq!(size("Int32"), Some(4)); + assert_eq!(size("Boolean"), Some(1)); + assert_eq!(size("String"), Some(std::mem::size_of::())); + assert_eq!(size("Object"), Some(std::mem::size_of::())); + assert_eq!(size("Windows.Foundation.Uri"), Some(std::mem::size_of::())); + assert_eq!(size("Windows.Foundation.Point"), Some(8)); + assert_eq!(size("Guid"), Some(16)); + assert_eq!(size("Var!0"), None); + assert!(parameter_layout_supported("ByRef Int32[]")); + assert!(parameter_layout_supported("ByRef Windows.Foundation.Uri[]")); + assert!(!parameter_layout_supported("ByRef Var!0[]")); + } + + #[test] + fn boxed_arrays_round_trip() { + use super::{box_array, boxed_array, ArrayValues}; + use windows::core::{Interface, HSTRING}; + use windows::Foundation::IPropertyValue; + let strings = vec![HSTRING::from("a"), HSTRING::from("bc")]; + let boxed = box_array(ArrayValues::Strings(strings)).unwrap(); + let value: IPropertyValue = boxed.cast().unwrap(); + let kind = unsafe { value.Type() }.unwrap(); + let (_, length, data) = boxed_array(&value, kind).unwrap(); + assert_eq!(length, 2); + let items = unsafe { std::slice::from_raw_parts(data as *const usize, 2) }; + let read: Vec = items + .iter() + .map(|&raw| unsafe { std::mem::transmute::(raw) }.to_string_lossy()) + .collect(); + assert_eq!(read, ["a", "bc"]); + unsafe { windows::Win32::System::Com::CoTaskMemFree(Some(data as *const _)) }; + + let numbers = box_array(ArrayValues::Numbers(vec![1.0, 2.5])).unwrap(); + let value: IPropertyValue = numbers.cast().unwrap(); + let kind = unsafe { value.Type() }.unwrap(); + assert_eq!(kind, windows::Foundation::PropertyType::DoubleArray); + let ints = box_array(ArrayValues::Numbers(vec![1.0, -2.0])).unwrap(); + let value: IPropertyValue = ints.cast().unwrap(); + assert_eq!(unsafe { value.Type() }.unwrap(), windows::Foundation::PropertyType::Int32Array); + assert!(box_array(ArrayValues::Typed(TypedArrayKind::I8, vec![1])).is_none()); + } + #[test] fn views_must_match_the_element() { assert!(ArrayElement::I32.accepts_view(TypedArrayKind::I32)); diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 538f950..0a3507e 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1091,6 +1091,109 @@ pub(crate) enum ReturnKind { value: Box, size: usize, }, + /// A `T[]` WinRT allocates: `(length, data)` land in the return buffer, each `size`-byte + /// element converts as `element`, and the buffer is freed after conversion. + Array { + element: Box, + #[cfg_attr(not(feature = "napi_engine"), allow(dead_code))] + element_type: Arc, + size: usize, + }, +} + +/// The [`ReturnKind::Array`] for an array of `element`, or `None` when its elements can't be +/// converted. +pub(crate) fn classify_array(element: &str) -> Option { + use crate::value::NativeType; + let element = crate::property_call::abi_type_arg(element); + let kind = classify_return(element, false); + let size = match &kind { + ReturnKind::Primitive( + NativeType::Void | NativeType::Pointer | NativeType::Buffer | NativeType::Function, + ) => return None, + ReturnKind::Primitive(NativeType::String) => std::mem::size_of::(), + ReturnKind::Primitive(nt) => nt.size(), + ReturnKind::Object { .. } + | ReturnKind::InterfaceObject { .. } + | ReturnKind::DynamicObject => std::mem::size_of::(), + ReturnKind::Struct(declaration) => { + crate::property_call::struct_declaration_size(&*declaration.read()) + } + ReturnKind::Guid => 16, + _ => return None, + }; + (size > 0).then(|| ReturnKind::Array { + element: Box::new(kind), + element_type: Arc::from(element), + size, + }) +} + +/// Reads a received array's `(length, data)` from the callee-written slots and converts it. +/// +/// # Safety +/// `length` and `data` point at the call's out slots, written by a successful call or zeroed. +#[cfg(feature = "classic")] +pub(crate) unsafe fn received_array_from_slots<'a>( + kind: &ReturnKind, + length: *const c_void, + data: *const c_void, + scope: &mut v8::PinScope<'a, '_>, +) -> Local<'a, v8::Value> { + let length = std::ptr::read_unaligned(length as *const u32); + let data = std::ptr::read_unaligned(data as *const *mut c_void); + received_array_value(kind, length, data, scope) +} + +/// Converts `length` elements at `data`, an array WinRT allocated, and frees it: primitives +/// become a typed array, anything else a JS array whose elements take ownership of the strings +/// and objects. +#[cfg(feature = "classic")] +pub(crate) unsafe fn received_array_value<'a>( + kind: &ReturnKind, + length: u32, + data: *mut c_void, + scope: &mut v8::PinScope<'a, '_>, +) -> Local<'a, v8::Value> { + let ReturnKind::Array { element, size, .. } = kind else { + return v8::undefined(scope).into(); + }; + let count = if data.is_null() { 0 } else { length as usize }; + let primitive = match element.as_ref() { + ReturnKind::Primitive(nt) => crate::helpers::ArrayElement::from_native(nt), + _ => None, + }; + let value = match primitive { + Some(primitive) => { + let bytes = if count == 0 { + Vec::new() + } else { + std::slice::from_raw_parts(data as *const u8, count * size).to_vec() + }; + crate::value::typed_array_from_bytes(scope, primitive, bytes) + } + None => { + let array = v8::Array::new(scope, count as i32); + for index in 0..count { + let slot = (data as *mut u8).add(index * size) as *mut c_void; + let item = match element.as_ref() { + ReturnKind::Object { .. } + | ReturnKind::InterfaceObject { .. } + | ReturnKind::DynamicObject => { + std::ptr::read_unaligned(slot as *const *mut c_void) + } + _ => slot, + }; + let item = return_value_from_kind(element, item, None, scope); + array.set_index(scope, index as u32, item); + } + array.into() + } + }; + if !data.is_null() { + windows::Win32::System::Com::CoTaskMemFree(Some(data as *const c_void)); + } + value } fn classify_reference_return(inner: &str) -> Option { @@ -1130,6 +1233,9 @@ pub(crate) fn classify_return(return_type: &str, is_void: bool) -> ReturnKind { if return_type == "Object" { return ReturnKind::DynamicObject; } + if let Some(element) = return_type.strip_suffix("[]") { + return classify_array(element).unwrap_or(ReturnKind::Void); + } if return_type.contains('.') { // A closed generic name ("IAsyncOperation`1") resolves to the instance declaration, // which carries the type arguments and the parameterized IID the wrapper needs; only @@ -1189,6 +1295,10 @@ pub(crate) fn return_value_from_kind<'a>( ReturnKind::Struct(declaration) => { create_struct_object_from_raw(declaration.clone(), result, scope).into() } + ReturnKind::Array { .. } => unsafe { + let data = (result as *const u8).add(8) as *const c_void; + received_array_from_slots(kind, result, data, scope) + }, ReturnKind::InterfaceObject { decl, type_name } => { if result.is_null() { v8::null(scope).into() diff --git a/runtime/src/method_call.rs b/runtime/src/method_call.rs index b2b6928..6e1afde 100644 --- a/runtime/src/method_call.rs +++ b/runtime/src/method_call.rs @@ -192,6 +192,10 @@ struct MethodStaticInfo { /// the call's stable return buffer instead of the pointer slot. is_scalar_return: bool, is_string_return: bool, + /// The return is a `T[]`: its `(length, data)` out slots point into the return buffer. + is_array_return: bool, + /// Per parameter, the element conversion of a received array (`ByRef T[]`). + received_arrays: Vec>, } impl MethodStaticInfo { @@ -214,6 +218,8 @@ impl MethodStaticInfo { param_plans: Vec::new(), is_scalar_return: false, is_string_return: false, + is_array_return: false, + received_arrays: Vec::new(), }) } } @@ -708,7 +714,13 @@ impl MethodCall { let mut param_sigs: Vec = Vec::with_capacity(number_of_parameters); parameter_types.push(NativeType::Pointer); - if !crate::helpers::return_layout_supported(&return_type) { + let is_array_return = !is_void && return_type.ends_with("[]"); + let supported = if is_array_return { + matches!(crate::classify_return(&return_type, false), ReturnKind::Array { .. }) + } else { + crate::helpers::return_layout_supported(&return_type) + }; + if !supported { return Self::new_init_error( interface, is_initializer, @@ -760,6 +772,10 @@ impl MethodCall { // space and pass the address for the callee to write into. if parameter.is_out() || signature.trim().starts_with("ByRef ") { parameter_types.push(NativeType::Pointer); + // A received array is (UINT32* length, T** data). + if crate::helpers::received_array_element(&signature).is_some() { + parameter_types.push(NativeType::Pointer); + } } else { // If the parsed parameter is a WinRT `String`, treat its ABI as // `NativeType::String` (handle-sized) rather than the generic @@ -793,6 +809,9 @@ impl MethodCall { if !is_void { parameter_types.push(NativeType::Pointer); } + if is_array_return { + parameter_types.push(NativeType::Pointer); + } } let params = match parameter_types @@ -849,6 +868,10 @@ impl MethodCall { let return_kind = crate::classify_return(&return_type, is_void); let is_scalar_return = is_scalar_return_type(&return_type); let is_string_return = return_type == "String"; + let received_arrays: Vec> = param_sigs + .iter() + .map(|sig| crate::helpers::received_array_element(sig).and_then(crate::classify_array)) + .collect(); // Resolve each Pointer in-param's marshaling plan once; out/ByRef params and // non-pointer types never consult their slot. @@ -885,6 +908,8 @@ impl MethodCall { param_plans, is_scalar_return, is_string_return, + is_array_return, + received_arrays, }); METHOD_STATIC_INFO_CACHE .with(|c| c.borrow_mut().insert(cache_key, Rc::clone(&static_info))); @@ -930,6 +955,7 @@ impl MethodCall { // HSTRING out-params must also land in a stable buffer so the returned // pointer remains valid after this call frame is unwound. let is_string_return = self.si.is_string_return; + let is_array_return = self.si.is_array_return; self.reset_args(); crate::error::clear_argument_error(); @@ -956,6 +982,24 @@ impl MethodCall { // Handle out (ByRef) parameters by allocating stable storage that // the callee can write into. Also treat a missing caller argument // for a `ByRef` signature as an implicit out-slot (e.g. TryParse). + if self.si.received_arrays[i].is_some() { + // (UINT32 length, T* data), written by the callee. + let slot_index = self.argument_buf.len(); + let mut buf: Vec = vec![0u8; 16]; + let base = buf.as_mut_ptr(); + struct_scratch.push(buf); + self.argument_buf.push(NativeValue { pointer: base as *mut c_void }); + self.argument_parse_types.push(None); + self.argument_buf.push(NativeValue { + pointer: unsafe { base.add(8) } as *mut c_void, + }); + self.argument_parse_types.push(None); + let wrapper_obj = + try_unwrap_out_param(scope, args.get(i as i32)).map(|(obj, _)| obj); + out_slots.push((slot_index, native_type.clone(), i, wrapper_obj)); + continue; + } + if parameter.is_out() || is_sig_byref { let slot_index = self.argument_buf.len(); let slot_size = match native_type { @@ -1274,7 +1318,17 @@ impl MethodCall { }; self.argument_parse_types.push(None); } else if !is_void { - if is_value_type || is_scalar_return || is_string_return { + if is_array_return { + // (UINT32 length, T* data) land in the return buffer. + self.return_value_buf[..16].fill(0); + let base = self.return_value_buf.as_mut_ptr(); + self.argument_buf.push(NativeValue { pointer: base as *mut c_void }); + self.argument_parse_types.push(None); + self.argument_buf.push(NativeValue { + pointer: unsafe { base.add(8) } as *mut c_void, + }); + self.argument_parse_types.push(None); + } else if is_value_type || is_scalar_return || is_string_return { let buf_ptr = self.return_value_buf.as_mut_ptr() as *mut c_void; self.argument_buf.push(NativeValue { pointer: buf_ptr }); self.argument_parse_types.push(None); @@ -1373,7 +1427,10 @@ impl MethodCall { } } - if !is_initializer && !is_void && (is_value_type || is_scalar_return || is_string_return) { + if !is_initializer + && !is_void + && (is_value_type || is_scalar_return || is_string_return || is_array_return) + { // Point result at the scratch buffer — all three categories write // into return_value_buf, so the pointer is stable for the caller. result = self.return_value_buf.as_mut_ptr() as *mut c_void; @@ -1382,6 +1439,19 @@ impl MethodCall { // Marshal out-parameters back into V8 values using the recorded slots. let mut out_values: Vec> = Vec::new(); for (slot_index, parse_native_type, param_idx, wrapper_obj) in out_slots.into_iter() { + if let Some(kind) = &self.si.received_arrays[param_idx] { + let v = unsafe { + let length = self.argument_buf[slot_index].pointer; + let data = self.argument_buf[slot_index + 1].pointer; + crate::received_array_from_slots(kind, length, data, scope) + }; + if let Some(wrapper) = wrapper_obj { + let _ = set_out_param_value(scope, wrapper, v); + } else { + out_values.push(v); + } + continue; + } let storage_ptr = unsafe { self.argument_buf .get(slot_index) @@ -1621,6 +1691,7 @@ impl MethodCall { ); let is_scalar_return = self.si.is_scalar_return; let is_string_return = self.si.is_string_return; + let is_array_return = self.si.is_array_return; self.reset_args(); crate::error::clear_argument_error(); @@ -1640,6 +1711,25 @@ impl MethodCall { let param_sig = &self.si.param_sigs[i]; let is_sig_byref = param_sig.starts_with("ByRef "); + if self.si.received_arrays[i].is_some() { + // (UINT32 length, T* data), written by the callee. + let slot_index = self.argument_buf.len(); + let mut buf: Vec = vec![0u8; 16]; + let base = buf.as_mut_ptr(); + struct_scratch.push(buf); + self.argument_buf.push(NativeValue { pointer: base as *mut c_void }); + self.argument_parse_types.push(None); + self.argument_buf.push(NativeValue { + pointer: unsafe { base.add(8) } as *mut c_void, + }); + self.argument_parse_types.push(None); + let wrapper_obj = arg_at(env, args, i) + .and_then(|value| nv::try_unwrap_out_param(env, &value)) + .map(|(obj, _)| obj); + out_slots.push((slot_index, native_type.clone(), i, wrapper_obj)); + continue; + } + if parameter.is_out() || is_sig_byref { let slot_index = self.argument_buf.len(); let slot_size = match native_type { @@ -1942,7 +2032,17 @@ impl MethodCall { }); self.argument_parse_types.push(None); } else if !is_void { - if is_value_type || is_scalar_return || is_string_return { + if is_array_return { + // (UINT32 length, T* data) land in the return buffer. + self.return_value_buf[..16].fill(0); + let base = self.return_value_buf.as_mut_ptr(); + self.argument_buf.push(NativeValue { pointer: base as *mut c_void }); + self.argument_parse_types.push(None); + self.argument_buf.push(NativeValue { + pointer: unsafe { base.add(8) } as *mut c_void, + }); + self.argument_parse_types.push(None); + } else if is_value_type || is_scalar_return || is_string_return { let buf_ptr = self.return_value_buf.as_mut_ptr() as *mut c_void; self.argument_buf.push(NativeValue { pointer: buf_ptr }); self.argument_parse_types.push(None); @@ -2030,7 +2130,10 @@ impl MethodCall { } } - if !is_initializer && !is_void && (is_value_type || is_scalar_return || is_string_return) { + if !is_initializer + && !is_void + && (is_value_type || is_scalar_return || is_string_return || is_array_return) + { result = self.return_value_buf.as_mut_ptr() as *mut c_void; } @@ -2040,6 +2143,22 @@ impl MethodCall { // ported; until then they surface as externals via the fallback below. let mut out_values: Vec = Vec::new(); for (slot_index, parse_native_type, param_idx, wrapper_obj) in out_slots.into_iter() { + if let Some(kind) = &self.si.received_arrays[param_idx] { + let converted = unsafe { + let length = self.argument_buf[slot_index].pointer; + let data = self.argument_buf[slot_index + 1].pointer; + crate::napi_engine::invoke::received_array_from_slots(env, kind, length, data) + }; + let Ok(v) = converted else { + return fail3(); + }; + if let Some(mut wrapper) = wrapper_obj { + let _ = nv::set_out_param_value(&mut wrapper, v); + } else { + out_values.push(v); + } + continue; + } let storage_ptr = unsafe { self.argument_buf .get(slot_index) diff --git a/runtime/src/napi_engine/invoke.rs b/runtime/src/napi_engine/invoke.rs index f27b0c9..a944376 100644 --- a/runtime/src/napi_engine/invoke.rs +++ b/runtime/src/napi_engine/invoke.rs @@ -59,6 +59,78 @@ fn class_declaration( /// Scalars/strings are read from the stable return buffer; enum returns are the slot value /// itself; reference types resolve to typed instance proxies (external fallback for structs /// and unresolvable types). Shared by method and property invocation. +/// Reads a received array's `(length, data)` from the callee-written slots and converts it. +/// +/// # Safety +/// `length` and `data` point at the call's out slots, written by a successful call or zeroed. +pub(crate) unsafe fn received_array_from_slots( + env: &Env, + kind: &ReturnKind, + length: *const c_void, + data: *const c_void, +) -> Result { + let length = std::ptr::read_unaligned(length as *const u32); + let data = std::ptr::read_unaligned(data as *const *mut c_void); + received_array(env, kind, length, data) +} + +/// Converts `length` elements at `data`, an array WinRT allocated, and frees it. +/// +/// # Safety +/// `data` is null or a CoTaskMem array of `length` elements of `kind`'s element type. +pub(crate) unsafe fn received_array( + env: &Env, + kind: &ReturnKind, + length: u32, + data: *mut c_void, +) -> Result { + let converted = received_array_value(env, kind, length, data); + if !data.is_null() { + windows::Win32::System::Com::CoTaskMemFree(Some(data as *const c_void)); + } + converted +} + +/// Converts `length` elements at `data`, an array WinRT allocated (the caller frees it): +/// primitives become a typed array, anything else a JS array whose elements take ownership of +/// the strings and objects. +unsafe fn received_array_value( + env: &Env, + kind: &ReturnKind, + length: u32, + data: *mut c_void, +) -> Result { + let ReturnKind::Array { element, element_type, size } = kind else { + return Err(type_error("not an array return")); + }; + let count = if data.is_null() { 0 } else { length as usize }; + let primitive = match element.as_ref() { + ReturnKind::Primitive(nt) => crate::helpers::ArrayElement::from_native(nt), + _ => None, + }; + if let Some(primitive) = primitive { + let bytes = if count == 0 { + &[][..] + } else { + std::slice::from_raw_parts(data as *const u8, count * size) + }; + return nv::typed_array_from_bytes(env, primitive, bytes); + } + let mut array = env.create_array_with_length(count).map_err(|e| type_error(e.to_string()))?; + for index in 0..count { + let slot = (data as *mut u8).add(index * size) as *mut c_void; + let item = match element.as_ref() { + ReturnKind::Object { .. } + | ReturnKind::InterfaceObject { .. } + | ReturnKind::DynamicObject => std::ptr::read_unaligned(slot as *const *mut c_void), + _ => slot, + }; + let value = convert_call_result(env, element, false, element_type, HRESULT(0), item)?; + array.set_element(index as u32, value).map_err(|e| type_error(e.to_string()))?; + } + Ok(nv::as_unknown(env, array)) +} + pub(crate) fn convert_call_result( env: &Env, kind: &ReturnKind, @@ -86,6 +158,10 @@ pub(crate) fn convert_call_result( // come back as a derived runtime class; interfaces; enums; structs; Guid; Object; pointer // primitives) takes the string-driven resolution below. match kind { + ReturnKind::Array { .. } => { + let data = unsafe { (result as *const u8).add(8) } as *const c_void; + return unsafe { received_array_from_slots(env, kind, result, data) }; + } ReturnKind::Object { decl, type_name, diff --git a/runtime/src/napi_engine/value.rs b/runtime/src/napi_engine/value.rs index e4d5497..90f09d3 100644 --- a/runtime/src/napi_engine/value.rs +++ b/runtime/src/napi_engine/value.rs @@ -964,6 +964,14 @@ pub fn napi_parse_pointer(env: &Env, arg: &JsUnknown) -> Result Result { + use crate::helpers::ArrayElement as E; + let kind: sys::napi_typedarray_type = match element { + E::I8 => 0, + E::U8 | E::Bool => 1, + E::I16 => 3, + E::U16 | E::Char16 => 4, + E::I32 => 5, + E::U32 => 6, + E::F32 => 7, + E::F64 => 8, + E::I64 => 9, + E::U64 => 10, + }; + let raw_env = env.raw(); + let mut data: *mut c_void = std::ptr::null_mut(); + let mut buffer: sys::napi_value = std::ptr::null_mut(); + let mut view: sys::napi_value = std::ptr::null_mut(); + unsafe { + let status = sys::napi_create_arraybuffer(raw_env, bytes.len(), &mut data, &mut buffer); + if status != sys::Status::napi_ok { + return Err(type_error("Could not allocate an ArrayBuffer for a received array")); + } + if !bytes.is_empty() { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), data as *mut u8, bytes.len()); + } + let count = bytes.len() / element.size(); + let status = sys::napi_create_typedarray(raw_env, kind, count, buffer, 0, &mut view); + if status != sys::Status::napi_ok { + return Err(type_error("Could not create a typed array for a received array")); + } + Ok(JsUnknown::from_raw_unchecked(raw_env, view)) + } +} + /// 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. @@ -1145,7 +1193,7 @@ pub(crate) fn napi_parse_array_arg( arg: &JsUnknown, element: crate::helpers::ArrayElement, ) -> Result { - use crate::helpers::{ArrayArg, ArrayElement, TypedArrayKind}; + use crate::helpers::{ArrayArg, ArrayElement}; let raw_env = env.raw(); let raw = unsafe { arg.raw() }; let mut is_array = false; @@ -1183,33 +1231,7 @@ pub(crate) fn napi_parse_array_arg( } 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 let Some((kind, _, _)) = typed_array_info(env, arg) { if !element.accepts_view(kind) { return Err(element.mismatch()); } @@ -1219,6 +1241,112 @@ pub(crate) fn napi_parse_array_arg( ArrayArg::borrowed(unsafe { value.pointer }, byte_length, element) } +/// The kind, data and byte length of a typed array; `None` for anything else. +fn typed_array_info( + env: &Env, + arg: &JsUnknown, +) -> Option<(crate::helpers::TypedArrayKind, *mut c_void, usize)> { + use crate::helpers::TypedArrayKind; + let raw_env = env.raw(); + let raw = unsafe { arg.raw() }; + 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 { + return None; + } + 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, + }; + let element_size = match kind { + TypedArrayKind::I8 | TypedArrayKind::U8 | TypedArrayKind::U8Clamped => 1, + TypedArrayKind::I16 | TypedArrayKind::U16 => 2, + TypedArrayKind::I32 | TypedArrayKind::U32 | TypedArrayKind::F32 => 4, + TypedArrayKind::I64 | TypedArrayKind::U64 | TypedArrayKind::F64 => 8, + TypedArrayKind::Other => 0, + }; + Some((kind, data, length * element_size)) +} + +/// A JS array or typed array boxed for an `Object` parameter. A plain array boxes when its +/// elements are all strings, all numbers, all booleans or all WinRT objects (or null). +fn box_array_arg(env: &Env, arg: &JsUnknown) -> Option { + use crate::helpers::ArrayValues; + if let Some((kind, data, length)) = typed_array_info(env, arg) { + let bytes = if data.is_null() || length == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(data as *const u8, length) }.to_vec() + }; + return crate::helpers::box_array(ArrayValues::Typed(kind, bytes)); + } + let mut is_array = false; + let status = unsafe { sys::napi_is_array(env.raw(), arg.raw(), &mut is_array) }; + if status != sys::Status::napi_ok || !is_array { + return None; + } + let array: JsObject = unsafe { arg.cast() }; + let length = array.get_array_length().ok()?; + let first = if length > 0 { + array.get_element::(0).ok()?.get_type().ok() + } else { + None + }; + let mut values = match first { + Some(ValueType::String) => ArrayValues::Strings(Vec::new()), + Some(ValueType::Number) => ArrayValues::Numbers(Vec::new()), + Some(ValueType::Boolean) => ArrayValues::Booleans(Vec::new()), + _ => ArrayValues::Objects(Vec::new()), + }; + for index in 0..length { + let item: JsUnknown = array.get_element(index).ok()?; + let kind = item.get_type().ok()?; + match (&mut values, kind) { + (ArrayValues::Strings(out), ValueType::String) => { + let s: JsString = unsafe { item.cast() }; + out.push(HSTRING::from_wide(&string_utf16(s).ok()?)); + } + (ArrayValues::Numbers(out), ValueType::Number) => { + let n: JsNumber = unsafe { item.cast() }; + out.push(n.get_double().ok()?); + } + (ArrayValues::Booleans(out), ValueType::Boolean) => { + let b: JsBoolean = unsafe { item.cast() }; + out.push(b.get_value().ok()?); + } + (ArrayValues::Objects(out), ValueType::Null | ValueType::Undefined) => out.push(None), + (ArrayValues::Objects(out), ValueType::Object) => { + let object: JsObject = unsafe { item.cast() }; + let pointer = try_get_external_handle(env, &object).filter(|p| !p.is_null())?; + let unknown = ManuallyDrop::new(unsafe { IUnknown::from_raw(pointer) }); + out.push(Some(unknown.cast::().ok()?)); + } + _ => return None, + } + } + crate::helpers::box_array(values) +} + pub fn napi_parse_struct( env: &Env, arg: &JsUnknown, @@ -1577,7 +1705,10 @@ pub fn try_unbox_property_value(env: &Env, raw: *mut c_void) -> Option boxed_struct(env, "Windows.Foundation.Rect", &unsafe { pv.GetRect() }.ok()?), PropertyType::TimeSpan => boxed_struct(env, "Windows.Foundation.TimeSpan", &unsafe { pv.GetTimeSpan() }.ok()?), PropertyType::DateTime => boxed_struct(env, "Windows.Foundation.DateTime", &unsafe { pv.GetDateTime() }.ok()?), - _ => None, + kind => { + let (kind, length, data) = crate::helpers::boxed_array(&pv, kind)?; + unsafe { crate::napi_engine::invoke::received_array(env, &kind, length, data) }.ok() + } } } diff --git a/runtime/src/ns_proxy.rs b/runtime/src/ns_proxy.rs index 7b48321..7e5a214 100644 --- a/runtime/src/ns_proxy.rs +++ b/runtime/src/ns_proxy.rs @@ -529,7 +529,10 @@ fn try_unbox_property_value<'a>( PropertyType::Rect => boxed_struct(scope, "Windows.Foundation.Rect", &pv.GetRect().ok()?), PropertyType::TimeSpan => boxed_struct(scope, "Windows.Foundation.TimeSpan", &pv.GetTimeSpan().ok()?), PropertyType::DateTime => boxed_struct(scope, "Windows.Foundation.DateTime", &pv.GetDateTime().ok()?), - _ => None, + kind => { + let (kind, length, data) = crate::helpers::boxed_array(&pv, kind)?; + Some(unsafe { crate::received_array_value(&kind, length, data, scope) }) + } } } @@ -744,7 +747,8 @@ pub(crate) fn handle_named_property_getter( .return_kind() { ReturnKind::Void => None, - kind @ ReturnKind::Reference { .. } => { + kind @ (ReturnKind::Reference { .. } + | ReturnKind::Array { .. }) => { Some(crate::return_value_from_kind( kind, result, None, scope, )) @@ -1080,7 +1084,7 @@ fn instance_method_dispatch( let return_value_opt: Option> = match method.return_kind() { ReturnKind::Void => None, - kind @ ReturnKind::Reference { .. } => { + kind @ (ReturnKind::Reference { .. } | ReturnKind::Array { .. }) => { Some(crate::return_value_from_kind(kind, result, None, scope)) } ReturnKind::Guid => { @@ -1370,7 +1374,7 @@ pub(crate) fn handle_instance_property_getter( let ret_val: Option> = match property_call.return_kind() { ReturnKind::Void => None, - kind @ ReturnKind::Reference { .. } => { + kind @ (ReturnKind::Reference { .. } | ReturnKind::Array { .. }) => { Some(crate::return_value_from_kind(kind, result, None, scope)) } ReturnKind::Guid => { @@ -2198,7 +2202,8 @@ pub(crate) fn create_ns_ctor_instance_object<'a>( } else if !method.is_void() { let ret_v: Option> = match method.return_kind() { ReturnKind::Void => None, - kind @ ReturnKind::Reference { .. } => { + kind @ (ReturnKind::Reference { .. } + | ReturnKind::Array { .. }) => { Some(crate::return_value_from_kind(kind, result, None, scope)) } ReturnKind::Guid => { @@ -2349,7 +2354,8 @@ pub(crate) fn create_ns_ctor_instance_object<'a>( } else if !method.is_void() { let ret_v: Option> = match method.return_kind() { ReturnKind::Void => None, - kind @ ReturnKind::Reference { .. } => Some( + kind @ (ReturnKind::Reference { .. } + | ReturnKind::Array { .. }) => Some( crate::return_value_from_kind(kind, result, None, scope), ), ReturnKind::Guid => { diff --git a/runtime/src/value.rs b/runtime/src/value.rs index 7839590..2efd364 100644 --- a/runtime/src/value.rs +++ b/runtime/src/value.rs @@ -1208,6 +1208,12 @@ pub fn ffi_parse_pointer_arg( } } + if let Some(inspectable) = box_array_arg(scope, arg) { + let ptr = inspectable.as_raw() as *mut c_void; + std::mem::forget(inspectable); + return Ok(NativeValue { pointer: ptr }); + } + if let Some(inspectable) = arg .is_object() .then(|| arg.to_object(scope)) @@ -1335,6 +1341,36 @@ pub fn ffi_parse_buffer_arg_with_length( Ok((NativeValue { pointer }, byte_length)) } +/// A typed array of `element` over `bytes`. +#[cfg(feature = "classic")] +pub(crate) fn typed_array_from_bytes<'a>( + scope: &mut v8::PinScope<'a, '_>, + element: crate::helpers::ArrayElement, + bytes: Vec, +) -> v8::Local<'a, v8::Value> { + use crate::helpers::ArrayElement as E; + let count = bytes.len() / element.size(); + let buffer = if bytes.is_empty() { + v8::ArrayBuffer::new(scope, 0) + } else { + let store = v8::ArrayBuffer::new_backing_store_from_vec(bytes).make_shared(); + v8::ArrayBuffer::with_backing_store(scope, &store) + }; + let view: Option> = match element { + E::U8 | E::Bool => v8::Uint8Array::new(scope, buffer, 0, count).map(Into::into), + E::I8 => v8::Int8Array::new(scope, buffer, 0, count).map(Into::into), + E::U16 | E::Char16 => v8::Uint16Array::new(scope, buffer, 0, count).map(Into::into), + E::I16 => v8::Int16Array::new(scope, buffer, 0, count).map(Into::into), + E::U32 => v8::Uint32Array::new(scope, buffer, 0, count).map(Into::into), + E::I32 => v8::Int32Array::new(scope, buffer, 0, count).map(Into::into), + E::U64 => v8::BigUint64Array::new(scope, buffer, 0, count).map(Into::into), + E::I64 => v8::BigInt64Array::new(scope, buffer, 0, count).map(Into::into), + E::F32 => v8::Float32Array::new(scope, buffer, 0, count).map(Into::into), + E::F64 => v8::Float64Array::new(scope, buffer, 0, count).map(Into::into), + }; + view.unwrap_or_else(|| v8::undefined(scope).into()) +} + /// 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. @@ -1344,7 +1380,7 @@ pub(crate) fn ffi_parse_array_arg( arg: v8::Local, element: crate::helpers::ArrayElement, ) -> std::result::Result { - use crate::helpers::{ArrayArg, ArrayElement, TypedArrayKind}; + use crate::helpers::{ArrayArg, ArrayElement}; if let Ok(array) = v8::Local::::try_from(arg) { let length = array.length(); let mut bytes = Vec::with_capacity(length as usize * element.size()); @@ -1362,7 +1398,21 @@ pub(crate) fn ffi_parse_array_arg( return Ok(ArrayArg::copied(bytes, element)); } if arg.is_typed_array() { - let kind = if arg.is_uint8_array() { + let kind = typed_array_kind(arg); + 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")] +fn typed_array_kind(arg: v8::Local) -> crate::helpers::TypedArrayKind { + use crate::helpers::TypedArrayKind; + { + if arg.is_uint8_array() { TypedArrayKind::U8 } else if arg.is_uint8_clamped_array() { TypedArrayKind::U8Clamped @@ -1386,14 +1436,57 @@ pub(crate) fn ffi_parse_array_arg( TypedArrayKind::U64 } else { TypedArrayKind::Other + } + } +} + +/// A JS array or typed array boxed for an `Object` parameter. A plain array boxes when its +/// elements are all strings, all numbers, all booleans or all WinRT objects (or null). +#[cfg(feature = "classic")] +fn box_array_arg( + scope: &mut v8::PinScope<'_, '_>, + arg: v8::Local, +) -> Option { + use crate::helpers::ArrayValues; + if arg.is_typed_array() { + let kind = typed_array_kind(arg); + let (value, length) = ffi_parse_buffer_arg_with_length(scope, arg).ok()?; + let bytes = match unsafe { value.pointer } { + pointer if pointer.is_null() || length == 0 => Vec::new(), + pointer => unsafe { std::slice::from_raw_parts(pointer as *const u8, length as usize) } + .to_vec(), }; - if !element.accepts_view(kind) { - return Err(element.mismatch()); + return crate::helpers::box_array(ArrayValues::Typed(kind, bytes)); + } + let array = v8::Local::::try_from(arg).ok()?; + let length = array.length(); + let first = (length > 0).then(|| array.get_index(scope, 0)).flatten(); + let mut values = match first { + Some(v) if v.is_string() => ArrayValues::Strings(Vec::new()), + Some(v) if v.is_number() => ArrayValues::Numbers(Vec::new()), + Some(v) if v.is_boolean() => ArrayValues::Booleans(Vec::new()), + _ => ArrayValues::Objects(Vec::new()), + }; + for index in 0..length { + let item = array.get_index(scope, index)?; + match &mut values { + ArrayValues::Strings(out) if item.is_string() => { + out.push(hstring_from_v8_value(scope, item)); + } + ArrayValues::Numbers(out) if item.is_number() => out.push(item.number_value(scope)?), + ArrayValues::Booleans(out) if item.is_boolean() => out.push(item.boolean_value(scope)), + ArrayValues::Objects(out) if item.is_null_or_undefined() => out.push(None), + ArrayValues::Objects(out) if item.is_object() => { + let object = item.to_object(scope)?; + let pointer = try_get_external_handle(scope, object).filter(|p| !p.is_null())?; + let unknown = + ManuallyDrop::new(unsafe { windows::core::IUnknown::from_raw(pointer) }); + out.push(Some(unknown.cast::().ok()?)); + } + _ => return None, } } - let (value, byte_length) = - ffi_parse_buffer_arg_with_length(scope, arg).map_err(|_| element.mismatch())?; - ArrayArg::borrowed(unsafe { value.pointer }, byte_length, element) + crate::helpers::box_array(values) } #[cfg(feature = "classic")] From c8b4c22fa19474f3b2b9dd5ff71544ac0243d446 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Fri, 6 Nov 2026 04:20:17 -0400 Subject: [PATCH 2/3] fix(runtime): release values boxed for Object arguments A JS string, number, boolean, struct shape or (since this branch) array passed for an Object parameter is boxed with PropertyValue, and the box's reference was forgotten so the pointer stayed valid through the call: every such argument leaked its box. Passing a 2 KB string 200k times grew the process by ~0.8 GB more than passing an existing WinRT object the same number of times. Each call now opens a BoxedArgs scope; boxes made while its arguments are parsed are released when the call returns. A call made during argument parsing opens its own scope and releases only its own boxes. Boxing outside any call keeps the reference as before. --- runtime/src/generic_method_call.rs | 1 + runtime/src/method_call.rs | 4 ++ runtime/src/napi_engine/value.rs | 36 ++++++------- runtime/src/property_call.rs | 2 + runtime/src/value.rs | 87 +++++++++++++++++++++++------- 5 files changed, 92 insertions(+), 38 deletions(-) diff --git a/runtime/src/generic_method_call.rs b/runtime/src/generic_method_call.rs index 1db2230..f22db7f 100644 --- a/runtime/src/generic_method_call.rs +++ b/runtime/src/generic_method_call.rs @@ -268,6 +268,7 @@ impl GenericMethodCall { if let Some(hr) = self.init_error { return (hr, std::ptr::null_mut(), Vec::new()); } + let _boxed_args = crate::value::BoxedArgs::begin(); let number_of_abi_parameters = self.number_of_abi_parameters; let mut arguments: Vec = Vec::with_capacity(number_of_abi_parameters); // Track parse-level types for each ABI argument slot. diff --git a/runtime/src/method_call.rs b/runtime/src/method_call.rs index 6e1afde..0f3232d 100644 --- a/runtime/src/method_call.rs +++ b/runtime/src/method_call.rs @@ -959,6 +959,7 @@ impl MethodCall { self.reset_args(); crate::error::clear_argument_error(); + let _boxed_args = crate::value::BoxedArgs::begin(); 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) @@ -1546,6 +1547,7 @@ impl MethodCall { let is_void = self.si.is_void; self.reset_args(); crate::error::clear_argument_error(); + let _boxed_args = crate::value::BoxedArgs::begin(); self.argument_buf.push(NativeValue { pointer: self.interface.as_raw() as *mut c_void, }); @@ -1601,6 +1603,7 @@ impl MethodCall { self.reset_args(); crate::error::clear_argument_error(); + let _boxed_args = crate::value::BoxedArgs::begin(); self.argument_buf.push(NativeValue { pointer: self.interface.as_raw() as *mut c_void, }); @@ -1695,6 +1698,7 @@ impl MethodCall { self.reset_args(); crate::error::clear_argument_error(); + let _boxed_args = crate::value::BoxedArgs::begin(); 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) diff --git a/runtime/src/napi_engine/value.rs b/runtime/src/napi_engine/value.rs index 90f09d3..d074b09 100644 --- a/runtime/src/napi_engine/value.rs +++ b/runtime/src/napi_engine/value.rs @@ -931,9 +931,9 @@ pub fn napi_parse_pointer(env: &Env, arg: &JsUnknown) -> Result Result call box_as_ireference() first. if n == n.trunc() && n >= i32::MIN as f64 && n <= i32::MAX as f64 { if let Ok(inspectable) = PropertyValue::CreateInt32(n as i32) { - let ptr = inspectable.as_raw() as *mut c_void; - std::mem::forget(inspectable); - return Ok(NativeValue { pointer: ptr }); + return Ok(NativeValue { + pointer: crate::value::boxed_arg(inspectable), + }); } } else if let Ok(inspectable) = PropertyValue::CreateDouble(n) { - let ptr = inspectable.as_raw() as *mut c_void; - std::mem::forget(inspectable); - return Ok(NativeValue { pointer: ptr }); + return Ok(NativeValue { + pointer: crate::value::boxed_arg(inspectable), + }); } } if vt == ValueType::Boolean { let b: JsBoolean = unsafe { arg.cast() }; if let Ok(inspectable) = PropertyValue::CreateBoolean(b.get_value().unwrap_or(false)) { - let ptr = inspectable.as_raw() as *mut c_void; - std::mem::forget(inspectable); - return Ok(NativeValue { pointer: ptr }); + return Ok(NativeValue { + pointer: crate::value::boxed_arg(inspectable), + }); } } if vt == ValueType::Object { if let Some(inspectable) = box_array_arg(env, arg) { - let ptr = inspectable.as_raw() as *mut c_void; - std::mem::forget(inspectable); - return Ok(NativeValue { pointer: ptr }); + return Ok(NativeValue { + pointer: crate::value::boxed_arg(inspectable), + }); } } if vt == ValueType::Object { let obj: JsObject = unsafe { arg.cast() }; if let Some(inspectable) = box_struct_shape(env, &obj) { - let ptr = inspectable.as_raw() as *mut c_void; - std::mem::forget(inspectable); - return Ok(NativeValue { pointer: ptr }); + return Ok(NativeValue { + pointer: crate::value::boxed_arg(inspectable), + }); } } diff --git a/runtime/src/property_call.rs b/runtime/src/property_call.rs index 91c0da6..31d16a7 100644 --- a/runtime/src/property_call.rs +++ b/runtime/src/property_call.rs @@ -1239,6 +1239,7 @@ impl PropertyCall { self.reset_args(); crate::error::clear_argument_error(); + let _boxed_args = crate::value::BoxedArgs::begin(); 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, @@ -1696,6 +1697,7 @@ impl PropertyCall { self.reset_args(); crate::error::clear_argument_error(); + let _boxed_args = crate::value::BoxedArgs::begin(); 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(); diff --git a/runtime/src/value.rs b/runtime/src/value.rs index 2efd364..241d065 100644 --- a/runtime/src/value.rs +++ b/runtime/src/value.rs @@ -1174,11 +1174,9 @@ pub fn ffi_parse_pointer_arg( if arg.is_string() { let hstring = hstring_from_v8_value(scope, arg); if let Ok(inspectable) = PropertyValue::CreateString(&hstring) { - let ptr = inspectable.as_raw() as *mut c_void; - // WinRT callees AddRef when storing; our reference is intentionally leaked - // here so the raw pointer stays valid through the FFI call. - std::mem::forget(inspectable); - return Ok(NativeValue { pointer: ptr }); + return Ok(NativeValue { + pointer: crate::value::boxed_arg(inspectable), + }); } } @@ -1188,30 +1186,30 @@ pub fn ffi_parse_pointer_arg( // Callers that need a specific IReference type call box_as_ireference() first. if n == n.trunc() && n >= i32::MIN as f64 && n <= i32::MAX as f64 { if let Ok(inspectable) = PropertyValue::CreateInt32(n as i32) { - let ptr = inspectable.as_raw() as *mut c_void; - std::mem::forget(inspectable); - return Ok(NativeValue { pointer: ptr }); + return Ok(NativeValue { + pointer: crate::value::boxed_arg(inspectable), + }); } } else if let Ok(inspectable) = PropertyValue::CreateDouble(n) { - let ptr = inspectable.as_raw() as *mut c_void; - std::mem::forget(inspectable); - return Ok(NativeValue { pointer: ptr }); + return Ok(NativeValue { + pointer: crate::value::boxed_arg(inspectable), + }); } } if arg.is_boolean() { let b = arg.boolean_value(scope); if let Ok(inspectable) = PropertyValue::CreateBoolean(b) { - let ptr = inspectable.as_raw() as *mut c_void; - std::mem::forget(inspectable); - return Ok(NativeValue { pointer: ptr }); + return Ok(NativeValue { + pointer: crate::value::boxed_arg(inspectable), + }); } } if let Some(inspectable) = box_array_arg(scope, arg) { - let ptr = inspectable.as_raw() as *mut c_void; - std::mem::forget(inspectable); - return Ok(NativeValue { pointer: ptr }); + return Ok(NativeValue { + pointer: crate::value::boxed_arg(inspectable), + }); } if let Some(inspectable) = arg @@ -1220,9 +1218,9 @@ pub fn ffi_parse_pointer_arg( .flatten() .and_then(|obj| box_struct_shape(scope, obj)) { - let ptr = inspectable.as_raw() as *mut c_void; - std::mem::forget(inspectable); - return Ok(NativeValue { pointer: ptr }); + return Ok(NativeValue { + pointer: crate::value::boxed_arg(inspectable), + }); } let pointer = if let Ok(value) = v8::Local::::try_from(arg) { @@ -1237,6 +1235,55 @@ pub fn ffi_parse_pointer_arg( Ok(NativeValue { pointer }) } +thread_local! { + static BOXED_ARGS: std::cell::RefCell> = + const { std::cell::RefCell::new(Vec::new()) }; + static BOXED_DEPTH: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +/// Keeps the values boxed for a call's arguments alive until the call returns. A call made +/// while its arguments are parsed opens its own scope and releases only what it boxed. +pub(crate) struct BoxedArgs { + start: usize, +} + +impl BoxedArgs { + pub(crate) fn begin() -> Self { + let _ = BOXED_DEPTH.try_with(|depth| depth.set(depth.get() + 1)); + let start = BOXED_ARGS.try_with(|boxed| boxed.borrow().len()).unwrap_or(0); + Self { start } + } +} + +impl Drop for BoxedArgs { + fn drop(&mut self) { + let _ = BOXED_DEPTH.try_with(|depth| depth.set(depth.get().saturating_sub(1))); + // Released after the borrow ends, so a Release can't re-enter the list. + let released = BOXED_ARGS + .try_with(|boxed| { + let mut boxed = boxed.borrow_mut(); + let start = self.start.min(boxed.len()); + boxed.split_off(start) + }) + .unwrap_or_default(); + drop(released); + } +} + +/// The pointer of a value boxed for an argument. Inside a call's [`BoxedArgs`] scope the +/// reference is released when the call returns; outside one it is kept for good, since nothing +/// bounds how long the pointer is used. +pub(crate) fn boxed_arg(boxed: windows::core::IInspectable) -> *mut c_void { + let pointer = windows::core::Interface::as_raw(&boxed) as *mut c_void; + let in_call = BOXED_DEPTH.try_with(|depth| depth.get() > 0).unwrap_or(false); + if in_call { + let _ = BOXED_ARGS.try_with(|list| list.borrow_mut().push(boxed)); + } else { + std::mem::forget(boxed); + } + pointer +} + /// `pointer` QI'd to `iid` (the guard holds the new reference), or a TypeError for an object /// that doesn't implement it. Null passes through. pub(crate) fn query_interface_arg( From 6038bfb64654fe501de87310e2897e836287023e Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Fri, 6 Nov 2026 04:24:21 -0400 Subject: [PATCH 3/3] feat(runtime): String[] and object arrays, array properties - String[] and object-array parameters (Object[], IJsonValue[], Uri[], ...) the caller passes or the callee fills, on both engines. A passed JS array becomes HSTRINGs or objects queried to the element interface, released after the call; a fill array (IVector.GetMany) takes a JS array as long as the slots wanted and gets the results written back into it. interop.Array.create('String', n), ('Object', n) or (TypeName, n) returns such an array. - Array-typed property getters (e.g. CertificateExtension.Value) convert like array returns. GenericMethodCall, used only for open generic interfaces, refuses string and object arrays rather than passing them as bytes. --- runtime/src/generic_method_call.rs | 4 +- runtime/src/helpers.rs | 123 ++++++++++++++++++++++++++++- runtime/src/js/interop_array.js | 24 ++++-- runtime/src/method_call.rs | 49 ++++++++++++ runtime/src/napi_engine/value.rs | 90 +++++++++++++++++++++ runtime/src/property_call.rs | 105 ++++++++++++++++++++++-- runtime/src/value.rs | 81 +++++++++++++++++++ typings/nswinrt-helpers.d.ts | 5 ++ 8 files changed, 465 insertions(+), 16 deletions(-) diff --git a/runtime/src/generic_method_call.rs b/runtime/src/generic_method_call.rs index f22db7f..3c4a0cf 100644 --- a/runtime/src/generic_method_call.rs +++ b/runtime/src/generic_method_call.rs @@ -134,7 +134,9 @@ impl GenericMethodCall { let metadata = parameter.metadata().unwrap(); let signature = Signature::to_string(metadata, &type_); - if !crate::helpers::parameter_layout_supported(&signature) { + if !crate::helpers::parameter_layout_supported(&signature) + || crate::helpers::ReferenceElement::of_signature(&signature).is_some() + { init_error = Some(E_NOTIMPL); } diff --git a/runtime/src/helpers.rs b/runtime/src/helpers.rs index a9a9db9..d490224 100644 --- a/runtime/src/helpers.rs +++ b/runtime/src/helpers.rs @@ -113,7 +113,104 @@ pub(crate) fn parameter_layout_supported(signature: &str) -> bool { } match signature.strip_suffix("[]") { None => true, - Some(element) => ArrayElement::from_name(element).is_some(), + Some(element) => { + ArrayElement::from_name(element).is_some() + || ReferenceElement::of_signature(signature).is_some() + } + } +} + +/// The element of a `String[]` or object-array parameter the caller passes or the callee fills: +/// how a filled element converts back, and the interface each passed object is queried for. +pub(crate) struct ReferenceElement { + pub(crate) kind: crate::ReturnKind, + pub(crate) type_name: std::sync::Arc, + /// `None` for strings. + pub(crate) iid: Option, +} + +thread_local! { + static REFERENCE_ELEMENTS: std::cell::RefCell< + ahash::AHashMap>>, + > = std::cell::RefCell::new(ahash::AHashMap::new()); +} + +impl ReferenceElement { + /// The element of a pass or fill `T[]` signature whose `T` is a string or an object type. + pub(crate) fn of_signature(signature: &str) -> Option> { + let signature = signature.trim(); + if signature.starts_with("ByRef ") || !signature.ends_with("[]") { + return None; + } + if let Some(cached) = REFERENCE_ELEMENTS.with(|c| c.borrow().get(signature).cloned()) { + return cached; + } + let element = Self::resolve(&signature[..signature.len() - 2]).map(std::rc::Rc::new); + REFERENCE_ELEMENTS.with(|c| c.borrow_mut().insert(signature.to_string(), element.clone())); + element + } + + fn resolve(element: &str) -> Option { + use crate::ReturnKind; + use metadata::declarations::class_declaration::ClassDeclaration; + use metadata::declarations::declaration::DeclarationKind; + use metadata::declarations::interface_declaration::generic_interface_instance_declaration::GenericInterfaceInstanceDeclaration; + use metadata::declarations::interface_declaration::InterfaceDeclaration; + use windows::core::Interface; + let kind = crate::classify_return(element, false); + let iid = match &kind { + ReturnKind::Primitive(NativeType::String) => None, + ReturnKind::DynamicObject => Some(windows::core::IInspectable::IID), + ReturnKind::Object { decl, .. } | ReturnKind::InterfaceObject { decl, .. } => { + let lock = decl.read(); + let iid = match lock.kind() { + DeclarationKind::Class => lock + .as_any() + .downcast_ref::() + .and_then(|class| class.default_interface()) + .map(|iface| iface.id()), + DeclarationKind::Interface => { + lock.as_any().downcast_ref::().map(|i| i.id()) + } + DeclarationKind::GenericInterfaceInstance => lock + .as_any() + .downcast_ref::() + .map(|i| i.id()), + _ => None, + }; + Some(iid?) + } + _ => return None, + }; + Some(Self { kind, type_name: std::sync::Arc::from(element), iid }) + } +} + +/// The native buffer behind a `String[]` or object-array argument: the strings or (queried) +/// objects passed in, or zeroed slots the callee fills. Dropping it releases what it holds. +pub(crate) enum ReferenceArray { + Strings(Vec), + Objects(Vec>), + Fill(Vec), +} + +impl ReferenceArray { + pub(crate) fn count(&self) -> u32 { + (match self { + Self::Strings(v) => v.len(), + Self::Objects(v) => v.len(), + Self::Fill(v) => v.len(), + }) as u32 + } + + /// The element buffer: `HSTRING` and `Option` are pointer-sized, so each `Vec` is + /// the `T*` the ABI expects. + pub(crate) fn pointer(&mut self) -> *mut std::ffi::c_void { + match self { + Self::Strings(v) => v.as_mut_ptr() as *mut std::ffi::c_void, + Self::Objects(v) => v.as_mut_ptr() as *mut std::ffi::c_void, + Self::Fill(v) => v.as_mut_ptr() as *mut std::ffi::c_void, + } } } @@ -470,8 +567,12 @@ 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("[]") { + let pass_or_fill = by_ref_inner.len() == signature.len(); return match ArrayElement::from_name(element) { Some(_) => NativeType::Buffer, + None if pass_or_fill && ReferenceElement::of_signature(signature).is_some() => { + NativeType::Buffer + } None => NativeType::Pointer, }; } @@ -528,7 +629,7 @@ mod array_element_tests { 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[]")); + assert!(!parameter_layout_supported("Var!0[]")); } #[test] @@ -605,6 +706,24 @@ mod array_element_tests { assert!(box_array(ArrayValues::Typed(TypedArrayKind::I8, vec![1])).is_none()); } + #[test] + fn string_and_object_arrays_are_reference_elements() { + use super::{ffi_native_type_from_signature, ReferenceElement}; + use crate::value::NativeType; + use windows::core::Interface; + let strings = ReferenceElement::of_signature("String[]").unwrap(); + assert!(strings.iid.is_none()); + let objects = ReferenceElement::of_signature("Object[]").unwrap(); + assert_eq!(objects.iid, Some(windows::core::IInspectable::IID)); + let uris = ReferenceElement::of_signature("Windows.Foundation.Uri[]").unwrap(); + assert_eq!(uris.iid, Some(windows::Foundation::Uri::IID)); + assert!(ReferenceElement::of_signature("Int32[]").is_none()); + assert!(ReferenceElement::of_signature("ByRef String[]").is_none()); + assert!(matches!(ffi_native_type_from_signature("String[]"), NativeType::Buffer)); + assert!(matches!(ffi_native_type_from_signature("ByRef String[]"), NativeType::Pointer)); + assert!(parameter_layout_supported("Windows.Data.Json.IJsonValue[]")); + } + #[test] fn views_must_match_the_element() { assert!(ArrayElement::I32.accepts_view(TypedArrayKind::I32)); diff --git a/runtime/src/js/interop_array.js b/runtime/src/js/interop_array.js index 41c859e..c5ec2f3 100644 --- a/runtime/src/js/interop_array.js +++ b/runtime/src/js/interop_array.js @@ -1,5 +1,6 @@ -// 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. +// interop.Array.create(type, count): an array for a WinRT `T[]` parameter to read, or a callee +// to fill, in place. Primitive element types get a zero-filled typed array backed by JS memory; +// strings, `Object` and WinRT types (by name or constructor) get a plain array of nulls. (function () { var interop = globalThis.interop || (globalThis.NSWinRT && globalThis.NSWinRT.interop); if (!interop || (interop.Array && typeof interop.Array.create === 'function')) return; @@ -13,16 +14,27 @@ Single: 'Float32Array', Double: 'Float64Array' }; + function isReferenceType(type, name) { + return name === 'String' || name === 'Object' || name.indexOf('.') > 0 || + (typeof type === 'function' && !type.BYTES_PER_ELEMENT); + } + function create(type, count) { var name = typeof type === 'function' && type.BYTES_PER_ELEMENT ? type.name : String(type); + 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'); + } + if (type != null && isReferenceType(type, name)) { + var array = new Array(count); + for (var i = 0; i < count; i++) array[i] = null; + return array; + } 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'); + '; expected one of ' + Object.keys(kinds).join(', ') + + ', String, Object or a WinRT type'); } return new ctor(count); } diff --git a/runtime/src/method_call.rs b/runtime/src/method_call.rs index 0f3232d..fdf5adf 100644 --- a/runtime/src/method_call.rs +++ b/runtime/src/method_call.rs @@ -973,6 +973,7 @@ impl MethodCall { self.argument_parse_types.push(None); let mut buffer_args = Vec::new(); + let mut reference_arrays = Vec::new(); for (i, native_type) in self.si.parse_parameter_types.iter().enumerate() { let parameter = &self.si.parameters[i]; // Use the pre-computed signature string (cached at construction time) to avoid @@ -1241,6 +1242,25 @@ impl MethodCall { } } NativeType::Buffer => { + let reference = crate::helpers::ReferenceElement::of_signature(param_sig); + if let Some(element) = reference { + let fill = parameter.is_fill_array(); + let parsed = + crate::value::ffi_parse_reference_array(scope, value, &element, fill); + let mut array = match parsed { + Ok(array) => array, + Err(e) => { + crate::error::set_argument_error(e); + return (call_failure(), std::ptr::null_mut(), Vec::new()); + } + }; + self.argument_buf.push(NativeValue { u32_value: array.count() }); + self.argument_parse_types.push(None); + self.argument_buf.push(NativeValue { pointer: array.pointer() }); + self.argument_parse_types.push(None); + reference_arrays.push((value, element, array, fill)); + continue; + } 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); @@ -1438,6 +1458,11 @@ impl MethodCall { } // Marshal out-parameters back into V8 values using the recorded slots. + for (value, element, mut array, fill) in reference_arrays { + if fill && hr.is_ok() { + crate::value::ffi_write_back_reference_array(scope, value, &element, &mut array); + } + } let mut out_values: Vec> = Vec::new(); for (slot_index, parse_native_type, param_idx, wrapper_obj) in out_slots.into_iter() { if let Some(kind) = &self.si.received_arrays[param_idx] { @@ -1710,6 +1735,7 @@ impl MethodCall { self.argument_parse_types.push(None); let mut buffer_args = Vec::new(); + let mut reference_arrays = Vec::new(); for (i, native_type) in self.si.parse_parameter_types.iter().enumerate() { let parameter = &self.si.parameters[i]; let param_sig = &self.si.param_sigs[i]; @@ -1964,6 +1990,24 @@ impl MethodCall { } } NativeType::Buffer => { + let reference = crate::helpers::ReferenceElement::of_signature(param_sig); + if let Some(element) = reference { + let fill = parameter.is_fill_array(); + let parsed = nv::napi_parse_reference_array(env, &value, &element, fill); + let mut array = match parsed { + Ok(array) => array, + Err(e) => { + crate::error::set_argument_error(e); + return fail3(); + } + }; + self.argument_buf.push(NativeValue { u32_value: array.count() }); + self.argument_parse_types.push(None); + self.argument_buf.push(NativeValue { pointer: array.pointer() }); + self.argument_parse_types.push(None); + reference_arrays.push((value, element, array, fill)); + continue; + } 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| { @@ -2145,6 +2189,11 @@ impl MethodCall { // TODO(ns_proxy port): WinRT-typed out params (structs / class instances) get typed // wrappers once create_struct_object_from_raw / create_ns_ctor_instance_object are // ported; until then they surface as externals via the fallback below. + for (value, element, mut array, fill) in reference_arrays { + if fill && hr.is_ok() { + nv::napi_write_back_reference_array(env, &value, &element, &mut array); + } + } let mut out_values: Vec = Vec::new(); for (slot_index, parse_native_type, param_idx, wrapper_obj) in out_slots.into_iter() { if let Some(kind) = &self.si.received_arrays[param_idx] { diff --git a/runtime/src/napi_engine/value.rs b/runtime/src/napi_engine/value.rs index d074b09..461de3d 100644 --- a/runtime/src/napi_engine/value.rs +++ b/runtime/src/napi_engine/value.rs @@ -1145,6 +1145,96 @@ pub fn napi_parse_buffer_with_length( )) } +/// The buffer for a `String[]` or object-array argument: the strings or queried objects of a +/// JS array, or for a fill array as many zeroed slots as the JS array is long. Null passes an +/// empty array. +pub(crate) fn napi_parse_reference_array( + env: &Env, + arg: &JsUnknown, + element: &crate::helpers::ReferenceElement, + fill: bool, +) -> Result { + use crate::helpers::ReferenceArray; + let mismatch = || { + type_error(format!( + "Expected an array or null for parameter type {}[]", + element.type_name + )) + }; + let kind = value_type(arg)?; + if kind == ValueType::Null || kind == ValueType::Undefined { + return Ok(ReferenceArray::Fill(Vec::new())); + } + let mut is_array = false; + let status = unsafe { sys::napi_is_array(env.raw(), arg.raw(), &mut is_array) }; + if status != sys::Status::napi_ok || !is_array { + return Err(mismatch()); + } + let array: JsObject = unsafe { arg.cast() }; + let length = array.get_array_length().map_err(|_| mismatch())? as usize; + if fill { + return Ok(ReferenceArray::Fill(vec![0; length])); + } + match element.iid { + None => { + let mut strings = Vec::with_capacity(length); + for index in 0..length { + let item: JsUnknown = array.get_element(index as u32).map_err(|_| mismatch())?; + match value_type(&item)? { + ValueType::Null | ValueType::Undefined => strings.push(HSTRING::new()), + ValueType::String => { + let s: JsString = unsafe { item.cast() }; + strings.push(HSTRING::from_wide(&string_utf16(s)?)); + } + _ => return Err(type_error(format!("Expected a string at index {index}"))), + } + } + Ok(ReferenceArray::Strings(strings)) + } + Some(iid) => { + let mut objects = Vec::with_capacity(length); + for index in 0..length { + let item: JsUnknown = array.get_element(index as u32).map_err(|_| mismatch())?; + let (_, queried) = napi_parse_query_interface(env, &item, &iid)?; + objects.push(queried); + } + Ok(ReferenceArray::Objects(objects)) + } + } +} + +/// Writes what the callee put in a fill array's slots back into the JS array it came from. +/// Each slot's string or object reference moves to its JS value. +pub(crate) fn napi_write_back_reference_array( + env: &Env, + arg: &JsUnknown, + element: &crate::helpers::ReferenceElement, + array: &mut crate::helpers::ReferenceArray, +) { + let crate::helpers::ReferenceArray::Fill(slots) = array else { + return; + }; + let mut target: JsObject = unsafe { arg.cast() }; + for (index, slot) in slots.iter_mut().enumerate() { + let item = match element.iid { + None => slot as *mut usize as *mut c_void, + Some(_) => *slot as *mut c_void, + }; + let converted = crate::napi_engine::invoke::convert_call_result( + env, + &element.kind, + false, + &element.type_name, + windows::core::HRESULT(0), + item, + ); + *slot = 0; + if let Ok(value) = converted { + let _ = target.set_element(index as u32, value); + } + } +} + /// A typed array of `element` holding a copy of `bytes`. pub(crate) fn typed_array_from_bytes( env: &Env, diff --git a/runtime/src/property_call.rs b/runtime/src/property_call.rs index 31d16a7..0fa0c6d 100644 --- a/runtime/src/property_call.rs +++ b/runtime/src/property_call.rs @@ -316,6 +316,8 @@ struct PropertyStaticInfo { /// the call's stable return buffer instead of the pointer slot. is_scalar_return: bool, is_string_return: bool, + /// The getter returns a `T[]`: its `(length, data)` out slots point into the return buffer. + is_array_return: bool, } /// Resolve the Pointer-parameter marshaling plans for a property/interface call's static info. @@ -689,7 +691,9 @@ impl PropertyCall { let signature = method.return_type(); let return_type = Signature::to_string(method.metadata()?, &signature); - if !crate::helpers::return_layout_supported(&return_type) { + let is_array_return = + matches!(crate::classify_return(&return_type, false), crate::ReturnKind::Array { .. }); + if !is_array_return && !crate::helpers::return_layout_supported(&return_type) { return None; } @@ -750,6 +754,9 @@ impl PropertyCall { if !is_void { parameter_types.push(NativeType::Pointer); } + if is_array_return { + parameter_types.push(NativeType::Pointer); + } } let number_of_abi_parameters = parameter_types.len(); @@ -818,6 +825,7 @@ impl PropertyCall { param_plans, is_scalar_return, is_string_return, + is_array_return, }); PROPERTY_STATIC_INFO_CACHE .with(|c| c.borrow_mut().insert(cache_key, Rc::clone(&static_info))); @@ -899,7 +907,9 @@ impl PropertyCall { let signature = method.return_type(); let raw_return_type = Signature::to_string(method.metadata()?, &signature); let return_type = substitute_type_vars(&raw_return_type, &type_args); - if !crate::helpers::return_layout_supported(&return_type) { + let is_array_return = + matches!(crate::classify_return(&return_type, false), crate::ReturnKind::Array { .. }); + if !is_array_return && !crate::helpers::return_layout_supported(&return_type) { return None; } let return_kind = crate::classify_return(&return_type, is_void); @@ -943,6 +953,9 @@ impl PropertyCall { if !is_void { parameter_types.push(NativeType::Pointer); } + if is_array_return { + parameter_types.push(NativeType::Pointer); + } let number_of_abi_parameters = parameter_types.len(); @@ -1005,6 +1018,7 @@ impl PropertyCall { param_plans, is_scalar_return, is_string_return, + is_array_return, }); INTERFACE_STATIC_INFO_CACHE .with(|c| c.borrow_mut().insert(cache_key, Rc::clone(&static_info))); @@ -1074,7 +1088,9 @@ impl PropertyCall { let signature = method.return_type(); let raw_return_type = Signature::to_string(method.metadata()?, &signature); let return_type = substitute_type_vars(&raw_return_type, &type_args); - if !crate::helpers::return_layout_supported(&return_type) { + let is_array_return = + matches!(crate::classify_return(&return_type, false), crate::ReturnKind::Array { .. }); + if !is_array_return && !crate::helpers::return_layout_supported(&return_type) { return None; } let return_kind = crate::classify_return(&return_type, is_void); @@ -1117,6 +1133,9 @@ impl PropertyCall { if !is_void { parameter_types.push(NativeType::Pointer); } + if is_array_return { + parameter_types.push(NativeType::Pointer); + } let number_of_abi_parameters = parameter_types.len(); @@ -1179,6 +1198,7 @@ impl PropertyCall { param_plans, is_scalar_return, is_string_return, + is_array_return, }); INTERFACE_STATIC_INFO_CACHE .with(|c| c.borrow_mut().insert(cache_key, Rc::clone(&static_info))); @@ -1236,6 +1256,7 @@ impl PropertyCall { // HSTRING out-params must also land in a stable buffer; the local // `result` variable goes out of scope before the caller can read it. let is_string_return = self.si.is_string_return; + let is_array_return = self.si.is_array_return; self.reset_args(); crate::error::clear_argument_error(); @@ -1253,6 +1274,7 @@ impl PropertyCall { self.argument_parse_types.push(None); let mut buffer_args = Vec::new(); + let mut reference_arrays = Vec::new(); for (i, native_type) in self.si.parse_parameter_types.iter().enumerate() { let value = values .get(i) @@ -1429,6 +1451,25 @@ impl PropertyCall { } } NativeType::Buffer => { + let reference = crate::helpers::ReferenceElement::of_signature(param_sig); + if let Some(element) = reference { + let fill = parameter.is_fill_array(); + let parsed = + crate::value::ffi_parse_reference_array(scope, value, &element, fill); + let mut array = match parsed { + Ok(array) => array, + Err(e) => { + crate::error::set_argument_error(e); + return (call_failure(), std::ptr::null_mut(), Vec::new()); + } + }; + self.argument_buf.push(NativeValue { u32_value: array.count() }); + self.argument_parse_types.push(None); + self.argument_buf.push(NativeValue { pointer: array.pointer() }); + self.argument_parse_types.push(None); + reference_arrays.push((value, element, array, fill)); + continue; + } 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); @@ -1521,7 +1562,17 @@ impl PropertyCall { let mut result: *mut c_void = std::ptr::null_mut(); if !self.is_initializer && !is_void { - if is_value_type || is_scalar_return || is_string_return { + if is_array_return { + // (UINT32 length, T* data) land in the return buffer. + self.return_value_buf[..16].fill(0); + let base = self.return_value_buf.as_mut_ptr(); + self.argument_buf.push(NativeValue { pointer: base as *mut c_void }); + self.argument_parse_types.push(None); + self.argument_buf.push(NativeValue { + pointer: unsafe { base.add(8) } as *mut c_void, + }); + self.argument_parse_types.push(None); + } else if is_value_type || is_scalar_return || is_string_return { let buf_ptr = self.return_value_buf.as_mut_ptr() as *mut c_void; self.argument_buf.push(NativeValue { pointer: buf_ptr }); self.argument_parse_types.push(None); @@ -1581,12 +1632,17 @@ impl PropertyCall { if !self.is_initializer && !is_void - && (is_value_type || is_scalar_return || is_string_return) + && (is_value_type || is_scalar_return || is_string_return || is_array_return) { result = self.return_value_buf.as_mut_ptr() as *mut c_void; } // Marshal out-parameters back into V8 values using the recorded slots. + for (value, element, mut array, fill) in reference_arrays { + if fill && hr.is_ok() { + crate::value::ffi_write_back_reference_array(scope, value, &element, &mut array); + } + } let mut out_values: Vec> = Vec::new(); for (slot_index, parse_native_type, param_idx, wrapper_obj) in out_slots.into_iter() { let storage_ptr = unsafe { @@ -1694,6 +1750,7 @@ impl PropertyCall { ); let is_scalar_return = self.si.is_scalar_return; let is_string_return = self.si.is_string_return; + let is_array_return = self.si.is_array_return; self.reset_args(); crate::error::clear_argument_error(); @@ -1708,6 +1765,7 @@ impl PropertyCall { self.argument_parse_types.push(None); let mut buffer_args = Vec::new(); + let mut reference_arrays = Vec::new(); for (i, native_type) in self.si.parse_parameter_types.iter().enumerate() { let value = match values.get(i) { Some(v) => dup_arg(env, v), @@ -1888,6 +1946,24 @@ impl PropertyCall { } } NativeType::Buffer => { + let reference = crate::helpers::ReferenceElement::of_signature(param_sig); + if let Some(element) = reference { + let fill = parameter.is_fill_array(); + let parsed = nv::napi_parse_reference_array(env, &value, &element, fill); + let mut array = match parsed { + Ok(array) => array, + Err(e) => { + crate::error::set_argument_error(e); + return fail3(); + } + }; + self.argument_buf.push(NativeValue { u32_value: array.count() }); + self.argument_parse_types.push(None); + self.argument_buf.push(NativeValue { pointer: array.pointer() }); + self.argument_parse_types.push(None); + reference_arrays.push((value, element, array, fill)); + continue; + } 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| { @@ -1978,7 +2054,17 @@ impl PropertyCall { let mut result: *mut c_void = std::ptr::null_mut(); if !self.is_initializer && !is_void { - if is_value_type || is_scalar_return || is_string_return { + if is_array_return { + // (UINT32 length, T* data) land in the return buffer. + self.return_value_buf[..16].fill(0); + let base = self.return_value_buf.as_mut_ptr(); + self.argument_buf.push(NativeValue { pointer: base as *mut c_void }); + self.argument_parse_types.push(None); + self.argument_buf.push(NativeValue { + pointer: unsafe { base.add(8) } as *mut c_void, + }); + self.argument_parse_types.push(None); + } else if is_value_type || is_scalar_return || is_string_return { let buf_ptr = self.return_value_buf.as_mut_ptr() as *mut c_void; self.argument_buf.push(NativeValue { pointer: buf_ptr }); self.argument_parse_types.push(None); @@ -2033,7 +2119,7 @@ impl PropertyCall { if !self.is_initializer && !is_void - && (is_value_type || is_scalar_return || is_string_return) + && (is_value_type || is_scalar_return || is_string_return || is_array_return) { result = self.return_value_buf.as_mut_ptr() as *mut c_void; } @@ -2042,6 +2128,11 @@ impl PropertyCall { // TODO(ns_proxy port): dotted-sig out params get typed wrappers // (create_struct_object_from_raw / create_ns_ctor_instance_object) once ported; // external fallback until then. Plain sigs keep exact v8 read parity. + for (value, element, mut array, fill) in reference_arrays { + if fill && hr.is_ok() { + nv::napi_write_back_reference_array(env, &value, &element, &mut array); + } + } let mut out_values: Vec = Vec::new(); for (slot_index, parse_native_type, param_idx, wrapper_obj) in out_slots.into_iter() { let storage_ptr = unsafe { diff --git a/runtime/src/value.rs b/runtime/src/value.rs index 241d065..0f90cc2 100644 --- a/runtime/src/value.rs +++ b/runtime/src/value.rs @@ -146,8 +146,11 @@ impl TryFrom<&str> for NativeType { // Arrays of primitives travel as (count, pointer) over JS memory; other arrays are // opaque pointers. if let Some(element) = by_ref_inner.strip_suffix("[]") { + let pass_or_fill = by_ref_inner.len() == signature.len(); + let reference = || crate::helpers::ReferenceElement::of_signature(signature).is_some(); return Ok(match crate::helpers::ArrayElement::from_name(element) { Some(_) => NativeType::Buffer, + None if pass_or_fill && reference() => NativeType::Buffer, None => NativeType::Pointer, }); } @@ -1388,6 +1391,84 @@ pub fn ffi_parse_buffer_arg_with_length( Ok((NativeValue { pointer }, byte_length)) } +/// The buffer for a `String[]` or object-array argument: the strings or queried objects of a +/// JS array, or for a fill array as many zeroed slots as the JS array is long. Null passes an +/// empty array. +#[cfg(feature = "classic")] +pub(crate) fn ffi_parse_reference_array( + scope: &mut v8::PinScope<'_, '_>, + arg: v8::Local, + element: &crate::helpers::ReferenceElement, + fill: bool, +) -> std::result::Result { + use crate::helpers::ReferenceArray; + let mismatch = || { + type_error(format!( + "Expected an array or null for parameter type {}[]", + element.type_name + )) + }; + if arg.is_null_or_undefined() { + return Ok(ReferenceArray::Fill(Vec::new())); + } + let array = v8::Local::::try_from(arg).map_err(|_| mismatch())?; + let length = array.length() as usize; + if fill { + return Ok(ReferenceArray::Fill(vec![0; length])); + } + match element.iid { + None => { + let mut strings = Vec::with_capacity(length); + for index in 0..length { + let item = array.get_index(scope, index as u32).ok_or_else(mismatch)?; + if item.is_null_or_undefined() { + strings.push(HSTRING::new()); + } else if item.is_string() { + strings.push(hstring_from_v8_value(scope, item)); + } else { + return Err(type_error(format!("Expected a string at index {index}"))); + } + } + Ok(ReferenceArray::Strings(strings)) + } + Some(iid) => { + let mut objects = Vec::with_capacity(length); + for index in 0..length { + let item = array.get_index(scope, index as u32).ok_or_else(mismatch)?; + let (_, queried) = ffi_parse_query_interface_arg(scope, item, &iid)?; + objects.push(queried); + } + Ok(ReferenceArray::Objects(objects)) + } + } +} + +/// Writes what the callee put in a fill array's slots back into the JS array it came from. +/// Each slot's string or object reference moves to its JS value. +#[cfg(feature = "classic")] +pub(crate) fn ffi_write_back_reference_array( + scope: &mut v8::PinScope<'_, '_>, + arg: v8::Local, + element: &crate::helpers::ReferenceElement, + array: &mut crate::helpers::ReferenceArray, +) { + let crate::helpers::ReferenceArray::Fill(slots) = array else { + return; + }; + let Ok(target) = v8::Local::::try_from(arg) else { + return; + }; + for (index, slot) in slots.iter_mut().enumerate() { + let item = match element.iid { + None => slot as *mut usize as *mut c_void, + Some(_) => *slot as *mut c_void, + }; + let value = crate::return_value_from_kind(&element.kind, item, None, scope); + *slot = 0; + target.set_index(scope, index as u32, value); + } +} + /// A typed array of `element` over `bytes`. #[cfg(feature = "classic")] pub(crate) fn typed_array_from_bytes<'a>( diff --git a/typings/nswinrt-helpers.d.ts b/typings/nswinrt-helpers.d.ts index b65e2ae..e3cab54 100644 --- a/typings/nswinrt-helpers.d.ts +++ b/typings/nswinrt-helpers.d.ts @@ -60,6 +60,11 @@ declare global { interface NSWinRTArray { /** A zero-filled array of a primitive WinRT element type for `T[]` parameters. */ create(type: K, count: number): NSWinRTArrayTypes[K]; + /** An array of nulls for a `String[]` or object-array parameter the callee fills. */ + create( + type: 'String' | 'Object' | string | (new (...args: any[]) => T), + count: number, + ): (T | null)[]; } interface NSWinRTPointer {