Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions metadata/src/signature.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,17 @@ impl Signature {
metadata: Option<&IMetaDataImport2>,
signature: &PCCOR_SIGNATURE,
preserve_arity: bool,
) -> String {
Signature::get_string_in(metadata, signature, preserve_arity, false)
}

/// `in_type_arg` is set inside a generic's type arguments, where an enum keeps its name: the
/// closed type's IID hashes `enum(Name;i4)`, and `Int32` there names a different type.
fn get_string_in(
metadata: Option<&IMetaDataImport2>,
signature: &PCCOR_SIGNATURE,
preserve_arity: bool,
in_type_arg: bool,
) -> String {
let mut signature = signature.clone();

Expand All @@ -184,8 +195,8 @@ impl Signature {
let token_type = CorTokenType(token as i32);

// WinRT enums are ELEMENT_TYPE_VALUETYPE but must be passed as
// Int32 on the ABI — not as a COM pointer.
if is_enum_type(metadata_ref, token_type) {
// Int32 on the ABI, not as a COM pointer.
if !in_type_arg && is_enum_type(metadata_ref, token_type) {
return "Int32".to_string();
}

Expand All @@ -211,7 +222,8 @@ impl Signature {
}
ELEMENT_TYPE_OBJECT => "Object".to_string(),
ELEMENT_TYPE_SZARRAY => {
let result = Signature::get_string(metadata, &mut signature, preserve_arity);
let result =
Signature::get_string_in(metadata, &mut signature, preserve_arity, in_type_arg);
format!("{}[]", result)
}
ELEMENT_TYPE_VAR => {
Expand Down Expand Up @@ -244,7 +256,7 @@ impl Signature {

for i in 0..generic_arguments_count {
let mut sig_type = Signature::consume_type(&mut signature);
let data = Signature::get_string(metadata, &mut sig_type, preserve_arity);
let data = Signature::get_string_in(metadata, &mut sig_type, preserve_arity, true);

result += data.as_ref();
if i != generic_arguments_count.saturating_sub(1) {
Expand Down
4 changes: 3 additions & 1 deletion packages/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,9 @@ nativescript-windows path/to/app.js # e.g. packages/test/test-app.js
```
`packages/test/interop-check.js` exercises the shared `NSWinRT.interop` surface (typed-value/
`reference` boxing, Pointer/OutParam, buffer + DateTime helpers) and is expected to pass on
all four engines. With a script argument the host runs it and then drives the **standalone event loop**
all four engines, as is `packages/test/enum-check.js` (generics closed over an enum:
`IAsyncOperation<Enum>`, `IVector<Enum>`, `IReference<Enum>`). With a script argument the host
runs it and then drives the **standalone event loop**
(`runtime::napi_engine::event_loop`) until the app goes idle: Windows message pump (STA WinRT
async completions, delegate invokes), `setTimeout`/`setInterval` timers
(`napi_engine::timers`, install-if-missing), and the engine's microtask drain. The prelude
Expand Down
50 changes: 50 additions & 0 deletions packages/test/enum-check.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Standalone-engine check for generics closed over an enum (IAsyncOperation<Enum>,
// IVector<Enum>, IReference<Enum>):
// nativescript-windows packages/test/enum-check.js
// Prints "enum-check: N passed, M failed" once the async check settles.
'use strict';

var pass = 0, fail = 0;
function check(name, ok) {
if (ok) { pass += 1; }
else { fail += 1; console.error('FAIL ' + name); }
}
function attempt(name, fn) {
try { return fn(); } catch (e) { fail += 1; console.error('FAIL ' + name + ': ' + e.message); }
}

var Enumeration = Windows.Devices.Enumeration;
attempt('IVector<Enum>', function () {
var classes = new Enumeration.DevicePicker().Filter.SupportedDeviceClasses;
classes.Append(Enumeration.DeviceClass.AudioCapture);
classes.Append(Enumeration.DeviceClass.VideoCapture);
check('IVector<Enum>.Size', classes.Size === 2);
check('IVector<Enum>.GetAt', classes.GetAt(0) === Enumeration.DeviceClass.AudioCapture);
});

var Devices = Windows.Media.Devices;
attempt('IReference<Enum>', function () {
var focus = new Devices.FocusSettings();
check('IReference<Enum> starts null', focus.Distance == null);
focus.Distance = Devices.ManualFocusDistance.Nearest;
check('IReference<Enum> round-trips', focus.Distance === Devices.ManualFocusDistance.Nearest);
focus.Distance = null;
check('IReference<Enum> clears to null', focus.Distance == null);
});

function done() {
console.log('enum-check: ' + pass + ' passed, ' + fail + ' failed');
}
var op = attempt('IAsyncOperation<Enum>', function () {
return Windows.Devices.Haptics.VibrationDevice.RequestAccessAsync();
});
if (op) {
NSWinRT.toPromise(op).then(function (status) {
check('IAsyncOperation<Enum> resolves to a number', typeof status === 'number');
check('IAsyncOperation<Enum> GetResults', op.GetResults() === status);
}, function (e) {
check('IAsyncOperation<Enum> resolves: ' + e.message, false);
}).then(done);
} else {
done();
}
32 changes: 32 additions & 0 deletions runtime/src/interop_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2684,6 +2684,38 @@ fn ireference_parameters_and_results() {
);
}

/// Generics closed over an enum (`IAsyncOperation<Enum>`, `IVector<Enum>`, `IReference<Enum>`)
/// resolve the IID hashed from the enum's name and carry the enum as a number.
#[test]
fn enum_type_arguments() {
run_js_assert(
"enum_type_arguments",
r#"
const Enumeration = Windows.Devices.Enumeration;
const classes = new Enumeration.DevicePicker().Filter.SupportedDeviceClasses;
classes.Append(Enumeration.DeviceClass.AudioCapture);
classes.Append(Enumeration.DeviceClass.VideoCapture);
if (classes.Size !== 2) throw new Error(`Size was ${classes.Size}`);
const first = classes.GetAt(0);
if (first !== Enumeration.DeviceClass.AudioCapture) throw new Error(`GetAt(0) was ${first}`);

const Devices = Windows.Media.Devices;
const focus = new Devices.FocusSettings();
if (focus.Distance !== null) throw new Error(`unset Distance was ${focus.Distance}`);
focus.Distance = Devices.ManualFocusDistance.Nearest;
if (focus.Distance !== Devices.ManualFocusDistance.Nearest) throw new Error(`Distance was ${focus.Distance}`);
focus.Distance = null;
if (focus.Distance !== null) throw new Error(`cleared Distance was ${focus.Distance}`);

const op = Windows.Devices.Haptics.VibrationDevice.RequestAccessAsync();
return NSWinRT.toPromise(op).then((status) => {
if (typeof status !== 'number') throw new Error(`access status was ${status}`);
if (op.GetResults() !== status) throw new Error(`GetResults was ${op.GetResults()}`);
});
"#,
);
}

/// Struct `IReference<T>` parameters take a plain object or a struct value, and null clears them.
#[test]
fn ireference_struct_parameters() {
Expand Down
7 changes: 4 additions & 3 deletions runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1116,7 +1116,8 @@ pub(crate) fn classify_return(return_type: &str, is_void: bool) -> ReturnKind {
return ReturnKind::Void;
}
if let Some(kind) =
crate::helpers::ireference_inner_type(return_type).and_then(classify_reference_return)
crate::helpers::ireference_inner_type(return_type)
.and_then(|inner| classify_reference_return(crate::property_call::abi_type_arg(inner)))
{
return kind;
}
Expand Down Expand Up @@ -8092,7 +8093,7 @@ pub(crate) fn delegate_param_sigs(iid_name: &str) -> Option<Vec<String>> {
.strip_prefix("Var!")
.and_then(|n| n.parse::<usize>().ok());
Some(match placeholder.and_then(|n| type_args.get(n)) {
Some(arg) => arg.clone(),
Some(arg) => crate::property_call::abi_type_arg(arg).to_owned(),
None => sig,
})
})
Expand Down Expand Up @@ -8155,7 +8156,7 @@ fn delegate_return_from_invoke(method: &MethodDeclaration, type_args: &[String])
let sig = Signature::to_string(meta, &method.return_type());
let placeholder = sig.strip_prefix("Var!").and_then(|n| n.parse::<usize>().ok());
let sig = match placeholder.and_then(|n| type_args.get(n)) {
Some(arg) => arg.clone(),
Some(arg) => crate::property_call::abi_type_arg(arg).to_owned(),
None => sig,
};
delegate_invoke::delegate_return_for_signature(&sig, ffi_type_for_delegate_param)
Expand Down
14 changes: 6 additions & 8 deletions runtime/src/method_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,16 +78,14 @@ impl PointerPlan {
type_args: &[String],
typename_special: bool,
) -> Self {
if let Some(inner) = crate::helpers::ireference_inner_type(signature) {
if crate::helpers::ireference_inner_type(signature).is_some() {
let name = signature.trim();
let name = name.strip_prefix("ByRef ").unwrap_or(name);
let iid = GenericInstanceIdBuilder::generate_id_from_name(
&crate::property_call::substitute_type_vars(name, type_args),
);
return PointerPlan::IReference(
crate::property_call::substitute_type_vars(inner, type_args),
iid,
);
// Substituted as a whole so an enum `T` keeps its name for boxing.
let name = crate::property_call::substitute_type_vars(name, type_args);
let iid = GenericInstanceIdBuilder::generate_id_from_name(&name);
let inner = crate::helpers::ireference_inner_type(&name).unwrap_or_default();
return PointerPlan::IReference(inner.to_owned(), iid);
}
if !signature.contains('.') {
return PointerPlan::Plain;
Expand Down
1 change: 1 addition & 0 deletions runtime/src/napi_engine/invoke.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ pub(crate) fn convert_call_result(
}
ReturnKind::Reference { value, size } => {
let inner = crate::helpers::ireference_inner_type(return_type).unwrap_or(return_type);
let inner = crate::property_call::abi_type_arg(inner);
return match unsafe { crate::value::read_reference_value(result, *size) } {
Some(mut buf) => {
let ptr = buf.as_mut_ptr() as *mut c_void;
Expand Down
5 changes: 5 additions & 0 deletions runtime/src/napi_engine/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -877,6 +877,11 @@ pub fn box_as_ireference(
return Some((NativeValue { pointer: reference.as_raw() }, Some(reference)));
}
}
if vt == ValueType::Number && crate::property_call::is_enum_name(inner_type) {
let value = coerce_f64(env, arg)? as i64 as u32;
let reference = crate::struct_reference::create(value.to_le_bytes().to_vec(), *iid);
return Some((NativeValue { pointer: reference.as_raw() }, Some(reference)));
}
let wrapped = if vt == ValueType::Object || vt == ValueType::Function {
let obj: JsObject = unsafe { arg.cast() };
try_get_external_handle(env, &obj).filter(|ptr| !ptr.is_null())
Expand Down
39 changes: 35 additions & 4 deletions runtime/src/property_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,17 +231,48 @@ pub(crate) fn append_struct_object_bytes_napi(
buf.extend(std::iter::repeat(0u8).take(tail));
}

/// Replaces `Var!N` placeholders in `s` with `type_args`. An enum argument stays named inside
/// another generic (it is part of that type's IID) and becomes its `Int32` ABI type elsewhere.
pub(crate) fn substitute_type_vars(s: &str, type_args: &[String]) -> String {
if type_args.is_empty() {
if type_args.is_empty() || !s.contains("Var!") {
return s.to_string();
}
let mut result = s.to_string();
for (i, arg) in type_args.iter().enumerate() {
result = result.replace(&format!("Var!{}", i), arg.as_str());
let mut result = String::with_capacity(s.len());
let mut depth = 0usize;
let mut rest = s;
while let Some(c) = rest.chars().next() {
if let Some(after) = rest.strip_prefix("Var!") {
let digits = after.bytes().take_while(u8::is_ascii_digit).count();
if let Some(arg) = after[..digits].parse::<usize>().ok().and_then(|i| type_args.get(i)) {
result.push_str(if depth == 0 { abi_type_arg(arg) } else { arg });
rest = &after[digits..];
continue;
}
}
match c {
'<' => depth += 1,
'>' => depth = depth.saturating_sub(1),
_ => {}
}
result.push(c);
rest = &rest[c.len_utf8()..];
}
result
}

/// The ABI type of a generic type argument: `Int32` for an enum, the argument itself otherwise.
pub(crate) fn abi_type_arg(arg: &str) -> &str {
if is_enum_name(arg) { "Int32" } else { arg }
}

/// Whether `name` is a WinRT enum's full name.
pub(crate) fn is_enum_name(name: &str) -> bool {
name.contains('.')
&& !name.contains('<')
&& MetadataReader::find_by_name(name)
.is_some_and(|decl| matches!(decl.read().kind(), DeclarationKind::Enum))
}

/// Immutable per-property-method-type data cached after first construction.
/// Class path key: (method_token as u64) << 1 | (is_initializer as u64).
/// Interface path key: (token, declaring IID, is_setter) — the IID disambiguates
Expand Down
3 changes: 2 additions & 1 deletion runtime/src/struct_reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
//!
//! `PropertyValue` only boxes primitives and a few Foundation types, so the struct's bytes (laid
//! out as for a struct parameter) are held by an agile object that answers the reference's IID,
//! which is computed from its closed generic name, and copies them out of `get_Value`.
//! which is computed from its closed generic name, and copies them out of `get_Value`. An enum
//! `IReference<T>` holds the enum's 4 bytes the same way.

use std::ffi::c_void;
use std::sync::atomic::{AtomicU32, Ordering};
Expand Down
5 changes: 5 additions & 0 deletions runtime/src/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,11 @@ pub fn box_as_ireference(
return Some((NativeValue { pointer: reference.as_raw() }, Some(reference)));
}
}
if arg.is_number() && crate::property_call::is_enum_name(inner_type) {
let value = arg.number_value(scope)? as i64 as u32;
let reference = crate::struct_reference::create(value.to_le_bytes().to_vec(), *iid);
return Some((NativeValue { pointer: reference.as_raw() }, Some(reference)));
}
let wrapped = if arg.is_object() {
arg.to_object(scope)
.and_then(|obj| try_get_external_handle(scope, obj))
Expand Down
55 changes: 55 additions & 0 deletions windows-napi/test/enum-generic-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Generics closed over an enum: IAsyncOperation<Enum>, IVector<Enum> and IReference<Enum>. The
// closed type's IID hashes the enum's name, so the enum must not be reduced to Int32 there.
const { Windows, toPromise, awaitWithPump } = require('../nswinrt.js');

let pass = 0, fail = 0;
function check(name, cond) {
if (cond) pass++;
else { console.log(`FAIL ${name}`); fail++; }
}
function attempt(name, fn) {
try { return fn(); } catch (e) { console.log(`FAIL ${name}: ${e.message}`); fail++; }
}

async function main() {
// IAsyncOperation<VibrationAccessStatus>
const Haptics = Windows.Devices.Haptics;
const op = Haptics.VibrationDevice.RequestAccessAsync();
check('IAsyncOperation<Enum> exposes Status', typeof op.Status === 'number');
let status;
try {
status = await awaitWithPump(toPromise(op), 5000);
} catch (e) {
console.log(`FAIL toPromise(IAsyncOperation<Enum>): ${e.message}`);
fail++;
}
check('IAsyncOperation<Enum> resolves to a number', typeof status === 'number');
check('IAsyncOperation<Enum> GetResults after completion', attempt('GetResults', () => op.GetResults()) === status);

// IVector<DeviceClass>
const Enumeration = Windows.Devices.Enumeration;
const classes = attempt('IVector<Enum>', () => new Enumeration.DevicePicker().Filter.SupportedDeviceClasses);
if (classes) {
attempt('IVector<Enum>.Append', () => {
classes.Append(Enumeration.DeviceClass.AudioCapture);
classes.Append(Enumeration.DeviceClass.VideoCapture);
});
check('IVector<Enum>.Size', classes.Size === 2);
check('IVector<Enum>.GetAt', attempt('GetAt', () => classes.GetAt(0)) === Enumeration.DeviceClass.AudioCapture);
check('IVector<Enum>[i]', classes[1] === Enumeration.DeviceClass.VideoCapture);
}

// IReference<ManualFocusDistance>
const Devices = Windows.Media.Devices;
const focus = new Devices.FocusSettings();
check('IReference<Enum> starts null', focus.Distance == null);
attempt('IReference<Enum> set', () => { focus.Distance = Devices.ManualFocusDistance.Nearest; });
check('IReference<Enum> round-trips', focus.Distance === Devices.ManualFocusDistance.Nearest);
attempt('IReference<Enum> set null', () => { focus.Distance = null; });
check('IReference<Enum> clears to null', focus.Distance == null);

console.log(`\n${pass} passed, ${fail} failed`);
process.exitCode = fail ? 1 : 0;
}

main().catch((e) => { console.log('FATAL', e && e.stack || e); process.exitCode = 1; });
Loading