Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1688598
Plan Python overload members once per class
lei9444 Sep 24, 2026
105a09a
Accept runtime-class objects for interface overload parameters
lei9444 Sep 24, 2026
6d11ba7
Group Python overloads by documented CLR method name
lei9444 Sep 24, 2026
f5e90a9
Deduplicate identical Python overload signatures
lei9444 Sep 24, 2026
a604399
Preserve exact interface targets in compatibility dispatch
lei9444 Sep 24, 2026
38ae09a
Pin compatibility dispatchers to stable private targets
lei9444 Sep 24, 2026
d874ffd
Preserve guard-free overload conversion fallbacks
lei9444 Sep 24, 2026
12915fd
Preserve exact legacy overload routing
lei9444 Sep 28, 2026
4d95df6
Require live overload compatibility regression
lei9444 Sep 28, 2026
4ccc91f
Update CI contract for overload regression
lei9444 Sep 28, 2026
3ce960b
Preserve bool calls to legacy numeric overloads
lei9444 Sep 28, 2026
094a1cd
Model legacy overload conversion domains
lei9444 Sep 28, 2026
cfe8153
Plan overload conversion compatibility by domain
lei9444 Sep 28, 2026
7f22040
Merge origin/main into Python overload fixes
lei9444 Sep 28, 2026
9eed365
Preserve cast diagnostics across runtime versions
lei9444 Sep 28, 2026
1f6a5f9
Propagate unexpected interface cast failures
lei9444 Sep 28, 2026
e93d463
Harden Python overload helpers and Async QI guards
lei9444 Sep 28, 2026
cf2b452
Merge main Python delegate projections into overload fixes
lei9444 Sep 29, 2026
21a538d
Keep ThreadPool delegate overload names distinct
lei9444 Sep 29, 2026
cc47ba4
Gate ThreadPool naming exception on exact WinRT ABI
lei9444 Sep 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 22 additions & 1 deletion bindings/py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>` values are projected as `T | None`; native values,
`None`, and generated `IReference_*` wrappers are accepted as inputs.
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions bindings/py/dynwinrt.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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: ...
Expand Down
160 changes: 160 additions & 0 deletions bindings/py/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<bool> {
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()`.
///
Expand Down Expand Up @@ -2675,6 +2698,84 @@ pub fn get_computer_name() -> PyResult<String> {
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<QueryCounts>,
}

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<QueryCounts>) {
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::<Self>() };
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::<Self>() };
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::<Self>() };
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::<Self>())) };
}
remaining
}
}

#[repr(C)]
struct TestDelegateVtbl {
Expand Down Expand Up @@ -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::<pyo3::exceptions::PyOSError>(py));
assert_eq!(
error
.value(py)
.getattr("winerror")
.unwrap()
.extract::<i32>()
.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::<PyRuntimeError>(py));
assert!(released.to_string().contains("released"));
});
}

#[test]
fn detached_invocation_releases_the_gil_on_the_same_native_thread() {
use dynwinrt::{
Expand Down
5 changes: 4 additions & 1 deletion eng/ci/test_ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down
54 changes: 50 additions & 4 deletions tests/e2e/e2e_specs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
]
},
{
Expand Down Expand Up @@ -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"] }
]
},
{
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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"] }
]
},
{
Expand All @@ -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",
Expand Down Expand Up @@ -646,7 +691,8 @@
"args": ["<root><child /></root>"],
"property_path": ["document_element", "tag_name"],
"expected": "root"
}
},
{ "kind": "xml_document_load_overloads", "member": "load_xml", "langs": ["py"] }
]
},
{
Expand Down
9 changes: 9 additions & 0 deletions tests/e2e/e2e_specs.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"kind": {
"enum": [
"property_equals",
"language_comprehensive",
"property_exists",
"property_in_range",
"method_equals",
Expand Down Expand Up @@ -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",
Expand Down
Loading
Loading