From 146dba327081d0f31b4ea1bf818676c5fbc3b2b5 Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Sat, 17 Oct 2026 07:29:38 -0400 Subject: [PATCH 1/2] perf: plain struct values and cached namespace lookups (classic) Structs: values are plain JS objects built from a per-type template cached per isolate (new struct_value.rs), replacing per-value interceptor objects. - fixes a ~550 byte leak per struct returned from native - fixes every `new Rect(...)` sharing one buffer - nested struct and String fields are now read - spread, JSON, instanceof, toString and expando properties work - `new Rect()` zero-fills; wrong field types throw TypeError - Colors.Red.R: ~3100 -> ~760 ns; held field read: 55 -> ~1 ns Namespaces/enums: the interceptor is non-masking and resolved names become read-only own properties, so repeat lookups skip it. - Windows.UI.Xaml.Controls.Button: 355 -> 27 ns --- integration-tests/tests/namespace_members.rs | 35 ++ integration-tests/tests/struct_values.rs | 97 +++ runtime/src/lib.rs | 629 ++----------------- runtime/src/method_call.rs | 29 +- runtime/src/napi_engine/value.rs | 5 - runtime/src/ns_proxy.rs | 400 +----------- runtime/src/property_call.rs | 21 +- runtime/src/struct_value.rs | 383 +++++++++++ runtime/src/value.rs | 22 +- 9 files changed, 614 insertions(+), 1007 deletions(-) create mode 100644 integration-tests/tests/namespace_members.rs create mode 100644 integration-tests/tests/struct_values.rs create mode 100644 runtime/src/struct_value.rs diff --git a/integration-tests/tests/namespace_members.rs b/integration-tests/tests/namespace_members.rs new file mode 100644 index 0000000..feaf553 --- /dev/null +++ b/integration-tests/tests/namespace_members.rs @@ -0,0 +1,35 @@ +use runtime::Runtime; + +fn eval(rt: &mut Runtime, expr: &str) -> String { + rt.eval_script_to_string(expr) + .unwrap_or_else(|| "".to_string()) +} + +#[test] +fn resolved_members_are_cached() { + let mut rt = Runtime::new("."); + // A miss here means every access builds a new constructor (and a new V8 template). + assert_eq!(eval(&mut rt, "Windows.Foundation.Uri === Windows.Foundation.Uri"), "true"); + assert_eq!(eval(&mut rt, "Windows.UI.Xaml === Windows.UI.Xaml"), "true"); + assert_eq!(eval(&mut rt, "'Uri' in Windows.Foundation"), "true"); + assert_eq!(eval(&mut rt, "Object.keys(Windows.Foundation).length"), "0"); + assert_eq!(eval(&mut rt, "Windows.UI.Xaml.Visibility.Collapsed"), "1"); + assert_eq!(eval(&mut rt, "new Windows.Foundation.Uri('http://x.y/').Host"), "x.y"); +} + +#[test] +fn resolved_members_are_read_only() { + let mut rt = Runtime::new("."); + assert_eq!( + eval(&mut rt, "Windows.Foundation.Uri; Windows.Foundation.Uri = 5; typeof Windows.Foundation.Uri"), + "function" + ); + assert_eq!( + eval( + &mut rt, + "(function () { 'use strict'; try { Windows.Foundation.Uri = 5; return false } catch (e) { return e instanceof TypeError } })()" + ), + "true" + ); + assert_eq!(eval(&mut rt, "Windows.Foundation.myShim = 42; Windows.Foundation.myShim"), "42"); +} diff --git a/integration-tests/tests/struct_values.rs b/integration-tests/tests/struct_values.rs new file mode 100644 index 0000000..78a9b44 --- /dev/null +++ b/integration-tests/tests/struct_values.rs @@ -0,0 +1,97 @@ +use runtime::Runtime; + +fn eval(rt: &mut Runtime, expr: &str) -> String { + rt.eval_script_to_string(expr) + .unwrap_or_else(|| "".to_string()) +} + +#[test] +fn returned_struct_is_plain_object() { + let mut rt = Runtime::new("."); + rt.run_script( + "globalThis.c = Windows.UI.ColorHelper.FromArgb(255, 1, 2, 3);", + "setup.js", + ); + assert_eq!(eval(&mut rt, "JSON.stringify(c)"), r#"{"A":255,"R":1,"G":2,"B":3}"#); + assert_eq!(eval(&mut rt, "JSON.stringify({ ...c })"), r#"{"A":255,"R":1,"G":2,"B":3}"#); + assert_eq!(eval(&mut rt, "Object.keys(c).join()"), "A,R,G,B"); + assert_eq!(eval(&mut rt, "String(c)"), "[object Windows.UI.Color]"); + assert_eq!(eval(&mut rt, "c instanceof Windows.UI.Color"), "true"); + assert_eq!(eval(&mut rt, "c.hasOwnProperty('R') && !('zzz' in c)"), "true"); + assert_eq!(eval(&mut rt, "c.foo = 5; c.foo"), "5"); + assert_eq!( + eval(&mut rt, "Object.getPrototypeOf(c) === Object.getPrototypeOf(Windows.UI.Colors.Red)"), + "true" + ); +} + +#[test] +fn constructor_builds_independent_values() { + let mut rt = Runtime::new("."); + let v = eval( + &mut rt, + "const a = new Windows.Foundation.Rect({ X: 1, Y: 2, Width: 3, Height: 4 }); + const b = new Windows.Foundation.Rect(5, 6, 7, 8); + JSON.stringify([a, b, a instanceof Windows.Foundation.Rect])", + ); + assert_eq!( + v, + r#"[{"X":1,"Y":2,"Width":3,"Height":4},{"X":5,"Y":6,"Width":7,"Height":8},true]"# + ); +} + +#[test] +fn constructor_zero_fills_and_validates() { + let mut rt = Runtime::new("."); + assert_eq!( + eval(&mut rt, "JSON.stringify(new Windows.Foundation.Rect())"), + r#"{"X":0,"Y":0,"Width":0,"Height":0}"# + ); + assert_eq!( + eval(&mut rt, "JSON.stringify(new Windows.Foundation.Rect({ X: 1 }))"), + r#"{"X":1,"Y":0,"Width":0,"Height":0}"# + ); + assert_eq!( + eval( + &mut rt, + "try { new Windows.Foundation.Rect({ X: 'a' }); 'no' } catch (e) { e instanceof TypeError }" + ), + "true" + ); + // Integer fields hold what native will receive. + assert_eq!(eval(&mut rt, "new Windows.UI.Color({ R: 1.7 }).R"), "1"); +} + +#[test] +fn nested_struct_fields_are_read() { + let mut rt = Runtime::new("."); + let v = eval( + &mut rt, + "(() => { try { + const d = Windows.UI.Xaml.DurationHelper.FromTimeSpan({ Duration: 10000000 }); + return JSON.stringify(d); + } catch (e) { return 'skip'; } })()", + ); + if v != "skip" { + assert_eq!(v, r#"{"TimeSpan":{"Duration":10000000},"Type":1}"#); + } +} + +#[test] +fn struct_values_round_trip_through_native() { + let mut rt = Runtime::new("."); + let v = eval( + &mut rt, + "(() => { try { + const s = new Windows.Media.Core.TimedTextStyle(); + s.Padding = { Before: 1, After: 2, Start: 3, End: 4, Unit: 1 }; + const p = s.Padding; + p.End = 9; + s.Padding = p; + return JSON.stringify(s.Padding); + } catch (e) { return 'skip'; } })()", + ); + if v != "skip" { + assert_eq!(v, r#"{"Before":1,"After":2,"Start":3,"End":9,"Unit":1}"#); + } +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 9e9a979..2674ac5 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -48,6 +48,8 @@ mod proxy_manifest_loader; pub mod source_protect; mod struct_reference; #[cfg(feature = "classic")] +mod struct_value; +#[cfg(feature = "classic")] pub mod timers; #[cfg(feature = "classic")] mod type_description; @@ -73,20 +75,11 @@ use crate::ns_proxy::CallbackThisObject; #[cfg(feature = "classic")] use crate::proxy_manifest_loader::SbgManifestLoader; #[cfg(feature = "classic")] -use crate::value::{ - ffi_parse_bool_arg, ffi_parse_buffer_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_string_arg, - ffi_parse_struct_arg, ffi_parse_u16_arg, ffi_parse_u32_arg, ffi_parse_u64_arg, - ffi_parse_u8_arg, ffi_parse_usize_arg, read_value_from_ptr, set_ret_val, MAX_SAFE_INTEGER, - MIN_SAFE_INTEGER, -}; +use crate::value::{read_value_from_ptr, set_ret_val, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER}; use crate::value::NativeType; #[cfg(feature = "classic")] use crate::delegate_invoke::DelegateArg; use crate::delegate_invoke::{DelegateReturn, DelegateSignature}; -#[cfg(feature = "classic")] -use crate::value::NativeValue; use ahash::AHashSet; #[cfg(feature = "classic")] use ahash::{AHashMap, AHasher}; @@ -120,8 +113,6 @@ use metadata::signature::Signature; #[cfg(feature = "classic")] use metadata::value::Value; use parking_lot::lock_api::{MappedRwLockReadGuard, RwLockReadGuard}; -#[cfg(feature = "classic")] -use parking_lot::lock_api::{MappedRwLockWriteGuard, RwLockWriteGuard}; use parking_lot::{RawRwLock, RwLock}; #[cfg(feature = "classic")] use parking_lot::Mutex; @@ -1262,7 +1253,6 @@ pub(crate) struct DeclarationFFI { pub(crate) instance: Option, #[cfg(feature = "classic")] pub(crate) parent: Option>>, - pub(crate) struct_instance: Option<(Vec, Vec)>, /// For inherited static properties/methods: the fully-qualified name of the /// WinRT class that declares them. Resolved lazily via class_activation_factory /// on first access so that constructors don't pay the cost of RoGetActivationFactory @@ -1295,7 +1285,6 @@ impl DeclarationFFI { inner: declaration, instance: None, parent: None, - struct_instance: None, static_factory_class: None, activation: std::cell::OnceCell::new(), qi: crate::method_call::QiCache::new(), @@ -1313,7 +1302,6 @@ impl DeclarationFFI { inner: declaration, instance, parent: None, - struct_instance: None, static_factory_class: None, activation: std::cell::OnceCell::new(), qi: crate::method_call::QiCache::new(), @@ -1330,11 +1318,6 @@ impl DeclarationFFI { pub fn read(&self) -> MappedRwLockReadGuard<'_, RawRwLock, dyn Declaration> { RwLockReadGuard::map(self.inner.read(), |dec| dec) } - - #[cfg(feature = "classic")] - pub fn write(&self) -> MappedRwLockWriteGuard<'_, RawRwLock, dyn Declaration> { - RwLockWriteGuard::map(self.inner.write(), |dec| dec) - } } impl Deref for DeclarationFFI { @@ -2585,11 +2568,16 @@ fn create_ns_object<'a>( let tmpl = FunctionTemplate::new(scope, handle_ns_func); tmpl.set_class_name(name_str); let object_tmpl = tmpl.instance_template(scope); + // Non-masking: names resolved once become own properties (see `memoize_ns_member`). object_tmpl.set_named_property_handler( v8::NamedPropertyHandlerConfiguration::new() .query(handle_named_property_query) .getter(handle_named_property_getter) - .setter(handle_named_property_setter), + .setter(handle_named_property_setter) + .flags( + v8::PropertyHandlerFlags::NON_MASKING + | v8::PropertyHandlerFlags::ONLY_INTERCEPT_STRINGS, + ), ); object_tmpl.set_internal_field_count(2); @@ -7039,545 +7027,14 @@ fn create_ns_ctor_object<'a>( ret.into() } -#[cfg(feature = "classic")] -fn create_ns_struct_ctor_object<'a>( - name: &str, - declaration: Arc>, - scope: &mut v8::PinScope<'a, '_>, -) -> Local<'a, v8::Value> { - let name = v8::String::new(scope, name).unwrap(); - - let ext = DeclarationFFI::new(Arc::clone(&declaration)); - - let ext = Box::into_raw(Box::new(ext)); - - let ext = v8::External::new(scope, ext as _); - - let tmpl = FunctionTemplate::builder( - |scope: &mut v8::PinScope<'_, '_>, - args: v8::FunctionCallbackArguments, - mut retval: v8::ReturnValue| { - let dec = unsafe { args.data().cast::() }; - - let dec = dec.value() as *mut DeclarationFFI; - - let dec = unsafe { &mut *dec }; - - let lock = dec.write(); - - let ext = args.data(); - - let object_tmpl = v8::ObjectTemplate::new(scope); - object_tmpl.set_internal_field_count(1); - - let mut field_args: Vec = Vec::new(); - - let mut field_types: Vec = Vec::new(); - - let Some(struct_dec) = lock.as_any().downcast_ref::() else { - return; - }; - - // Support both positional args `new Thickness(5, 10, 15, 20)` and - // named-field object `new Thickness({ Left: 5, Top: 10, Right: 15, Bottom: 20 })`. - let use_positional = args.length() > 0 && !args.get(0).is_object(); - let named_object: Option> = if !use_positional { - match args.get(0).to_object(scope) { - Some(obj) => Some(obj), - None => { - throw_js_error( - scope, - "Expected object or positional arguments for struct constructor", - ); - return; - } - } - } else { - None - }; - - for (field_idx, field) in struct_dec.fields().iter().enumerate() { - let Some(metadata) = field.base().metadata() else { - continue; - }; - let field_type = Signature::to_string(metadata, &field.type_()); - - let Ok(native_type) = NativeType::try_from(field_type.as_str()) else { - continue; - }; - - field_types.push(native_type.clone()); - - let field_value = if use_positional { - Some(args.get(field_idx as i32)) - } else { - let Some(name) = v8::String::new(scope, field.name()) else { - continue; - }; - named_object.unwrap().get(scope, name.into()) - }; - - match field_value { - None => { - let message = format!("Missing key {}", field.name()); - let message = v8::String::new(scope, message.as_str()).unwrap(); - let error = v8::Exception::error(scope, message.into()); - scope.throw_exception(error); - } - Some(field) => { - let value = match native_type { - NativeType::Void => Err(error::type_error( - "Void is not a valid WinRT struct field type", - )), - NativeType::Bool => ffi_parse_bool_arg(field), - NativeType::U8 => ffi_parse_u8_arg(field), - NativeType::I8 => ffi_parse_i8_arg(field), - NativeType::U16 => ffi_parse_u16_arg(field), - NativeType::I16 => ffi_parse_i16_arg(field), - NativeType::U32 => ffi_parse_u32_arg(field), - NativeType::I32 => ffi_parse_i32_arg(field), - NativeType::U64 => ffi_parse_u64_arg(scope, field), - NativeType::I64 => ffi_parse_i64_arg(scope, field), - NativeType::USize => ffi_parse_usize_arg(scope, field), - NativeType::ISize => ffi_parse_isize_arg(scope, field), - NativeType::F32 => ffi_parse_f32_arg(field), - NativeType::F64 => ffi_parse_f64_arg(field), - NativeType::Pointer => ffi_parse_pointer_arg(scope, field), - NativeType::Buffer => ffi_parse_buffer_arg(scope, field), - NativeType::Function => ffi_parse_function_arg(scope, field), - NativeType::Struct(_) => ffi_parse_struct_arg(scope, field), - NativeType::String => ffi_parse_string_arg(scope, field), - }; - match value { - Ok(value) => { - field_args.push(value); - } - Err(err) => { - let message = err.to_string(); - let message = v8::String::new(scope, message.as_str()).unwrap(); - let error = v8::Exception::error(scope, message.into()); - scope.throw_exception(error); - } - } - } - } - } - - let mut struct_size = 0_usize; - - let params = field_types - .clone() - .into_iter() - .map(|item| { - struct_size = struct_size + item.size(); - libffi::middle::Type::try_from(item) - }) - .collect::, error::AnyError>>(); - - if params.is_err() { - return; - } - - let mut struct_buf: Vec = unsafe { vec![0_u8; struct_size] }; - - struct_buf.shrink_to_fit(); - - let mut position = 0_isize; - - for (field_type, field_value) in field_types.iter().zip(field_args.iter()) { - let size = field_type.size(); - - unsafe { - let buffer = struct_buf.as_mut_ptr(); - let buffer = buffer.offset(position); - - let value: *mut u8 = std::mem::transmute(field_value.as_arg(field_type)); - - let slice = std::slice::from_raw_parts_mut(buffer, size); - - std::ptr::copy(value, slice.as_mut_ptr(), size); - } - - position = position + size as isize; - } - - let name = lock.name().to_string(); - - drop(lock); - - dec.struct_instance = Some((struct_buf, field_types)); - - let _name = v8::String::new(scope, name.as_str()).unwrap(); - - let getter = |scope: &mut v8::PinScope<'_, '_>, - key: Local, - args: v8::PropertyCallbackArguments, - mut rv: v8::ReturnValue| - -> v8::Intercepted { - let key = key.to_rust_string_lossy(scope); - - let this = args.data(); - - let dec = unsafe { this.cast::() }; - - let dec = dec.value() as *mut DeclarationFFI; - - let dec = unsafe { &*dec }; - - let lock = dec.read(); - - if key == "toString" { - let name = lock.name(); - - let name = v8::String::new(scope, name).unwrap(); - let func = v8::Function::builder( - |_scope: &mut v8::PinScope<'_, '_>, - args: v8::FunctionCallbackArguments, - mut retval: v8::ReturnValue| { - retval.set(args.data()); - }, - ) - .data(name.into()) - .build(scope); - - if let Some(f) = func { - rv.set(f.into()); - } - return v8::Intercepted::kYes; - } - - let Some(struct_dec) = lock.as_any().downcast_ref::() else { - return v8::Intercepted::kNo; - }; - - let mut offset = 0; - let instance = dec.struct_instance.as_ref(); - let mut position = 0; - for field in struct_dec.fields() { - if field.name() == key.as_str() { - if let Some((buffer, types)) = instance { - let mut current_field_position = 0; - for field_type in types.iter() { - let size = field_type.size(); - - if position == current_field_position { - unsafe { - let buffer = buffer.as_ptr(); - let buffer = buffer.offset(offset); - - let slice = std::slice::from_raw_parts(buffer, size); - - match field_type { - NativeType::Void => {} - NativeType::Bool => { - let ret: &u8 = std::mem::transmute( - slice.as_ptr() as *const u8 - ); - rv.set_bool(*ret == 1); - } - NativeType::U8 => { - let ret: &u8 = std::mem::transmute( - slice.as_ptr() as *const u8 - ); - rv.set_uint32(*ret as u32); - } - NativeType::I8 => { - let ret: &i8 = std::mem::transmute( - slice.as_ptr() as *const i8 - ); - rv.set_int32(*ret as i32); - } - NativeType::U16 => { - let ret: &u16 = std::mem::transmute( - slice.as_ptr() as *const u16 - ); - rv.set_uint32(*ret as u32); - } - NativeType::I16 => { - let ret: &i16 = std::mem::transmute( - slice.as_ptr() as *const i16 - ); - rv.set_int32(*ret as i32); - } - NativeType::U32 => { - let ret: &u32 = std::mem::transmute( - slice.as_ptr() as *const u32 - ); - rv.set_uint32(*ret); - } - NativeType::I32 => { - let ret: &i32 = std::mem::transmute( - slice.as_ptr() as *const i32 - ); - rv.set_int32(*ret); - } - NativeType::U64 => { - let ret: u64 = - *std::mem::transmute::<*const u64, &u64>( - slice.as_ptr() as *const u64, - ); - - let local_value: v8::Local = - if ret > MAX_SAFE_INTEGER as u64 { - v8::BigInt::new_from_u64(scope, ret).into() - } else { - v8::Number::new(scope, ret as f64).into() - }; - - rv.set(local_value); - } - NativeType::I64 => { - let ret: i64 = - *std::mem::transmute::<*const i64, &i64>( - slice.as_ptr() as *const i64, - ); - let local_value: v8::Local = if ret - > MAX_SAFE_INTEGER as i64 - || ret < MIN_SAFE_INTEGER as i64 - { - v8::BigInt::new_from_i64(scope, ret).into() - } else { - v8::Number::new(scope, ret as f64).into() - }; - rv.set(local_value); - } - NativeType::USize => {} - NativeType::ISize => {} - NativeType::F32 => { - //let ret: &f32 = std::mem::transmute(slice.as_ptr() as *const f32); - - let ret: f32 = if cfg!(target_endian = "big") { - f32::from_be_bytes( - <[u8; 4]>::try_from(slice).unwrap(), - ) - } else { - f32::from_le_bytes( - <[u8; 4]>::try_from(slice).unwrap(), - ) - }; - - rv.set(v8::Number::new(scope, ret as f64).into()); - } - NativeType::F64 => { - let ret: &f64 = std::mem::transmute( - slice.as_ptr() as *const f64 - ); - rv.set(v8::Number::new(scope, *ret).into()); - } - NativeType::Pointer => {} - NativeType::Buffer => {} - NativeType::Function => {} - NativeType::Struct(_) => {} - NativeType::String => { - // TODO - } - } - } - } - - current_field_position = current_field_position + 1; - - offset = offset + size as isize; - } - } - break; - } - position = position + 1; - } - v8::Intercepted::kYes - }; - - let setter = |scope: &mut v8::PinScope<'_, '_>, - key: Local, - value: Local, - args: v8::PropertyCallbackArguments, - _rv: v8::ReturnValue<()>| - -> v8::Intercepted { - let key = key.to_rust_string_lossy(scope); - - let this = args.data(); - - let dec = unsafe { this.cast::() }; - - let dec = dec.value() as *mut DeclarationFFI; - - let instance = unsafe { (&mut *dec).struct_instance.as_mut() }; - - let dec = unsafe { &mut *dec }; - - let lock = dec.write(); - - let Some(struct_dec) = lock.as_any().downcast_ref::() else { - return v8::Intercepted::kNo; - }; - - let mut offset = 0; - - let mut position = 0; - for field in struct_dec.fields() { - if field.name() == key.as_str() { - if let Some((buffer, types)) = instance { - let field = value; - - let mut current_field_position = 0; - for field_type in types.iter() { - let size = field_type.size(); - - if position == current_field_position { - let value = match field_type { - NativeType::Void => Err(error::type_error( - "Void is not a valid WinRT struct field type", - )), - NativeType::Bool => ffi_parse_bool_arg(field), - NativeType::U8 => ffi_parse_u8_arg(field), - NativeType::I8 => ffi_parse_i8_arg(field), - NativeType::U16 => ffi_parse_u16_arg(field), - NativeType::I16 => ffi_parse_i16_arg(field), - NativeType::U32 => ffi_parse_u32_arg(field), - NativeType::I32 => ffi_parse_i32_arg(field), - NativeType::U64 => ffi_parse_u64_arg(scope, field), - NativeType::I64 => ffi_parse_i64_arg(scope, field), - NativeType::USize => ffi_parse_usize_arg(scope, field), - NativeType::ISize => ffi_parse_isize_arg(scope, field), - NativeType::F32 => ffi_parse_f32_arg(field), - NativeType::F64 => ffi_parse_f64_arg(field), - NativeType::Pointer => ffi_parse_pointer_arg(scope, field), - NativeType::Buffer => ffi_parse_buffer_arg(scope, field), - NativeType::Function => { - ffi_parse_function_arg(scope, field) - } - NativeType::Struct(_) => ffi_parse_struct_arg(scope, field), - NativeType::String => ffi_parse_string_arg(scope, field), - }; - match value { - Ok(value) => unsafe { - let buffer = buffer.as_mut_ptr(); - let buffer = buffer.offset(offset); - - let value: *mut u8 = - std::mem::transmute(value.as_arg(field_type)); - - let slice = - std::slice::from_raw_parts_mut(buffer, size); - - std::ptr::copy(value, slice.as_mut_ptr(), size); - }, - Err(err) => { - let message = err.to_string(); - let message = - v8::String::new(scope, message.as_str()).unwrap(); - let error = v8::Exception::error(scope, message.into()); - scope.throw_exception(error); - } - } - } - - current_field_position = current_field_position + 1; - - offset = offset + size as isize; - } - } - break; - } - position = position + 1; - } - v8::Intercepted::kYes - }; - - object_tmpl.set_named_property_handler( - v8::NamedPropertyHandlerConfiguration::new() - .getter(getter) - .setter(setter) - .data(ext), - ); - - let Some(object) = object_tmpl.new_instance(scope) else { - return; - }; - - object.set_internal_field(0, ext.into()); - - retval.set(object.into()); - }, - ) - .data(ext.into()) - .build(scope); - tmpl.set_class_name(name); - - let Some(func) = tmpl.get_function(scope) else { - return v8::undefined(scope).into(); - }; - let ret = func; - - ret.into() -} - #[cfg(feature = "classic")] pub(crate) fn create_struct_object_from_raw<'a>( declaration: Arc>, raw_data: *mut c_void, scope: &mut v8::PinScope<'a, '_>, ) -> Local<'a, v8::Object> { - let fallback = v8::Object::new(scope); - - // Build the byte buffer from raw_data before setting up the template. - let (struct_buf, field_types) = { - let lock = declaration.read(); - let Some(struct_dec) = lock.as_any().downcast_ref::() else { - return fallback; - }; - let mut buf: Vec = Vec::new(); - let mut types: Vec = Vec::new(); - let mut offset: isize = 0; - for field in struct_dec.fields() { - let Some(metadata) = field.base().metadata() else { - continue; - }; - let field_type_str = Signature::to_string(metadata, &field.type_()); - let Ok(native_type) = NativeType::try_from(field_type_str.as_str()) else { - continue; - }; - let size = native_type.size() as isize; - let field_ptr = unsafe { (raw_data as *const u8).offset(offset) as *mut c_void }; - let buf_start = buf.len(); - buf.extend(std::iter::repeat(0u8).take(size as usize)); - unsafe { - std::ptr::copy_nonoverlapping( - field_ptr as *const u8, - buf[buf_start..].as_mut_ptr(), - size as usize, - ); - } - types.push(native_type); - offset += size; - } - (buf, types) - }; - - // Store the byte buffer in a DeclarationFFI so native code can read it back - // via ffi_parse_pointer_arg → try_get_external_handle, AND so the property - // interceptors can keep reads/writes in sync with the same buffer. - let mut dec_ffi = DeclarationFFI::new(Arc::clone(&declaration)); - dec_ffi.struct_instance = Some((struct_buf, field_types)); - let dec_raw = Box::into_raw(Box::new(dec_ffi)); - let ext = v8::External::new(scope, dec_raw as *mut c_void); - - // Use property interceptors so that JS writes (e.g. `size.Width = 300`) go - // through to the buffer, not just to a detached plain JS property. - let tmpl = v8::ObjectTemplate::new(scope); - tmpl.set_internal_field_count(1); - tmpl.set_named_property_handler( - v8::NamedPropertyHandlerConfiguration::new() - .getter(crate::ns_proxy::ns_struct_field_getter) - .setter(crate::ns_proxy::ns_struct_field_setter) - .enumerator(crate::ns_proxy::ns_struct_field_enumerator) - .data(ext.into()), - ); - let Some(object) = tmpl.new_instance(scope) else { - return fallback; - }; - object.set_internal_field(0, ext.into()); - object + crate::struct_value::from_raw(scope, &declaration, raw_data) + .unwrap_or_else(|| v8::Object::new(scope)) } #[cfg(feature = "classic")] @@ -7630,6 +7087,9 @@ fn handle_named_property_setter( args: v8::PropertyCallbackArguments, mut _rv: v8::ReturnValue<()>, ) -> v8::Intercepted { + if NS_MEMOIZING.get() { + return v8::Intercepted::kNo; + } let this = args.holder(); let Some(dec_field) = this.get_internal_field(scope, 0) else { return v8::Intercepted::kNo; @@ -7725,11 +7185,46 @@ fn handle_named_property_setter( if !is_reserved { store.set(scope, key.into(), value); v8::Intercepted::kYes + } else if matches!(kind, DeclarationKind::Namespace | DeclarationKind::Enum) { + // Read-only, same as once memoized; falling through would shadow the metadata. + if args.should_throw_on_error() { + let message = format!("Cannot assign to read only property '{name}'"); + if let Some(message) = v8::String::new(scope, &message) { + let error = v8::Exception::type_error(scope, message); + scope.throw_exception(error); + } + } + v8::Intercepted::kYes } else { v8::Intercepted::kNo } } +#[cfg(feature = "classic")] +thread_local! { + /// Set while `memoize_ns_member` defines a property; the define reaches the setter interceptor. + static NS_MEMOIZING: std::cell::Cell = const { std::cell::Cell::new(false) }; +} + +/// Caches a resolved namespace/enum member as a read-only own property, so later reads skip +/// the (non-masking) interceptor. The side store keeps a copy in case the define fails. +#[cfg(feature = "classic")] +fn memoize_ns_member<'s>( + scope: &v8::PinScope<'s, '_>, + holder: Local, + store: &crate::ns_proxy::SideStore<'s>, + key: Local, + value: Local, +) { + let attrs = v8::PropertyAttribute::READ_ONLY + | v8::PropertyAttribute::DONT_DELETE + | v8::PropertyAttribute::DONT_ENUM; + store.set(scope, key.into(), value); + NS_MEMOIZING.set(true); + holder.define_own_property(scope, key, value, attrs); + NS_MEMOIZING.set(false); +} + #[cfg(feature = "classic")] fn handle_named_property_query( _scope: &mut v8::PinScope<'_, '_>, @@ -7789,21 +7284,9 @@ fn handle_named_property_getter( match lock.kind() { DeclarationKind::Struct => { - let Some(struct_dec) = - lock.as_any().downcast_ref::() - else { - return v8::Intercepted::kNo; - }; - let name = struct_dec.name().to_string(); drop(lock); - - let ret = create_ns_struct_ctor_object( - name.as_str(), - Arc::clone(&dec), - scope, - ); - let ret: Local = ret.into(); - store.set(scope, key.into(), ret); + let ret = crate::struct_value::constructor(scope, &dec); + memoize_ns_member(scope, this, &store, key, ret); rv.set(ret); } DeclarationKind::Class => { @@ -7814,7 +7297,7 @@ fn handle_named_property_getter( scope, ) .into(); - store.set(scope, key.into(), ret); + memoize_ns_member(scope, this, &store, key, ret); rv.set(ret); } DeclarationKind::Interface @@ -7831,13 +7314,13 @@ fn handle_named_property_getter( scope, ) .into(); - store.set(scope, key.into(), ret); + memoize_ns_member(scope, this, &store, key, ret); rv.set(ret); } _ => { let ret: Local = create_ns_object(name.as_str(), declaration, scope).into(); - store.set(scope, key.into(), ret); + memoize_ns_member(scope, this, &store, key, ret); rv.set(ret); } } @@ -7953,14 +7436,14 @@ fn handle_named_property_getter( // Store as v8::Integer (Smi) so the cached value passes // v8::Local::::try_from when used as a setter arg. let ret: Local = v8::Integer::new(scope, value).into(); - store.set(scope, key.into(), ret); + memoize_ns_member(scope, this, &store, key, ret); return v8::Intercepted::kYes; } Value::Uint32(value) => { rv.set_uint32(value); let ret: Local = v8::Integer::new_from_unsigned(scope, value).into(); - store.set(scope, key.into(), ret); + memoize_ns_member(scope, this, &store, key, ret); return v8::Intercepted::kYes; } _ => {} diff --git a/runtime/src/method_call.rs b/runtime/src/method_call.rs index d2be21b..873d44a 100644 --- a/runtime/src/method_call.rs +++ b/runtime/src/method_call.rs @@ -1013,8 +1013,8 @@ impl MethodCall { } PointerPlan::TypeName => { { - // If the JS argument is a class constructor (instance=None, - // struct_instance=None) synthesise a TypeName{Name,Kind=Metadata}. + // If the JS argument is a class constructor (instance=None) + // synthesise a TypeName{Name,Kind=Metadata}. let synthesized: Option<*mut c_void> = 'synth: { if value.is_object() { let obj = value.to_object(scope).unwrap(); @@ -1023,9 +1023,7 @@ impl MethodCall { let dec_ffi = unsafe { &*(ext.value() as *mut DeclarationFFI) }; - if dec_ffi.struct_instance.is_none() - && dec_ffi.instance.is_none() - { + if dec_ffi.instance.is_none() { let class_name = dec_ffi .inner .read() @@ -1056,9 +1054,22 @@ impl MethodCall { }; if let Some(ptr) = synthesized { Ok(NativeValue { pointer: ptr }) + } else if let (Some(obj), Some(decl)) = ( + (value.is_object() && !value.is_function()) + .then(|| value.to_object(scope)) + .flatten(), + crate::struct_value::type_name_declaration(), + ) { + // A TypeName value ({ Name, Kind }). + Ok(NativeValue { + pointer: crate::struct_value::serialize_into( + scope, + &decl, + obj, + &mut struct_scratch, + ), + }) } else { - // Already a TypeName struct instance — ffi_parse_pointer_arg - // extracts struct_instance.buf.as_ptr() via try_get_external_handle. ffi_parse_pointer_arg(scope, value) } } @@ -1658,9 +1669,7 @@ impl MethodCall { if let Ok(dec_ffi) = env.unwrap::(&obj) { - if dec_ffi.struct_instance.is_none() - && dec_ffi.instance.is_none() - { + if dec_ffi.instance.is_none() { let class_name = dec_ffi .inner .read() diff --git a/runtime/src/napi_engine/value.rs b/runtime/src/napi_engine/value.rs index ff8d8b0..d93d31f 100644 --- a/runtime/src/napi_engine/value.rs +++ b/runtime/src/napi_engine/value.rs @@ -657,11 +657,6 @@ pub(crate) fn try_get_external_handle(env: &Env, obj: &JsObject) -> Option<*mut if let Some(ref instance) = dec.instance { return Some(instance.as_raw() as *mut c_void); } - // Struct objects have no COM instance; return a pointer to the raw byte buffer so - // WinRT setters receive the struct data by reference. - if let Some((ref buf, _)) = dec.struct_instance { - return Some(buf.as_ptr() as *mut c_void); - } return Some(std::ptr::null_mut()); } diff --git a/runtime/src/ns_proxy.rs b/runtime/src/ns_proxy.rs index 49f7bf7..49890f1 100644 --- a/runtime/src/ns_proxy.rs +++ b/runtime/src/ns_proxy.rs @@ -3,17 +3,11 @@ use crate::class_helpers::{ find_class_method, find_class_property, find_event_methods, find_interface_event_methods, find_static_property_declaring_class, }; -use crate::error; use crate::generic_method_call::GenericMethodCall; use crate::method_call::MethodCall; use crate::property_call::PropertyCall; use crate::value::{ - ffi_parse_bool_arg, ffi_parse_buffer_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_string_arg, - ffi_parse_struct_arg, ffi_parse_u16_arg, ffi_parse_u32_arg, ffi_parse_u64_arg, - ffi_parse_u8_arg, ffi_parse_usize_arg, read_value_from_ptr, set_ret_val, NativeType, - NativeValue, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, + read_value_from_ptr, set_ret_val, NativeType, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, }; use crate::{ class_activation_factory, resolve_class_factory_from_parent, throw_js_error, DeclarationFFI, @@ -629,17 +623,8 @@ pub(crate) fn handle_named_property_getter( match lock.kind() { DeclarationKind::Struct => { - let struct_dec = - lock.as_any().downcast_ref::().unwrap(); - let name = struct_dec.name().to_string(); drop(lock); - - let ret = create_ns_struct_ctor_object( - name.as_str(), - Arc::clone(&dec), - scope, - ); - let ret: Local = ret.into(); + let ret = crate::struct_value::constructor(scope, &dec); store.set(scope, key.into(), ret); rv.set(ret); } @@ -3813,387 +3798,6 @@ pub(crate) fn create_ns_ctor_object<'a>( func.into() } -pub(crate) fn ns_struct_field_getter( - scope: &mut v8::PinScope<'_, '_>, - key: v8::Local, - args: v8::PropertyCallbackArguments, - mut rv: v8::ReturnValue, -) -> v8::Intercepted { - let key = key.to_rust_string_lossy(scope); - let this = args.data(); - let dec = unsafe { this.cast::() }; - let dec = dec.value() as *mut DeclarationFFI; - let dec = unsafe { &*dec }; - let lock = dec.read(); - - if key == "toString" { - let name = lock.name(); - let name = v8::String::new(scope, name).unwrap(); - let func = v8::Function::builder( - |_scope: &mut v8::PinScope<'_, '_>, - args: v8::FunctionCallbackArguments, - mut retval: v8::ReturnValue| { - retval.set(args.data()); - }, - ) - .data(name.into()) - .build(scope); - rv.set(func.unwrap().into()); - return v8::Intercepted::kYes; - } - - let struct_dec = lock.as_any().downcast_ref::().unwrap(); - let mut offset = 0_isize; - let instance = dec.struct_instance.as_ref(); - let mut position = 0; - for field in struct_dec.fields() { - if field.name() == key.as_str() { - if let Some((buffer, types)) = instance { - let mut current_field_position = 0; - for field_type in types.iter() { - let size = field_type.size(); - if position == current_field_position { - unsafe { - let buffer = buffer.as_ptr().offset(offset); - let slice = std::slice::from_raw_parts(buffer, size); - match field_type { - NativeType::Void => {} - NativeType::Bool => { - let ret: &u8 = std::mem::transmute(slice.as_ptr() as *const u8); - rv.set_bool(*ret == 1); - } - NativeType::U8 => { - let ret: &u8 = std::mem::transmute(slice.as_ptr() as *const u8); - rv.set_uint32(*ret as u32); - } - NativeType::I8 => { - let ret: &i8 = std::mem::transmute(slice.as_ptr() as *const i8); - rv.set_int32(*ret as i32); - } - NativeType::U16 => { - let ret: &u16 = - std::mem::transmute(slice.as_ptr() as *const u16); - rv.set_uint32(*ret as u32); - } - NativeType::I16 => { - let ret: &i16 = - std::mem::transmute(slice.as_ptr() as *const i16); - rv.set_int32(*ret as i32); - } - NativeType::U32 => { - let ret: &u32 = - std::mem::transmute(slice.as_ptr() as *const u32); - rv.set_uint32(*ret); - } - NativeType::I32 => { - let ret: &i32 = - std::mem::transmute(slice.as_ptr() as *const i32); - rv.set_int32(*ret); - } - NativeType::U64 => { - let ret = *std::mem::transmute::<*const u64, &u64>( - slice.as_ptr() as *const u64, - ); - let v: v8::Local = if ret > MAX_SAFE_INTEGER as u64 { - v8::BigInt::new_from_u64(scope, ret).into() - } else { - v8::Number::new(scope, ret as f64).into() - }; - rv.set(v); - } - NativeType::I64 => { - let ret = *std::mem::transmute::<*const i64, &i64>( - slice.as_ptr() as *const i64, - ); - let v: v8::Local = if ret > MAX_SAFE_INTEGER as i64 - || ret < MIN_SAFE_INTEGER as i64 - { - v8::BigInt::new_from_i64(scope, ret).into() - } else { - v8::Number::new(scope, ret as f64).into() - }; - rv.set(v); - } - NativeType::USize | NativeType::ISize => {} - NativeType::F32 => { - let ret: f32 = if cfg!(target_endian = "big") { - f32::from_be_bytes(<[u8; 4]>::try_from(slice).unwrap()) - } else { - f32::from_le_bytes(<[u8; 4]>::try_from(slice).unwrap()) - }; - rv.set(v8::Number::new(scope, ret as f64).into()); - } - NativeType::F64 => { - let ret: &f64 = - std::mem::transmute(slice.as_ptr() as *const f64); - rv.set(v8::Number::new(scope, *ret).into()); - } - NativeType::Pointer - | NativeType::Buffer - | NativeType::Function - | NativeType::Struct(_) - | NativeType::String => {} - } - } - } - current_field_position += 1; - offset += size as isize; - } - } - break; - } - position += 1; - } - v8::Intercepted::kYes -} - -pub(crate) fn ns_struct_field_setter( - scope: &mut v8::PinScope<'_, '_>, - key: v8::Local, - value: v8::Local, - args: v8::PropertyCallbackArguments, - _rv: v8::ReturnValue<()>, -) -> v8::Intercepted { - let key = key.to_rust_string_lossy(scope); - let this = args.data(); - let dec = unsafe { this.cast::() }; - let dec = dec.value() as *mut DeclarationFFI; - let instance = unsafe { (&mut *dec).struct_instance.as_mut() }; - let dec = unsafe { &mut *dec }; - let lock = dec.write(); - let struct_dec = lock.as_any().downcast_ref::().unwrap(); - let mut offset = 0_isize; - let mut position = 0; - for field in struct_dec.fields() { - if field.name() == key.as_str() { - if let Some((buffer, types)) = instance { - let mut current_field_position = 0; - for field_type in types.iter() { - let size = field_type.size(); - if position == current_field_position { - let parsed = match field_type { - NativeType::Void => Err(error::type_error( - "Void is not a valid WinRT struct field type", - )), - NativeType::Bool => ffi_parse_bool_arg(value), - NativeType::U8 => ffi_parse_u8_arg(value), - NativeType::I8 => ffi_parse_i8_arg(value), - NativeType::U16 => ffi_parse_u16_arg(value), - NativeType::I16 => ffi_parse_i16_arg(value), - NativeType::U32 => ffi_parse_u32_arg(value), - NativeType::I32 => ffi_parse_i32_arg(value), - NativeType::U64 => ffi_parse_u64_arg(scope, value), - NativeType::I64 => ffi_parse_i64_arg(scope, value), - NativeType::USize => ffi_parse_usize_arg(scope, value), - NativeType::ISize => ffi_parse_isize_arg(scope, value), - NativeType::F32 => ffi_parse_f32_arg(value), - NativeType::F64 => ffi_parse_f64_arg(value), - NativeType::Pointer => ffi_parse_pointer_arg(scope, value), - NativeType::Buffer => ffi_parse_buffer_arg(scope, value), - NativeType::Function => ffi_parse_function_arg(scope, value), - NativeType::Struct(_) => ffi_parse_struct_arg(scope, value), - NativeType::String => ffi_parse_string_arg(scope, value), - }; - match parsed { - Ok(v) => unsafe { - let buf_ptr = buffer.as_mut_ptr().offset(offset); - let src: *mut u8 = std::mem::transmute(v.as_arg(field_type)); - let slice = std::slice::from_raw_parts_mut(buf_ptr, size); - std::ptr::copy(src, slice.as_mut_ptr(), size); - }, - Err(err) => { - let message = v8::String::new(scope, &err.to_string()).unwrap(); - let error = v8::Exception::error(scope, message.into()); - scope.throw_exception(error); - } - } - } - current_field_position += 1; - offset += size as isize; - } - } - break; - } - position += 1; - } - v8::Intercepted::kYes -} - -pub(crate) fn ns_struct_field_enumerator( - scope: &mut v8::PinScope<'_, '_>, - args: v8::PropertyCallbackArguments, - mut rv: v8::ReturnValue, -) { - let this = args.data(); - let dec = unsafe { this.cast::() }; - let dec = dec.value() as *mut DeclarationFFI; - let dec = unsafe { &*dec }; - let field_names: Vec = { - let lock = dec.read(); - match lock.as_any().downcast_ref::() { - Some(s) => s.fields().iter().map(|f| f.name().to_string()).collect(), - None => return, - } - }; - let elements: Vec> = field_names - .iter() - .filter_map(|name| v8::String::new(scope, name.as_str()).map(|s| s.into())) - .collect(); - let array = v8::Array::new_with_elements(scope, &elements); - rv.set(array); -} - -pub(crate) fn create_ns_struct_ctor_object<'a>( - name: &str, - declaration: Arc>, - scope: &mut v8::PinScope<'a, '_>, -) -> Local<'a, v8::Value> { - let name = v8::String::new(scope, name).unwrap(); - - let ext = DeclarationFFI::new(Arc::clone(&declaration)); - let ext = Box::into_raw(Box::new(ext)); - let ext = v8::External::new(scope, ext as _); - - let tmpl = FunctionTemplate::builder( - |scope: &mut v8::PinScope<'_, '_>, - args: v8::FunctionCallbackArguments, - mut retval: v8::ReturnValue| { - let dec = unsafe { args.data().cast::() }; - let dec = dec.value() as *mut DeclarationFFI; - let dec = unsafe { &mut *dec }; - let lock = dec.write(); - let ext = args.data(); - - let object_tmpl = v8::ObjectTemplate::new(scope); - object_tmpl.set_internal_field_count(1); - - let mut field_args: Vec = Vec::new(); - let mut field_types: Vec = Vec::new(); - - let struct_dec = lock.as_any().downcast_ref::().unwrap(); - - let field_count = struct_dec.fields().len(); - let arg_count = args.length() as usize; - // Positional mode: new Vector3(1, 2, 3) — arg count matches field count and - // the first arg is not a plain object. Object-literal mode: new Vector3({X:1,...}) - let use_positional = arg_count == field_count - && (arg_count > 1 || (arg_count == 1 && !args.get(0).is_object())); - - for (idx, field) in struct_dec.fields().iter().enumerate() { - let field_type = - Signature::to_string(field.base().metadata().unwrap(), &field.type_()); - let native_type = NativeType::try_from(field_type.as_str()).unwrap(); - field_types.push(native_type.clone()); - - let field_value = if use_positional { - Some(args.get(idx as i32)) - } else { - let object = args.get(0).to_object(scope).unwrap(); - let name = v8::String::new(scope, field.name()).unwrap(); - object.get(scope, name.into()) - }; - - match field_value { - None => { - let message = format!("Missing key {}", field.name()); - let message = v8::String::new(scope, message.as_str()).unwrap(); - let error = v8::Exception::error(scope, message.into()); - scope.throw_exception(error); - } - Some(field) => { - let value = match native_type { - NativeType::Void => Err(error::type_error( - "Void is not a valid WinRT struct field type", - )), - NativeType::Bool => ffi_parse_bool_arg(field), - NativeType::U8 => ffi_parse_u8_arg(field), - NativeType::I8 => ffi_parse_i8_arg(field), - NativeType::U16 => ffi_parse_u16_arg(field), - NativeType::I16 => ffi_parse_i16_arg(field), - NativeType::U32 => ffi_parse_u32_arg(field), - NativeType::I32 => ffi_parse_i32_arg(field), - NativeType::U64 => ffi_parse_u64_arg(scope, field), - NativeType::I64 => ffi_parse_i64_arg(scope, field), - NativeType::USize => ffi_parse_usize_arg(scope, field), - NativeType::ISize => ffi_parse_isize_arg(scope, field), - NativeType::F32 => ffi_parse_f32_arg(field), - NativeType::F64 => ffi_parse_f64_arg(field), - NativeType::Pointer => ffi_parse_pointer_arg(scope, field), - NativeType::Buffer => ffi_parse_buffer_arg(scope, field), - NativeType::Function => ffi_parse_function_arg(scope, field), - NativeType::Struct(_) => ffi_parse_struct_arg(scope, field), - NativeType::String => ffi_parse_string_arg(scope, field), - }; - match value { - Ok(value) => { - field_args.push(value); - } - Err(err) => { - let message = err.to_string(); - let message = v8::String::new(scope, message.as_str()).unwrap(); - let error = v8::Exception::error(scope, message.into()); - scope.throw_exception(error); - } - } - } - } - } - - let mut struct_size = 0_usize; - let params = field_types - .clone() - .into_iter() - .map(|item| { - struct_size = struct_size + item.size(); - libffi::middle::Type::try_from(item) - }) - .collect::, error::AnyError>>(); - - assert!(params.is_ok()); - - let mut struct_buf: Vec = unsafe { vec![0_u8; struct_size] }; - struct_buf.shrink_to_fit(); - - let mut position = 0_isize; - for (field_type, field_value) in field_types.iter().zip(field_args.iter()) { - let size = field_type.size(); - unsafe { - let buffer = struct_buf.as_mut_ptr(); - let buffer = buffer.offset(position); - let value: *mut u8 = std::mem::transmute(field_value.as_arg(field_type)); - let slice = std::slice::from_raw_parts_mut(buffer, size); - std::ptr::copy(value, slice.as_mut_ptr(), size); - } - position = position + size as isize; - } - - let name = lock.name().to_string(); - drop(lock); - dec.struct_instance = Some((struct_buf, field_types)); - - let _name = v8::String::new(scope, name.as_str()).unwrap(); - - object_tmpl.set_named_property_handler( - v8::NamedPropertyHandlerConfiguration::new() - .getter(ns_struct_field_getter) - .setter(ns_struct_field_setter) - .enumerator(ns_struct_field_enumerator) - .data(ext), - ); - - let object = object_tmpl.new_instance(scope).unwrap(); - object.set_internal_field(0, ext.into()); - retval.set(object.into()); - }, - ) - .data(ext.into()) - .build(scope); - tmpl.set_class_name(name); - - let func = tmpl.get_function(scope).unwrap(); - func.into() -} - pub(crate) fn init_meta( scope: &mut v8::ContextScope>, context: Local, diff --git a/runtime/src/property_call.rs b/runtime/src/property_call.rs index b06131f..0ea16a7 100644 --- a/runtime/src/property_call.rs +++ b/runtime/src/property_call.rs @@ -1252,11 +1252,22 @@ impl PropertyCall { // Signature classification precomputed into the per-parameter plan at // static-info build time (see method_call::PointerPlan). match &self.si.param_plans[i] { - // TypeName synthesis was never applied on the PropertyCall path - // (parity with the original decision tree). - PointerPlan::Plain | PointerPlan::TypeName => { - ffi_parse_pointer_arg(scope, value) - } + PointerPlan::Plain => ffi_parse_pointer_arg(scope, value), + // A TypeName value ({ Name, Kind }); ctor synthesis is method-call only. + PointerPlan::TypeName => match ( + value.is_object().then(|| value.to_object(scope)).flatten(), + crate::struct_value::type_name_declaration(), + ) { + (Some(obj), Some(decl)) if !value.is_function() => Ok(NativeValue { + pointer: crate::struct_value::serialize_into( + scope, + &decl, + obj, + &mut struct_scratch, + ), + }), + _ => ffi_parse_pointer_arg(scope, value), + }, // IReference parameters: box JS primitives with the correct Create* call // so XAML receives the right typed IPropertyValue (e.g. IReference). PointerPlan::IReference(inner, iid) => { diff --git a/runtime/src/struct_value.rs b/runtime/src/struct_value.rs new file mode 100644 index 0000000..ffe4617 --- /dev/null +++ b/runtime/src/struct_value.rs @@ -0,0 +1,383 @@ +//! WinRT value structs (classic engine): plain JS snapshots built from a per-isolate, +//! per-type template, so all values of a type share one map and the constructor's prototype. + +use crate::helpers::strip_generic_suffix; +use crate::property_call::{align_up, sig_size_align_pub}; +use crate::value::{read_value_from_ptr, NativeType}; +use metadata::declarations::declaration::{Declaration, DeclarationKind}; +use metadata::declarations::struct_declaration::StructDeclaration; +use metadata::meta_data_reader::MetadataReader; +use metadata::signature::Signature; +use parking_lot::RwLock; +use std::cell::{OnceCell, RefCell}; +use std::ffi::c_void; +use std::mem::ManuallyDrop; +use std::rc::Rc; +use std::sync::Arc; +use windows::core::HSTRING; + +const MAX_SAFE_INTEGER: f64 = 9007199254740991.0; + +enum FieldKind { + /// Primitive, HSTRING, or enum (as Int32). + Value(NativeType), + Struct(Rc), + /// Not marshaled (e.g. `Guid`, `IReference`); exposed as `null`. + Opaque, +} + +struct Field { + /// Internalized. + key: v8::Global, + name: String, + offset: usize, + kind: FieldKind, +} + +/// Layout and templates for one struct type. The cache keeps it alive for the constructor's raw pointer. +pub(crate) struct StructType { + name: String, + fields: Vec, + tmpl: OnceCell>, + instance_tmpl: OnceCell>, +} + +/// Per-isolate struct types by full name (isolate slot). +#[derive(Default)] +pub(crate) struct StructCache { + types: RefCell>>, +} + +fn cache(scope: &mut v8::PinScope<'_, '_>) -> &'static StructCache { + if scope.get_slot::().is_none() { + scope.set_slot(StructCache::default()); + } + // Slot lives as long as the isolate; detach the borrow from `scope`. + let cache = scope.get_slot::().unwrap(); + unsafe { &*(cache as *const StructCache) } +} + +fn struct_type_for( + scope: &mut v8::PinScope<'_, '_>, + declaration: &Arc>, +) -> Option> { + let lock = declaration.read(); + let cache = cache(scope); + if let Some(ty) = cache.types.borrow().get(lock.full_name()) { + return Some(Rc::clone(ty)); + } + let sd = lock.as_any().downcast_ref::()?; + let full_name = lock.full_name().to_string(); + let simple_name = lock.name().to_string(); + + let mut fields = Vec::with_capacity(sd.fields().len()); + let mut offset = 0usize; + for f in sd.fields().iter() { + let Some(m) = f.base().metadata() else { + continue; + }; + let sig = Signature::to_string(m, &f.type_()); + // Same layout as the serializer. + let (size, align) = sig_size_align_pub(&sig); + offset = align_up(offset, align); + + let kind = if sig.contains('.') { + let nested = MetadataReader::find_by_name(strip_generic_suffix(&sig)); + let nested_kind = nested.as_ref().map(|d| d.read().kind()); + match (nested, nested_kind) { + (Some(d), Some(DeclarationKind::Struct)) => struct_type_for(scope, &d) + .map(FieldKind::Struct) + .unwrap_or(FieldKind::Opaque), + (_, Some(DeclarationKind::Enum)) => FieldKind::Value(NativeType::I32), + _ => FieldKind::Opaque, + } + } else { + match NativeType::try_from(sig.as_str()) { + Ok(NativeType::Pointer | NativeType::Buffer | NativeType::Function) + | Ok(NativeType::Struct(_) | NativeType::Void) + | Err(_) => FieldKind::Opaque, + Ok(nt) => FieldKind::Value(nt), + } + }; + + let key = + v8::String::new_from_utf8(scope, f.name().as_bytes(), v8::NewStringType::Internalized)?; + fields.push(Field { + key: v8::Global::new(scope, key), + name: f.name().to_string(), + offset, + kind, + }); + offset += size; + } + drop(lock); + + let ty = Rc::new(StructType { + name: simple_name, + fields, + tmpl: OnceCell::new(), + instance_tmpl: OnceCell::new(), + }); + let data = v8::External::new(scope, Rc::as_ptr(&ty) as *mut c_void); + let tmpl = v8::FunctionTemplate::builder(construct) + .data(data.into()) + .build(scope); + if let Some(class_name) = v8::String::new(scope, &ty.name) { + tmpl.set_class_name(class_name); + } + let instance_tmpl = tmpl.instance_template(scope); + for field in ty.fields.iter() { + let key = v8::Local::new(scope, &field.key); + let placeholder: v8::Local = match field.kind { + FieldKind::Value(NativeType::String) => v8::String::empty(scope).into(), + FieldKind::Value(NativeType::Bool) => v8::Boolean::new(scope, false).into(), + FieldKind::Value(_) => v8::Integer::new(scope, 0).into(), + FieldKind::Struct(_) | FieldKind::Opaque => v8::null(scope).into(), + }; + instance_tmpl.set(key.into(), placeholder); + } + let tag = v8::Symbol::get_to_string_tag(scope); + if let Some(tag_value) = v8::String::new(scope, &full_name) { + tmpl.prototype_template(scope).set_with_attr( + tag.into(), + tag_value.into(), + v8::PropertyAttribute::READ_ONLY | v8::PropertyAttribute::DONT_ENUM, + ); + } + let _ = ty.tmpl.set(v8::Global::new(scope, tmpl)); + let _ = ty.instance_tmpl.set(v8::Global::new(scope, instance_tmpl)); + + cache.types.borrow_mut().insert(full_name, Rc::clone(&ty)); + Some(ty) +} + +fn instantiate<'a>( + scope: &mut v8::PinScope<'a, '_>, + ty: &StructType, +) -> Option> { + let tmpl = v8::Local::new(scope, ty.instance_tmpl.get()?); + tmpl.new_instance(scope) +} + +/// The JS constructor for a struct type (`Windows.Foundation.Rect`). +pub(crate) fn constructor<'a>( + scope: &mut v8::PinScope<'a, '_>, + declaration: &Arc>, +) -> v8::Local<'a, v8::Value> { + struct_type_for(scope, declaration) + .and_then(|ty| { + let tmpl = v8::Local::new(scope, ty.tmpl.get()?); + tmpl.get_function(scope) + }) + .map(Into::into) + .unwrap_or_else(|| v8::undefined(scope).into()) +} + +/// Copies a native struct into a JS value. `raw` is borrowed (HSTRINGs are not released); null gives zeros. +pub(crate) fn from_raw<'a>( + scope: &mut v8::PinScope<'a, '_>, + declaration: &Arc>, + raw: *const c_void, +) -> Option> { + let ty = struct_type_for(scope, declaration)?; + read_struct(scope, &ty, raw as *const u8) +} + +/// Serializes `obj` into `scratch` (must outlive the call) and returns its address. +pub(crate) fn serialize_into( + scope: &mut v8::PinScope<'_, '_>, + declaration: &Arc>, + obj: v8::Local, + scratch: &mut Vec>, +) -> *mut c_void { + let mut buf = Vec::new(); + { + let lock = declaration.read(); + if let Some(sd) = lock.as_any().downcast_ref::() { + crate::property_call::append_struct_object_bytes(&mut buf, scope, obj, sd); + } + } + if buf.is_empty() { + buf.push(0); + } + let ptr = buf.as_mut_ptr() as *mut c_void; + scratch.push(buf); + ptr +} + +pub(crate) fn type_name_declaration() -> Option>> { + MetadataReader::find_by_name("Windows.UI.Xaml.Interop.TypeName") +} + +fn read_struct<'a>( + scope: &mut v8::PinScope<'a, '_>, + ty: &StructType, + raw: *const u8, +) -> Option> { + let obj = instantiate(scope, ty)?; + if raw.is_null() { + return Some(obj); + } + for field in ty.fields.iter() { + let ptr = unsafe { raw.add(field.offset) }; + let value: v8::Local = match &field.kind { + FieldKind::Value(NativeType::String) => unsafe { + let raw_hstring = std::ptr::read_unaligned(ptr as *const usize); + let hstring = ManuallyDrop::new(std::mem::transmute::(raw_hstring)); + crate::value::two_byte_string(scope, &hstring).into() + }, + FieldKind::Value(nt) => unsafe { + read_value_from_ptr(ptr as *const c_void, scope, nt.clone()) + }, + FieldKind::Struct(nested) => match read_struct(scope, nested, ptr) { + Some(v) => v.into(), + None => continue, + }, + FieldKind::Opaque => continue, + }; + let key = v8::Local::new(scope, &field.key); + obj.set(scope, key.into(), value); + } + Some(obj) +} + +/// `new Rect({ X, Y, Width, Height })`, `new Rect(x, y, w, h)` or `new Rect()`; missing fields are zero. +fn construct<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, +) { + let Ok(data) = v8::Local::::try_from(args.data()) else { + return; + }; + let ty = unsafe { &*(data.value() as *const StructType) }; + + let this = if args.new_target().is_undefined() { + match instantiate(scope, ty) { + Some(obj) => obj, + None => return, + } + } else { + args.this() + }; + + // One object argument: named fields; otherwise positional. + let first = args.get(0); + let named = if args.length() == 1 && first.is_object() { + first.to_object(scope) + } else { + None + }; + let filled = fill_fields(scope, ty, this, |scope, i, key| match named { + Some(obj) => obj.get(scope, key.into()), + None => Some(args.get(i as i32)), + }); + if filled.is_some() { + rv.set(this.into()); + } +} + +/// Coerces and stores each field; `None` means a TypeError was thrown. +fn fill_fields<'a>( + scope: &mut v8::PinScope<'a, '_>, + ty: &StructType, + target: v8::Local<'a, v8::Object>, + mut source: impl FnMut( + &mut v8::PinScope<'a, '_>, + usize, + v8::Local<'a, v8::String>, + ) -> Option>, +) -> Option<()> { + for (i, field) in ty.fields.iter().enumerate() { + let key = v8::Local::new(scope, &field.key); + let input = source(scope, i, key)?; + let value = coerce_field(scope, ty, field, input)?; + target.set(scope, key.into(), value)?; + } + Some(()) +} + +fn coerce_field<'a>( + scope: &mut v8::PinScope<'a, '_>, + owner: &StructType, + field: &Field, + input: v8::Local<'a, v8::Value>, +) -> Option> { + let missing = input.is_null_or_undefined(); + match &field.kind { + FieldKind::Value(NativeType::Bool) => { + Some(v8::Boolean::new(scope, !missing && input.boolean_value(scope)).into()) + } + FieldKind::Value(NativeType::String) => { + if missing { + Some(v8::String::empty(scope).into()) + } else if input.is_string() { + Some(input) + } else { + type_error(scope, owner, field, "a string") + } + } + // BigInt stays BigInt only beyond the safe-integer range, as the reader does. + FieldKind::Value(nt @ (NativeType::I64 | NativeType::U64)) if input.is_big_int() => { + let bi = v8::Local::::try_from(input).ok()?; + let n = match nt { + NativeType::U64 => bi.u64_value().0 as f64, + _ => bi.i64_value().0 as f64, + }; + if n.abs() <= MAX_SAFE_INTEGER { + Some(v8::Number::new(scope, n).into()) + } else { + Some(input) + } + } + FieldKind::Value(nt) => { + if missing { + return Some(v8::Integer::new(scope, 0).into()); + } + if !input.is_number() { + return type_error(scope, owner, field, "a number"); + } + let n = input.number_value(scope).unwrap_or(0.0); + // Same casts as the serializer. + let n = match nt { + NativeType::F32 => n as f32 as f64, + NativeType::I32 => n as i32 as f64, + NativeType::U32 => n as u32 as f64, + NativeType::I16 => n as i16 as f64, + NativeType::U16 => n as u16 as f64, + NativeType::I8 => n as i8 as f64, + NativeType::U8 => n as u8 as f64, + NativeType::I64 => n as i64 as f64, + NativeType::U64 => n as u64 as f64, + _ => n, + }; + Some(v8::Number::new(scope, n).into()) + } + FieldKind::Struct(nested) => { + let obj = instantiate(scope, nested)?; + if missing { + return Some(obj.into()); + } + let Some(src) = input.to_object(scope).filter(|_| input.is_object()) else { + return type_error(scope, owner, field, "an object"); + }; + fill_fields(scope, nested, obj, |scope, _, key| src.get(scope, key.into()))?; + Some(obj.into()) + } + FieldKind::Opaque => Some(if missing { v8::null(scope).into() } else { input }), + } +} + +fn type_error<'a, T>( + scope: &mut v8::PinScope<'a, '_>, + owner: &StructType, + field: &Field, + expected: &str, +) -> Option { + let message = format!("{}.{} expects {}", owner.name, field.name, expected); + if let Some(message) = v8::String::new(scope, &message) { + let error = v8::Exception::type_error(scope, message); + scope.throw_exception(error); + } + None +} diff --git a/runtime/src/value.rs b/runtime/src/value.rs index a2af7af..a554e3a 100644 --- a/runtime/src/value.rs +++ b/runtime/src/value.rs @@ -782,12 +782,6 @@ fn try_get_external_handle( return Some(instance.as_raw() as *mut c_void); } - // Struct objects have no COM instance; return a pointer to the raw - // byte buffer so WinRT setters receive the struct data by reference. - if let Some((ref buf, _)) = dec.struct_instance { - return Some(buf.as_ptr() as *mut c_void); - } - return Some(std::ptr::null_mut()); } @@ -1321,16 +1315,6 @@ pub fn ffi_parse_query_interface_arg( Ok((ffi_parse_pointer_arg(scope, arg)?, None)) } -#[cfg(feature = "classic")] -#[inline] -pub fn ffi_parse_buffer_arg( - scope: &mut v8::PinScope<'_, '_>, - arg: v8::Local, -) -> std::result::Result { - let (value, _) = ffi_parse_buffer_arg_with_length(scope, arg)?; - Ok(value) -} - #[cfg(feature = "classic")] #[inline] pub fn ffi_parse_buffer_arg_with_length( @@ -1437,6 +1421,12 @@ pub(crate) fn append_struct_field_bytes( NativeType::I8 => buf.extend_from_slice(&(num as i8).to_le_bytes()), NativeType::U8 => buf.push(num as u8), NativeType::Bool => buf.push(if value.boolean_value(scope) { 1u8 } else { 0u8 }), + // Leaked: the struct bytes can't own it (same as TypeName synthesis). + NativeType::String if value.is_string() => { + let hstring = std::mem::ManuallyDrop::new(hstring_from_v8_value(scope, value)); + let raw: usize = unsafe { std::mem::transmute_copy(&*hstring) }; + buf.extend_from_slice(&raw.to_le_bytes()); + } _ => buf.extend(std::iter::repeat(0u8).take(native_type.size())), } } From a9955839842d0309cba1d46375d3aa39c5ea654e Mon Sep 17 00:00:00 2001 From: Osei Fortune Date: Sat, 17 Oct 2026 08:05:14 -0400 Subject: [PATCH 2/2] test: round-trip structs through a real WinRT property TimedTextStyle has no Padding property, so the old test only round-tripped a JS expando. Use TimedTextRegion.Padding and assert no own property. --- integration-tests/tests/struct_values.rs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/integration-tests/tests/struct_values.rs b/integration-tests/tests/struct_values.rs index 78a9b44..4837d0c 100644 --- a/integration-tests/tests/struct_values.rs +++ b/integration-tests/tests/struct_values.rs @@ -82,16 +82,15 @@ fn struct_values_round_trip_through_native() { let mut rt = Runtime::new("."); let v = eval( &mut rt, - "(() => { try { - const s = new Windows.Media.Core.TimedTextStyle(); - s.Padding = { Before: 1, After: 2, Start: 3, End: 4, Unit: 1 }; - const p = s.Padding; + "(() => { + const r = new Windows.Media.Core.TimedTextRegion(); + r.Padding = { Before: 1, After: 2, Start: 3, End: 4, Unit: 1 }; + const p = r.Padding; p.End = 9; - s.Padding = p; - return JSON.stringify(s.Padding); - } catch (e) { return 'skip'; } })()", + r.Padding = p; + // An own property would mean the write never reached native. + return JSON.stringify(r.Padding) + ' own=' + r.hasOwnProperty('Padding'); + })()", ); - if v != "skip" { - assert_eq!(v, r#"{"Before":1,"After":2,"Start":3,"End":9,"Unit":1}"#); - } + assert_eq!(v, r#"{"Before":1,"After":2,"Start":3,"End":9,"Unit":1} own=false"#); }