Skip to content

Commit 85ffefa

Browse files
committed
fix: struct constructors on napi; boxed struct values on both engines
- napi namespaces resolve struct types to a constructor (`new Rect({...})`, `new Rect(x, y, w, h)`, `new Rect()`), returning the same plain object a struct return gives - IPropertyValue holding Point/Size/Rect/TimeSpan/DateTime unboxes to a struct value instead of an empty wrapper - plain objects shaped like those five types box back when passed to an Object parameter (PropertySet.Insert, settings values) - missing struct fields serialize as zero instead of NaN - napi struct String fields are written
1 parent a995583 commit 85ffefa

6 files changed

Lines changed: 279 additions & 3 deletions

File tree

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use runtime::Runtime;
2+
3+
fn eval(rt: &mut Runtime, expr: &str) -> String {
4+
rt.eval_script_to_string(expr)
5+
.unwrap_or_else(|| "<exception>".to_string())
6+
}
7+
8+
#[test]
9+
fn boxed_structs_unbox_and_box_by_shape() {
10+
let mut rt = Runtime::new(".");
11+
rt.run_script(
12+
"const PV = Windows.Foundation.PropertyValue;
13+
globalThis.ps = new Windows.Foundation.Collections.PropertySet();
14+
ps.Insert('size', PV.CreateSize({ Width: 9, Height: 8 }));
15+
ps.Insert('point', { X: 1, Y: 2 });
16+
ps.Insert('rect', { X: 1, Y: 2, Width: 3, Height: 4 });
17+
ps.Insert('span', { Duration: 10000000 });",
18+
"setup.js",
19+
);
20+
assert_eq!(eval(&mut rt, "JSON.stringify(ps.Lookup('size'))"), r#"{"Width":9,"Height":8}"#);
21+
assert_eq!(eval(&mut rt, "JSON.stringify(ps.Lookup('point'))"), r#"{"X":1,"Y":2}"#);
22+
assert_eq!(
23+
eval(&mut rt, "JSON.stringify(ps.Lookup('rect'))"),
24+
r#"{"X":1,"Y":2,"Width":3,"Height":4}"#
25+
);
26+
assert_eq!(eval(&mut rt, "JSON.stringify(ps.Lookup('span'))"), r#"{"Duration":10000000}"#);
27+
assert_eq!(
28+
eval(&mut rt, "JSON.stringify(Windows.Foundation.PropertyValue.CreatePoint({ X: 1.5, Y: 2.5 }))"),
29+
r#"{"X":1.5,"Y":2.5}"#
30+
);
31+
}
32+
33+
#[test]
34+
fn by_value_point_round_trips() {
35+
let mut rt = Runtime::new(".");
36+
assert_eq!(
37+
eval(
38+
&mut rt,
39+
"JSON.stringify(new Windows.UI.Input.Inking.InkPoint({ X: 3, Y: 4 }, 0.5).Position)"
40+
),
41+
r#"{"X":3,"Y":4}"#
42+
);
43+
}
44+
45+
#[test]
46+
fn missing_struct_fields_serialize_as_zero() {
47+
let mut rt = Runtime::new(".");
48+
let v = eval(
49+
&mut rt,
50+
"(() => { try {
51+
const s = new Windows.Media.Core.TimedTextStyle();
52+
s.Padding = { Before: 1, Unit: 1 };
53+
return JSON.stringify(s.Padding);
54+
} catch (e) { return 'skip'; } })()",
55+
);
56+
if v != "skip" {
57+
assert_eq!(v, r#"{"Before":1,"After":0,"Start":0,"End":0,"Unit":1}"#);
58+
}
59+
}

‎runtime/src/napi_engine/ns_proxy.rs‎

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -618,11 +618,49 @@ fn resolve_member(env: &Env, full_name: &str) -> napi::Result<JsUnknown> {
618618
create_ctor_proxy(env, full_name, declaration)?,
619619
)),
620620
DeclarationKind::Enum => Ok(as_unknown(env, create_enum_object(env, &declaration)?)),
621-
// Interfaces / structs arrive with later slices.
621+
DeclarationKind::Struct => Ok(as_unknown(env, create_struct_ctor(env, full_name, declaration)?)),
622+
// Interfaces arrive with later slices.
622623
_ => undefined_js(env),
623624
}
624625
}
625626

