From 93529af8ecc83c9ea0fbf61d1a6b30f4f43aa151 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 1 Oct 2026 21:59:25 +0800 Subject: [PATCH 1/2] Fix explicit WinRT delegate root selection Classify validated non-generic WinRT delegates before runtime-class fallback and reuse the shared dependency interface parser for JavaScript and Python. Include namespace roots, selection-order/incremental regressions, strict callable consumers, and bounded real ThreadPool callback fixtures. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- tools/dynwinrt-codegen/README.md | 23 +- tools/dynwinrt-codegen/src/main.rs | 9 +- tools/dynwinrt-codegen/src/meta.rs | 120 +++ .../tests/delegate_root_selection_test.rs | 924 ++++++++++++++++++ .../tests/fixtures/delegate_root_native.mjs | 68 ++ .../tests/fixtures/delegate_root_native.py | 55 ++ 6 files changed, 1197 insertions(+), 2 deletions(-) create mode 100644 tools/dynwinrt-codegen/tests/delegate_root_selection_test.rs create mode 100644 tools/dynwinrt-codegen/tests/fixtures/delegate_root_native.mjs create mode 100644 tools/dynwinrt-codegen/tests/fixtures/delegate_root_native.py diff --git a/tools/dynwinrt-codegen/README.md b/tools/dynwinrt-codegen/README.md index e436347b..d377d3a7 100644 --- a/tools/dynwinrt-codegen/README.md +++ b/tools/dynwinrt-codegen/README.md @@ -52,7 +52,7 @@ package. | `--winmd-list FILE` | Newline-separated metadata paths to emit; blank lines and `#` comments are ignored. | | `--folder DIR` | Load every `.winmd` file directly inside a directory. | | `--namespace NS` | Generate one namespace. Without it, generate all non-`Windows.*` namespaces in the input. | -| `--class-name NAME[,NAME...]` | Generate specific classes, public interfaces, or native `Apis` containers. Use fully qualified names, or unqualified names together with `--namespace`. | +| `--class-name NAME[,NAME...]` | Generate specific classes, public interfaces, non-generic WinRT delegates, or native `Apis` containers. Use fully qualified names, or unqualified names together with `--namespace`. | | `--ref PATH[;PATH...]` | Metadata used only for type resolution. Sibling discovery is disabled for references. | | `--ref-list FILE` | Newline-separated reference metadata paths; blank lines and `#` comments are ignored. | | `--lang js\|py` | `js` emits CommonJS `.js`, an ESM facade, and `.d.ts` files (default); `py` emits `.py`, `.pyi`, and `py.typed`. | @@ -82,6 +82,20 @@ dynwinrt-codegen generate ` --output .\generated-python ``` +Explicitly select a WinRT class and its delegate (also supported with `--lang py`): + +```powershell +dynwinrt-codegen generate ` + --namespace Windows.System.Threading ` + --class-name ThreadPool,WorkItemHandler ` + --output .\generated +``` + +Delegates can also be selected alone or emitted through their namespace. Their +IID and callback parameter types come from the same metadata pipeline used for +automatic dependencies. Open generic delegate definitions are not supported as +explicit roots; closed instantiations continue to be resolved as dependencies. + Load emitted metadata and reference metadata from list files: ```powershell @@ -188,6 +202,13 @@ cargo build -p dynwinrt-codegen --release cargo test -p dynwinrt-codegen ``` +`delegate_root_selection_test` covers explicit, incremental, namespace, and +automatic delegate selection. Its native callback cases use a built JavaScript +binding and `DYNWINRT_TEST_PYTHON` pointing to a Python environment with the +matching wheel installed. `DYNWINRT_TEST_JS_RUNTIME` (binding package directory) +and `DYNWINRT_TEST_NODE` can select a matching JavaScript runtime/architecture; +`DYNWINRT_PYRIGHT` enables the additional strict Python consumer checks. + Official npm and PyPI packages are built and published by the repository release pipelines. diff --git a/tools/dynwinrt-codegen/src/main.rs b/tools/dynwinrt-codegen/src/main.rs index 9bbdd142..83fbc7fd 100644 --- a/tools/dynwinrt-codegen/src/main.rs +++ b/tools/dynwinrt-codegen/src/main.rs @@ -143,7 +143,8 @@ enum Commands { #[arg(long, value_name = "NS")] namespace: Option, - /// Generate bindings for specific class(es), comma-separated. + /// Generate specific classes, public interfaces, or non-generic WinRT delegates, + /// comma-separated. /// Names may be qualified, or unqualified when --namespace is supplied. /// E.g. --class-name Uri or --class-name Windows.Foundation.Uri #[arg(long, name = "class", value_name = "NAME")] @@ -595,6 +596,10 @@ fn run() -> Result<(), String> { com_coclasses.push(coclass); continue; } + if let Some(delegate) = meta::parse_delegate(&winmd, ns, cls)? { + requested_winrt_interfaces.push(delegate); + continue; + } if let Some(interface) = meta::parse_public_interface(&winmd, ns, cls) { requested_winrt_interfaces.push(interface); continue; @@ -1108,6 +1113,7 @@ fn run() -> Result<(), String> { } selected_classes.extend(meta::parse_namespace(&winmd, ns)); selected_interfaces.extend(meta::parse_interfaces(&winmd, ns)); + selected_interfaces.extend(meta::parse_delegates(&winmd, ns)?); selected_enums.extend(meta::parse_enums(&winmd, ns)); } winui::add_implicit_classes(&winmd, &mut selected_classes); @@ -1138,6 +1144,7 @@ fn run() -> Result<(), String> { for ns in &namespaces { all_classes.extend(meta::parse_namespace(&winmd, ns)); all_interfaces.extend(meta::parse_interfaces(&winmd, ns)); + all_interfaces.extend(meta::parse_delegates(&winmd, ns)?); all_enums.extend(meta::parse_enums(&winmd, ns)); } winui::add_implicit_classes(&winmd, &mut all_classes); diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index 126f5039..95c010cf 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -626,6 +626,126 @@ fn parse_public_interface_from_def( parse_interface(index, def.namespace(), def.name()) } +/// Parse a non-generic WinRT delegate root through the interface dependency pipeline. +/// Invalid delegate definitions must not fall back to runtime-class projection. +pub fn parse_delegate( + winmd_paths: &str, + namespace: &str, + name: &str, +) -> Result, String> { + let index = load_index(winmd_paths) + .ok_or_else(|| "Failed to load WinRT delegate metadata".to_string())?; + let Some(def) = index.get(namespace, name).next() else { + return Ok(None); + }; + if !is_delegate_definition(&def) { + return Ok(None); + } + parse_delegate_from_def(&index, &def).map(Some) +} + +/// Parse public, non-generic WinRT delegates selected by a namespace root. +pub fn parse_delegates(winmd_paths: &str, namespace: &str) -> Result, String> { + let index = load_index(winmd_paths) + .ok_or_else(|| "Failed to load WinRT delegate metadata".to_string())?; + index + .all() + .filter(|def| { + def.namespace() == namespace + && is_delegate_definition(def) + && def + .flags() + .contains(windows_metadata::TypeAttributes::WindowsRuntime) + && def + .flags() + .contains(windows_metadata::TypeAttributes::Public) + && !def.flags().is_nested() + && !def.name().starts_with('<') + && def.generic_params().next().is_none() + }) + .map(|def| parse_delegate_from_def(&index, &def)) + .collect() +} + +fn is_delegate_definition(def: &reader::TypeDef) -> bool { + def.extends().is_some_and(|base| { + base.namespace() == "System" && matches!(base.name(), "Delegate" | "MulticastDelegate") + }) +} + +fn parse_delegate_from_def( + index: &reader::Index, + def: &reader::TypeDef, +) -> Result { + use windows_metadata::{Type, TypeAttributes, Value}; + + let invalid = |reason: &str| { + format!( + "Cannot generate WinRT delegate {}.{}: {reason}", + def.namespace(), + def.name() + ) + }; + if !def.flags().contains(TypeAttributes::WindowsRuntime) + || !def.flags().contains(TypeAttributes::Public) + || def.flags().is_nested() + || def.flags().contains(TypeAttributes::Interface) + || def.name().starts_with('<') + || def.has_attribute("ExclusiveToAttribute") + { + return Err(invalid("not a public Windows Runtime delegate")); + } + if def.generic_params().next().is_some() { + return Err(invalid( + "open generic delegate roots require closed type arguments and are not supported", + )); + } + let Some(attribute) = def.find_attribute("GuidAttribute") else { + return Err(invalid("missing GuidAttribute")); + }; + if !matches!( + attribute.value().as_slice(), + [ + (_, Value::U32(_)), + (_, Value::U16(_)), + (_, Value::U16(_)), + (_, Value::U8(_)), + (_, Value::U8(_)), + (_, Value::U8(_)), + (_, Value::U8(_)), + (_, Value::U8(_)), + (_, Value::U8(_)), + (_, Value::U8(_)), + (_, Value::U8(_)) + ] + ) { + return Err(invalid("malformed GuidAttribute")); + } + if !def.methods().any(|method| method.name() == ".ctor") + || def + .methods() + .filter(|method| method.name() == "Invoke") + .count() + != 1 + { + return Err(invalid("requires .ctor and exactly one Invoke contract")); + } + + let mut contract = InterfaceImplementationMetadata::default(); + collect_implementation_delegates( + &Type::named(def.namespace(), def.name()), + index, + &[], + &mut contract, + ); + if !contract.diagnostics.is_empty() { + return Err(invalid(&contract.diagnostics.join("; "))); + } + parse_interface(index, def.namespace(), def.name()) + .filter(InterfaceMeta::is_delegate) + .ok_or_else(|| invalid("could not parse delegate metadata")) +} + /// Parse enums in a namespace. pub fn parse_enums(winmd_paths: &str, namespace: &str) -> Vec { let index = match load_index(winmd_paths) { diff --git a/tools/dynwinrt-codegen/tests/delegate_root_selection_test.rs b/tools/dynwinrt-codegen/tests/delegate_root_selection_test.rs new file mode 100644 index 00000000..3ecaa1cc --- /dev/null +++ b/tools/dynwinrt-codegen/tests/delegate_root_selection_test.rs @@ -0,0 +1,924 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output, Stdio}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{Duration, Instant}; + +use dynwinrt_codegen::meta; +use dynwinrt_codegen::types::{TypeIdentityKind, TypeMeta}; +use windows_metadata::{ + GenericParamAttributes, HasAttributes, MethodAttributes, MethodCallAttributes, + MethodImplAttributes, ParamAttributes, Signature, Type, TypeAttributes, Value, reader, writer, +}; + +const NAMESPACE: &str = "Windows.System.Threading"; +const DEFAULT_WINDOWS_WINMD: &str = + r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd"; +static NEXT: AtomicU64 = AtomicU64::new(0); + +struct Fixture(PathBuf); + +impl Fixture { + fn new() -> Self { + let path = repo().join("target").join(format!( + "dr{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn repo() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap() + .to_path_buf() +} + +fn windows_winmd() -> Option { + let path = std::env::var_os("DYNWINRT_WINDOWS_WINMD") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from(DEFAULT_WINDOWS_WINMD)); + if path.is_file() { + return Some(path); + } + assert!( + std::env::var("DYNWINRT_REQUIRE_TSC").as_deref() != Ok("1"), + "Windows metadata is missing at {}", + path.display() + ); + eprintln!("Skipping stock delegate checks: Windows metadata unavailable"); + None +} + +fn diagnostics(output: &Output) -> String { + format!( + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ) +} + +fn generate( + metadata: &Path, + output: &Path, + namespace: &str, + roots: Option<&str>, + language: &str, + no_pyi: bool, +) -> Output { + let mut command = Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")); + command + .args(["generate", "--winmd"]) + .arg(metadata) + .args(["--namespace", namespace, "--lang", language, "--output"]) + .arg(output); + if let Some(roots) = roots { + command.args(["--class-name", roots]); + } + if no_pyi { + command.arg("--no-pyi"); + } + command.output().unwrap() +} + +fn generate_ok(metadata: &Path, output: &Path, roots: Option<&str>, language: &str, no_pyi: bool) { + let result = generate(metadata, output, NAMESPACE, roots, language, no_pyi); + assert!(result.status.success(), "{}", diagnostics(&result)); +} + +fn module_file(output: &Path, name: &str, language: &str, extension: &str) -> PathBuf { + if language == "js" { + output + .join("windows") + .join("system") + .join("threading") + .join(format!("{name}.{extension}")) + } else { + let module = dynwinrt_codegen::codegen::python::to_snake_case_filename(name); + output.join(format!("windows__system__threading__{module}.{extension}")) + } +} + +fn assert_delegate(output: &Path, name: &str, language: &str, no_pyi: bool) { + let extension = if language == "js" { "js" } else { "py" }; + let source = fs::read_to_string(module_file(output, name, language, extension)).unwrap(); + assert!(source.contains(&format!("IID_{name} = ")), "{source}"); + assert!( + source.contains(&format!("{name}_PARAM_TYPES = [")), + "{source}" + ); + assert!(!source.contains(&format!("class {name}")), "{source}"); + assert!(!source.contains("Placeholder"), "{source}"); + if language == "js" { + assert!( + source.contains(&format!("exports.IID_{name} = IID_{name};")), + "{source}" + ); + let declaration = fs::read_to_string(module_file(output, name, language, "d.ts")).unwrap(); + assert!( + declaration.contains(&format!("export type {name} = ")), + "{declaration}" + ); + } else if !no_pyi { + let stub = fs::read_to_string(module_file(output, name, language, "pyi")).unwrap(); + assert!(stub.contains(&format!("IID_{name}: WinGUID")), "{stub}"); + assert!( + stub.contains(&format!("{name}_PARAM_TYPES: list[DynWinRTType]")), + "{stub}" + ); + } else { + assert!(!module_file(output, name, language, "pyi").exists()); + assert!(!output.join("py.typed").exists()); + } +} + +fn assert_same_modules( + expected: &Path, + actual: &Path, + names: &[&str], + language: &str, + no_pyi: bool, +) { + let extensions = match (language, no_pyi) { + ("js", _) => &["js", "d.ts"][..], + ("py", false) => &["py", "pyi"][..], + ("py", true) => &["py"][..], + _ => unreachable!(), + }; + for name in names { + for extension in extensions { + let expected = module_file(expected, name, language, extension); + let actual = module_file(actual, name, language, extension); + assert_eq!( + fs::read(&expected).unwrap(), + fs::read(&actual).unwrap(), + "{} differs from {}", + actual.display(), + expected.display() + ); + } + } +} + +#[test] +fn stock_delegate_roots_match_automatic_dependencies_in_both_languages() { + let Some(winmd) = windows_winmd() else { + return; + }; + for (class, delegate) in [ + ("ThreadPool", "WorkItemHandler"), + ("ThreadPoolTimer", "TimerElapsedHandler"), + ] { + for (language, no_pyi) in [("js", false), ("py", false), ("py", true)] { + let fixture = Fixture::new(); + let automatic = fixture.0.join("automatic"); + generate_ok(&winmd, &automatic, Some(class), language, no_pyi); + assert_delegate(&automatic, delegate, language, no_pyi); + for roots in [ + delegate.to_string(), + format!("{class},{delegate}"), + format!("{delegate},{class}"), + ] { + let explicit = fixture.0.join("explicit"); + generate_ok(&winmd, &explicit, Some(&roots), language, no_pyi); + assert_delegate(&explicit, delegate, language, no_pyi); + assert_same_modules(&automatic, &explicit, &[delegate], language, no_pyi); + if roots.contains(class) { + assert_same_modules(&automatic, &explicit, &[class], language, no_pyi); + } + } + + let incremental = fixture.0.join("incremental"); + for roots in [ + delegate.to_string(), + class.to_string(), + format!("{class},{delegate}"), + format!("{delegate},{class}"), + format!("{delegate},{class}"), + ] { + generate_ok(&winmd, &incremental, Some(&roots), language, no_pyi); + assert_delegate(&incremental, delegate, language, no_pyi); + } + assert_same_modules( + &automatic, + &incremental, + &[class, delegate], + language, + no_pyi, + ); + + generate_ok(&winmd, &automatic, Some(delegate), language, no_pyi); + assert_delegate(&automatic, delegate, language, no_pyi); + assert_same_modules( + &incremental, + &automatic, + &[class, delegate], + language, + no_pyi, + ); + + let namespace = fixture.0.join("namespace"); + generate_ok(&winmd, &namespace, None, language, no_pyi); + assert_delegate(&namespace, delegate, language, no_pyi); + assert_same_modules(&incremental, &namespace, &[delegate], language, no_pyi); + } + } +} + +#[test] +fn stock_delegate_classification_preserves_winmd_identity_and_invoke_contracts() { + let Some(winmd) = windows_winmd() else { + return; + }; + let path = winmd.to_str().unwrap(); + let index = reader::Index::read(path).unwrap(); + for (class, name) in [ + ("ThreadPool", "WorkItemHandler"), + ("ThreadPoolTimer", "TimerElapsedHandler"), + ] { + let definition = index.get(NAMESPACE, name).next().unwrap(); + let base = definition.extends().unwrap(); + assert_eq!( + (base.namespace(), base.name()), + ("System", "MulticastDelegate") + ); + assert!(definition.flags().contains(TypeAttributes::WindowsRuntime)); + assert!(!definition.flags().contains(TypeAttributes::Interface)); + assert!(definition.generic_params().next().is_none()); + assert!(definition.find_attribute("GuidAttribute").is_some()); + assert!(meta::parse_public_interface(path, NAMESPACE, name).is_none()); + + let root = meta::parse_delegate(path, NAMESPACE, name) + .unwrap() + .unwrap(); + assert_eq!( + root.type_identity().kind(), + Some(TypeIdentityKind::Delegate) + ); + let class = meta::parse_class(path, NAMESPACE, class).unwrap(); + let dependencies = meta::resolve_dependencies(path, &[class], &[], &[]); + let dependency = dependencies + .interfaces + .iter() + .find(|interface| interface.namespace == NAMESPACE && interface.name == name) + .unwrap(); + assert_eq!(root.iid, dependency.iid); + let invoke = root + .methods + .iter() + .find(|method| method.name == "Invoke") + .unwrap(); + let dependency_invoke = dependency + .methods + .iter() + .find(|method| method.name == "Invoke") + .unwrap(); + assert!(invoke.return_type.is_none()); + assert_eq!(invoke.params.len(), 1); + assert_eq!(invoke.params[0].typ, dependency_invoke.params[0].typ); + assert_eq!( + invoke.params[0].direction, + dependency_invoke.params[0].direction + ); + if name == "WorkItemHandler" { + assert_eq!(root.iid, "1d1a8b8b-fa66-414f-9cbd-b65fc99d17fa"); + assert_eq!(invoke.params[0].typ, TypeMeta::AsyncAction); + } else { + assert!(matches!( + &invoke.params[0].typ, + TypeMeta::RuntimeClass { namespace, name, .. } + if namespace == NAMESPACE && name == "ThreadPoolTimer" + )); + } + eprintln!("{NAMESPACE}.{name}: {} -> {:?}", root.iid, invoke.params); + } + for (namespace, name) in [ + (NAMESPACE, "ThreadPool"), + (NAMESPACE, "WorkItemPriority"), + ("Windows.Foundation", "Point"), + ("Windows.Foundation", "IStringable"), + ("Windows.Foundation", "IUriRuntimeClass"), + ("Windows.Foundation", "IReference"), + ] { + assert!( + meta::parse_delegate(path, namespace, name) + .unwrap() + .is_none() + ); + } + let error = meta::parse_delegate(path, "Windows.Foundation", "EventHandler").unwrap_err(); + assert!(error.contains("open generic delegate roots"), "{error}"); + let fixture = Fixture::new(); + for language in ["js", "py"] { + let output = fixture.0.join(language); + let result = generate( + &winmd, + &output, + "Windows.Foundation", + Some("EventHandler"), + language, + false, + ); + assert!(!result.status.success(), "{}", diagnostics(&result)); + assert!(diagnostics(&result).contains("open generic delegate roots")); + assert!(!output.exists()); + } +} + +fn guid(file: &mut writer::File, definition: writer::TypeDef, values: Vec) { + let attribute = file.TypeRef("Windows.Foundation.Metadata", "GuidAttribute"); + let types = values + .iter() + .map(|value| match value { + Value::U32(_) => Type::U32, + Value::U16(_) => Type::U16, + Value::U8(_) => Type::U8, + Value::I32(_) => Type::I32, + Value::Utf8(_) => Type::String, + _ => unreachable!(), + }) + .collect(); + let constructor = file.MemberRef( + ".ctor", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::Void, + types, + }, + writer::MemberRefParent::TypeRef(attribute), + ); + file.Attribute( + writer::HasAttribute::TypeDef(definition), + writer::AttributeType::MemberRef(constructor), + &values + .into_iter() + .map(|value| (String::new(), value)) + .collect::>(), + ); +} + +fn delegate_fixture() -> Vec { + let mut file = writer::File::new("DelegateRoots"); + let base = file.TypeRef("System", "MulticastDelegate"); + let object = file.TypeRef("System", "Object"); + for name in [ + "ValidHandler", + "ClrHandler", + "PrivateHandler", + "NestedHandler", + "InterfaceHandler", + "MissingGuid", + "ShortGuid", + "WrongGuidTypes", + "MissingCtor", + "MissingInvoke", + "DuplicateInvoke", + "StaticInvoke", + "MissingParam", + "InOutParam", + "NativePointer", + "PreserveSig", + "GenericHandler", + "Pretender", + ] { + let visibility = match name { + "PrivateHandler" => TypeAttributes::default(), + _ => TypeAttributes::Public, + }; + let flags = visibility | TypeAttributes::Sealed; + let flags = if name == "NestedHandler" { + // This includes nested visibility bits absent from the writer's named constants. + !TypeAttributes::Interface + } else if name == "InterfaceHandler" { + flags | TypeAttributes::Interface + } else { + flags + }; + let definition = file.TypeDef( + "Tests.DelegateRoots", + name, + writer::TypeDefOrRef::TypeRef(if name == "Pretender" { object } else { base }), + if name == "ClrHandler" { + flags + } else { + flags | TypeAttributes::WindowsRuntime + }, + ); + if name == "GenericHandler" { + file.GenericParam( + "T", + writer::TypeOrMethodDef::TypeDef(definition), + 0, + GenericParamAttributes::default(), + ); + } + if name != "MissingGuid" { + let mut values = vec![Value::U32(1), Value::U16(2), Value::U16(3)]; + values.extend((4..12).map(Value::U8)); + if name == "ShortGuid" { + values = vec![Value::Utf8("not-a-guid".into())]; + } else if name == "WrongGuidTypes" { + values[0] = Value::I32(1); + } + guid(&mut file, definition, values); + } + if name != "MissingCtor" { + file.MethodDef( + ".ctor", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::Void, + types: vec![], + }, + MethodAttributes::Public | MethodAttributes::SpecialName, + MethodImplAttributes::default(), + ); + } + if name == "MissingInvoke" { + continue; + } + for _ in 0..if name == "DuplicateInvoke" { 2 } else { 1 } { + file.MethodDef( + "Invoke", + &Signature { + flags: if name == "StaticInvoke" { + MethodCallAttributes::default() + } else { + MethodCallAttributes::HASTHIS + }, + return_type: Type::Void, + types: vec![if name == "NativePointer" { + Type::PtrMut(Box::new(Type::I32), 1) + } else { + Type::I32 + }], + }, + MethodAttributes::Public | MethodAttributes::Virtual | MethodAttributes::NewSlot, + if name == "PreserveSig" { + MethodImplAttributes::PreserveSig + } else { + MethodImplAttributes::default() + }, + ); + if name != "MissingParam" { + file.Param( + "value", + 1, + if name == "InOutParam" { + ParamAttributes::In | ParamAttributes::Out + } else { + ParamAttributes::In + }, + ); + } + } + } + file.into_stream() +} + +#[test] +fn standalone_namespace_delegates_are_emitted_without_referencing_classes() { + let fixture = Fixture::new(); + let mut file = writer::File::new("StandaloneDelegate"); + let base = file.TypeRef("System", "MulticastDelegate"); + let definition = file.TypeDef( + "Tests.StandaloneDelegate", + "Handler", + writer::TypeDefOrRef::TypeRef(base), + TypeAttributes::Public | TypeAttributes::Sealed | TypeAttributes::WindowsRuntime, + ); + let mut values = vec![Value::U32(1), Value::U16(2), Value::U16(3)]; + values.extend((4..12).map(Value::U8)); + guid(&mut file, definition, values); + for name in [".ctor", "Invoke"] { + file.MethodDef( + name, + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::Void, + types: vec![], + }, + MethodAttributes::Public, + MethodImplAttributes::default(), + ); + } + let metadata = fixture.0.join("Standalone.winmd"); + fs::write(&metadata, file.into_stream()).unwrap(); + for language in ["js", "py"] { + let output = fixture.0.join(language); + let result = generate( + &metadata, + &output, + "Tests.StandaloneDelegate", + None, + language, + false, + ); + assert!(result.status.success(), "{}", diagnostics(&result)); + let module = if language == "js" { + output + .join("tests") + .join("standalone-delegate") + .join("Handler.js") + } else { + output.join("tests__standalone_delegate__handler.py") + }; + let source = fs::read_to_string(module).unwrap(); + assert!(source.contains("IID_Handler = "), "{source}"); + assert!(source.contains("Handler_PARAM_TYPES = []"), "{source}"); + assert!(!source.contains("class Handler"), "{source}"); + } +} + +#[test] +fn malformed_or_non_winrt_delegate_roots_fail_before_class_fallback() { + let fixture = Fixture::new(); + let metadata = fixture.0.join("DelegateRoots.winmd"); + fs::write(&metadata, delegate_fixture()).unwrap(); + let path = metadata.to_str().unwrap(); + let namespace = "Tests.DelegateRoots"; + let valid = meta::parse_delegate(path, namespace, "ValidHandler") + .unwrap() + .unwrap(); + assert!(valid.is_delegate()); + assert_eq!( + valid + .methods + .iter() + .find(|method| method.name == "Invoke") + .unwrap() + .params[0] + .typ, + TypeMeta::I32 + ); + assert!( + meta::parse_delegate(path, namespace, "Pretender") + .unwrap() + .is_none() + ); + assert!( + meta::parse_delegate(path, namespace, "DoesNotExist") + .unwrap() + .is_none() + ); + for (name, reason) in [ + ("ClrHandler", "not a public Windows Runtime delegate"), + ("PrivateHandler", "not a public Windows Runtime delegate"), + ("NestedHandler", "not a public Windows Runtime delegate"), + ("InterfaceHandler", "not a public Windows Runtime delegate"), + ("MissingGuid", "missing GuidAttribute"), + ("ShortGuid", "malformed GuidAttribute"), + ("WrongGuidTypes", "malformed GuidAttribute"), + ( + "MissingCtor", + "requires .ctor and exactly one Invoke contract", + ), + ( + "MissingInvoke", + "requires .ctor and exactly one Invoke contract", + ), + ( + "DuplicateInvoke", + "requires .ctor and exactly one Invoke contract", + ), + ("StaticInvoke", "unsupported native calling convention"), + ("MissingParam", "incomplete delegate parameter contracts"), + ("InOutParam", "unsupported direction contract"), + ("NativePointer", "unsupported native type PtrMut"), + ("PreserveSig", "unsupported native calling convention"), + ("GenericHandler", "open generic delegate roots"), + ] { + let error = meta::parse_delegate(path, namespace, name).unwrap_err(); + assert!(error.contains(reason), "{name}: {error}"); + for language in ["js", "py"] { + let output = fixture.0.join(format!("{name}-{language}")); + let result = generate(&metadata, &output, namespace, Some(name), language, false); + assert!(!result.status.success(), "{name}: {}", diagnostics(&result)); + assert!( + diagnostics(&result).contains(reason), + "{}", + diagnostics(&result) + ); + assert!( + !output.exists(), + "invalid root committed {}", + output.display() + ); + } + } +} + +fn python() -> PathBuf { + std::env::var_os("DYNWINRT_TEST_PYTHON") + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("python")) +} + +fn js_runtime_package(output: &Path, runtime: &Path) { + let package = output + .join("node_modules") + .join("@microsoft") + .join("dynwinrt"); + fs::create_dir_all(&package).unwrap(); + fs::write( + package.join("package.json"), + serde_json::to_vec(&serde_json::json!({ + "name": "@microsoft/dynwinrt", + "main": runtime.join("dist").join("winrt.js"), + "types": runtime.join("dist").join("winrt.d.ts") + })) + .unwrap(), + ) + .unwrap(); +} + +fn bounded_native(command: &mut Command) -> Output { + let mut child = command + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .unwrap(); + let deadline = Instant::now() + Duration::from_secs(20); + while child.try_wait().unwrap().is_none() { + if Instant::now() >= deadline { + child.kill().unwrap(); + let output = child.wait_with_output().unwrap(); + panic!("Native delegate child timed out: {}", diagnostics(&output)); + } + std::thread::sleep(Duration::from_millis(10)); + } + let output = child.wait_with_output().unwrap(); + assert!(output.status.success(), "{}", diagnostics(&output)); + let evidence = String::from_utf8_lossy(&output.stdout); + assert!(evidence.contains("\"callbacks\""), "{evidence}"); + eprintln!("{evidence}"); + output +} + +#[test] +fn explicitly_selected_js_delegate_runs_a_real_thread_pool_callback_cjs_and_esm() { + let Some(winmd) = windows_winmd() else { + return; + }; + let runtime = std::env::var_os("DYNWINRT_TEST_JS_RUNTIME") + .map(PathBuf::from) + .unwrap_or_else(|| repo().join("bindings").join("js")); + if !runtime.join("dist").join("winrt.js").is_file() { + eprintln!("Skipping native JS delegate checks: matching production binding is not built"); + return; + } + let node = std::env::var_os("DYNWINRT_TEST_NODE").unwrap_or_else(|| "node".into()); + for module in ["cjs", "esm"] { + let fixture = Fixture::new(); + js_runtime_package(&fixture.0, &runtime); + let output = fixture.0.join("generated"); + generate_ok( + &winmd, + &output, + Some("ThreadPool,WorkItemHandler"), + "js", + false, + ); + bounded_native( + Command::new(&node) + .arg("--expose-gc") + .arg( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("delegate_root_native.mjs"), + ) + .arg(&runtime) + .arg(&output) + .arg(module), + ); + } +} + +#[test] +fn explicitly_selected_python_delegate_runs_a_real_thread_pool_callback_with_and_without_stubs() { + let Some(winmd) = windows_winmd() else { + return; + }; + if std::env::var_os("DYNWINRT_TEST_PYTHON").is_none() { + eprintln!( + "Skipping native Python delegate checks: set DYNWINRT_TEST_PYTHON to the matching wheel" + ); + return; + } + for no_pyi in [false, true] { + let fixture = Fixture::new(); + let output = fixture.0.join("generated"); + generate_ok( + &winmd, + &output, + Some("ThreadPool,WorkItemHandler"), + "py", + no_pyi, + ); + bounded_native( + Command::new(python()) + .arg("-B") + .arg( + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("delegate_root_native.py"), + ) + .arg(&output), + ); + } +} + +#[test] +fn explicitly_selected_delegate_callables_pass_strict_typescript() { + let Some(winmd) = windows_winmd() else { + return; + }; + let tsc = std::env::var_os("DYNWINRT_TSC") + .map(PathBuf::from) + .unwrap_or_else(|| { + repo() + .join("bindings") + .join("js") + .join("node_modules") + .join("typescript") + .join("bin") + .join("tsc") + }); + if !tsc.is_file() { + assert!( + std::env::var("DYNWINRT_REQUIRE_TSC").as_deref() != Ok("1"), + "Missing TypeScript compiler" + ); + eprintln!("Skipping strict delegate consumer: TypeScript unavailable"); + return; + } + let fixture = Fixture::new(); + let output = fixture.0.join("generated"); + generate_ok( + &winmd, + &output, + Some("ThreadPool,WorkItemHandler"), + "js", + false, + ); + let package = fixture + .0 + .join("node_modules") + .join("@microsoft") + .join("dynwinrt"); + fs::create_dir_all(&package).unwrap(); + fs::write( + package.join("package.json"), + r#"{"name":"@microsoft/dynwinrt","types":"index.d.ts"}"#, + ) + .unwrap(); + fs::write(package.join("index.d.ts"), "export declare class DynWinRtType {}\nexport declare class WinGuid {}\nexport declare class DynWinRtValue { release(): void; }\n").unwrap(); + fs::write(fixture.0.join("consumer.ts"), r#" +import { ThreadPool } from './generated/windows/system/threading/ThreadPool.js'; +import { WorkItemHandler, IID_WorkItemHandler, WorkItemHandler_PARAM_TYPES } from './generated/windows/system/threading/WorkItemHandler.js'; +import { DynWinRtValue, DynWinRtType, WinGuid } from '@microsoft/dynwinrt'; +const handler: WorkItemHandler = (operation: DynWinRtValue): void => { operation.release(); }; +const operation: Promise = ThreadPool.runAsync(handler); +const iid: WinGuid = IID_WorkItemHandler; +const parameters: DynWinRtType[] = WorkItemHandler_PARAM_TYPES; +// @ts-expect-error A delegate input must be callable. +ThreadPool.runAsync(42); +// @ts-expect-error The callback receives one argument, not two required arguments. +ThreadPool.runAsync((first: DynWinRtValue, second: DynWinRtValue) => {}); +// @ts-expect-error A delegate is a callback type, not a runtime class constructor. +new WorkItemHandler(); +void [operation, iid, parameters]; +"#).unwrap(); + let result = Command::new("node") + .arg(tsc) + .args([ + "--noEmit", + "--strict", + "--target", + "ES2022", + "--module", + "Node16", + "--moduleResolution", + "Node16", + "consumer.ts", + ]) + .current_dir(&fixture.0) + .output() + .unwrap(); + assert!(result.status.success(), "{}", diagnostics(&result)); +} + +#[test] +fn explicitly_selected_delegate_callables_pass_strict_mypy_and_pyright() { + let Some(winmd) = windows_winmd() else { + return; + }; + let available = Command::new(python()) + .args(["-m", "mypy", "--version"]) + .output() + .is_ok_and(|output| output.status.success()); + assert!( + available || std::env::var("DYNWINRT_REQUIRE_MYPY").as_deref() != Ok("1"), + "Missing mypy" + ); + if !available { + eprintln!("Skipping strict delegate consumers: mypy unavailable"); + return; + } + for no_pyi in [false, true] { + let fixture = Fixture::new(); + let output = fixture.0.join("generated"); + generate_ok( + &winmd, + &output, + Some("ThreadPool,WorkItemHandler"), + "py", + no_pyi, + ); + for (negative, expected_errors) in [(false, 0), (true, 1)] { + let consumer = fixture.0.join("consumer.py"); + fs::write( + &consumer, + format!( + "# pyright: strict\n\ + from typing import assert_type\n\ + from dynwinrt import DynWinRTValue, WinRTCoroutine\n\ + from generated.windows__system__threading__thread_pool import ThreadPool\n\ + def handler(operation: DynWinRTValue) -> None:\n operation.release()\n\ + assert_type(ThreadPool.run_async(handler), WinRTCoroutine[None])\n{}", + if negative { + "ThreadPool.run_async(42)\n" + } else { + "" + }, + ), + ) + .unwrap(); + let result = Command::new(python()) + .args([ + "-B", + "-m", + "mypy", + "--strict", + "--no-incremental", + "--follow-imports=silent", + "--no-pretty", + "--show-error-codes", + ]) + .arg(&consumer) + .env( + "MYPYPATH", + repo().join("bindings").join("py").join("python"), + ) + .current_dir(&fixture.0) + .output() + .unwrap(); + let text = diagnostics(&result); + assert_eq!( + text.lines() + .filter(|line| line.contains(": error:")) + .count(), + expected_errors, + "{text}" + ); + assert_eq!(result.status.success(), expected_errors == 0, "{text}"); + if negative { + assert!(text.contains("[arg-type]"), "{text}"); + } + if let Some(pyright) = std::env::var_os("DYNWINRT_PYRIGHT") { + let result = Command::new(pyright) + .arg("--pythonpath") + .arg(python()) + .arg(&consumer) + .current_dir(&fixture.0) + .output() + .unwrap(); + let text = diagnostics(&result); + assert_eq!( + text.lines() + .filter(|line| line.contains(" - error: ")) + .count(), + expected_errors, + "{text}" + ); + assert_eq!(result.status.success(), expected_errors == 0, "{text}"); + } + } + } +} diff --git a/tools/dynwinrt-codegen/tests/fixtures/delegate_root_native.mjs b/tools/dynwinrt-codegen/tests/fixtures/delegate_root_native.mjs new file mode 100644 index 00000000..30fbd956 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/fixtures/delegate_root_native.mjs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +const require = createRequire(import.meta.url); +const [runtimePath, generatedPath, moduleKind, expectedFailure] = + process.argv.slice(2); +const runtime = require(runtimePath); +runtime.roInitialize(1); + +const generated = + moduleKind === "esm" + ? await import(pathToFileURL(path.join(generatedPath, "index.mjs")).href) + : require(generatedPath); +const scope = generated.createProjectedLifetimeScope(); +let callbacks = 0; +let callbackError; +let failure; +const timeout = setTimeout(() => { + console.error("Timed out waiting for the generated ThreadPool callback"); + process.exit(2); +}, 10_000); + +try { + await generated.ThreadPool.runAsync((operation) => { + try { + assert.ok(operation instanceof runtime.DynWinRtValue); + assert.equal(operation.isNull(), false); + callbacks += 1; + } catch (error) { + callbackError = error; + } finally { + operation.release(); + } + }); + assert.ifError(callbackError); + assert.equal(callbacks, 1); +} catch (error) { + failure = error; +} finally { + clearTimeout(timeout); + scope.dispose(); +} + +// Generated callback wrappers are GC-owned; release them before the child exits. +await new Promise((resolve) => setImmediate(resolve)); +global.gc(); +await new Promise((resolve) => setImmediate(resolve)); + +if (expectedFailure === "expect-selection-failure") { + assert.equal(callbacks, 0); + assert.match(String(failure), /Failed to recover `?WinGUID`?/); +} else { + assert.ifError(failure); +} +console.log( + JSON.stringify({ + language: "js", + arch: process.arch, + module: moduleKind, + callbacks, + error: failure ? String(failure) : null, + }), +); diff --git a/tools/dynwinrt-codegen/tests/fixtures/delegate_root_native.py b/tools/dynwinrt-codegen/tests/fixtures/delegate_root_native.py new file mode 100644 index 00000000..41d3e4ca --- /dev/null +++ b/tools/dynwinrt-codegen/tests/fixtures/delegate_root_native.py @@ -0,0 +1,55 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import asyncio +import importlib +import json +import platform +import sys +import sysconfig +from pathlib import Path + +from dynwinrt import DynWinRTValue, RoApartment, projected_lifetime_scope + +generated = Path(sys.argv[1]).resolve() +sys.path.insert(0, str(generated.parent)) +package = importlib.import_module(generated.name) +callbacks = 0 +failure = None + + +def work_item(operation: DynWinRTValue) -> None: + global callbacks + try: + assert isinstance(operation, DynWinRTValue) + assert not operation.is_null() + callbacks += 1 + finally: + operation.release() + + +async def run() -> None: + await asyncio.wait_for(package.ThreadPool.run_async(work_item), timeout=10) + + +with RoApartment(), projected_lifetime_scope(): + try: + asyncio.run(run()) + assert callbacks == 1 + except Exception as error: + failure = error + +if len(sys.argv) > 2 and sys.argv[2] == "expect-selection-failure": + assert callbacks == 0 + assert isinstance(failure, TypeError), repr(failure) + assert "DynWinRTValue" in str(failure), str(failure) +else: + assert failure is None, repr(failure) + +print(json.dumps({ + "language": "py", + "arch": sysconfig.get_platform(), + "host": platform.machine(), + "callbacks": callbacks, + "error": None if failure is None else str(failure), +})) From 8949875953907ee413e51c52e4bead2d8a64233e Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 1 Oct 2026 21:08:03 +0800 Subject: [PATCH 2/2] Keep test-hook stubs compatible with current rustfmt Accept optional trailing commas and format the two existing macro calls required by hosted Rust 1.99. Generated native signatures and behavior are unchanged. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- bindings/js/src/com_completion_test_hooks.rs | 21 +++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/bindings/js/src/com_completion_test_hooks.rs b/bindings/js/src/com_completion_test_hooks.rs index a1285d2b..732bb670 100644 --- a/bindings/js/src/com_completion_test_hooks.rs +++ b/bindings/js/src/com_completion_test_hooks.rs @@ -652,15 +652,30 @@ unsafe extern "system" fn result_u32(this: *mut c_void, output: *mut u32) -> HRE } macro_rules! stub { - ($name:ident($($arg:ident: $typ:ty),*)) => { + ($name:ident($($arg:ident: $typ:ty),* $(,)?)) => { unsafe extern "system" fn $name(_this: *mut c_void, $($arg: $typ),*) -> HRESULT { E_NOTIMPL } }; } -stub!(audio_initialize(_mode: AUDCLNT_SHAREMODE, _flags: u32, _duration: i64, _period: i64, _format: *const WAVEFORMATEX, _session: *const GUID)); +stub!( + audio_initialize( + _mode: AUDCLNT_SHAREMODE, + _flags: u32, + _duration: i64, + _period: i64, + _format: *const WAVEFORMATEX, + _session: *const GUID, + ) +); stub!(audio_i64(_value: *mut i64)); -stub!(audio_format_supported(_mode: AUDCLNT_SHAREMODE, _format: *const WAVEFORMATEX, _closest: *mut *mut WAVEFORMATEX)); +stub!( + audio_format_supported( + _mode: AUDCLNT_SHAREMODE, + _format: *const WAVEFORMATEX, + _closest: *mut *mut WAVEFORMATEX, + ) +); stub!(audio_mix_format(_format: *mut *mut WAVEFORMATEX)); stub!(audio_period(_default: *mut i64, _minimum: *mut i64)); stub!(audio_noargs());