diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4f1038a..4b4e9abd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -543,12 +543,15 @@ jobs: .\eng\release\python\test_generated_python_long_paths.ps1 ` -Python .\bindings\py\.venv\Scripts\python.exe ` -Codegen $env:DYNWINRT_CODEGEN - - name: Test generated implementation symbol identities + - name: Test generated implementation and overload identities shell: pwsh run: | $env:DYNWINRT_TEST_PYTHON = (Resolve-Path .\bindings\py\.venv\Scripts\python.exe).Path $env:DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME = '1' - cargo test -p dynwinrt-codegen --test implementation_naming_test --test python_released_implementation_test + cargo test -p dynwinrt-codegen ` + --test implementation_naming_test ` + --test python_overload_legacy_compat_test ` + --test python_released_implementation_test - name: Run E2E tests run: .\tests\e2e\e2e_test.ps1 -SkipBuild -Codegen $env:DYNWINRT_CODEGEN # This optional-SDK behavioral smoke is separate from generated coverage diff --git a/bindings/py/README.md b/bindings/py/README.md index f44119a8..cc2ca69c 100644 --- a/bindings/py/README.md +++ b/bindings/py/README.md @@ -15,6 +15,9 @@ dynwinrt-codegen generate --namespace Windows.Foundation --class-name Uri ` Generated package manifests pin `dynwinrt` to the exact version of `dynwinrt-codegen` that produced them. The runtime wheel includes `__init__.pyi` and `py.typed` for static type checking. +Do not mix generated bindings with an older runtime wheel. Upgrade to the +matching runtime version and regenerate all Python bindings together; generated +interface overloads fail explicitly when a required native guard is missing. Generated `IReference` values are projected as `T | None`; native values, `None`, and generated `IReference_*` wrappers are accepted as inputs. @@ -170,8 +173,26 @@ def work(action: DynWinRTValue) -> None: operation = ThreadPool.run_async(work) ``` -WinRT flags enums are projected as `enum.IntFlag`. Overloaded methods share one +`ThreadPool.run_async(handler)` intentionally retains its original single +argument and exact callback annotation, so mypy can infer the type of an +unannotated callback lambda. Its priority and options overloads remain +separate as `run_with_priority_async(handler, priority)` and +`run_with_priority_and_options_async(handler, priority, options)`. These are +distinct names in both generated Python and `.pyi`; passing priority or options +to `run_async` is not supported. + +WinRT flags enums are projected as `enum.IntFlag`. Most overloaded methods share one Python name with runtime type/arity dispatch and `typing.overload` declarations. +That name is the documented (CLR) method name, so `StorageFile.CopyAsync` +overloads are all `copy_async(...)`. The unique `[Overload]` ABI names emitted by +earlier releases, such as `copy_overload` or `launch_file_with_options_async`, +remain available as compatibility aliases that keep calling the same overload. +A method keeps its earlier name when the documented name would clash with +another member, such as a property or the generated `close()`. Interface +parameters of overloads accept any object that implements the interface, such +as a runtime class instance or a `DynWinRTValue`, including a raw async +operation that implements `IAsyncInfo`. Native QueryInterface failures other +than `E_NOINTERFACE` propagate instead of silently choosing another overload. Activatable runtime classes use normal constructors, for example `Uri("https://example.com")`. Constructor overloads come only from WinMD `ActivatableAttribute` and public `ComposableAttribute` declarations. Classes diff --git a/bindings/py/dynwinrt.pyi b/bindings/py/dynwinrt.pyi index 2d77c677..ce6ef496 100644 --- a/bindings/py/dynwinrt.pyi +++ b/bindings/py/dynwinrt.pyi @@ -466,6 +466,7 @@ class DynWinRTValue: def to_guid(self) -> WinGUID: ... def to_bytes(self) -> bytes: ... def is_null(self) -> bool: ... + def _try_query_interface(self, iid: WinGUID) -> bool: ... # True after release(), release_projected(), or a closing # projected_lifetime_scope(); a WinRT null reference is not released. def is_released(self) -> bool: ... diff --git a/bindings/py/src/runtime.rs b/bindings/py/src/runtime.rs index 618b7b5e..7279f54e 100644 --- a/bindings/py/src/runtime.rs +++ b/bindings/py/src/runtime.rs @@ -1721,6 +1721,29 @@ impl DynWinRTValue { self.0.is_null_object() } + /// Guard-only QueryInterface probe; never treats a native failure as a non-match. + fn _try_query_interface(&self, iid: &WinGUID) -> PyResult { + self.ensure_live()?; + if !matches!( + &self.0, + dynwinrt::WinRTValue::Object(_) | dynwinrt::WinRTValue::Async(_) + ) { + return Ok(false); + } + match self.0.cast(&iid.0) { + Ok(interface) => { + drop(interface); + Ok(true) + } + Err(dynwinrt::Error::WindowsError(error)) + if error.code() == windows::Win32::Foundation::E_NOINTERFACE => + { + Ok(false) + } + Err(error) => Err(map_dynwinrt_error(error)), + } + } + /// Whether `release()` has run on this value, directly or through /// `release_projected()` or a closing `projected_lifetime_scope()`. /// @@ -2675,6 +2698,84 @@ pub fn get_computer_name() -> PyResult { mod tests { use super::*; use pyo3::types::PyDict; + use std::ffi::c_void; + use std::sync::atomic::{AtomicU32, Ordering}; + + #[derive(Default)] + struct QueryCounts { + queries: AtomicU32, + addrefs: AtomicU32, + releases: AtomicU32, + } + + #[repr(C)] + struct QueryProbe { + vtable: *const windows::core::IUnknown_Vtbl, + references: AtomicU32, + counts: Arc, + } + + impl QueryProbe { + const SUPPORTED: GUID = IUnknown::IID; + const FAILURE: GUID = GUID::from_u128(0x12113896_999b_42d5_87c1_7c68e83592eb); + const UNKNOWN: GUID = GUID::from_u128(0x6192657c_dbc2_4262_98c8_8ead575ac434); + const VTABLE: windows::core::IUnknown_Vtbl = windows::core::IUnknown_Vtbl { + QueryInterface: Self::query, + AddRef: Self::add_ref, + Release: Self::release, + }; + + fn new() -> (IUnknown, Arc) { + let counts = Arc::new(QueryCounts::default()); + let object = Box::new(Self { + vtable: &Self::VTABLE, + references: AtomicU32::new(1), + counts: counts.clone(), + }); + ( + unsafe { IUnknown::from_raw(Box::into_raw(object).cast()) }, + counts, + ) + } + + unsafe extern "system" fn query( + this: *mut c_void, + iid: *const GUID, + result: *mut *mut c_void, + ) -> windows::core::HRESULT { + if iid.is_null() || result.is_null() { + return windows::core::HRESULT(0x80004003u32 as i32); + } + let object = unsafe { &*this.cast::() }; + object.counts.queries.fetch_add(1, Ordering::SeqCst); + unsafe { *result = std::ptr::null_mut() }; + match unsafe { *iid } { + Self::SUPPORTED => { + unsafe { *result = this }; + unsafe { Self::add_ref(this) }; + windows::core::HRESULT(0) + } + Self::FAILURE => windows::core::HRESULT(0x80004005u32 as i32), + _ => windows::Win32::Foundation::E_NOINTERFACE, + } + } + + unsafe extern "system" fn add_ref(this: *mut c_void) -> u32 { + let object = unsafe { &*this.cast::() }; + object.counts.addrefs.fetch_add(1, Ordering::SeqCst); + object.references.fetch_add(1, Ordering::SeqCst) + 1 + } + + unsafe extern "system" fn release(this: *mut c_void) -> u32 { + let object = unsafe { &*this.cast::() }; + object.counts.releases.fetch_add(1, Ordering::SeqCst); + let remaining = object.references.fetch_sub(1, Ordering::SeqCst) - 1; + if remaining == 0 { + unsafe { drop(Box::from_raw(this.cast::())) }; + } + remaining + } + } #[repr(C)] struct TestDelegateVtbl { @@ -2706,6 +2807,65 @@ mod tests { assert_eq!(array.to_i32_list().unwrap(), vec![0, 0x80004005u32 as i32]); } + #[test] + fn private_query_guard_releases_successful_qi_and_preserves_other_failures() { + Python::initialize(); + Python::attach(|py| { + let (object, counts) = QueryProbe::new(); + let mut value = DynWinRTValue::new(dynwinrt::WinRTValue::Object(object)); + assert!( + value + ._try_query_interface(&WinGUID(QueryProbe::SUPPORTED)) + .unwrap() + ); + assert_eq!(counts.addrefs.load(Ordering::SeqCst), 1); + assert_eq!(counts.releases.load(Ordering::SeqCst), 1); + assert!( + !value + ._try_query_interface(&WinGUID(QueryProbe::UNKNOWN)) + .unwrap() + ); + let error = value + ._try_query_interface(&WinGUID(QueryProbe::FAILURE)) + .unwrap_err(); + assert!(error.is_instance_of::(py)); + assert_eq!( + error + .value(py) + .getattr("winerror") + .unwrap() + .extract::() + .unwrap(), + 0x80004005u32 as i32 + ); + assert_eq!(counts.queries.load(Ordering::SeqCst), 3); + assert_eq!(counts.addrefs.load(Ordering::SeqCst), 1); + assert_eq!(counts.releases.load(Ordering::SeqCst), 1); + + for payload in [ + dynwinrt::WinRTValue::I32(5), + dynwinrt::WinRTValue::HString("scalar".into()), + dynwinrt::WinRTValue::Null, + dynwinrt::WinRTValue::RawPtr(std::ptr::null_mut()), + ] { + assert!( + !DynWinRTValue::new(payload) + ._try_query_interface(&WinGUID(QueryProbe::SUPPORTED)) + .unwrap() + ); + } + assert_eq!(counts.queries.load(Ordering::SeqCst), 3); + + value.release(); + assert_eq!(counts.releases.load(Ordering::SeqCst), 2); + let released = value + ._try_query_interface(&WinGUID(QueryProbe::SUPPORTED)) + .unwrap_err(); + assert!(released.is_instance_of::(py)); + assert!(released.to_string().contains("released")); + }); + } + #[test] fn detached_invocation_releases_the_gil_on_the_same_native_thread() { use dynwinrt::{ diff --git a/eng/ci/test_ci.py b/eng/ci/test_ci.py index e0eeada5..df818269 100644 --- a/eng/ci/test_ci.py +++ b/eng/ci/test_ci.py @@ -241,7 +241,10 @@ def test_current_run_artifacts_and_no_consumer_rebuilds(self): "npm run test:borrowed-copy"): self.assertNotIn(forbidden, commands) runtime = "\n".join(step.get("run", "") for step in JOBS["e2e-runtime"]["steps"]) - self.assertIn("cargo test -p dynwinrt-codegen --test implementation_naming_test", runtime) + self.assertIn("cargo test -p dynwinrt-codegen `", runtime) + self.assertIn("--test implementation_naming_test", runtime) + self.assertIn("--test python_overload_legacy_compat_test", runtime) + self.assertIn("--test python_released_implementation_test", runtime) def test_release_notes_validated_in_lightweight_lane(self): steps = [ diff --git a/tests/e2e/e2e_specs.json b/tests/e2e/e2e_specs.json index a9b29a7d..7e850a9e 100644 --- a/tests/e2e/e2e_specs.json +++ b/tests/e2e/e2e_specs.json @@ -154,7 +154,8 @@ "args": ["en-US"] }, "checks": [ - { "kind": "property_equals", "member": "language_tag", "expected": "en-US" } + { "kind": "property_equals", "member": "language_tag", "expected": "en-US" }, + { "kind": "language_comprehensive", "member": "self", "langs": ["py"] } ] }, { @@ -337,7 +338,8 @@ "checks": [ { "kind": "sequence_protocol", "member": "languages", "expected_size": 1 }, { "kind": "datetime_roundtrip", "member": "date_time", "langs": ["py"] }, - { "kind": "calendar_comprehensive", "member": "self" } + { "kind": "calendar_comprehensive", "member": "self" }, + { "kind": "calendar_documented_overloads", "member": "month_as_string", "langs": ["py"] } ] }, { @@ -396,6 +398,16 @@ { "kind": "work_item_callback_passthrough", "member": "run_async" } ] }, + { + "id": "preallocated_work_item_delegate_constructor", + "namespace": "Windows.System.Threading.Core", + "class": "PreallocatedWorkItem", + "langs": ["py"], + "instantiate": { "kind": "none" }, + "checks": [ + { "kind": "preallocated_work_item_callback", "member": "run_async" } + ] + }, { "id": "thread_pool_timer_callback_projection", "namespace": "Windows.System.Threading", @@ -505,7 +517,9 @@ "instantiate": { "kind": "none" }, "checks": [ { "kind": "async_memory_roundtrip", "member": "roundtrip", "write_value": 42 }, - { "kind": "data_stream_scalar_roundtrip", "member": "roundtrip", "langs": ["py"] } + { "kind": "data_stream_scalar_roundtrip", "member": "roundtrip", "langs": ["py"] }, + { "kind": "data_stream_constructor_roundtrip", "member": "__init__", "langs": ["py"] }, + { "kind": "can_cast_non_object", "member": "_dynwinrt_can_cast", "langs": ["py"] } ] }, { @@ -528,6 +542,37 @@ { "kind": "storage_query_temp_folder", "member": "query" } ] }, + { + "id": "python_storage_file_copy_overloads", + "namespace": "Windows.Storage", + "class": "StorageFile", + "langs": ["py"], + "instantiate": { "kind": "none" }, + "checks": [ + { "kind": "storage_file_copy_overloads", "member": "copy_async" } + ] + }, + { + "id": "python_stream_copy_overloads", + "namespace": "Windows.Storage.Streams", + "class": "RandomAccessStream", + "langs": ["py"], + "extra_classes": ["InMemoryRandomAccessStream", "DataWriter", "DataReader"], + "instantiate": { "kind": "none" }, + "checks": [ + { "kind": "random_access_stream_copy_overloads", "member": "copy_async" } + ] + }, + { + "id": "python_decimal_formatter_overloads", + "namespace": "Windows.Globalization.NumberFormatting", + "class": "DecimalFormatter", + "langs": ["py"], + "instantiate": { "kind": "none" }, + "checks": [ + { "kind": "decimal_formatter_overloads", "member": "format" } + ] + }, { "id": "issue_69_device_information_async_collection", "namespace": "Windows.Devices.Enumeration", @@ -646,7 +691,8 @@ "args": [""], "property_path": ["document_element", "tag_name"], "expected": "root" - } + }, + { "kind": "xml_document_load_overloads", "member": "load_xml", "langs": ["py"] } ] }, { diff --git a/tests/e2e/e2e_specs.schema.json b/tests/e2e/e2e_specs.schema.json index 2edceae8..8e484996 100644 --- a/tests/e2e/e2e_specs.schema.json +++ b/tests/e2e/e2e_specs.schema.json @@ -57,6 +57,7 @@ "kind": { "enum": [ "property_equals", + "language_comprehensive", "property_exists", "property_in_range", "method_equals", @@ -99,11 +100,19 @@ "constructor_overload_dispatch", "value_set_mapping", "data_stream_scalar_roundtrip", + "data_stream_constructor_roundtrip", + "can_cast_non_object", "calendar_comprehensive", "storage_query_temp_folder", + "storage_file_copy_overloads", + "random_access_stream_copy_overloads", + "decimal_formatter_overloads", + "xml_document_load_overloads", + "calendar_documented_overloads", "value_set_event_lifecycle", "map_changed_event_projection", "work_item_callback_passthrough", + "preallocated_work_item_callback", "timer_callback_projection", "static_event_native_delegate_passthrough", "nested_struct_runtime", diff --git a/tests/e2e/runners/py_runner.py b/tests/e2e/runners/py_runner.py index 177e4cf6..a761965b 100644 --- a/tests/e2e/runners/py_runner.py +++ b/tests/e2e/runners/py_runner.py @@ -196,6 +196,65 @@ async def run_check( else: cr['pass'] = True + elif kind == 'language_comprehensive': + properties = ( + obj.language_tag, + obj.display_name, + obj.native_name, + obj.script, + obj.layout_direction, + obj.abbreviated_name, + ) + extension_subtags = obj.get_extension_subtags('u') + mui_languages = cls.get_mui_compatible_language_list_from_language_tags( + ['en-US', 'fr-FR'] + ) + static_values = ( + cls.is_well_formed('en-US'), + cls.is_well_formed('not a language tag!'), + cls.get_current_input_method_language_tag(), + ) + language_module = importlib.import_module( + implementation_module_name( + pkg_name, 'Windows.Globalization', 'Language' + ) + ) + projections = [ + obj.as_interface(language_module.ILanguageExtensionSubtags), + obj.as_interface(language_module.ILanguage2), + obj.as_interface(language_module.ILanguage3), + ] + try: + projected_values = ( + projections[0].get_extension_subtags('u'), + projections[1].layout_direction, + projections[2].abbreviated_name, + ) + finally: + for projection in projections: + dw.release_projected(projection) + if mui_languages is not None: + mui_values = list(mui_languages) + dw.release_projected(mui_languages) + else: + mui_values = None + if ( + not all(value is not None for value in properties) + or extension_subtags is None + or static_values[0] is not True + or static_values[1] is not False + or not isinstance(static_values[2], str) + or mui_values is None + or projected_values[0] is None + ): + cr['error'] = ( + f'Language API coverage failed: properties={properties!r}, ' + f'subtags={extension_subtags!r}, static={static_values!r}, ' + f'mui={mui_values!r}, projected={projected_values!r}' + ) + else: + cr['pass'] = True + elif kind == 'ibuffer_copied_roundtrip': empty = cls.from_bytes(b'') if empty.capacity != 0 or empty.length != 0 or empty.to_bytes() != b'': @@ -1188,21 +1247,84 @@ def retain_first(sender, args): dw.release_projected(retained_args) dw.release_projected(retained_sender) del obj[key] + elif check['expected_type'] == 'IObservableMap_String_Object': + null_events = [] + + def observe_null(sender, args): + if args.collection_change == change_type.ItemInserted: + null_events.append((sender, args, sender['null-element'])) + + null_token = obj.on_map_changed(observe_null) + try: + obj['null-element'] = dw.DynWinRTValue.null_value() + finally: + obj.off_map_changed(null_token) + if ( + len(null_events) != 1 + or not isinstance(null_events[0][0], sender_type) + or not isinstance(null_events[0][1], args_type) + or null_events[0][1].key != 'null-element' + or null_events[0][2] is not None + ): + cr['error'] = ( + 'nullable map element was not visible through the ' + f'projected callback sender: {null_events!r}' + ) + return cr + dw.release_projected(null_events[0][1]) + dw.release_projected(null_events[0][0]) + del obj['null-element'] cr['pass'] = True elif kind == 'work_item_callback_passthrough': + run_async = getattr(cls, member) + priority_type = generated_type(pkg_name, 'WorkItemPriority') + options_type = generated_type(pkg_name, 'WorkItemOptions') + priority = priority_type.Normal + options = options_type.TimeSliced + with_priority = cls.run_with_priority_async + with_options = cls.run_with_priority_and_options_async received = [] - await getattr(cls, member)(received.append) - if len(received) != 1: + await run_async(received.append) + await with_priority(received.append, priority) + await with_options(received.append, priority, options) + if len(received) != 3: cr['error'] = f'work item ran {len(received)} times' return cr - if not isinstance(received[0], dw.DynWinRTValue): + if not all(isinstance(value, dw.DynWinRTValue) for value in received): cr['error'] = ( - 'work item callable did not receive the raw IAsyncAction: ' - f'{type(received[0]).__name__}' + 'work item callables did not receive raw IAsyncAction values: ' + f'{received!r}' ) return cr + invalid_calls = [ + ((received.append, priority), {}), + ((received.append, priority, options), {}), + ((received.append,), {'priority': priority}), + ((received.append,), {'priority': priority, 'options': options}), + ((), {'handler': received.append, 'priority': priority}), + ( + (), + { + 'handler': received.append, + 'priority': priority, + 'options': options, + }, + ), + ] + for args, kwargs in invalid_calls: + try: + run_async(*args, **kwargs) + except TypeError: + pass + else: + cr['error'] = ( + 'run_async accepted a priority/options call shape: ' + f'args={args!r}, kwargs={kwargs!r}' + ) + return cr + threading_namespace = importlib.import_module( namespace_module_name(pkg_name, 'Windows.System.Threading') ) @@ -1212,11 +1334,15 @@ def retain_first(sender, args): threading_namespace.WorkItemHandler_PARAM_TYPES, native_received.append, ) - await getattr(cls, member)(delegate) + await run_async(delegate) + await with_priority(delegate, priority) + await with_options(delegate, priority, options) raw_value = delegate.to_value() - await getattr(cls, member)(raw_value) + await run_async(raw_value) + await with_priority(raw_value, priority) + await with_options(raw_value, priority, options) raw_value.release() - if len(native_received) != 2 or not all( + if len(native_received) != 6 or not all( isinstance(value, dw.DynWinRTValue) for value in native_received ): @@ -1227,6 +1353,44 @@ def retain_first(sender, args): else: cr['pass'] = True + elif kind == 'preallocated_work_item_callback': + threading_namespace = importlib.import_module( + namespace_module_name(pkg_name, 'Windows.System.Threading') + ) + with dw.projected_lifetime_scope(): + received = [] + work = cls(received.append) + await getattr(work, member)() + native_received = [] + delegate = dw.DynWinRtDelegate.create( + threading_namespace.IID_WorkItemHandler, + threading_namespace.WorkItemHandler_PARAM_TYPES, + native_received.append, + ) + native_work = cls(delegate) + await getattr(native_work, member)() + if ( + len(received) != 1 + or not isinstance(received[0], dw.DynWinRTValue) + or len(native_received) != 1 + or not isinstance(native_received[0], dw.DynWinRTValue) + ): + cr['error'] = ( + 'constructor delegate did not receive raw async values: ' + f'callable={received!r}, native={native_received!r}' + ) + return cr + try: + cls(42) + except TypeError as error: + if 'No matching constructor for PreallocatedWorkItem' not in str(error): + cr['error'] = f'unexpected constructor error: {error}' + return cr + else: + cr['error'] = 'invalid constructor delegate was accepted' + return cr + cr['pass'] = True + elif kind == 'static_event_native_delegate_passthrough': gaming_namespace = importlib.import_module( namespace_module_name(pkg_name, 'Windows.Gaming.Input') @@ -1340,6 +1504,36 @@ def elapsed(timer): return cr except IndexError: pass + view_mode = obj.view_mode + settings_identifier = obj.settings_identifier + suggested_start_location = obj.suggested_start_location + commit_button_text = obj.commit_button_text + obj.view_mode = view_mode + obj.settings_identifier = 'dynwinrt-e2e' + obj.settings_identifier = settings_identifier + obj.suggested_start_location = suggested_start_location + obj.commit_button_text = 'Open' + obj.commit_button_text = commit_button_text + picker_module = importlib.import_module( + implementation_module_name( + pkg_name, + 'Windows.Storage.Pickers', + 'FileOpenPicker', + ) + ) + picker_interfaces = [ + obj.as_interface(picker_module.IFileOpenPicker2), + obj.as_interface( + picker_module.IFileOpenPickerWithOperationId + ), + obj.as_interface(picker_module.IFileOpenPicker3), + ] + try: + _ = picker_interfaces[0].continuation_data + _ = picker_interfaces[2].user + finally: + for projected in picker_interfaces: + dw.release_projected(projected) cr['pass'] = True elif kind == 'datetime_roundtrip': @@ -1731,6 +1925,180 @@ def progress_without_loop(): else: cr['pass'] = True + elif kind == 'data_stream_constructor_roundtrip': + writer_cls = generated_type(pkg_name, 'DataWriter') + reader_cls = generated_type(pkg_name, 'DataReader') + stream = cls() + # The factories take IOutputStream/IInputStream; runtime-class streams + # must dispatch through QueryInterface instead of raising TypeError. + writer = writer_cls(stream) + writer.write_int32(-7) + writer.write_string('dynwinrt') + stored = await writer.store_async() + keyword_writer = writer_cls(output_stream=cls()) + keyword_writer.write_byte(1) + keyword_stored = await keyword_writer.store_async() + + reader = reader_cls(stream.get_input_stream_at(0)) + loaded = await reader.load_async(stored) + values = (reader.read_int32(), reader.read_string(8)) + stream.seek(0) + runtime_class_reader = reader_cls(stream) + runtime_class_loaded = await runtime_class_reader.load_async(stored) + runtime_class_values = ( + runtime_class_reader.read_int32(), + runtime_class_reader.read_string(8), + ) + + rejected = [] + for invalid in ((42,), ('stream',), (object(),)): + try: + writer_cls(*invalid) + rejected.append(f'accepted {invalid!r}') + except TypeError as error: + if 'No matching constructor for DataWriter' not in str(error): + rejected.append(f'unexpected error {error!r}') + if ( + stored != 12 + or keyword_stored != 1 + or loaded != stored + or runtime_class_loaded != stored + or values != (-7, 'dynwinrt') + or runtime_class_values != values + or rejected + ): + cr['error'] = ( + 'runtime-class stream constructor dispatch failed: ' + f'stored={stored}, keyword_stored={keyword_stored}, loaded={loaded}, ' + f'runtime_class_loaded={runtime_class_loaded}, values={values!r}, ' + f'runtime_class_values={runtime_class_values!r}, rejected={rejected!r}' + ) + else: + cr['pass'] = True + + elif kind == 'can_cast_non_object': + runtime = importlib.import_module(f'{pkg_name}._runtime') + iid = dw.WinGUID.parse('00000000-0000-0000-c000-000000000046') + values = [ + dw.DynWinRTValue.null_value(), + dw.DynWinRTValue.from_i32(1), + dw.DynWinRTValue.from_hstring('not an object'), + object(), + ] + results = [runtime._dynwinrt_can_cast(value, iid) for value in values] + legacy = runtime._dynwinrt_legacy_call( + lambda first, second: (first, second), + ('first', 'second'), + (1,), + {'second': 2}, + 'example', + ) + legacy_error = None + try: + runtime._dynwinrt_legacy_call( + lambda value: value, + ('value',), + (), + {}, + 'example', + ) + except TypeError as error: + legacy_error = str(error) + int_guards = [ + runtime._dynwinrt_legacy_int_guard('42'), + runtime._dynwinrt_legacy_int_guard('not numeric'), + runtime._dynwinrt_legacy_int_guard('42', 0, 100), + runtime._dynwinrt_legacy_int_guard('42', 43, 100), + runtime._dynwinrt_legacy_int_guard(float('inf')), + ] + int_error = None + class UnexpectedIntError: + def __int__(self): + raise RuntimeError('unexpected-int-error') + try: + runtime._dynwinrt_legacy_int_guard(UnexpectedIntError()) + except RuntimeError as error: + int_error = str(error) + released = dw.DynWinRTValue.from_i32(1) + released.release() + released_error = None + try: + runtime._dynwinrt_can_cast(released, iid) + except RuntimeError as error: + released_error = str(error) + unsupported_iid = dw.WinGUID.parse( + '11111111-1111-1111-1111-111111111111' + ) + qi_subject = dw.DynWinRTValue.activation_factory( + 'Windows.Foundation.Uri' + ) + unsupported_error = None + try: + unexpected_projection = qi_subject.cast(unsupported_iid) + except OSError as error: + unsupported_error = error.winerror + else: + unexpected_projection.release() + unsupported = runtime._dynwinrt_can_cast(qi_subject, unsupported_iid) + qi_subject.release() + + controlled_error = OSError( + None, + 'controlled QI failure', + None, + -2147467259, + ) + + class FailingValue: + def _try_query_interface(self, _iid): + raise controlled_error + + original_value_type = runtime.DynWinRTValue + qi_error = None + mismatch_error = None + try: + runtime.DynWinRTValue = FailingValue + runtime._dynwinrt_can_cast(FailingValue(), iid) + except OSError as error: + qi_error = error + finally: + runtime.DynWinRTValue = original_value_type + class MissingValue: + pass + runtime.DynWinRTValue = MissingValue + try: + runtime._dynwinrt_can_cast(MissingValue(), iid) + except RuntimeError as error: + mismatch_error = str(error) + finally: + runtime.DynWinRTValue = original_value_type + if ( + results != [False, False, False, False] + or legacy != (1, 2) + or legacy_error != 'No matching overload for example' + or int_guards != [True, False, True, False, False] + or int_error != 'unexpected-int-error' + or released_error is None + or 'released' not in released_error + or unsupported_error != -2147467262 + or unsupported + or qi_error is not controlled_error + or mismatch_error is None + or 'regenerate all Python bindings' not in mismatch_error + ): + cr['error'] = ( + 'runtime dispatch helpers failed: ' + f'casts={results!r}, legacy={legacy!r}, error={legacy_error!r}, ' + f'int_guards={int_guards!r}, int_error={int_error!r}, ' + f'released_error={released_error!r}, ' + f'unsupported_error={unsupported_error!r}, ' + f'unsupported={unsupported!r}, qi_error=' + f'{getattr(qi_error, "winerror", None)!r}, ' + f'mismatch_error={mismatch_error!r}' + ) + else: + cr['pass'] = True + elif kind == 'calendar_comprehensive': obj.year = 2024 obj.month = 1 @@ -1819,26 +2187,59 @@ def progress_without_loop(): obj.change_clock(clock) obj.change_time_zone(time_zone) obj.numeral_system = numeral_system + constructed = [ + cls(), + cls(['en-US']), + cls(['en-US'], calendar_system, clock), + cls(['en-US'], calendar_system, clock, time_zone), + ] + try: + constructor_values = [ + ( + value.get_calendar_system(), + value.get_clock(), + value.get_time_zone(), + ) + for value in constructed + ] + finally: + for value in constructed: + dw.release_projected(value) + if any( + not all(isinstance(item, str) for item in values) + for values in constructor_values + ): + cr['error'] = ( + f'Calendar constructors returned invalid values: ' + f'{constructor_values!r}' + ) + return cr string_calls = [ ('era_as_full_string', ()), + ('era_as_string', ()), ('era_as_string', (3,)), ('year_as_string', ()), ('year_as_truncated_string', (2,)), ('year_as_padded_string', (4,)), ('month_as_full_string', ()), + ('month_as_string', ()), ('month_as_string', (3,)), ('month_as_full_solo_string', ()), + ('month_as_solo_string', ()), ('month_as_solo_string', (3,)), ('month_as_numeric_string', ()), ('month_as_padded_numeric_string', (2,)), ('day_as_string', ()), ('day_as_padded_string', (2,)), ('day_of_week_as_full_string', ()), + ('day_of_week_as_string', ()), ('day_of_week_as_string', (3,)), ('day_of_week_as_full_solo_string', ()), + ('day_of_week_as_solo_string', ()), ('day_of_week_as_solo_string', (3,)), ('period_as_full_string', ()), + ('period_as_string', ()), ('period_as_string', (2,)), ('hour_as_string', ()), ('hour_as_padded_string', (2,)), @@ -1849,6 +2250,7 @@ def progress_without_loop(): ('nanosecond_as_string', ()), ('nanosecond_as_padded_string', (3,)), ('time_zone_as_full_string', ()), + ('time_zone_as_string', ()), ('time_zone_as_string', (3,)), ] formatted = [ @@ -1892,13 +2294,38 @@ def progress_without_loop(): direct_files = await ( folder.get_files_async_overload_default_options_start_and_count() ) + common_file_query = generated_type(pkg_name, 'CommonFileQuery').DefaultQuery + common_folder_query = generated_type(pkg_name, 'CommonFolderQuery').DefaultQuery + canonical_files = [ + await folder.get_files_async(), + await folder.get_files_async(common_file_query), + await folder.get_files_async(common_file_query, 0, 10), + ] + canonical_folders = [ + await folder.get_folders_async(), + await folder.get_folders_async(common_folder_query), + await folder.get_folders_async(common_folder_query, 0, 10), + ] + canonical_items = [ + await folder.get_items_async(), + await folder.get_items_async(0, 10), + ] query = folder.create_file_query_overload_default() if query is None: cr['error'] = 'StorageFolder.create_file_query returned null' return cr count = await query.get_item_count_async() query_files = await query.get_files_async_default_start_and_count() + canonical_query_files = [ + await query.get_files_async(), + await query.get_files_async(0, 10), + ] options = query.get_current_query_options() + canonical_queries = [ + folder.create_file_query(), + folder.create_file_query(common_file_query), + folder.create_file_query(options), + ] query_folder = query.folder missing = await folder.try_get_item_async('missing.file') alpha = await folder.get_file_async('alpha.txt') @@ -1906,11 +2333,72 @@ def progress_without_loop(): if options is not None: query.apply_new_query_options(options) + folder_interface = folder.as_interface( + generated_type(pkg_name, 'IStorageFolder') + ) + item_interface = alpha.as_interface( + generated_type(pkg_name, 'IStorageItem') + ) + creation = generated_type( + pkg_name, 'CreationCollisionOption' + ).ReplaceExisting + created_file = await folder_interface.create_file_async( + 'interface-file.txt' + ) + replaced_file = await folder_interface.create_file_async( + 'interface-file.txt', creation + ) + created_folder = await folder_interface.create_folder_async( + 'interface-folder' + ) + replaced_folder = await folder_interface.create_folder_async( + 'interface-folder', creation + ) + interface_results = [ + await folder_interface.get_file_async('alpha.txt'), + await folder_interface.get_item_async('alpha.txt'), + await folder_interface.get_files_async(), + await folder_interface.get_folders_async(), + await folder_interface.get_items_async(), + item_interface.name, + item_interface.path, + item_interface.attributes, + item_interface.date_created, + item_interface.is_of_type( + generated_type(pkg_name, 'StorageItemTypes').File + ), + await item_interface.get_basic_properties_async(), + ] + await replaced_file.delete_async() + await replaced_folder.delete_async() + dw.release_projected(folder_interface) + dw.release_projected(item_interface) + direct_names = sorted(file.name for file in direct_files or []) query_names = sorted(file.name for file in query_files or []) + canonical_file_names = [ + sorted(file.name for file in files or []) + for files in canonical_files + ] + canonical_query_names = [ + sorted(file.name for file in files or []) + for files in canonical_query_files + ] if ( direct_names != ['alpha.txt', 'beta.txt'] or query_names != direct_names + or any(names != direct_names for names in canonical_file_names) + or any(names != direct_names for names in canonical_query_names) + or any(folders for folders in canonical_folders) + or any( + sorted(item.name for item in items or []) + != ['alpha.txt', 'beta.txt'] + for items in canonical_items + ) + or any(value is None for value in canonical_queries) + or any(value is None for value in interface_results) + or created_file is None + or created_folder is None or count != 2 or query_folder is None or not query_folder.is_equal(folder) @@ -1924,11 +2412,346 @@ def progress_without_loop(): cr['error'] = ( f'Storage query failed: direct={direct_names!r}, ' f'query={query_names!r}, count={count}, ' + f'canonical_files={canonical_file_names!r}, ' + f'canonical_query={canonical_query_names!r}, ' f'missing={missing!r}, alpha={alpha!r}' ) else: cr['pass'] = True + elif kind == 'storage_file_copy_overloads': + from pathlib import Path + from tempfile import TemporaryDirectory + + folder_cls = generated_type(pkg_name, 'StorageFolder') + collision = generated_type(pkg_name, 'NameCollisionOption') + with TemporaryDirectory(prefix='dynwinrt-copy-') as temp_dir: + root = Path(temp_dir) + (root / 'source.txt').write_text('payload', encoding='utf-8') + for name in ('move1.txt', 'move2.txt', 'move3.txt'): + (root / name).write_text('move payload', encoding='utf-8') + for name in ('documented', 'legacy'): + (root / name).mkdir() + source = await cls.get_file_from_path_async(str(root / 'source.txt')) + opened = await source.open_async(0) + legacy_opened = await source.open_with_options_async(0, 0) + documented = await folder_cls.get_folder_from_path_async( + str(root / 'documented') + ) + legacy = await folder_cls.get_folder_from_path_async(str(root / 'legacy')) + + # CopyAsync overloads share the documented name; the folder argument is a + # runtime class passed to an IStorageFolder parameter. + copies = [ + await source.copy_async(documented), + await source.copy_async(documented, 'named.txt'), + await source.copy_async( + documented, 'named.txt', collision.ReplaceExisting + ), + await source.copy_async( + destination_folder=documented, + desired_new_name='keyword.txt', + ), + ] + # Former [Overload] names keep calling the same ABI overload, + # including a plain int for the NameCollisionOption parameter. + legacy_copies = [ + await source.copy_overload_default_name_and_options(legacy), + await source.copy_overload_default_options(legacy, 'named.txt'), + await source.copy_overload(legacy, 'named.txt', collision.ReplaceExisting), + await source.copy_overload(legacy, 'named.txt', 1), + ] + move1 = await cls.get_file_from_path_async(str(root / 'move1.txt')) + move2 = await cls.get_file_from_path_async(str(root / 'move2.txt')) + move3 = await cls.get_file_from_path_async(str(root / 'move3.txt')) + await move1.move_async(documented) + await move2.move_async(documented, 'moved2.txt') + await move3.move_async( + documented, 'moved3.txt', collision.ReplaceExisting + ) + rejected = [] + for arguments in (('not a folder',), (documented, 42)): + try: + await source.copy_async(*arguments) + rejected.append(f'accepted {arguments!r}') + except TypeError as error: + if 'No matching overload for copy_async' not in str(error): + rejected.append(f'unexpected error {error!r}') + + names = [copy.name for copy in copies] + legacy_names = [copy.name for copy in legacy_copies] + contents = sorted( + path.name + for path in (root / 'documented').iterdir() + if path.read_text(encoding='utf-8') == 'payload' + ) + moved = sorted( + path.name + for path in (root / 'documented').iterdir() + if path.read_text(encoding='utf-8') == 'move payload' + ) + if ( + names != ['source.txt', 'named.txt', 'named.txt', 'keyword.txt'] + or legacy_names != ['source.txt', 'named.txt', 'named.txt', 'named.txt'] + or contents != ['keyword.txt', 'named.txt', 'source.txt'] + or moved != ['move1.txt', 'moved2.txt', 'moved3.txt'] + or opened is None + or legacy_opened is None + or rejected + ): + cr['error'] = ( + f'StorageFile copy overloads failed: names={names!r}, ' + f'legacy={legacy_names!r}, contents={contents!r}, ' + f'moved={moved!r}, ' + f'opened={opened!r}, legacy_opened={legacy_opened!r}, ' + f'rejected={rejected!r}' + ) + else: + cr['pass'] = True + if opened is not None: + opened.close() + if legacy_opened is not None: + legacy_opened.close() + + elif kind == 'random_access_stream_copy_overloads': + stream_cls = generated_type(pkg_name, 'InMemoryRandomAccessStream') + writer_cls = generated_type(pkg_name, 'DataWriter') + reader_cls = generated_type(pkg_name, 'DataReader') + source = stream_cls() + writer = writer_cls(source) + writer.write_string('0123456789') + await writer.store_async() + writer.detach_stream() + + async def read_all(stream): + stream.seek(0) + reader = reader_cls(stream) + loaded = await reader.load_async(stream.size) + return reader.read_string(loaded) + + results = [] + for copy in ( + lambda destination: cls.copy_async(source, destination), + lambda destination: cls.copy_async(source, destination, 4), + lambda destination: cls.copy_size_async(source, destination, 4), + ): + source.seek(0) + destination = stream_cls() + copied = await copy(destination) + results.append((copied, await read_all(destination))) + random_access = source.as_interface( + generated_type(pkg_name, 'IRandomAccessStream') + ) + stream_views = [ + random_access.get_input_stream_at(0), + random_access.get_output_stream_at(random_access.size), + random_access.clone_stream(), + ] + stream_properties = ( + random_access.can_read, + random_access.can_write, + random_access.position, + random_access.size, + ) + random_access.seek(0) + random_access.size = random_access.size + for view in stream_views: + dw.release_projected(view) + dw.release_projected(random_access) + if results != [(10, '0123456789'), (4, '0123'), (4, '0123')]: + cr['error'] = f'RandomAccessStream copy overloads failed: {results!r}' + elif ( + stream_properties[0] is not True + or stream_properties[1] is not True + or not all(isinstance(value, int) for value in stream_properties[2:]) + ): + cr['error'] = ( + f'IRandomAccessStream properties failed: {stream_properties!r}' + ) + else: + cr['pass'] = True + + elif kind == 'decimal_formatter_overloads': + formatter = cls() + localized = cls(['en-US'], 'US') + beyond_double = 2**53 + 1 + beyond_int64 = 2**64 - 1 + pairs = { + 'int': (formatter.format(5), formatter.format_int(5)), + 'double': (formatter.format(2.5), formatter.format_double(2.5)), + 'int64 precision': ( + formatter.format(beyond_double), + formatter.format_int(beyond_double), + ), + 'uint64': ( + formatter.format(beyond_int64), + formatter.format_u_int(beyond_int64), + ), + } + property_values = { + 'languages': formatter.languages, + 'geographic_region': formatter.geographic_region, + 'integer_digits': formatter.integer_digits, + 'fraction_digits': formatter.fraction_digits, + 'is_grouped': formatter.is_grouped, + 'is_decimal_point_always_displayed': ( + formatter.is_decimal_point_always_displayed + ), + 'numeral_system': formatter.numeral_system, + 'resolved_language': formatter.resolved_language, + 'resolved_geographic_region': formatter.resolved_geographic_region, + 'significant_digits': formatter.significant_digits, + 'number_rounder': formatter.number_rounder, + 'is_zero_signed': formatter.is_zero_signed, + } + for name in ( + 'integer_digits', + 'fraction_digits', + 'is_grouped', + 'is_decimal_point_always_displayed', + 'numeral_system', + 'significant_digits', + 'is_zero_signed', + ): + setattr(formatter, name, property_values[name]) + + formatted_int = formatter.format_int(5) + formatted_uint = formatter.format_u_int(5) + formatted_double = formatter.format_double(2.5) + parsed = ( + formatter.parse_int(formatted_int), + formatter.parse_u_int(formatted_uint), + formatter.parse_double(formatted_double), + ) + + formatter_module = importlib.import_module( + implementation_module_name( + pkg_name, + 'Windows.Globalization.NumberFormatting', + 'DecimalFormatter', + ) + ) + interface_values = [] + options = formatter.as_interface( + formatter_module.INumberFormatterOptions + ) + formatter2 = formatter.as_interface( + formatter_module.INumberFormatter2 + ) + parser = formatter.as_interface(formatter_module.INumberParser) + significant = formatter.as_interface( + formatter_module.ISignificantDigitsOption + ) + rounder = formatter.as_interface( + formatter_module.INumberRounderOption + ) + signed = formatter.as_interface( + formatter_module.ISignedZeroOption + ) + projected_interfaces = [ + options, + formatter2, + parser, + significant, + rounder, + signed, + ] + try: + interface_values.extend( + [ + options.languages, + options.geographic_region, + options.integer_digits, + options.fraction_digits, + options.is_grouped, + options.is_decimal_point_always_displayed, + options.numeral_system, + options.resolved_language, + options.resolved_geographic_region, + formatter2.format_int(5), + formatter2.format_u_int(5), + formatter2.format_double(2.5), + parser.parse_int(formatted_int), + parser.parse_u_int(formatted_uint), + parser.parse_double(formatted_double), + significant.significant_digits, + rounder.number_rounder, + signed.is_zero_signed, + ] + ) + options.integer_digits = options.integer_digits + options.fraction_digits = options.fraction_digits + options.is_grouped = options.is_grouped + options.is_decimal_point_always_displayed = ( + options.is_decimal_point_always_displayed + ) + options.numeral_system = options.numeral_system + significant.significant_digits = significant.significant_digits + signed.is_zero_signed = signed.is_zero_signed + for projected in projected_interfaces: + same_interface = projected.as_interface(type(projected)) + dw.release_projected(same_interface) + finally: + for projected in projected_interfaces: + dw.release_projected(projected) + + mismatches = { + name: values for name, values in pairs.items() if values[0] != values[1] + } + if ( + mismatches + or localized.resolved_language == '' + or parsed[0] != 5 + or parsed[1] != 5 + or parsed[2] != 2.5 + or any(value is None for value in interface_values[:9]) + or formatter.format(beyond_double) + == formatter.format_double(float(beyond_double)) + ): + cr['error'] = ( + 'DecimalFormatter overloads/options failed: ' + f'pairs={pairs!r}, parsed={parsed!r}, ' + f'properties={property_values!r}, interfaces={interface_values!r}' + ) + else: + cr['pass'] = True + + elif kind == 'xml_document_load_overloads': + settings = generated_type(pkg_name, 'XmlLoadSettings')() + loaded = [] + for load in ( + lambda: obj.load_xml(''), + lambda: obj.load_xml('', settings), + lambda: obj.load_xml(xml='', load_settings=settings), + lambda: obj.load_xml_with_settings('', settings), + ): + load() + loaded.append(obj.document_element.tag_name) + if loaded != ['documented', 'settings', 'keyword', 'legacy']: + cr['error'] = f'XmlDocument.load_xml overloads failed: {loaded!r}' + else: + cr['pass'] = True + + elif kind == 'calendar_documented_overloads': + results = { + 'month': (obj.month_as_string(), obj.month_as_full_string()), + 'abbreviated month': ( + obj.month_as_string(3), + obj.month_as_string(ideal_length=3), + ), + 'day of week': ( + obj.day_of_week_as_string(), + obj.day_of_week_as_full_string(), + ), + } + if any( + left != right or not isinstance(left, str) or not left + for left, right in results.values() + ): + cr['error'] = f'Calendar documented overloads failed: {results!r}' + else: + cr['pass'] = True + elif kind == 'async_cancellation': import dynwinrt as dw diff --git a/tests/e2e/typecheck/python_generated_api.py b/tests/e2e/typecheck/python_generated_api.py index 2e076248..377cb1cd 100644 --- a/tests/e2e/typecheck/python_generated_api.py +++ b/tests/e2e/typecheck/python_generated_api.py @@ -20,6 +20,7 @@ ) from python_bindings.windows.gaming.input import Gamepad from python_bindings.windows.application_model.contacts import ContactDate +from python_bindings.windows.data.xml.dom import XmlDocument, XmlLoadSettings from python_bindings.windows.foundation import ( IReference_UInt32, IWwwFormUrlDecoderEntry, @@ -33,13 +34,26 @@ PropertySet, StringMap, ) -from python_bindings.windows.system.threading import ThreadPool, ThreadPoolTimer +from python_bindings.windows.system.threading import ( + ThreadPool, + ThreadPoolTimer, + WorkItemOptions, + WorkItemPriority, +) from python_bindings.windows.globalization import Calendar +from python_bindings.windows.globalization.number_formatting import DecimalFormatter +from python_bindings.windows.storage import ( + NameCollisionOption, + StorageFile, + StorageFolder, +) from python_bindings.windows.storage.streams import ( Buffer as WinRTBuffer, DataWriter, IBuffer, + InMemoryRandomAccessStream, IOutputStream, + RandomAccessStream, ) @@ -158,6 +172,49 @@ def check_async_types( ) +def check_documented_overload_names( + formatter: DecimalFormatter, + calendar: Calendar, + document: XmlDocument, + settings: XmlLoadSettings, +) -> None: + formatted: List[str] = [ + formatter.format(5), + formatter.format(2.5), + formatter.format_int(5), + formatter.format_u_int(5), + calendar.month_as_string(), + calendar.month_as_string(3), + calendar.month_as_full_string(), + ] + document.load_xml("") + document.load_xml("", settings) + document.load_xml_with_settings("", settings) + _: List[str] = formatted + + +async def check_documented_async_overload_names( + file: StorageFile, + folder: StorageFolder, + source: InMemoryRandomAccessStream, + target: InMemoryRandomAccessStream, +) -> None: + option = NameCollisionOption.ReplaceExisting + copies: List[StorageFile | None] = [ + await file.copy_async(folder), + await file.copy_async(folder, "copy.txt"), + await file.copy_async(folder, "copy.txt", option), + await file.copy_overload(folder, "copy.txt", option), + ] + copied: List[int] = [ + await RandomAccessStream.copy_async(source, target), + await RandomAccessStream.copy_async(source, target, 4), + await RandomAccessStream.copy_size_async(source, target, 4), + ] + writer: DataWriter = DataWriter(source) + _: Tuple[List[StorageFile | None], List[int], DataWriter] = (copies, copied, writer) + + def check_ibuffer_bytes() -> None: interface_buffer: IBuffer = IBuffer.from_bytes(bytearray(b"\x00\xff")) runtime_buffer: WinRTBuffer = WinRTBuffer.from_bytes(b"\x01\x02") @@ -194,14 +251,40 @@ def on_properties( def check_delegate_callback_parameters() -> None: - work: WinRTCoroutine[None] = ThreadPool.run_async( + inferred_work: WinRTCoroutine[None] = ThreadPool.run_async( lambda operation: assert_type(operation, DynWinRTValue) ) + + def on_work_item(operation: DynWinRTValue) -> None: + assert_type(operation, DynWinRTValue) + + work: WinRTCoroutine[None] = ThreadPool.run_async(on_work_item) + priority_work: WinRTCoroutine[None] = ThreadPool.run_with_priority_async( + lambda operation: assert_type(operation, DynWinRTValue), + WorkItemPriority.Normal, + ) + options_work: WinRTCoroutine[None] = ThreadPool.run_with_priority_and_options_async( + lambda operation: assert_type(operation, DynWinRTValue), + WorkItemPriority.Normal, + WorkItemOptions.TimeSliced, + ) timer: ThreadPoolTimer | None = ThreadPoolTimer.create_timer( lambda elapsed: assert_type(elapsed.delay, timedelta), timedelta(milliseconds=1), ) - _: Tuple[WinRTCoroutine[None], ThreadPoolTimer | None] = (work, timer) + _: Tuple[ + WinRTCoroutine[None], + WinRTCoroutine[None], + WinRTCoroutine[None], + WinRTCoroutine[None], + ThreadPoolTimer | None, + ] = ( + inferred_work, + work, + priority_work, + options_work, + timer, + ) def check_native_delegate_inputs( @@ -214,5 +297,9 @@ def check_native_delegate_inputs( properties.subscribe_map_changed(raw)() ThreadPool.run_async(native) ThreadPool.run_async(raw) + ThreadPool.run_with_priority_async(native, WorkItemPriority.Normal) + ThreadPool.run_with_priority_and_options_async( + native, WorkItemPriority.Normal, WorkItemOptions.TimeSliced + ) static_token = Gamepad.add_gamepad_added(native) Gamepad.remove_gamepad_added(static_token) diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs index fab5a18c..09620b72 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs @@ -10,6 +10,9 @@ use crate::codegen::winrt::extensions::winui::{self, WinUiAbiType}; use crate::codegen::winrt::python::collections::{ CollectionKind, class_interface, interface_kind, map_iterable_identity, runtime_mixin, }; +use crate::codegen::winrt::python::member_plan::{ + ClassMemberPlan, PlannedMember, ScopePlan, class_instance_interfaces, interface_member_plan, +}; use crate::meta::{ConstructorKind, ParamMeta}; use crate::types::{TypeIdentity, TypeIdentityKind}; @@ -28,9 +31,9 @@ fn interface_symbol(context: &PythonProjectionContext, interface: &InterfaceMeta } /// Generate a Python file for a single RuntimeClass. -pub fn generate_class( +pub fn generate_class<'a>( context: &PythonProjectionContext, - class: &ClassMeta, + class: &'a ClassMeta, shared_iids: &HashSet, ) -> String { let used_structs = collect_used_structs_from_class_and_callbacks(class); @@ -59,12 +62,29 @@ pub fn generate_class( }; let projectable = super::super::has_projectable_default_interface(class); let native_projectable = super::super::has_native_projector(class); + let plan = ClassMemberPlan::new(class, context); + let needs_legacy_helper = plan.statics.has_legacy_fallback() + || plan.instance.has_legacy_fallback() + || class + .required_interfaces + .iter() + .any(|interface| interface_member_plan(interface, context).has_legacy_fallback()); + let needs_legacy_int_guard = plan.statics.has_legacy_int_guard() + || plan.instance.has_legacy_int_guard() + || class + .required_interfaces + .iter() + .any(|interface| interface_member_plan(interface, context).has_legacy_int_guard()); let mut out = String::new(); // Header out.push_str(HEADER); out.push_str(FUTURE_ANNOTATIONS); - out.push_str(&import_line(context)); + out.push_str(&import_line( + context, + needs_legacy_helper, + needs_legacy_int_guard, + )); if has_public_composition { out.push_str( "from dynwinrt import register_xaml_runtime_class as _dynwinrt_register_xaml_runtime_class\n", @@ -233,15 +253,16 @@ pub fn generate_class( for iface in &all_class_ifaces { for method in &iface.methods { for parameter in get_in_params(method) { - py_collect_runtime_class_iid_consts(¶meter.typ, &mut argument_iids); + py_collect_argument_iid_consts(¶meter.typ, &mut argument_iids); } } } argument_iids.sort(); argument_iids.dedup(); for (name, iid) in argument_iids { - if declared_iids.insert(name.clone()) { - out.push_str(&format!("{} = WinGUID.parse('{}')\n", name, iid)); + let symbol = context.argument_iid_reference(&name); + if declared_iids.insert(symbol.to_string()) { + out.push_str(&format!("{symbol} = WinGUID.parse('{iid}')\n")); } } out.push('\n'); @@ -307,6 +328,7 @@ pub fn generate_class( class, collection_iface, collection_uses_default, + &plan.statics, )); if crate::codegen::winrt::is_buffer_class(&class.namespace, &class.name) { @@ -376,59 +398,43 @@ pub fn generate_class( out.push('\n'); } - let static_methods = class + let static_overload = |iface: &'a InterfaceMeta, method: &'a MethodMeta| StaticOverload { + class, + iface, + method, + kind: if class + .factory_interfaces + .iter() + .any(|factory| std::ptr::eq(factory, iface)) + { + StaticOverloadKind::Factory + } else { + StaticOverloadKind::Static + }, + }; + let static_members = class .factory_interfaces .iter() - .flat_map(|iface| iface.methods.iter()) - .chain( - class - .static_interfaces - .iter() - .flat_map(|iface| iface.methods.iter()), - ) - .collect::>(); - let static_method_names = - crate::codegen::winrt::python::overloads::method_names(static_methods.iter().copied()); - let mut static_groups: Vec<(String, Vec>)> = Vec::new(); - for (kind, interfaces) in [ - (StaticOverloadKind::Factory, &class.factory_interfaces), - (StaticOverloadKind::Static, &class.static_interfaces), - ] { - for iface in interfaces { - for method in &iface.methods { - let mut key = crate::codegen::winrt::python::overloads::method_group_key( - method, - &static_method_names, - ); - if method.is_property_getter - || method.is_property_setter - || method.is_event_add - || method.is_event_remove - { - key = format!("{}#{key}", interface_symbol(context, iface)); - } - let overload = StaticOverload { - class, - iface, - method, - kind, - }; - if let Some((_, group)) = static_groups - .iter_mut() - .find(|(group_key, _)| group_key == &key) - { - group.push(overload); - } else { - static_groups.push((key, vec![overload])); - } - } - } - } - for (_, overloads) in static_groups { + .chain(class.static_interfaces.iter()) + .flat_map(|iface| iface.methods.iter().map(move |method| (iface, method))); + for member in plan.statics.members(static_members) { out.push('\n'); - out.push_str(&generate_static_method_group(&overloads, context)); + out.push_str(&match member { + PlannedMember::Accessor(iface, method) => { + generate_static_accessor(&static_overload(iface, method), context) + } + PlannedMember::Group(group) => generate_static_method_group( + group, + |candidate| static_overload(candidate.interface, candidate.method), + context, + ), + }); } - let static_aliases = generate_compatibility_aliases(static_methods.iter().copied()); + let static_aliases = generate_static_compatibility_aliases( + &plan.statics, + |candidate| static_overload(candidate.interface, candidate.method), + context, + ); if !static_aliases.is_empty() { out.push('\n'); out.push_str(&static_aliases); @@ -577,18 +583,7 @@ pub fn generate_class( out.push_str(" return _app\n"); } - let mut method_groups: Vec<(String, Vec>)> = Vec::new(); - let instance_ifaces = class - .default_interface - .iter() - .chain(class.required_interfaces.iter()) - .filter(|iface| iface.iid != "30d5a829-7fa4-4026-83bb-d75bae4ea99e") - .collect::>(); - let instance_method_names = crate::codegen::winrt::python::overloads::method_names( - instance_ifaces - .iter() - .flat_map(|iface| iface.methods.iter()), - ); + let instance_ifaces = class_instance_interfaces(class).collect::>(); let property_getters = instance_ifaces .iter() .flat_map(|iface| iface.methods.iter()) @@ -596,67 +591,62 @@ pub fn generate_class( .filter_map(|method| method.name.strip_prefix("get_")) .map(str::to_string) .collect::>(); + let instance_overload = |iface: &'a InterfaceMeta, method: &'a MethodMeta| { + let obj_expr = if collection_iface + .is_some_and(|collection| collection.type_identity() == iface.type_identity()) + { + collection_obj_expr.to_string() + } else if class + .default_interface + .as_ref() + .is_some_and(|default_iface| default_iface.type_identity() == iface.type_identity()) + { + "self._obj".to_string() + } else { + format!("self._obj.cast(IID_{})", interface_symbol(context, iface)) + }; + InstanceOverload { + iface_var: context.registration_symbol(iface), + obj_expr, + method, + sibling_methods: Some(iface.methods.as_slice()), + property_has_getter: !method.is_property_setter + || method + .name + .strip_prefix("put_") + .is_some_and(|suffix| property_getters.contains(suffix)), + } + }; // Python evaluates decorators while building the class. Emit every getter // before any cross-interface setter that references it. + let mut instance_members = Vec::new(); for setter_phase in [false, true] { for iface in &instance_ifaces { - let obj_expr = if collection_iface - .is_some_and(|collection| collection.type_identity() == iface.type_identity()) - { - collection_obj_expr - } else if class - .default_interface - .as_ref() - .is_some_and(|default_iface| default_iface.type_identity() == iface.type_identity()) - { - "self._obj" - } else { - "" - }; - let iface_symbol = interface_symbol(context, iface); - let obj_expr = if obj_expr.is_empty() { - format!("self._obj.cast(IID_{iface_symbol})") - } else { - obj_expr.to_string() - }; - for method in reorder_getters_before_setters(&iface.methods) - .into_iter() - .filter(|method| method.is_property_setter == setter_phase) - { - let key = crate::codegen::winrt::python::overloads::method_group_key( - method, - &instance_method_names, - ); - let overload = InstanceOverload { - iface_var: context.registration_symbol(iface), - obj_expr: obj_expr.clone(), - method, - sibling_methods: Some(iface.methods.as_slice()), - property_has_getter: !method.is_property_setter - || method - .name - .strip_prefix("put_") - .is_some_and(|suffix| property_getters.contains(suffix)), - }; - if let Some((_, group)) = method_groups - .iter_mut() - .find(|(group_key, _)| group_key == &key) - { - group.push(overload); - } else { - method_groups.push((key, vec![overload])); - } - } + instance_members.extend( + reorder_getters_before_setters(&iface.methods) + .into_iter() + .filter(|method| method.is_property_setter == setter_phase) + .map(|method| (*iface, method)), + ); } } - for (_, overloads) in method_groups { + for member in plan.instance.members(instance_members) { out.push('\n'); - out.push_str(&generate_instance_method_group(&overloads, context)); + out.push_str(&match member { + PlannedMember::Accessor(iface, method) => { + generate_instance_accessor(&instance_overload(iface, method), context) + } + PlannedMember::Group(group) => generate_instance_method_group( + group, + |candidate| instance_overload(candidate.interface, candidate.method), + context, + ), + }); } - let instance_aliases = generate_compatibility_aliases( - instance_ifaces - .iter() - .flat_map(|iface| iface.methods.iter()), + let instance_aliases = generate_instance_compatibility_aliases( + &plan.instance, + |candidate| instance_overload(candidate.interface, candidate.method), + context, ); if !instance_aliases.is_empty() { out.push('\n'); @@ -860,29 +850,41 @@ pub fn generate_class( )); out.push('\n'); out.push_str(&as_interface_method(context)); - for methods in crate::codegen::winrt::python::overloads::grouped_methods( - reorder_getters_before_setters(&req_iface.methods), - ) { + let iface_plan = interface_member_plan(req_iface, context); + let overload = |method: &'a MethodMeta| InstanceOverload { + iface_var: reg_var.clone(), + obj_expr: "self._obj".into(), + method, + sibling_methods: Some(req_iface.methods.as_slice()), + property_has_getter: !method.is_property_setter + || method.name.strip_prefix("put_").is_some_and(|suffix| { + req_iface + .methods + .iter() + .any(|candidate| candidate.name == format!("get_{suffix}")) + }), + }; + let members = reorder_getters_before_setters(&req_iface.methods) + .into_iter() + .map(|method| (req_iface, method)); + for member in iface_plan.members(members) { out.push('\n'); - let overloads = methods - .into_iter() - .map(|method| InstanceOverload { - iface_var: reg_var.clone(), - obj_expr: "self._obj".into(), - method, - sibling_methods: Some(req_iface.methods.as_slice()), - property_has_getter: !method.is_property_setter - || method.name.strip_prefix("put_").is_some_and(|suffix| { - req_iface - .methods - .iter() - .any(|candidate| candidate.name == format!("get_{suffix}")) - }), - }) - .collect::>(); - out.push_str(&generate_instance_method_group(&overloads, context)); + out.push_str(&match member { + PlannedMember::Accessor(_, method) => { + generate_instance_accessor(&overload(method), context) + } + PlannedMember::Group(group) => generate_instance_method_group( + group, + |candidate| overload(candidate.method), + context, + ), + }); } - let aliases = generate_compatibility_aliases(req_iface.methods.iter()); + let aliases = generate_instance_compatibility_aliases( + &iface_plan, + |candidate| overload(candidate.method), + context, + ); if !aliases.is_empty() { out.push('\n'); out.push_str(&aliases); @@ -950,6 +952,9 @@ struct PyCtorCandidate<'a> { public_params: Vec<&'a ParamMeta>, /// Full call expression, e.g. `type(self).create_instance(_bound[0], None)`. call_expr: String, + /// Tie-breaker between constructors whose parameters sort equally: the call + /// under pre-CLR-name method names, so renaming never reorders dispatch. + order_key: String, /// Aggregated call for Python subclasses. `None` means subclass activation /// is not semantically available for this constructor shape. composed_call_expr: Option, @@ -958,7 +963,7 @@ struct PyCtorCandidate<'a> { fn build_ctor_candidates<'a>( context: &PythonProjectionContext, class: &'a ClassMeta, - factory_names: &HashSet, + statics: &ScopePlan<'_>, ) -> Vec> { fn push_unique<'a>(candidates: &mut Vec>, candidate: PyCtorCandidate<'a>) { if let Some(existing) = candidates.iter_mut().find(|existing| { @@ -989,11 +994,13 @@ fn build_ctor_candidates<'a>( match constructor.kind { ConstructorKind::DefaultActivation => { let ctor_name = default_constructor_name(has_create_factory); + let call_expr = format!("type(self).{}()", ctor_name); push_unique( &mut candidates, PyCtorCandidate { public_params: Vec::new(), - call_expr: format!("type(self).{}()", ctor_name), + order_key: call_expr.clone(), + call_expr, composed_call_expr: None, }, ); @@ -1012,13 +1019,15 @@ fn build_ctor_candidates<'a>( continue; } let in_params = crate::codegen::winrt::shared::imports::get_in_params(method); - let call_expr = - build_factory_call_expr(class, method, &in_params, None, factory_names); + let (call_name, previous_name) = static_attributes(statics, method); + let call_expr = build_factory_call_expr(call_name, &in_params, None); + let order_key = build_factory_call_expr(previous_name, &in_params, None); push_unique( &mut candidates, PyCtorCandidate { public_params: in_params, call_expr, + order_key, composed_call_expr: None, }, ); @@ -1043,13 +1052,11 @@ fn build_ctor_candidates<'a>( else { continue; }; - let call_expr = build_factory_call_expr( - class, - method, - &in_params, - Some(outer_index), - factory_names, - ); + let (call_name, previous_name) = static_attributes(statics, method); + let call_expr = + build_factory_call_expr(call_name, &in_params, Some(outer_index)); + let order_key = + build_factory_call_expr(previous_name, &in_params, Some(outer_index)); let inner_output_index = method .params .iter() @@ -1086,6 +1093,7 @@ fn build_ctor_candidates<'a>( PyCtorCandidate { public_params, call_expr, + order_key, composed_call_expr: Some(composed_call_expr), }, ); @@ -1098,47 +1106,22 @@ fn build_ctor_candidates<'a>( candidates } +/// The attributes implementing a constructor factory method now and before +/// CLR-name grouping. +fn static_attributes<'p>(statics: &'p ScopePlan<'_>, method: &MethodMeta) -> (&'p str, &'p str) { + let planned = "constructor factory methods are planned static methods"; + ( + statics.attribute(method).expect(planned), + statics.previous_attribute(method).expect(planned), + ) +} + /// Build a `type(self).(_bound[0], _bound[1], ..., None_for_outer)` call. fn build_factory_call_expr( - class: &ClassMeta, - method: &MethodMeta, + call_name: &str, in_params: &[&ParamMeta], outer_index: Option, - factory_names: &HashSet, ) -> String { - let public_name = - crate::codegen::winrt::python::overloads::method_group_key(method, factory_names); - let mut overloads = class - .factory_interfaces - .iter() - .flat_map(|interface| interface.methods.iter()) - .chain( - class - .static_interfaces - .iter() - .flat_map(|interface| interface.methods.iter()), - ) - .filter(|candidate| { - crate::codegen::winrt::python::overloads::method_group_key(candidate, factory_names) - == public_name - }) - .collect::>(); - let call_name = if overloads.len() > 1 { - overloads.sort_by(|left, right| { - crate::codegen::winrt::python::overloads::cmp_python_dispatch_methods(left, right) - }); - let private_names = crate::codegen::winrt::python::method::private_overload_names( - &public_name, - overloads.iter().copied(), - ); - let index = overloads - .iter() - .position(|candidate| std::ptr::eq(*candidate, method)) - .expect("constructor method must be present in its static overload group"); - private_names[index].clone() - } else { - to_snake_case(&method.name) - }; let mut public_idx = 0usize; let args = in_params .iter() @@ -1218,6 +1201,7 @@ fn generate_python_constructor( class: &ClassMeta, collection_iface: Option<&InterfaceMeta>, collection_uses_default: bool, + statics: &ScopePlan<'_>, ) -> String { let mut out = String::new(); let native_projectable = super::super::has_native_projector(class); @@ -1264,26 +1248,13 @@ fn generate_python_constructor( supported_override_names.sort(); supported_override_names.dedup(); let supported_override_names_expr = python_tuple(&supported_override_names); - let static_methods = class - .factory_interfaces - .iter() - .flat_map(|iface| iface.methods.iter()) - .chain( - class - .static_interfaces - .iter() - .flat_map(|iface| iface.methods.iter()), - ) - .collect::>(); - let factory_names = - crate::codegen::winrt::python::overloads::method_names(static_methods.iter().copied()); - let mut candidates = build_ctor_candidates(context, class, &factory_names); + let mut candidates = build_ctor_candidates(context, class, statics); candidates.sort_by(|left, right| { - crate::codegen::winrt::python::overloads::cmp_python_dispatch_params( + crate::codegen::winrt::python::member_plan::cmp_python_dispatch_params( &left.public_params, &right.public_params, ) - .then_with(|| left.call_expr.cmp(&right.call_expr)) + .then_with(|| left.order_key.cmp(&right.order_key)) }); out.push_str(" def __new__(cls, *args, **kwargs):\n"); @@ -1298,39 +1269,18 @@ fn generate_python_constructor( " if cls is {}:\n", context.class_name(class) )); - for candidate in &candidates { - let parameter_names = candidate - .public_params - .iter() - .map(|param| format!("'{}'", to_snake_case(¶m.name))) - .collect::>() - .join(", "); - let parameter_names = if parameter_names.is_empty() { - "()".to_string() - } else { - format!("({parameter_names},)") - }; - out.push_str(&format!( - " _bound = _dynwinrt_bind_overload({parameter_names}, args, kwargs)\n" - )); - let guards = candidate - .public_params - .iter() - .enumerate() - .map(|(index, param)| { - py_method_type_guard(&format!("_bound[{index}]"), ¶m.typ, context) - }) - .collect::>(); - let condition = if guards.is_empty() { - "_bound is not None".to_string() - } else { - format!("_bound is not None and {}", guards.join(" and ")) - }; - let call_expr = candidate.call_expr.replace("type(self)", "cls"); - out.push_str(&format!( - " if {condition}:\n return {call_expr}\n" - )); - } + let dispatch = candidates + .iter() + .map(|candidate| DispatchCandidate { + legacy_preservation: None, + params: candidate.public_params.clone(), + body: vec![format!( + "return {}", + candidate.call_expr.replace("type(self)", "cls") + )], + }) + .collect::>(); + emit_dispatch(&mut out, " ", &dispatch, None, context); } out.push_str(" return super().__new__(cls)\n\n"); @@ -1513,54 +1463,33 @@ fn generate_python_constructor( )); } } - for candidate in &candidates { - let parameter_names = candidate - .public_params - .iter() - .map(|param| format!("'{}'", to_snake_case(¶m.name))) - .collect::>() - .join(", "); - let parameter_names = if parameter_names.is_empty() { - "()".to_string() - } else { - format!("({parameter_names},)") - }; - out.push_str(&format!( - " _bound = _dynwinrt_bind_overload({parameter_names}, args, kwargs)\n" - )); - let guards = candidate - .public_params - .iter() - .enumerate() - .map(|(index, param)| { - py_method_type_guard(&format!("_bound[{index}]"), ¶m.typ, context) - }) - .collect::>(); - let condition = if guards.is_empty() { - "_bound is not None".to_string() - } else { - format!("_bound is not None and {}", guards.join(" and ")) - }; - out.push_str(&format!(" if {condition}:\n")); - if let Some(composed_call) = &candidate.composed_call_expr { - out.push_str(" if _is_python_subclass:\n"); - out.push_str(&format!( - " self._set_native({composed_call}, _allow_native_overrides=True)\n\ - \x20 return\n" - )); - } else if has_public_composition { - out.push_str(" if _is_python_subclass:\n"); - out.push_str(&format!( - " raise TypeError(\"{} does not support Python subclass construction for this constructor\")\n", - context.class_name(class) - )); - } - out.push_str(&format!( - " self._set_native({}._obj)\n\ - \x20 return\n", - candidate.call_expr - )); - } + let dispatch = candidates + .iter() + .map(|candidate| { + let mut body = Vec::new(); + if let Some(composed_call) = &candidate.composed_call_expr { + body.push("if _is_python_subclass:".to_string()); + body.push(format!( + " self._set_native({composed_call}, _allow_native_overrides=True)" + )); + body.push(" return".to_string()); + } else if has_public_composition { + body.push("if _is_python_subclass:".to_string()); + body.push(format!( + " raise TypeError(\"{} does not support Python subclass construction for this constructor\")", + context.class_name(class) + )); + } + body.push(format!("self._set_native({}._obj)", candidate.call_expr)); + body.push("return".to_string()); + DispatchCandidate { + legacy_preservation: None, + params: candidate.public_params.clone(), + body, + } + }) + .collect::>(); + emit_dispatch(&mut out, " ", &dispatch, None, context); if candidates.is_empty() { out.push_str(&format!( " raise TypeError(\"{} cannot be constructed directly\")\n\n", @@ -1721,17 +1650,21 @@ mod tests { let context = PythonProjectionContext::packaged([enum_type("Mode").type_identity()]).unwrap(); + let forward_class = constructor_class(vec![integer.clone(), enumeration.clone()]); + let reverse_class = constructor_class(vec![enumeration, integer]); let forward = generate_python_constructor( &context, - &constructor_class(vec![integer.clone(), enumeration.clone()]), + &forward_class, None, false, + &ClassMemberPlan::new(&forward_class, &context).statics, ); let reverse = generate_python_constructor( &context, - &constructor_class(vec![enumeration, integer]), + &reverse_class, None, false, + &ClassMemberPlan::new(&reverse_class, &context).statics, ); assert_eq!(forward, reverse); @@ -1836,11 +1769,14 @@ print(json.dumps([exercise(WidgetForward), exercise(WidgetReverse)])) name: "WorkItemHandler".into(), iid: "11111111-1111-1111-1111-111111111111".into(), }; + let context = PythonProjectionContext::standalone([delegate.type_identity()]).unwrap(); + let class = constructor_class(vec![constructor_method("Create", 6, delegate)]); let code = generate_python_constructor( - &PythonProjectionContext::standalone([delegate.type_identity()]).unwrap(), - &constructor_class(vec![constructor_method("Create", 6, delegate)]), + &context, + &class, None, false, + &ClassMemberPlan::new(&class, &context).statics, ); assert!( code.contains("isinstance(_bound[0], DynWinRtDelegate)"), diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs index 6b8a3f2c..31f50044 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs @@ -22,14 +22,16 @@ use crate::codegen::winrt::shared::structs::{ collect_used_structs_from_struct, }; +use super::member_plan::{Candidate, ScopePlan}; use super::method::{ - InstanceOverload, StaticOverload, StaticOverloadKind, generate_instance_method_group, - generate_static_method_group, py_method_type_guard, + DispatchCandidate, InstanceOverload, StaticOverload, StaticOverloadKind, emit_dispatch, + generate_instance_accessor, generate_instance_method_group, generate_static_accessor, + generate_static_method_group, }; use super::naming::{PythonProjectionContext, PythonSupportSymbol, is_py_reserved, to_snake_case}; use super::shared::reorder_getters_before_setters; use super::signature::{ - py_collect_runtime_class_iid_consts, py_dynwinrt_type, py_generate_interface_registration, + py_collect_argument_iid_consts, py_dynwinrt_type, py_generate_interface_registration, py_interface_iid_expr, py_runtime_named_symbol, py_runtime_symbol, py_wrap_native_value, }; use super::structs::{ @@ -39,8 +41,26 @@ use super::type_helpers::methods_have_async_output; const HEADER: &str = "# Generated by dynwinrt-codegen — do not edit\n"; const FUTURE_ANNOTATIONS: &str = "from __future__ import annotations\n"; -fn import_line(context: &PythonProjectionContext) -> String { +fn import_line( + context: &PythonProjectionContext, + needs_legacy_helper: bool, + needs_legacy_int_guard: bool, +) -> String { let object_input = context.support_symbol_import(PythonSupportSymbol::ObjectInput); + let can_cast = context.support_symbol_import(PythonSupportSymbol::CanCast); + let mut legacy_helpers = String::new(); + if needs_legacy_helper { + legacy_helpers.push_str(&format!( + " {},\n", + context.support_symbol_import(PythonSupportSymbol::LegacyCall) + )); + } + if needs_legacy_int_guard { + legacy_helpers.push_str(&format!( + " {},\n", + context.support_symbol_import(PythonSupportSymbol::LegacyIntGuard) + )); + } let as_interface = context.support_symbol_import(PythonSupportSymbol::AsInterface); format!( "\ @@ -50,9 +70,9 @@ from ._runtime import ( DynWinRTType, DynWinRTMethodSig, DynWinRTValue, DynWinRTArray, DynWinRTStruct, DynWinRtDelegate, DynWinRTOverrideInterface, {object_input}, _property, _weakref_ref, - _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_can_cast, _dynwinrt_create_delegate, + _dynwinrt_array, _dynwinrt_bind_overload, {can_cast}, _dynwinrt_create_delegate, _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _dynwinrt_guid, - _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, +{legacy_helpers} _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, _dynwinrt_ticks_to_timedelta, _dynwinrt_timedelta_to_ticks, {as_interface}, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, @@ -136,16 +156,37 @@ def _dynwinrt_project_callback(callback, project): _active_projected_lifetime_scope.reset(token) return callback(*arguments) return invoke + + def _dynwinrt_can_cast(value, iid): raw = getattr(value, '_obj', value) if not isinstance(raw, DynWinRTValue): return False + try_query = getattr(raw, '_try_query_interface', None) + if not callable(try_query): + raise RuntimeError( + 'Generated Python bindings require a matching dynwinrt runtime; ' + 'install the matching runtime and regenerate all Python bindings.' + ) + return try_query(iid) + + +def _dynwinrt_legacy_call(impl, parameter_names, args, kwargs, public_name): + bound = _dynwinrt_bind_overload(parameter_names, args, kwargs) + if bound is None: + raise TypeError(f'No matching overload for {public_name}') + return impl(*bound) + + +def _dynwinrt_legacy_int_guard(value, minimum=None, maximum=None): try: - projected = raw.cast(iid) - except OSError: + converted = int(value) + except (TypeError, ValueError, OverflowError): return False - projected.release() - return True + return ( + (minimum is None or minimum <= converted) + and (maximum is None or converted <= maximum) + ) def _dynwinrt_as_interface(native, interface_class): @@ -239,11 +280,38 @@ fn has_ireference_struct_field(structs: &[TypeMeta]) -> bool { structs.iter().any(contains) } -fn generate_compatibility_aliases<'a>(methods: impl IntoIterator) -> String { - super::overloads::compatibility_aliases(methods) - .into_iter() - .map(|(legacy, canonical)| format!(" {legacy} = {canonical}\n")) - .collect() +fn generate_instance_compatibility_aliases<'a>( + plan: &ScopePlan<'a>, + overload: impl Fn(&Candidate<'a>) -> InstanceOverload<'a>, + context: &PythonProjectionContext, +) -> String { + plan.aliases() + .iter() + .map(|alias| { + alias.dispatcher.as_ref().map_or_else( + || format!(" {} = {}\n", alias.name, alias.target), + |group| generate_instance_method_group(group, &overload, context), + ) + }) + .collect::>() + .join("\n") +} + +fn generate_static_compatibility_aliases<'a>( + plan: &ScopePlan<'a>, + overload: impl Fn(&Candidate<'a>) -> StaticOverload<'a>, + context: &PythonProjectionContext, +) -> String { + plan.aliases() + .iter() + .map(|alias| { + alias.dispatcher.as_ref().map_or_else( + || format!(" {} = {}\n", alias.name, alias.target), + |group| generate_static_method_group(group, &overload, context), + ) + }) + .collect::>() + .join("\n") } pub use class::generate_class; @@ -267,6 +335,33 @@ mod tests { assert!(!runtime.contains("copy_context")); } + #[test] + fn generated_cast_guard_rejects_null_and_non_object_values() { + let runtime = generate_runtime_support_module(); + + assert!(runtime.contains("try_query = getattr(raw, '_try_query_interface', None)")); + assert!(runtime.contains("if not callable(try_query):\n raise RuntimeError(")); + assert!( + runtime.contains("install the matching runtime and regenerate all Python bindings.") + ); + assert!(runtime.contains("return try_query(iid)")); + assert!(!runtime.contains("raw.as_raw()")); + assert!(!runtime.contains("except OSError:")); + assert!(!runtime.contains("except RuntimeError:")); + assert!(runtime.contains( + "def _dynwinrt_legacy_call(impl, parameter_names, args, kwargs, public_name):" + )); + assert!(runtime.contains("raise TypeError(f'No matching overload for {public_name}')")); + assert!( + runtime.contains("def _dynwinrt_legacy_int_guard(value, minimum=None, maximum=None):") + ); + assert!( + runtime + .contains("except (TypeError, ValueError, OverflowError):\n return False") + ); + assert!(!runtime.contains("except (TypeError, ValueError, OverflowError, RuntimeError)")); + } + #[test] fn delegate_inputs_pass_native_delegates_and_project_outside_lifetime_scopes() { let runtime = generate_runtime_support_module(); diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs index 8fa74f45..6ad8489d 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs @@ -59,7 +59,7 @@ pub fn generate_struct(context: &PythonProjectionContext, s: &TypeMeta) -> Optio let mut out = String::new(); out.push_str(HEADER); out.push_str(FUTURE_ANNOTATIONS); - out.push_str(&import_line(context)); + out.push_str(&import_line(context, false, false)); out.push_str(&generate_struct_imports(context, &dependencies)); if has_ireference_struct_field(std::slice::from_ref(s)) { diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs index c87b76c1..60ad336e 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs @@ -10,6 +10,7 @@ use crate::codegen::winrt::python::collections::{ CollectionKind, interface_kind, map_iterable_identity, observable_collection_identity, observable_vector_identity, runtime_mixin, }; +use crate::codegen::winrt::python::member_plan::{PlannedMember, interface_member_plan}; use crate::types::{TypeIdentity, TypeIdentityKind}; /// Generate a Python file for a single enum. @@ -88,11 +89,16 @@ pub fn generate_interface(context: &PythonProjectionContext, iface: &InterfaceMe return generate_delegate(iface); } let implementation = super::super::implementation::project(context, iface); + let plan = interface_member_plan(iface, context); let mut out = String::new(); out.push_str(HEADER); out.push_str(FUTURE_ANNOTATIONS); - out.push_str(&import_line(context)); + out.push_str(&import_line( + context, + plan.has_legacy_fallback(), + plan.has_legacy_int_guard(), + )); if implementation.supported { out.push_str(super::super::implementation::IMPORTS); } @@ -217,13 +223,17 @@ pub fn generate_interface(context: &PythonProjectionContext, iface: &InterfaceMe let mut argument_iids = Vec::new(); for method in &iface.methods { for parameter in get_in_params(method) { - py_collect_runtime_class_iid_consts(¶meter.typ, &mut argument_iids); + py_collect_argument_iid_consts(¶meter.typ, &mut argument_iids); } } argument_iids.sort(); argument_iids.dedup(); for (name, iid) in argument_iids { - out.push_str(&format!("{} = WinGUID.parse('{}')\n", name, iid)); + out.push_str(&format!( + "{} = WinGUID.parse('{}')\n", + context.argument_iid_reference(&name), + iid + )); } out.push('\n'); @@ -564,29 +574,40 @@ pub fn generate_interface(context: &PythonProjectionContext, iface: &InterfaceMe } else { "self._obj" }; - for methods in crate::codegen::winrt::python::overloads::grouped_methods( - reorder_getters_before_setters(&iface.methods), - ) { + let overload = |method| InstanceOverload { + iface_var: iface_var.clone(), + obj_expr: obj_expr.to_string(), + method, + sibling_methods: Some(iface.methods.as_slice()), + property_has_getter: !method.is_property_setter + || method.name.strip_prefix("put_").is_some_and(|suffix| { + iface + .methods + .iter() + .any(|candidate| candidate.name == format!("get_{suffix}")) + }), + }; + let members = reorder_getters_before_setters(&iface.methods) + .into_iter() + .map(|method| (iface, method)); + for member in plan.members(members) { out.push('\n'); - let overloads = methods - .into_iter() - .map(|method| InstanceOverload { - iface_var: iface_var.clone(), - obj_expr: obj_expr.to_string(), - method, - sibling_methods: Some(iface.methods.as_slice()), - property_has_getter: !method.is_property_setter - || method.name.strip_prefix("put_").is_some_and(|suffix| { - iface - .methods - .iter() - .any(|candidate| candidate.name == format!("get_{suffix}")) - }), - }) - .collect::>(); - out.push_str(&generate_instance_method_group(&overloads, context)); - } - let aliases = generate_compatibility_aliases(iface.methods.iter()); + out.push_str(&match member { + PlannedMember::Accessor(_, method) => { + generate_instance_accessor(&overload(method), context) + } + PlannedMember::Group(group) => generate_instance_method_group( + group, + |candidate| overload(candidate.method), + context, + ), + }); + } + let aliases = generate_instance_compatibility_aliases( + &plan, + |candidate| overload(candidate.method), + context, + ); if !aliases.is_empty() { out.push('\n'); out.push_str(&aliases); diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/member_plan.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/member_plan.rs new file mode 100644 index 00000000..aaf3915f --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/member_plan.rs @@ -0,0 +1,2468 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Python member plan: overload groups, public method names, private dispatch +//! names, and compatibility aliases. +//! +//! A plan is computed once per generated Python class from its WinRT +//! interfaces. The runtime (`.py`) and stub (`.pyi`) generators both render the +//! same plan, so they cannot disagree about which methods share a Python name, +//! the dispatch order of the overloads, the private name of each overload, or +//! which earlier names remain as aliases. Generators keep their own member +//! order and emit a group where its first method appears. +//! +//! All overloads of a WinRT method share one CLR (MethodDef) name, while +//! `[Overload]` gives each ABI slot a unique name. Python projects the +//! overloads of a CLR method as one dispatched method named after the CLR +//! name, so `IStorageFile.CopyAsync` is `copy_async` rather than +//! `copy_overload`, `copy_overload_default_options`, and so on. The +//! established suffix heuristics (`Foo2`, `FooOverload...`, `FooWithOptions` +//! next to `foo`) still merge on top of the CLR name. +//! `ThreadPool.RunAsync` deliberately keeps its three ABI names so its +//! original single-argument callable retains contextual lambda typing. +//! +//! Names are planned per Python class namespace: a runtime class shares one +//! namespace between its static and instance members, properties, event +//! helpers, and generated members. The plan never removes a public name and +//! never changes which overload an existing name reaches. A CLR-name group +//! keeps its previous names when its new name would collide with another +//! member, or when an existing name would lose one of its overloads or gain an +//! overload that could take its calls. Every previously emitted method name +//! that is no longer a public method stays as a compatibility alias: of the +//! exact implementation it called when it was a standalone method, otherwise of +//! the dispatcher that reaches its overloads. +//! +//! Interface implementation handlers keep one name per ABI slot and do not use +//! this module. + +use std::cmp::Ordering; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; + +use crate::codegen::winrt::extensions::winui; +use crate::codegen::winrt::shared::imports::{get_in_params, ireference_inner_type}; +use crate::meta::{ + ClassMeta, ConstructorKind, InterfaceMeta, MethodMeta, ParamDirection, ParamMeta, +}; +use crate::types::TypeMeta; + +use super::collections::{ + class_interface, interface_kind, observable_vector_identity, runtime_mixin, type_kind, +}; +use super::naming::{PythonProjectionContext, to_snake_case}; +use super::native_types::{FoundationType, foundation_type}; +use super::signature::{ + LegacyGuardOverlap, LegacyPreservationPredicate, legacy_guard_overlap, + py_dispatch_type_sort_key, py_has_permissive_guard, +}; + +const ICLOSABLE_IID: &str = "30d5a829-7fa4-4026-83bb-d75bae4ea99e"; +const ITHREAD_POOL_STATICS_IID: &str = "b6bf67dd-84bd-44f8-ac1c-93ebcb9dba91"; +const WORK_ITEM_HANDLER_IID: &str = "1d1a8b8b-fa66-414f-9cbd-b65fc99d17fa"; + +#[derive(Clone, Copy)] +enum ThreadPoolInput { + Handler, + Priority, + Options, +} + +impl ThreadPoolInput { + fn matches(self, typ: &TypeMeta) -> bool { + match (self, typ) { + ( + Self::Handler, + TypeMeta::Delegate { + namespace, + name, + iid, + } + | TypeMeta::Interface { + namespace, + name, + iid, + }, + ) => { + namespace == "Windows.System.Threading" + && name == "WorkItemHandler" + && iid.eq_ignore_ascii_case(WORK_ITEM_HANDLER_IID) + } + ( + Self::Priority, + TypeMeta::Enum { + namespace, + name, + underlying, + .. + }, + ) => { + namespace == "Windows.System.Threading" + && name == "WorkItemPriority" + && underlying.as_ref() == &TypeMeta::I32 + } + ( + Self::Options, + TypeMeta::Enum { + namespace, + name, + underlying, + .. + }, + ) => { + namespace == "Windows.System.Threading" + && name == "WorkItemOptions" + && underlying.as_ref() == &TypeMeta::U32 + } + _ => false, + } + } +} + +/// Members inherited from the `collections.abc` bases of generated collection mixins. +const COLLECTION_MIXIN_MEMBERS: &[&str] = &[ + "append", + "clear", + "count", + "extend", + "get", + "index", + "insert", + "items", + "keys", + "pop", + "popitem", + "remove", + "reverse", + "setdefault", + "update", + "values", +]; + +/// One overload of a planned method group. +pub(crate) struct Candidate<'a> { + pub(crate) interface: &'a InterfaceMeta, + pub(crate) method: &'a MethodMeta, + /// Attribute implementing this overload: the group name when it is the only + /// candidate, otherwise its private dispatch name. + pub(crate) attribute: String, + /// Whether this group defines the implementation attribute. Compatibility + /// dispatchers may also call an implementation defined by its canonical + /// CLR-name group. + pub(crate) define: bool, + /// Predicates that identify values accepted by the former guard-free + /// method and therefore reserve this call for the exact legacy tier. + pub(crate) legacy_preservation: Option, +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct LegacyPreservation { + pub(crate) strict: Option>, + pub(crate) permissive: Option>, +} + +/// Methods projected as one Python method, in dispatch order. +pub(crate) struct MethodGroup<'a> { + pub(crate) name: String, + pub(crate) candidates: Vec>, + /// The exact formerly standalone method to call, without type guards, + /// when no typed overload candidate accepts the call. + pub(crate) legacy_fallback: Option>, +} + +pub(crate) struct LegacyFallback<'a> { + pub(crate) method: &'a MethodMeta, + pub(crate) attribute: String, +} + +/// A previously emitted method name kept as a class attribute alias. +pub(crate) struct Alias<'a> { + pub(crate) name: String, + /// Attribute the alias is bound to. + pub(crate) target: String, + /// Exact former dispatcher retained when the broader target could select a + /// different native method for a previously successful call. + pub(crate) dispatcher: Option>, + /// Methods whose signatures the stub declares for this name. + pub(crate) signatures: Vec<&'a MethodMeta>, +} + +/// A member of a Python class in generator order. +pub(crate) enum PlannedMember<'p, 'a> { + /// Property accessor or event method, emitted by its own rules. + Accessor(&'a InterfaceMeta, &'a MethodMeta), + /// The first appearance of a method group. + Group(&'p MethodGroup<'a>), +} + +/// Member plan for one scope (static or instance members) of a Python class. +pub(crate) struct ScopePlan<'a> { + groups: Vec>, + group_of: HashMap<*const MethodMeta, usize>, + aliases: Vec>, + previous_attributes: HashMap<*const MethodMeta, String>, + #[cfg(test)] + fallbacks: Vec, +} + +impl<'a> ScopePlan<'a> { + /// Walk members in generator order, yielding each method group once, at its + /// first method. + pub(crate) fn members<'p>( + &'p self, + methods: impl IntoIterator, + ) -> Vec> { + let mut emitted = HashSet::new(); + let mut members = Vec::new(); + for (interface, method) in methods { + match self.group_of.get(&(method as *const MethodMeta)) { + Some(&index) => { + if emitted.insert(index) { + members.push(PlannedMember::Group(&self.groups[index])); + } + } + None => { + assert!( + is_accessor(method), + "method {} is not part of this member plan", + method.name + ); + members.push(PlannedMember::Accessor(interface, method)); + } + } + } + members + } + + /// The attribute implementing `method`, or `None` for accessors. + pub(crate) fn attribute(&self, method: &MethodMeta) -> Option<&str> { + let group = &self.groups[*self.group_of.get(&(method as *const MethodMeta))?]; + group + .candidates + .iter() + .find(|candidate| std::ptr::eq(candidate.method, method)) + .map(|candidate| candidate.attribute.as_str()) + } + + /// The attribute that implemented `method` before CLR-name grouping. Used + /// only where generated dispatch order depended on attribute names. + pub(crate) fn previous_attribute(&self, method: &MethodMeta) -> Option<&str> { + self.previous_attributes + .get(&(method as *const MethodMeta)) + .map(String::as_str) + } + + /// Previously emitted names kept as aliases, sorted by name. + pub(crate) fn aliases(&self) -> &[Alias<'a>] { + &self.aliases + } + + pub(crate) fn has_legacy_fallback(&self) -> bool { + self.groups + .iter() + .any(|group| group.legacy_fallback.is_some()) + } + + pub(crate) fn has_legacy_int_guard(&self) -> bool { + self.groups + .iter() + .chain( + self.aliases + .iter() + .filter_map(|alias| alias.dispatcher.as_ref()), + ) + .any(|group| { + group.candidates.iter().any(|candidate| { + candidate.legacy_preservation.as_ref().is_some_and(|plan| { + plan.strict + .iter() + .chain(plan.permissive.iter()) + .flatten() + .any(|predicate| { + matches!( + predicate, + LegacyPreservationPredicate::ExactIntConversionRangeOrSubclass { + .. + } + ) + }) + }) + }) + }) + } + + /// CLR names that kept their previous Python names because of a collision. + #[cfg(test)] + pub(crate) fn fallbacks(&self) -> &[String] { + &self.fallbacks + } +} + +/// Member plans for a runtime class; its static and instance members share +/// one Python class namespace. +pub(crate) struct ClassMemberPlan<'a> { + pub(crate) statics: ScopePlan<'a>, + pub(crate) instance: ScopePlan<'a>, +} + +impl<'a> ClassMemberPlan<'a> { + pub(crate) fn new(class: &'a ClassMeta, context: &PythonProjectionContext) -> Self { + let statics = class + .factory_interfaces + .iter() + .chain(class.static_interfaces.iter()) + .collect(); + let instance = class_instance_interfaces(class).collect(); + let reserved = class_reserved_names(class, context); + let mut scopes = plan_scopes_with_context( + &[statics, instance], + &reserved, + context, + &class_abi_name_exceptions(class), + ) + .into_iter(); + Self { + statics: scopes.next().expect("static scope"), + instance: scopes.next().expect("instance scope"), + } + } +} + +fn class_abi_name_exceptions(class: &ClassMeta) -> BTreeSet<(usize, String)> { + if class.namespace != "Windows.System.Threading" || class.name != "ThreadPool" { + return BTreeSet::new(); + } + let interfaces = class + .static_interfaces + .iter() + .filter(|interface| { + interface.namespace == class.namespace + && interface.name == "IThreadPoolStatics" + && interface.iid.eq_ignore_ascii_case(ITHREAD_POOL_STATICS_IID) + }) + .collect::>(); + let [interface] = interfaces.as_slice() else { + return BTreeSet::new(); + }; + let expected: [(&str, usize, &[(&str, ThreadPoolInput)]); 3] = [ + ("RunAsync", 6, &[("handler", ThreadPoolInput::Handler)]), + ( + "RunWithPriorityAsync", + 7, + &[ + ("handler", ThreadPoolInput::Handler), + ("priority", ThreadPoolInput::Priority), + ], + ), + ( + "RunWithPriorityAndOptionsAsync", + 8, + &[ + ("handler", ThreadPoolInput::Handler), + ("priority", ThreadPoolInput::Priority), + ("options", ThreadPoolInput::Options), + ], + ), + ]; + let methods = interface + .methods + .iter() + .filter(|method| method.raw_name == "RunAsync") + .collect::>(); + if methods.len() != expected.len() + || !expected.iter().all(|(name, slot, parameters)| { + methods.iter().any(|method| { + method.name == *name + && method.vtable_index == *slot + && !is_accessor(method) + && method.return_type.as_ref() == Some(&TypeMeta::AsyncAction) + && method.params.len() == parameters.len() + && method.params.iter().zip(parameters.iter()).all( + |(param, (expected_name, expected_type))| { + param.direction == ParamDirection::In + && to_snake_case(¶m.name) == *expected_name + && expected_type.matches(¶m.typ) + }, + ) + }) + }) + { + return BTreeSet::new(); + } + + // Keep the original single-argument callback contextually typed by mypy. + BTreeSet::from([(0, "run_async".to_string())]) +} + +/// Member plan for an interface wrapper class. +pub(crate) fn interface_member_plan<'a>( + interface: &'a InterfaceMeta, + context: &PythonProjectionContext, +) -> ScopePlan<'a> { + plan_scopes_with_context( + &[vec![interface]], + &interface_reserved_names(interface), + context, + &BTreeSet::new(), + ) + .pop() + .expect("interface scope") +} + +/// Interfaces whose methods are projected as instance members of a runtime class. +pub(crate) fn class_instance_interfaces(class: &ClassMeta) -> impl Iterator { + class + .default_interface + .iter() + .chain(class.required_interfaces.iter()) + .filter(|interface| interface.iid != ICLOSABLE_IID) +} + +pub(crate) fn is_accessor(method: &MethodMeta) -> bool { + method.is_property_getter + || method.is_property_setter + || method.is_event_add + || method.is_event_remove +} + +/// Snake-case ABI name of a method: its public name before CLR-name grouping. +fn abi_name(method: &MethodMeta) -> String { + to_snake_case(&method.name) +} + +/// Snake-case CLR (MethodDef) name shared by every overload of a method. +fn clr_name(method: &MethodMeta) -> String { + if method.raw_name.is_empty() { + abi_name(method) + } else { + to_snake_case(&method.raw_name) + } +} + +/// Merge `foo2`, `foo_overload...`, and `foo_with_options` into an existing `foo`. +fn suffix_group_key(name: &str, names: &HashSet) -> String { + let mut candidates = Vec::new(); + if let Some((base, _)) = name.split_once("_overload") { + candidates.push(base); + } + if let Some(base) = name.strip_suffix("_with_options") { + candidates.push(base); + } + let numeric_base = name.trim_end_matches(|character: char| character.is_ascii_digit()); + if numeric_base.len() < name.len() { + candidates.push(numeric_base); + } + candidates + .into_iter() + .find(|base| !base.is_empty() && names.contains(*base)) + .map(str::to_string) + .unwrap_or_else(|| name.to_string()) +} + +pub(crate) fn private_overload_names<'a>( + public_name: &str, + methods: impl IntoIterator, +) -> Vec { + let base_names = methods + .into_iter() + .map(|method| format!("_{public_name}_{}", method.vtable_index)) + .collect::>(); + base_names + .iter() + .enumerate() + .map(|(index, base)| { + if base_names + .iter() + .filter(|candidate| *candidate == base) + .count() + > 1 + { + format!("{base}_{index}") + } else { + base.clone() + } + }) + .collect() +} + +fn insert_accessor_names(method: &MethodMeta, is_static: bool, names: &mut HashSet) { + if is_static { + if method.is_property_getter && get_in_params(method).is_empty() { + let property = method.name.strip_prefix("get_").unwrap_or(&method.name); + names.insert(format!("get_{}", to_snake_case(property))); + } else if is_accessor(method) { + names.insert(abi_name(method)); + } + } else if method.is_property_getter { + names.insert(to_snake_case( + method.name.strip_prefix("get_").unwrap_or(&method.name), + )); + } else if method.is_property_setter { + let property = to_snake_case(method.name.strip_prefix("put_").unwrap_or(&method.name)); + names.insert(format!("set_{property}")); + names.insert(property); + } else if method.is_event_add || method.is_event_remove { + let event = to_snake_case( + method + .name + .strip_prefix("add_") + .or_else(|| method.name.strip_prefix("remove_")) + .unwrap_or(&method.name), + ); + for prefix in ["on", "off", "subscribe", "once"] { + names.insert(format!("{prefix}_{event}")); + } + } +} + +/// Non-method names in a runtime class namespace: accessors and generated members. +fn class_reserved_names(class: &ClassMeta, context: &PythonProjectionContext) -> HashSet { + let mut names = HashSet::from(["as_interface".to_string()]); + for method in class_instance_interfaces(class).flat_map(|interface| interface.methods.iter()) { + insert_accessor_names(method, false, &mut names); + } + for method in class + .factory_interfaces + .iter() + .chain(class.static_interfaces.iter()) + .flat_map(|interface| interface.methods.iter()) + { + insert_accessor_names(method, true, &mut names); + } + let factory_names = class + .factory_interfaces + .iter() + .flat_map(|interface| interface.methods.iter()) + .map(abi_name) + .collect::>(); + if class.has_default_activation() { + let default_constructor = if factory_names.iter().any(|name| name.starts_with("create")) { + "create_default" + } else { + "create" + }; + names.insert(default_constructor.to_string()); + } else if !factory_names.iter().any(|name| name == "create") + && class.factory_interfaces.iter().any(|interface| { + class.is_public_constructor_factory(interface) + && interface.methods.iter().any(|method| { + method.name == "CreateInstance" + && get_in_params(method).is_empty() + && matches!( + method.return_type.as_ref(), + Some(TypeMeta::RuntimeClass { namespace, name, .. }) + if namespace == &class.namespace && name == &class.name + ) + }) + }) + { + names.insert("create".to_string()); + } + if class + .required_interfaces + .iter() + .any(|interface| interface.iid == ICLOSABLE_IID) + { + names.insert("close".to_string()); + } + if crate::codegen::winrt::is_buffer_class(&class.namespace, &class.name) { + names.extend(["from_bytes".to_string(), "to_bytes".to_string()]); + } + if winui::is_dispatcher_queue(class) { + names.extend([ + "enqueue_async".to_string(), + "enqueue_with_priority_async".to_string(), + ]); + } + if winui::resolve_application_bootstrap(class, &context.known_full_names()).is_some() { + names.extend([ + "create".to_string(), + "create_with_metadata_provider".to_string(), + ]); + } + if class + .constructors + .iter() + .any(|constructor| constructor.kind == ConstructorKind::PublicComposition) + { + names.insert("register_xaml_runtime_class".to_string()); + } + if class_interface(class) + .and_then(interface_kind) + .and_then(runtime_mixin) + .is_some() + { + names.extend(COLLECTION_MIXIN_MEMBERS.iter().map(|name| name.to_string())); + } + names +} + +/// Non-method names in an interface wrapper class: accessors and generated members. +fn interface_reserved_names(interface: &InterfaceMeta) -> HashSet { + let mut names = [ + "as_interface", + "as_vector", + "create", + "from_bytes", + "from_implementation", + "from_value", + "implement", + "implementation", + "release_callbacks", + "to_bytes", + ] + .into_iter() + .map(str::to_string) + .collect::>(); + for method in &interface.methods { + insert_accessor_names(method, false, &mut names); + } + if interface_kind(interface).and_then(runtime_mixin).is_some() + || observable_vector_identity(interface).is_some() + { + names.extend(COLLECTION_MIXIN_MEMBERS.iter().map(|name| name.to_string())); + } + names +} + +struct Entry<'a> { + interface: &'a InterfaceMeta, + method: &'a MethodMeta, + scope: usize, + abi: String, + abi_key: String, + clr_key: String, +} + +type Groups = Vec>>; + +#[cfg(test)] +fn plan_scopes<'a>( + scopes: &[Vec<&'a InterfaceMeta>], + reserved: &HashSet, +) -> Vec> { + plan_scopes_with_context( + scopes, + reserved, + &PythonProjectionContext::default(), + &BTreeSet::new(), + ) +} + +/// Plan the scopes of one Python class namespace; `reserved` holds its +/// non-method member names. +fn plan_scopes_with_context<'a>( + scopes: &[Vec<&'a InterfaceMeta>], + reserved: &HashSet, + context: &PythonProjectionContext, + abi_name_exceptions: &BTreeSet<(usize, String)>, +) -> Vec> { + let mut entries = Vec::new(); + for (scope, interfaces) in scopes.iter().enumerate() { + let methods = interfaces + .iter() + .flat_map(|interface| { + interface + .methods + .iter() + .map(move |method| (*interface, method)) + }) + .filter(|(_, method)| !is_accessor(method)) + .collect::>(); + let abi_names = methods + .iter() + .map(|(_, method)| abi_name(method)) + .collect::>(); + let clr_names = methods + .iter() + .map(|(_, method)| clr_name(method)) + .collect::>(); + for (interface, method) in methods { + let abi = abi_name(method); + entries.push(Entry { + interface, + method, + scope, + abi_key: suffix_group_key(&abi, &abi_names), + clr_key: suffix_group_key(&clr_name(method), &clr_names), + abi, + }); + } + } + let dispatch_order = |members: &[usize]| { + let mut ordered = members.to_vec(); + ordered.sort_by(|left, right| { + cmp_python_dispatch_methods(entries[*left].method, entries[*right].method) + }); + ordered + }; + let equivalent = |left: usize, right: usize| { + left == right || equivalent_overloads(entries[left].method, entries[right].method) + }; + // The overloads a group dispatched to, without duplicates that dispatch + // always shadowed (the same method on two interfaces). + let reachable = |members: &[usize]| { + let mut kept = Vec::::new(); + for index in dispatch_order(members) { + if !kept.iter().any(|&other| equivalent(other, index)) { + kept.push(index); + } + } + kept + }; + let preservation = + |legacy: usize, candidate: usize| -> Result, ()> { + if legacy == candidate { + return Ok(None); + } + let strict = legacy_guard_overlap( + entries[legacy].method, + entries[candidate].method, + false, + context, + ); + let has_permissive = get_in_params(entries[candidate].method) + .iter() + .any(|param| py_has_permissive_guard(¶m.typ, context)); + let permissive = has_permissive.then(|| { + legacy_guard_overlap( + entries[legacy].method, + entries[candidate].method, + true, + context, + ) + }); + if strict == LegacyGuardOverlap::Unknown + || permissive.as_ref() == Some(&LegacyGuardOverlap::Unknown) + { + return Err(()); + } + let strict = match strict { + LegacyGuardOverlap::Preserve(predicates) => Some(predicates), + LegacyGuardOverlap::Disjoint => None, + LegacyGuardOverlap::Unknown => unreachable!("handled above"), + }; + let permissive = match permissive { + Some(LegacyGuardOverlap::Preserve(predicates)) => Some(predicates), + Some(LegacyGuardOverlap::Disjoint) | None => None, + Some(LegacyGuardOverlap::Unknown) => unreachable!("handled above"), + }; + Ok((strict.is_some() || permissive.is_some()) + .then_some(LegacyPreservation { strict, permissive })) + }; + + // Names emitted before CLR-name grouping, mapped to the group each reached. + let mut previous_groups: Groups = vec![BTreeMap::new(); scopes.len()]; + for (index, entry) in entries.iter().enumerate() { + previous_groups[entry.scope] + .entry(entry.abi_key.clone()) + .or_default() + .push(index); + } + let mut existing: Vec> = vec![BTreeMap::new(); scopes.len()]; + for (scope, groups) in previous_groups.iter().enumerate() { + for key in groups.keys() { + existing[scope].insert(key.clone(), key.clone()); + } + } + for entry in &entries { + existing[entry.scope] + .entry(entry.abi.clone()) + .or_insert_with(|| entry.abi_key.clone()); + } + + let mut fallback = abi_name_exceptions.clone(); + let groups = loop { + let key_of = |index: usize| -> &String { + let entry = &entries[index]; + if fallback.contains(&(entry.scope, entry.clr_key.clone())) { + &entry.abi_key + } else { + &entry.clr_key + } + }; + let mut groups: Groups = vec![BTreeMap::new(); scopes.len()]; + for (index, entry) in entries.iter().enumerate() { + groups[entry.scope] + .entry(key_of(index).clone()) + .or_default() + .push(index); + } + let mut blamed = BTreeSet::new(); + let can_fall_back = |members: &[usize]| { + members.iter().any(|&index| { + !fallback.contains(&(entries[index].scope, entries[index].clr_key.clone())) + }) + }; + let mut blame = |members: &[usize]| { + for &index in members { + let key = (entries[index].scope, entries[index].clr_key.clone()); + if !fallback.contains(&key) { + blamed.insert(key); + } + } + }; + let covers_exact = |members: &[usize], index: usize| members.contains(&index); + let covers_shape = |members: &[usize], index: usize| { + members.iter().any(|&member| equivalent(member, index)) + }; + // Whether a new overload could take a call that reached `expected`. + let takes_calls = |members: &[usize], expected: &[usize]| { + members.iter().any(|&member| { + !covers_shape(expected, member) + && expected.iter().any(|&index| { + if expected.len() == 1 { + preservation(index, member).is_err() + } else { + overloads_may_overlap(entries[member].method, entries[index].method) + } + }) + }) + }; + for scope in 0..scopes.len() { + for (name, previous_key) in &existing[scope] { + let previous_members = &previous_groups[scope][previous_key]; + let expected = reachable(previous_members); + if let Some(members) = groups[scope].get(name) { + // An existing public method keeps every overload it reached + // and gains none that could take its calls. + for &index in &expected { + if covers_exact(members, index) || covers_shape(members, index) { + continue; + } + if can_fall_back(members) { + blame(members); + } else { + // The name already kept its previous meaning; return + // the overload that moved to another CLR-name group. + blame(&groups[scope][key_of(index)]); + } + } + if takes_calls(members, &expected) { + blame(members); + } + } else if previous_members.len() > 1 { + // A former dispatcher becomes an alias of the dispatcher that + // now owns all of its overloads. + let targets = expected + .iter() + .map(|&index| key_of(index)) + .collect::>(); + for target in &targets { + let members = &groups[scope][*target]; + if targets.len() > 1 || takes_calls(members, &expected) { + blame(members); + } + } + } + } + for (name, members) in &groups[scope] { + if existing[scope].contains_key(name) { + continue; + } + let collides = reserved.contains(name) + || (0..scopes.len()).any(|other| { + other != scope + && (existing[other].contains_key(name) + || groups[other].contains_key(name)) + }); + if collides { + blame(members); + } + } + } + if blamed.is_empty() { + break groups; + } + fallback.extend(blamed); + }; + + // An existing name can now be the documented name of a different, + // identically shaped interface method. Keep its old dispatcher exact by + // prepending the methods it previously dispatched to, while the same + // implementations remain available from their canonical CLR-name group. + let mut effective_groups = groups.clone(); + let mut compatibility_dispatchers = BTreeSet::new(); + for scope in 0..scopes.len() { + for (name, previous_key) in &existing[scope] { + let Some(members) = groups[scope].get(name) else { + continue; + }; + let expected = reachable(&previous_groups[scope][previous_key]); + let current = reachable(members); + let needs_compatibility_dispatcher = expected.iter().any(|&index| { + !current.contains(&index) && members.iter().any(|&member| equivalent(member, index)) + }); + if !needs_compatibility_dispatcher { + continue; + } + let mut combined = dispatch_order(&previous_groups[scope][previous_key]); + let extras = dispatch_order(members) + .into_iter() + .filter(|index| !combined.contains(index)) + .collect::>(); + combined.extend(extras); + effective_groups[scope].insert(name.clone(), combined); + compatibility_dispatchers.insert((scope, name.clone())); + } + } + + let previous_attributes = |name: &str, members: &[usize]| { + let ordered = dispatch_order(members); + let names = if ordered.len() == 1 { + vec![name.to_string()] + } else { + private_overload_names(name, ordered.iter().map(|&index| entries[index].method)) + }; + ordered.into_iter().zip(names).collect::>() + }; + (0..scopes.len()) + .map(|scope| { + let primary_group_of = groups[scope] + .iter() + .flat_map(|(name, members)| members.iter().map(move |&index| (index, name.clone()))) + .collect::>(); + let borrowed = effective_groups[scope] + .iter() + .flat_map(|(name, members)| { + members + .iter() + .copied() + .filter(|index| primary_group_of[index] != *name) + }) + .collect::>(); + let mut attribute_of = HashMap::new(); + for (name, members) in &groups[scope] { + let ordered = dispatch_order(members); + let names = if effective_groups[scope][name].len() == 1 + && members.iter().all(|index| !borrowed.contains(index)) + { + vec![name.clone()] + } else { + private_overload_names(name, ordered.iter().map(|&index| entries[index].method)) + }; + attribute_of.extend(ordered.into_iter().zip(names)); + } + + let mut ordered_groups = effective_groups[scope].iter().collect::>(); + ordered_groups.sort_by_key(|(name, _)| groups[scope][*name][0]); + let mut plan_groups = Vec::with_capacity(ordered_groups.len()); + let mut group_of = HashMap::new(); + for (position, (name, members)) in ordered_groups.into_iter().enumerate() { + let ordered = if compatibility_dispatchers.contains(&(scope, name.clone())) { + members.clone() + } else { + dispatch_order(members) + }; + let legacy_index = existing[scope].get(name).and_then(|previous_key| { + let previous = &previous_groups[scope][previous_key]; + (previous.len() == 1 && ordered.len() > 1).then_some(previous[0]) + }); + let candidates = ordered + .into_iter() + .map(|index| { + let entry = &entries[index]; + let define = primary_group_of[&index] == *name; + if define { + group_of.insert(entry.method as *const MethodMeta, position); + } + Candidate { + interface: entry.interface, + method: entry.method, + attribute: attribute_of[&index].clone(), + define, + legacy_preservation: legacy_index.and_then(|legacy| { + preservation(legacy, index) + .expect("unknown legacy overlap must retain ABI grouping") + }), + } + }) + .collect::>(); + let legacy_fallback = legacy_index.map(|index| LegacyFallback { + method: entries[index].method, + attribute: attribute_of[&index].clone(), + }); + plan_groups.push(MethodGroup { + name: name.clone(), + candidates, + legacy_fallback, + }); + } + let previous_attributes = previous_groups[scope] + .iter() + .flat_map(|(name, members)| previous_attributes(name, members)) + .map(|(index, attribute)| (entries[index].method as *const MethodMeta, attribute)) + .collect(); + let aliases = existing[scope] + .iter() + .filter(|(name, _)| !groups[scope].contains_key(*name)) + .map(|(name, previous_key)| { + let previous_members = &previous_groups[scope][previous_key]; + // A former standalone method has no dispatch guards; bind + // its name to the exact implementation it called. + let target = if previous_members.len() == 1 { + attribute_of[&previous_members[0]].clone() + } else { + let expected = reachable(previous_members); + let entry = &entries[expected[0]]; + plan_groups[group_of[&(entry.method as *const MethodMeta)]] + .name + .clone() + }; + let dispatcher = (previous_members.len() > 1) + .then(|| { + let target_group = + plan_groups.iter().find(|group| group.name == target)?; + let old_methods = previous_members + .iter() + .map(|&index| entries[index].method as *const MethodMeta) + .collect::>(); + let needs_exact_dispatcher = previous_members.iter().any(|&index| { + let method = entries[index].method; + let Some(position) = target_group + .candidates + .iter() + .position(|candidate| std::ptr::eq(candidate.method, method)) + else { + return false; + }; + target_group.candidates[..position].iter().any(|candidate| { + !old_methods.contains(&(candidate.method as *const MethodMeta)) + && equivalent_overloads(candidate.method, method) + }) + }); + needs_exact_dispatcher.then(|| MethodGroup { + name: name.clone(), + candidates: dispatch_order(previous_members) + .into_iter() + .map(|index| Candidate { + interface: entries[index].interface, + method: entries[index].method, + attribute: attribute_of[&index].clone(), + define: false, + legacy_preservation: None, + }) + .collect(), + legacy_fallback: None, + }) + }) + .flatten(); + // Stubs keep the signatures each name declared before. + let signatures = if name == previous_key { + dispatch_order(previous_members) + } else { + previous_members + .iter() + .copied() + .filter(|&index| &entries[index].abi == name) + .collect() + }; + Alias { + name: name.clone(), + target, + dispatcher, + signatures: signatures + .into_iter() + .map(|index| entries[index].method) + .collect(), + } + }) + .collect(); + ScopePlan { + groups: plan_groups, + group_of, + aliases, + previous_attributes, + #[cfg(test)] + fallbacks: fallback + .iter() + .filter(|(fallback_scope, key)| { + *fallback_scope == scope + && entries.iter().any(|entry| { + entry.scope == scope + && &entry.clr_key == key + && &entry.abi_key != key + }) + }) + .map(|(_, key)| key.clone()) + .collect(), + } + }) + .collect() +} + +fn outputs(method: &MethodMeta) -> Vec<&TypeMeta> { + method + .params + .iter() + .filter(|param| param.direction == ParamDirection::Out) + .map(|param| ¶m.typ) + .chain(method.return_type.as_ref()) + .collect() +} + +/// Overloads that bind and behave identically, such as the same projected +/// method on two interfaces (`INumberFormatter.FormatInt` and `INumberFormatter2.FormatInt`). +fn equivalent_overloads(left: &MethodMeta, right: &MethodMeta) -> bool { + let left_params = get_in_params(left); + let right_params = get_in_params(right); + left_params.len() == right_params.len() + && left_params.iter().zip(&right_params).all(|(left, right)| { + to_snake_case(&left.name) == to_snake_case(&right.name) && left.typ == right.typ + }) + && outputs(left) == outputs(right) +} + +#[derive(Clone, Copy, PartialEq, Eq)] +enum GuardDomain { + Bool, + Number, + Enum, + Text, + Guid, + DateTime, + TimeSpan, + Struct, + Sequence, + Collection, + Object, +} + +fn guard_domain(typ: &TypeMeta) -> GuardDomain { + match typ { + TypeMeta::Bool => GuardDomain::Bool, + TypeMeta::I8 + | TypeMeta::U8 + | TypeMeta::I16 + | TypeMeta::U16 + | TypeMeta::I32 + | TypeMeta::U32 + | TypeMeta::I64 + | TypeMeta::U64 + | TypeMeta::F32 + | TypeMeta::F64 => GuardDomain::Number, + TypeMeta::Enum { .. } => GuardDomain::Enum, + TypeMeta::Char16 | TypeMeta::String => GuardDomain::Text, + TypeMeta::Guid => GuardDomain::Guid, + typ if foundation_type(typ) == Some(FoundationType::DateTime) => GuardDomain::DateTime, + typ if foundation_type(typ) == Some(FoundationType::TimeSpan) => GuardDomain::TimeSpan, + TypeMeta::Struct { .. } => GuardDomain::Struct, + TypeMeta::Array(_) => GuardDomain::Sequence, + typ if type_kind(typ).is_some() => GuardDomain::Collection, + TypeMeta::Object + | TypeMeta::Interface { .. } + | TypeMeta::RuntimeClass { .. } + | TypeMeta::Delegate { .. } + | TypeMeta::Parameterized { .. } + | TypeMeta::AsyncAction + | TypeMeta::AsyncActionWithProgress(_) + | TypeMeta::AsyncOperation(_) + | TypeMeta::AsyncOperationWithProgress(_, _) => GuardDomain::Object, + } +} + +/// Whether one value could satisfy the dispatch guards of both parameter types. +/// +/// Conservative: returns `true` unless the guards are provably disjoint. Enums +/// may use an integer guard when their type is not projected, and projected +/// objects are sequences or collections when they wrap WinRT collections. +fn guard_types_may_overlap(left: &TypeMeta, right: &TypeMeta) -> bool { + if left == right { + return true; + } + match (ireference_inner_type(left), ireference_inner_type(right)) { + (Some(_), Some(_)) => return true, + (Some(inner), None) => return guard_types_may_overlap(inner, right), + (None, Some(inner)) => return guard_types_may_overlap(left, inner), + (None, None) => {} + } + use GuardDomain::*; + match (guard_domain(left), guard_domain(right)) { + (Struct, Struct) => false, + (left, right) if left == right => true, + (Number, Enum) | (Enum, Number) => true, + (Sequence | Collection | Object, Sequence | Collection | Object) => true, + _ => false, + } +} + +/// Whether some positional call could satisfy the guards of both overloads. +fn overloads_may_overlap(left: &MethodMeta, right: &MethodMeta) -> bool { + let left_params = get_in_params(left); + let right_params = get_in_params(right); + left_params.len() == right_params.len() + && left_params + .iter() + .zip(&right_params) + .all(|(left, right)| guard_types_may_overlap(&left.typ, &right.typ)) +} + +pub(crate) fn cmp_python_dispatch_methods(left: &MethodMeta, right: &MethodMeta) -> Ordering { + cmp_python_dispatch_params(&get_in_params(left), &get_in_params(right)) + .then_with(|| left.raw_name.cmp(&right.raw_name)) + .then_with(|| left.name.cmp(&right.name)) + .then_with(|| left.vtable_index.cmp(&right.vtable_index)) +} + +pub(crate) fn cmp_python_dispatch_params(left: &[&ParamMeta], right: &[&ParamMeta]) -> Ordering { + let sort_key = |params: &[&ParamMeta]| { + params + .iter() + .map(|param| py_dispatch_type_sort_key(¶m.typ)) + .collect::>() + }; + sort_key(left).cmp(&sort_key(right)) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::meta::{ParamDirection, ParamMeta}; + use crate::types::TypeMeta; + + fn method(name: &str, vtable_index: usize, typ: TypeMeta) -> MethodMeta { + overload(name, name, vtable_index, &[("value", typ)]) + } + + fn overload( + name: &str, + raw_name: &str, + vtable_index: usize, + params: &[(&str, TypeMeta)], + ) -> MethodMeta { + MethodMeta { + name: name.into(), + raw_name: raw_name.into(), + vtable_index, + params: params + .iter() + .map(|(name, typ)| ParamMeta { + name: (*name).into(), + typ: typ.clone(), + direction: ParamDirection::In, + }) + .collect(), + return_type: Some(TypeMeta::String), + ..Default::default() + } + } + + fn interface_type(name: &str) -> TypeMeta { + TypeMeta::Interface { + namespace: "Contoso".into(), + name: name.into(), + iid: "11111111-1111-1111-1111-111111111111".into(), + } + } + + fn enumeration(name: &str) -> TypeMeta { + TypeMeta::Enum { + namespace: "Contoso".into(), + name: name.into(), + underlying: Box::new(TypeMeta::I32), + members: Vec::new(), + is_flags: false, + doc: None, + deprecated: None, + } + } + + fn interface(name: &str, methods: Vec) -> InterfaceMeta { + InterfaceMeta { + name: name.into(), + namespace: "Contoso".into(), + methods, + ..Default::default() + } + } + + /// Owned view of a scope plan for assertions. + #[derive(Debug, Default, PartialEq)] + struct Planned { + /// `(ABI name, vtable) -> (group name, attribute)`. + methods: BTreeMap<(String, usize), (String, String)>, + aliases: Vec<(String, String)>, + fallbacks: Vec, + legacy_fallbacks: BTreeMap, + } + + impl Planned { + fn group(&self, name: &str, vtable_index: usize) -> &str { + &self.methods[&(name.to_string(), vtable_index)].0 + } + + fn attribute(&self, name: &str, vtable_index: usize) -> &str { + &self.methods[&(name.to_string(), vtable_index)].1 + } + } + + fn summarize(plan: &ScopePlan<'_>) -> Planned { + Planned { + methods: plan + .groups + .iter() + .flat_map(|group| { + group.candidates.iter().map(|candidate| { + ( + (candidate.method.name.clone(), candidate.method.vtable_index), + (group.name.clone(), candidate.attribute.clone()), + ) + }) + }) + .collect(), + aliases: plan + .aliases() + .iter() + .map(|alias| (alias.name.clone(), alias.target.clone())) + .collect(), + fallbacks: plan.fallbacks().to_vec(), + legacy_fallbacks: plan + .groups + .iter() + .filter_map(|group| { + group.legacy_fallback.as_ref().map(|fallback| { + ( + group.name.clone(), + ( + fallback.method.name.clone(), + fallback.method.vtable_index, + fallback.attribute.clone(), + ), + ) + }) + }) + .collect(), + } + } + + fn plan_scope(methods: Vec, reserved: &[&str]) -> Planned { + let widget = interface("IWidget", methods); + let reserved = reserved.iter().map(|name| name.to_string()).collect(); + summarize(&plan_scopes(&[vec![&widget]], &reserved)[0]) + } + + fn aliases(pairs: &[(&str, &str)]) -> Vec<(String, String)> { + pairs + .iter() + .map(|(name, target)| (name.to_string(), target.to_string())) + .collect() + } + + #[test] + fn python_numeric_overload_method_cmp_prefers_narrower_and_signed_ranges() { + let i8 = method("Read", 6, TypeMeta::I8); + let u8 = method("Read2", 7, TypeMeta::U8); + let i16 = method("Read3", 8, TypeMeta::I16); + + assert_eq!(cmp_python_dispatch_methods(&i8, &i16), Ordering::Less); + assert_eq!(cmp_python_dispatch_methods(&i8, &u8), Ordering::Less); + } + + #[test] + fn python_numeric_overload_method_cmp_prefers_char16_integer_and_f64() { + let char16 = method("Pick", 6, TypeMeta::Char16); + let string = method("Pick2", 7, TypeMeta::String); + let int = method("Pick3", 8, TypeMeta::I32); + let f64 = method("Pick4", 9, TypeMeta::F64); + let f32 = method("Pick5", 10, TypeMeta::F32); + + assert_eq!( + cmp_python_dispatch_methods(&char16, &string), + Ordering::Less + ); + assert_eq!(cmp_python_dispatch_methods(&int, &f64), Ordering::Less); + assert_eq!(cmp_python_dispatch_methods(&f64, &f32), Ordering::Less); + } + + #[test] + fn python_overload_suffixes_merge_only_when_base_method_exists() { + let planned = plan_scope( + vec![ + method("CreateFileAsync", 6, TypeMeta::String), + method("CreateFileAsyncOverloadDefaultOptions", 7, TypeMeta::String), + method("RunEventLoopWithOptions", 8, TypeMeta::String), + ], + &[], + ); + + assert_eq!(planned.group("CreateFileAsync", 6), "create_file_async"); + assert_eq!( + planned.group("CreateFileAsyncOverloadDefaultOptions", 7), + "create_file_async" + ); + assert_eq!( + planned.group("RunEventLoopWithOptions", 8), + "run_event_loop_with_options" + ); + assert_eq!( + planned.aliases, + aliases(&[( + "create_file_async_overload_default_options", + "create_file_async" + )]), + "a former alias keeps aliasing the dispatcher" + ); + } + + #[test] + fn plan_orders_candidates_for_dispatch_and_names_private_overloads() { + let first = interface("IFirst", vec![method("Register", 6, TypeMeta::String)]); + let second = interface("ISecond", vec![method("Register", 6, TypeMeta::I32)]); + let registered = summarize(&plan_scopes(&[vec![&first, &second]], &HashSet::new())[0]); + let values = registered.methods.values().collect::>(); + assert_eq!( + values.len(), + 1, + "same ABI slot on two interfaces: {registered:?}" + ); + + let plans = plan_scopes(&[vec![&first, &second]], &HashSet::new()); + let group = &plans[0].groups[0]; + let attributes = group + .candidates + .iter() + .map(|candidate| { + ( + candidate.method.params[0].typ.clone(), + candidate.attribute.as_str(), + ) + }) + .collect::>(); + assert_eq!( + attributes, + [ + (TypeMeta::String, "_register_6_0"), + (TypeMeta::I32, "_register_6_1"), + ] + ); + + let read = plan_scope( + vec![ + method("Read2", 7, TypeMeta::F64), + method("Read", 6, TypeMeta::I8), + ], + &[], + ); + assert_eq!(read.group("Read2", 7), "read"); + assert_eq!(read.attribute("Read", 6), "_read_6"); + assert_eq!(read.attribute("Read2", 7), "_read_7"); + } + + #[test] + fn clr_name_groups_overloads_without_a_documented_python_name() { + let folder = interface_type("IStorageFolder"); + let planned = plan_scope( + vec![ + overload( + "CopyOverloadDefaultNameAndOptions", + "CopyAsync", + 8, + &[("destination_folder", folder.clone())], + ), + overload( + "CopyOverloadDefaultOptions", + "CopyAsync", + 9, + &[ + ("destination_folder", folder.clone()), + ("desired_new_name", TypeMeta::String), + ], + ), + overload( + "CopyOverload", + "CopyAsync", + 10, + &[ + ("destination_folder", folder), + ("desired_new_name", TypeMeta::String), + ("option", enumeration("NameCollisionOption")), + ], + ), + ], + &[], + ); + + for vtable_index in 8..=10 { + let name = [ + "CopyOverloadDefaultNameAndOptions", + "CopyOverloadDefaultOptions", + "CopyOverload", + ][vtable_index - 8]; + assert_eq!(planned.group(name, vtable_index), "copy_async"); + } + assert_eq!( + planned.aliases, + aliases(&[ + ("copy_overload", "_copy_async_10"), + ("copy_overload_default_name_and_options", "_copy_async_8"), + ("copy_overload_default_options", "_copy_async_9"), + ]), + "former standalone methods alias the exact implementation they called" + ); + assert!(planned.fallbacks.is_empty()); + } + + #[test] + fn clr_name_renames_single_overloads_and_keeps_the_abi_name_as_alias() { + let planned = plan_scope( + vec![overload( + "LaunchUriWithDataAsync", + "LaunchUriAsync", + 8, + &[("uri", TypeMeta::String)], + )], + &[], + ); + + assert_eq!( + planned.group("LaunchUriWithDataAsync", 8), + "launch_uri_async" + ); + assert_eq!( + planned.attribute("LaunchUriWithDataAsync", 8), + "launch_uri_async" + ); + assert_eq!( + planned.aliases, + aliases(&[("launch_uri_with_data_async", "launch_uri_async")]) + ); + } + + #[test] + fn clr_name_keeps_real_methods_that_share_an_overload_name() { + let format_int = interface( + "INumberFormatter", + vec![ + overload("FormatInt", "Format", 6, &[("value", TypeMeta::I64)]), + overload("FormatUInt", "Format", 7, &[("value", TypeMeta::U64)]), + ], + ); + let real = interface( + "INumberFormatter2", + vec![ + overload("FormatInt", "FormatInt", 6, &[("value", TypeMeta::I64)]), + overload("FormatUInt", "FormatUInt", 7, &[("value", TypeMeta::U64)]), + ], + ); + let plans = plan_scopes(&[vec![&format_int, &real]], &HashSet::new()); + let planned = summarize(&plans[0]); + + let groups = plans[0] + .groups + .iter() + .map(|group| { + ( + group.name.as_str(), + group + .candidates + .iter() + .map(|candidate| candidate.interface.name.as_str()) + .collect::>(), + ) + }) + .collect::>(); + assert_eq!( + groups, + [ + ("format", vec!["INumberFormatter", "INumberFormatter"]), + ("format_int", vec!["INumberFormatter", "INumberFormatter2"]), + ( + "format_u_int", + vec!["INumberFormatter", "INumberFormatter2"] + ), + ] + ); + for (name, canonical_attribute, real_attribute) in [ + ("format_int", "_format_6", "_format_int_6"), + ("format_u_int", "_format_7", "_format_u_int_7"), + ] { + let group = plans[0] + .groups + .iter() + .find(|group| group.name == name) + .unwrap(); + assert_eq!( + group + .candidates + .iter() + .map(|candidate| (candidate.attribute.as_str(), candidate.define)) + .collect::>(), + [(canonical_attribute, false), (real_attribute, true)] + ); + } + assert!(planned.aliases.is_empty(), "{planned:?}"); + assert!(planned.fallbacks.is_empty(), "{planned:?}"); + } + + #[test] + fn compatibility_dispatcher_keeps_the_exact_previously_selected_interface() { + let first = interface( + "IFirst", + vec![overload( + "Pick", + "Choose", + 6, + &[("value", TypeMeta::String)], + )], + ); + let second = interface( + "ISecond", + vec![overload("Pick", "Pick", 6, &[("value", TypeMeta::String)])], + ); + let plans = plan_scopes(&[vec![&first, &second]], &HashSet::new()); + let choose = plans[0] + .groups + .iter() + .find(|group| group.name == "choose") + .unwrap(); + let pick = plans[0] + .groups + .iter() + .find(|group| group.name == "pick") + .unwrap(); + + assert_eq!( + choose + .candidates + .iter() + .map(|candidate| { + ( + candidate.interface.name.as_str(), + candidate.attribute.as_str(), + candidate.define, + ) + }) + .collect::>(), + [("IFirst", "_choose_6", true)] + ); + assert_eq!( + pick.candidates + .iter() + .map(|candidate| { + ( + candidate.interface.name.as_str(), + candidate.attribute.as_str(), + candidate.define, + ) + }) + .collect::>(), + [("IFirst", "_choose_6", false), ("ISecond", "_pick_6", true),], + "pick() must still call IFirst first, while ISecond.Pick remains projected" + ); + } + + #[test] + fn compatibility_dispatcher_pins_exact_target_within_a_canonical_group() { + let first = interface( + "IFirst", + vec![overload("Foo", "Foo", 6, &[("value", TypeMeta::String)])], + ); + let second = interface( + "ISecond", + vec![overload("Bar", "Foo", 6, &[("value", TypeMeta::String)])], + ); + let plans = plan_scopes(&[vec![&first, &second]], &HashSet::new()); + let foo = plans[0] + .groups + .iter() + .find(|group| group.name == "foo") + .unwrap(); + + assert_eq!( + foo.candidates + .iter() + .map(|candidate| candidate.interface.name.as_str()) + .collect::>(), + ["IFirst", "ISecond"], + "Foo must keep the exact IFirst target first even though Bar sorts before Foo" + ); + } + + #[test] + fn clr_name_does_not_split_distinct_overloads_of_an_existing_name() { + // Same shape as INumberFormatter/INumberFormatter2, but the overloads + // differ (parameter names), so `format_int` must keep reaching both. + let clr = interface( + "IFirst", + vec![ + overload("FormatInt", "Format", 6, &[("value", TypeMeta::I64)]), + overload("FormatDouble", "Format", 7, &[("value", TypeMeta::F64)]), + ], + ); + let abi = interface( + "ISecond", + vec![overload( + "FormatInt", + "FormatInt", + 6, + &[("number", TypeMeta::I64)], + )], + ); + let plans = plan_scopes(&[vec![&clr, &abi]], &HashSet::new()); + let planned = summarize(&plans[0]); + + assert_eq!(planned.group("FormatDouble", 7), "format_double"); + let format_int = plans[0] + .groups + .iter() + .find(|group| group.name == "format_int") + .expect("format_int group"); + let interfaces = format_int + .candidates + .iter() + .map(|candidate| candidate.interface.name.as_str()) + .collect::>(); + assert_eq!(interfaces, ["IFirst", "ISecond"], "{planned:?}"); + assert_eq!(planned.fallbacks, ["format"]); + } + + #[test] + fn clr_name_grouping_keeps_existing_suffix_merges() { + let planned = plan_scope( + vec![ + overload("Read", "Read", 6, &[("value", TypeMeta::String)]), + overload("Read2", "Read2", 7, &[("value", TypeMeta::I32)]), + overload("ReadWithHint", "Read", 8, &[("value", TypeMeta::Bool)]), + ], + &[], + ); + + assert_eq!(planned.group("Read2", 7), "read"); + assert_eq!(planned.group("ReadWithHint", 8), "read"); + assert_eq!(planned.attribute("Read2", 7), "_read_7"); + assert_eq!( + planned.aliases, + aliases(&[("read2", "read"), ("read_with_hint", "_read_8")]) + ); + } + + #[test] + fn thread_pool_statics_keep_exact_callable_names_and_slots() { + let handler = TypeMeta::Delegate { + namespace: "Windows.System.Threading".into(), + name: "WorkItemHandler".into(), + iid: "1d1a8b8b-fa66-414f-9cbd-b65fc99d17fa".into(), + }; + let context = PythonProjectionContext::packaged([handler.type_identity()]).unwrap(); + let enumeration = |name: &str, underlying| TypeMeta::Enum { + namespace: "Windows.System.Threading".into(), + name: name.into(), + underlying: Box::new(underlying), + members: Vec::new(), + is_flags: name == "WorkItemOptions", + doc: None, + deprecated: None, + }; + let priority = enumeration("WorkItemPriority", TypeMeta::I32); + let options = enumeration("WorkItemOptions", TypeMeta::U32); + let mut methods = vec![ + overload("RunAsync", "RunAsync", 6, &[("handler", handler.clone())]), + overload( + "RunWithPriorityAsync", + "RunAsync", + 7, + &[("handler", handler.clone()), ("priority", priority.clone())], + ), + overload( + "RunWithPriorityAndOptionsAsync", + "RunAsync", + 8, + &[ + ("handler", handler), + ("priority", priority), + ("options", options), + ], + ), + ]; + for method in &mut methods { + method.return_type = Some(TypeMeta::AsyncAction); + } + let class = ClassMeta { + name: "ThreadPool".into(), + namespace: "Windows.System.Threading".into(), + full_name: "Windows.System.Threading.ThreadPool".into(), + static_interfaces: vec![InterfaceMeta { + name: "IThreadPoolStatics".into(), + namespace: "Windows.System.Threading".into(), + iid: "b6bf67dd-84bd-44f8-ac1c-93ebcb9dba91".into(), + methods, + ..Default::default() + }], + ..Default::default() + }; + let plan = ClassMemberPlan::new(&class, &context); + let planned = summarize(&plan.statics); + for (name, slot, public) in [ + ("RunAsync", 6, "run_async"), + ("RunWithPriorityAsync", 7, "run_with_priority_async"), + ( + "RunWithPriorityAndOptionsAsync", + 8, + "run_with_priority_and_options_async", + ), + ] { + assert_eq!(planned.group(name, slot), public, "{planned:?}"); + assert_eq!(planned.attribute(name, slot), public, "{planned:?}"); + let group = plan + .statics + .groups + .iter() + .find(|group| group.name == public) + .unwrap(); + assert_eq!(group.candidates.len(), 1); + assert!(group.legacy_fallback.is_none()); + } + assert!(planned.aliases.is_empty(), "{planned:?}"); + assert_eq!(planned.fallbacks, ["run_async"]); + + let mut unrelated = class.clone(); + unrelated.name = "OtherThreadPool".into(); + unrelated.full_name = "Windows.System.Threading.OtherThreadPool".into(); + let unrelated = summarize(&ClassMemberPlan::new(&unrelated, &context).statics); + assert_eq!(unrelated.group("RunWithPriorityAsync", 7), "run_async"); + + let mut other_interface = class.clone(); + other_interface.static_interfaces[0].iid = "11111111-1111-1111-1111-111111111111".into(); + let other_interface = summarize(&ClassMemberPlan::new(&other_interface, &context).statics); + assert_eq!( + other_interface.group("RunWithPriorityAsync", 7), + "run_async" + ); + + let mut raw_name_drift = class.clone(); + raw_name_drift.static_interfaces[0].methods[1].raw_name = "OtherAsync".into(); + assert!(class_abi_name_exceptions(&raw_name_drift).is_empty()); + let raw_name_plan = summarize(&ClassMemberPlan::new(&raw_name_drift, &context).statics); + assert_eq!( + raw_name_plan.group("RunWithPriorityAndOptionsAsync", 8), + "run_async" + ); + + let mut abi_name_drift = class.clone(); + abi_name_drift.static_interfaces[0].methods[1].name = "OtherPriorityAsync".into(); + assert!(class_abi_name_exceptions(&abi_name_drift).is_empty()); + + let mut slot_drift = class.clone(); + slot_drift.static_interfaces[0].methods[2].vtable_index = 9; + assert!(class_abi_name_exceptions(&slot_drift).is_empty()); + + let mut order_drift = class.clone(); + order_drift.static_interfaces[0].methods[2] + .params + .swap(1, 2); + assert!(class_abi_name_exceptions(&order_drift).is_empty()); + + let mut type_drift = class.clone(); + type_drift.static_interfaces[0].methods[1].params[1].typ = TypeMeta::String; + assert!(class_abi_name_exceptions(&type_drift).is_empty()); + + let mut return_drift = class.clone(); + return_drift.static_interfaces[0].methods[2].return_type = Some(TypeMeta::Object); + assert!(class_abi_name_exceptions(&return_drift).is_empty()); + } + + #[test] + fn sdk_thread_pool_exception_matches_real_method_metadata() { + const WINMD: &str = + r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd"; + if !std::path::Path::new(WINMD).is_file() { + eprintln!("Skipping: Windows.winmd not found"); + return; + } + let class = crate::meta::parse_class(WINMD, "Windows.System.Threading", "ThreadPool") + .expect("ThreadPool metadata"); + assert_eq!( + class_abi_name_exceptions(&class), + BTreeSet::from([(0, "run_async".to_string())]), + "{:#?}", + class.static_interfaces + ); + } + + #[test] + fn clr_name_falls_back_when_the_name_is_a_property_or_generated_member() { + let getter = MethodMeta { + name: "get_Source".into(), + raw_name: "get_Source".into(), + vtable_index: 8, + return_type: Some(TypeMeta::String), + is_property_getter: true, + ..Default::default() + }; + let mut reserved = HashSet::from(["close".to_string()]); + insert_accessor_names(&getter, false, &mut reserved); + let widget = interface( + "IWidget", + vec![ + overload("CloseWithStatus", "Close", 6, &[("code", TypeMeta::U16)]), + overload( + "SetSourceWithOptions", + "Source", + 7, + &[("value", TypeMeta::String)], + ), + getter, + ], + ); + let plans = plan_scopes(&[vec![&widget]], &reserved); + let planned = summarize(&plans[0]); + + assert_eq!(planned.group("CloseWithStatus", 6), "close_with_status"); + assert_eq!( + planned.group("SetSourceWithOptions", 7), + "set_source_with_options" + ); + assert!(planned.aliases.is_empty()); + assert_eq!(planned.fallbacks, ["close", "source"]); + let members = plans[0].members(widget.methods.iter().map(|method| (&widget, method))); + assert!( + matches!(members[2], PlannedMember::Accessor(_, method) if method.name == "get_Source") + ); + } + + #[test] + fn clr_name_falls_back_when_the_other_scope_owns_the_name() { + let statics = interface( + "IWidgetStatics", + vec![ + overload("CopyFromAsync", "CopyAsync", 7, &[("value", TypeMeta::I32)]), + overload("MergeWith", "Merge", 8, &[("value", TypeMeta::I32)]), + ], + ); + let instance = interface( + "IWidget", + vec![ + overload("CopyAsync", "CopyAsync", 6, &[("value", TypeMeta::String)]), + overload("Merge", "Merge", 9, &[("value", TypeMeta::String)]), + ], + ); + let plans = plan_scopes(&[vec![&statics], vec![&instance]], &HashSet::new()); + let static_plan = summarize(&plans[0]); + let instance_plan = summarize(&plans[1]); + + assert_eq!(static_plan.group("CopyFromAsync", 7), "copy_from_async"); + assert_eq!(static_plan.group("MergeWith", 8), "merge_with"); + assert_eq!(instance_plan.group("CopyAsync", 6), "copy_async"); + assert_eq!(instance_plan.group("Merge", 9), "merge"); + assert_eq!(static_plan.fallbacks, ["copy_async", "merge"]); + assert!(static_plan.aliases.is_empty()); + } + + #[test] + fn clr_name_falls_back_when_an_existing_name_would_change_meaning() { + // `CreateUpdater()` is an overload name of `CreateUpdaterForUser`, while + // `CreateUpdater(String)` is named `CreateUpdaterWithId`. + let planned = plan_scope( + vec![ + overload("CreateUpdater", "CreateUpdaterForUser", 6, &[]), + overload( + "CreateUpdaterWithId", + "CreateUpdater", + 7, + &[("id", TypeMeta::String)], + ), + ], + &[], + ); + + assert_eq!(planned.group("CreateUpdater", 6), "create_updater_for_user"); + assert_eq!( + planned.group("CreateUpdaterWithId", 7), + "create_updater_with_id" + ); + assert_eq!( + planned.aliases, + aliases(&[("create_updater", "create_updater_for_user")]) + ); + assert_eq!(planned.fallbacks, ["create_updater"]); + } + + #[test] + fn clr_name_preserves_safe_overlaps_and_falls_back_for_unknown_ones() { + let by_interface = overload("Show", "Show", 6, &[("target", interface_type("ITarget"))]); + let by_text = overload( + "ShowText", + "Show", + 8, + &[("target", TypeMeta::String), ("mode", TypeMeta::I32)], + ); + let planned = plan_scope( + vec![ + by_interface.clone(), + overload("ShowObject", "Show", 7, &[("target", TypeMeta::Object)]), + by_text.clone(), + ], + &[], + ); + assert_eq!(planned.group("Show", 6), "show"); + assert_eq!(planned.group("ShowObject", 7), "show"); + assert_eq!(planned.group("ShowText", 8), "show"); + assert_eq!( + planned.aliases, + aliases(&[("show_object", "_show_7"), ("show_text", "_show_8")]) + ); + assert!(planned.fallbacks.is_empty()); + + let planned = plan_scope( + vec![ + by_interface, + overload("ShowKind", "Show", 9, &[("target", enumeration("Kind"))]), + by_text, + ], + &[], + ); + assert_eq!(planned.group("ShowKind", 9), "show_kind"); + assert_eq!(planned.group("ShowText", 8), "show_text"); + assert!(planned.aliases.is_empty()); + assert_eq!(planned.fallbacks, ["show"]); + assert!(planned.legacy_fallbacks.is_empty()); + + let planned = plan_scope( + vec![ + overload( + "Load", + "Load", + 6, + &[("values", TypeMeta::Array(Box::new(TypeMeta::I32)))], + ), + overload("LoadObject", "Load", 7, &[("values", TypeMeta::Object)]), + ], + &[], + ); + assert_eq!(planned.group("Load", 6), "load"); + assert_eq!(planned.group("LoadObject", 7), "load_object"); + assert_eq!(planned.fallbacks, ["load"]); + } + + #[test] + fn former_dispatchers_alias_the_dispatcher_that_owns_their_overloads() { + let planned = plan_scope( + vec![ + overload( + "TryUpdatePosition", + "TryUpdatePosition", + 6, + &[("value", TypeMeta::F32)], + ), + overload( + "TryUpdatePositionWithOption", + "TryUpdatePosition", + 7, + &[ + ("value", TypeMeta::F32), + ("option", enumeration("Clamping")), + ], + ), + overload( + "TryUpdatePositionWithOption", + "TryUpdatePosition", + 8, + &[ + ("value", TypeMeta::F32), + ("option", enumeration("Clamping")), + ("update", enumeration("Update")), + ], + ), + ], + &[], + ); + + assert_eq!( + planned.group("TryUpdatePositionWithOption", 8), + "try_update_position" + ); + assert_eq!( + planned.aliases, + aliases(&[("try_update_position_with_option", "try_update_position")]) + ); + } + + #[test] + fn former_dispatcher_keeps_exact_candidates_when_canonical_target_has_a_foreign_equivalent() { + let canonical = interface( + "ICanonical", + vec![overload("Foo", "Foo", 6, &[("value", TypeMeta::String)])], + ); + let legacy_text = interface( + "ILegacyText", + vec![overload( + "FooVersion", + "Foo", + 6, + &[("value", TypeMeta::String)], + )], + ); + let legacy_int = interface( + "ILegacyInt", + vec![overload( + "FooVersion", + "Foo", + 6, + &[("value", TypeMeta::I32)], + )], + ); + let plans = plan_scopes( + &[vec![&canonical, &legacy_text, &legacy_int]], + &HashSet::new(), + ); + let alias = plans[0] + .aliases() + .iter() + .find(|alias| alias.name == "foo_version") + .unwrap(); + assert_eq!(alias.target, "foo"); + assert_eq!( + alias + .dispatcher + .as_ref() + .unwrap() + .candidates + .iter() + .map(|candidate| candidate.interface.name.as_str()) + .collect::>(), + ["ILegacyText", "ILegacyInt"] + ); + } + + #[test] + fn legacy_conversion_shadows_require_the_complete_parameter_shape() { + let context = PythonProjectionContext::default(); + let old_enum = overload( + "Open", + "Open", + 6, + &[("mode", enumeration("Mode")), ("label", TypeMeta::String)], + ); + let overlapping = overload( + "OpenText", + "Open", + 7, + &[("mode", TypeMeta::String), ("label", TypeMeta::String)], + ); + let nonoverlapping = overload( + "OpenFlag", + "Open", + 8, + &[("mode", TypeMeta::String), ("enabled", TypeMeta::Bool)], + ); + + assert_eq!( + legacy_guard_overlap(&old_enum, &overlapping, false, &context), + LegacyGuardOverlap::Preserve(vec![ + LegacyPreservationPredicate::ExactIntConversionRangeOrSubclass { + index: 0, + exact_types: "(str,)", + minimum: i32::MIN as i128, + maximum: i32::MAX as i128, + } + ]) + ); + assert_eq!( + legacy_guard_overlap(&old_enum, &nonoverlapping, false, &context), + LegacyGuardOverlap::Disjoint + ); + + for typ in [ + TypeMeta::I8, + TypeMeta::U8, + TypeMeta::I16, + TypeMeta::U16, + TypeMeta::I32, + TypeMeta::U32, + TypeMeta::I64, + TypeMeta::U64, + TypeMeta::F32, + TypeMeta::F64, + enumeration("Mode"), + TypeMeta::Struct { + namespace: "Windows.Foundation".into(), + name: "HResult".into(), + fields: Vec::new(), + }, + ] { + let old_numeric = overload("Pick", "Pick", 6, &[("value", typ)]); + let by_bool = overload("PickBool", "Pick", 7, &[("value", TypeMeta::Bool)]); + assert_eq!( + legacy_guard_overlap(&old_numeric, &by_bool, false, &context), + LegacyGuardOverlap::Preserve(Vec::new()) + ); + } + let old_char = overload("Pick", "Pick", 6, &[("value", TypeMeta::Char16)]); + let by_bool = overload("PickBool", "Pick", 7, &[("value", TypeMeta::Bool)]); + assert_eq!( + legacy_guard_overlap(&old_char, &by_bool, false, &context), + LegacyGuardOverlap::Disjoint + ); + + let old_numeric_pair = overload( + "Pick", + "Pick", + 6, + &[("value", TypeMeta::I32), ("label", TypeMeta::String)], + ); + let bool_other_parameter = overload( + "PickBool", + "Pick", + 7, + &[("value", TypeMeta::Bool), ("enabled", TypeMeta::Bool)], + ); + let bool_other_arity = overload( + "PickBool2", + "Pick", + 8, + &[ + ("value", TypeMeta::Bool), + ("label", TypeMeta::String), + ("enabled", TypeMeta::Bool), + ], + ); + assert_eq!( + legacy_guard_overlap(&old_numeric_pair, &bool_other_parameter, false, &context), + LegacyGuardOverlap::Disjoint + ); + assert_eq!( + legacy_guard_overlap(&old_numeric_pair, &bool_other_arity, false, &context), + LegacyGuardOverlap::Disjoint + ); + + let old_integer_pair = overload( + "Pick", + "Pick", + 6, + &[("first", TypeMeta::I32), ("second", TypeMeta::I32)], + ); + let bool_wide = overload( + "PickBool", + "Pick", + 7, + &[("first", TypeMeta::Bool), ("second", TypeMeta::I64)], + ); + assert_eq!( + legacy_guard_overlap(&old_integer_pair, &bool_wide, false, &context), + LegacyGuardOverlap::Preserve(vec![LegacyPreservationPredicate::IntBaseRange { + index: 1, + minimum: i32::MIN as i128, + maximum: i32::MAX as i128, + }]) + ); + } + + #[test] + fn interface_conversion_overlap_is_planned_for_strict_and_permissive_tiers() { + let old_type = TypeMeta::Interface { + namespace: "Contoso".into(), + name: "IOld".into(), + iid: "11111111-1111-1111-1111-111111111111".into(), + }; + let new_type = TypeMeta::Interface { + namespace: "Contoso".into(), + name: "INew".into(), + iid: "22222222-2222-2222-2222-222222222222".into(), + }; + let context = PythonProjectionContext::standalone([ + old_type.type_identity(), + new_type.type_identity(), + enumeration("Mode").type_identity(), + ]) + .unwrap(); + let old = overload("Use", "Use", 6, &[("value", old_type)]); + let candidate = overload("UseNew", "Use", 7, &[("value", new_type)]); + let expected = LegacyGuardOverlap::Preserve(vec![LegacyPreservationPredicate::CanCast { + index: 0, + iid: "IID_ARG_Contoso_IOld".into(), + }]); + + assert_eq!( + legacy_guard_overlap(&old, &candidate, false, &context), + expected + ); + assert_eq!( + legacy_guard_overlap(&old, &candidate, true, &context), + expected + ); + let enum_candidate = overload("UseKind", "Use", 8, &[("value", enumeration("Mode"))]); + assert_eq!( + legacy_guard_overlap(&old, &enum_candidate, false, &context), + expected + ); + } + + #[test] + fn previous_attributes_keep_pre_clr_names_for_ordering() { + let widget = interface( + "IWidget", + vec![ + overload("CreateWithName", "Create", 6, &[("name", TypeMeta::String)]), + overload("Create", "Create", 7, &[]), + ], + ); + let plan = &plan_scopes(&[vec![&widget]], &HashSet::new())[0]; + + assert_eq!(plan.attribute(&widget.methods[0]), Some("_create_6")); + assert_eq!( + plan.previous_attribute(&widget.methods[0]), + Some("create_with_name") + ); + assert_eq!(plan.previous_attribute(&widget.methods[1]), Some("create")); + } + + #[test] + fn windows_corpus_marks_every_new_dispatcher_with_its_old_standalone_method() { + use crate::codegen::winrt::python::naming::PythonProjectionContext; + use crate::meta; + use std::path::Path; + + const WINMD: &str = + r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd"; + if !Path::new(WINMD).is_file() { + eprintln!("Skipping: Windows.winmd not found"); + return; + } + + fn assert_scope(interfaces: &[&InterfaceMeta], plan: &ScopePlan<'_>) -> (usize, usize) { + let methods = interfaces + .iter() + .flat_map(|interface| interface.methods.iter()) + .filter(|method| !is_accessor(method)) + .collect::>(); + let names = methods.iter().map(|method| abi_name(method)).collect(); + let mut previous = BTreeMap::>::new(); + for method in methods { + previous + .entry(suffix_group_key(&abi_name(method), &names)) + .or_default() + .push(method); + } + + let mut count = 0; + let mut bool_shadows = 0; + for group in &plan.groups { + let Some(methods) = previous.get(&group.name) else { + continue; + }; + if methods.len() != 1 || group.candidates.len() <= 1 { + continue; + } + let fallback = group.legacy_fallback.as_ref().unwrap_or_else(|| { + panic!("{} became a dispatcher without a legacy tier", group.name) + }); + assert!( + std::ptr::eq(fallback.method, methods[0]), + "{} legacy tier changed its native method", + group.name + ); + bool_shadows += group + .candidates + .iter() + .filter(|candidate| { + candidate.legacy_preservation.is_some() + && get_in_params(candidate.method) + .iter() + .any(|param| param.typ == TypeMeta::Bool) + }) + .count(); + count += 1; + } + (count, bool_shadows) + } + + let namespaces = meta::list_namespaces(WINMD); + let mut identities = BTreeSet::new(); + for namespace in &namespaces { + identities.extend( + meta::parse_enums(WINMD, namespace) + .into_iter() + .map(|typ| typ.type_identity()), + ); + identities.extend( + meta::parse_interfaces(WINMD, namespace) + .into_iter() + .map(|interface| interface.type_identity()), + ); + identities.extend( + meta::parse_namespace(WINMD, namespace) + .into_iter() + .map(|class| { + crate::types::TypeIdentity::named( + crate::types::TypeIdentityKind::Class, + class.namespace, + class.name, + ) + }), + ); + } + let context = PythonProjectionContext::standalone(identities).unwrap(); + let mut runtime_count = 0; + let mut all_plan_sites = 0; + let mut bool_shadows = 0; + let mut compatibility_fallbacks = BTreeSet::new(); + let mut interface_fallbacks = BTreeSet::new(); + for namespace in namespaces { + for class in meta::parse_namespace(WINMD, &namespace) { + let statics = class + .factory_interfaces + .iter() + .chain(class.static_interfaces.iter()) + .collect::>(); + let instance = class_instance_interfaces(&class).collect::>(); + let plan = ClassMemberPlan::new(&class, &context); + compatibility_fallbacks.extend( + plan.statics + .fallbacks() + .iter() + .chain(plan.instance.fallbacks()) + .map(|name| format!("{}.{}.{name}", class.namespace, class.name)), + ); + + let static_count = assert_scope(&statics, &plan.statics); + let instance_count = assert_scope(&instance, &plan.instance); + let count = static_count.0 + instance_count.0; + runtime_count += count; + all_plan_sites += count; + bool_shadows += static_count.1 + instance_count.1; + for interface in &class.required_interfaces { + let interface_plan = interface_member_plan(interface, &context); + interface_fallbacks.extend(interface_plan.fallbacks().iter().map(|name| { + format!( + "{}.{}[{}].{name}", + class.namespace, class.name, interface.name + ) + })); + let count = assert_scope(&[interface], &interface_plan); + all_plan_sites += count.0; + bool_shadows += count.1; + } + } + for interface in meta::parse_interfaces(WINMD, &namespace) { + if !interface.is_delegate() { + let interface_plan = interface_member_plan(&interface, &context); + interface_fallbacks.extend( + interface_plan.fallbacks().iter().map(|name| { + format!("{}.{}.{name}", interface.namespace, interface.name) + }), + ); + let count = assert_scope(&[&interface], &interface_plan); + all_plan_sites += count.0; + bool_shadows += count.1; + } + } + } + + assert_eq!(runtime_count, 765); + assert_eq!(all_plan_sites, 896); + assert_eq!(bool_shadows, 0); + assert!(interface_fallbacks.is_empty()); + assert_eq!( + compatibility_fallbacks, + [ + "Windows.Networking.Sockets.MessageWebSocket.close", + "Windows.Networking.Sockets.ServerMessageWebSocket.close", + "Windows.Networking.Sockets.ServerStreamWebSocket.close", + "Windows.Networking.Sockets.StreamWebSocket.close", + "Windows.System.Threading.ThreadPool.run_async", + "Windows.UI.Notifications.TileUpdateManagerForUser.create_tile_updater_for_application", + ] + .into_iter() + .map(str::to_string) + .collect() + ); + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs index d5e16ec7..77201c84 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs @@ -13,9 +13,12 @@ use super::delegates::{ py_delegate_input_arg, py_event_handler_arg, py_once_callback_check, py_runtime_delegate_callable_type, py_runtime_delegate_param_type, }; -use super::naming::{PythonProjectionContext, to_snake_case}; +use super::member_plan::{Candidate, LegacyPreservation, MethodGroup}; +use super::naming::{PythonProjectionContext, PythonSupportSymbol, to_snake_case}; use super::signature::{ - py_convert_return, py_type_guard, py_wrap_arg, py_wrap_async, py_wrap_async_with_converters, + LegacyPreservationPredicate, py_convert_return, py_has_permissive_guard, + py_interface_cast_guard, py_type_guard, py_wrap_arg, py_wrap_async, + py_wrap_async_with_converters, }; use super::type_helpers::{ method_pydoc, py_factory_return_type, py_method_abi_output_count, py_method_outputs, @@ -77,6 +80,215 @@ pub(crate) fn py_method_type_guard( py_type_guard(name, typ, context) } +/// Dispatch guard for one bound argument of an overload candidate. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct ParamGuard { + /// Exact guard used by the first dispatch pass. + pub(crate) strict: String, + /// Broader guard for a second pass that runs only after every candidate's + /// strict guards failed. `None` keeps the strict guard. + pub(crate) permissive: Option, +} + +/// Guards for one overload parameter. +/// +/// Generated runtime-class wrappers do not inherit interface wrappers, so the +/// strict `isinstance` guard of a known interface parameter rejects runtime +/// class instances and raw `DynWinRTValue`s that implement the interface. Its +/// permissive guard also accepts anything that supports the interface through +/// QueryInterface. +pub(crate) fn param_guard( + name: &str, + typ: &TypeMeta, + context: &PythonProjectionContext, +) -> ParamGuard { + ParamGuard { + strict: py_method_type_guard(name, typ, context), + permissive: py_has_permissive_guard(typ, context) + .then(|| py_interface_cast_guard(name, typ, context)) + .flatten(), + } +} + +/// One candidate of a generated `*args, **kwargs` overload dispatcher. +pub(crate) struct DispatchCandidate<'a> { + pub(crate) legacy_preservation: Option<&'a LegacyPreservation>, + /// Python-visible input parameters, in call order. + pub(crate) params: Vec<&'a crate::meta::ParamMeta>, + /// Statements run when the candidate matches, relative to its `if` block. + pub(crate) body: Vec, +} + +pub(crate) struct LegacyDispatch<'a> { + pub(crate) params: Vec<&'a crate::meta::ParamMeta>, + pub(crate) target: String, + pub(crate) public_name: String, +} + +/// Emit argument binding and guards for an overload dispatcher. +/// +/// The first pass tries every candidate, in order, with its strict guards. +/// Candidates with permissive guards are retried in a second pass that runs +/// only after the first pass matched nothing, so a permissive guard can never +/// change which overload an already-matching call reaches. New candidates +/// exclude values known to remain accepted by the old guard-free conversion; +/// those reach the exact final legacy candidate instead. +pub(crate) fn emit_dispatch( + out: &mut String, + indent: &str, + candidates: &[DispatchCandidate<'_>], + legacy: Option<&LegacyDispatch<'_>>, + context: &PythonProjectionContext, +) { + let guards = candidates + .iter() + .map(|candidate| { + candidate + .params + .iter() + .enumerate() + .map(|(index, param)| param_guard(&format!("_bound[{index}]"), ¶m.typ, context)) + .collect::>() + }) + .collect::>(); + let needs_legacy_bound = candidates.iter().any(|candidate| { + candidate + .legacy_preservation + .as_ref() + .is_some_and(|plan| plan.strict.is_some() || plan.permissive.is_some()) + }); + if needs_legacy_bound { + let legacy = legacy.expect("legacy conversion shadows require a legacy candidate"); + out.push_str(&format!( + "{indent}_legacy_bound = _dynwinrt_bind_overload({}, args, kwargs)\n", + dispatch_parameter_names(&legacy.params), + )); + } + let preservation_expression = |predicates: &[LegacyPreservationPredicate]| { + std::iter::once("_legacy_bound is not None".to_string()) + .chain(predicates.iter().map(|predicate| match predicate { + LegacyPreservationPredicate::IntBaseRange { + index, + minimum, + maximum, + } => format!( + "{minimum} <= int.__index__(_legacy_bound[{index}]) <= {maximum}" + ), + LegacyPreservationPredicate::RealToIntRange { + index, + minimum, + maximum, + } => format!( + "({minimum} <= int.__index__(_legacy_bound[{index}]) <= {maximum} if isinstance(_legacy_bound[{index}], int) else type(_legacy_bound[{index}]) is not float)" + ), + LegacyPreservationPredicate::ExactIntConversionRangeOrSubclass { + index, + exact_types, + minimum, + maximum, + } => format!( + "(type(_legacy_bound[{index}]) not in {exact_types} or {}(_legacy_bound[{index}], {minimum}, {maximum}))", + context.support_symbol_reference(PythonSupportSymbol::LegacyIntGuard) + ), + LegacyPreservationPredicate::BuiltinSubclass { index, exact_types } => { + format!("type(_legacy_bound[{index}]) not in {exact_types}") + } + LegacyPreservationPredicate::Char16OrStringSubclass { index } => format!( + "(type(_legacy_bound[{index}]) is not str or (len(_legacy_bound[{index}]) == 1 and ord(_legacy_bound[{index}]) <= 65535))" + ), + LegacyPreservationPredicate::CanCast { index, iid } => { + format!( + "{}(_legacy_bound[{index}], {})", + context.support_symbol_reference(PythonSupportSymbol::CanCast), + context.argument_iid_reference(iid) + ) + } + LegacyPreservationPredicate::DynWinRTValue { index } => { + format!( + "isinstance(getattr(_legacy_bound[{index}], '_obj', _legacy_bound[{index}]), DynWinRTValue)" + ) + } + LegacyPreservationPredicate::CallableOrDynWinRTValue { index } => { + format!( + "(callable(_legacy_bound[{index}]) or isinstance(getattr(_legacy_bound[{index}], '_obj', _legacy_bound[{index}]), DynWinRTValue))" + ) + } + })) + .collect::>() + .join(" and ") + }; + for (candidate, guards) in candidates.iter().zip(&guards) { + let mut strict = guards + .iter() + .map(|guard| guard.strict.clone()) + .collect::>(); + if let Some(predicates) = candidate + .legacy_preservation + .and_then(|plan| plan.strict.as_deref()) + { + strict.insert(0, format!("not ({})", preservation_expression(predicates))); + } + emit_dispatch_candidate(out, indent, candidate, &strict); + } + for (candidate, guards) in candidates.iter().zip(&guards) { + if guards.iter().any(|guard| guard.permissive.is_some()) { + let mut permissive = guards + .iter() + .map(|guard| guard.permissive.as_ref().unwrap_or(&guard.strict).clone()) + .collect::>(); + if let Some(predicates) = candidate + .legacy_preservation + .and_then(|plan| plan.permissive.as_deref()) + { + permissive.insert(0, format!("not ({})", preservation_expression(predicates))); + } + emit_dispatch_candidate(out, indent, candidate, &permissive); + } + } + if let Some(legacy) = legacy { + out.push_str(&format!( + "{indent}return {}({}, {}, args, kwargs, '{}')\n", + context.support_symbol_reference(PythonSupportSymbol::LegacyCall), + legacy.target, + dispatch_parameter_names(&legacy.params), + legacy.public_name, + )); + } +} + +fn dispatch_parameter_names(params: &[&crate::meta::ParamMeta]) -> String { + let names = params + .iter() + .map(|param| format!("'{}'", to_snake_case(¶m.name))) + .collect::>() + .join(", "); + if names.is_empty() { + "()".to_string() + } else { + format!("({names},)") + } +} + +fn emit_dispatch_candidate( + out: &mut String, + indent: &str, + candidate: &DispatchCandidate<'_>, + guards: &[String], +) { + let parameter_names = dispatch_parameter_names(&candidate.params); + out.push_str(&format!( + "{indent}_bound = _dynwinrt_bind_overload({parameter_names}, args, kwargs)\n" + )); + let condition = std::iter::once("_bound is not None") + .chain(guards.iter().map(String::as_str)) + .collect::>() + .join(" and "); + out.push_str(&format!("{indent}if {condition}:\n")); + for line in &candidate.body { + out.push_str(&format!("{indent} {line}\n")); + } +} + fn convert_method_output(expr: &str, typ: &TypeMeta, context: &PythonProjectionContext) -> String { if let Some(converter) = delegate_value_converter(typ, context) { return format!("({converter})({expr})"); @@ -181,15 +393,6 @@ fn emit_method_result( // ====================================================================== // Method generation — Python call pattern -pub(crate) fn generate_factory_method_invoke( - class: &ClassMeta, - iface: &InterfaceMeta, - method: &MethodMeta, - context: &PythonProjectionContext, -) -> String { - generate_factory_method_invoke_named(class, iface, method, context, None) -} - fn generate_factory_method_invoke_named( class: &ClassMeta, iface: &InterfaceMeta, @@ -342,114 +545,91 @@ pub(crate) struct InstanceOverload<'a> { pub(crate) property_has_getter: bool, } -pub(crate) fn private_overload_names<'a>( - public_name: &str, - methods: impl IntoIterator, -) -> Vec { - let base_names = methods - .into_iter() - .map(|method| format!("_{public_name}_{}", method.vtable_index)) - .collect::>(); - base_names - .iter() - .enumerate() - .map(|(index, base)| { - if base_names - .iter() - .filter(|candidate| *candidate == base) - .count() - > 1 - { - format!("{base}_{index}") - } else { - base.clone() - } - }) - .collect() +/// Render an instance accessor (property or event method). +pub(crate) fn generate_instance_accessor( + overload: &InstanceOverload<'_>, + context: &PythonProjectionContext, +) -> String { + generate_method_body( + &overload.iface_var, + &overload.obj_expr, + overload.method, + context, + None, + overload.sibling_methods, + overload.property_has_getter, + ) } -pub(crate) fn generate_instance_method_group( - overloads: &[InstanceOverload<'_>], +/// Render a planned instance method group; `overload` supplies each candidate's +/// interface binding. +pub(crate) fn generate_instance_method_group<'a>( + group: &MethodGroup<'a>, + overload: impl Fn(&Candidate<'a>) -> InstanceOverload<'a>, context: &PythonProjectionContext, ) -> String { - if overloads.len() == 1 { - let overload = &overloads[0]; - return generate_method_body( - &overload.iface_var, - &overload.obj_expr, - overload.method, - context, - None, - overload.sibling_methods, - overload.property_has_getter, - ); - } - - let mut ordered_overloads = overloads.iter().collect::>(); - ordered_overloads.sort_by(|left, right| { - super::overloads::cmp_python_dispatch_methods(left.method, right.method) - }); - - let overload_names = - super::overloads::method_names(ordered_overloads.iter().map(|overload| overload.method)); - let public_name = - super::overloads::method_group_key(ordered_overloads[0].method, &overload_names); + let overloads = group + .candidates + .iter() + .map(|candidate| { + ( + overload(candidate), + candidate.attribute.as_str(), + candidate.define, + ) + }) + .collect::>(); let mut out = String::new(); - let private_names = private_overload_names( - &public_name, - ordered_overloads.iter().map(|overload| overload.method), - ); - for (overload, private_name) in ordered_overloads.iter().zip(&private_names) { - out.push_str(&generate_method_body( - &overload.iface_var, - &overload.obj_expr, - overload.method, - context, - Some(private_name), - overload.sibling_methods, - overload.property_has_getter, - )); - out.push('\n'); + let public_name = &group.name; + for (overload, attribute, define) in &overloads { + if *define { + out.push_str(&generate_method_body( + &overload.iface_var, + &overload.obj_expr, + overload.method, + context, + Some(attribute), + overload.sibling_methods, + overload.property_has_getter, + )); + } + if overloads.len() == 1 { + if *attribute != public_name { + out.push_str(&format!("\n {public_name} = {attribute}\n")); + } + return out; + } + if *define { + out.push('\n'); + } } out.push_str(&format!(" def {public_name}(self, *args, **kwargs):\n")); - let public_params = get_in_params(ordered_overloads[0].method); - out.push_str(&method_pydoc(ordered_overloads[0].method, &public_params)); - for (overload, private_name) in ordered_overloads.iter().zip(private_names) { - let in_params = get_in_params(overload.method); - let parameter_names = in_params - .iter() - .map(|param| format!("'{}'", to_snake_case(¶m.name))) - .collect::>() - .join(", "); - let parameter_names = if parameter_names.is_empty() { - "()".to_string() - } else { - format!("({parameter_names},)") - }; - out.push_str(&format!( - " _bound = _dynwinrt_bind_overload({}, args, kwargs)\n", - parameter_names - )); - let guards = in_params - .iter() - .enumerate() - .map(|(index, param)| { - py_method_type_guard(&format!("_bound[{index}]"), ¶m.typ, context) - }) - .collect::>(); - let condition = if guards.is_empty() { - "_bound is not None".to_string() - } else { - format!("_bound is not None and {}", guards.join(" and ")) - }; + let public_params = get_in_params(group.candidates[0].method); + out.push_str(&method_pydoc(group.candidates[0].method, &public_params)); + let candidates = overloads + .iter() + .zip(&group.candidates) + .map(|((overload, attribute, _), candidate)| DispatchCandidate { + legacy_preservation: candidate.legacy_preservation.as_ref(), + params: get_in_params(overload.method), + body: vec![format!("return self.{attribute}(*_bound)")], + }) + .collect::>(); + let legacy = group + .legacy_fallback + .as_ref() + .map(|fallback| LegacyDispatch { + params: get_in_params(fallback.method), + target: format!("self.{}", fallback.attribute), + public_name: public_name.clone(), + }); + emit_dispatch(&mut out, " ", &candidates, legacy.as_ref(), context); + if legacy.is_none() { out.push_str(&format!( - " if {condition}:\n return self.{private_name}(*_bound)\n" + " raise TypeError(\"No matching overload for {public_name}\")\n" )); } - out.push_str(&format!( - " raise TypeError(\"No matching overload for {public_name}\")\n" - )); out } @@ -466,102 +646,109 @@ pub(crate) struct StaticOverload<'a> { pub(crate) kind: StaticOverloadKind, } -pub(crate) fn generate_static_method_group( - overloads: &[StaticOverload<'_>], +/// Render a static accessor (property or event method). +pub(crate) fn generate_static_accessor( + overload: &StaticOverload<'_>, context: &PythonProjectionContext, ) -> String { - if overloads.len() == 1 { - let overload = &overloads[0]; - return match overload.kind { - StaticOverloadKind::Factory => generate_factory_method_invoke( - overload.class, - overload.iface, - overload.method, - context, - ), - StaticOverloadKind::Static => generate_static_method_invoke( - overload.class, - overload.iface, - overload.method, - context, - ), - }; + match overload.kind { + StaticOverloadKind::Factory => generate_factory_method_invoke_named( + overload.class, + overload.iface, + overload.method, + context, + None, + ), + StaticOverloadKind::Static => { + generate_static_method_invoke(overload.class, overload.iface, overload.method, context) + } } +} - let mut ordered_overloads = overloads.iter().collect::>(); - ordered_overloads.sort_by(|left, right| { - super::overloads::cmp_python_dispatch_methods(left.method, right.method) - }); - - let overload_names = - super::overloads::method_names(ordered_overloads.iter().map(|overload| overload.method)); - let public_name = - super::overloads::method_group_key(ordered_overloads[0].method, &overload_names); +/// Render a planned static or factory method group; `overload` supplies each +/// candidate's class binding. +pub(crate) fn generate_static_method_group<'a>( + group: &MethodGroup<'a>, + overload: impl Fn(&Candidate<'a>) -> StaticOverload<'a>, + context: &PythonProjectionContext, +) -> String { + let overloads = group + .candidates + .iter() + .map(|candidate| { + ( + overload(candidate), + candidate.attribute.as_str(), + candidate.define, + ) + }) + .collect::>(); let mut out = String::new(); - let private_names = private_overload_names( - &public_name, - ordered_overloads.iter().map(|overload| overload.method), - ); - for (overload, private_name) in ordered_overloads.iter().zip(&private_names) { - let code = match overload.kind { - StaticOverloadKind::Factory => generate_factory_method_invoke_named( - overload.class, - overload.iface, - overload.method, - context, - Some(private_name), - ), - StaticOverloadKind::Static => generate_static_method_invoke_named( - overload.class, - overload.iface, - overload.method, - context, - Some(private_name), - ), - }; - out.push_str(&code); - out.push('\n'); + let public_name = &group.name; + for (overload, attribute, define) in &overloads { + if *define { + out.push_str(&match overload.kind { + StaticOverloadKind::Factory => generate_factory_method_invoke_named( + overload.class, + overload.iface, + overload.method, + context, + Some(attribute), + ), + StaticOverloadKind::Static => generate_static_method_invoke_named( + overload.class, + overload.iface, + overload.method, + context, + Some(attribute), + ), + }); + } + if overloads.len() == 1 { + if *attribute != public_name { + out.push_str(&format!("\n {public_name} = {attribute}\n")); + } + return out; + } + if *define { + out.push('\n'); + } } out.push_str(" @staticmethod\n"); out.push_str(&format!(" def {public_name}(*args, **kwargs):\n")); - let public_params = get_in_params(ordered_overloads[0].method); - out.push_str(&method_pydoc(ordered_overloads[0].method, &public_params)); - for (overload, private_name) in ordered_overloads.iter().zip(private_names) { - let in_params = get_in_params(overload.method); - let parameter_names = in_params - .iter() - .map(|param| format!("'{}'", to_snake_case(¶m.name))) - .collect::>() - .join(", "); - let parameter_names = if parameter_names.is_empty() { - "()".to_string() - } else { - format!("({parameter_names},)") - }; - out.push_str(&format!( - " _bound = _dynwinrt_bind_overload({parameter_names}, args, kwargs)\n" - )); - let guards = in_params - .iter() - .enumerate() - .map(|(index, param)| { - py_method_type_guard(&format!("_bound[{index}]"), ¶m.typ, context) - }) - .collect::>(); - let condition = if guards.is_empty() { - "_bound is not None".to_string() - } else { - format!("_bound is not None and {}", guards.join(" and ")) - }; + let public_params = get_in_params(group.candidates[0].method); + out.push_str(&method_pydoc(group.candidates[0].method, &public_params)); + let candidates = overloads + .iter() + .zip(&group.candidates) + .map(|((overload, attribute, _), candidate)| DispatchCandidate { + legacy_preservation: candidate.legacy_preservation.as_ref(), + params: get_in_params(overload.method), + body: vec![format!( + "return {}.{attribute}(*_bound)", + context.class_name(overload.class) + )], + }) + .collect::>(); + let legacy = group + .legacy_fallback + .as_ref() + .map(|fallback| LegacyDispatch { + params: get_in_params(fallback.method), + target: format!( + "{}.{}", + context.class_name(overloads[0].0.class), + fallback.attribute + ), + public_name: public_name.clone(), + }); + emit_dispatch(&mut out, " ", &candidates, legacy.as_ref(), context); + if legacy.is_none() { out.push_str(&format!( - " if {condition}:\n return {}.{private_name}(*_bound)\n", - context.class_name(overload.class) + " raise TypeError(\"No matching overload for {public_name}\")\n" )); } - out.push_str(&format!( - " raise TypeError(\"No matching overload for {public_name}\")\n" - )); out } @@ -755,6 +942,9 @@ pub(crate) fn generate_method_body( #[cfg(test)] mod tests { use super::*; + use crate::codegen::winrt::python::member_plan::{ + ClassMemberPlan, PlannedMember, class_instance_interfaces, + }; use crate::meta::{ParamDirection, ParamMeta}; use crate::types::{TypeIdentity, TypeIdentityKind}; use std::process::Command; @@ -773,54 +963,130 @@ mod tests { } } - fn instance_overload(method: &MethodMeta) -> InstanceOverload<'_> { - InstanceOverload { - iface_var: "_IReader".into(), - obj_expr: "self._obj".into(), - method, - sibling_methods: None, - property_has_getter: true, + fn interface(name: &str, methods: &[&MethodMeta]) -> InterfaceMeta { + InterfaceMeta { + name: name.into(), + namespace: "Contoso".into(), + methods: methods.iter().map(|method| (*method).clone()).collect(), + ..Default::default() } } - #[test] - fn overloads_with_the_same_vtable_slot_get_unique_private_names() { - let first = overloaded_method("Register", 6, TypeMeta::String); - let second = overloaded_method("Register", 6, TypeMeta::I32); - let overloads = [ - InstanceOverload { - iface_var: "_IFirst".into(), + /// Plan `interfaces` as the instance members of a class and render the first + /// method group. + fn instance_group_on( + interfaces: &[(&str, &[&MethodMeta])], + context: &PythonProjectionContext, + ) -> String { + let class = ClassMeta { + name: "Reader".into(), + required_interfaces: interfaces + .iter() + .map(|(name, methods)| interface(name, methods)) + .collect(), + ..Default::default() + }; + let plan = ClassMemberPlan::new(&class, context); + let members = class_instance_interfaces(&class) + .flat_map(|iface| iface.methods.iter().map(move |method| (iface, method))); + let Some(PlannedMember::Group(group)) = plan.instance.members(members).into_iter().next() + else { + panic!("expected a method group"); + }; + generate_instance_method_group( + group, + |candidate| InstanceOverload { + iface_var: format!("_{}", candidate.interface.name), obj_expr: "self._obj".into(), - method: &first, + method: candidate.method, sibling_methods: None, property_has_getter: true, }, - InstanceOverload { - iface_var: "_ISecond".into(), - obj_expr: "self._obj".into(), - method: &second, - sibling_methods: None, - property_has_getter: true, + context, + ) + } + + fn instance_group(methods: &[&MethodMeta], context: &PythonProjectionContext) -> String { + instance_group_on(&[("IReader", methods)], context) + } + + /// Plan `methods` as the statics of `class_name` and render the first method group. + fn static_group( + class_name: &str, + methods: &[&MethodMeta], + context: &PythonProjectionContext, + ) -> String { + let class = ClassMeta { + name: class_name.into(), + static_interfaces: vec![interface("IFactoryStatics", methods)], + ..Default::default() + }; + let plan = ClassMemberPlan::new(&class, context); + let members = class + .static_interfaces + .iter() + .flat_map(|iface| iface.methods.iter().map(move |method| (iface, method))); + let Some(PlannedMember::Group(group)) = plan.statics.members(members).into_iter().next() + else { + panic!("expected a method group"); + }; + generate_static_method_group( + group, + |candidate| StaticOverload { + class: &class, + iface: candidate.interface, + method: candidate.method, + kind: StaticOverloadKind::Static, }, - ]; + context, + ) + } - let code = generate_instance_method_group(&overloads, &PythonProjectionContext::default()); + #[test] + fn overloads_with_the_same_vtable_slot_get_unique_private_names() { + let first = overloaded_method("Register", 6, TypeMeta::String); + let second = overloaded_method("Register", 6, TypeMeta::I32); + + let code = instance_group_on( + &[("IFirst", &[&first]), ("ISecond", &[&second])], + &PythonProjectionContext::default(), + ); assert_eq!(code.matches("def _register_6_").count(), 2, "{code}"); assert!(code.contains("self._register_6_0(*_bound)"), "{code}"); assert!(code.contains("self._register_6_1(*_bound)"), "{code}"); } - fn static_overload<'a>( - class: &'a ClassMeta, - iface: &'a InterfaceMeta, - method: &'a MethodMeta, - ) -> StaticOverload<'a> { - StaticOverload { - class, - iface, - method, - kind: StaticOverloadKind::Static, - } + #[test] + fn shared_single_candidate_uses_a_private_implementation() { + let method = overloaded_method("Choose", 6, TypeMeta::String); + let interface = interface("IChooser", &[&method]); + let group = MethodGroup { + name: "choose".into(), + candidates: vec![Candidate { + interface: &interface, + method: &method, + attribute: "_choose_6".into(), + define: true, + legacy_preservation: None, + }], + legacy_fallback: None, + }; + + let code = generate_instance_method_group( + &group, + |candidate| InstanceOverload { + iface_var: "_IChooser".into(), + obj_expr: "self._obj".into(), + method: candidate.method, + sibling_methods: None, + property_has_getter: true, + }, + &PythonProjectionContext::default(), + ); + + assert!(code.contains("def _choose_6(self, value: str)"), "{code}"); + assert!(code.contains("\n choose = _choose_6\n"), "{code}"); + assert!(!code.contains("def choose(self, value: str)"), "{code}"); } fn enum_type(name: &str, is_flags: bool) -> TypeMeta { @@ -1010,24 +1276,7 @@ mod tests { }], ..Default::default() }; - let overloads = vec![ - InstanceOverload { - iface_var: "_IReader".into(), - obj_expr: "self._obj".into(), - method: &first, - sibling_methods: None, - property_has_getter: true, - }, - InstanceOverload { - iface_var: "_IReader".into(), - obj_expr: "self._obj".into(), - method: &second, - sibling_methods: None, - property_has_getter: true, - }, - ]; - - let code = generate_instance_method_group(&overloads, &PythonProjectionContext::default()); + let code = instance_group(&[&first, &second], &PythonProjectionContext::default()); assert!(code.contains("def _read_6(self, value: str)")); assert!(code.contains("def _read_7(self, value: int)")); assert!(code.contains("def read(self, *args, **kwargs)")); @@ -1065,26 +1314,9 @@ mod tests { }], ..Default::default() }; - let overloads = vec![ - InstanceOverload { - iface_var: "_IRunner".into(), - obj_expr: "self._obj".into(), - method: &callback, - sibling_methods: None, - property_has_getter: true, - }, - InstanceOverload { - iface_var: "_IRunner".into(), - obj_expr: "self._obj".into(), - method: &text, - sibling_methods: None, - property_has_getter: true, - }, - ]; - let context = PythonProjectionContext::standalone([callback.params[0].typ.type_identity()]).unwrap(); - let code = generate_instance_method_group(&overloads, &context); + let code = instance_group_on(&[("IRunner", &[&callback, &text])], &context); assert!(code.contains("callable(_bound[0])")); assert!(code.contains("isinstance(_bound[0], DynWinRtDelegate)")); assert!(code.contains("_dynwinrt_delegate(handler,")); @@ -1233,14 +1465,6 @@ print(Runner().run(DynWinRtDelegate())) #[test] fn static_overloads_generate_one_dispatcher() { - let class = ClassMeta { - name: "Factory".into(), - ..Default::default() - }; - let iface = InterfaceMeta { - name: "IFactoryStatics".into(), - ..Default::default() - }; let first = MethodMeta { name: "Create".into(), raw_name: "Create".into(), @@ -1258,22 +1482,11 @@ print(Runner().run(DynWinRtDelegate())) }], ..Default::default() }; - let overloads = vec![ - StaticOverload { - class: &class, - iface: &iface, - method: &first, - kind: StaticOverloadKind::Static, - }, - StaticOverload { - class: &class, - iface: &iface, - method: &second, - kind: StaticOverloadKind::Static, - }, - ]; - - let code = generate_static_method_group(&overloads, &PythonProjectionContext::default()); + let code = static_group( + "Factory", + &[&first, &second], + &PythonProjectionContext::default(), + ); assert!(code.contains("def _create_6()")); assert!(code.contains("def _create_7(value: str)")); assert!(code.contains("def create(*args, **kwargs)")); @@ -1284,14 +1497,8 @@ print(Runner().run(DynWinRtDelegate())) let wide = overloaded_method("Read2", 7, TypeMeta::I32); let narrow = overloaded_method("Read", 6, TypeMeta::I8); - let forward = generate_instance_method_group( - &[instance_overload(&wide), instance_overload(&narrow)], - &PythonProjectionContext::default(), - ); - let reverse = generate_instance_method_group( - &[instance_overload(&narrow), instance_overload(&wide)], - &PythonProjectionContext::default(), - ); + let forward = instance_group(&[&wide, &narrow], &PythonProjectionContext::default()); + let reverse = instance_group(&[&narrow, &wide], &PythonProjectionContext::default()); assert_eq!(forward, reverse); assert_contains_in_order( @@ -1309,16 +1516,10 @@ print(Runner().run(DynWinRtDelegate())) let char16 = overloaded_method("Pick3", 8, TypeMeta::Char16); let boolean = overloaded_method("Pick2", 7, TypeMeta::Bool); let signed = overloaded_method("Pick", 6, TypeMeta::I8); - let overloads = vec![ - instance_overload(&float), - instance_overload(&unsigned), - instance_overload(&string), - instance_overload(&char16), - instance_overload(&boolean), - instance_overload(&signed), - ]; - - let code = generate_instance_method_group(&overloads, &PythonProjectionContext::default()); + let code = instance_group( + &[&float, &unsigned, &string, &char16, &boolean, &signed], + &PythonProjectionContext::default(), + ); assert_contains_in_order( &code, @@ -1351,29 +1552,17 @@ print(Runner().run(DynWinRtDelegate())) #[test] fn python_numeric_overload_static_dispatch_is_declaration_order_independent() { - let class = ClassMeta { - name: "Factory".into(), - ..Default::default() - }; - let iface = InterfaceMeta { - name: "IFactoryStatics".into(), - ..Default::default() - }; let integer = overloaded_method("Create", 6, TypeMeta::I16); let float = overloaded_method("Create2", 7, TypeMeta::F64); - let forward = generate_static_method_group( - &[ - static_overload(&class, &iface, &float), - static_overload(&class, &iface, &integer), - ], + let forward = static_group( + "Factory", + &[&float, &integer], &PythonProjectionContext::default(), ); - let reverse = generate_static_method_group( - &[ - static_overload(&class, &iface, &integer), - static_overload(&class, &iface, &float), - ], + let reverse = static_group( + "Factory", + &[&integer, &float], &PythonProjectionContext::default(), ); @@ -1393,14 +1582,8 @@ print(Runner().run(DynWinRtDelegate())) PythonProjectionContext::standalone([enum_type("Mode", false).type_identity()]) .unwrap(); - let forward = generate_instance_method_group( - &[instance_overload(&integer), instance_overload(&enumeration)], - &context, - ); - let reverse = generate_instance_method_group( - &[instance_overload(&enumeration), instance_overload(&integer)], - &context, - ); + let forward = instance_group(&[&integer, &enumeration], &context); + let reverse = instance_group(&[&enumeration, &integer], &context); let forward_dispatcher = extract_generated_block(&forward, " def read(self, *args, **kwargs):\n"); @@ -1477,33 +1660,8 @@ print(json.dumps([exercise(ReaderForward), exercise(ReaderReverse)])) let context = PythonProjectionContext::standalone([enum_type("Options", true).type_identity()]) .unwrap(); - let iface = InterfaceMeta { - name: "IFactoryStatics".into(), - ..Default::default() - }; - let class_forward = ClassMeta { - name: "FactoryForward".into(), - ..Default::default() - }; - let class_reverse = ClassMeta { - name: "FactoryReverse".into(), - ..Default::default() - }; - - let forward = generate_static_method_group( - &[ - static_overload(&class_forward, &iface, &integer), - static_overload(&class_forward, &iface, &flags), - ], - &context, - ); - let reverse = generate_static_method_group( - &[ - static_overload(&class_reverse, &iface, &flags), - static_overload(&class_reverse, &iface, &integer), - ], - &context, - ); + let forward = static_group("FactoryForward", &[&integer, &flags], &context); + let reverse = static_group("FactoryReverse", &[&flags, &integer], &context); let forward_dispatcher = extract_generated_block( &forward, @@ -1580,4 +1738,208 @@ print(json.dumps([exercise(FactoryForward), exercise(FactoryReverse)])) r#"[["enum", "i32", "TypeError"], ["enum", "i32", "TypeError"]]"# ); } + + fn interface_type(name: &str, iid: &str) -> TypeMeta { + TypeMeta::Interface { + namespace: "Contoso".into(), + name: name.into(), + iid: iid.into(), + } + } + + fn widget_type() -> TypeMeta { + TypeMeta::RuntimeClass { + namespace: "Contoso".into(), + name: "Widget".into(), + default_interface: Some(Box::new(interface_type( + "IWidget", + "22222222-2222-2222-2222-222222222222", + ))), + } + } + + #[test] + fn interface_overloads_accept_query_interface_only_after_exact_guards_fail() { + let foo = interface_type("IFoo", "11111111-1111-1111-1111-111111111111"); + let by_interface = overloaded_method("Write", 6, foo.clone()); + let by_class = overloaded_method("Write2", 7, widget_type()); + let by_text = overloaded_method("Write3", 8, TypeMeta::String); + let context = PythonProjectionContext::standalone([ + foo.type_identity(), + widget_type().type_identity(), + ]) + .unwrap(); + + let code = instance_group(&[&by_text, &by_class, &by_interface], &context); + let exact_interface = "if _bound is not None and isinstance(_bound[0], _dynwinrt_symbol('contoso__i_foo', 'IFoo')):"; + let relaxed_interface = "if _bound is not None and (isinstance(_bound[0], _dynwinrt_symbol('contoso__i_foo', 'IFoo')) or _dynwinrt_can_cast(_bound[0], IID_ARG_Contoso_IFoo)):"; + assert_eq!(code.matches(exact_interface).count(), 1, "{code}"); + assert_eq!(code.matches(relaxed_interface).count(), 1, "{code}"); + assert_eq!( + code.matches("_dynwinrt_can_cast(_bound[0], IID_ARG_Contoso_Widget)") + .count(), + 1, + "only interface candidates are retried: {code}" + ); + for first_pass in [ + "isinstance(_bound[0], str):", + exact_interface, + "_dynwinrt_can_cast(_bound[0], IID_ARG_Contoso_Widget):", + ] { + assert_contains_in_order(&code, first_pass, relaxed_interface); + } + + let dispatcher = extract_generated_block(&code, " def write(self, *args, **kwargs):\n"); + let script = format!( + r#"import json + +class DynWinRTValue: + def __init__(self, *interfaces): + self.interfaces = set(interfaces) + + def cast(self, iid): + if iid not in self.interfaces: + raise OSError('E_NOINTERFACE') + return DynWinRTValue(*self.interfaces) + + def release(self): + pass + +IID_ARG_Contoso_IFoo = 'IFoo' +IID_ARG_Contoso_Widget = 'IWidget' + +def _dynwinrt_bind_overload(parameter_names, args, kwargs): + if len(args) > len(parameter_names): + return None + bound = list(args) + for name in parameter_names[len(args):]: + if name not in kwargs: + return None + bound.append(kwargs[name]) + if len(kwargs) != len(parameter_names) - len(args): + return None + return tuple(bound) + +def _dynwinrt_can_cast(value, iid): + raw = getattr(value, '_obj', value) + if not isinstance(raw, DynWinRTValue): + return False + try: + projected = raw.cast(iid) + except OSError: + return False + projected.release() + return True + +def _dynwinrt_symbol(module, name): + return globals()[name] + +class IFoo: + def __init__(self, obj): + self._obj = obj + +class Widget: + def __init__(self, obj): + self._obj = obj + +class PythonFoo(IFoo): + def __init__(self): + pass + +class Writer: + def _write_6(self, value): + return "interface" + + def _write_7(self, value): + return "runtime class" + + def _write_8(self, value): + return "text" + +{dispatcher} + +writer = Writer() +results = [ + writer.write(Widget(DynWinRTValue('IWidget', 'IFoo'))), + writer.write(IFoo(DynWinRTValue('IFoo'))), + writer.write(PythonFoo()), + writer.write(Widget(DynWinRTValue('IFoo'))), + writer.write(DynWinRTValue('IFoo')), + writer.write(value=DynWinRTValue('IFoo')), + writer.write('text'), +] +for rejected in (DynWinRTValue(), object(), None): + try: + writer.write(rejected) + except TypeError: + results.append("TypeError") + else: + results.append("unexpected") +print(json.dumps(results)) +"# + ); + + assert_eq!( + run_python(&script), + r#"["runtime class", "interface", "interface", "interface", "interface", "interface", "text", "TypeError", "TypeError", "TypeError"]"# + ); + } + + #[test] + fn static_interface_overloads_retry_with_query_interface() { + let foo = interface_type("IFoo", "11111111-1111-1111-1111-111111111111"); + let by_interface = overloaded_method("Create", 6, foo.clone()); + let by_text = overloaded_method("Create2", 7, TypeMeta::String); + let context = PythonProjectionContext::standalone([foo.type_identity()]).unwrap(); + + let code = static_group("Factory", &[&by_interface, &by_text], &context); + + assert_contains_in_order( + &code, + "if _bound is not None and isinstance(_bound[0], str):", + "if _bound is not None and (isinstance(_bound[0], _dynwinrt_symbol('contoso__i_foo', 'IFoo')) or _dynwinrt_can_cast(_bound[0], IID_ARG_Contoso_IFoo)):\n return Factory._create_6(*_bound)\n", + ); + } + + #[test] + fn param_guards_are_permissive_only_for_known_interfaces() { + let foo = interface_type("IFoo", "11111111-1111-1111-1111-111111111111"); + let handler = interface_type("Handler", "33333333-3333-3333-3333-333333333333"); + let context = PythonProjectionContext::standalone([ + foo.type_identity(), + widget_type().type_identity(), + handler + .type_identity() + .with_kind(TypeIdentityKind::Delegate), + ]) + .unwrap(); + + let guard = param_guard("value", &foo, &context); + assert_eq!( + guard.strict, + "isinstance(value, _dynwinrt_symbol('contoso__i_foo', 'IFoo'))" + ); + assert_eq!( + guard.permissive.as_deref(), + Some( + "(isinstance(value, _dynwinrt_symbol('contoso__i_foo', 'IFoo')) or _dynwinrt_can_cast(value, IID_ARG_Contoso_IFoo))" + ) + ); + for strict_only in [ + widget_type(), + handler, + TypeMeta::String, + TypeMeta::Object, + interface_type( + "IUnknownToProjection", + "44444444-4444-4444-4444-444444444444", + ), + ] { + assert_eq!( + param_guard("value", &strict_only, &context).permissive, + None, + "{strict_only:?}" + ); + } + } } diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs index e4065102..3f529291 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs @@ -6,10 +6,10 @@ pub(crate) mod delegates; mod docs; mod generator; mod implementation; +pub(crate) mod member_plan; pub(crate) mod method; pub(crate) mod naming; mod native_types; -pub(crate) mod overloads; mod shared; pub(crate) mod signature; pub(crate) mod structs; diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs index 955e5704..c8a70071 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs @@ -19,6 +19,9 @@ pub type PythonTypeIdentity = TypeIdentity; pub(crate) enum PythonSupportSymbol { ObjectInput, AsInterface, + CanCast, + LegacyCall, + LegacyIntGuard, } impl PythonSupportSymbol { @@ -26,10 +29,23 @@ impl PythonSupportSymbol { match self { Self::ObjectInput => "_DynWinRTObject", Self::AsInterface => "_dynwinrt_as_interface", + Self::CanCast => "_dynwinrt_can_cast", + Self::LegacyCall => "_dynwinrt_legacy_call", + Self::LegacyIntGuard => "_dynwinrt_legacy_int_guard", } } } +fn argument_iid_names<'a>(methods: impl IntoIterator) -> BTreeSet { + let mut constants = Vec::new(); + for method in methods { + for param in crate::codegen::winrt::shared::imports::get_in_params(method) { + super::signature::py_collect_argument_iid_consts(¶m.typ, &mut constants); + } + } + constants.into_iter().map(|(name, _)| name).collect() +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) enum PythonSymbol { Type, @@ -519,6 +535,7 @@ pub struct PythonProjectionContext { implementation_helpers: BTreeMap>, module_symbols: HashMap<(PythonTypeIdentity, PythonSymbol), String>, module_support_symbols: HashMap, + module_argument_iids: HashMap, // Shared by module contexts, which clone the projection state. delegate_invokes: Arc>, } @@ -646,6 +663,7 @@ impl PythonProjectionContext { implementation_helpers: BTreeMap::new(), module_symbols: HashMap::new(), module_support_symbols: HashMap::new(), + module_argument_iids: HashMap::new(), delegate_invokes: Arc::default(), }) } @@ -749,7 +767,23 @@ impl PythonProjectionContext { .has_default_activation() .then(|| (owner.clone(), PythonSymbol::ActivationFactoryRegistration)), ); - self.with_local_types(Some(owner.clone()), structs, imports, registrations) + let argument_iids = argument_iid_names( + class + .all_interfaces() + .chain(class.overridable_interfaces.iter()) + .flat_map(|interface| interface.methods.iter()), + ); + self.with_local_types_and_iids( + Some(owner.clone()), + structs, + imports, + registrations, + argument_iids, + class + .all_interfaces() + .chain(class.overridable_interfaces.iter()) + .map(InterfaceMeta::type_identity), + ) } pub(super) fn for_interface_module( @@ -766,12 +800,14 @@ impl PythonProjectionContext { std::slice::from_ref(&delegate.invoke), )); } - self.with_local_types( + self.with_local_types_and_iids( Some(interface.type_identity()), structs, self.imported_type_symbols(collect_iface_type_imports_by_identity(interface), generics), (!interface.is_delegate()) .then(|| (interface.type_identity(), PythonSymbol::Registration)), + argument_iid_names(interface.methods.iter()), + [interface.type_identity()], ) } @@ -797,6 +833,18 @@ impl PythonProjectionContext { structs: &[TypeMeta], imports: impl IntoIterator, registrations: impl IntoIterator, + ) -> Cow<'_, Self> { + self.with_local_types_and_iids(owner, structs, imports, registrations, [], []) + } + + fn with_local_types_and_iids( + &self, + owner: Option, + structs: &[TypeMeta], + imports: impl IntoIterator, + registrations: impl IntoIterator, + argument_iids: impl IntoIterator, + iid_owners: impl IntoIterator, ) -> Cow<'_, Self> { let declarations = owner .iter() @@ -917,10 +965,16 @@ impl PythonProjectionContext { .insert((identity.clone(), *role), name); } } + for identity in iid_owners { + reserved.insert(format!("IID_{}", context.reference_name(&identity))); + } // Support imports yield to metadata declarations and their allocated roles. for helper in [ PythonSupportSymbol::ObjectInput, PythonSupportSymbol::AsInterface, + PythonSupportSymbol::CanCast, + PythonSupportSymbol::LegacyCall, + PythonSupportSymbol::LegacyIntGuard, ] { let preferred = helper.name(); let mut name = preferred.to_string(); @@ -933,6 +987,20 @@ impl PythonProjectionContext { context.to_mut().module_support_symbols.insert(helper, name); } } + for preferred in argument_iids { + let mut name = preferred.clone(); + let mut index = 2; + while !reserved.insert(name.clone()) { + name = format!("{preferred}_{index}"); + index += 1; + } + if name != preferred { + context + .to_mut() + .module_argument_iids + .insert(preferred, name); + } + } context } @@ -952,6 +1020,12 @@ impl PythonProjectionContext { } } + pub(crate) fn argument_iid_reference<'a>(&'a self, preferred: &'a str) -> &'a str { + self.module_argument_iids + .get(preferred) + .map_or(preferred, String::as_str) + } + pub(crate) fn declaration_name(&self, identity: &PythonTypeIdentity) -> String { if identity.kind() == Some(TypeIdentityKind::Struct) { legacy_projected_name(identity) @@ -1470,6 +1544,9 @@ mod tests { let others = [ PythonSupportSymbol::ObjectInput, PythonSupportSymbol::AsInterface, + PythonSupportSymbol::CanCast, + PythonSupportSymbol::LegacyCall, + PythonSupportSymbol::LegacyIntGuard, ] .into_iter() .filter(|other| *other != helper) @@ -1530,6 +1607,40 @@ mod tests { assert_support_helper_yields_to_visible_roles(PythonSupportSymbol::AsInterface); } + #[test] + fn overload_helpers_yield_to_visible_roles_without_renaming_metadata() { + for helper in [ + PythonSupportSymbol::CanCast, + PythonSupportSymbol::LegacyCall, + PythonSupportSymbol::LegacyIntGuard, + ] { + assert_support_helper_yields_to_visible_roles(helper); + } + } + + #[test] + fn argument_iid_constants_yield_to_metadata_declarations_and_imports() { + let preferred = "IID_ARG_Audit_IFoo"; + let owner = TypeIdentity::named(TypeIdentityKind::Class, "Audit", preferred); + let peer = TypeIdentity::named(TypeIdentityKind::Enum, "Audit", format!("{preferred}_2")); + let context = PythonProjectionContext::packaged([owner.clone(), peer.clone()]).unwrap(); + let module = context.with_local_types_and_iids( + Some(owner.clone()), + &[], + [(peer.clone(), PythonSymbol::Type)], + [], + [preferred.to_string()], + [], + ); + assert_eq!(module.reference_name(&owner), preferred); + assert_eq!(module.reference_name(&peer), format!("{preferred}_2")); + assert_eq!( + module.argument_iid_reference(preferred), + format!("{preferred}_3") + ); + assert_eq!(context.argument_iid_reference(preferred), preferred); + } + #[test] fn companion_aliases_freeze_roles_and_use_only_visible_symbols() { let owner = TypeIdentity::named(TypeIdentityKind::Class, "Audit", "Widget"); diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/overloads.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/overloads.rs deleted file mode 100644 index a0bd7990..00000000 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/overloads.rs +++ /dev/null @@ -1,184 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use crate::codegen::winrt::shared::imports::get_in_params; -use crate::meta::{MethodMeta, ParamMeta}; -use std::cmp::Ordering; -use std::collections::{BTreeMap, HashSet}; - -use super::naming::to_snake_case; -use super::signature::py_dispatch_type_sort_key; - -pub(crate) fn grouped_methods<'a>( - methods: impl IntoIterator, -) -> Vec> { - let methods = methods.into_iter().collect::>(); - let names = method_names(methods.iter().copied()); - let mut groups: Vec<(String, Vec<&MethodMeta>)> = Vec::new(); - for method in methods { - let key = method_group_key(method, &names); - if let Some((_, group)) = groups.iter_mut().find(|(name, _)| name == &key) { - group.push(method); - } else { - groups.push((key, vec![method])); - } - } - groups.into_iter().map(|(_, methods)| methods).collect() -} - -pub(crate) fn method_names<'a>( - methods: impl IntoIterator, -) -> HashSet { - methods - .into_iter() - .filter(|method| { - !method.is_property_getter - && !method.is_property_setter - && !method.is_event_add - && !method.is_event_remove - }) - .map(|method| to_snake_case(&method.name)) - .collect() -} - -pub(crate) fn method_group_key(method: &MethodMeta, names: &HashSet) -> String { - if method.is_property_getter - || method.is_property_setter - || method.is_event_add - || method.is_event_remove - { - format!("{}#{}", method.name, method.vtable_index) - } else { - let name = to_snake_case(&method.name); - let mut candidates = Vec::new(); - if let Some((base, _)) = name.split_once("_overload") { - candidates.push(base); - } - if let Some(base) = name.strip_suffix("_with_options") { - candidates.push(base); - } - let numeric_base = name.trim_end_matches(|character: char| character.is_ascii_digit()); - if numeric_base.len() < name.len() { - candidates.push(numeric_base); - } - candidates - .into_iter() - .find(|base| !base.is_empty() && names.contains(*base)) - .map(str::to_string) - .unwrap_or(name) - } -} - -pub(crate) fn compatibility_aliases<'a>( - methods: impl IntoIterator, -) -> Vec<(String, String)> { - let methods = methods.into_iter().collect::>(); - let names = method_names(methods.iter().copied()); - let canonical_names = methods - .iter() - .map(|method| method_group_key(method, &names)) - .collect::>(); - let mut aliases = BTreeMap::new(); - for method in methods { - if method.is_property_getter - || method.is_property_setter - || method.is_event_add - || method.is_event_remove - { - continue; - } - let legacy = to_snake_case(&method.name); - let canonical = method_group_key(method, &names); - if legacy != canonical && !canonical_names.contains(&legacy) { - aliases.entry(legacy).or_insert(canonical); - } - } - aliases.into_iter().collect() -} - -pub(crate) fn cmp_python_dispatch_methods(left: &MethodMeta, right: &MethodMeta) -> Ordering { - cmp_python_dispatch_params(&get_in_params(left), &get_in_params(right)) - .then_with(|| left.raw_name.cmp(&right.raw_name)) - .then_with(|| left.name.cmp(&right.name)) - .then_with(|| left.vtable_index.cmp(&right.vtable_index)) -} - -pub(crate) fn cmp_python_dispatch_params(left: &[&ParamMeta], right: &[&ParamMeta]) -> Ordering { - let sort_key = |params: &[&ParamMeta]| { - params - .iter() - .map(|param| py_dispatch_type_sort_key(¶m.typ)) - .collect::>() - }; - sort_key(left).cmp(&sort_key(right)) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::meta::{ParamDirection, ParamMeta}; - use crate::types::TypeMeta; - - fn method(name: &str, vtable_index: usize, typ: TypeMeta) -> MethodMeta { - MethodMeta { - name: name.into(), - raw_name: name.into(), - vtable_index, - params: vec![ParamMeta { - name: "value".into(), - typ, - direction: ParamDirection::In, - }], - ..Default::default() - } - } - - #[test] - fn python_numeric_overload_method_cmp_prefers_narrower_and_signed_ranges() { - let i8 = method("Read", 6, TypeMeta::I8); - let u8 = method("Read2", 7, TypeMeta::U8); - let i16 = method("Read3", 8, TypeMeta::I16); - - assert_eq!(cmp_python_dispatch_methods(&i8, &i16), Ordering::Less); - assert_eq!(cmp_python_dispatch_methods(&i8, &u8), Ordering::Less); - } - - #[test] - fn python_numeric_overload_method_cmp_prefers_char16_integer_and_f64() { - let char16 = method("Pick", 6, TypeMeta::Char16); - let string = method("Pick2", 7, TypeMeta::String); - let int = method("Pick3", 8, TypeMeta::I32); - let f64 = method("Pick4", 9, TypeMeta::F64); - let f32 = method("Pick5", 10, TypeMeta::F32); - - assert_eq!( - cmp_python_dispatch_methods(&char16, &string), - Ordering::Less - ); - assert_eq!(cmp_python_dispatch_methods(&int, &f64), Ordering::Less); - assert_eq!(cmp_python_dispatch_methods(&f64, &f32), Ordering::Less); - } - - #[test] - fn python_overload_suffixes_merge_only_when_base_method_exists() { - let base = method("CreateFileAsync", 6, TypeMeta::String); - let default = method("CreateFileAsyncOverloadDefaultOptions", 7, TypeMeta::String); - let unrelated = method("RunEventLoopWithOptions", 8, TypeMeta::String); - let methods = [&base, &default, &unrelated]; - let names = method_names(methods); - - assert_eq!(method_group_key(&base, &names), "create_file_async"); - assert_eq!(method_group_key(&default, &names), "create_file_async"); - assert_eq!( - method_group_key(&unrelated, &names), - "run_event_loop_with_options" - ); - assert_eq!( - compatibility_aliases(methods), - vec![( - "create_file_async_overload_default_options".into(), - "create_file_async".into(), - )] - ); - } -} diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs index 9bbb5ba6..d4003050 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs @@ -6,10 +6,10 @@ use crate::meta::{InterfaceMeta, MethodMeta, ParamDirection}; use crate::types::{TypeIdentity, TypeIdentityKind, TypeMeta}; -use super::naming::{PythonProjectionContext, PythonSymbol}; +use super::naming::{PythonProjectionContext, PythonSupportSymbol, PythonSymbol}; use crate::codegen::winrt::python::collections::{CollectionKind, is_mapping_input, type_kind}; use crate::codegen::winrt::python::native_types::{FoundationType, foundation_type}; -use crate::codegen::winrt::shared::imports::ireference_inner_type; +use crate::codegen::winrt::shared::imports::{get_in_params, ireference_inner_type}; pub(crate) fn py_runtime_symbol( context: &PythonProjectionContext, @@ -77,6 +77,531 @@ pub(crate) fn py_integer_bounds(typ: &TypeMeta) -> Option<(i128, i128)> { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum LegacyPreservationPredicate { + /// The candidate accepts builtin ints over a wider range. Exact ints must + /// fit the old converter; subclasses stay on the old path without probing + /// potentially visible conversion methods. + IntBaseRange { + index: usize, + minimum: i128, + maximum: i128, + }, + /// The candidate accepts builtin ints and floats. Only exact builtin + /// values are preflighted; subclasses conservatively stay on the old path. + RealToIntRange { + index: usize, + minimum: i128, + maximum: i128, + }, + /// The old enum converter calls `int()`. Parse only exact builtin strings + /// or numeric values; subclasses conservatively stay on the old path. + ExactIntConversionRangeOrSubclass { + index: usize, + exact_types: &'static str, + minimum: i128, + maximum: i128, + }, + BuiltinSubclass { + index: usize, + exact_types: &'static str, + }, + Char16OrStringSubclass { + index: usize, + }, + CanCast { + index: usize, + iid: String, + }, + DynWinRTValue { + index: usize, + }, + CallableOrDynWinRTValue { + index: usize, + }, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) enum LegacyGuardOverlap { + Disjoint, + Preserve(Vec), + /// The overlap cannot be checked without invoking user conversion code or + /// consuming an input. The planner must retain the old ABI grouping. + Unknown, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum LegacyConversionDomain { + Bool, + Integer { minimum: i128, maximum: i128 }, + Real, + EnumInteger { minimum: i128, maximum: i128 }, + Char16, + String, + Guid, + DateTime, + TimeSpan, + Struct(TypeIdentity), + QueryInterface(String), + DynWinRTValue, + Delegate, + Collection, + Unknown, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum CandidateGuardDomain { + Bool, + Integer { minimum: i128, maximum: i128 }, + Real, + KnownEnum { minimum: i128, maximum: i128 }, + Char16, + String, + Guid, + DateTime, + TimeSpan, + Struct(TypeIdentity), + QueryInterface(String), + DynWinRTValue, + Delegate, + Collection, + Unknown, +} + +fn enum_bounds(typ: &TypeMeta) -> Option<(i128, i128)> { + let TypeMeta::Enum { underlying, .. } = typ else { + return None; + }; + py_integer_bounds(underlying) +} + +fn hresult(typ: &TypeMeta) -> bool { + matches!(typ, TypeMeta::Struct { name, .. } if name == "HResult") +} + +fn legacy_conversion_domain( + typ: &TypeMeta, + context: &PythonProjectionContext, +) -> LegacyConversionDomain { + if ireference_inner_type(typ).is_some() { + return LegacyConversionDomain::Unknown; + } + if type_kind(typ).is_some() { + return LegacyConversionDomain::Collection; + } + if let Some((minimum, maximum)) = py_integer_bounds(typ) { + return LegacyConversionDomain::Integer { minimum, maximum }; + } + if hresult(typ) { + return LegacyConversionDomain::Integer { + minimum: i32::MIN as i128, + maximum: i32::MAX as i128, + }; + } + match typ { + TypeMeta::Bool => LegacyConversionDomain::Bool, + TypeMeta::F32 | TypeMeta::F64 => LegacyConversionDomain::Real, + TypeMeta::Enum { .. } => enum_bounds(typ) + .map_or(LegacyConversionDomain::Unknown, |(minimum, maximum)| { + LegacyConversionDomain::EnumInteger { minimum, maximum } + }), + TypeMeta::Char16 => LegacyConversionDomain::Char16, + TypeMeta::String => LegacyConversionDomain::String, + TypeMeta::Guid => LegacyConversionDomain::Guid, + typ if foundation_type(typ) == Some(FoundationType::DateTime) => { + LegacyConversionDomain::DateTime + } + typ if foundation_type(typ) == Some(FoundationType::TimeSpan) => { + LegacyConversionDomain::TimeSpan + } + TypeMeta::Struct { .. } => LegacyConversionDomain::Struct(typ.type_identity()), + typ @ TypeMeta::RuntimeClass { .. } => py_runtime_class_iid_const(typ) + .map(|(iid, _)| LegacyConversionDomain::QueryInterface(iid)) + .unwrap_or(LegacyConversionDomain::Unknown), + typ @ TypeMeta::Interface { .. } => py_interface_iid_const(typ) + .map(|(iid, _)| LegacyConversionDomain::QueryInterface(iid)) + .unwrap_or(LegacyConversionDomain::Unknown), + TypeMeta::Delegate { .. } if context.is_delegate_type(typ) => { + LegacyConversionDomain::Delegate + } + TypeMeta::Object | TypeMeta::Delegate { .. } | TypeMeta::Parameterized { .. } => { + LegacyConversionDomain::DynWinRTValue + } + _ => LegacyConversionDomain::Unknown, + } +} + +fn candidate_guard_domain( + typ: &TypeMeta, + permissive: bool, + context: &PythonProjectionContext, +) -> CandidateGuardDomain { + if ireference_inner_type(typ).is_some() { + return CandidateGuardDomain::Unknown; + } + if type_kind(typ).is_some() { + return CandidateGuardDomain::Collection; + } + if context.is_delegate_type(typ) { + return CandidateGuardDomain::Delegate; + } + if let Some((minimum, maximum)) = py_integer_bounds(typ) { + return CandidateGuardDomain::Integer { minimum, maximum }; + } + if hresult(typ) { + return CandidateGuardDomain::Integer { + minimum: i32::MIN as i128, + maximum: i32::MAX as i128, + }; + } + match typ { + TypeMeta::Bool => CandidateGuardDomain::Bool, + TypeMeta::F32 | TypeMeta::F64 => CandidateGuardDomain::Real, + TypeMeta::Enum { .. } if context.is_known_type(typ) => enum_bounds(typ) + .map_or(CandidateGuardDomain::Unknown, |(minimum, maximum)| { + CandidateGuardDomain::KnownEnum { minimum, maximum } + }), + TypeMeta::Enum { .. } => CandidateGuardDomain::Integer { + minimum: i128::MIN, + maximum: i128::MAX, + }, + TypeMeta::Char16 => CandidateGuardDomain::Char16, + TypeMeta::String => CandidateGuardDomain::String, + TypeMeta::Guid => CandidateGuardDomain::Guid, + typ if foundation_type(typ) == Some(FoundationType::DateTime) => { + CandidateGuardDomain::DateTime + } + typ if foundation_type(typ) == Some(FoundationType::TimeSpan) => { + CandidateGuardDomain::TimeSpan + } + TypeMeta::Struct { .. } => CandidateGuardDomain::Struct(typ.type_identity()), + typ @ TypeMeta::RuntimeClass { .. } => py_runtime_class_iid_const(typ) + .map(|(iid, _)| CandidateGuardDomain::QueryInterface(iid)) + .unwrap_or(CandidateGuardDomain::DynWinRTValue), + typ @ TypeMeta::Interface { .. } if permissive => py_interface_iid_const(typ) + .map(|(iid, _)| CandidateGuardDomain::QueryInterface(iid)) + .unwrap_or(CandidateGuardDomain::DynWinRTValue), + TypeMeta::Interface { .. } => CandidateGuardDomain::DynWinRTValue, + TypeMeta::Object | TypeMeta::Delegate { .. } | TypeMeta::Parameterized { .. } => { + CandidateGuardDomain::DynWinRTValue + } + _ => CandidateGuardDomain::Unknown, + } +} + +fn overlap_parameter( + legacy: &LegacyConversionDomain, + candidate: &CandidateGuardDomain, + index: usize, +) -> LegacyGuardOverlap { + use CandidateGuardDomain as Guard; + use LegacyConversionDomain as Legacy; + use LegacyGuardOverlap::{Disjoint, Preserve, Unknown}; + use LegacyPreservationPredicate as Predicate; + + match (legacy, candidate) { + (Legacy::Bool, Guard::Bool) => Preserve(Vec::new()), + (Legacy::Bool, Guard::Integer { .. } | Guard::Real | Guard::KnownEnum { .. }) => Disjoint, + (Legacy::Integer { .. }, Guard::Bool) => Preserve(Vec::new()), + ( + Legacy::Integer { minimum, maximum }, + Guard::Integer { + minimum: candidate_minimum, + maximum: candidate_maximum, + }, + ) if candidate_minimum >= minimum && candidate_maximum <= maximum => Preserve(Vec::new()), + (Legacy::Integer { minimum, maximum }, Guard::Integer { .. }) => { + Preserve(vec![Predicate::IntBaseRange { + index, + minimum: *minimum, + maximum: *maximum, + }]) + } + (Legacy::Integer { minimum, maximum }, Guard::Real) => { + Preserve(vec![Predicate::RealToIntRange { + index, + minimum: *minimum, + maximum: *maximum, + }]) + } + ( + Legacy::Integer { minimum, maximum }, + Guard::KnownEnum { + minimum: candidate_minimum, + maximum: candidate_maximum, + }, + ) if candidate_minimum >= minimum && candidate_maximum <= maximum => Preserve(Vec::new()), + (Legacy::Integer { minimum, maximum }, Guard::KnownEnum { .. }) => { + Preserve(vec![Predicate::IntBaseRange { + index, + minimum: *minimum, + maximum: *maximum, + }]) + } + (Legacy::Integer { .. }, Guard::Char16 | Guard::String) => { + Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(str,)", + }]) + } + (Legacy::Integer { .. }, Guard::Guid) => Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(UUID,)", + }]), + (Legacy::Integer { .. }, Guard::DateTime) => Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(datetime,)", + }]), + (Legacy::Integer { .. }, Guard::TimeSpan) => Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(timedelta,)", + }]), + (Legacy::Integer { .. }, Guard::Struct(_)) => Unknown, + (Legacy::Real, Guard::Bool | Guard::Integer { .. } | Guard::KnownEnum { .. }) => { + Preserve(Vec::new()) + } + (Legacy::Real, Guard::Real) => Unknown, + (Legacy::Real, Guard::Char16 | Guard::String) => { + Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(str,)", + }]) + } + (Legacy::Real, Guard::Guid) => Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(UUID,)", + }]), + (Legacy::Real, Guard::DateTime) => Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(datetime,)", + }]), + (Legacy::Real, Guard::TimeSpan) => Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(timedelta,)", + }]), + (Legacy::Real, Guard::Struct(_)) => Unknown, + (Legacy::EnumInteger { minimum, maximum }, Guard::Bool) => { + if *minimum <= 0 && *maximum >= 1 { + Preserve(Vec::new()) + } else { + Preserve(vec![Predicate::IntBaseRange { + index, + minimum: *minimum, + maximum: *maximum, + }]) + } + } + ( + Legacy::EnumInteger { minimum, maximum }, + Guard::Integer { + minimum: candidate_minimum, + maximum: candidate_maximum, + }, + ) if candidate_minimum >= minimum && candidate_maximum <= maximum => Preserve(Vec::new()), + (Legacy::EnumInteger { minimum, maximum }, Guard::Integer { .. }) => { + Preserve(vec![Predicate::IntBaseRange { + index, + minimum: *minimum, + maximum: *maximum, + }]) + } + (Legacy::EnumInteger { minimum, maximum }, Guard::Real) => { + Preserve(vec![Predicate::ExactIntConversionRangeOrSubclass { + index, + exact_types: "(int, float)", + minimum: *minimum, + maximum: *maximum, + }]) + } + (Legacy::EnumInteger { minimum, maximum }, Guard::String | Guard::Char16) => { + Preserve(vec![Predicate::ExactIntConversionRangeOrSubclass { + index, + exact_types: "(str,)", + minimum: *minimum, + maximum: *maximum, + }]) + } + ( + Legacy::EnumInteger { minimum, maximum }, + Guard::KnownEnum { + minimum: candidate_minimum, + maximum: candidate_maximum, + }, + ) if candidate_minimum >= minimum && candidate_maximum <= maximum => Preserve(Vec::new()), + (Legacy::EnumInteger { minimum, maximum }, Guard::KnownEnum { .. }) => { + Preserve(vec![Predicate::IntBaseRange { + index, + minimum: *minimum, + maximum: *maximum, + }]) + } + (Legacy::EnumInteger { .. }, Guard::Guid) => Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(UUID,)", + }]), + (Legacy::EnumInteger { .. }, Guard::DateTime) => { + Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(datetime,)", + }]) + } + (Legacy::EnumInteger { .. }, Guard::TimeSpan) => { + Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(timedelta,)", + }]) + } + (Legacy::EnumInteger { .. }, Guard::Struct(_)) => Unknown, + (Legacy::Char16, Guard::Char16) => Preserve(Vec::new()), + (Legacy::Char16, Guard::String) => { + Preserve(vec![Predicate::Char16OrStringSubclass { index }]) + } + ( + Legacy::Char16, + Guard::Bool + | Guard::Integer { .. } + | Guard::Real + | Guard::KnownEnum { .. } + | Guard::Guid + | Guard::DateTime + | Guard::TimeSpan, + ) => Disjoint, + (Legacy::Char16, Guard::Struct(_)) => Unknown, + (Legacy::String, Guard::String | Guard::Char16) => Preserve(Vec::new()), + ( + Legacy::String, + Guard::Bool + | Guard::Integer { .. } + | Guard::Real + | Guard::KnownEnum { .. } + | Guard::Guid + | Guard::DateTime + | Guard::TimeSpan, + ) => Disjoint, + (Legacy::String, Guard::Struct(_)) => Unknown, + (Legacy::Guid, Guard::Guid) => Preserve(Vec::new()), + (Legacy::DateTime, Guard::DateTime) => Preserve(Vec::new()), + (Legacy::TimeSpan, Guard::TimeSpan) => Preserve(Vec::new()), + (Legacy::Struct(left), Guard::Struct(right)) if left == right => Preserve(Vec::new()), + (Legacy::Struct(_), Guard::Bool) => Disjoint, + (Legacy::Struct(_), Guard::Integer { .. }) => Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(int,)", + }]), + (Legacy::Struct(_), Guard::Real) => Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(int, float)", + }]), + (Legacy::Struct(_), Guard::Char16 | Guard::String) => { + Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(str,)", + }]) + } + (Legacy::Struct(_), Guard::Guid) => Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(UUID,)", + }]), + (Legacy::Struct(_), Guard::DateTime) => Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(datetime,)", + }]), + (Legacy::Struct(_), Guard::TimeSpan) => Preserve(vec![Predicate::BuiltinSubclass { + index, + exact_types: "(timedelta,)", + }]), + (Legacy::Struct(_), _) | (_, Guard::Struct(_)) => Unknown, + ( + Legacy::Guid | Legacy::DateTime | Legacy::TimeSpan, + Guard::Bool + | Guard::Integer { .. } + | Guard::Real + | Guard::KnownEnum { .. } + | Guard::Char16 + | Guard::String + | Guard::Guid + | Guard::DateTime + | Guard::TimeSpan, + ) => Disjoint, + (Legacy::QueryInterface(left), Guard::QueryInterface(right)) if left == right => { + Preserve(Vec::new()) + } + ( + Legacy::QueryInterface(iid), + Guard::QueryInterface(_) | Guard::DynWinRTValue | Guard::KnownEnum { .. }, + ) => Preserve(vec![Predicate::CanCast { + index, + iid: iid.clone(), + }]), + (Legacy::QueryInterface(_), Guard::Bool) => Disjoint, + (Legacy::QueryInterface(_), _) => Unknown, + (Legacy::DynWinRTValue, Guard::DynWinRTValue | Guard::QueryInterface(_)) => { + Preserve(vec![Predicate::DynWinRTValue { index }]) + } + (Legacy::DynWinRTValue, Guard::Bool) => Disjoint, + (Legacy::DynWinRTValue, _) => Unknown, + (Legacy::Delegate, Guard::Delegate) => Preserve(Vec::new()), + (Legacy::Delegate, Guard::DynWinRTValue | Guard::QueryInterface(_)) => { + Preserve(vec![Predicate::CallableOrDynWinRTValue { index }]) + } + (Legacy::Delegate | Legacy::Collection, Guard::Bool) => Disjoint, + (Legacy::Delegate, _) => Unknown, + ( + Legacy::Bool + | Legacy::Integer { .. } + | Legacy::Real + | Legacy::EnumInteger { .. } + | Legacy::Char16 + | Legacy::String + | Legacy::Guid + | Legacy::DateTime + | Legacy::TimeSpan, + Guard::QueryInterface(_) | Guard::DynWinRTValue | Guard::Delegate | Guard::Collection, + ) => Unknown, + ( + Legacy::Collection, + Guard::Integer { .. } + | Guard::Real + | Guard::KnownEnum { .. } + | Guard::Char16 + | Guard::String + | Guard::Guid + | Guard::DateTime + | Guard::TimeSpan, + ) => Unknown, + _ => Unknown, + } +} + +pub(crate) fn legacy_guard_overlap( + legacy: &MethodMeta, + candidate: &MethodMeta, + permissive: bool, + context: &PythonProjectionContext, +) -> LegacyGuardOverlap { + let legacy_params = get_in_params(legacy); + let candidate_params = get_in_params(candidate); + if legacy_params.len() != candidate_params.len() { + return LegacyGuardOverlap::Disjoint; + } + let mut predicates = Vec::new(); + for (index, (legacy, candidate)) in legacy_params.iter().zip(candidate_params).enumerate() { + if legacy.typ == candidate.typ { + continue; + } + match overlap_parameter( + &legacy_conversion_domain(&legacy.typ, context), + &candidate_guard_domain(&candidate.typ, permissive, context), + index, + ) { + LegacyGuardOverlap::Disjoint => return LegacyGuardOverlap::Disjoint, + LegacyGuardOverlap::Unknown => return LegacyGuardOverlap::Unknown, + LegacyGuardOverlap::Preserve(mut parameter) => predicates.append(&mut parameter), + } + } + LegacyGuardOverlap::Preserve(predicates) +} + /// Return a stable overload-dispatch sort key for a projected Python argument type. /// /// Python overload dispatch is branch-ordered, so same-arity branches need a @@ -126,6 +651,20 @@ pub(crate) fn py_dispatch_type_sort_key(typ: &TypeMeta) -> (u8, u16, u8, u8, Str // Python type expression // ====================================================================== +fn py_argument_iid_const_name(namespace: &str, name: &str) -> String { + let qualified = format!("{}_{}", namespace, name) + .chars() + .map(|character| { + if character.is_ascii_alphanumeric() { + character + } else { + '_' + } + }) + .collect::(); + format!("IID_ARG_{}", qualified) +} + pub(crate) fn py_runtime_class_iid_const(typ: &TypeMeta) -> Option<(String, String)> { let TypeMeta::RuntimeClass { namespace, @@ -141,17 +680,32 @@ pub(crate) fn py_runtime_class_iid_const(typ: &TypeMeta) -> Option<(String, Stri if iid.is_empty() { return None; } - let qualified = format!("{}_{}", namespace, name) - .chars() - .map(|character| { - if character.is_ascii_alphanumeric() { - character - } else { - '_' - } - }) - .collect::(); - Some((format!("IID_ARG_{}", qualified), iid.clone())) + Some((py_argument_iid_const_name(namespace, name), iid.clone())) +} + +/// Module-level IID constant used to QueryInterface-check an interface-typed argument. +pub(crate) fn py_interface_iid_const(typ: &TypeMeta) -> Option<(String, String)> { + let TypeMeta::Interface { + namespace, + name, + iid, + } = typ + else { + return None; + }; + if iid.is_empty() { + return None; + } + Some((py_argument_iid_const_name(namespace, name), iid.clone())) +} + +/// Collect the module-level IID constants needed by an input parameter: runtime-class +/// casts (including collection elements) and interface-parameter dispatch guards. +pub(crate) fn py_collect_argument_iid_consts(typ: &TypeMeta, output: &mut Vec<(String, String)>) { + if let Some(value) = py_interface_iid_const(typ) { + output.push(value); + } + py_collect_runtime_class_iid_consts(typ, output); } pub(crate) fn py_collect_runtime_class_iid_consts( @@ -180,10 +734,10 @@ pub(crate) fn py_collect_runtime_class_iid_consts( } } -fn py_runtime_class_wrap(name: &str, typ: &TypeMeta) -> String { +fn py_runtime_class_wrap(name: &str, typ: &TypeMeta, context: &PythonProjectionContext) -> String { let raw = format!("getattr({}, '_obj', {})", name, name); py_runtime_class_iid_const(typ) - .map(|(iid, _)| format!("{}.cast({})", raw, iid)) + .map(|(iid, _)| format!("{}.cast({})", raw, context.argument_iid_reference(&iid))) .unwrap_or(raw) } @@ -390,7 +944,7 @@ pub(crate) fn py_wrap_arg(name: &str, typ: &TypeMeta, context: &PythonProjection TypeMeta::F32 => format!("DynWinRTValue.from_f32({})", name), TypeMeta::F64 => format!("DynWinRTValue.from_f64({})", name), TypeMeta::Guid => format!("DynWinRTValue.from_guid(_dynwinrt_guid({}))", name), - TypeMeta::RuntimeClass { .. } => py_runtime_class_wrap(name, typ), + TypeMeta::RuntimeClass { .. } => py_runtime_class_wrap(name, typ, context), TypeMeta::Object | TypeMeta::Interface { .. } | TypeMeta::Delegate { .. } => { format!("getattr({}, '_obj', {})", name, name) } @@ -457,7 +1011,7 @@ pub(crate) fn py_wrap_native_value( context.struct_symbol(typ, PythonSymbol::PrivatePack), name ), - TypeMeta::RuntimeClass { .. } => py_runtime_class_wrap(name, typ), + TypeMeta::RuntimeClass { .. } => py_runtime_class_wrap(name, typ, context), TypeMeta::Object | TypeMeta::Interface { .. } | TypeMeta::Parameterized { .. } @@ -595,7 +1149,11 @@ pub(crate) fn py_type_guard( ), typ @ TypeMeta::RuntimeClass { .. } if py_runtime_class_iid_const(typ).is_some() => { let (iid, _) = py_runtime_class_iid_const(typ).expect("checked above"); - format!("_dynwinrt_can_cast({name}, {iid})") + format!( + "{}({name}, {})", + context.support_symbol_reference(PythonSupportSymbol::CanCast), + context.argument_iid_reference(&iid) + ) } TypeMeta::RuntimeClass { namespace, @@ -636,6 +1194,36 @@ pub(crate) fn py_type_guard( } } +/// QueryInterface-based guard for a known interface-typed overload parameter. +/// +/// Generated runtime-class wrappers do not inherit interface wrapper classes, so the +/// exact `isinstance` guard from [`py_type_guard`] rejects runtime-class objects and raw +/// `DynWinRTValue`s that implement the interface. This relaxed guard keeps the exact +/// check (for interface wrappers and Python implementations) and also accepts any +/// object that supports the interface through QueryInterface. Returns `None` when the +/// parameter keeps its exact guard. +pub(crate) fn py_interface_cast_guard( + name: &str, + typ: &TypeMeta, + context: &PythonProjectionContext, +) -> Option { + if !matches!(typ, TypeMeta::Interface { .. }) || !context.is_known_type(typ) { + return None; + } + + let (iid, _) = py_interface_iid_const(typ)?; + Some(format!( + "({} or {}({name}, {}))", + py_type_guard(name, typ, context), + context.support_symbol_reference(PythonSupportSymbol::CanCast), + context.argument_iid_reference(&iid) + )) +} + +pub(crate) fn py_has_permissive_guard(typ: &TypeMeta, context: &PythonProjectionContext) -> bool { + !context.is_delegate_type(typ) && py_interface_cast_guard("value", typ, context).is_some() +} + /// Convert a Python return expression, given the raw `.call()` result expression. pub(crate) fn py_convert_return( expr: &str, @@ -950,6 +1538,52 @@ mod tests { ); } + #[test] + fn known_interface_inputs_get_a_query_interface_dispatch_guard() { + let stream = TypeMeta::Interface { + namespace: "Windows.Storage.Streams".into(), + name: "IOutputStream".into(), + iid: "905a0fe6-bc53-11df-8c49-001e4fc686da".into(), + }; + let context = PythonProjectionContext::packaged([stream.type_identity()]).unwrap(); + let exact = "isinstance(value, _dynwinrt_symbol('windows__storage__streams__i_output_stream', 'IOutputStream'))"; + + assert_eq!(py_type_guard("value", &stream, &context), exact); + assert_eq!( + py_interface_cast_guard("value", &stream, &context), + Some(format!( + "({exact} or _dynwinrt_can_cast(value, IID_ARG_Windows_Storage_Streams_IOutputStream))" + )) + ); + assert_eq!( + py_interface_cast_guard("value", &stream, &PythonProjectionContext::default()), + None, + "unknown interfaces keep their permissive native-object guard" + ); + assert_eq!( + py_interface_cast_guard("value", &geometry_type(), &context), + None, + "runtime classes already use a QueryInterface guard" + ); + let mut constants = Vec::new(); + py_collect_argument_iid_consts(&stream, &mut constants); + py_collect_argument_iid_consts( + &TypeMeta::Interface { + namespace: "Contoso".into(), + name: "IUnnamed".into(), + iid: String::new(), + }, + &mut constants, + ); + assert_eq!( + constants, + vec![( + "IID_ARG_Windows_Storage_Streams_IOutputStream".into(), + "905a0fe6-bc53-11df-8c49-001e4fc686da".into(), + )] + ); + } + #[test] fn python_numeric_overload_integer_guards_use_exact_ranges() { let context = PythonProjectionContext::default(); diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs index b34a08ed..f0831427 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs @@ -28,6 +28,10 @@ use super::collections::{ CollectionKind, abc_name, class_interface, interface_kind, observable_vector_identity, projected_interface_kind, }; +use super::member_plan::{ + ClassMemberPlan, MethodGroup, PlannedMember, ScopePlan, class_instance_interfaces, + interface_member_plan, +}; use super::naming::{PythonProjectionContext, PythonSupportSymbol, is_py_reserved, to_snake_case}; use super::native_types::foundation_type; use super::shared::reorder_getters_before_setters; @@ -567,37 +571,46 @@ pub fn generate_interface_stub(context: &PythonProjectionContext, iface: &Interf )); } - for methods in super::overloads::grouped_methods(reorder_getters_before_setters(&iface.methods)) - { - let event_has_remove = methods.first().is_some_and(|method| { - method.name.strip_prefix("add_").is_some_and(|suffix| { - iface - .methods - .iter() - .any(|candidate| candidate.name == format!("remove_{suffix}")) - }) - }); - let property_has_getter = methods.first().is_none_or(|method| { - !method.is_property_setter - || method.name.strip_prefix("put_").is_some_and(|suffix| { + let plan = interface_member_plan(iface, context); + let members = reorder_getters_before_setters(&iface.methods) + .into_iter() + .map(|method| (iface, method)); + for member in plan.members(members) { + out.push('\n'); + out.push_str(&match member { + PlannedMember::Accessor(_, method) => { + let event_has_remove = method.name.strip_prefix("add_").is_some_and(|suffix| { iface .methods .iter() - .any(|candidate| candidate.name == format!("get_{suffix}")) - }) + .any(|candidate| candidate.name == format!("remove_{suffix}")) + }); + let property_has_getter = !method.is_property_setter + || method.name.strip_prefix("put_").is_some_and(|suffix| { + iface + .methods + .iter() + .any(|candidate| candidate.name == format!("get_{suffix}")) + }); + emit_method_stub( + method, + context, + 4, + event_has_remove, + property_has_getter, + collection_kind == Some(CollectionKind::MutableSequence), + ) + } + PlannedMember::Group(group) => emit_instance_stub_group( + group, + context, + 4, + collection_kind == Some(CollectionKind::MutableSequence), + ), }); - out.push('\n'); - out.push_str(&emit_instance_stub_group( - &methods, - context, - 4, - event_has_remove, - property_has_getter, - collection_kind == Some(CollectionKind::MutableSequence), - )); } out.push_str(&emit_instance_compatibility_alias_stubs( - iface.methods.iter(), + &plan, context, 4, collection_kind == Some(CollectionKind::MutableSequence), @@ -607,9 +620,9 @@ pub fn generate_interface_stub(context: &PythonProjectionContext, iface: &Interf } /// Generate a `.pyi` stub for a runtime class. -pub fn generate_class_stub( +pub fn generate_class_stub<'a>( context: &PythonProjectionContext, - class: &ClassMeta, + class: &'a ClassMeta, shared_iids: &HashSet, ) -> String { let used_structs = collect_used_structs_from_class_and_callbacks(class); @@ -636,6 +649,7 @@ pub fn generate_class_stub( let projectable = super::has_projectable_default_interface(class); let native_projectable = super::has_native_projector(class); let supports_interface_projection = projectable || !class.required_interfaces.is_empty(); + let plan = ClassMemberPlan::new(class, context); let mut out = String::new(); out.push_str(HEADER); @@ -844,8 +858,14 @@ pub fn generate_class_stub( )), _ => None, }); - let mut instance_stub_body = - emit_class_instance_stubs(class, context, collection_iface, false, has_closable); + let mut instance_stub_body = emit_class_instance_stubs( + class, + context, + &plan.instance, + collection_iface, + false, + has_closable, + ); if crate::codegen::winrt::is_buffer_class(&class.namespace, &class.name) { instance_stub_body.push_str(" def to_bytes(self) -> bytes: ...\n"); } @@ -916,6 +936,7 @@ pub fn generate_class_stub( out.push_str(&emit_class_instance_stubs( class, context, + &plan.instance, collection_iface, collection_kind == Some(CollectionKind::MutableSequence), has_closable, @@ -944,28 +965,29 @@ pub fn generate_class_stub( )); } - let static_methods = class + let static_members = class .factory_interfaces .iter() - .flat_map(|iface| iface.methods.iter().map(|method| (method, true))) - .chain( - class - .static_interfaces - .iter() - .flat_map(|iface| iface.methods.iter().map(|method| (method, false))), - ) - .collect::>(); - for group in grouped_static_stubs(&static_methods) { + .chain(class.static_interfaces.iter()) + .flat_map(|iface| iface.methods.iter().map(move |method| (iface, method))); + for member in plan.statics.members(static_members) { out.push('\n'); - out.push_str(&emit_static_stub_group( - &context.class_name(class), - &group, - context, - )); + out.push_str(&match member { + PlannedMember::Accessor(iface, method) => emit_static_method_stub( + &context.class_name(class), + method, + context, + is_factory_interface(class, iface), + ), + PlannedMember::Group(group) => { + emit_static_stub_group(&context.class_name(class), class, group, context) + } + }); } out.push_str(&emit_static_compatibility_alias_stubs( &context.class_name(class), - static_methods.iter().copied(), + class, + &plan.statics, context, 4, )); @@ -1054,38 +1076,46 @@ pub fn generate_class_stub( out.push_str( " def as_interface(self, interface_class: _DynWinRTProjector[_InterfaceT]) -> _InterfaceT: ...\n", ); - for methods in - super::overloads::grouped_methods(reorder_getters_before_setters(&req_iface.methods)) - { - let event_has_remove = methods.first().is_some_and(|method| { - method.name.strip_prefix("add_").is_some_and(|suffix| { - req_iface - .methods - .iter() - .any(|candidate| candidate.name == format!("remove_{suffix}")) - }) - }); - let property_has_getter = methods.first().is_none_or(|method| { - !method.is_property_setter - || method.name.strip_prefix("put_").is_some_and(|suffix| { + let iface_plan = interface_member_plan(req_iface, context); + let members = reorder_getters_before_setters(&req_iface.methods) + .into_iter() + .map(|method| (req_iface, method)); + for member in iface_plan.members(members) { + out.push('\n'); + out.push_str(&match member { + PlannedMember::Accessor(_, method) => { + let event_has_remove = method.name.strip_prefix("add_").is_some_and(|suffix| { req_iface .methods .iter() - .any(|candidate| candidate.name == format!("get_{suffix}")) - }) + .any(|candidate| candidate.name == format!("remove_{suffix}")) + }); + let property_has_getter = !method.is_property_setter + || method.name.strip_prefix("put_").is_some_and(|suffix| { + req_iface + .methods + .iter() + .any(|candidate| candidate.name == format!("get_{suffix}")) + }); + emit_method_stub( + method, + context, + 4, + event_has_remove, + property_has_getter, + interface_kind(req_iface) == Some(CollectionKind::MutableSequence), + ) + } + PlannedMember::Group(group) => emit_instance_stub_group( + group, + context, + 4, + interface_kind(req_iface) == Some(CollectionKind::MutableSequence), + ), }); - out.push('\n'); - out.push_str(&emit_instance_stub_group( - &methods, - context, - 4, - event_has_remove, - property_has_getter, - interface_kind(req_iface) == Some(CollectionKind::MutableSequence), - )); } out.push_str(&emit_instance_compatibility_alias_stubs( - req_iface.methods.iter(), + &iface_plan, context, 4, interface_kind(req_iface) == Some(CollectionKind::MutableSequence), @@ -1096,9 +1126,10 @@ pub fn generate_class_stub( out } -fn emit_class_instance_stubs( - class: &ClassMeta, +fn emit_class_instance_stubs<'a>( + class: &'a ClassMeta, context: &PythonProjectionContext, + plan: &ScopePlan<'a>, collection_iface: Option<&InterfaceMeta>, mutable_sequence_override: bool, has_closable: bool, @@ -1111,12 +1142,7 @@ fn emit_class_instance_stubs( out.push_str(&collection_protocol_stubs(collection_iface, context, 4)); } - let instance_ifaces = class - .default_interface - .iter() - .chain(class.required_interfaces.iter()) - .filter(|iface| iface.iid != "30d5a829-7fa4-4026-83bb-d75bae4ea99e") - .collect::>(); + let instance_ifaces = class_instance_interfaces(class).collect::>(); let paired_events = instance_ifaces .iter() .flat_map(|iface| { @@ -1139,12 +1165,16 @@ fn emit_class_instance_stubs( .collect::>(); let original_instance_methods = instance_ifaces .iter() - .flat_map(|iface| reorder_getters_before_setters(&iface.methods)) + .flat_map(|iface| { + reorder_getters_before_setters(&iface.methods) + .into_iter() + .map(move |method| (*iface, method)) + }) .collect::>(); let mut emitted = HashSet::<*const MethodMeta>::new(); let mut instance_methods = Vec::with_capacity(original_instance_methods.len()); - for method in &original_instance_methods { - if emitted.contains(&(*method as *const MethodMeta)) { + for &(iface, method) in &original_instance_methods { + if emitted.contains(&(method as *const MethodMeta)) { continue; } if method.is_property_setter @@ -1156,53 +1186,54 @@ fn emit_class_instance_stubs( continue; } - instance_methods.push(*method); - emitted.insert(*method as *const MethodMeta); + instance_methods.push((iface, method)); + emitted.insert(method as *const MethodMeta); if let Some(suffix) = method .is_property_getter .then(|| method.name.strip_prefix("get_")) .flatten() { - for setter in original_instance_methods - .iter() - .copied() - .filter(|candidate| { + for &(setter_iface, setter) in + original_instance_methods.iter().filter(|(_, candidate)| { candidate.is_property_setter && candidate.name.strip_prefix("put_") == Some(suffix) }) { if emitted.insert(setter as *const MethodMeta) { - instance_methods.push(setter); + instance_methods.push((setter_iface, setter)); } } } } - for methods in super::overloads::grouped_methods(instance_methods) { - let event_has_remove = methods.first().is_some_and(|method| { - method - .name - .strip_prefix("add_") - .is_some_and(|suffix| paired_events.contains(suffix)) - }); - let property_has_getter = methods.first().is_none_or(|method| { - !method.is_property_setter - || method + for member in plan.members(instance_methods) { + out.push('\n'); + out.push_str(&match member { + PlannedMember::Accessor(_, method) => { + let event_has_remove = method .name - .strip_prefix("put_") - .is_some_and(|suffix| property_getters.contains(suffix)) + .strip_prefix("add_") + .is_some_and(|suffix| paired_events.contains(suffix)); + let property_has_getter = !method.is_property_setter + || method + .name + .strip_prefix("put_") + .is_some_and(|suffix| property_getters.contains(suffix)); + emit_method_stub( + method, + context, + 4, + event_has_remove, + property_has_getter, + mutable_sequence_override, + ) + } + PlannedMember::Group(group) => { + emit_instance_stub_group(group, context, 4, mutable_sequence_override) + } }); - out.push('\n'); - out.push_str(&emit_instance_stub_group( - &methods, - context, - 4, - event_has_remove, - property_has_getter, - mutable_sequence_override, - )); } out.push_str(&emit_instance_compatibility_alias_stubs( - original_instance_methods.iter().copied(), + plan, context, 4, mutable_sequence_override, @@ -1420,7 +1451,10 @@ fn emit_constructor_stubs(class: &ClassMeta, context: &PythonProjectionContext) out.push_str(" def __init__(self, _not_constructible: NoReturn) -> None: ...\n"); return out; } - overloads.sort_by(|left, right| super::overloads::cmp_python_dispatch_params(left, right)); + overloads.sort_by(|left, right| super::member_plan::cmp_python_dispatch_params(left, right)); + let mut signatures = HashSet::new(); + overloads + .retain(|params| signatures.insert(super::type_helpers::py_param_list(params, context))); let count = overloads.len(); for params in &overloads { @@ -1496,161 +1530,215 @@ fn has_constructor_stub_overload(class: &ClassMeta) -> bool { }) } +/// Keep one declaration per distinct full Python signature. When parameter +/// signatures are identical but return types differ, keep both declarations +/// and mark the later one so strict type checkers accept it. +fn typed_signatures<'m>( + methods: impl IntoIterator, + return_type: impl Fn(&MethodMeta) -> String, + context: &PythonProjectionContext, +) -> Vec<(&'m MethodMeta, bool)> { + let mut signatures = HashSet::new(); + let mut parameter_signatures = HashSet::new(); + methods + .into_iter() + .filter_map(|method| { + let parameters = super::type_helpers::py_param_list( + &crate::codegen::winrt::shared::imports::get_in_params(method), + context, + ); + signatures + .insert((parameters.clone(), return_type(method))) + .then(|| (method, !parameter_signatures.insert(parameters))) + }) + .collect() +} + +fn ignore_unreachable_overload(mut stub: String) -> String { + let definition = stub.find("def ").expect("method stub has a definition"); + let line_end = definition + + stub[definition..] + .find('\n') + .unwrap_or(stub.len() - definition); + let line = &stub[definition..line_end]; + if let Some(ignore) = line.find("# type: ignore[") { + let close = line[ignore..] + .find(']') + .map(|offset| definition + ignore + offset) + .expect("type ignore has a closing bracket"); + stub.insert_str(close, ", overload-cannot-match"); + } else { + stub.insert_str(line_end, " # type: ignore[overload-cannot-match]"); + } + stub +} + fn emit_instance_stub_group( - methods: &[&MethodMeta], + group: &MethodGroup<'_>, context: &PythonProjectionContext, indent_spaces: usize, - event_has_remove: bool, - property_has_getter: bool, overrides_mutable_sequence: bool, ) -> String { - let mut ordered_methods = methods.iter().copied().collect::>(); - ordered_methods - .sort_by(|left, right| super::overloads::cmp_python_dispatch_methods(left, right)); - - if ordered_methods.len() == 1 { - return emit_method_stub( - ordered_methods[0], - context, - indent_spaces, - event_has_remove, - property_has_getter, - overrides_mutable_sequence, - ); - } - let names = super::overloads::method_names(ordered_methods.iter().copied()); - let public_name = super::overloads::method_group_key(ordered_methods[0], &names); let indent = " ".repeat(indent_spaces); - ordered_methods - .iter() - .map(|method| { - format!( - "{indent}@overload\n{}", - emit_method_stub_named( - method, - context, - indent_spaces, - Some(&public_name), - event_has_remove, - property_has_getter, - overrides_mutable_sequence, - ) - ) + let methods = typed_signatures( + group.candidates.iter().map(|candidate| candidate.method), + |method| super::type_helpers::py_method_return_type(method, context), + context, + ); + let overloaded = methods.len() > 1; + methods + .into_iter() + .map(|(method, duplicate)| { + let mut stub = emit_method_stub_named( + method, + context, + indent_spaces, + Some(&group.name), + false, + true, + overrides_mutable_sequence, + ); + if duplicate { + stub = ignore_unreachable_overload(stub); + } + if overloaded { + format!("{indent}@overload\n{stub}") + } else { + stub + } }) .collect() } -fn emit_instance_compatibility_alias_stubs<'a>( - methods: impl IntoIterator, +fn emit_instance_compatibility_alias_stubs( + plan: &ScopePlan<'_>, context: &PythonProjectionContext, indent_spaces: usize, overrides_mutable_sequence: bool, ) -> String { - let methods = methods.into_iter().collect::>(); - let aliases = super::overloads::compatibility_aliases(methods.iter().copied()); let indent = " ".repeat(indent_spaces); let mut out = String::new(); - for (legacy, _) in aliases { - let matching = methods - .iter() - .copied() - .filter(|method| to_snake_case(&method.name) == legacy) - .collect::>(); - for method in &matching { - if matching.len() > 1 { + for alias in plan.aliases() { + let methods = typed_signatures( + alias.signatures.iter().copied(), + |method| super::type_helpers::py_method_return_type(method, context), + context, + ); + for (method, duplicate) in &methods { + if methods.len() > 1 { out.push_str(&format!("{indent}@overload\n")); } - out.push_str(&emit_method_stub_named( + let mut stub = emit_method_stub_named( method, context, indent_spaces, - Some(&legacy), + Some(&alias.name), false, true, overrides_mutable_sequence, - )); + ); + if *duplicate { + stub = ignore_unreachable_overload(stub); + } + out.push_str(&stub); } } out } -fn emit_static_compatibility_alias_stubs<'a>( +fn is_factory_interface(class: &ClassMeta, iface: &InterfaceMeta) -> bool { + class + .factory_interfaces + .iter() + .any(|factory| std::ptr::eq(factory, iface)) +} + +fn is_factory_method(class: &ClassMeta, method: &MethodMeta) -> bool { + class + .factory_interfaces + .iter() + .flat_map(|factory| factory.methods.iter()) + .any(|candidate| std::ptr::eq(candidate, method)) +} + +fn emit_static_compatibility_alias_stubs( class_name: &str, - methods: impl IntoIterator, + class: &ClassMeta, + plan: &ScopePlan<'_>, context: &PythonProjectionContext, indent_spaces: usize, ) -> String { - let methods = methods.into_iter().collect::>(); - let aliases = - super::overloads::compatibility_aliases(methods.iter().map(|(method, _)| *method)); let indent = " ".repeat(indent_spaces); let mut out = String::new(); - for (legacy, _) in aliases { - let matching = methods - .iter() - .filter(|(method, _)| to_snake_case(&method.name) == legacy) - .collect::>(); - for (method, is_factory) in &matching { - if matching.len() > 1 { + for alias in plan.aliases() { + let methods = typed_signatures( + alias.signatures.iter().copied(), + |method| static_return_type(class_name, class, method, context), + context, + ); + for (method, duplicate) in &methods { + if methods.len() > 1 { out.push_str(&format!("{indent}@overload\n")); } - out.push_str(&emit_static_method_stub_named( + let mut stub = emit_static_method_stub_named( class_name, method, context, - *is_factory, - Some(&legacy), - )); + is_factory_method(class, method), + Some(&alias.name), + ); + if *duplicate { + stub = ignore_unreachable_overload(stub); + } + out.push_str(&stub); } } out } -fn grouped_static_stubs<'a>( - methods: &[(&'a MethodMeta, bool)], -) -> Vec> { - let names = super::overloads::method_names(methods.iter().map(|(method, _)| *method)); - let mut groups: Vec<(String, Vec<(&MethodMeta, bool)>)> = Vec::new(); - for &(method, is_factory) in methods { - let key = super::overloads::method_group_key(method, &names); - if let Some((_, group)) = groups.iter_mut().find(|(name, _)| name == &key) { - group.push((method, is_factory)); - } else { - groups.push((key, vec![(method, is_factory)])); - } +fn static_return_type( + class_name: &str, + class: &ClassMeta, + method: &MethodMeta, + context: &PythonProjectionContext, +) -> String { + if is_factory_method(class, method) { + super::type_helpers::py_factory_return_type(class_name, method, context) + } else { + super::type_helpers::py_method_return_type(method, context) } - groups.into_iter().map(|(_, group)| group).collect() } fn emit_static_stub_group( class_name: &str, - methods: &[(&MethodMeta, bool)], + class: &ClassMeta, + group: &MethodGroup<'_>, context: &PythonProjectionContext, ) -> String { - let mut ordered_methods = methods.iter().copied().collect::>(); - ordered_methods.sort_by(|(left, _), (right, _)| { - super::overloads::cmp_python_dispatch_methods(left, right) - }); - - if ordered_methods.len() == 1 { - let (method, is_factory) = ordered_methods[0]; - return emit_static_method_stub(class_name, method, context, is_factory); - } - let names = super::overloads::method_names(ordered_methods.iter().map(|(method, _)| *method)); - let public_name = super::overloads::method_group_key(ordered_methods[0].0, &names); - ordered_methods - .iter() - .map(|(method, is_factory)| { - format!( - " @overload\n{}", - emit_static_method_stub_named( - class_name, - method, - context, - *is_factory, - Some(&public_name), - ) - ) + let methods = typed_signatures( + group.candidates.iter().map(|candidate| candidate.method), + |method| static_return_type(class_name, class, method, context), + context, + ); + let overloaded = methods.len() > 1; + methods + .into_iter() + .map(|(method, duplicate)| { + let mut stub = emit_static_method_stub_named( + class_name, + method, + context, + is_factory_method(class, method), + Some(&group.name), + ); + if duplicate { + stub = ignore_unreachable_overload(stub); + } + if overloaded { + format!(" @overload\n{stub}") + } else { + stub + } }) .collect() } @@ -1875,3 +1963,56 @@ pub fn generate_public_struct_index_stub( } out } + +#[cfg(test)] +mod tests { + use super::*; + use crate::meta::{ParamDirection, ParamMeta}; + + fn method(vtable_index: usize) -> MethodMeta { + MethodMeta { + name: format!("Read{vtable_index}"), + raw_name: "Read".into(), + vtable_index, + params: vec![ParamMeta { + name: "value".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }], + ..Default::default() + } + } + + #[test] + fn typed_signatures_deduplicate_full_matches_but_keep_distinct_returns() { + let first = method(6); + let different_return = method(7); + let duplicate = method(8); + let context = PythonProjectionContext::default(); + let signatures = typed_signatures( + [&first, &different_return, &duplicate], + |method| { + if method.vtable_index == 7 { + "bytes".to_string() + } else { + "str".to_string() + } + }, + &context, + ); + + assert_eq!( + signatures + .iter() + .map(|(method, duplicate)| (method.vtable_index, *duplicate)) + .collect::>(), + [(6, false), (7, true)] + ); + assert_eq!( + ignore_unreachable_overload( + " def read(self, value: int) -> bytes: ...\n".to_string() + ), + " def read(self, value: int) -> bytes: ... # type: ignore[overload-cannot-match]\n" + ); + } +} diff --git a/tools/dynwinrt-codegen/tests/python_constructor_boundary_test.rs b/tools/dynwinrt-codegen/tests/python_constructor_boundary_test.rs index 9b73889b..1bb07f47 100644 --- a/tools/dynwinrt-codegen/tests/python_constructor_boundary_test.rs +++ b/tools/dynwinrt-codegen/tests/python_constructor_boundary_test.rs @@ -400,7 +400,13 @@ fn numeric_constructor_overloads_dispatch_by_specificity() { ); let pyi = common::generate_class_stub(&class, &known, &HashSet::new(), &HashSet::new()); - assert_eq!(pyi.matches(" @overload\n").count(), 4, "{pyi}"); + assert_eq!( + pyi.matches("def __init__(self, value: int) -> None: ...") + .count(), + 1, + "I8 and I32 both project as one typed `int` constructor signature:\n{pyi}" + ); + assert_eq!(pyi.matches(" @overload\n").count(), 0, "{pyi}"); } #[test] diff --git a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs index 36b50b9b..98ad13ee 100644 --- a/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs +++ b/tools/dynwinrt-codegen/tests/python_consumer_typing_test.rs @@ -813,6 +813,81 @@ print("collection-subscript-native-ok", flush=True) } } +#[test] +fn thread_pool_abi_names_keep_precise_callable_and_native_inputs() { + let winmd = Path::new( + r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd", + ); + if !winmd.is_file() || !has_mypy() { + eprintln!("Skipping ThreadPool typing: Windows.winmd or mypy unavailable."); + return; + } + let fixture = Fixture::new(); + let output = Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args(["generate", "--winmd"]) + .arg(winmd) + .args([ + "--class-name", + "Windows.System.Threading.ThreadPool", + "--lang", + "py", + "--output", + ]) + .arg(fixture.0.join("sdk")) + .output() + .unwrap(); + assert!(output.status.success(), "{}", diagnostics(&output)); + typecheck( + &fixture, + &["sdk"], + r#"from typing import assert_type +from dynwinrt import DynWinRTValue, DynWinRtDelegate, WinRTCoroutine +from sdk.windows.system.threading import ThreadPool, WorkItemOptions, WorkItemPriority + +def supported(native: DynWinRtDelegate, raw: DynWinRTValue) -> None: + first: WinRTCoroutine[None] = ThreadPool.run_async( + lambda action: assert_type(action, DynWinRTValue) + ) + second: WinRTCoroutine[None] = ThreadPool.run_with_priority_async( + lambda action: assert_type(action, DynWinRTValue), WorkItemPriority.Normal + ) + third: WinRTCoroutine[None] = ThreadPool.run_with_priority_and_options_async( + lambda action: assert_type(action, DynWinRTValue), + WorkItemPriority.Normal, WorkItemOptions.TimeSliced, + ) + ThreadPool.run_async(native) + ThreadPool.run_with_priority_async(raw, WorkItemPriority.Normal) + ThreadPool.run_with_priority_and_options_async( + native, WorkItemPriority.Normal, WorkItemOptions.TimeSliced + ) + _ = first, second, third +"#, + &[], + ); + typecheck( + &fixture, + &["sdk"], + r#"from dynwinrt import DynWinRtDelegate +from sdk.windows.system.threading import ThreadPool, WorkItemOptions, WorkItemPriority + +def invalid(native: DynWinRtDelegate) -> None: + ThreadPool.run_async(native, WorkItemPriority.Normal) + ThreadPool.run_async(native, WorkItemPriority.Normal, WorkItemOptions.TimeSliced) + ThreadPool.run_async(handler=native, priority=WorkItemPriority.Normal) + ThreadPool.run_async( + handler=native, priority=WorkItemPriority.Normal, options=WorkItemOptions.TimeSliced + ) +"#, + &[ + "[call-arg]", + "[call-arg]", + "[call-arg]", + "[call-arg]", + "[call-arg]", + ], + ); +} + #[test] fn map_changed_handlers_receive_typed_observable_maps_and_arguments() { let winmd = Path::new( diff --git a/tools/dynwinrt-codegen/tests/python_delegate_callback_test.rs b/tools/dynwinrt-codegen/tests/python_delegate_callback_test.rs index fd2a2e0f..7690ed51 100644 --- a/tools/dynwinrt-codegen/tests/python_delegate_callback_test.rs +++ b/tools/dynwinrt-codegen/tests/python_delegate_callback_test.rs @@ -276,12 +276,24 @@ fn static_events_callback_parameters_and_setters_project_callables() { ); let thread_pool = output.read("windows__system__threading__thread_pool", "py"); + let runtime_callback = "Callable[..., object] | DynWinRTValue | DynWinRtDelegate"; assert!( thread_pool.contains( - "def run_async(handler: Callable[..., object] | DynWinRTValue | DynWinRtDelegate)" - ), + &format!("def run_async(handler: {runtime_callback})") + ) && thread_pool.contains(&format!( + "def run_with_priority_async(handler: {runtime_callback}, priority: 'WorkItemPriority')" + )) && thread_pool.contains(&format!( + "def run_with_priority_and_options_async(handler: {runtime_callback}, priority: 'WorkItemPriority', options: 'WorkItemOptions')" + )) && !thread_pool.contains("def run_async(*args, **kwargs):") + && !thread_pool.contains("_dynwinrt_legacy_call("), "{thread_pool}" ); + for slot in [6, 7, 8] { + assert!( + thread_pool.contains(&format!("_IThreadPoolStatics.method({slot}).invoke(")), + "{thread_pool}" + ); + } assert!( thread_pool.contains("'WorkItemHandler_PARAM_TYPES'))"), "{thread_pool}" @@ -290,6 +302,22 @@ fn static_events_callback_parameters_and_setters_project_callables() { !thread_pool.contains("'WorkItemHandler_PARAM_TYPES'), lambda "), "{thread_pool}" ); + let thread_pool_stub = output.read("windows__system__threading__thread_pool", "pyi"); + let work_item_callback = + "Callable[[DynWinRTValue], object] | 'DynWinRTValue | DynWinRtDelegate'"; + for signature in [ + format!("def run_async(handler: {work_item_callback}) -> WinRTCoroutine[None]: ..."), + format!( + "def run_with_priority_async(handler: {work_item_callback}, priority: 'WorkItemPriority') -> WinRTCoroutine[None]: ..." + ), + format!( + "def run_with_priority_and_options_async(handler: {work_item_callback}, priority: 'WorkItemPriority', options: 'WorkItemOptions') -> WinRTCoroutine[None]: ..." + ), + ] { + assert!(thread_pool_stub.contains(&signature), "{thread_pool_stub}"); + } + assert_eq!(thread_pool_stub.matches("def run_async(").count(), 1); + assert!(!thread_pool_stub.contains("@overload")); let timer_callback = "Callable[..., object] | DynWinRTValue | DynWinRtDelegate"; let timer_stub_callback = @@ -297,8 +325,11 @@ fn static_events_callback_parameters_and_setters_project_callables() { let timer = output.read("windows__system__threading__thread_pool_timer", "py"); assert!( timer.contains(&format!( - "def create_timer(handler: {timer_callback}, delay: timedelta)" - )), + "def _create_timer_7(handler: {timer_callback}, delay: timedelta)" + )) && timer.contains("def create_timer(*args, **kwargs):") + && timer.contains( + "return _dynwinrt_legacy_call(ThreadPoolTimer._create_timer_7, ('handler', 'delay',), args, kwargs, 'create_timer')" + ), "{timer}" ); assert!( diff --git a/tools/dynwinrt-codegen/tests/python_interface_dispatch_test.rs b/tools/dynwinrt-codegen/tests/python_interface_dispatch_test.rs new file mode 100644 index 00000000..572f0993 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/python_interface_dispatch_test.rs @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod common; + +use std::collections::{BTreeSet, HashSet}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; + +use dynwinrt_codegen::meta; +use dynwinrt_codegen::types::TypeMeta; + +const 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 = Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .unwrap() + .parent() + .unwrap() + .join("target") + .join(format!( + "pid{}-{}", + 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 generate_real_class(namespace: &str, name: &str) -> Option { + let class = meta::parse_class(WINDOWS_WINMD, namespace, name)?; + let deps = meta::resolve_python_dependencies(WINDOWS_WINMD, &[class.clone()], &[], &[]); + let mut known = HashSet::new(); + known.insert(class.name.clone()); + known.extend(deps.classes.iter().map(|class| class.name.clone())); + known.extend( + deps.interfaces + .iter() + .map(|interface| interface.name.clone()), + ); + known.extend(deps.enums.iter().filter_map(|typ| match typ { + TypeMeta::Enum { name, .. } => Some(name.clone()), + _ => None, + })); + Some(common::generate_class( + &class, + &known, + &HashSet::new(), + &HashSet::new(), + )) +} + +/// Return the `if _bound ...` guard lines of the generated block starting at `marker`. +fn guard_lines<'a>(code: &'a str, marker: &str) -> Vec<&'a str> { + let start = code + .find(marker) + .unwrap_or_else(|| panic!("missing `{marker}` in:\n{code}")); + let block = &code[start + marker.len()..]; + let end = block.find("\n def ").unwrap_or(block.len()); + block[..end] + .lines() + .map(str::trim) + .filter(|line| line.starts_with("if _bound is not None")) + .collect() +} + +#[test] +fn data_stream_constructors_accept_runtime_class_streams_through_query_interface() { + if !Path::new(WINDOWS_WINMD).exists() { + eprintln!("Skipping: Windows.winmd not found"); + return; + } + for (class, interface, module, iid) in [ + ( + "DataWriter", + "IOutputStream", + "windows__storage__streams__i_output_stream", + "905a0fe6-bc53-11df-8c49-001e4fc686da", + ), + ( + "DataReader", + "IInputStream", + "windows__storage__streams__i_input_stream", + "905a0fe2-bc53-11df-8c49-001e4fc686da", + ), + ] { + let code = generate_real_class("Windows.Storage.Streams", class).expect("class metadata"); + let constant = format!("IID_ARG_Windows_Storage_Streams_{interface}"); + let exact = format!("isinstance(_bound[0], _dynwinrt_symbol('{module}', '{interface}'))"); + let relaxed = format!("({exact} or _dynwinrt_can_cast(_bound[0], {constant}))"); + + assert!( + code.contains(&format!("\n{constant} = WinGUID.parse('{iid}')\n")), + "{code}" + ); + for marker in [ + " def __new__(cls, *args, **kwargs):\n", + " def __init__(self, *args, **kwargs):\n", + ] { + let guards = guard_lines(&code, marker); + let first_relaxed = guards + .iter() + .position(|guard| guard.contains(&relaxed)) + .unwrap_or_else(|| panic!("{class} {marker} lacks a QI guard:\n{code}")); + assert!( + guards[..first_relaxed] + .iter() + .any(|guard| guard.contains(&format!("{exact}:"))), + "{class} {marker} must keep its exact guard first:\n{code}" + ); + assert!( + guards[first_relaxed..] + .iter() + .all(|guard| guard.contains("_dynwinrt_can_cast(_bound[")), + "{class} {marker} must try every exact guard before QueryInterface:\n{code}" + ); + } + } +} + +fn identifiers(text: &str, prefix: &str) -> BTreeSet { + let mut found = BTreeSet::new(); + let mut rest = text; + while let Some(index) = rest.find(prefix) { + let preceded_by_identifier = rest[..index] + .chars() + .next_back() + .is_some_and(|character| character.is_ascii_alphanumeric() || character == '_'); + let tail = &rest[index..]; + let end = tail + .find(|character: char| !(character.is_ascii_alphanumeric() || character == '_')) + .unwrap_or(tail.len()); + if !preceded_by_identifier { + found.insert(tail[..end].to_string()); + } + rest = &tail[end..]; + } + found +} + +#[test] +fn generated_python_modules_define_every_argument_iid_they_reference() { + if !Path::new(WINDOWS_WINMD).exists() { + eprintln!("Skipping: Windows.winmd not found"); + return; + } + let fixture = Fixture::new(); + let output = Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args(["generate", "--winmd", WINDOWS_WINMD, "--class-name"]) + .arg( + "Windows.Storage.Streams.DataWriter,Windows.Storage.Streams.DataReader,\ + Windows.Storage.Streams.RandomAccessStream,Windows.Storage.StorageFile,\ + Windows.System.Launcher,Windows.Data.Xml.Dom.XmlDocument,\ + Windows.Web.Http.HttpClient,Windows.UI.Notifications.ToastNotifier", + ) + .args(["--lang", "py", "--no-pyi", "--output"]) + .arg(&fixture.0) + .output() + .expect("run dynwinrt-codegen"); + assert!( + output.status.success(), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + + let mut modules = 0; + let mut guarded_modules = 0; + let mut pending = vec![fixture.0.clone()]; + while let Some(directory) = pending.pop() { + for entry in fs::read_dir(&directory).unwrap() { + let path = entry.unwrap().path(); + if path.is_dir() { + pending.push(path); + continue; + } + if path.extension().and_then(|extension| extension.to_str()) != Some("py") { + continue; + } + let code = fs::read_to_string(&path).unwrap(); + modules += 1; + let defined = code + .lines() + .filter_map(|line| line.split_once(" = WinGUID.parse(").map(|(name, _)| name)) + .filter(|name| name.starts_with("IID_ARG_")) + .map(str::to_string) + .collect::>(); + let referenced = identifiers(&code, "IID_ARG_"); + let missing = referenced.difference(&defined).collect::>(); + assert!( + missing.is_empty(), + "{} references undefined argument IIDs {missing:?}", + path.display() + ); + if code.contains("_dynwinrt_can_cast(_bound[") { + guarded_modules += 1; + } + } + } + assert!( + modules > 20, + "expected a generated package, found {modules} modules" + ); + assert!( + guarded_modules > 0, + "expected QueryInterface dispatch guards in the generated package" + ); +} diff --git a/tools/dynwinrt-codegen/tests/python_overload_legacy_compat_test.rs b/tools/dynwinrt-codegen/tests/python_overload_legacy_compat_test.rs new file mode 100644 index 00000000..40850685 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/python_overload_legacy_compat_test.rs @@ -0,0 +1,1567 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::sync::atomic::{AtomicU64, Ordering}; + +use dynwinrt_codegen::codegen::python; +use dynwinrt_codegen::meta::{self, InterfaceMeta}; +use dynwinrt_codegen::types::{TypeIdentity, TypeIdentityKind, TypeMeta}; +use windows_metadata::{ + FieldAttributes, MethodAttributes, MethodCallAttributes, MethodImplAttributes, ParamAttributes, + Signature, Type, TypeAttributes, Value, writer, +}; + +const NAMESPACE: &str = "Tests.OverloadCompatibility"; +const 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 directory = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("target") + .join(format!( + "python-overload-legacy-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed), + )); + fs::create_dir_all(&directory).unwrap(); + Self(directory) + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn root() -> PathBuf { + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .canonicalize() + .unwrap(); + let text = path.to_string_lossy(); + PathBuf::from(text.strip_prefix(r"\\?\").unwrap_or(&text)) +} + +fn python() -> PathBuf { + std::env::var_os("DYNWINRT_TEST_PYTHON") + .map(PathBuf::from) + .unwrap_or_else(|| { + let venv = root() + .join("bindings") + .join("py") + .join(".venv") + .join("Scripts") + .join("python.exe"); + if venv.is_file() { + venv + } else { + PathBuf::from("python") + } + }) +} + +fn success(output: Output) { + assert!( + output.status.success(), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); +} + +fn guid(file: &mut writer::File, owner: writer::TypeDef, value: u32) { + let attribute = file.TypeRef("Windows.Foundation.Metadata", "GuidAttribute"); + let constructor = file.MemberRef( + ".ctor", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::Void, + types: vec![ + Type::U32, + Type::U16, + Type::U16, + Type::U8, + Type::U8, + Type::U8, + Type::U8, + Type::U8, + Type::U8, + Type::U8, + Type::U8, + ], + }, + writer::MemberRefParent::TypeRef(attribute), + ); + let values = [ + Value::U32(value), + Value::U16(0x6281), + Value::U16(0x4900), + Value::U8(0xb7), + Value::U8(0x82), + Value::U8(4), + Value::U8(3), + Value::U8(2), + Value::U8(1), + Value::U8(9), + Value::U8(0x10), + ]; + file.Attribute( + writer::HasAttribute::TypeDef(owner), + writer::AttributeType::MemberRef(constructor), + &values + .into_iter() + .map(|value| (String::new(), value)) + .collect::>(), + ); +} + +fn overload(file: &mut writer::File, method: writer::MethodDef, name: &str) { + let attribute = file.TypeRef("Windows.Foundation.Metadata", "OverloadAttribute"); + let constructor = file.MemberRef( + ".ctor", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::Void, + types: vec![Type::String], + }, + writer::MemberRefParent::TypeRef(attribute), + ); + file.Attribute( + writer::HasAttribute::MethodDef(method), + writer::AttributeType::MemberRef(constructor), + &[(String::new(), Value::Utf8(name.into()))], + ); +} + +fn interface( + file: &mut writer::File, + name: &str, + id: u32, + raw_name: &str, + abi_name: &str, + parameters: &[(&str, Type)], +) { + let definition = file.TypeDef( + NAMESPACE, + name, + writer::TypeDefOrRef::default(), + TypeAttributes::Public + | TypeAttributes::Interface + | TypeAttributes::Abstract + | TypeAttributes::WindowsRuntime, + ); + guid(file, definition, id); + let method = file.MethodDef( + raw_name, + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::I32, + types: parameters.iter().map(|(_, typ)| typ.clone()).collect(), + }, + MethodAttributes::Public + | MethodAttributes::Abstract + | MethodAttributes::Virtual + | MethodAttributes::NewSlot, + MethodImplAttributes::default(), + ); + for (index, (name, _)) in parameters.iter().enumerate() { + file.Param(name, index as u16 + 1, ParamAttributes::In); + } + if abi_name != raw_name { + overload(file, method, abi_name); + } +} + +fn runtime_class(file: &mut writer::File, name: &str, interfaces: &[&str]) { + let object = file.TypeRef("System", "Object"); + let definition = file.TypeDef( + NAMESPACE, + name, + writer::TypeDefOrRef::TypeRef(object), + TypeAttributes::Public | TypeAttributes::Sealed | TypeAttributes::WindowsRuntime, + ); + let default_attribute = file.TypeRef("Windows.Foundation.Metadata", "DefaultAttribute"); + let default_constructor = file.MemberRef( + ".ctor", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::Void, + types: vec![], + }, + writer::MemberRefParent::TypeRef(default_attribute), + ); + for (index, interface) in interfaces.iter().enumerate() { + let implementation = file.InterfaceImpl(definition, &Type::named(NAMESPACE, *interface)); + if index == 0 { + file.Attribute( + writer::HasAttribute::InterfaceImpl(implementation), + writer::AttributeType::MemberRef(default_constructor), + &[], + ); + } + } +} + +fn metadata(path: &Path) { + let mut file = writer::File::new("PythonOverloadLegacyCompatibility"); + let enum_base = file.TypeRef("System", "Enum"); + let _mode = file.TypeDef( + NAMESPACE, + "Mode", + writer::TypeDefOrRef::TypeRef(enum_base), + TypeAttributes::Public | TypeAttributes::Sealed | TypeAttributes::WindowsRuntime, + ); + file.Field( + "value__", + &Type::I32, + FieldAttributes::Public | FieldAttributes::SpecialName | FieldAttributes::RTSpecialName, + ); + let one = file.Field( + "One", + &Type::named(NAMESPACE, "Mode"), + FieldAttributes::Public + | FieldAttributes::Static + | FieldAttributes::Literal + | FieldAttributes::HasDefault, + ); + file.Constant(writer::HasConstant::Field(one), &Value::I32(1)); + + interface( + &mut file, + "IAliasCanonical", + 0x51931901, + "Foo", + "Foo", + &[("text", Type::String)], + ); + interface( + &mut file, + "IAliasLegacyString", + 0x51931902, + "Foo", + "FooVersion", + &[("value", Type::String)], + ); + interface( + &mut file, + "IAliasLegacyInt", + 0x51931903, + "Foo", + "FooVersion", + &[("value", Type::I32)], + ); + runtime_class( + &mut file, + "AliasProbe", + &["IAliasCanonical", "IAliasLegacyString", "IAliasLegacyInt"], + ); + + interface( + &mut file, + "IEnumLegacy", + 0x51931904, + "Foo", + "Foo", + &[("value", Type::named(NAMESPACE, "Mode"))], + ); + interface( + &mut file, + "IStringCanonical", + 0x51931905, + "Foo", + "FooText", + &[("value", Type::String)], + ); + runtime_class(&mut file, "EnumProbe", &["IEnumLegacy", "IStringCanonical"]); + interface( + &mut file, + "IEnumPairLegacy", + 0x51931906, + "Bar", + "Bar", + &[ + ("mode", Type::named(NAMESPACE, "Mode")), + ("label", Type::String), + ], + ); + interface( + &mut file, + "IStringPairCanonical", + 0x51931907, + "Bar", + "BarText", + &[("mode", Type::String), ("enabled", Type::Bool)], + ); + runtime_class( + &mut file, + "PairProbe", + &["IEnumPairLegacy", "IStringPairCanonical"], + ); + interface( + &mut file, + "IIntBoolLegacy", + 0x51931908, + "Qux", + "Qux", + &[("value", Type::I32)], + ); + interface( + &mut file, + "IBoolCanonical", + 0x51931909, + "Qux", + "QuxBool", + &[("value", Type::Bool)], + ); + runtime_class( + &mut file, + "BoolProbe", + &["IIntBoolLegacy", "IBoolCanonical"], + ); + interface( + &mut file, + "IIntBoolPairLegacy", + 0x5193190a, + "Quux", + "Quux", + &[("value", Type::I32), ("label", Type::String)], + ); + interface( + &mut file, + "IBoolPairCanonical", + 0x5193190b, + "Quux", + "QuuxBool", + &[("value", Type::Bool), ("enabled", Type::Bool)], + ); + runtime_class( + &mut file, + "BoolPairProbe", + &["IIntBoolPairLegacy", "IBoolPairCanonical"], + ); + interface( + &mut file, + "IIntBoolArityLegacy", + 0x5193190c, + "Zap", + "Zap", + &[("value", Type::I32)], + ); + interface( + &mut file, + "IBoolArityCanonical", + 0x5193190d, + "Zap", + "ZapBool", + &[("value", Type::Bool), ("enabled", Type::Bool)], + ); + runtime_class( + &mut file, + "BoolArityProbe", + &["IIntBoolArityLegacy", "IBoolArityCanonical"], + ); + interface( + &mut file, + "IIntPairLegacy", + 0x5193190e, + "Pick", + "Pick", + &[("first", Type::I32), ("second", Type::I32)], + ); + interface( + &mut file, + "IBoolWideCanonical", + 0x5193190f, + "Pick", + "PickBool", + &[("first", Type::Bool), ("second", Type::I64)], + ); + runtime_class( + &mut file, + "NumericDomainProbe", + &["IIntPairLegacy", "IBoolWideCanonical"], + ); + interface( + &mut file, + "IQiLegacy", + 0x51931910, + "Use", + "Use", + &[("target", Type::named(NAMESPACE, "IAliasCanonical"))], + ); + interface( + &mut file, + "IQiCanonical", + 0x51931911, + "Use", + "Apply", + &[("target", Type::named(NAMESPACE, "IAliasLegacyString"))], + ); + runtime_class(&mut file, "QiDispatchProbe", &["IQiLegacy", "IQiCanonical"]); + interface( + &mut file, + "IIndexLegacy", + 0x51931912, + "Sift", + "Sift", + &[("value", Type::I32)], + ); + interface( + &mut file, + "IIndexStringCanonical", + 0x51931913, + "Sift", + "SiftText", + &[("value", Type::String)], + ); + runtime_class( + &mut file, + "IndexStringProbe", + &["IIndexLegacy", "IIndexStringCanonical"], + ); + interface( + &mut file, + "IByteLegacy", + 0x51931914, + "Rank", + "Rank", + &[("value", Type::I8)], + ); + interface( + &mut file, + "IModeCanonical", + 0x51931915, + "Rank", + "RankMode", + &[("value", Type::named(NAMESPACE, "Mode"))], + ); + runtime_class( + &mut file, + "EnumComparisonProbe", + &["IByteLegacy", "IModeCanonical"], + ); + runtime_class( + &mut file, + "_dynwinrt_legacy_call", + &["IEnumLegacy", "IStringCanonical"], + ); + runtime_class( + &mut file, + "_dynwinrt_legacy_int_guard", + &["IEnumLegacy", "IStringCanonical"], + ); + runtime_class( + &mut file, + "_dynwinrt_can_cast", + &["IQiLegacy", "IQiCanonical"], + ); + runtime_class( + &mut file, + "IID_ARG_Tests_OverloadCompatibility_IAliasCanonical", + &["IQiLegacy", "IQiCanonical"], + ); + let int_guard_interface = file.TypeDef( + NAMESPACE, + "IIntGuardDispatch", + writer::TypeDefOrRef::default(), + TypeAttributes::Public + | TypeAttributes::Interface + | TypeAttributes::Abstract + | TypeAttributes::WindowsRuntime, + ); + guid(&mut file, int_guard_interface, 0x51931916); + file.MethodDef( + "Foo", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::I32, + types: vec![Type::named(NAMESPACE, "Mode")], + }, + MethodAttributes::Public + | MethodAttributes::Abstract + | MethodAttributes::Virtual + | MethodAttributes::NewSlot, + MethodImplAttributes::default(), + ); + file.Param("value", 1, ParamAttributes::In); + let text_method = file.MethodDef( + "Foo", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::I32, + types: vec![Type::String], + }, + MethodAttributes::Public + | MethodAttributes::Abstract + | MethodAttributes::Virtual + | MethodAttributes::NewSlot, + MethodImplAttributes::default(), + ); + file.Param("value", 1, ParamAttributes::In); + overload(&mut file, text_method, "FooText"); + file.MethodDef( + "Use", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::I32, + types: vec![Type::named(NAMESPACE, "_dynwinrt_legacy_int_guard")], + }, + MethodAttributes::Public + | MethodAttributes::Abstract + | MethodAttributes::Virtual + | MethodAttributes::NewSlot, + MethodImplAttributes::default(), + ); + file.Param("target", 1, ParamAttributes::In); + + let registered_iid = "ARG_Tests_OverloadCompatibility_IAliasCanonical"; + interface( + &mut file, + registered_iid, + 0x51931917, + "Identity", + "Identity", + &[], + ); + runtime_class( + &mut file, + "IIDRegistrationProbe", + &[registered_iid, "IQiLegacy", "IQiCanonical"], + ); + + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, file.into_stream()).unwrap(); +} + +fn async_input_metadata(path: &Path) { + let mut file = writer::File::new("PythonAsyncOverloadInput"); + let definition = file.TypeDef( + NAMESPACE, + "IAsyncInputProbe", + writer::TypeDefOrRef::default(), + TypeAttributes::Public + | TypeAttributes::Interface + | TypeAttributes::Abstract + | TypeAttributes::WindowsRuntime, + ); + guid(&mut file, definition, 0x51931918); + file.TypeRef("Windows.Foundation", "IAsyncInfo"); + file.MethodDef( + "Use", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::I32, + types: vec![Type::named("Windows.Foundation", "IAsyncInfo")], + }, + MethodAttributes::Public + | MethodAttributes::Abstract + | MethodAttributes::Virtual + | MethodAttributes::NewSlot, + MethodImplAttributes::default(), + ); + file.Param("target", 1, ParamAttributes::In); + let numeric = file.MethodDef( + "Use", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::I32, + types: vec![Type::I32], + }, + MethodAttributes::Public + | MethodAttributes::Abstract + | MethodAttributes::Virtual + | MethodAttributes::NewSlot, + MethodImplAttributes::default(), + ); + file.Param("value", 1, ParamAttributes::In); + overload(&mut file, numeric, "UseOverload"); + runtime_class(&mut file, "AsyncInputProbe", &["IAsyncInputProbe"]); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, file.into_stream()).unwrap(); +} + +#[test] +fn raw_async_interface_arguments_dispatch_to_their_native_slot() { + let available = Path::new(WINDOWS_WINMD).is_file() + && Command::new(python()) + .args([ + "-c", + "import dynwinrt; assert hasattr(dynwinrt, 'DynWinRTImplementationHandle')", + ]) + .output() + .is_ok_and(|output| output.status.success()); + if !available { + assert_ne!( + std::env::var("DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME").as_deref(), + Ok("1"), + "Windows SDK metadata and native Python implementation runtime are required" + ); + eprintln!("Skipping live Async argument probe; SDK metadata or runtime unavailable"); + return; + } + + let fixture = Fixture::new(); + let winmd = fixture.0.join("metadata").join("AsyncInput.winmd"); + async_input_metadata(&winmd); + let package = fixture.0.join("pyviews"); + success( + Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args(["generate", "--winmd"]) + .arg(&winmd) + .args([ + "--ref", + WINDOWS_WINMD, + "--namespace", + NAMESPACE, + "--lang", + "py", + "--output", + ]) + .arg(&package) + .output() + .unwrap(), + ); + let source = + fs::read_to_string(package.join("tests__overload_compatibility__async_input_probe.py")) + .unwrap(); + assert!( + package + .join("windows__foundation__i_async_info.py") + .is_file(), + "known-interface guards require their projected interface dependency" + ); + let manifest = fs::read_to_string(package.join("pyproject.toml")).unwrap(); + assert!( + manifest.contains(&format!( + "dependencies = [\"dynwinrt=={}\"]", + env!("CARGO_PKG_VERSION") + )), + "{manifest}" + ); + assert!( + source.contains("def use(self, *args, **kwargs):") + && source.contains("IID_ARG_Windows_Foundation_IAsyncInfo = WinGUID.parse(") + && source.contains("_IAsyncInputProbe.method(6).invoke(") + && source.contains("_IAsyncInputProbe.method(7).invoke(") + && !source.contains("_dynwinrt_legacy_call("), + "{source}" + ); + + let probe = r#" +import importlib +from pathlib import Path +import dynwinrt as dw +from pyviews.tests__overload_compatibility__async_input_probe import AsyncInputProbe +from pyviews.tests__overload_compatibility__i_async_input_probe import IAsyncInputProbe + +runtime = importlib.import_module("pyviews._runtime") +info_iid = dw.WinGUID.parse("00000036-0000-0000-c000-000000000046") +wrong_iid = dw.WinGUID.parse("11111111-1111-1111-1111-111111111111") +statics_iid = dw.WinGUID.parse("5984c710-daf2-43c8-8bb4-a4d3eacfd03f") +file_type = dw.DynWinRTType.runtime_class( + "Windows.Storage.StorageFile", + dw.DynWinRTType.interface(dw.WinGUID.parse("fa3f6186-4214-428c-a64c-14c9ac7315ea")), +) +statics = dw.DynWinRTType.register_interface("IStorageFileStatics", statics_iid).add_method( + "GetFileFromPathAsync", + dw.DynWinRTMethodSig().add_in(dw.DynWinRTType.hstring()).add_out( + dw.DynWinRTType.i_async_operation(file_type) + ), +) +calls = [] +class Handlers: + def use(self, target): + calls.append(("IAsyncInfo", type(target).__name__)) + return 301 + def use_overload(self, value): + calls.append(("Int32", value)) + return 302 + +def rejects(call, error_type): + try: + call() + except error_type as error: + return error + raise AssertionError(f"Expected {error_type.__name__}") + +with dw.RoApartment(1): + factory = dw.DynWinRTValue.activation_factory("Windows.Storage.StorageFile") + factory_view = factory.cast(statics_iid) + try: + operation = statics.method(6).invoke( + factory_view, [dw.DynWinRTValue.from_hstring(str(Path(__file__).resolve()))] + ) + finally: + factory_view.release() + factory.release() + assert "Object value" in str(rejects(operation.as_raw, RuntimeError)) + assert runtime._dynwinrt_can_cast(operation, info_iid), "raw Async QI rejected" + casted = operation.cast(info_iid) + scalar = dw.DynWinRTValue.from_i32(5) + bad_scalar = dw.DynWinRTValue.from_hstring("bad") + null = dw.DynWinRTValue.null_value() + assert casted._try_query_interface(info_iid) + assert operation._try_query_interface(info_iid) + for value in (scalar, bad_scalar, null): + assert not value._try_query_interface(info_iid) + assert not runtime._dynwinrt_can_cast(value, info_iid) + assert not operation._try_query_interface(wrong_iid) + assert not runtime._dynwinrt_can_cast(operation, wrong_iid) + + with IAsyncInputProbe.implement(Handlers()) as owner: + value = AsyncInputProbe._from_native(owner.value._obj) + try: + assert value.use(casted) == 301 + assert value.use(operation) == 301 + assert value.use(target=operation) == 301 + assert value.use(5) == 302 + assert value.use(value=9) == 302 + for invalid in (scalar, bad_scalar, null): + assert "No matching overload for use" in str( + rejects(lambda: value.use(invalid), TypeError) + ) + controlled = OSError(None, "controlled QI failure", None, -2147467259) + class FailingValue: + def _try_query_interface(self, _iid): raise controlled + class OldValue: + pass + original = runtime.DynWinRTValue + try: + runtime.DynWinRTValue = FailingValue + assert rejects( + lambda: runtime._dynwinrt_can_cast(FailingValue(), info_iid), OSError + ) is controlled + runtime.DynWinRTValue = OldValue + mismatch = rejects( + lambda: runtime._dynwinrt_can_cast(OldValue(), info_iid), RuntimeError + ) + assert "matching dynwinrt runtime" in str(mismatch) + assert "regenerate all Python bindings" in str(mismatch) + finally: + runtime.DynWinRTValue = original + + operation.release() + assert operation.is_released() + assert "released" in str( + rejects(lambda: operation._try_query_interface(info_iid), RuntimeError) + ) + assert "released" in str( + rejects(lambda: runtime._dynwinrt_can_cast(operation, info_iid), RuntimeError) + ) + assert "released" in str(rejects(lambda: value.use(operation), RuntimeError)) + finally: + dw.release_projected(value) + casted.release() + assert calls == [ + ("IAsyncInfo", "IAsyncInfo"), + ("IAsyncInfo", "IAsyncInfo"), + ("IAsyncInfo", "IAsyncInfo"), + ("Int32", 5), + ("Int32", 9), + ], calls +print("async-interface-input-ok") +"#; + fs::write(fixture.0.join("async_probe.py"), probe).unwrap(); + let output = Command::new(python()) + .args(["-B", "async_probe.py"]) + .current_dir(&fixture.0) + .output() + .unwrap(); + success(output.clone()); + assert!( + String::from_utf8_lossy(&output.stdout).contains("async-interface-input-ok"), + "{}", + String::from_utf8_lossy(&output.stdout) + ); +} + +#[test] +fn colliding_legacy_support_names_keep_metadata_types_and_native_targets() { + let fixture = Fixture::new(); + let winmd = fixture.0.join("metadata").join("Input.winmd"); + metadata(&winmd); + let winmd_path = winmd.to_str().unwrap(); + let class_names = [ + "_dynwinrt_legacy_call", + "_dynwinrt_legacy_int_guard", + "_dynwinrt_can_cast", + "IID_ARG_Tests_OverloadCompatibility_IAliasCanonical", + "IIDRegistrationProbe", + ]; + let classes = class_names + .iter() + .map(|name| meta::parse_class(winmd_path, NAMESPACE, name).unwrap()) + .collect::>(); + let interface = + meta::parse_public_interface(winmd_path, NAMESPACE, "IIntGuardDispatch").unwrap(); + let identities = meta::parse_namespace(winmd_path, NAMESPACE) + .iter() + .map(|class| TypeIdentity::named(TypeIdentityKind::Class, NAMESPACE, &class.name)) + .chain( + meta::parse_interfaces(winmd_path, NAMESPACE) + .iter() + .map(InterfaceMeta::type_identity), + ) + .chain( + meta::parse_enums(winmd_path, NAMESPACE) + .iter() + .map(TypeMeta::type_identity), + ) + .collect::>(); + let runtime_available = Command::new(python()) + .args([ + "-c", + "import dynwinrt; assert hasattr(dynwinrt, 'DynWinRTImplementationHandle')", + ]) + .output() + .is_ok_and(|output| output.status.success()); + assert!( + runtime_available + || std::env::var("DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME").as_deref() != Ok("1"), + "native Python implementation runtime is required for the collision probe" + ); + + for packaged in [true, false] { + let flavor = if packaged { "packaged" } else { "standalone" }; + let parent = fixture.0.join(flavor); + let package = parent.join("pyviews"); + success( + Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args(["generate", "--winmd"]) + .arg(&winmd) + .args(["--namespace", NAMESPACE, "--lang", "py", "--output"]) + .arg(&package) + .output() + .unwrap(), + ); + let context = python::PythonProjectionContext::new(identities.clone(), packaged).unwrap(); + if !packaged { + for class in &classes { + let identity = TypeIdentity::named(TypeIdentityKind::Class, NAMESPACE, &class.name); + fs::write( + package.join(format!("{}.py", context.implementation_module(&identity))), + python::generate_class(&context, class, &Default::default()), + ) + .unwrap(); + } + fs::write( + package.join(format!( + "{}.py", + context.implementation_module(&interface.type_identity()) + )), + python::generate_interface(&context, &interface), + ) + .unwrap(); + } + + let module = |name: &str, kind| { + let identity = TypeIdentity::named(kind, NAMESPACE, name); + context.implementation_module(&identity) + }; + for (name, kind, helper) in [ + ( + "_dynwinrt_legacy_call", + TypeIdentityKind::Class, + "_dynwinrt_legacy_call", + ), + ( + "_dynwinrt_legacy_int_guard", + TypeIdentityKind::Class, + "_dynwinrt_legacy_int_guard", + ), + ( + "_dynwinrt_can_cast", + TypeIdentityKind::Class, + "_dynwinrt_can_cast", + ), + ] { + let source = + fs::read_to_string(package.join(format!("{}.py", module(name, kind)))).unwrap(); + assert!( + source.contains(&format!("\nclass {name}:")), + "{flavor}: {source}" + ); + assert!( + source.contains(&format!("{helper} as {helper}_2")) + && source.contains(&format!("{helper}_2(")), + "{flavor}: {source}" + ); + } + let interface_source = fs::read_to_string(package.join(format!( + "{}.py", + module(&interface.name, TypeIdentityKind::Interface) + ))) + .unwrap(); + assert!( + interface_source.contains("_dynwinrt_legacy_int_guard as _dynwinrt_legacy_int_guard_2") + && interface_source.contains("_dynwinrt_legacy_int_guard_2(") + && interface_source.contains("\nclass IIntGuardDispatch:"), + "{flavor}: {interface_source}" + ); + let iid_name = "IID_ARG_Tests_OverloadCompatibility_IAliasCanonical"; + let iid_source = fs::read_to_string( + package.join(format!("{}.py", module(iid_name, TypeIdentityKind::Class))), + ) + .unwrap(); + assert!( + iid_source.contains(&format!("\nclass {iid_name}:")), + "{iid_source}" + ); + assert!( + iid_source.contains(&format!("{iid_name}_2 = WinGUID.parse(")) + && iid_source.contains(&format!("_dynwinrt_can_cast(_bound[0], {iid_name}_2)")), + "{flavor}: {iid_source}" + ); + let registration_source = fs::read_to_string(package.join(format!( + "{}.py", + module(class_names[4], TypeIdentityKind::Class) + ))) + .unwrap(); + assert!( + registration_source.contains(&format!( + "{iid_name} = WinGUID.parse('51931917-6281-4900-b782-040302010910')" + )) && registration_source.contains(&format!( + "{iid_name}_2 = WinGUID.parse('51931901-6281-4900-b782-040302010910')" + )) && registration_source.contains(&format!( + "_dynwinrt_can_cast(_legacy_bound[0], {iid_name}_2)" + )), + "{flavor}: {registration_source}" + ); + + if !runtime_available { + eprintln!("Skipping native collision probe; prepared Python binding is unavailable"); + continue; + } + let probe = r#" +import importlib +import dynwinrt as dw +from pyviews.tests__overload_compatibility__i_enum_legacy import IEnumLegacy +from pyviews.tests__overload_compatibility__i_string_canonical import IStringCanonical +from pyviews.tests__overload_compatibility__i_qi_legacy import IQiLegacy +from pyviews.tests__overload_compatibility__i_qi_canonical import IQiCanonical +from pyviews.tests__overload_compatibility__i_alias_canonical import IAliasCanonical +from pyviews.{REGISTERED_IID_MODULE} import ARG_Tests_OverloadCompatibility_IAliasCanonical as IRegistration + +legacy_module = importlib.import_module("pyviews.{LEGACY_MODULE}") +int_guard_module = importlib.import_module("pyviews.{INT_GUARD_MODULE}") +interface_module = importlib.import_module("pyviews.{INTERFACE_MODULE}") +can_cast_module = importlib.import_module("pyviews.{CAN_CAST_MODULE}") +iid_module = importlib.import_module("pyviews.{IID_MODULE}") +registration_module = importlib.import_module("pyviews.{REGISTRATION_MODULE}") +runtime = importlib.import_module("pyviews._runtime") + +Legacy = legacy_module._dynwinrt_legacy_call +IntGuard = int_guard_module._dynwinrt_legacy_int_guard +IIntGuardDispatch = interface_module.IIntGuardDispatch +CanCast = can_cast_module._dynwinrt_can_cast +IidCollision = iid_module.IID_ARG_Tests_OverloadCompatibility_IAliasCanonical +IIDRegistrationProbe = registration_module.IIDRegistrationProbe +assert legacy_module._dynwinrt_legacy_call_2 is runtime._dynwinrt_legacy_call +assert int_guard_module._dynwinrt_legacy_int_guard_2 is runtime._dynwinrt_legacy_int_guard +assert interface_module._dynwinrt_legacy_int_guard_2 is runtime._dynwinrt_legacy_int_guard +assert can_cast_module._dynwinrt_can_cast_2 is runtime._dynwinrt_can_cast +assert iid_module.IID_ARG_Tests_OverloadCompatibility_IAliasCanonical_2 is not IidCollision + +calls = [] +class EnumHandler: + def foo(self, value): + calls.append(("enum", int(value))) + return 301 +class TextHandler: + def foo_text(self, value): + calls.append(("text", value)) + return 401 +class BothHandlers(EnumHandler, TextHandler): + def use(self, target): + return 666 +class AliasHandler: + def foo(self, value): + return 101 +class LegacyQi: + def use(self, value): + calls.append(("qi-legacy", type(value).__name__)) + return 1101 +class CanonicalQi: + def apply(self, value): + calls.append(("qi-canonical", type(value).__name__)) + return 1102 +class Registration: + def identity(self): + return 1700 + +with dw.RoApartment(1): + with IEnumLegacy.implement( + EnumHandler(), interfaces=[(IStringCanonical, TextHandler())] + ) as owner: + value = Legacy._from_native(owner.value._obj) + try: + assert value.foo("1") == 301 + assert value.foo(value="2") == 301 + assert value.foo("child") == 401 + finally: + dw.release_projected(value) + + with IEnumLegacy.implement( + EnumHandler(), interfaces=[(IStringCanonical, TextHandler())] + ) as owner: + value = IntGuard._from_native(owner.value._obj) + try: + assert value.foo("1") == 301 + assert value.foo(value="2") == 301 + assert value.foo("child") == 401 + finally: + dw.release_projected(value) + + with IIntGuardDispatch.implement(BothHandlers()) as owner: + value = owner.value + assert value.foo("1") == 301 + assert value.foo(value="2") == 301 + assert value.foo("child") == 401 + + with IAliasCanonical.implement(AliasHandler()) as target_owner: + with IQiLegacy.implement( + LegacyQi(), interfaces=[(IQiCanonical, CanonicalQi())] + ) as owner: + for class_type in (CanCast, IidCollision): + value = class_type._from_native(owner.value._obj) + try: + assert value.use(target_owner.value._obj) == 1101 + assert value.use(target=target_owner.value._obj) == 1101 + finally: + dw.release_projected(value) + with IRegistration.implement( + Registration(), + interfaces=[(IQiLegacy, LegacyQi()), (IQiCanonical, CanonicalQi())], + ) as owner: + value = IIDRegistrationProbe._from_native(owner.value._obj) + try: + assert value.identity() == 1700 + assert value.use(target_owner.value._obj) == 1101 + assert value.use(target=target_owner.value._obj) == 1101 + finally: + dw.release_projected(value) + +assert calls == [ + ("enum", 1), ("enum", 2), ("text", "child"), + ("enum", 1), ("enum", 2), ("text", "child"), + ("enum", 1), ("enum", 2), ("text", "child"), + ("qi-legacy", "IAliasCanonical"), ("qi-legacy", "IAliasCanonical"), + ("qi-legacy", "IAliasCanonical"), ("qi-legacy", "IAliasCanonical"), + ("qi-legacy", "IAliasCanonical"), ("qi-legacy", "IAliasCanonical"), +], calls +print("overload-support-collisions-ok") +"#; + let probe = [ + ( + "{LEGACY_MODULE}", + module(class_names[0], TypeIdentityKind::Class), + ), + ( + "{INT_GUARD_MODULE}", + module(class_names[1], TypeIdentityKind::Class), + ), + ( + "{INTERFACE_MODULE}", + module(&interface.name, TypeIdentityKind::Interface), + ), + ( + "{CAN_CAST_MODULE}", + module(class_names[2], TypeIdentityKind::Class), + ), + ( + "{IID_MODULE}", + module(class_names[3], TypeIdentityKind::Class), + ), + ( + "{REGISTERED_IID_MODULE}", + module( + "ARG_Tests_OverloadCompatibility_IAliasCanonical", + TypeIdentityKind::Interface, + ), + ), + ( + "{REGISTRATION_MODULE}", + module(class_names[4], TypeIdentityKind::Class), + ), + ] + .into_iter() + .fold(probe.to_string(), |source, (placeholder, value)| { + source.replace(placeholder, &value) + }); + fs::write(parent.join("collision_probe.py"), probe).unwrap(); + let output = Command::new(python()) + .args(["-B", "collision_probe.py"]) + .current_dir(&parent) + .output() + .unwrap(); + success(output.clone()); + assert!( + String::from_utf8_lossy(&output.stdout).contains("overload-support-collisions-ok"), + "{flavor}: {}", + String::from_utf8_lossy(&output.stdout) + ); + } +} + +#[test] +fn old_dispatchers_and_guard_free_conversions_keep_their_exact_targets() { + let fixture = Fixture::new(); + let winmd = fixture.0.join("metadata").join("Input.winmd"); + metadata(&winmd); + let package = fixture.0.join("pyviews"); + success( + Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args(["generate", "--winmd"]) + .arg(&winmd) + .args(["--namespace", NAMESPACE, "--lang", "py", "--output"]) + .arg(&package) + .output() + .unwrap(), + ); + let alias_source = + fs::read_to_string(package.join("tests__overload_compatibility__alias_probe.py")).unwrap(); + let alias_class = alias_source + .split("\nclass IAliasLegacyString:") + .next() + .unwrap(); + assert!( + alias_class.contains(" def foo_version(self, *args, **kwargs):") + && alias_class.contains("_IAliasLegacyString.method(6).invoke(") + && !alias_class.contains(" foo_version = foo\n"), + "{alias_source}" + ); + let enum_source = + fs::read_to_string(package.join("tests__overload_compatibility__enum_probe.py")).unwrap(); + assert!( + enum_source.contains( + "not (_legacy_bound is not None and (type(_legacy_bound[0]) not in (str,) or _dynwinrt_legacy_int_guard(_legacy_bound[0], -2147483648, 2147483647)))" + ) && enum_source.contains( + "return _dynwinrt_legacy_call(self._foo_6_1, ('value',), args, kwargs, 'foo')" + ), + "{enum_source}" + ); + let pair_source = + fs::read_to_string(package.join("tests__overload_compatibility__pair_probe.py")).unwrap(); + assert!( + !pair_source.contains("_dynwinrt_legacy_int_guard") + && pair_source.contains("return self._bar_6_0(*_bound)"), + "{pair_source}" + ); + let bool_source = + fs::read_to_string(package.join("tests__overload_compatibility__bool_probe.py")).unwrap(); + assert!( + bool_source.contains("not (_legacy_bound is not None) and isinstance(_bound[0], bool)") + && bool_source.contains( + "return _dynwinrt_legacy_call(self._qux_6_1, ('value',), args, kwargs, 'qux')" + ) + && !bool_source.contains("_dynwinrt_legacy_int_guard"), + "{bool_source}" + ); + for name in ["bool_pair_probe", "bool_arity_probe"] { + let source = + fs::read_to_string(package.join(format!("tests__overload_compatibility__{name}.py"))) + .unwrap(); + assert!( + !source.contains("_legacy_bound") + && !source.contains("and not (_legacy_bound is not None"), + "{source}" + ); + } + let numeric_domain_source = + fs::read_to_string(package.join("tests__overload_compatibility__numeric_domain_probe.py")) + .unwrap(); + assert!( + numeric_domain_source.contains( + "-2147483648 <= int.__index__(_legacy_bound[1]) <= 2147483647" + ) && numeric_domain_source.contains( + "return _dynwinrt_legacy_call(self._pick_6_1, ('first', 'second',), args, kwargs, 'pick')" + ), + "{numeric_domain_source}" + ); + let qi_source = + fs::read_to_string(package.join("tests__overload_compatibility__qi_dispatch_probe.py")) + .unwrap(); + assert_eq!( + qi_source + .matches("_dynwinrt_can_cast(_legacy_bound[0], IID_ARG_Tests_OverloadCompatibility_IAliasCanonical)") + .count(), + 2, + "{qi_source}" + ); + let index_string_source = + fs::read_to_string(package.join("tests__overload_compatibility__index_string_probe.py")) + .unwrap(); + assert!( + index_string_source + .contains("type(_legacy_bound[0]) not in (str,)) and isinstance(_bound[0], str)") + && index_string_source.contains( + "return _dynwinrt_legacy_call(self._sift_6_1, ('value',), args, kwargs, 'sift')" + ), + "{index_string_source}" + ); + let enum_comparison_source = + fs::read_to_string(package.join("tests__overload_compatibility__enum_comparison_probe.py")) + .unwrap(); + assert!( + enum_comparison_source.contains("-128 <= int.__index__(_legacy_bound[0]) <= 127") + && !enum_comparison_source.contains("-128 <= _legacy_bound[0] <= 127"), + "{enum_comparison_source}" + ); + + let available = Command::new(python()) + .args([ + "-c", + "import dynwinrt; assert hasattr(dynwinrt, 'DynWinRTImplementationHandle')", + ]) + .output() + .is_ok_and(|output| output.status.success()); + if !available { + assert_ne!( + std::env::var("DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME").as_deref(), + Ok("1"), + "native Python implementation runtime is required" + ); + eprintln!("Skipping live compatibility probe; prepared Python binding is unavailable"); + return; + } + + let probe = r#" +import json +import importlib +import dynwinrt as dw +from pyviews.tests__overload_compatibility__alias_probe import AliasProbe +from pyviews.tests__overload_compatibility__bool_arity_probe import BoolArityProbe +from pyviews.tests__overload_compatibility__bool_pair_probe import BoolPairProbe +from pyviews.tests__overload_compatibility__bool_probe import BoolProbe +from pyviews.tests__overload_compatibility__enum_probe import EnumProbe +from pyviews.tests__overload_compatibility__enum_comparison_probe import EnumComparisonProbe +from pyviews.tests__overload_compatibility__index_string_probe import IndexStringProbe +from pyviews.tests__overload_compatibility__numeric_domain_probe import NumericDomainProbe +from pyviews.tests__overload_compatibility__pair_probe import PairProbe +from pyviews.tests__overload_compatibility__qi_dispatch_probe import QiDispatchProbe +from pyviews.tests__overload_compatibility__i_alias_canonical import IAliasCanonical +from pyviews.tests__overload_compatibility__i_alias_legacy_int import IAliasLegacyInt +from pyviews.tests__overload_compatibility__i_alias_legacy_string import IAliasLegacyString +from pyviews.tests__overload_compatibility__i_bool_arity_canonical import IBoolArityCanonical +from pyviews.tests__overload_compatibility__i_bool_canonical import IBoolCanonical +from pyviews.tests__overload_compatibility__i_bool_pair_canonical import IBoolPairCanonical +from pyviews.tests__overload_compatibility__i_enum_legacy import IEnumLegacy +from pyviews.tests__overload_compatibility__i_enum_pair_legacy import IEnumPairLegacy +from pyviews.tests__overload_compatibility__i_byte_legacy import IByteLegacy +from pyviews.tests__overload_compatibility__i_int_bool_arity_legacy import IIntBoolArityLegacy +from pyviews.tests__overload_compatibility__i_int_bool_legacy import IIntBoolLegacy +from pyviews.tests__overload_compatibility__i_int_bool_pair_legacy import IIntBoolPairLegacy +from pyviews.tests__overload_compatibility__i_int_pair_legacy import IIntPairLegacy +from pyviews.tests__overload_compatibility__i_index_legacy import IIndexLegacy +from pyviews.tests__overload_compatibility__i_index_string_canonical import IIndexStringCanonical +from pyviews.tests__overload_compatibility__i_qi_canonical import IQiCanonical +from pyviews.tests__overload_compatibility__i_qi_legacy import IQiLegacy +from pyviews.tests__overload_compatibility__i_mode_canonical import IModeCanonical +from pyviews.tests__overload_compatibility__i_string_canonical import IStringCanonical +from pyviews.tests__overload_compatibility__i_string_pair_canonical import IStringPairCanonical +from pyviews.tests__overload_compatibility__i_bool_wide_canonical import IBoolWideCanonical +from pyviews.tests__overload_compatibility__mode import Mode + +calls = [] +conversion_events = [] +index_events = [] +runtime = importlib.import_module("pyviews._runtime") + +class AliasCanonical: + def foo(self, value): + calls.append(("alias-canonical", value)) + return 101 + +class AliasLegacyString: + def foo_version(self, value): + calls.append(("alias-legacy-string", value)) + return 201 + +class AliasLegacyInt: + def foo_version(self, value): + calls.append(("alias-legacy-int", value)) + return 202 + +class EnumLegacy: + def foo(self, value): + calls.append(("enum-legacy", int(value))) + return 301 + +class StringCanonical: + def foo_text(self, value): + calls.append(("string-canonical", value)) + return 401 + +class NumericString(str): + def __int__(self): + conversion_events.append(("numeric-string-int", str(self))) + return int(str(self)) + +class IndexString(str): + def __index__(self): + index_events.append(("index-string-index", str(self))) + return 7 + +class EnumPairLegacy: + def bar(self, mode, label): + calls.append(("enum-pair-legacy", int(mode), label)) + return 501 + +class StringPairCanonical: + def bar_text(self, mode, enabled): + calls.append(("string-pair-canonical", mode, enabled)) + return 601 + +class IntBoolLegacy: + def qux(self, value): + calls.append(("int-bool-legacy", value)) + return 701 + +class BoolCanonical: + def qux_bool(self, value): + calls.append(("bool-canonical", value)) + return 702 + +class IntBoolPairLegacy: + def quux(self, value, label): + calls.append(("int-bool-pair-legacy", value, label)) + return 801 + +class BoolPairCanonical: + def quux_bool(self, value, enabled): + calls.append(("bool-pair-canonical", value, enabled)) + return 802 + +class IntBoolArityLegacy: + def zap(self, value): + calls.append(("int-bool-arity-legacy", value)) + return 901 + +class BoolArityCanonical: + def zap_bool(self, value, enabled): + calls.append(("bool-arity-canonical", value, enabled)) + return 902 + +class IntPairLegacy: + def pick(self, first, second): + calls.append(("int-pair-legacy", first, second)) + return 1001 + +class BoolWideCanonical: + def pick_bool(self, first, second): + calls.append(("bool-wide-canonical", first, second)) + return 1002 + +class QiLegacy: + def use(self, target): + calls.append(("qi-legacy", target.__class__.__name__)) + return 1101 + +class QiCanonical: + def apply(self, target): + calls.append(("qi-canonical", target.__class__.__name__)) + return 1102 + +class IndexLegacy: + def sift(self, value): + calls.append(("index-legacy", value)) + return 1201 + +class IndexStringCanonical: + def sift_text(self, value): + calls.append(("index-string-canonical", value)) + return 1202 + +class ByteLegacy: + def rank(self, value): + calls.append(("byte-legacy", value)) + return 1301 + +class ModeCanonical: + def rank_mode(self, value): + calls.append(("mode-canonical", int(value))) + return 1302 + +results = {} +class UnexpectedIntError: + def __int__(self): + raise RuntimeError("unexpected-int-error") + +try: + runtime._dynwinrt_legacy_int_guard(UnexpectedIntError()) +except RuntimeError as error: + results["unexpected_int_error"] = str(error) + +with dw.RoApartment(1): + with IAliasCanonical.implement( + AliasCanonical(), + interfaces=[ + (IAliasLegacyString, AliasLegacyString()), + (IAliasLegacyInt, AliasLegacyInt()), + ], + ) as implementation: + value = AliasProbe._from_native(implementation.value._obj) + try: + results["alias_positional"] = value.foo_version("7") + results["alias_keyword"] = value.foo_version(value="8") + finally: + dw.release_projected(value) + + with IEnumLegacy.implement( + EnumLegacy(), + interfaces=[(IStringCanonical, StringCanonical())], + ) as implementation: + value = EnumProbe._from_native(implementation.value._obj) + try: + results["enum_positional"] = value.foo("1") + results["enum_keyword"] = value.foo(value="1") + results["text_positional"] = value.foo("not numeric") + results["enum_string_subclass"] = value.foo(NumericString("9")) + results["enum_string_conversion_count"] = len(conversion_events) + finally: + dw.release_projected(value) + + with IEnumPairLegacy.implement( + EnumPairLegacy(), + interfaces=[(IStringPairCanonical, StringPairCanonical())], + ) as implementation: + value = PairProbe._from_native(implementation.value._obj) + try: + results["nonoverlap_positional"] = value.bar("1", True) + results["nonoverlap_keyword"] = value.bar(mode="1", enabled=False) + finally: + dw.release_projected(value) + + with IIntBoolLegacy.implement( + IntBoolLegacy(), + interfaces=[(IBoolCanonical, BoolCanonical())], + ) as implementation: + value = BoolProbe._from_native(implementation.value._obj) + try: + results["bool_positional"] = value.qux(True) + results["bool_keyword"] = value.qux(value=False) + finally: + dw.release_projected(value) + + with IIntBoolPairLegacy.implement( + IntBoolPairLegacy(), + interfaces=[(IBoolPairCanonical, BoolPairCanonical())], + ) as implementation: + value = BoolPairProbe._from_native(implementation.value._obj) + try: + results["bool_pair_positional"] = value.quux(True, False) + results["bool_pair_keyword"] = value.quux(value=False, enabled=True) + finally: + dw.release_projected(value) + + with IIntBoolArityLegacy.implement( + IntBoolArityLegacy(), + interfaces=[(IBoolArityCanonical, BoolArityCanonical())], + ) as implementation: + value = BoolArityProbe._from_native(implementation.value._obj) + try: + results["bool_arity_positional"] = value.zap(True, False) + results["bool_arity_keyword"] = value.zap(value=False, enabled=True) + finally: + dw.release_projected(value) + + with IIntPairLegacy.implement( + IntPairLegacy(), + interfaces=[(IBoolWideCanonical, BoolWideCanonical())], + ) as implementation: + value = NumericDomainProbe._from_native(implementation.value._obj) + try: + results["numeric_pair_positional"] = value.pick(True, 5) + results["numeric_pair_keyword"] = value.pick(first=False, second=5) + results["numeric_pair_i32_max"] = value.pick(True, 2**31 - 1) + results["numeric_pair_i32_min"] = value.pick(False, -(2**31)) + results["numeric_pair_wide_high"] = value.pick(True, 2**31) + results["numeric_pair_wide_low"] = value.pick(False, -(2**31) - 1) + finally: + dw.release_projected(value) + + with IAliasCanonical.implement( + AliasCanonical(), + interfaces=[(IAliasLegacyString, AliasLegacyString())], + ) as target_implementation: + target = AliasProbe._from_native(target_implementation.value._obj) + try: + with IQiLegacy.implement( + QiLegacy(), + interfaces=[(IQiCanonical, QiCanonical())], + ) as implementation: + value = QiDispatchProbe._from_native(implementation.value._obj) + try: + results["qi_positional"] = value.use(target) + results["qi_keyword"] = value.use(target=target) + finally: + dw.release_projected(value) + finally: + dw.release_projected(target) + + with IIndexLegacy.implement( + IndexLegacy(), + interfaces=[(IIndexStringCanonical, IndexStringCanonical())], + ) as implementation: + value = IndexStringProbe._from_native(implementation.value._obj) + try: + results["index_string_positional"] = value.sift(IndexString("child")) + results["index_string_keyword"] = value.sift(value=IndexString("child")) + results["index_string_builtin"] = value.sift("child") + results["index_string_conversion_count"] = len(index_events) + finally: + dw.release_projected(value) + + comparison_events = [] + original_le = Mode.__le__ + Mode.__le__ = lambda self, other: ( + comparison_events.append(("unexpected-le", int(self))), + (_ for _ in ()).throw(RuntimeError("unexpected comparison")), + )[1] + try: + with IByteLegacy.implement( + ByteLegacy(), + interfaces=[(IModeCanonical, ModeCanonical())], + ) as implementation: + value = EnumComparisonProbe._from_native(implementation.value._obj) + try: + results["enum_comparison_positional"] = value.rank(Mode.One) + results["enum_comparison_keyword"] = value.rank(value=Mode.One) + results["enum_comparison_side_effects"] = len(comparison_events) + finally: + dw.release_projected(value) + finally: + Mode.__le__ = original_le + +print(json.dumps({"results": results, "calls": calls})) +"#; + fs::write(fixture.0.join("probe.py"), probe).unwrap(); + let output = Command::new(python()) + .args(["-B", "probe.py"]) + .current_dir(&fixture.0) + .output() + .unwrap(); + success(output.clone()); + let stdout = String::from_utf8(output.stdout).unwrap(); + assert!( + stdout.contains( + r#""results": {"unexpected_int_error": "unexpected-int-error", "alias_positional": 201, "alias_keyword": 201, "enum_positional": 301, "enum_keyword": 301, "text_positional": 401, "enum_string_subclass": 301, "enum_string_conversion_count": 1, "nonoverlap_positional": 601, "nonoverlap_keyword": 601, "bool_positional": 701, "bool_keyword": 701, "bool_pair_positional": 802, "bool_pair_keyword": 802, "bool_arity_positional": 902, "bool_arity_keyword": 902, "numeric_pair_positional": 1001, "numeric_pair_keyword": 1001, "numeric_pair_i32_max": 1001, "numeric_pair_i32_min": 1001, "numeric_pair_wide_high": 1002, "numeric_pair_wide_low": 1002, "qi_positional": 1101, "qi_keyword": 1101, "index_string_positional": 1201, "index_string_keyword": 1201, "index_string_builtin": 1202, "index_string_conversion_count": 2, "enum_comparison_positional": 1301, "enum_comparison_keyword": 1301, "enum_comparison_side_effects": 0}"# + ), + "{stdout}" + ); + assert!( + stdout.contains( + r#""calls": [["alias-legacy-string", "7"], ["alias-legacy-string", "8"], ["enum-legacy", 1], ["enum-legacy", 1], ["string-canonical", "not numeric"], ["enum-legacy", 9], ["string-pair-canonical", "1", true], ["string-pair-canonical", "1", false], ["int-bool-legacy", 1], ["int-bool-legacy", 0], ["bool-pair-canonical", true, false], ["bool-pair-canonical", false, true], ["bool-arity-canonical", true, false], ["bool-arity-canonical", false, true], ["int-pair-legacy", 1, 5], ["int-pair-legacy", 0, 5], ["int-pair-legacy", 1, 2147483647], ["int-pair-legacy", 0, -2147483648], ["bool-wide-canonical", true, 2147483648], ["bool-wide-canonical", false, -2147483649], ["qi-legacy", "IAliasCanonical"], ["qi-legacy", "IAliasCanonical"], ["index-legacy", 7], ["index-legacy", 7], ["index-string-canonical", "child"], ["byte-legacy", 1], ["byte-legacy", 1]]"# + ), + "{stdout}" + ); +} diff --git a/tools/dynwinrt-codegen/tests/python_overload_normalization_test.rs b/tools/dynwinrt-codegen/tests/python_overload_normalization_test.rs index da6eebdc..59a96f76 100644 --- a/tools/dynwinrt-codegen/tests/python_overload_normalization_test.rs +++ b/tools/dynwinrt-codegen/tests/python_overload_normalization_test.rs @@ -3,10 +3,12 @@ mod common; -use std::collections::HashSet; +use std::collections::{BTreeMap, BTreeSet, HashSet}; use std::path::Path; -use dynwinrt_codegen::meta::{self, InterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; +use dynwinrt_codegen::meta::{ + self, ClassMeta, InterfaceMeta, MethodMeta, ParamDirection, ParamMeta, +}; use dynwinrt_codegen::types::{TypeMeta, TypeMeta::AsyncOperation}; const WINDOWS_WINMD: &str = @@ -103,6 +105,132 @@ fn default_option_method_is_one_python_overload_group_with_legacy_alias() { ); } +#[test] +fn newly_dispatched_methods_end_with_their_guard_free_legacy_tier() { + let mode = TypeMeta::Enum { + namespace: "Contoso".into(), + name: "Mode".into(), + underlying: Box::new(TypeMeta::I32), + members: Vec::new(), + is_flags: false, + doc: None, + deprecated: None, + }; + let iterable = TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IIterable`1".into(), + piid: "faa585ea-6214-4217-afda-7f46de5869b3".into(), + args: vec![TypeMeta::String], + }; + let param = |name: &str, typ| ParamMeta { + name: name.into(), + typ, + direction: ParamDirection::In, + }; + let overload = |name: &str, raw_name: &str, index: usize, params: Vec| MethodMeta { + name: name.into(), + raw_name: raw_name.into(), + vtable_index: index, + params, + ..Default::default() + }; + let interface = InterfaceMeta { + name: "IWidget".into(), + namespace: "Contoso".into(), + iid: "11111111-1111-1111-1111-111111111111".into(), + methods: vec![ + overload( + "OpenAsync", + "OpenAsync", + 6, + vec![param("mode", mode.clone())], + ), + overload( + "OpenWithOptionsAsync", + "OpenAsync", + 7, + vec![param("mode", mode.clone()), param("options", mode.clone())], + ), + overload( + "FindAsync", + "FindAsync", + 8, + vec![param("id", TypeMeta::Guid)], + ), + overload( + "FindWithOptionsAsync", + "FindAsync", + 9, + vec![ + param("id", TypeMeta::Guid), + param("options", TypeMeta::String), + ], + ), + overload( + "CountAsync", + "CountAsync", + 10, + vec![param("count", TypeMeta::I32)], + ), + overload( + "CountWithOptionsAsync", + "CountAsync", + 11, + vec![ + param("count", TypeMeta::I32), + param("options", TypeMeta::Bool), + ], + ), + overload( + "LoadAsync", + "LoadAsync", + 12, + vec![param("items", iterable.clone())], + ), + overload( + "LoadWithOptionsAsync", + "LoadAsync", + 13, + vec![param("items", iterable), param("options", TypeMeta::Bool)], + ), + ], + ..Default::default() + }; + let known = HashSet::from(["IWidget".into(), "Mode".into()]); + let runtime = common::generate_interface(&interface, &known, &HashSet::new()); + let wrapper = &runtime[runtime.rfind("\nclass IWidget:").unwrap()..]; + + for (name, parameter, private, conversion) in [ + ("open_async", "mode", "_open_async_6", "int(mode)"), + ("find_async", "id", "_find_async_8", "_dynwinrt_guid(id)"), + ( + "count_async", + "count", + "_count_async_10", + "DynWinRTValue.from_i32(count)", + ), + ( + "load_async", + "items", + "_load_async_12", + "_dynwinrt_vector(items", + ), + ] { + let body = member_body(wrapper, name); + let tier = format!( + "return _dynwinrt_legacy_call(self.{private}, ('{parameter}',), args, kwargs, '{name}')" + ); + assert!( + body.contains(&tier), + "{name} lacks its final legacy tier:\n{body}" + ); + assert!( + member_body(wrapper, private).contains(conversion), + "{private} lost its permissive conversion:\n{runtime}" + ); + } +} + #[test] fn real_storage_folder_default_options_method_is_normalized() { if !Path::new(WINDOWS_WINMD).exists() { @@ -130,3 +258,654 @@ fn real_storage_folder_default_options_method_is_normalized() { ); assert_eq!(stub.matches("def create_file_async(").count(), 2, "{stub}"); } + +/// Port of the generator's `to_snake_case` for the ABI names used below. +fn to_snake_case(name: &str) -> String { + let characters = name.chars().collect::>(); + let mut result = String::new(); + for (index, &character) in characters.iter().enumerate() { + if character.is_uppercase() { + if index > 0 { + let previous = characters[index - 1]; + let next_lower = characters + .get(index + 1) + .is_some_and(|next| next.is_lowercase()); + if previous.is_lowercase() + || previous.is_ascii_digit() + || (next_lower && previous.is_uppercase()) + { + result.push('_'); + } + } + result.extend(character.to_lowercase()); + } else { + result.push(character); + } + } + let tokens = result + .trim_start_matches('_') + .split('_') + .collect::>(); + let mut merged = Vec::new(); + let mut index = 0; + while index < tokens.len() { + if tokens[index] == "u" + && tokens + .get(index + 1) + .is_some_and(|next| ["int8", "int16", "int32", "int64"].contains(next)) + { + merged.push(format!("u{}", tokens[index + 1])); + index += 2; + } else { + merged.push(tokens[index].to_string()); + index += 1; + } + } + let result = merged.join("_"); + if ["from", "import", "global", "print", "lambda", "pass", "del"].contains(&result.as_str()) { + format!("{result}_") + } else { + result + } +} + +/// Public members of `class_name` and its `Like` protocol. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +struct Member { + definitions: usize, + overloads: usize, + assigned: bool, +} + +fn class_members(code: &str, class_name: &str) -> BTreeMap { + let owners = [class_name.to_string(), format!("{class_name}Like")]; + let mut members = BTreeMap::::new(); + let mut inside = false; + let mut decorated = false; + for line in code.lines() { + if let Some(header) = line.strip_prefix("class ") { + let name = header + .split(|character| character == '(' || character == ':') + .next() + .unwrap_or_default(); + inside = owners.iter().any(|owner| owner == name); + continue; + } + if !line.is_empty() && !line.starts_with(' ') { + inside = false; + } + if !inside { + continue; + } + let Some(member) = line.strip_prefix(" ") else { + continue; + }; + if member == "@overload" { + decorated = true; + continue; + } + if member.starts_with('@') { + continue; + } + if let Some(definition) = member.strip_prefix("def ") { + let name = definition.split('(').next().unwrap_or_default().to_string(); + let entry = members.entry(name).or_default(); + entry.definitions += 1; + entry.overloads += usize::from(decorated); + } else if let Some((name, _)) = member.split_once(" = ") + && name + .chars() + .all(|character| character.is_ascii_alphanumeric() || character == '_') + { + members.entry(name.to_string()).or_default().assigned = true; + } + decorated = false; + } + members.retain(|name, _| !name.starts_with('_')); + members +} + +fn single_definition() -> Member { + Member { + definitions: 1, + overloads: 0, + assigned: false, + } +} + +fn real_class(namespace: &str, name: &str) -> Option<(ClassMeta, String, String)> { + let class = meta::parse_class(WINDOWS_WINMD, namespace, name)?; + let deps = meta::resolve_python_dependencies(WINDOWS_WINMD, &[class.clone()], &[], &[]); + let mut known = HashSet::from([class.name.clone()]); + known.extend(deps.classes.iter().map(|class| class.name.clone())); + known.extend( + deps.interfaces + .iter() + .map(|interface| interface.name.clone()), + ); + known.extend(deps.enums.iter().filter_map(|typ| match typ { + TypeMeta::Enum { name, .. } => Some(name.clone()), + _ => None, + })); + let runtime = common::generate_class(&class, &known, &HashSet::new(), &HashSet::new()); + let stub = common::generate_class_stub(&class, &known, &HashSet::new(), &HashSet::new()); + Some((class, runtime, stub)) +} + +/// Every public method name emitted before CLR-name grouping: the snake-case ABI +/// names, which were either dispatcher names or their compatibility aliases. +fn previous_method_names(class: &ClassMeta) -> BTreeSet { + class + .factory_interfaces + .iter() + .chain(class.static_interfaces.iter()) + .chain(class.default_interface.iter()) + .chain(class.required_interfaces.iter()) + .filter(|interface| interface.iid != "30d5a829-7fa4-4026-83bb-d75bae4ea99e") + .flat_map(|interface| interface.methods.iter()) + .filter(|method| { + !method.is_property_getter + && !method.is_property_setter + && !method.is_event_add + && !method.is_event_remove + }) + .map(|method| to_snake_case(&method.name)) + .collect() +} + +const REPRESENTATIVE_CLASSES: &[(&str, &str)] = &[ + ("Windows.Storage", "StorageFile"), + ("Windows.Storage", "StorageFolder"), + ("Windows.UI.Notifications", "ToastNotifier"), + ("Windows.UI.Notifications", "TileUpdateManagerForUser"), + ("Windows.Globalization.NumberFormatting", "DecimalFormatter"), + ("Windows.System", "Launcher"), + ("Windows.Web.Http", "HttpClient"), + ("Windows.Globalization", "Calendar"), + ("Windows.Data.Xml.Dom", "XmlDocument"), + ("Windows.Storage.Streams", "RandomAccessStream"), + ("Windows.Storage.Streams", "DataWriter"), + ("Windows.Networking.Sockets", "StreamSocket"), + ("Windows.Networking.Sockets", "StreamWebSocket"), + ("Windows.UI.Composition.Interactions", "InteractionTracker"), + ("Windows.UI.Xaml", "PropertyMetadata"), + ("Windows.Devices.Enumeration", "DeviceInformation"), +]; + +#[test] +fn real_classes_keep_every_previous_public_method_name() { + if !Path::new(WINDOWS_WINMD).exists() { + eprintln!("Skipping: Windows.winmd not found"); + return; + } + for (namespace, name) in REPRESENTATIVE_CLASSES { + let (class, runtime, stub) = real_class(namespace, name).expect("class metadata"); + let runtime_members = class_members(&runtime, name); + let stub_members = class_members(&stub, name); + for previous in previous_method_names(&class) { + assert!( + runtime_members.contains_key(&previous), + "{namespace}.{name}.py lost `{previous}`:\n{runtime}" + ); + assert!( + stub_members.contains_key(&previous), + "{namespace}.{name}.pyi lost `{previous}`:\n{stub}" + ); + } + } +} + +#[test] +fn real_classes_project_overloads_under_documented_clr_names() { + if !Path::new(WINDOWS_WINMD).exists() { + eprintln!("Skipping: Windows.winmd not found"); + return; + } + // (class, documented name, overloads, previous names kept as aliases) + let expectations: &[(&str, &str, &str, usize, &[&str])] = &[ + ( + "Windows.Storage", + "StorageFile", + "copy_async", + 3, + &[ + "copy_overload", + "copy_overload_default_options", + "copy_overload_default_name_and_options", + ], + ), + ( + "Windows.Storage", + "StorageFile", + "move_async", + 3, + &[ + "move_overload", + "move_overload_default_options", + "move_overload_default_name_and_options", + ], + ), + ( + "Windows.UI.Notifications", + "ToastNotifier", + "update", + 2, + &["update_with_tag", "update_with_tag_and_group"], + ), + ( + "Windows.Globalization.NumberFormatting", + "DecimalFormatter", + "format", + 2, + &[], + ), + ( + "Windows.System", + "Launcher", + "launch_file_async", + 2, + &["launch_file_with_options_async"], + ), + ( + "Windows.System", + "Launcher", + "launch_uri_async", + 3, + &[ + "launch_uri_with_options_async", + "launch_uri_with_data_async", + ], + ), + ( + "Windows.Web.Http", + "HttpClient", + "get_async", + 2, + &["get_with_option_async"], + ), + ( + "Windows.Globalization", + "Calendar", + "month_as_string", + 2, + &["month_as_full_string"], + ), + ( + "Windows.Data.Xml.Dom", + "XmlDocument", + "load_xml", + 2, + &["load_xml_with_settings"], + ), + ( + "Windows.Storage.Streams", + "RandomAccessStream", + "copy_async", + 2, + &["copy_size_async"], + ), + ]; + let mut generated = BTreeMap::new(); + for (namespace, name, documented, overloads, aliases) in expectations { + let (_, runtime, stub) = generated + .entry((*namespace, *name)) + .or_insert_with(|| real_class(namespace, name).expect("class metadata")); + let runtime_members = class_members(runtime, name); + let stub_members = class_members(stub, name); + assert_eq!( + runtime_members.get(*documented), + Some(&single_definition()), + "{name}.{documented} must be one runtime dispatcher:\n{runtime}" + ); + assert!( + runtime.contains(&format!("def {documented}(self, *args, **kwargs):")) + || runtime.contains(&format!("def {documented}(*args, **kwargs):")), + "{name}.{documented} must dispatch overloads:\n{runtime}" + ); + assert_eq!( + stub_members.get(*documented), + Some(&Member { + definitions: *overloads, + overloads: *overloads, + assigned: false, + }), + "{name}.{documented} must declare {overloads} overloads:\n{stub}" + ); + for alias in *aliases { + assert_eq!( + runtime_members.get(*alias).map(|member| member.assigned), + Some(true), + "{name}.{alias} must stay available as an alias:\n{runtime}" + ); + assert!( + stub_members.contains_key(*alias), + "{name}.{alias} must stay typed:\n{stub}" + ); + } + } + + // The runtime dispatcher keeps all three ABI overloads, trying Int64 before + // UInt64 (larger values) and Double. + let (_, runtime, _) = + &generated[&("Windows.Globalization.NumberFormatting", "DecimalFormatter")]; + let dispatcher = &runtime[runtime + .find(" def format(self, *args, **kwargs):") + .unwrap()..]; + let order = ["self._format_6(", "self._format_7(", "self._format_8("].map(|call| { + dispatcher + .find(call) + .unwrap_or_else(|| panic!("{call}:\n{runtime}")) + }); + assert!(order.is_sorted(), "{runtime}"); + assert!(runtime.contains("from_i64(value)") && runtime.contains("from_u64(value)")); + + // INumberFormatter2's FormatInt/FormatUInt/FormatDouble are real methods, not + // aliases of the INumberFormatter.Format overloads. + let (_, runtime, _) = + &generated[&("Windows.Globalization.NumberFormatting", "DecimalFormatter")]; + let members = class_members(runtime, "DecimalFormatter"); + for method in ["format_int", "format_u_int", "format_double"] { + assert_eq!(members.get(method), Some(&single_definition()), "{runtime}"); + } + let format_int = member_body(runtime, "format_int"); + assert!( + format_int.find("self._format_6(").unwrap() + < format_int.find("self._format_int_6(").unwrap(), + "the compatibility dispatcher must try the exact interface method that format_int used before CLR grouping:\n{runtime}" + ); + assert_eq!( + runtime.matches(" def _format_6(").count(), + 1, + "the canonical implementation should be defined once:\n{runtime}" + ); + assert!( + runtime.contains("return _INumberFormatter2.method(6)"), + "{runtime}" + ); +} + +#[test] +fn real_collisions_keep_previous_python_names() { + if !Path::new(WINDOWS_WINMD).exists() { + eprintln!("Skipping: Windows.winmd not found"); + return; + } + // IWebSocket.CloseWithStatus is a `Close` overload, but `close()` is the + // generated IClosable member of every WebSocket runtime class. + let (_, runtime, stub) = + real_class("Windows.Networking.Sockets", "StreamWebSocket").expect("class metadata"); + let members = class_members(&runtime, "StreamWebSocket"); + assert_eq!( + members.get("close_with_status"), + Some(&single_definition()), + "{runtime}" + ); + assert!(runtime.contains(" def close(self):\n"), "{runtime}"); + assert!( + stub.contains("def close_with_status(self, code: int, reason: str)"), + "{stub}" + ); + + // `CreateTileUpdaterForApplication()` is an overload name of + // `CreateTileUpdaterForApplicationForUser`, so the documented + // `CreateTileUpdaterForApplication(String)` cannot take over that name. + let (_, runtime, stub) = + real_class("Windows.UI.Notifications", "TileUpdateManagerForUser").expect("class metadata"); + let members = class_members(&runtime, "TileUpdateManagerForUser"); + assert_eq!( + members.get("create_tile_updater_for_application_for_user"), + Some(&single_definition()), + "{runtime}" + ); + assert!( + runtime.contains( + " create_tile_updater_for_application = create_tile_updater_for_application_for_user\n" + ), + "{runtime}" + ); + assert_eq!( + members.get("create_tile_updater_for_application_with_id"), + Some(&single_definition()), + "{runtime}" + ); + assert!( + stub.contains("def create_tile_updater_for_application(self) ->"), + "{stub}" + ); +} + +/// The body of `def {name}(` in `code`, up to the next member. +fn member_body<'a>(code: &'a str, name: &str) -> &'a str { + let start = code + .find(&format!(" def {name}(")) + .unwrap_or_else(|| panic!("missing `{name}`:\n{code}")); + let rest = &code[start..]; + let end = rest[1..] + .find("\n def ") + .or_else(|| rest[1..].find("\n @")) + .map_or(rest.len(), |end| end + 1); + &rest[..end] +} + +#[test] +fn real_former_standalone_names_keep_calling_their_own_overload() { + if !Path::new(WINDOWS_WINMD).exists() { + eprintln!("Skipping: Windows.winmd not found"); + return; + } + // INumberFormatter.Format(UInt64) was `format_u_int`. Through `format`, + // format_u_int(5) would reach Format(Int64); the alias must keep UInt64. + let interfaces = + meta::parse_interfaces(WINDOWS_WINMD, "Windows.Globalization.NumberFormatting"); + let formatter = interfaces + .iter() + .find(|interface| interface.name == "INumberFormatter") + .expect("INumberFormatter metadata"); + let unsigned = formatter + .methods + .iter() + .find(|method| method.name == "FormatUInt") + .expect("FormatUInt"); + let runtime = common::generate_interface(formatter, &HashSet::new(), &HashSet::new()); + let target = format!("_format_{}", unsigned.vtable_index); + assert!( + runtime.contains(&format!("\n format_u_int = {target}\n")), + "{runtime}" + ); + let body = member_body(&runtime, &target); + assert!( + body.contains(&format!( + "_INumberFormatter.method({})", + unsigned.vtable_index + )) && body.contains("DynWinRTValue.from_u64(value)"), + "{body}" + ); + let stub = common::generate_interface_stub(formatter, &HashSet::new(), &HashSet::new()); + assert!( + stub.contains(" def format_u_int(self, value: int) -> str"), + "{stub}" + ); + + // PropertyMetadata.Create(Object) was `create_with_default_value`. Through + // `create`, a projected object would reach Create(CreateDefaultValueCallback). + let (class, runtime, stub) = + real_class("Windows.UI.Xaml", "PropertyMetadata").expect("class metadata"); + let with_default = class + .static_interfaces + .iter() + .flat_map(|interface| interface.methods.iter()) + .find(|method| method.name == "CreateWithDefaultValue") + .expect("CreateWithDefaultValue"); + let target = format!("_create_{}", with_default.vtable_index); + assert!( + runtime.contains(&format!("\n create_with_default_value = {target}\n")), + "{runtime}" + ); + let body = member_body(&runtime, &target); + assert!( + body.contains(&format!( + "_IPropertyMetadataStatics.method({})", + with_default.vtable_index + )) && body.contains("getattr(default_value, '_obj', default_value)"), + "{body}" + ); + assert_eq!( + class_members(&stub, "PropertyMetadata").get("create_with_default_value"), + Some(&single_definition()), + "{stub}" + ); +} + +static NEXT_FIXTURE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + +struct Fixture(std::path::PathBuf); + +impl Fixture { + fn new() -> Self { + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("target") + .join(format!( + "ovl{}-{}", + std::process::id(), + NEXT_FIXTURE.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + )); + std::fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } +} + +fn python() -> std::path::PathBuf { + std::env::var_os("DYNWINRT_TEST_PYTHON") + .map(std::path::PathBuf::from) + .unwrap_or_else(|| std::path::PathBuf::from("python")) +} + +fn has_mypy() -> bool { + let available = std::process::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"), + "DYNWINRT_REQUIRE_MYPY=1 but mypy is unavailable", + ); + available +} + +#[test] +fn real_merged_overload_stubs_pass_strict_mypy() { + if !Path::new(WINDOWS_WINMD).exists() || !has_mypy() { + eprintln!("Skipping: Windows.winmd or mypy unavailable"); + return; + } + let fixture = Fixture::new(); + let output = std::process::Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args(["generate", "--winmd", WINDOWS_WINMD, "--class-name"]) + .arg( + "Windows.Globalization.NumberFormatting.DecimalFormatter,Windows.Storage.StorageFile,\ + Windows.System.Launcher,Windows.Data.Xml.Dom.XmlDocument,\ + Windows.Globalization.Calendar,Windows.Storage.Streams.RandomAccessStream,\ + Windows.Storage.Streams.InMemoryRandomAccessStream,Windows.Storage.Streams.DataWriter", + ) + .args(["--lang", "py", "--output"]) + .arg(fixture.0.join("sdk")) + .output() + .unwrap(); + assert!( + output.status.success(), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + std::fs::write( + fixture.0.join("consumer.py"), + r#"from typing import assert_type +from sdk.windows.data.xml.dom import XmlDocument, XmlLoadSettings +from sdk.windows.foundation import Uri +from sdk.windows.globalization import Calendar +from sdk.windows.globalization.number_formatting import DecimalFormatter +from sdk.windows.storage import NameCollisionOption, StorageFile, StorageFolder +from sdk.windows.storage.streams import DataWriter, InMemoryRandomAccessStream, RandomAccessStream +from sdk.windows.system import Launcher + +def formatting(formatter: DecimalFormatter) -> None: + assert_type(formatter.format(5), str) + assert_type(formatter.format(2.5), str) + assert_type(formatter.format_int(5), str) + assert_type(formatter.format_u_int(5), str) + +async def storage(file: StorageFile, folder: StorageFolder) -> None: + assert_type(await file.copy_async(folder), StorageFile | None) + assert_type(await file.copy_async(folder, "name.txt"), StorageFile | None) + option = NameCollisionOption.ReplaceExisting + assert_type(await file.copy_async(folder, "name.txt", option), StorageFile | None) + assert_type(await file.copy_overload(folder, "name.txt", option), StorageFile | None) + await file.move_async(folder) + +async def launching(file: StorageFile, uri: Uri) -> None: + assert_type(await Launcher.launch_file_async(file), bool) + assert_type(await Launcher.launch_uri_async(uri), bool) + +def xml(document: XmlDocument, settings: XmlLoadSettings) -> None: + document.load_xml("") + document.load_xml("", settings) + document.load_xml_with_settings("", settings) + +def calendar(value: Calendar) -> None: + assert_type(value.month_as_string(), str) + assert_type(value.month_as_string(3), str) + assert_type(value.month_as_full_string(), str) + +async def streams(source: InMemoryRandomAccessStream, target: InMemoryRandomAccessStream) -> None: + assert_type(await RandomAccessStream.copy_async(source, target), int) + assert_type(await RandomAccessStream.copy_async(source, target, 4), int) + assert_type(await RandomAccessStream.copy_size_async(source, target, 4), int) + DataWriter(source) +"#, + ) + .unwrap(); + let output = std::process::Command::new(python()) + .args([ + "-B", + "-m", + "mypy", + "--strict", + "--no-incremental", + "--follow-imports=normal", + "--no-pretty", + "--show-error-codes", + "--cache-dir", + ".mypy_cache", + "sdk", + "consumer.py", + ]) + .env( + "MYPYPATH", + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("bindings") + .join("py"), + ) + .current_dir(&fixture.0) + .output() + .unwrap(); + let text = format!( + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + assert!( + !text.contains("overload-cannot-match") && !text.contains("overload-overlap"), + "{text}" + ); + assert!(output.status.success(), "{text}"); +} diff --git a/tools/dynwinrt-codegen/tests/snapshots/data_writer_py/data_writer.py b/tools/dynwinrt-codegen/tests/snapshots/data_writer_py/data_writer.py index 050fcba2..42b53708 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/data_writer_py/data_writer.py +++ b/tools/dynwinrt-codegen/tests/snapshots/data_writer_py/data_writer.py @@ -8,6 +8,7 @@ _DynWinRTObject, _property, _weakref_ref, _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_can_cast, _dynwinrt_create_delegate, _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _dynwinrt_guid, + _dynwinrt_legacy_call, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, _dynwinrt_ticks_to_timedelta, _dynwinrt_timedelta_to_ticks, _dynwinrt_as_interface, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, @@ -26,6 +27,8 @@ IID_IDataWriter = WinGUID.parse('64b89265-d341-4922-b38a-dd4af8808c4e') IID_IDataWriterFactory = WinGUID.parse('338c67c2-8b84-4c2b-9c50-7b8767847a1f') IID_IClosable = WinGUID.parse('30d5a829-7fa4-4026-83bb-d75bae4ea99e') +IID_ARG_Windows_Storage_Streams_IBuffer = WinGUID.parse('905a0fe0-bc53-11df-8c49-001e4fc686da') +IID_ARG_Windows_Storage_Streams_IOutputStream = WinGUID.parse('905a0fe6-bc53-11df-8c49-001e4fc686da') _IDataWriter = DynWinRTType.register_interface( "IDataWriter", IID_IDataWriter) \ @@ -108,6 +111,9 @@ def __new__(cls, *args, **kwargs): _bound = _dynwinrt_bind_overload(('output_stream',), args, kwargs) if _bound is not None and isinstance(_bound[0], _dynwinrt_symbol('windows__storage__streams__i_output_stream', 'IOutputStream')): return cls.create_data_writer(_bound[0]) + _bound = _dynwinrt_bind_overload(('output_stream',), args, kwargs) + if _bound is not None and (isinstance(_bound[0], _dynwinrt_symbol('windows__storage__streams__i_output_stream', 'IOutputStream')) or _dynwinrt_can_cast(_bound[0], IID_ARG_Windows_Storage_Streams_IOutputStream)): + return cls.create_data_writer(_bound[0]) return super().__new__(cls) def _set_native(self, obj: DynWinRTValue): @@ -135,6 +141,10 @@ def __init__(self, *args, **kwargs): if _bound is not None and isinstance(_bound[0], _dynwinrt_symbol('windows__storage__streams__i_output_stream', 'IOutputStream')): self._set_native(type(self).create_data_writer(_bound[0])._obj) return + _bound = _dynwinrt_bind_overload(('output_stream',), args, kwargs) + if _bound is not None and (isinstance(_bound[0], _dynwinrt_symbol('windows__storage__streams__i_output_stream', 'IOutputStream')) or _dynwinrt_can_cast(_bound[0], IID_ARG_Windows_Storage_Streams_IOutputStream)): + self._set_native(type(self).create_data_writer(_bound[0])._obj) + return raise TypeError("No matching constructor for DataWriter") @staticmethod @@ -168,12 +178,27 @@ def write_byte(self, value: int) -> None: def write_bytes(self, value: DynWinRTArray | bytes | bytearray | Sequence[int]) -> None: _IDataWriter.method(12).invoke(self._obj, [_dynwinrt_array(value, lambda item: DynWinRTValue.from_u8(item), DynWinRTType.u8_type(), True)]) - def write_buffer(self, buffer: 'IBuffer') -> None: + def _write_buffer_13(self, buffer: 'IBuffer') -> None: _IDataWriter.method(13).invoke(self._obj, [getattr(buffer, '_obj', buffer)]) - def write_buffer_range(self, buffer: 'IBuffer', start: int, count: int) -> None: + def _write_buffer_14(self, buffer: 'IBuffer', start: int, count: int) -> None: _IDataWriter.method(14).invoke(self._obj, [getattr(buffer, '_obj', buffer), DynWinRTValue.from_u32(start), DynWinRTValue.from_u32(count)]) + def write_buffer(self, *args, **kwargs): + _bound = _dynwinrt_bind_overload(('buffer',), args, kwargs) + if _bound is not None and isinstance(_bound[0], _dynwinrt_symbol('windows__storage__streams__i_buffer', 'IBuffer')): + return self._write_buffer_13(*_bound) + _bound = _dynwinrt_bind_overload(('buffer', 'start', 'count',), args, kwargs) + if _bound is not None and isinstance(_bound[0], _dynwinrt_symbol('windows__storage__streams__i_buffer', 'IBuffer')) and isinstance(_bound[1], int) and not isinstance(_bound[1], bool) and not isinstance(_bound[1], __import__('enum').Enum) and 0 <= _bound[1] <= 4294967295 and isinstance(_bound[2], int) and not isinstance(_bound[2], bool) and not isinstance(_bound[2], __import__('enum').Enum) and 0 <= _bound[2] <= 4294967295: + return self._write_buffer_14(*_bound) + _bound = _dynwinrt_bind_overload(('buffer',), args, kwargs) + if _bound is not None and (isinstance(_bound[0], _dynwinrt_symbol('windows__storage__streams__i_buffer', 'IBuffer')) or _dynwinrt_can_cast(_bound[0], IID_ARG_Windows_Storage_Streams_IBuffer)): + return self._write_buffer_13(*_bound) + _bound = _dynwinrt_bind_overload(('buffer', 'start', 'count',), args, kwargs) + if _bound is not None and (isinstance(_bound[0], _dynwinrt_symbol('windows__storage__streams__i_buffer', 'IBuffer')) or _dynwinrt_can_cast(_bound[0], IID_ARG_Windows_Storage_Streams_IBuffer)) and isinstance(_bound[1], int) and not isinstance(_bound[1], bool) and not isinstance(_bound[1], __import__('enum').Enum) and 0 <= _bound[1] <= 4294967295 and isinstance(_bound[2], int) and not isinstance(_bound[2], bool) and not isinstance(_bound[2], __import__('enum').Enum) and 0 <= _bound[2] <= 4294967295: + return self._write_buffer_14(*_bound) + return _dynwinrt_legacy_call(self._write_buffer_13, ('buffer',), args, kwargs, 'write_buffer') + def write_boolean(self, value: bool) -> None: _IDataWriter.method(15).invoke(self._obj, [DynWinRTValue.from_bool(value)]) @@ -236,6 +261,8 @@ def unicode_encoding(self, value: 'UnicodeEncoding'): def byte_order(self, value: 'ByteOrder'): _IDataWriter.method(10).invoke(self._obj, [DynWinRTValue.enum_value(DynWinRTType.enum_type('Windows.Storage.Streams.ByteOrder', ['LittleEndian', 'BigEndian'], [0, 1]), int(value))]) + write_buffer_range = _write_buffer_14 + def close(self): if self._closed: return diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/www_form_url_decoder.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/www_form_url_decoder.py index 1d714c6f..c1c7cd1b 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/www_form_url_decoder.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/www_form_url_decoder.py @@ -24,6 +24,7 @@ IID_IWwwFormUrlDecoderRuntimeClassFactory = WinGUID.parse('5b8c6b3d-24ae-41b5-a1bf-f0c3d544845b') IID_IVectorView_IWwwFormUrlDecoderEntry = DynWinRTType.parameterized(WinGUID.parse('bbe1fa4c-b0e3-4583-baef-1f1b2e483e56'), [DynWinRTType.interface(WinGUID.parse('125e7431-f678-4e8e-b670-20a9b06c512d'))]).iid() IID_IIterable_IWwwFormUrlDecoderEntry = DynWinRTType.parameterized(WinGUID.parse('faa585ea-6214-4217-afda-7f46de5869b3'), [DynWinRTType.interface(WinGUID.parse('125e7431-f678-4e8e-b670-20a9b06c512d'))]).iid() +IID_ARG_Windows_Foundation_IWwwFormUrlDecoderEntry = WinGUID.parse('125e7431-f678-4e8e-b670-20a9b06c512d') _IWwwFormUrlDecoderRuntimeClass = DynWinRTType.register_interface( "IWwwFormUrlDecoderRuntimeClass", IID_IWwwFormUrlDecoderRuntimeClass) \