627+
/// `new Rect({ X, Y, Width, Height })`, `new Rect(x, y, w, h)` or `new Rect()`: a plain struct
628+
/// value, round-tripped through the struct's bytes so missing fields are zero and values match
629+
/// what native receives.
630+
fn create_struct_ctor(env: &Env, full_name: &str, declaration: Decl) -> napi::Result<JsFunction> {
631+
use metadata::declarations::struct_declaration::StructDeclaration;
632+
633+
let simple_name = full_name.rsplit('.').next().unwrap_or(full_name);
634+
env.create_function_from_closure(simple_name, move |ctx: CallContext| {
635+
let env = &ctx.env;
636+
let first = if ctx.length > 0 { Some(ctx.get::<JsUnknown>(0)?) } else { None };
637+
let named = ctx.length == 1
638+
&& first.as_ref().is_some_and(|v| matches!(v.get_type(), Ok(ValueType::Object)));
639+
let mut buf = Vec::new();
640+
{
641+
let lock = declaration.read();
642+
let Some(sd) = lock.as_any().downcast_ref::<StructDeclaration>() else {
643+
return undefined_js(env);
644+
};
645+
let source: JsObject = if named {
646+
unsafe { first.unwrap().cast() }
647+
} else {
648+
let mut positional = env.create_object()?;
649+
for (i, field) in sd.fields().iter().enumerate().take(ctx.length) {
650+
positional.set_named_property(field.name(), ctx.get::<JsUnknown>(i)?)?;
651+
}
652+
positional
653+
};
654+
crate::property_call::append_struct_object_bytes_napi(env, &mut buf, &source, sd);
655+
}
656+
if buf.is_empty() {
657+
buf.push(0);
658+
}
659+
let value = create_struct_object_from_raw(env, &declaration, buf.as_ptr())?;
660+
Ok(as_unknown(env, value))
661+
})
662+
}
663+
626664
/// A WinRT enum as a plain JS object of name → numeric value (Int32/UInt32, same
627665
/// representation the v8 interceptor produced).
628666
fn create_enum_object(env: &Env, declaration: &Decl) -> napi::Result<JsObject> {

‎runtime/src/napi_engine/value.rs‎

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -892,6 +892,43 @@ pub fn box_as_ireference(
892892

893893
//
894894

895+
/// Boxes a plain object shaped like one of the struct types `IPropertyValue` supports
896+
/// (`{X,Y}`, `{Width,Height}`, `{X,Y,Width,Height}`, `{Duration}`, `{UniversalTime}`).
897+
fn box_struct_shape(env: &Env, obj: &JsObject) -> Option<windows::core::IInspectable> {
898+
use windows::Foundation::{DateTime, Point, Rect, Size, TimeSpan};
899+
let names = obj.get_property_names().ok()?;
900+
let len = names.get_array_length().ok()?;
901+
let mut keys: Vec<String> = Vec::with_capacity(len as usize);
902+
for i in 0..len {
903+
let k: JsString = names.get_element(i).ok()?;
904+
keys.push(k.into_utf8().ok()?.as_str().ok()?.to_owned());
905+
}
906+
keys.sort();
907+
let get = |name: &str| obj.get_named_property::<JsUnknown>(name).ok();
908+
let f = |name: &str| {
909+
get(name)
910+
.and_then(|v| coerce_f64(env, &v))
911+
.filter(|n| !n.is_nan())
912+
.unwrap_or(0.0) as f32
913+
};
914+
let i = |name: &str| get(name).map(|v| js_to_i64(env, &v)).unwrap_or(0);
915+
let keys: Vec<&str> = keys.iter().map(String::as_str).collect();
916+
let boxed = match keys.as_slice() {
917+
["X", "Y"] => PropertyValue::CreatePoint(Point { X: f("X"), Y: f("Y") }),
918+
["Height", "Width"] => PropertyValue::CreateSize(Size { Width: f("Width"), Height: f("Height") }),
919+
["Height", "Width", "X", "Y"] => PropertyValue::CreateRect(Rect {
920+
X: f("X"),
921+
Y: f("Y"),
922+
Width: f("Width"),
923+
Height: f("Height"),
924+
}),
925+
["Duration"] => PropertyValue::CreateTimeSpan(TimeSpan { Duration: i("Duration") }),
926+
["UniversalTime"] => PropertyValue::CreateDateTime(DateTime { UniversalTime: i("UniversalTime") }),
927+
_ => return None,
928+
};
929+
boxed.ok()?.cast().ok()
930+
}
931+
895932
pub fn napi_parse_pointer(env: &Env, arg: &JsUnknown) -> Result<NativeValue, AnyError> {
896933
let vt = value_type(arg)?;
897934

@@ -941,6 +978,15 @@ pub fn napi_parse_pointer(env: &Env, arg: &JsUnknown) -> Result<NativeValue, Any
941978
}
942979
}
943980

981+
if vt == ValueType::Object {
982+
let obj: JsObject = unsafe { arg.cast() };
983+
if let Some(inspectable) = box_struct_shape(env, &obj) {
984+
let ptr = inspectable.as_raw() as *mut c_void;
985+
std::mem::forget(inspectable);
986+
return Ok(NativeValue { pointer: ptr });
987+
}
988+
}
989+
944990
let pointer = if vt == ValueType::External {
945991
ptr_from_external(env, arg).ok_or_else(|| {
946992
type_error("Invalid FFI pointer type, expected null, External, or { handle: External|null }")
@@ -1143,7 +1189,10 @@ pub fn append_struct_field_bytes(
11431189
value: &JsUnknown,
11441190
native_type: &NativeType,
11451191
) {
1146-
let num = coerce_f64(env, value).unwrap_or(0.0);
1192+
let num = match value.get_type() {
1193+
Ok(ValueType::Undefined | ValueType::Null) => 0.0,
1194+
_ => coerce_f64(env, value).unwrap_or(0.0),
1195+
};
11471196
match native_type {
11481197
NativeType::F64 => buf.extend_from_slice(&num.to_le_bytes()),
11491198
NativeType::F32 => buf.extend_from_slice(&(num as f32).to_le_bytes()),
@@ -1157,6 +1206,12 @@ pub fn append_struct_field_bytes(
11571206
NativeType::I8 => buf.extend_from_slice(&(num as i8).to_le_bytes()),
11581207
NativeType::U8 => buf.push(num as u8),
11591208
NativeType::Bool => buf.push(if coerce_bool(env, value) { 1u8 } else { 0u8 }),
1209+
// Ownership moves into the bytes; the struct reader releases it, call args leak it.
1210+
NativeType::String if matches!(value.get_type(), Ok(ValueType::String)) => {
1211+
let hstring = ManuallyDrop::new(hstring_from_js_value(env, value));
1212+
let raw: usize = unsafe { std::mem::transmute_copy(&*hstring) };
1213+
buf.extend_from_slice(&raw.to_le_bytes());
1214+
}
11601215
_ => buf.extend(std::iter::repeat(0u8).take(native_type.size())),
11611216
}
11621217
}
@@ -1455,6 +1510,19 @@ pub fn try_unbox_property_value(env: &Env, raw: *mut c_void) -> Option<JsUnknown
14551510
let g = unsafe { pv.GetGuid() }.ok()?;
14561511
env.create_string(&format!("{g:?}")).ok().map(|v| as_unknown(env, v))
14571512
}
1513+
PropertyType::Point => boxed_struct(env, "Windows.Foundation.Point", &unsafe { pv.GetPoint() }.ok()?),
1514+
PropertyType::Size => boxed_struct(env, "Windows.Foundation.Size", &unsafe { pv.GetSize() }.ok()?),
1515+
PropertyType::Rect => boxed_struct(env, "Windows.Foundation.Rect", &unsafe { pv.GetRect() }.ok()?),
1516+
PropertyType::TimeSpan => boxed_struct(env, "Windows.Foundation.TimeSpan", &unsafe { pv.GetTimeSpan() }.ok()?),
1517+
PropertyType::DateTime => boxed_struct(env, "Windows.Foundation.DateTime", &unsafe { pv.GetDateTime() }.ok()?),
14581518
_ => None,
14591519
}
14601520
}
1521+
1522+
/// A boxed struct value (`IReference<Point>` etc.) as the same plain object a struct return gives.
1523+
fn boxed_struct<T>(env: &Env, type_name: &str, value: &T) -> Option<JsUnknown> {
1524+
let decl = metadata::meta_data_reader::MetadataReader::find_by_name(type_name)?;
1525+
crate::napi_engine::ns_proxy::create_struct_object_from_raw(env, &decl, value as *const T as *const u8)
1526+
.ok()
1527+
.map(|o| as_unknown(env, o))
1528+
}

‎runtime/src/ns_proxy.rs‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,10 +521,25 @@ fn try_unbox_property_value<'a>(
521521
let g = pv.GetGuid().ok()?;
522522
v8::String::new(scope, &format!("{g:?}")).map(Into::into)
523523
}
524+
PropertyType::Point => boxed_struct(scope, "Windows.Foundation.Point", &pv.GetPoint().ok()?),
525+
PropertyType::Size => boxed_struct(scope, "Windows.Foundation.Size", &pv.GetSize().ok()?),
526+
PropertyType::Rect => boxed_struct(scope, "Windows.Foundation.Rect", &pv.GetRect().ok()?),
527+
PropertyType::TimeSpan => boxed_struct(scope, "Windows.Foundation.TimeSpan", &pv.GetTimeSpan().ok()?),
528+
PropertyType::DateTime => boxed_struct(scope, "Windows.Foundation.DateTime", &pv.GetDateTime().ok()?),
524529
_ => None,
525530
}
526531
}
527532

533+
/// A boxed struct value (`IReference<Point>` etc.) as the same object a struct return gives.
534+
fn boxed_struct<'a, T>(
535+
scope: &mut v8::PinScope<'a, '_>,
536+
type_name: &str,
537+
value: &T,
538+
) -> Option<v8::Local<'a, v8::Value>> {
539+
let decl = MetadataReader::find_by_name(type_name)?;
540+
Some(crate::create_struct_object_from_raw(decl, value as *const T as *mut c_void, scope).into())
541+
}
542+
528543
/// Sets a method/property return value, resolving `Object`/IInspectable returns to a full typed
529544
/// proxy via GetRuntimeClassName so property/event interceptors work (e.g. the WebView2 returned by
530545
/// `XamlReader.Load`). Any other signature defers to `set_ret_val`. A non-resolvable Object falls

‎runtime/src/value.rs‎

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1248,6 +1248,17 @@ pub fn ffi_parse_pointer_arg(
12481248
}
12491249
}
12501250

1251+
if let Some(inspectable) = arg
1252+
.is_object()
1253+
.then(|| arg.to_object(scope))
1254+
.flatten()
1255+
.and_then(|obj| box_struct_shape(scope, obj))
1256+
{
1257+
let ptr = inspectable.as_raw() as *mut c_void;
1258+
std::mem::forget(inspectable);
1259+
return Ok(NativeValue { pointer: ptr });
1260+
}
1261+
12511262
let pointer = if let Ok(value) = v8::Local::<v8::External>::try_from(arg) {
12521263
value.value()
12531264
} else if arg.is_null_or_undefined() {
@@ -1398,6 +1409,62 @@ pub fn ffi_parse_struct_arg(
13981409
Ok(NativeValue { pointer })
13991410
}
14001411

1412+
/// Boxes a plain object shaped like one of the struct types `IPropertyValue` supports
1413+
/// (`{X,Y}`, `{Width,Height}`, `{X,Y,Width,Height}`, `{Duration}`, `{UniversalTime}`).
1414+
#[cfg(feature = "classic")]
1415+
fn box_struct_shape(
1416+
scope: &mut v8::PinScope<'_, '_>,
1417+
obj: v8::Local<v8::Object>,
1418+
) -> Option<windows::core::IInspectable> {
1419+
use windows::Foundation::{DateTime, Point, Rect, Size, TimeSpan};
1420+
let names = obj.get_own_property_names(scope, Default::default())?;
1421+
let mut keys: Vec<String> = (0..names.length())
1422+
.filter_map(|i| names.get_index(scope, i))
1423+
.map(|k| k.to_rust_string_lossy(scope))
1424+
.collect();
1425+
keys.sort();
1426+
let mut get = |name: &str| -> v8::Local<v8::Value> {
1427+
v8::String::new(scope, name)
1428+
.and_then(|k| obj.get(scope, k.into()))
1429+
.unwrap_or_else(|| v8::undefined(scope).into())
1430+
};
1431+
let keys: Vec<&str> = keys.iter().map(String::as_str).collect();
1432+
let boxed = match keys.as_slice() {
1433+
["X", "Y"] => {
1434+
let (x, y) = (get("X"), get("Y"));
1435+
PropertyValue::CreatePoint(Point { X: f32_of(scope, x), Y: f32_of(scope, y) })
1436+
}
1437+
["Height", "Width"] => {
1438+
let (w, h) = (get("Width"), get("Height"));
1439+
PropertyValue::CreateSize(Size { Width: f32_of(scope, w), Height: f32_of(scope, h) })
1440+
}
1441+
["Height", "Width", "X", "Y"] => {
1442+
let (x, y, w, h) = (get("X"), get("Y"), get("Width"), get("Height"));
1443+
PropertyValue::CreateRect(Rect {
1444+
X: f32_of(scope, x),
1445+
Y: f32_of(scope, y),
1446+
Width: f32_of(scope, w),
1447+
Height: f32_of(scope, h),
1448+
})
1449+
}
1450+
["Duration"] => {
1451+
let d = get("Duration");
1452+
PropertyValue::CreateTimeSpan(TimeSpan { Duration: v8_to_i64(scope, d) })
1453+
}
1454+
["UniversalTime"] => {
1455+
let t = get("UniversalTime");
1456+
PropertyValue::CreateDateTime(DateTime { UniversalTime: v8_to_i64(scope, t) })
1457+
}
1458+
_ => return None,
1459+
};
1460+
boxed.ok()?.cast().ok()
1461+
}
1462+
1463+
#[cfg(feature = "classic")]
1464+
fn f32_of(scope: &mut v8::PinScope<'_, '_>, v: v8::Local<v8::Value>) -> f32 {
1465+
v.number_value(scope).filter(|n| !n.is_nan()).unwrap_or(0.0) as f32
1466+
}
1467+
14011468
/// Write a single struct field value (from a V8 value) into a byte buffer in little-endian order.
14021469
/// Used when converting a plain JS object like `{A:255, R:0, G:0, B:0}` to WinRT struct bytes.
14031470
#[cfg(feature = "classic")]
@@ -1407,7 +1474,11 @@ pub(crate) fn append_struct_field_bytes(
14071474
value: v8::Local<v8::Value>,
14081475
native_type: &NativeType,
14091476
) {
1410-
let num = value.number_value(scope).unwrap_or(0.0);
1477+
let num = if value.is_null_or_undefined() {
1478+
0.0
1479+
} else {
1480+
value.number_value(scope).unwrap_or(0.0)
1481+
};
14111482
match native_type {
14121483
NativeType::F64 => buf.extend_from_slice(&num.to_le_bytes()),
14131484
NativeType::F32 => buf.extend_from_slice(&(num as f32).to_le_bytes()),

‎windows-napi/test/struct-test.js‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,5 +33,30 @@ check('Colors.Red B', red.B, 0);
3333
check('struct spreads', JSON.stringify({ ...color }), '{"A":255,"R":128,"G":64,"B":32}');
3434
check('struct JSON', JSON.stringify(color), s => s.includes('"A":255'));
3535

36+
// Struct constructors from namespaces; missing fields are zero.
37+
const F = Windows.Foundation;
38+
check('struct ctor named', JSON.stringify(new F.Rect({ X: 1, Y: 2, Width: 3, Height: 4 })), '{"X":1,"Y":2,"Width":3,"Height":4}');
39+
check('struct ctor positional', JSON.stringify(new F.Rect(5, 6, 7, 8)), '{"X":5,"Y":6,"Width":7,"Height":8}');
40+
check('struct ctor empty', JSON.stringify(new F.Rect()), '{"X":0,"Y":0,"Width":0,"Height":0}');
41+
check('struct ctor partial', JSON.stringify(new F.Rect({ X: 1 })), '{"X":1,"Y":0,"Width":0,"Height":0}');
42+
check('struct ctor nested', JSON.stringify(new Windows.UI.Xaml.Duration({ TimeSpan: { Duration: 5 }, Type: 1 })), '{"TimeSpan":{"Duration":5},"Type":1}');
43+
44+
// Point passed by value round-trips.
45+
const inkPoint = new Windows.UI.Input.Inking.InkPoint(new F.Point(3, 4), 0.5);
46+
check('by-value Point', JSON.stringify(inkPoint.Position), '{"X":3,"Y":4}');
47+
48+
// Boxed structs (IReference<Point> etc.) unbox to plain objects and box back by shape.
49+
const PV = F.PropertyValue;
50+
check('PropertyValue.CreatePoint', JSON.stringify(PV.CreatePoint({ X: 1.5, Y: 2.5 })), '{"X":1.5,"Y":2.5}');
51+
const ps = new F.Collections.PropertySet();
52+
ps.Insert('size', PV.CreateSize({ Width: 9, Height: 8 }));
53+
ps.Insert('point', { X: 1, Y: 2 });
54+
ps.Insert('rect', new F.Rect(1, 2, 3, 4));
55+
ps.Insert('span', { Duration: 10000000 });
56+
check('boxed Size', JSON.stringify(ps.Lookup('size')), '{"Width":9,"Height":8}');
57+
check('boxed Point', JSON.stringify(ps.Lookup('point')), '{"X":1,"Y":2}');
58+
check('boxed Rect', JSON.stringify(ps.Lookup('rect')), '{"X":1,"Y":2,"Width":3,"Height":4}');
59+
check('boxed TimeSpan', JSON.stringify(ps.Lookup('span')), '{"Duration":10000000}');
60+
3661
console.log(`\n${pass} passed, ${fail} failed`);
3762
process.exit(fail ? 1 : 0);

0 commit comments

Comments
 (0)