diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 70a2bd8e..f1175164 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -35,6 +35,10 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Verify coverage threshold fixtures + shell: pwsh + run: .\eng\coverage\coverage.threshold.tests.ps1 + - uses: dtolnay/rust-toolchain@stable with: components: llvm-tools-preview diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index db6ce142..89b97feb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -57,7 +57,9 @@ Run the mixed-language coverage pipeline: The script builds instrumented Python and Node native modules, runs the Rust/Python/JavaScript tests and E2E suite, and writes HTML, LCOV, XML, and JSON -reports under `artifacts\coverage`. +reports under `artifacts\coverage`. Default line gates are Rust 45%, Python +70%, and JavaScript 18%; override them with the `-Min*LineCoverage` parameters, +or re-check an existing report tree without rerunning tests via `-ValidateOnly`. ### Code Style diff --git a/bindings/py/src/async_runtime.rs b/bindings/py/src/async_runtime.rs index 87793a5e..644dcbb9 100644 --- a/bindings/py/src/async_runtime.rs +++ b/bindings/py/src/async_runtime.rs @@ -5,6 +5,10 @@ use std::cell::Cell; use std::future::IntoFuture; use std::sync::{Arc, Mutex, MutexGuard}; +use crate::errors::{ + map_dynwinrt_error, map_dynwinrt_error_with_context, map_windows_error_with_context, +}; +use crate::runtime::DynWinRTValue; use pyo3::exceptions::PyRuntimeError; use pyo3::prelude::*; use windows::Win32::Foundation::CO_E_NOTINITIALIZED; @@ -14,11 +18,6 @@ use windows::Win32::System::Com::{ }; use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize, RoUninitialize}; -use crate::errors::{ - map_dynwinrt_error, map_dynwinrt_error_with_context, map_windows_error_with_context, -}; -use crate::runtime::DynWinRTValue; - thread_local! { static TOKIO_RO_INITIALIZED: Cell = const { Cell::new(false) }; } @@ -235,6 +234,22 @@ impl AsyncOperation { } } +pub(crate) fn finish_progress_registration( + set_result: dynwinrt::Result<()>, + is_started_after: impl FnOnce() -> PyResult, +) -> PyResult<()> { + match set_result { + Ok(()) => Ok(()), + Err(error) => { + if is_started_after()? { + Err(map_dynwinrt_error_with_context(error, "SetProgress failed")) + } else { + Ok(()) + } + } + } +} + #[pyclass(name = "_DynWinRTAsync")] pub struct DynWinRTAsync { operation: Option>, @@ -407,22 +422,58 @@ impl DynWinRTAsyncWithProgress { }); let handler = dynwinrt::create_progress_handler(handler_iid, progress_type, progress_callback); - match info.set_progress_handler(&handler) { - Ok(()) => Ok(()), - Err(error) => { - let is_started = info.is_started().map_err(map_dynwinrt_error)?; - if is_started { - Err(map_dynwinrt_error_with_context(error, "SetProgress failed")) - } else { - // Completion raced with put_Progress; there is no handler - // left to install and no future progress to deliver. - Ok(()) - } - } - } + finish_progress_registration(info.set_progress_handler(&handler), || { + info.is_started().map_err(map_dynwinrt_error) + }) } fn __repr__(&self) -> &'static str { "_DynWinRTAsyncWithProgress(...)" } } + +#[cfg(test)] +mod tests { + use std::sync::{ + Arc, Barrier, + atomic::{AtomicBool, Ordering}, + }; + + use super::*; + + fn set_progress_error() -> dynwinrt::Error { + dynwinrt::Error::WindowsError(windows::core::Error::from_hresult(windows::core::HRESULT( + 0x80004005u32 as i32, + ))) + } + + #[test] + fn progress_registration_ignores_failure_after_concurrent_completion() { + let started = Arc::new(AtomicBool::new(true)); + let begin_transition = Arc::new(Barrier::new(2)); + let transition_done = Arc::new(Barrier::new(2)); + let worker_started = started.clone(); + let worker_begin = begin_transition.clone(); + let worker_done = transition_done.clone(); + let worker = std::thread::spawn(move || { + worker_begin.wait(); + worker_started.store(false, Ordering::SeqCst); + worker_done.wait(); + }); + + let result = finish_progress_registration(Err(set_progress_error()), || { + begin_transition.wait(); + transition_done.wait(); + Ok(started.load(Ordering::SeqCst)) + }); + + worker.join().expect("completion worker failed"); + assert!(result.is_ok()); + } + + #[test] + fn progress_registration_surfaces_failure_while_operation_is_started() { + let result = finish_progress_registration(Err(set_progress_error()), || Ok(true)); + assert!(result.is_err()); + } +} diff --git a/bindings/py/src/errors.rs b/bindings/py/src/errors.rs index 8d9ad165..b43293ab 100644 --- a/bindings/py/src/errors.rs +++ b/bindings/py/src/errors.rs @@ -2,7 +2,7 @@ // Licensed under the MIT License. use pyo3::exceptions::asyncio::CancelledError as PyCancelledError; -use pyo3::exceptions::{PyOSError, PyRuntimeError}; +use pyo3::exceptions::{PyIndexError, PyOSError, PyRuntimeError}; use pyo3::prelude::*; pub(crate) fn map_windows_error(error: windows::core::Error) -> PyErr { @@ -28,6 +28,9 @@ pub(crate) fn map_dynwinrt_error(error: dynwinrt::Error) -> PyErr { dynwinrt::Error::Canceled => { PyCancelledError::new_err("WinRT async operation was canceled") } + dynwinrt::Error::IndexOutOfBounds { index, len } => { + PyIndexError::new_err(format!("Index {index} out of bounds (len {len})")) + } other => PyRuntimeError::new_err(other.message()), } } diff --git a/bindings/py/src/lib.rs b/bindings/py/src/lib.rs index c57ff19a..ec40b5cd 100644 --- a/bindings/py/src/lib.rs +++ b/bindings/py/src/lib.rs @@ -26,11 +26,14 @@ from collections.abc import ( Sequence as _Sequence, ) from datetime import datetime as _datetime, timedelta as _timedelta, timezone as _timezone +from itertools import count as _count from contextvars import ContextVar as _ContextVar from operator import index as _index +from threading import get_ident as _thread_get_ident from typing import Protocol as _Protocol, TypeVar as _TypeVar from typing import Awaitable as _Awaitable, Callable as _Callable from uuid import UUID as _UUID +from weakref import WeakValueDictionary as _WeakValueDictionary _T = _TypeVar('_T', covariant=True) _P = _TypeVar('_P', covariant=True) @@ -48,6 +51,8 @@ _active_projected_lifetime_scope = _ContextVar( 'dynwinrt_active_projected_lifetime_scope', default=None, ) +_projected_wrapper_cache = _WeakValueDictionary() +_projected_scope_serial = _count(1) def _dynwinrt_projected_native_values(value): native_values = [] @@ -68,6 +73,78 @@ def _dynwinrt_projected_native_values(value): native_values.append(value) return native_values +def _dynwinrt_projection_scope_token(): + scope = _active_projected_lifetime_scope.get() + if scope is None or not scope._active or scope._disposed: + return None + token = getattr(scope, '_projection_cache_token', None) + if token is None: + token = next(_projected_scope_serial) + scope._projection_cache_token = token + return token + +def _dynwinrt_projected_cache_key(wrapper_type, native): + if not isinstance(native, DynWinRTValue): + return None + try: + identity = native.identity_raw() + except RuntimeError: + return None + return ( + _thread_get_ident(), + _dynwinrt_projection_scope_token(), + wrapper_type, + identity, + ) + +def _dynwinrt_projected_wrapper_is_live(wrapper): + native_values = _dynwinrt_projected_native_values(wrapper) + if not native_values: + return False + for native in native_values: + is_null = getattr(native, 'is_null', None) + if callable(is_null) and is_null(): + return False + return True + +def _dynwinrt_release_redundant_native(native, wrapper): + if not callable(getattr(native, 'release', None)): + return + for existing in _dynwinrt_projected_native_values(wrapper): + if existing is native: + return + native.release() + +def _dynwinrt_cache_projected(value): + wrapper_type = type(value) + for native in _dynwinrt_projected_native_values(value): + key = _dynwinrt_projected_cache_key(wrapper_type, native) + if key is None: + continue + try: + _projected_wrapper_cache[key] = value + except TypeError: + pass + return value + +def _dynwinrt_projected_from_native(wrapper_type, native, initializer_name): + key = _dynwinrt_projected_cache_key(wrapper_type, native) + if key is not None: + cached = _projected_wrapper_cache.get(key) + if cached is not None: + if _dynwinrt_projected_wrapper_is_live(cached): + _dynwinrt_release_redundant_native(native, cached) + return cached + _projected_wrapper_cache.pop(key, None) + wrapper = object.__new__(wrapper_type) + getattr(wrapper_type, initializer_name)(wrapper, native) + if key is not None: + try: + _projected_wrapper_cache[key] = wrapper + except TypeError: + pass + return wrapper + class ProjectedLifetimeScope: def __init__(self): self._registry = {} @@ -75,6 +152,7 @@ class ProjectedLifetimeScope: self._active = False self._disposed = False self._retry_pending = False + self._projection_cache_token = None @property def disposed(self): @@ -175,6 +253,8 @@ def _dynwinrt_uuid(value): def _dynwinrt_datetime_to_ticks(value): if not isinstance(value, _datetime): raise TypeError('requires datetime.datetime object') + if value.utcoffset() is None: + raise ValueError('requires a timezone-aware datetime.datetime object') value = value.astimezone(_timezone.utc) delta = value - _WINRT_EPOCH return ((delta.days * 86400 + delta.seconds) * 1_000_000 + delta.microseconds) * 10 @@ -391,7 +471,6 @@ def _dynwinrt_dispatch_progress(callback, converter, value): m )?)?; m.add_function(wrap_pyfunction!(super::runtime::get_computer_name, m)?)?; - m.py().run( c" __all__ = [name for name in __all__ if not name.startswith('_')] diff --git a/bindings/py/src/runtime.rs b/bindings/py/src/runtime.rs index 17ae25f1..d79e5c24 100644 --- a/bindings/py/src/runtime.rs +++ b/bindings/py/src/runtime.rs @@ -4,7 +4,7 @@ use std::sync::{Arc, Mutex}; use dynwinrt; -use pyo3::exceptions::{PyRuntimeError, PyTypeError}; +use pyo3::exceptions::{PyIndexError, PyOverflowError, PyRuntimeError, PyTypeError}; use pyo3::prelude::*; use pyo3::types::PyDict; use windows::core::{GUID, HSTRING, IUnknown, Interface}; @@ -15,6 +15,84 @@ use crate::errors::{map_dynwinrt_error, map_dynwinrt_error_with_context, map_win static TABLE: std::sync::LazyLock> = std::sync::LazyLock::new(|| dynwinrt::MetadataTable::new()); +fn checked_index(index: i64) -> PyResult { + usize::try_from(index) + .map_err(|_| PyIndexError::new_err(format!("index {index} out of bounds"))) +} + +fn checked_i8(value: i32, context: &str) -> PyResult { + i8::try_from(value) + .map_err(|_| PyOverflowError::new_err(format!("{context}: {value} does not fit in Int8"))) +} + +fn checked_u8(value: u32, context: &str) -> PyResult { + u8::try_from(value) + .map_err(|_| PyOverflowError::new_err(format!("{context}: {value} does not fit in UInt8"))) +} + +fn checked_i16(value: i32, context: &str) -> PyResult { + i16::try_from(value) + .map_err(|_| PyOverflowError::new_err(format!("{context}: {value} does not fit in Int16"))) +} + +fn checked_u16(value: u32, context: &str) -> PyResult { + u16::try_from(value) + .map_err(|_| PyOverflowError::new_err(format!("{context}: {value} does not fit in UInt16"))) +} + +fn ensure_field_kind( + value: &dynwinrt::ValueTypeData, + index: usize, + expected: dynwinrt::TypeKind, + accepted: &[dynwinrt::TypeKind], +) -> PyResult<()> { + let actual = value + .field_kind_checked(index) + .map_err(map_dynwinrt_error)?; + let enum_storage = matches!(actual, dynwinrt::TypeKind::Enum(_)) + && matches!(expected, dynwinrt::TypeKind::I32 | dynwinrt::TypeKind::U32); + let bool_storage = actual == dynwinrt::TypeKind::Bool && expected == dynwinrt::TypeKind::U8; + let hresult_storage = + actual == dynwinrt::TypeKind::HResult && expected == dynwinrt::TypeKind::I32; + if actual == expected + || accepted.contains(&actual) + || enum_storage + || bool_storage + || hresult_storage + { + Ok(()) + } else { + Err(map_dynwinrt_error(dynwinrt::Error::InvalidType( + expected, actual, + ))) + } +} + +fn get_typed_field( + value: &dynwinrt::ValueTypeData, + index: i64, + expected: dynwinrt::TypeKind, + accepted: &[dynwinrt::TypeKind], + convert: impl FnOnce(T) -> U, +) -> PyResult { + let index = checked_index(index)?; + ensure_field_kind(value, index, expected, accepted)?; + Ok(convert(value.get_field::(index))) +} + +fn set_typed_field( + value: &mut dynwinrt::ValueTypeData, + index: i64, + field_value: T, + expected: dynwinrt::TypeKind, + accepted: &[dynwinrt::TypeKind], +) -> PyResult<()> { + let index = checked_index(index)?; + ensure_field_kind(value, index, expected, accepted)?; + value.set_field(index, field_value); + Ok(()) +} + // ====================================================================== // Runtime initialization // ====================================================================== @@ -1033,20 +1111,28 @@ impl DynWinRTValue { DynWinRTValue(dynwinrt::WinRTValue::Bool(value)) } #[staticmethod] - fn from_i8(value: i32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::I8(value as i8)) + fn from_i8(value: i32) -> PyResult { + Ok(DynWinRTValue(dynwinrt::WinRTValue::I8(checked_i8( + value, "from_i8", + )?))) } #[staticmethod] - fn from_u8(value: u32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::U8(value as u8)) + fn from_u8(value: u32) -> PyResult { + Ok(DynWinRTValue(dynwinrt::WinRTValue::U8(checked_u8( + value, "from_u8", + )?))) } #[staticmethod] - fn from_i16(value: i32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::I16(value as i16)) + fn from_i16(value: i32) -> PyResult { + Ok(DynWinRTValue(dynwinrt::WinRTValue::I16(checked_i16( + value, "from_i16", + )?))) } #[staticmethod] - fn from_u16(value: u32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::U16(value as u16)) + fn from_u16(value: u32) -> PyResult { + Ok(DynWinRTValue(dynwinrt::WinRTValue::U16(checked_u16( + value, "from_u16", + )?))) } #[staticmethod] fn from_i32(value: i32) -> DynWinRTValue { @@ -1188,11 +1274,13 @@ impl DynWinRTValue { dynwinrt::WinRTValue::Async(a) => a, _ => return Err(PyRuntimeError::new_err("on_progress: not an async value")), }; - let progress_type = async_info .progress_type() .ok_or_else(|| PyRuntimeError::new_err("on_progress: not a WithProgress async type"))?; super::async_runtime::ensure_progress_type_supported(&progress_type)?; + if !async_info.is_started().map_err(map_dynwinrt_error)? { + return Ok(()); + } let handler_iid = async_info.progress_handler_iid().ok_or_else(|| { PyRuntimeError::new_err("on_progress: cannot compute progress handler IID") @@ -1218,11 +1306,10 @@ impl DynWinRTValue { }); let handler = dynwinrt::create_progress_handler(handler_iid, progress_type, progress_cb); - async_info - .set_progress_handler(&handler) - .map_err(|error| map_dynwinrt_error_with_context(error, "SetProgress failed"))?; - - Ok(()) + super::async_runtime::finish_progress_registration( + async_info.set_progress_handler(&handler), + || async_info.is_started().map_err(map_dynwinrt_error), + ) } // -- Conversion methods -- @@ -1525,8 +1612,12 @@ impl DynWinRTArray { } /// Per-element access. - fn get(&self, index: usize) -> DynWinRTValue { - DynWinRTValue(self.0.get(index)) + fn get(&self, index: i64) -> PyResult { + let index = checked_index(index)?; + self.0 + .try_get(index) + .map(DynWinRTValue) + .map_err(map_dynwinrt_error) } /// Convert all elements to a list of DynWinRTValue. @@ -1561,9 +1652,9 @@ impl DynWinRTArray { .map(|i| self.0.get(i).as_i32().unwrap_or(0) as u32) .collect() } - fn to_i32_list(&self) -> Vec { + fn to_i32_list(&self) -> PyResult> { (0..self.0.len()) - .map(|i| self.0.get(i).as_i32().unwrap_or(0)) + .map(|i| self.0.get_i32(i).map_err(map_dynwinrt_error)) .collect() } fn to_u32_list(&self) -> Vec { @@ -1617,12 +1708,20 @@ impl DynWinRTArray { // -- Construction from Python lists -- #[staticmethod] - fn from_i8_values(values: Vec) -> DynWinRTArray { + fn from_i8_values(values: Vec) -> PyResult { let wvals: Vec = values .into_iter() - .map(|v| dynwinrt::WinRTValue::I8(v as i8)) - .collect(); - DynWinRTArray(dynwinrt::ArrayData::from_values(TABLE.i8_type(), &wvals)) + .map(|value| { + Ok(dynwinrt::WinRTValue::I8(checked_i8( + value, + "from_i8_values", + )?)) + }) + .collect::>()?; + Ok(DynWinRTArray(dynwinrt::ArrayData::from_values( + TABLE.i8_type(), + &wvals, + ))) } #[staticmethod] fn from_u8_values(values: Vec) -> DynWinRTArray { @@ -1631,20 +1730,36 @@ impl DynWinRTArray { DynWinRTArray(dynwinrt::ArrayData::from_values(TABLE.u8_type(), &wvals)) } #[staticmethod] - fn from_i16_values(values: Vec) -> DynWinRTArray { + fn from_i16_values(values: Vec) -> PyResult { let wvals: Vec = values .into_iter() - .map(|v| dynwinrt::WinRTValue::I16(v as i16)) - .collect(); - DynWinRTArray(dynwinrt::ArrayData::from_values(TABLE.i16_type(), &wvals)) + .map(|value| { + Ok(dynwinrt::WinRTValue::I16(checked_i16( + value, + "from_i16_values", + )?)) + }) + .collect::>()?; + Ok(DynWinRTArray(dynwinrt::ArrayData::from_values( + TABLE.i16_type(), + &wvals, + ))) } #[staticmethod] - fn from_u16_values(values: Vec) -> DynWinRTArray { + fn from_u16_values(values: Vec) -> PyResult { let wvals: Vec = values .into_iter() - .map(|v| dynwinrt::WinRTValue::U16(v as u16)) - .collect(); - DynWinRTArray(dynwinrt::ArrayData::from_values(TABLE.u16_type(), &wvals)) + .map(|value| { + Ok(dynwinrt::WinRTValue::U16(checked_u16( + value, + "from_u16_values", + )?)) + }) + .collect::>()?; + Ok(DynWinRTArray(dynwinrt::ArrayData::from_values( + TABLE.u16_type(), + &wvals, + ))) } #[staticmethod] fn from_i32_values(values: Vec) -> DynWinRTArray { @@ -1782,115 +1897,208 @@ impl DynWinRTStruct { // -- Blittable field access (get/set pairs) -- - fn get_i8(&self, index: usize) -> i32 { - self.0.get_field::(index) as i32 + fn get_i8(&self, index: i64) -> PyResult { + get_typed_field(&self.0, index, dynwinrt::TypeKind::I8, &[], |value: i8| { + value as i32 + }) } - fn set_i8(&mut self, index: usize, value: i32) { - self.0.set_field(index, value as i8); + fn set_i8(&mut self, index: i64, value: i32) -> PyResult<()> { + set_typed_field( + &mut self.0, + index, + checked_i8(value, "set_i8")?, + dynwinrt::TypeKind::I8, + &[], + ) } - fn get_u8(&self, index: usize) -> u32 { - self.0.get_field::(index) as u32 + fn get_u8(&self, index: i64) -> PyResult { + get_typed_field(&self.0, index, dynwinrt::TypeKind::U8, &[], |value: u8| { + value as u32 + }) } - fn set_u8(&mut self, index: usize, value: u32) { - self.0.set_field(index, value as u8); + fn set_u8(&mut self, index: i64, value: u32) -> PyResult<()> { + set_typed_field( + &mut self.0, + index, + checked_u8(value, "set_u8")?, + dynwinrt::TypeKind::U8, + &[], + ) } - fn get_i16(&self, index: usize) -> i32 { - self.0.get_field::(index) as i32 + fn get_i16(&self, index: i64) -> PyResult { + get_typed_field( + &self.0, + index, + dynwinrt::TypeKind::I16, + &[], + |value: i16| value as i32, + ) } - fn set_i16(&mut self, index: usize, value: i32) { - self.0.set_field(index, value as i16); + fn set_i16(&mut self, index: i64, value: i32) -> PyResult<()> { + set_typed_field( + &mut self.0, + index, + checked_i16(value, "set_i16")?, + dynwinrt::TypeKind::I16, + &[], + ) } - fn get_u16(&self, index: usize) -> u32 { - self.0.get_field::(index) as u32 + fn get_u16(&self, index: i64) -> PyResult { + get_typed_field( + &self.0, + index, + dynwinrt::TypeKind::U16, + &[dynwinrt::TypeKind::Char16], + |value: u16| value as u32, + ) } - fn set_u16(&mut self, index: usize, value: u32) { - self.0.set_field(index, value as u16); + fn set_u16(&mut self, index: i64, value: u32) -> PyResult<()> { + set_typed_field( + &mut self.0, + index, + checked_u16(value, "set_u16")?, + dynwinrt::TypeKind::U16, + &[dynwinrt::TypeKind::Char16], + ) } - fn get_i32(&self, index: usize) -> i32 { - self.0.get_field::(index) + fn get_i32(&self, index: i64) -> PyResult { + get_typed_field( + &self.0, + index, + dynwinrt::TypeKind::I32, + &[], + |value: i32| value, + ) } - fn set_i32(&mut self, index: usize, value: i32) { - self.0.set_field(index, value); + fn set_i32(&mut self, index: i64, value: i32) -> PyResult<()> { + set_typed_field(&mut self.0, index, value, dynwinrt::TypeKind::I32, &[]) } - fn get_u32(&self, index: usize) -> u32 { - self.0.get_field::(index) + fn get_u32(&self, index: i64) -> PyResult { + get_typed_field( + &self.0, + index, + dynwinrt::TypeKind::U32, + &[], + |value: u32| value, + ) } - fn set_u32(&mut self, index: usize, value: u32) { - self.0.set_field(index, value); + fn set_u32(&mut self, index: i64, value: u32) -> PyResult<()> { + set_typed_field(&mut self.0, index, value, dynwinrt::TypeKind::U32, &[]) } - fn get_f32(&self, index: usize) -> f64 { - self.0.get_field::(index) as f64 + fn get_f32(&self, index: i64) -> PyResult { + get_typed_field( + &self.0, + index, + dynwinrt::TypeKind::F32, + &[], + |value: f32| value as f64, + ) } - fn set_f32(&mut self, index: usize, value: f64) { - self.0.set_field(index, value as f32); + fn set_f32(&mut self, index: i64, value: f64) -> PyResult<()> { + set_typed_field( + &mut self.0, + index, + value as f32, + dynwinrt::TypeKind::F32, + &[], + ) } - fn get_f64(&self, index: usize) -> f64 { - self.0.get_field::(index) + fn get_f64(&self, index: i64) -> PyResult { + get_typed_field( + &self.0, + index, + dynwinrt::TypeKind::F64, + &[], + |value: f64| value, + ) } - fn set_f64(&mut self, index: usize, value: f64) { - self.0.set_field(index, value); + fn set_f64(&mut self, index: i64, value: f64) -> PyResult<()> { + set_typed_field(&mut self.0, index, value, dynwinrt::TypeKind::F64, &[]) } - fn get_i64(&self, index: usize) -> i64 { - self.0.get_field::(index) + fn get_i64(&self, index: i64) -> PyResult { + get_typed_field( + &self.0, + index, + dynwinrt::TypeKind::I64, + &[], + |value: i64| value, + ) } - fn set_i64(&mut self, index: usize, value: i64) { - self.0.set_field(index, value); + fn set_i64(&mut self, index: i64, value: i64) -> PyResult<()> { + set_typed_field(&mut self.0, index, value, dynwinrt::TypeKind::I64, &[]) } - fn get_u64(&self, index: usize) -> u64 { - self.0.get_field::(index) + fn get_u64(&self, index: i64) -> PyResult { + get_typed_field( + &self.0, + index, + dynwinrt::TypeKind::U64, + &[], + |value: u64| value, + ) } - fn set_u64(&mut self, index: usize, value: u64) { - self.0.set_field(index, value); + fn set_u64(&mut self, index: i64, value: u64) -> PyResult<()> { + set_typed_field(&mut self.0, index, value, dynwinrt::TypeKind::U64, &[]) } // -- Non-blittable field access -- - fn get_hstring(&self, index: usize) -> PyResult { + fn get_hstring(&self, index: i64) -> PyResult { + let index = checked_index(index)?; self.0 .get_field_hstring(index) .map(|value| value.to_string()) .map_err(map_dynwinrt_error) } - fn set_hstring(&mut self, index: usize, value: String) -> PyResult<()> { + fn set_hstring(&mut self, index: i64, value: String) -> PyResult<()> { + let index = checked_index(index)?; self.0 .set_field_hstring(index, HSTRING::from(&value)) .map_err(map_dynwinrt_error) } - fn get_guid(&self, index: usize) -> WinGUID { - WinGUID(self.0.get_field::(index)) + fn get_guid(&self, index: i64) -> PyResult { + get_typed_field(&self.0, index, dynwinrt::TypeKind::Guid, &[], WinGUID) } - fn set_guid(&mut self, index: usize, value: &WinGUID) { - self.0.set_field(index, value.0); + fn set_guid(&mut self, index: i64, value: &WinGUID) -> PyResult<()> { + set_typed_field(&mut self.0, index, value.0, dynwinrt::TypeKind::Guid, &[]) } - fn get_struct(&self, index: usize) -> DynWinRTStruct { - DynWinRTStruct(self.0.get_field_struct(index)) + fn get_struct(&self, index: i64) -> PyResult { + let index = checked_index(index)?; + self.0 + .get_field_struct_checked(index) + .map(DynWinRTStruct) + .map_err(map_dynwinrt_error) } - fn set_struct(&mut self, index: usize, value: &DynWinRTStruct) { - self.0.set_field_struct(index, &value.0); + fn set_struct(&mut self, index: i64, value: &DynWinRTStruct) -> PyResult<()> { + let index = checked_index(index)?; + self.0 + .set_field_struct_checked(index, &value.0) + .map_err(map_dynwinrt_error) } - fn get_object(&self, index: usize) -> PyResult { + fn get_object(&self, index: i64) -> PyResult { + let index = checked_index(index)?; match self.0.get_field_object(index).map_err(map_dynwinrt_error)? { Some(object) => Ok(DynWinRTValue(dynwinrt::WinRTValue::Object(object))), None => Ok(DynWinRTValue(dynwinrt::WinRTValue::Null)), } } - fn set_object(&mut self, index: usize, value: &DynWinRTValue) -> PyResult<()> { + fn set_object(&mut self, index: i64, value: &DynWinRTValue) -> PyResult<()> { + let index = checked_index(index)?; match &value.0 { dynwinrt::WinRTValue::Object(obj) => self .0 @@ -2199,6 +2407,17 @@ mod tests { unsafe { ((*vtable).invoke)(raw, std::ptr::null_mut(), std::ptr::null_mut()) } } + #[test] + fn hresult_arrays_convert_to_signed_integers() { + let values = [ + dynwinrt::WinRTValue::HResult(windows::core::HRESULT(0)), + dynwinrt::WinRTValue::HResult(windows::core::HRESULT(0x80004005u32 as i32)), + ]; + let array = DynWinRTArray(dynwinrt::ArrayData::from_values(TABLE.hresult(), &values)); + + assert_eq!(array.to_i32_list().unwrap(), vec![0, 0x80004005u32 as i32]); + } + #[test] fn python_delegate_reports_unraisable_callback_errors() { Python::initialize(); diff --git a/bindings/py/tests/test_basic.py b/bindings/py/tests/test_basic.py index a5117af7..26d5408f 100644 --- a/bindings/py/tests/test_basic.py +++ b/bindings/py/tests/test_basic.py @@ -2,6 +2,7 @@ # Licensed under the MIT License. import dynwinrt +import pytest from dynwinrt import ( DynWinRTType, DynWinRTMethodSig, @@ -361,6 +362,42 @@ def test_array_all_types(): assert DynWinRTArray.from_string_values(["a", "b"]).to_string_list() == ["a", "b"] +def test_narrow_array_constructors_enforce_boundaries(): + assert DynWinRTArray.from_i8_values([-128, 127]).to_i8_list() == [-128, 127] + assert DynWinRTArray.from_u8_values([0, 255]).to_u8_list() == bytes([0, 255]) + assert DynWinRTArray.from_i16_values([-32768, 32767]).to_i16_list() == [ + -32768, + 32767, + ] + assert DynWinRTArray.from_u16_values([0, 0xFFFF]).to_u16_list() == [0, 0xFFFF] + + with pytest.raises(OverflowError): + DynWinRTArray.from_i8_values([-129]) + with pytest.raises(OverflowError): + DynWinRTArray.from_i8_values([128]) + with pytest.raises(OverflowError): + DynWinRTArray.from_u8_values([-1]) + with pytest.raises(OverflowError): + DynWinRTArray.from_u8_values([256]) + with pytest.raises(OverflowError): + DynWinRTArray.from_i16_values([-32769]) + with pytest.raises(OverflowError): + DynWinRTArray.from_i16_values([32768]) + with pytest.raises(OverflowError): + DynWinRTArray.from_u16_values([-1]) + with pytest.raises(OverflowError): + DynWinRTArray.from_u16_values([0x1_0000]) + + +def test_array_get_invalid_index_raises_index_error(): + arr = DynWinRTArray.from_i32_values([1, 2, 3]) + + with pytest.raises(IndexError): + arr.get(-1) + with pytest.raises(IndexError): + arr.get(3) + + def test_array_to_value(): """Array can be wrapped as DynWinRTValue.""" arr = DynWinRTArray.from_i32_values([10, 20]) @@ -389,6 +426,28 @@ def test_struct_to_value(): assert val.is_struct() +def test_struct_enum_field_uses_underlying_integer_accessor(): + enum_type = DynWinRTType.enum_type("TestStructEnum", ["A", "B"], [0, 1]) + typ = DynWinRTType.struct_type("TestStructWithEnum", [enum_type]) + value = DynWinRTStruct.create(typ) + + value.set_i32(0, 1) + assert value.get_i32(0) == 1 + + +def test_struct_bool_and_hresult_use_abi_integer_accessors(): + typ = DynWinRTType.struct_type( + "TestStructAbiIntegerAliases", + [DynWinRTType.bool_type(), DynWinRTType.hresult()], + ) + value = DynWinRTStruct.create(typ) + + value.set_u8(0, 1) + value.set_i32(1, -1) + assert value.get_u8(0) == 1 + assert value.get_i32(1) == -1 + + def test_struct_array_round_trip(): typ = DynWinRTType.struct_type( "TestStructArray", @@ -447,3 +506,106 @@ def test_struct_all_field_types(): assert s.get_i64(4) == -999999 s.set_u64(5, 12345678) assert s.get_u64(5) == 12345678 + + +def test_struct_narrow_setters_enforce_boundaries_and_char16_range(): + typ = DynWinRTType.struct_type( + "TestStructNarrowBounds", + [ + DynWinRTType.i8_type(), + DynWinRTType.u8_type(), + DynWinRTType.i16_type(), + DynWinRTType.u16_type(), + DynWinRTType.char16(), + ], + ) + value = DynWinRTStruct.create(typ) + + value.set_i8(0, -128) + assert value.get_i8(0) == -128 + value.set_i8(0, 127) + assert value.get_i8(0) == 127 + value.set_u8(1, 255) + assert value.get_u8(1) == 255 + value.set_i16(2, -32768) + assert value.get_i16(2) == -32768 + value.set_i16(2, 32767) + assert value.get_i16(2) == 32767 + value.set_u16(3, 0xFFFF) + assert value.get_u16(3) == 0xFFFF + value.set_u16(4, 0xFFFF) + assert value.get_u16(4) == 0xFFFF + + with pytest.raises(OverflowError): + value.set_i8(0, -129) + with pytest.raises(OverflowError): + value.set_i8(0, 128) + with pytest.raises(OverflowError): + value.set_u8(1, -1) + with pytest.raises(OverflowError): + value.set_u8(1, 256) + with pytest.raises(OverflowError): + value.set_i16(2, -32769) + with pytest.raises(OverflowError): + value.set_i16(2, 32768) + with pytest.raises(OverflowError): + value.set_u16(3, -1) + with pytest.raises(OverflowError): + value.set_u16(3, 0x1_0000) + with pytest.raises(OverflowError): + value.set_u16(4, 0x1_0000) + + +def test_struct_indexed_accessors_raise_index_error_for_invalid_indices(): + typ = DynWinRTType.struct_type("TestStructIndexErrors", [DynWinRTType.i32_type()]) + value = DynWinRTStruct.create(typ) + inner = DynWinRTStruct.create( + DynWinRTType.struct_type("TestStructIndexErrorsInner", [DynWinRTType.i32_type()]) + ) + + with pytest.raises(IndexError): + value.get_i32(-1) + with pytest.raises(IndexError): + value.get_guid(1) + with pytest.raises(IndexError): + value.set_hstring(1, "bad") + with pytest.raises(IndexError): + value.get_struct(1) + with pytest.raises(IndexError): + value.set_struct(1, inner) + with pytest.raises(IndexError): + value.get_object(1) + with pytest.raises(IndexError): + value.set_object(1, DynWinRTValue.null_value()) + + +def test_struct_indexed_accessors_raise_runtime_error_for_wrong_field_shape(): + typ = DynWinRTType.struct_type("TestStructWrongFieldShape", [DynWinRTType.i32_type()]) + value = DynWinRTStruct.create(typ) + inner = DynWinRTStruct.create( + DynWinRTType.struct_type("TestStructWrongFieldShapeInner", [DynWinRTType.i32_type()]) + ) + guid = WinGUID.parse("9e365e57-48b2-4160-956f-c7385120bbfc") + + with pytest.raises(RuntimeError): + value.get_i8(0) + with pytest.raises(RuntimeError): + value.set_i8(0, 1) + with pytest.raises(RuntimeError): + value.get_hstring(0) + with pytest.raises(RuntimeError): + value.set_hstring(0, "bad") + with pytest.raises(RuntimeError): + value.get_guid(0) + with pytest.raises(RuntimeError): + value.set_guid(0, guid) + with pytest.raises(RuntimeError): + value.get_struct(0) + with pytest.raises(RuntimeError): + value.set_struct(0, inner) + with pytest.raises(RuntimeError): + value.get_object(0) + with pytest.raises(RuntimeError): + value.set_object(0, DynWinRTValue.null_value()) + with pytest.raises(RuntimeError): + DynWinRTStruct.create(DynWinRTType.i32_type()).get_i32(0) diff --git a/bindings/py/tests/test_phase1.py b/bindings/py/tests/test_phase1.py index b67f7840..de4ce45f 100644 --- a/bindings/py/tests/test_phase1.py +++ b/bindings/py/tests/test_phase1.py @@ -44,9 +44,11 @@ from dynwinrt.dynwinrt import ( _DynWinRTAsync, _DynWinRTAsyncWithProgress, + _dynwinrt_cache_projected, _dynwinrt_dispatch_progress, _dynwinrt_datetime_to_ticks, _dynwinrt_new_vector, + _dynwinrt_projected_from_native, _dynwinrt_track_projected, _dynwinrt_ticks_to_datetime, _dynwinrt_ticks_to_timedelta, @@ -70,6 +72,36 @@ def _setup_module(): _setup_module() +def _projected_wrapper_type(name): + class Wrapper: + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native( + cls, + args[0], + "_set_native", + ) + return super().__new__(cls) + + def _set_native(self, obj): + self._obj = obj + self._dynwinrt_native_ready = True + _dynwinrt_track_projected(self, f"Tests.{name}") + _dynwinrt_cache_projected(self) + + def __init__(self, obj): + if getattr(self, "_dynwinrt_native_ready", False): + return + type(self)._set_native(self, obj) + + @classmethod + def _from_native(cls, obj): + return cls(obj) + + Wrapper.__name__ = name + return Wrapper + + # ---------------------------------------------------------------------- # invoke_all — multi-out return shape # ---------------------------------------------------------------------- @@ -414,6 +446,157 @@ def test_com_identity_and_element_factory_callback_release(): factory.release() +def test_projected_identity_cache_reuses_live_wrappers_and_skips_released_ones(): + Wrapper = _projected_wrapper_type("IdentityWrapper") + + with RoApartment(1): + raw = DynWinRTValue.activation_factory("Windows.Foundation.Uri") + wrapped = Wrapper._from_native(raw) + assert weakref.ref(wrapped)() is wrapped + + duplicate = raw.cast(WinGUID.parse(IID_IURI_FACTORY)) + assert Wrapper._from_native(duplicate) is wrapped + assert duplicate.is_null() + + revival = raw.cast(WinGUID.parse(IID_IURI_FACTORY)) + release_projected(wrapped) + assert raw.is_null() + + revived = Wrapper._from_native(revival) + assert revived is not wrapped + release_projected(revived) + + +def test_projected_identity_cache_is_partitioned_by_lifetime_scope(): + Wrapper = _projected_wrapper_type("ScopedIdentityWrapper") + + with RoApartment(1): + raw = DynWinRTValue.activation_factory("Windows.Foundation.Uri") + carried = raw.cast(WinGUID.parse(IID_IURI_FACTORY)) + + with projected_lifetime_scope(): + scoped = Wrapper._from_native(raw) + assert Wrapper._from_native(raw) is scoped + + assert scoped._obj.is_null() + unscoped = Wrapper._from_native(carried) + assert unscoped is not scoped + release_projected(unscoped) + + +def test_projected_identity_cache_allows_non_weakrefable_wrappers(): + class SlottedWrapper: + __slots__ = ("_obj", "_dynwinrt_native_ready") + + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native( + cls, + args[0], + "_set_native", + ) + return super().__new__(cls) + + def _set_native(self, obj): + self._obj = obj + self._dynwinrt_native_ready = True + _dynwinrt_track_projected(self, "Tests.SlottedWrapper") + _dynwinrt_cache_projected(self) + + def __init__(self, obj): + if getattr(self, "_dynwinrt_native_ready", False): + return + type(self)._set_native(self, obj) + + @classmethod + def _from_native(cls, obj): + return cls(obj) + + with RoApartment(1): + first_raw = DynWinRTValue.activation_factory("Windows.Foundation.Uri") + first = SlottedWrapper._from_native(first_raw) + second_raw = first_raw.cast(WinGUID.parse(IID_IURI_FACTORY)) + second = SlottedWrapper._from_native(second_raw) + + assert first is not second + release_projected(first) + release_projected(second) + + +def test_direct_projected_uri_constructor_registers_final_self_in_identity_cache(): + factory_iid = WinGUID.parse(IID_IURI_FACTORY) + uri_iid = WinGUID.parse(IID_IURI) + factory_type = DynWinRTType.register_interface( + "ProjectedUriIdentityFactory", + factory_iid, + ).add_method( + "CreateUri", + DynWinRTMethodSig() + .add_in(DynWinRTType.hstring()) + .add_out(DynWinRTType.object()), + ) + uri_type = DynWinRTType.register_interface( + "ProjectedUriIdentityClass", + uri_iid, + ).add_method( + "get_AbsoluteUri", + DynWinRTMethodSig().add_out(DynWinRTType.hstring()), + ) + + class ProjectedUri: + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native( + cls, + args[0], + "_set_native", + ) + return super().__new__(cls) + + def _set_native(self, obj): + self._obj = obj.cast(uri_iid) + self._dynwinrt_native_ready = True + _dynwinrt_track_projected(self, "Windows.Foundation.Uri") + _dynwinrt_cache_projected(self) + + def __init__(self, *args, **kwargs): + if getattr(self, "_dynwinrt_native_ready", False): + return + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + self._set_native(args[0]) + return + if len(args) == 1 and not kwargs and isinstance(args[0], str): + self._set_native(type(self).create_uri(args[0])._obj) + return + raise TypeError("No matching constructor for ProjectedUri") + + @classmethod + def _from_native(cls, obj): + return cls(obj) + + @staticmethod + def create_uri(uri): + factory = DynWinRTValue.activation_factory("Windows.Foundation.Uri").cast( + factory_iid, + ) + return ProjectedUri._from_native( + factory_type.method(6).invoke( + factory, + [DynWinRTValue.from_hstring(uri)], + ), + ) + + @property + def absolute_uri(self): + return uri_type.method(6).invoke(self._obj, []).to_string() + + with RoApartment(1): + uri = ProjectedUri("https://example.com/path") + assert ProjectedUri(uri._obj) is uri + assert uri.absolute_uri == "https://example.com/path" + release_projected(uri) + + def test_native_override_interface_rejects_unknown_or_unsupported_callback_shapes(): iid = WinGUID.parse("FFC6FD98-F38C-5904-9CE4-97A3427CF4BA") with pytest.raises(RuntimeError, match="unsupported native override ABI shape"): @@ -822,6 +1005,24 @@ def test_winrt_datetime_round_trip(): assert _dynwinrt_ticks_to_datetime(_dynwinrt_datetime_to_ticks(value)) == value +def test_winrt_datetime_normalizes_offsets_and_rejects_naive_values(): + value = datetime( + 2024, + 1, + 2, + 11, + 4, + 5, + 678901, + tzinfo=timezone(timedelta(hours=8)), + ) + expected = datetime(2024, 1, 2, 3, 4, 5, 678901, tzinfo=timezone.utc) + assert _dynwinrt_ticks_to_datetime(_dynwinrt_datetime_to_ticks(value)) == expected + + with pytest.raises(ValueError, match="timezone-aware"): + _dynwinrt_datetime_to_ticks(datetime(2024, 1, 2, 3, 4, 5)) + + @pytest.mark.parametrize( "value", [ diff --git a/bindings/py/tests/test_runtime.py b/bindings/py/tests/test_runtime.py index bf3baed8..c4101a44 100644 --- a/bindings/py/tests/test_runtime.py +++ b/bindings/py/tests/test_runtime.py @@ -31,6 +31,41 @@ def test_unsigned_conversions_preserve_full_range(): DynWinRTValue.from_u64(0xFFFFFFFFFFFFFFFF).to_i64() +@pytest.mark.parametrize( + ("factory", "value", "expected"), + [ + (DynWinRTValue.from_i8, -128, -128), + (DynWinRTValue.from_i8, 127, 127), + (DynWinRTValue.from_u8, 0, 0), + (DynWinRTValue.from_u8, 255, 255), + (DynWinRTValue.from_i16, -32768, -32768), + (DynWinRTValue.from_i16, 32767, 32767), + (DynWinRTValue.from_u16, 0, 0), + (DynWinRTValue.from_u16, 0xFFFF, 0xFFFF), + ], +) +def test_narrow_scalar_constructors_accept_boundaries(factory, value, expected): + assert factory(value).to_int() == expected + + +@pytest.mark.parametrize( + ("factory", "value"), + [ + (DynWinRTValue.from_i8, -129), + (DynWinRTValue.from_i8, 128), + (DynWinRTValue.from_u8, -1), + (DynWinRTValue.from_u8, 256), + (DynWinRTValue.from_i16, -32769), + (DynWinRTValue.from_i16, 32768), + (DynWinRTValue.from_u16, -1), + (DynWinRTValue.from_u16, 0x1_0000), + ], +) +def test_narrow_scalar_constructors_reject_overflow(factory, value): + with pytest.raises(OverflowError): + factory(value) + + def test_to_f64(): assert DynWinRTValue.from_f64(3.14).to_f64() == 3.14 assert abs(DynWinRTValue.from_f32(1.5).to_f64() - 1.5) < 0.01 diff --git a/crates/dynwinrt/src/array.rs b/crates/dynwinrt/src/array.rs index ec6dc235..571882ec 100644 --- a/crates/dynwinrt/src/array.rs +++ b/crates/dynwinrt/src/array.rs @@ -240,16 +240,22 @@ impl ArrayData { /// For Values arrays, returns a clone of the stored value. /// For CoTaskMem arrays, reads from raw bytes (AddRef / DuplicateString as needed). pub fn get(&self, index: usize) -> WinRTValue { - assert!( - index < self.len(), - "ArrayData::get index {} out of bounds (len {})", - index, - self.len() - ); - match &self.buffer { + let len = self.len(); + self.try_get(index).unwrap_or_else(|_| { + panic!("ArrayData::get index {} out of bounds (len {})", index, len) + }) + } + + /// Fallible element access that reports invalid indices instead of panicking. + pub fn try_get(&self, index: usize) -> crate::result::Result { + let len = self.len(); + if index >= len { + return Err(crate::result::Error::IndexOutOfBounds { index, len }); + } + Ok(match &self.buffer { ArrayBuffer::Values(v) => v[index].clone(), ArrayBuffer::CoTaskMem { ptr, .. } => self.get_from_raw(index, *ptr as *const u8), - } + }) } /// Read element from a raw byte buffer (CoTaskMem path). @@ -269,6 +275,9 @@ impl ArrayData { value: *(base.add(index * elem_size) as *const i32), type_handle: self.element_type.clone(), }, + TypeKind::HResult => WinRTValue::HResult(windows_core::HRESULT( + *(base.add(index * elem_size) as *const i32), + )), TypeKind::U32 => WinRTValue::U32(*(base.add(index * elem_size) as *const u32)), TypeKind::I64 => WinRTValue::I64(*(base.add(index * elem_size) as *const i64)), TypeKind::U64 => WinRTValue::U64(*(base.add(index * elem_size) as *const u64)), @@ -314,14 +323,29 @@ impl ArrayData { // Convenience typed getters // ------------------------------------------------------------------ - pub fn get_i32(&self, index: usize) -> i32 { - match &self.buffer { - ArrayBuffer::Values(v) => v[index].as_i32().unwrap(), - ArrayBuffer::CoTaskMem { ptr, len } => { - assert!(index < *len); - unsafe { *((*ptr as *const u8).add(index * 4) as *const i32) } + /// Read an `i32`-compatible element (plain `i32`, named enum, or `HRESULT`). + pub fn get_i32(&self, index: usize) -> crate::result::Result { + let len = self.len(); + if index >= len { + return Err(crate::result::Error::IndexOutOfBounds { index, len }); + } + + match self.element_type.kind() { + TypeKind::I32 | TypeKind::Enum(_) | TypeKind::HResult => {} + other => { + return Err(crate::result::Error::InvalidType(TypeKind::I32, other)); } } + + match self.try_get(index)? { + WinRTValue::I32(value) => Ok(value), + WinRTValue::Enum { value, .. } => Ok(value), + WinRTValue::HResult(value) => Ok(value.0), + other => Err(crate::result::Error::InvalidType( + TypeKind::I32, + other.get_type_kind(), + )), + } } // ------------------------------------------------------------------ @@ -518,6 +542,7 @@ fn serialize_to_buffer(element_type: &TypeHandle, values: &[WinRTValue]) -> Vec< WinRTValue::U16(v) => buffer.extend_from_slice(&v.to_ne_bytes()), WinRTValue::I32(v) => buffer.extend_from_slice(&v.to_ne_bytes()), WinRTValue::Enum { value, .. } => buffer.extend_from_slice(&value.to_ne_bytes()), + WinRTValue::HResult(value) => buffer.extend_from_slice(&value.0.to_ne_bytes()), WinRTValue::U32(v) => buffer.extend_from_slice(&v.to_ne_bytes()), WinRTValue::I64(v) => buffer.extend_from_slice(&v.to_ne_bytes()), WinRTValue::U64(v) => buffer.extend_from_slice(&v.to_ne_bytes()), @@ -596,6 +621,78 @@ mod tests { assert_eq!(async_array.serialize_for_abi(), 0usize.to_ne_bytes()); } + #[test] + fn test_try_get_reports_out_of_bounds() { + let table = MetadataTable::new(); + let array = ArrayData::from_values(table.i32_type(), &[WinRTValue::I32(17)]); + + assert_eq!(array.try_get(0).unwrap().as_i32(), Some(17)); + assert!(matches!( + array.try_get(1), + Err(crate::result::Error::IndexOutOfBounds { index: 1, len: 1 }) + )); + } + + #[test] + fn test_get_i32_reports_invalid_index_and_type_for_values() { + let table = MetadataTable::new(); + let array = ArrayData::from_values(table.i32_type(), &[WinRTValue::I32(17)]); + assert_eq!(array.get_i32(0).unwrap(), 17); + assert!(matches!( + array.get_i32(1), + Err(crate::result::Error::IndexOutOfBounds { index: 1, len: 1 }) + )); + + let wrong_type = ArrayData::from_values(table.u32_type(), &[WinRTValue::U32(17)]); + assert!(matches!( + wrong_type.get_i32(0), + Err(crate::result::Error::InvalidType( + TypeKind::I32, + TypeKind::U32 + )) + )); + } + + #[test] + fn test_get_i32_accepts_hresult_values() { + let table = MetadataTable::new(); + let value = windows_core::HRESULT(0x80004005u32 as i32); + let array = ArrayData::from_values(table.hresult(), &[WinRTValue::HResult(value)]); + + assert_eq!(array.get_i32(0).unwrap(), value.0); + assert_eq!(array.serialize_for_abi(), value.0.to_ne_bytes()); + } + + #[test] + fn test_get_i32_reports_invalid_index_and_type_for_cotaskmem() { + let table = MetadataTable::new(); + let len = 1usize; + let total = std::mem::size_of::() * len; + let ptr = unsafe { windows::Win32::System::Com::CoTaskMemAlloc(total) as *mut i32 }; + assert!(!ptr.is_null()); + unsafe { ptr.write(17) }; + + let array = ArrayData::from_cotaskmem(table.i32_type(), ptr.cast(), len); + assert_eq!(array.get_i32(0).unwrap(), 17); + assert!(matches!( + array.get_i32(1), + Err(crate::result::Error::IndexOutOfBounds { index: 1, len: 1 }) + )); + + let wrong_ptr = unsafe { windows::Win32::System::Com::CoTaskMemAlloc(total) as *mut u32 }; + assert!(!wrong_ptr.is_null()); + unsafe { wrong_ptr.write(17) }; + + let wrong_type = ArrayData::from_cotaskmem(table.u32_type(), wrong_ptr.cast(), len); + assert!(matches!( + wrong_type.get_i32(0), + Err(crate::result::Error::InvalidType( + TypeKind::I32, + TypeKind::U32 + )) + )); + } + /// P1: CoTaskMem array of structs with HString fields — Clone/Drop must recurse. #[test] fn test_struct_array_with_hstring_clone_drop() { diff --git a/crates/dynwinrt/src/lib.rs b/crates/dynwinrt/src/lib.rs index 884f7e7f..10382db3 100644 --- a/crates/dynwinrt/src/lib.rs +++ b/crates/dynwinrt/src/lib.rs @@ -379,11 +379,11 @@ mod tests { // Verify let array = results[0].as_array().expect("Expected WinRTValue::Array"); assert_eq!(array.len(), 5); - assert_eq!(array.get_i32(0), 100); - assert_eq!(array.get_i32(1), 200); - assert_eq!(array.get_i32(2), 300); - assert_eq!(array.get_i32(3), 400); - assert_eq!(array.get_i32(4), 500); + assert_eq!(array.get_i32(0).unwrap(), 100); + assert_eq!(array.get_i32(1).unwrap(), 200); + assert_eq!(array.get_i32(2).unwrap(), 300); + assert_eq!(array.get_i32(3).unwrap(), 400); + assert_eq!(array.get_i32(4).unwrap(), 500); Ok(()) } diff --git a/crates/dynwinrt/src/metadata_table/value_data.rs b/crates/dynwinrt/src/metadata_table/value_data.rs index be0a97e4..9d9bc2d2 100644 --- a/crates/dynwinrt/src/metadata_table/value_data.rs +++ b/crates/dynwinrt/src/metadata_table/value_data.rs @@ -10,6 +10,9 @@ use super::type_kind::TypeKind; /// Release non-blittable fields (HString, COM pointers, nested structs) in a struct buffer. /// Called by Drop and before overwriting. Recurses into nested structs. unsafe fn release_non_blittable_fields(handle: &TypeHandle, ptr: *const u8) { + if !matches!(handle.kind(), TypeKind::Struct(_)) { + return; + } let count = handle.field_count(); for i in 0..count { let kind = handle.table.field_kind(handle.kind, i); @@ -45,6 +48,9 @@ unsafe fn release_non_blittable_fields(handle: &TypeHandle, ptr: *const u8) { /// The source retains its references; the destination gets new ones. /// Recurses into nested structs. unsafe fn duplicate_non_blittable_fields(handle: &TypeHandle, ptr: *mut u8) { + if !matches!(handle.kind(), TypeKind::Struct(_)) { + return; + } let count = handle.field_count(); for i in 0..count { let kind = handle.table.field_kind(handle.kind, i); @@ -82,6 +88,9 @@ unsafe fn duplicate_non_blittable_fields(handle: &TypeHandle, ptr: *mut u8) { /// Check if a struct type has any non-blittable fields (recursing into nested structs). fn has_non_blittable_fields(handle: &TypeHandle) -> bool { + if !matches!(handle.kind(), TypeKind::Struct(_)) { + return false; + } let count = handle.field_count(); for i in 0..count { let kind = handle.table.field_kind(handle.kind, i); @@ -142,6 +151,34 @@ impl ValueTypeData { self.ptr } + fn checked_field_handle(&self, index: usize) -> crate::result::Result<(TypeHandle, usize)> { + let kind = self.type_handle.kind(); + if !matches!(kind, TypeKind::Struct(_)) { + return Err(crate::result::Error::ExpectStructTypeError(kind)); + } + let field_count = self.type_handle.field_count(); + if index >= field_count { + return Err(crate::result::Error::IndexOutOfBounds { + index, + len: field_count, + }); + } + Ok(( + self.type_handle.field_type(index), + self.type_handle.field_offset(index), + )) + } + + pub fn field_type_checked(&self, index: usize) -> crate::result::Result { + self.checked_field_handle(index) + .map(|(field_handle, _)| field_handle) + } + + pub fn field_kind_checked(&self, index: usize) -> crate::result::Result { + self.field_type_checked(index) + .map(|field_handle| field_handle.kind()) + } + pub(crate) unsafe fn copy_to_abi(&self, result: *mut c_void) { let layout = self.type_handle.layout(); if layout.size() == 0 { @@ -181,15 +218,13 @@ impl ValueTypeData { } pub fn get_field_hstring(&self, index: usize) -> crate::result::Result { - let h = &self.type_handle; - let field_handle = h.field_type(index); + let (field_handle, offset) = self.checked_field_handle(index)?; if field_handle.kind() != TypeKind::HString { return Err(crate::result::Error::InvalidType( TypeKind::HString, field_handle.kind(), )); } - let offset = h.field_offset(index); let raw = unsafe { *(self.ptr.add(offset) as *const *mut c_void) }; if raw.is_null() { Ok(HSTRING::new()) @@ -200,15 +235,13 @@ impl ValueTypeData { } pub fn set_field_hstring(&mut self, index: usize, value: HSTRING) -> crate::result::Result<()> { - let h = &self.type_handle; - let field_handle = h.field_type(index); + let (field_handle, offset) = self.checked_field_handle(index)?; if field_handle.kind() != TypeKind::HString { return Err(crate::result::Error::InvalidType( TypeKind::HString, field_handle.kind(), )); } - let offset = h.field_offset(index); let field = unsafe { &mut *(self.ptr.add(offset) as *mut *mut c_void) }; let old_raw = std::mem::replace(field, unsafe { std::mem::transmute(value) }); if !old_raw.is_null() { @@ -218,14 +251,12 @@ impl ValueTypeData { } pub fn get_field_object(&self, index: usize) -> crate::result::Result> { - let h = &self.type_handle; - let field_handle = h.field_type(index); + let (field_handle, offset) = self.checked_field_handle(index)?; if !field_handle.kind().is_com_pointer() { return Err(crate::result::Error::expect_object_type( field_handle.kind(), )); } - let offset = h.field_offset(index); let raw = unsafe { *(self.ptr.add(offset) as *const *mut c_void) }; if raw.is_null() { Ok(None) @@ -239,14 +270,12 @@ impl ValueTypeData { index: usize, value: Option<&IUnknown>, ) -> crate::result::Result<()> { - let h = &self.type_handle; - let field_handle = h.field_type(index); + let (field_handle, offset) = self.checked_field_handle(index)?; if !field_handle.kind().is_com_pointer() { return Err(crate::result::Error::expect_object_type( field_handle.kind(), )); } - let offset = h.field_offset(index); let field = unsafe { &mut *(self.ptr.add(offset) as *mut *mut c_void) }; let new_raw = if let Some(object) = value { let iid = if field_handle.kind() == TypeKind::Object { @@ -270,9 +299,17 @@ impl ValueTypeData { } pub fn get_field_struct(&self, index: usize) -> ValueTypeData { - let h = &self.type_handle; - let offset = h.field_offset(index); - let field_handle = h.field_type(index); + self.get_field_struct_checked(index) + .expect("get_field_struct failed") + } + + pub fn get_field_struct_checked(&self, index: usize) -> crate::result::Result { + let (field_handle, offset) = self.checked_field_handle(index)?; + if !matches!(field_handle.kind(), TypeKind::Struct(_)) { + return Err(crate::result::Error::ExpectStructTypeError( + field_handle.kind(), + )); + } let layout = field_handle.layout(); let result = field_handle.default_value(); if layout.size() > 0 { @@ -284,15 +321,33 @@ impl ValueTypeData { } } } - result + Ok(result) } pub fn set_field_struct(&mut self, index: usize, value: &ValueTypeData) { - let h = &self.type_handle; - let offset = h.field_offset(index); - let field_handle = h.field_type(index); + self.set_field_struct_checked(index, value) + .expect("set_field_struct failed"); + } + + pub fn set_field_struct_checked( + &mut self, + index: usize, + value: &ValueTypeData, + ) -> crate::result::Result<()> { + let (field_handle, offset) = self.checked_field_handle(index)?; + if !matches!(field_handle.kind(), TypeKind::Struct(_)) { + return Err(crate::result::Error::ExpectStructTypeError( + field_handle.kind(), + )); + } + if field_handle.kind() != value.type_handle.kind() { + return Err(crate::result::Error::InvalidType( + field_handle.kind(), + value.type_handle.kind(), + )); + } let size = field_handle.size_of(); - assert_eq!( + debug_assert_eq!( size, value.type_handle.size_of(), "set_field_struct size mismatch" @@ -310,6 +365,7 @@ impl ValueTypeData { } } } + Ok(()) } pub fn call_method_struct_to_object( @@ -395,6 +451,7 @@ impl Clone for ValueTypeData { mod tests { use super::*; use crate::metadata_table::MetadataTable; + use crate::metadata_table::TypeKind; #[test] fn hstring_field_round_trips_overwrites_and_clones() { @@ -422,4 +479,26 @@ mod tests { assert!(value.get_field_hstring(0).is_err()); assert!(value.set_field_hstring(0, HSTRING::from("wrong")).is_err()); } + + #[test] + fn checked_field_access_reports_invalid_indices_and_non_struct_values() { + let table = MetadataTable::new(); + let typ = table.struct_type("Test.CheckedFieldAccess", &[table.i32_type()]); + let value = typ.default_value(); + + assert!(matches!( + value.field_type_checked(1), + Err(crate::result::Error::IndexOutOfBounds { index: 1, len: 1 }) + )); + assert!(matches!( + value.get_field_struct_checked(0), + Err(crate::result::Error::ExpectStructTypeError(TypeKind::I32)) + )); + + let non_struct = table.i32_type().default_value(); + assert!(matches!( + non_struct.field_type_checked(0), + Err(crate::result::Error::ExpectStructTypeError(TypeKind::I32)) + )); + } } diff --git a/crates/dynwinrt/src/result.rs b/crates/dynwinrt/src/result.rs index 8a064931..dacf2990 100644 --- a/crates/dynwinrt/src/result.rs +++ b/crates/dynwinrt/src/result.rs @@ -7,6 +7,11 @@ use crate::metadata_table::TypeKind; #[derive(Debug)] pub enum Error { ExpectObjectTypeError(TypeKind), + ExpectStructTypeError(TypeKind), + IndexOutOfBounds { + index: usize, + len: usize, + }, InvalidType(TypeKind, TypeKind), InvalidNestedOutType(TypeKind), InvalidTypeAbiToWinRT(TypeKind, AbiType), @@ -31,6 +36,12 @@ impl Error { Error::ExpectObjectTypeError(actual) => { format!("Expected object type, found {:?}", actual) } + Error::ExpectStructTypeError(actual) => { + format!("Expected struct type, found {:?}", actual) + } + Error::IndexOutOfBounds { index, len } => { + format!("Index {index} out of bounds (len {len})") + } Error::InvalidType(expected, actual) => { format!("Invalid type: expected {:?}, found {:?}", expected, actual) } diff --git a/docs/status/PYTHON_CHECKLIST.md b/docs/status/PYTHON_CHECKLIST.md index e45c7f3e..c099a8c9 100644 --- a/docs/status/PYTHON_CHECKLIST.md +++ b/docs/status/PYTHON_CHECKLIST.md @@ -127,7 +127,7 @@ of a dynamic projection. - [ ] Document generated-code version compatibility with `dynwinrt`. - [ ] Add troubleshooting for metadata, apartment, bootstrap, architecture, and wheel compatibility failures. -- [ ] Add progress-callback tests for worker-thread delivery and operations +- [x] Add progress-callback tests for worker-thread delivery and operations that complete concurrently with callback registration. ## WinUI milestone @@ -172,7 +172,7 @@ of a dynamic projection. ## Later -- [ ] Preserve projected object identity where it affects Python semantics. +- [x] Preserve projected object identity where it affects Python semantics. - [ ] Support delegates with more than two ABI parameters. - [ ] Add zero-copy Python buffer protocol integration. - [ ] Add performance benchmarks against pywinrt for representative APIs. diff --git a/docs/status/TODO.md b/docs/status/TODO.md index 3925839e..72fc1854 100644 --- a/docs/status/TODO.md +++ b/docs/status/TODO.md @@ -56,9 +56,11 @@ _None currently. Reserved for issues that make v0.1 unshippable (crash on happy - [ ] **Codegen: `--class-name` docs vs `--class` CLI**. The CLI derives the flag from the field name (`class_name` → `--class-name`), and docs use `--class-name`. Confirm both are wired consistently and that any lingering `--class` example is updated. (One instance in `main.rs` after_help was fixed in this review round.) -- [ ] **Codegen: snapshot coverage too narrow**. Only `Windows.Foundation.Uri` is snapshotted. Add snapshots for (a) an event-heavy type exercising `on*` / `off*` emission, (b) a parameterized interface / generic instantiation, (c) a class exercising inherited-interface flattening. Otherwise the recent IR refactors have no regression net. - -- [ ] **Rust: array typed getter can panic on bad index**. `crates/dynwinrt/src/array.rs:317` — `ArrayBuffer::Values(v) => v[index].as_i32().unwrap()` uses unchecked indexing while the CoTaskMem branch bounds-checks. Unify. +- [ ] **Codegen: snapshot coverage too narrow**. Python snapshots now cover + `Windows.Foundation.Uri` and the method-rich + `Windows.Storage.Streams.DataWriter`, but event-heavy types, parameterized + interfaces, and inherited-interface flattening still need dedicated + snapshots. - [ ] **Rust: `AppendOnlyBoxArena::stable_ptr` panics on out-of-range**. `crates/dynwinrt/src/metadata_table/append_only_arena.rs:45-49` — trusted internal use, but the invariant is undocumented and callers can drift. Add a documented safety contract and a debug assertion (or return `Option`). @@ -137,6 +139,7 @@ Kept for reference; git history is the source of truth. Grouped by area. - [x] `lock_or!` macro returns HRESULT on poisoning instead of panicking across FFI - [x] Nested struct recursive Clone/Drop (HString, COM pointers in nested structs) - [x] `ArrayData::get()` returns `WinRTValue::Null` for null COM elements instead of `IUnknown::from_raw(null)` (UB fix) +- [x] `ArrayData::get_i32()` returns checked `Result` errors for invalid indices/types across Values and CoTaskMem arrays - [x] FillArray / ReceiveArray error paths use `ArrayData::drop` for per-element release; `ArrayOutSlot` + `FillArraySlot` have Drop impls - [x] FillArray `actual_count` clamped to `capacity` (OOB read prevention) - [x] F32 delegate ABI: separate f32/f64 trampolines for 1- and 2-param delegates diff --git a/eng/coverage/coverage.ps1 b/eng/coverage/coverage.ps1 index 1de9f89c..e196176a 100644 --- a/eng/coverage/coverage.ps1 +++ b/eng/coverage/coverage.ps1 @@ -7,6 +7,13 @@ param( [string]$Python, [string]$CargoTarget, [string]$Win32Winmd = $env:DYNWINRT_WIN32_WINMD, + [ValidateRange(0, 100)] + [double]$MinRustLineCoverage = 45, + [ValidateRange(0, 100)] + [double]$MinPythonLineCoverage = 70, + [ValidateRange(0, 100)] + [double]$MinJavaScriptLineCoverage = 18, + [switch]$ValidateOnly, [switch]$SkipE2E, [switch]$SkipCom ) @@ -124,16 +131,18 @@ foreach ($candidate in ($outputCandidates | Select-Object -Unique)) { )) { throw "OutputDirectory cannot be a filesystem root: $candidate" } - foreach ($protectedPath in @($originalLocation, $root)) { - if (Test-PathContains $candidate $protectedPath) { - throw "OutputDirectory cannot contain protected directory: $protectedPath" + if (-not $ValidateOnly) { + foreach ($protectedPath in @($originalLocation, $root)) { + if (Test-PathContains $candidate $protectedPath) { + throw "OutputDirectory cannot contain protected directory: $protectedPath" + } + } + if ( + (Test-PathContains $root $candidate) -and + -not (Test-PathContains $repositoryArtifactRoot $candidate) + ) { + throw "OutputDirectory inside the repository must be under: $repositoryArtifactRoot" } - } - if ( - (Test-PathContains $root $candidate) -and - -not (Test-PathContains $repositoryArtifactRoot $candidate) - ) { - throw "OutputDirectory inside the repository must be under: $repositoryArtifactRoot" } } @@ -149,6 +158,17 @@ function Invoke-Step { } } +function Assert-MinimumCoverage { + param( + [string]$Name, + [double]$Actual, + [double]$Minimum + ) + if ($Actual -lt $Minimum) { + throw "$Name line coverage $Actual% is below the required $Minimum%" + } +} + function New-LiteralDirectory { param([string[]]$Path) foreach ($directory in $Path) { @@ -390,12 +410,13 @@ function Write-Reports { } } -function Write-CoverageSummary { +function Get-CoverageSummaryMarkdown { $rows = @() $rustSummary = Join-Path $rustReport "summary.json" if (Test-Path -LiteralPath $rustSummary) { $totals = (Get-Content -LiteralPath $rustSummary -Raw | ConvertFrom-Json).data[0].totals + Assert-MinimumCoverage "Rust" $totals.lines.percent $MinRustLineCoverage $rows += "| Rust, including native .pyd/.node | $([math]::Round($totals.lines.percent, 2))% | $([math]::Round($totals.functions.percent, 2))% | $([math]::Round($totals.regions.percent, 2))% regions |" } @@ -412,6 +433,7 @@ function Write-CoverageSummary { } else { 100 } + Assert-MinimumCoverage "Generated Python" $linePercent $MinPythonLineCoverage $rows += "| Generated Python projections | $linePercent% | n/a | $branchPercent% branches |" } @@ -424,21 +446,47 @@ function Write-CoverageSummary { $jsSummary = Join-Path $javascriptLayer.Path "coverage-summary.json" if (Test-Path -LiteralPath $jsSummary) { $totals = (Get-Content -LiteralPath $jsSummary -Raw | ConvertFrom-Json).total + if ($javascriptLayer.Name -eq "JavaScript aggregate") { + Assert-MinimumCoverage ` + $javascriptLayer.Name ` + $totals.lines.pct ` + $MinJavaScriptLineCoverage + } $rows += "| $($javascriptLayer.Name) | $($totals.lines.pct)% | $($totals.functions.pct)% | $($totals.branches.pct)% branches |" } } if ($rows.Count -gt 0) { - $summary = @( + return @( "# Mixed-language coverage" "" "| Layer | Lines | Functions | Branches/regions |" "| --- | ---: | ---: | ---: |" ) + $rows + } + + return @() +} + +function Write-CoverageSummary { + $summary = @(Get-CoverageSummaryMarkdown) + if ($summary.Count -gt 0) { Set-Content -LiteralPath (Join-Path $output "summary.md") -Value $summary -Encoding utf8 } } +if ($ValidateOnly) { + Set-Location $root + $summary = @(Get-CoverageSummaryMarkdown) + if ($summary.Count -eq 0) { + throw "No coverage summaries were found under $output" + } + foreach ($line in $summary) { + Write-Host $line + } + return +} + $scriptError = $null try { Set-Location $root diff --git a/eng/coverage/coverage.threshold.tests.ps1 b/eng/coverage/coverage.threshold.tests.ps1 new file mode 100644 index 00000000..b66c8ef5 --- /dev/null +++ b/eng/coverage/coverage.threshold.tests.ps1 @@ -0,0 +1,53 @@ +#!/usr/bin/env pwsh +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$ErrorActionPreference = "Stop" +$root = (Resolve-Path (Join-Path $PSScriptRoot "..\..")).Path +$coverageScript = Join-Path $root "eng\coverage\coverage.ps1" +$fixture = Join-Path $root "eng\coverage\testdata\threshold-baseline" + +function Assert-ExpectedFailure { + param( + [scriptblock]$Command, + [string]$ExpectedMessage + ) + + try { + & $Command + } catch { + if ($_.Exception.Message -like "*$ExpectedMessage*") { + return + } + throw + } + + throw "Expected failure containing: $ExpectedMessage" +} + +Write-Host "Validating default coverage thresholds against the recorded baseline..." +& $coverageScript ` + -OutputDirectory $fixture ` + -ValidateOnly + +Write-Host "Validating Rust threshold failures..." +Assert-ExpectedFailure { + & $coverageScript ` + -OutputDirectory $fixture ` + -ValidateOnly ` + -MinRustLineCoverage 47 ` + -MinPythonLineCoverage 70 ` + -MinJavaScriptLineCoverage 18 +} "Rust line coverage 46.67% is below the required 47%" + +Write-Host "Validating JavaScript threshold failures..." +Assert-ExpectedFailure { + & $coverageScript ` + -OutputDirectory $fixture ` + -ValidateOnly ` + -MinRustLineCoverage 45 ` + -MinPythonLineCoverage 70 ` + -MinJavaScriptLineCoverage 19 +} "JavaScript aggregate line coverage 18.77% is below the required 19%" + +Write-Host "Coverage threshold validation passed." diff --git a/eng/coverage/testdata/threshold-baseline/javascript/coverage-summary.json b/eng/coverage/testdata/threshold-baseline/javascript/coverage-summary.json new file mode 100644 index 00000000..e30374bd --- /dev/null +++ b/eng/coverage/testdata/threshold-baseline/javascript/coverage-summary.json @@ -0,0 +1,7 @@ +{ + "total": { + "lines": { "pct": 18.77 }, + "functions": { "pct": 20.83 }, + "branches": { "pct": 52.26 } + } +} diff --git a/eng/coverage/testdata/threshold-baseline/python/coverage.json b/eng/coverage/testdata/threshold-baseline/python/coverage.json new file mode 100644 index 00000000..bdfc8ddd --- /dev/null +++ b/eng/coverage/testdata/threshold-baseline/python/coverage.json @@ -0,0 +1,8 @@ +{ + "totals": { + "covered_lines": 159, + "num_statements": 220, + "covered_branches": 48, + "num_branches": 64 + } +} diff --git a/eng/coverage/testdata/threshold-baseline/rust/summary.json b/eng/coverage/testdata/threshold-baseline/rust/summary.json new file mode 100644 index 00000000..701dae03 --- /dev/null +++ b/eng/coverage/testdata/threshold-baseline/rust/summary.json @@ -0,0 +1,11 @@ +{ + "data": [ + { + "totals": { + "lines": { "percent": 46.67 }, + "functions": { "percent": 53.21 }, + "regions": { "percent": 44.98 } + } + } + ] +} diff --git a/tests/e2e/e2e_specs.json b/tests/e2e/e2e_specs.json index dd9b41fc..69758ba9 100644 --- a/tests/e2e/e2e_specs.json +++ b/tests/e2e/e2e_specs.json @@ -97,7 +97,8 @@ "args": ["https://example.com"] }, "checks": [ - { "kind": "interface_cast", "member": "as_interface", "interface_module": "uri", "interface_class": "IStringable", "method": "to_string", "contains": "example.com" } + { "kind": "interface_cast", "member": "as_interface", "interface_module": "uri", "interface_class": "IStringable", "method": "to_string", "contains": "example.com" }, + { "kind": "projection_identity", "member": "as_interface", "langs": ["py"], "interface_class": "IStringable" } ] }, { @@ -160,6 +161,8 @@ { "kind": "static_not_null", "member": "create_single", "args": [1.5] }, { "kind": "static_not_null", "member": "create_u_int8", "args": [200] }, { "kind": "static_not_null", "member": "create_u_int32", "args": [100000] }, + { "kind": "narrow_integer_overflow", "member": "create_u_int8", "langs": ["py"] }, + { "kind": "nullable_object_array_roundtrip", "member": "create_inspectable_array", "langs": ["py"] }, { "kind": "static_uuid_input", "member": "create_guid", "langs": ["py"] }, { "kind": "static_bytes_input", "member": "create_u_int8_array", "langs": ["py"] }, { "kind": "static_sequence_input", "member": "create_int32_array", "langs": ["py"] } diff --git a/tests/e2e/e2e_specs.schema.json b/tests/e2e/e2e_specs.schema.json index 9c58cb13..94494f3a 100644 --- a/tests/e2e/e2e_specs.schema.json +++ b/tests/e2e/e2e_specs.schema.json @@ -63,7 +63,10 @@ "method_result_contains", "static_equals", "static_not_null", + "narrow_integer_overflow", + "nullable_object_array_roundtrip", "interface_cast", + "projection_identity", "struct_roundtrip", "array_roundtrip", "static_string_length", diff --git a/tests/e2e/runners/py_runner.py b/tests/e2e/runners/py_runner.py index b218512b..e09e85ec 100644 --- a/tests/e2e/runners/py_runner.py +++ b/tests/e2e/runners/py_runner.py @@ -22,10 +22,32 @@ import threading +_WINRT_UINT_SUFFIXES = {'int8', 'int16', 'int32', 'int64'} + + +def collapse_winrt_uint_tokens(name: str) -> str: + tokens = name.split('_') + collapsed = [] + index = 0 + while index < len(tokens): + if ( + tokens[index] == 'u' + and index + 1 < len(tokens) + and tokens[index + 1] in _WINRT_UINT_SUFFIXES + ): + collapsed.append(f'u{tokens[index + 1]}') + index += 2 + else: + collapsed.append(tokens[index]) + index += 1 + return '_'.join(collapsed) + + def to_snake_case(name: str) -> str: """Convert PascalCase/camelCase to snake_case.""" value = re.sub(r'(.)([A-Z][a-z]+)', r'\1_\2', name) - return re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', value).lstrip('_').lower() + value = re.sub(r'([a-z0-9])([A-Z])', r'\1_\2', value).lstrip('_').lower() + return collapse_winrt_uint_tokens(value) def to_camel_case(name: str) -> str: @@ -153,6 +175,8 @@ async def run_check( namespace: str, ) -> dict: """Run a single check. Returns { kind, member, pass, error }.""" + import dynwinrt as dw + kind = check['kind'] member = to_snake_case(check['member']) if 'member' in check else '' cr = {'kind': kind, 'member': member, 'pass': False, 'error': None} @@ -289,6 +313,78 @@ async def run_check( else: cr['pass'] = True + elif kind == 'narrow_integer_overflow': + cases = ( + ('create_uint8', (256,)), + ('create_int16', (32768,)), + ('create_uint16', (65536,)), + ('create_char16', ('\U0001f600',)), + ('create_uint16_array', ([65536],)), + ('create_char16_array', (['\U0001f600'],)), + ) + for method_name, args in cases: + try: + getattr(cls, method_name)(*args) + except OverflowError: + continue + except Exception as error: + cr['error'] = ( + f'{method_name} raised {type(error).__name__}, ' + 'expected OverflowError' + ) + return cr + cr['error'] = f'{method_name} accepted an out-of-range value' + return cr + cr['pass'] = True + + elif kind == 'nullable_object_array_roundtrip': + uri_cls = generated_type(pkg_name, 'Uri') + uri = uri_cls.create_uri('https://example.com/null-array') + boxed = getattr(cls, member)( + [dw.DynWinRTValue.null_value(), uri._obj] + ) + if boxed is None: + cr['error'] = 'CreateInspectableArray returned None' + return cr + values = boxed.call_0( + 38, + dw.DynWinRTType.array_type(dw.DynWinRTType.object()), + ).as_array().to_values() + if len(values) != 2: + cr['error'] = f'expected 2 inspectable values, got {len(values)}' + elif not values[0].is_null(): + cr['error'] = 'null inspectable array element was not preserved' + elif values[1].identity_raw() != uri._obj.identity_raw(): + cr['error'] = 'inspectable array element lost COM identity' + else: + cr['pass'] = True + + elif kind == 'projection_identity': + import weakref + + iface_cls = generated_type(pkg_name, check['interface_class']) + same_class = cls(obj._obj) + iface_one = obj.as_interface(iface_cls) + iface_two = iface_cls.from_value(obj._obj) + + try: + class_ref = weakref.ref(obj) + iface_ref = weakref.ref(iface_one) + except TypeError as error: + cr['error'] = f'projected wrappers must support weak references: {error}' + return cr + + if same_class is not obj: + cr['error'] = 'runtime-class projection did not preserve wrapper identity' + elif iface_one is not iface_two: + cr['error'] = 'interface projection did not preserve wrapper identity' + elif iface_one is obj: + cr['error'] = 'distinct projected wrapper types shared one cache entry' + elif class_ref() is not obj or iface_ref() is not iface_one: + cr['error'] = 'projected wrappers did not remain weak-referenceable' + else: + cr['pass'] = True + elif kind == 'property_set_equals': set_value = check['set_value'] setattr(obj, member, set_value) @@ -363,7 +459,7 @@ async def run_check( return cr property_value_cls = generated_type(pkg_name, 'PropertyValue') - factory = getattr(property_value_cls, check['factory']) + factory = getattr(property_value_cls, to_snake_case(check['factory'])) boxed = factory(check['compatibility_value']) reference_cls = generated_type(pkg_name, check['reference_class']) @@ -1050,9 +1146,9 @@ def progress_without_loop(): writer.write_int16(-1234) writer.write_int32(-12345678) writer.write_int64(-1234567890123) - writer.write_u_int16(54321) - writer.write_u_int32(3_000_000_000) - writer.write_u_int64(9_000_000_000_000_000_000) + writer.write_uint16(54321) + writer.write_uint32(3_000_000_000) + writer.write_uint64(9_000_000_000_000_000_000) writer.write_single(1.25) writer.write_double(2.5) writer.write_date_time(timestamp) @@ -1074,9 +1170,9 @@ def progress_without_loop(): 'i16': reader.read_int16(), 'i32': reader.read_int32(), 'i64': reader.read_int64(), - 'u16': reader.read_u_int16(), - 'u32': reader.read_u_int32(), - 'u64': reader.read_u_int64(), + 'u16': reader.read_uint16(), + 'u32': reader.read_uint32(), + 'u64': reader.read_uint64(), 'f32': reader.read_single(), 'f64': reader.read_double(), 'datetime': reader.read_date_time(), @@ -1630,7 +1726,11 @@ def block_on_sta(): f'{module_path.name}: value wrapping branches failed' ) return cr - dw.release_projected(wrapped[1]) + if wrapped[1] is not uri: + cr['error'] = ( + f'{module_path.name}: wrapper identity was not reused' + ) + return cr counters['wrap_values'] += 1 box_reference = getattr( 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 62ed8026..28f00bb1 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs @@ -733,15 +733,40 @@ pub fn generate_class( } else { out.push_str(&format!("\nclass {}:\n", req_iface.name)); } - out.push_str(" def __init__(self, obj: DynWinRTValue):\n"); + out.push_str(" def __new__(cls, *args, **kwargs):\n"); + out.push_str( + " if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue):\n\ + \x20 return _dynwinrt_projected_from_native(cls, args[0], '_set_native')\n\ + \x20 return super().__new__(cls)\n\n", + ); + out.push_str(" def _set_native(self, obj: DynWinRTValue):\n"); out.push_str(&format!( " self._obj = obj.cast(IID_{})\n", req_iface.name )); + out.push_str(" self._dynwinrt_native_ready = True\n"); out.push_str(&format!( " _dynwinrt_track_projected(self, '{}.{}')\n", req_iface.namespace, req_iface.name )); + out.push_str(" _dynwinrt_cache_projected(self)\n"); + out.push('\n'); + out.push_str(" def __init__(self, obj: DynWinRTValue):\n"); + out.push_str( + " if getattr(self, '_dynwinrt_native_ready', False):\n\ + \x20 return\n", + ); + out.push_str(&format!( + " {}._set_native(self, obj)\n", + req_iface.name + )); + out.push('\n'); + out.push_str(" @classmethod\n"); + out.push_str(&format!( + " def _from_native(cls, obj: DynWinRTValue) -> '{}':\n", + req_iface.name + )); + out.push_str(" return cls(obj)\n"); out.push('\n'); out.push_str(" @staticmethod\n"); out.push_str(&format!( @@ -749,7 +774,7 @@ pub fn generate_class( req_iface.name )); out.push_str(&format!( - " return {}(obj.cast(IID_{}))\n", + " return {}._from_native(obj.cast(IID_{}))\n", req_iface.name, req_iface.name )); for method in reorder_getters_before_setters(&req_iface.methods) { @@ -1112,6 +1137,69 @@ fn generate_python_constructor( supported_override_names.sort(); supported_override_names.dedup(); let supported_override_names_expr = python_tuple(&supported_override_names); + let factory_methods = class + .factory_interfaces + .iter() + .flat_map(|iface| iface.methods.iter()) + .collect::>(); + let factory_names = + crate::codegen::winrt::python::overloads::method_names(factory_methods.iter().copied()); + let mut candidates = build_ctor_candidates(class, &factory_names, delegate_type_names); + candidates.sort_by(|left, right| { + crate::codegen::winrt::python::overloads::cmp_python_dispatch_params( + &left.public_params, + &right.public_params, + ) + .then_with(|| left.call_expr.cmp(&right.call_expr)) + }); + + out.push_str(" def __new__(cls, *args, **kwargs):\n"); + out.push_str( + " if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue):\n\ + \x20 return _dynwinrt_projected_from_native(cls, args[0], '_set_native')\n", + ); + if !candidates.is_empty() { + out.push_str(&format!(" if cls is {}:\n", class.name)); + 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, + known_types, + delegate_type_names, + ) + }) + .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" + )); + } + } + out.push_str(" return super().__new__(cls)\n\n"); if has_public_composition { out.push_str( @@ -1162,16 +1250,16 @@ fn generate_python_constructor( { out.push_str(" self._closed = False\n"); } + out.push_str(" self._dynwinrt_native_ready = True\n"); out.push_str(&format!( " _dynwinrt_track_projected(self, '{}')\n", class.full_name )); + out.push_str(" _dynwinrt_cache_projected(self)\n"); out.push('\n'); out.push_str(" @classmethod\n"); out.push_str(" def _from_native(cls, obj: DynWinRTValue):\n"); - out.push_str(" instance = cls.__new__(cls)\n"); - out.push_str(" instance._set_native(obj)\n"); - out.push_str(" return instance\n\n"); + out.push_str(" return cls(obj)\n\n"); if let Some(native_override_names) = &native_override_names { out.push_str(" @classmethod\n"); out.push_str( @@ -1215,21 +1303,16 @@ fn generate_python_constructor( )); } out.push_str(" def __init__(self, *args, **kwargs):\n"); + out.push_str( + " if getattr(self, '_dynwinrt_native_ready', False):\n\ + \x20 return\n", + ); out.push_str( " if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue):\n\ \x20 self._set_native(args[0])\n\ \x20 return\n", ); - let factory_methods = class - .factory_interfaces - .iter() - .flat_map(|iface| iface.methods.iter()) - .collect::>(); - let factory_names = - crate::codegen::winrt::python::overloads::method_names(factory_methods.iter().copied()); - - let candidates = build_ctor_candidates(class, &factory_names, delegate_type_names); if has_public_composition { let native_override_names = native_override_names .as_ref() @@ -1373,3 +1456,207 @@ fn factory_methods_for_name( }) .count() } + +#[cfg(test)] +mod tests { + use super::*; + use crate::meta::ConstructorMeta; + use crate::types::{TypeKind, TypeRef}; + use std::process::Command; + + fn enum_type(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 constructor_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, + }], + return_type: Some(TypeMeta::RuntimeClass { + namespace: "Contoso".into(), + name: "Widget".into(), + default_interface: None, + }), + ..Default::default() + } + } + + fn constructor_class(factory_methods: Vec) -> ClassMeta { + ClassMeta { + name: "Widget".into(), + namespace: "Contoso".into(), + full_name: "Contoso.Widget".into(), + factory_interfaces: vec![InterfaceMeta { + name: "IWidgetFactory".into(), + namespace: "Contoso".into(), + methods: factory_methods, + ..Default::default() + }], + constructors: vec![ConstructorMeta { + kind: ConstructorKind::FactoryActivation, + factory_interface: Some(TypeRef { + namespace: "Contoso".into(), + name: "IWidgetFactory".into(), + kind: TypeKind::Interface, + }), + }], + ..Default::default() + } + } + + fn run_python(script: &str) -> String { + fn invoke( + program: &str, + args: &[&str], + script: &str, + ) -> std::io::Result { + let mut command = Command::new(program); + for arg in args { + command.arg(arg); + } + command.arg(script).output() + } + + let output = invoke("python", &["-c"], script).or_else(|error| { + if error.kind() == std::io::ErrorKind::NotFound { + invoke("py", &["-3", "-c"], script) + } else { + Err(error) + } + }); + let output = output.unwrap_or_else(|error| panic!("failed to launch Python: {error}")); + assert!( + output.status.success(), + "python script failed\nstdout:\n{}\nstderr:\n{}\nscript:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + script + ); + String::from_utf8_lossy(&output.stdout).trim().to_string() + } + + #[test] + fn python_constructor_enum_overload_prefers_enum_over_i32_in_both_orders() { + let integer = constructor_method("Create", 6, TypeMeta::I32); + let enumeration = constructor_method("Create2", 7, enum_type("Mode")); + let known_types = HashSet::from(["Mode".to_string()]); + + let forward = generate_python_constructor( + &constructor_class(vec![integer.clone(), enumeration.clone()]), + &known_types, + &HashSet::new(), + None, + false, + ); + let reverse = generate_python_constructor( + &constructor_class(vec![enumeration, integer]), + &known_types, + &HashSet::new(), + None, + false, + ); + + assert_eq!(forward, reverse); + assert!(forward.contains( + "isinstance(_bound[0], int) and not isinstance(_bound[0], bool) and not isinstance(_bound[0], __import__('enum').Enum)" + )); + assert!(forward.contains("isinstance(_bound[0], _dynwinrt_symbol('mode', 'Mode'))")); + let forward_script = forward.replace("if cls is Widget:", "if cls is WidgetForward:"); + let reverse_script = reverse.replace("if cls is Widget:", "if cls is WidgetReverse:"); + + let script = format!( + r#"from enum import IntEnum +import json + +class DynWinRTValue: + pass + +def _dynwinrt_bind_overload(parameter_names, args, kwargs): + if kwargs: + if args: + return None + if len(kwargs) != len(parameter_names) or any(name not in kwargs for name in parameter_names): + return None + return tuple(kwargs[name] for name in parameter_names) + return args if len(args) == len(parameter_names) else None + +def _dynwinrt_projected_from_native(cls, obj, setter_name): + return obj + +def _dynwinrt_track_projected(obj, name): + return None + +def _dynwinrt_cache_projected(*args, **kwargs): + return None + +def _dynwinrt_symbol(module, name): + return globals()[name] + +class Mode(IntEnum): + VALUE = 1 + +class OtherMode(IntEnum): + VALUE = 1 + +class _CtorResult: + def __init__(self, value): + self._obj = value + +class WidgetForward: + @staticmethod + def _create_6(value): + return _CtorResult("i32") + + @staticmethod + def _create_7(value): + return _CtorResult("enum") + +{forward_script} + +class WidgetReverse: + @staticmethod + def _create_6(value): + return _CtorResult("i32") + + @staticmethod + def _create_7(value): + return _CtorResult("enum") + +{reverse_script} + +def exercise(widget_type): + enum_widget = widget_type(Mode.VALUE) + int_widget = widget_type(42) + results = [enum_widget._obj, int_widget._obj] + try: + widget_type(OtherMode.VALUE) + except TypeError as error: + results.append(type(error).__name__) + else: + results.append("unexpected") + return results + +print(json.dumps([exercise(WidgetForward), exercise(WidgetReverse)])) +"# + ); + + assert_eq!( + run_python(&script), + r#"[["enum", "i32", "TypeError"], ["enum", "i32", "TypeError"]]"# + ); + } +} 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 60ef8166..dacd1099 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs @@ -56,7 +56,8 @@ from dynwinrt import ( from dynwinrt.dynwinrt import ( _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_datetime_to_ticks, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, _dynwinrt_ticks_to_timedelta, - _dynwinrt_timedelta_to_ticks, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, + _dynwinrt_timedelta_to_ticks, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, + _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, ) 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 aeaf43ed..aefa945d 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs @@ -6,6 +6,7 @@ use super::*; use crate::codegen::winrt::python::native_types::{FoundationType, foundation_type}; use crate::codegen::winrt::python::type_helpers::py_optional_type; +use crate::types::FieldMeta; // ====================================================================== // Struct helpers: Python dataclass-style + _unpack/_pack functions @@ -26,11 +27,19 @@ pub(super) fn generate_struct_helpers(s: &TypeMeta) -> String { }; let mut out = String::new(); let snake_name = to_snake_case(name); + let field_names = py_struct_field_names(fields); + let slot_names = fields.iter().map(py_struct_slot_name).collect::>(); // Python class with typed fields out.push_str(&format!("\nclass {}:\n", name)); + out.push_str(&format!( + " __slots__ = {}\n", + py_string_tuple_literal(&slot_names) + )); + out.push('\n'); if fields.is_empty() { - out.push_str(" pass\n"); + out.push_str(" def __init__(self):\n"); + out.push_str(" pass\n"); } else { // __init__ with typed fields let init_params: Vec = fields @@ -80,6 +89,25 @@ pub(super) fn generate_struct_helpers(s: &TypeMeta) -> String { } } out.push('\n'); + out.push_str(" def __eq__(self, other: object) -> bool:\n"); + out.push_str(" if type(other) is not type(self):\n"); + out.push_str(" return NotImplemented\n"); + if field_names.is_empty() { + out.push_str(" return True\n"); + } else { + out.push_str(&format!( + " return {} == {}\n", + py_attribute_tuple_expr("self", &field_names), + py_attribute_tuple_expr("other", &field_names), + )); + } + out.push('\n'); + out.push_str(" def __repr__(self) -> str:\n"); + out.push_str(&format!( + " return {}\n", + py_struct_repr_expr(&field_names) + )); + out.push('\n'); // unpack function out.push_str(&format!( @@ -131,6 +159,65 @@ pub(super) fn generate_struct_helpers(s: &TypeMeta) -> String { out } +fn py_struct_slot_name(field: &FieldMeta) -> String { + let snake = to_snake_case(&field.name); + if ireference_inner_type(&field.typ).is_some() { + format!("_{snake}") + } else { + snake + } +} + +fn py_struct_field_names(fields: &[FieldMeta]) -> Vec { + fields + .iter() + .map(|field| to_snake_case(&field.name)) + .collect() +} + +fn py_string_tuple_literal(values: &[String]) -> String { + match values { + [] => "()".to_string(), + [value] => format!("('{value}',)"), + _ => format!( + "({})", + values + .iter() + .map(|value| format!("'{value}'")) + .collect::>() + .join(", ") + ), + } +} + +fn py_attribute_tuple_expr(receiver: &str, field_names: &[String]) -> String { + match field_names { + [] => "()".to_string(), + [field] => format!("({receiver}.{field},)"), + _ => format!( + "({})", + field_names + .iter() + .map(|field| format!("{receiver}.{field}")) + .collect::>() + .join(", ") + ), + } +} + +fn py_struct_repr_expr(field_names: &[String]) -> String { + if field_names.is_empty() { + return "f'{type(self).__name__}()'".to_string(); + } + + let fields = field_names + .iter() + .map(|field| format!("{field}={{self.{field}!r}}")) + .collect::>() + .join(", "); + format!("f'{{type(self).__name__}}({fields})'") +} + fn generate_foundation_struct_helpers(s: &TypeMeta, kind: FoundationType) -> String { let TypeMeta::Struct { namespace, 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 443941f5..2220d4d7 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs @@ -219,10 +219,16 @@ pub fn generate_interface( &doc, " ", )); } - out.push_str(" def __init__(self, obj: DynWinRTValue):\n"); + out.push_str(" def __new__(cls, *args, **kwargs):\n"); + out.push_str( + " if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue):\n\ + \x20 return _dynwinrt_projected_from_native(cls, args[0], '_set_native')\n\ + \x20 return super().__new__(cls)\n\n", + ); + out.push_str(" def _set_native(self, obj: DynWinRTValue):\n"); if let Some(vector_name) = &observable_vector { out.push_str(&format!( - " {}.__init__(self, obj)\n", + " {}._set_native(self, obj)\n", py_runtime_symbol(vector_name, vector_name) )); out.push_str(&format!( @@ -237,10 +243,26 @@ pub fn generate_interface( } else { out.push_str(" self._obj = obj\n"); } + out.push_str(" self._dynwinrt_native_ready = True\n"); out.push_str(&format!( " _dynwinrt_track_projected(self, '{}.{}')\n", iface.namespace, iface.name )); + out.push_str(" _dynwinrt_cache_projected(self)\n"); + out.push('\n'); + out.push_str(" def __init__(self, obj: DynWinRTValue):\n"); + out.push_str( + " if getattr(self, '_dynwinrt_native_ready', False):\n\ + \x20 return\n", + ); + out.push_str(&format!(" {}._set_native(self, obj)\n", iface.name)); + out.push('\n'); + out.push_str(" @classmethod\n"); + out.push_str(&format!( + " def _from_native(cls, obj: DynWinRTValue) -> '{}':\n", + iface.name + )); + out.push_str(" return cls(obj)\n"); out.push('\n'); // static from() — QI cast @@ -251,7 +273,7 @@ pub fn generate_interface( iface.name )); out.push_str(&format!( - " return {}(obj.cast(IID_{}))\n", + " return {}._from_native(obj.cast(IID_{}))\n", iface.name, iface.name )); out.push('\n'); @@ -394,7 +416,7 @@ pub fn generate_interface( " implementation = DynWinRtElementFactory.create(\n\ \x20 {ui_element_iid}, get_native, recycle_native\n\ \x20 )\n\ - \x20 factory = IElementFactory(implementation.to_value())\n\ + \x20 factory = IElementFactory._from_native(implementation.to_value())\n\ \x20 factory._element_factory_implementation = implementation\n\ \x20 factory._element_factory_elements = elements\n\ \x20 factory._element_factory_callback_state = callback_state\n\ diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs index 03182cab..96be151c 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs @@ -491,12 +491,18 @@ pub(crate) fn generate_instance_method_group( ); } + 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(overloads.iter().map(|overload| overload.method)); - let public_name = super::overloads::method_group_key(overloads[0].method, &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 mut out = String::new(); - let mut private_names = Vec::with_capacity(overloads.len()); - for overload in overloads { + let mut private_names = Vec::with_capacity(ordered_overloads.len()); + for overload in &ordered_overloads { let private_name = format!("_{}_{}", public_name, overload.method.vtable_index); out.push_str(&generate_method_body( &overload.iface_var, @@ -513,7 +519,7 @@ pub(crate) fn generate_instance_method_group( } out.push_str(&format!(" def {public_name}(self, *args, **kwargs):\n")); - for (overload, private_name) in overloads.iter().zip(private_names) { + 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() @@ -594,12 +600,18 @@ pub(crate) fn generate_static_method_group( }; } + 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(overloads.iter().map(|overload| overload.method)); - let public_name = super::overloads::method_group_key(overloads[0].method, &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 mut out = String::new(); - let mut private_names = Vec::with_capacity(overloads.len()); - for overload in overloads { + let mut private_names = Vec::with_capacity(ordered_overloads.len()); + for overload in &ordered_overloads { let private_name = format!("_{}_{}", public_name, overload.method.vtable_index); let code = match overload.kind { StaticOverloadKind::Factory => generate_factory_method_invoke_named( @@ -626,7 +638,7 @@ pub(crate) fn generate_static_method_group( out.push_str(" @staticmethod\n"); out.push_str(&format!(" def {public_name}(*args, **kwargs):\n")); - for (overload, private_name) in overloads.iter().zip(private_names) { + 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() @@ -893,7 +905,104 @@ pub(crate) fn generate_method_body( #[cfg(test)] mod tests { use super::*; - use crate::meta::ParamMeta; + use crate::meta::{ParamDirection, ParamMeta}; + use std::process::Command; + + fn overloaded_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() + } + } + + 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 static_overload<'a>( + class: &'a ClassMeta, + iface: &'a InterfaceMeta, + method: &'a MethodMeta, + ) -> StaticOverload<'a> { + StaticOverload { + class, + iface, + method, + kind: StaticOverloadKind::Static, + } + } + + fn enum_type(name: &str, is_flags: bool) -> TypeMeta { + TypeMeta::Enum { + namespace: "Contoso".into(), + name: name.into(), + underlying: Box::new(TypeMeta::I32), + members: Vec::new(), + is_flags, + doc: None, + deprecated: None, + } + } + + fn assert_contains_in_order(text: &str, first: &str, second: &str) { + let first_index = text + .find(first) + .unwrap_or_else(|| panic!("missing `{first}` in:\n{text}")); + let second_index = text + .find(second) + .unwrap_or_else(|| panic!("missing `{second}` in:\n{text}")); + assert!(first_index < second_index, "{text}"); + } + + fn extract_generated_block(code: &str, marker: &str) -> String { + code.find(marker) + .map(|index| code[index..].to_string()) + .unwrap_or_else(|| panic!("missing `{marker}` in:\n{code}")) + } + + fn run_python(script: &str) -> String { + fn invoke( + program: &str, + args: &[&str], + script: &str, + ) -> std::io::Result { + let mut command = Command::new(program); + for arg in args { + command.arg(arg); + } + command.arg(script).output() + } + + let output = invoke("python", &["-c"], script).or_else(|error| { + if error.kind() == std::io::ErrorKind::NotFound { + invoke("py", &["-3", "-c"], script) + } else { + Err(error) + } + }); + let output = output.unwrap_or_else(|error| panic!("failed to launch Python: {error}")); + assert!( + output.status.success(), + "python script failed\nstdout:\n{}\nstderr:\n{}\nscript:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + script + ); + String::from_utf8_lossy(&output.stdout).trim().to_string() + } #[test] fn static_delegate_return_stays_raw() { @@ -1046,7 +1155,9 @@ mod tests { assert!(code.contains("def _read_7(self, value: int)")); assert!(code.contains("def read(self, *args, **kwargs)")); assert!(code.contains("isinstance(_bound[0], str)")); - assert!(code.contains("isinstance(_bound[0], int)")); + assert!(code.contains( + "isinstance(_bound[0], int) and not isinstance(_bound[0], bool) and not isinstance(_bound[0], __import__('enum').Enum)" + )); } #[test] @@ -1250,4 +1361,310 @@ mod tests { assert!(code.contains("def _create_7(value: str)")); assert!(code.contains("def create(*args, **kwargs)")); } + + #[test] + fn python_numeric_overload_instance_dispatch_is_declaration_order_independent() { + 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)], + &HashSet::new(), + &HashSet::new(), + ); + let reverse = generate_instance_method_group( + &[instance_overload(&narrow), instance_overload(&wide)], + &HashSet::new(), + &HashSet::new(), + ); + + assert_eq!(forward, reverse); + assert_contains_in_order( + &forward, + "-128 <= _bound[0] <= 127", + "-2147483648 <= _bound[0] <= 2147483647", + ); + } + + #[test] + fn python_numeric_overload_dispatch_separates_bool_char16_ranges_and_float() { + let float = overloaded_method("Pick6", 11, TypeMeta::F64); + let unsigned = overloaded_method("Pick5", 10, TypeMeta::U8); + let string = overloaded_method("Pick4", 9, TypeMeta::String); + 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, &HashSet::new(), &HashSet::new()); + + assert_contains_in_order( + &code, + "if _bound is not None and isinstance(_bound[0], bool):", + "if _bound is not None and isinstance(_bound[0], int) and not isinstance(_bound[0], bool) and not isinstance(_bound[0], __import__('enum').Enum) and -128 <= _bound[0] <= 127:", + ); + assert_contains_in_order( + &code, + "if _bound is not None and isinstance(_bound[0], str) and len(_bound[0]) == 1 and ord(_bound[0]) <= 65535:", + "if _bound is not None and isinstance(_bound[0], str):", + ); + assert_contains_in_order( + &code, + "if _bound is not None and isinstance(_bound[0], int) and not isinstance(_bound[0], bool) and not isinstance(_bound[0], __import__('enum').Enum) and -128 <= _bound[0] <= 127:", + "if _bound is not None and isinstance(_bound[0], int) and not isinstance(_bound[0], bool) and not isinstance(_bound[0], __import__('enum').Enum) and 0 <= _bound[0] <= 255:", + ); + assert_contains_in_order( + &code, + "if _bound is not None and isinstance(_bound[0], int) and not isinstance(_bound[0], bool) and not isinstance(_bound[0], __import__('enum').Enum) and 0 <= _bound[0] <= 255:", + "if _bound is not None and isinstance(_bound[0], (int, float)) and not isinstance(_bound[0], bool) and not isinstance(_bound[0], __import__('enum').Enum):", + ); + assert!(code.contains("raise TypeError(\"No matching overload for pick\")")); + assert!( + !code.contains( + "if _bound is not None and isinstance(_bound[0], int) and not isinstance(_bound[0], bool) and not isinstance(_bound[0], __import__('enum').Enum):" + ), + "{code}" + ); + } + + #[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), + ], + &HashSet::new(), + &HashSet::new(), + ); + let reverse = generate_static_method_group( + &[ + static_overload(&class, &iface, &integer), + static_overload(&class, &iface, &float), + ], + &HashSet::new(), + &HashSet::new(), + ); + + assert_eq!(forward, reverse); + assert_contains_in_order( + &forward, + "-32768 <= _bound[0] <= 32767", + "isinstance(_bound[0], (int, float)) and not isinstance(_bound[0], bool) and not isinstance(_bound[0], __import__('enum').Enum)", + ); + } + + #[test] + fn python_known_int_enum_instance_overload_prefers_enum_over_i8_in_both_orders() { + let integer = overloaded_method("Read", 6, TypeMeta::I8); + let enumeration = overloaded_method("Read2", 7, enum_type("Mode", false)); + let known_types = HashSet::from(["Mode".to_string()]); + + let forward = generate_instance_method_group( + &[instance_overload(&integer), instance_overload(&enumeration)], + &known_types, + &HashSet::new(), + ); + let reverse = generate_instance_method_group( + &[instance_overload(&enumeration), instance_overload(&integer)], + &known_types, + &HashSet::new(), + ); + + let forward_dispatcher = + extract_generated_block(&forward, " def read(self, *args, **kwargs):\n"); + let reverse_dispatcher = + extract_generated_block(&reverse, " def read(self, *args, **kwargs):\n"); + let script = format!( + r#"from enum import IntEnum +import json + +class DynWinRTValue: + pass + +def _dynwinrt_bind_overload(parameter_names, args, kwargs): + if kwargs: + if args: + return None + if len(kwargs) != len(parameter_names) or any(name not in kwargs for name in parameter_names): + return None + return tuple(kwargs[name] for name in parameter_names) + return args if len(args) == len(parameter_names) else None + +def _dynwinrt_symbol(module, name): + return globals()[name] + +class Mode(IntEnum): + VALUE = 1 + +class OtherMode(IntEnum): + VALUE = 1 + +class ReaderForward: + def _read_6(self, value): + return "i8" + + def _read_7(self, value): + return "enum" + +{forward_dispatcher} + +class ReaderReverse: + def _read_6(self, value): + return "i8" + + def _read_7(self, value): + return "enum" + +{reverse_dispatcher} + +def exercise(reader_type): + reader = reader_type() + results = [reader.read(Mode.VALUE), reader.read(7)] + try: + reader.read(OtherMode.VALUE) + except TypeError as error: + results.append(type(error).__name__) + else: + results.append("unexpected") + return results + +print(json.dumps([exercise(ReaderForward), exercise(ReaderReverse)])) +"# + ); + + assert_eq!( + run_python(&script), + r#"[["enum", "i8", "TypeError"], ["enum", "i8", "TypeError"]]"# + ); + } + + #[test] + fn python_known_int_flag_static_overload_prefers_enum_over_i32_in_both_orders() { + let integer = overloaded_method("Create", 6, TypeMeta::I32); + let flags = overloaded_method("Create2", 7, enum_type("Options", true)); + let known_types = HashSet::from(["Options".to_string()]); + 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), + ], + &known_types, + &HashSet::new(), + ); + let reverse = generate_static_method_group( + &[ + static_overload(&class_reverse, &iface, &flags), + static_overload(&class_reverse, &iface, &integer), + ], + &known_types, + &HashSet::new(), + ); + + let forward_dispatcher = extract_generated_block( + &forward, + " @staticmethod\n def create(*args, **kwargs):\n", + ); + let reverse_dispatcher = extract_generated_block( + &reverse, + " @staticmethod\n def create(*args, **kwargs):\n", + ); + let script = format!( + r#"from enum import IntFlag +import json + +class DynWinRTValue: + pass + +def _dynwinrt_bind_overload(parameter_names, args, kwargs): + if kwargs: + if args: + return None + if len(kwargs) != len(parameter_names) or any(name not in kwargs for name in parameter_names): + return None + return tuple(kwargs[name] for name in parameter_names) + return args if len(args) == len(parameter_names) else None + +def _dynwinrt_symbol(module, name): + return globals()[name] + +class Options(IntFlag): + A = 1 + B = 2 + +class OtherOptions(IntFlag): + A = 1 + +class FactoryForward: + @staticmethod + def _create_6(value): + return "i32" + + @staticmethod + def _create_7(value): + return "enum" + +{forward_dispatcher} + +class FactoryReverse: + @staticmethod + def _create_6(value): + return "i32" + + @staticmethod + def _create_7(value): + return "enum" + +{reverse_dispatcher} + +def exercise(factory_type): + results = [factory_type.create(Options.A | Options.B), factory_type.create(42)] + try: + factory_type.create(OtherOptions.A) + except TypeError as error: + results.append(type(error).__name__) + else: + results.append("unexpected") + return results + +print(json.dumps([exercise(FactoryForward), exercise(FactoryReverse)])) +"# + ); + + assert_eq!( + run_python(&script), + r#"[["enum", "i32", "TypeError"], ["enum", "i32", "TypeError"]]"# + ); + } } diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs index ffb65128..a90b579d 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs @@ -108,6 +108,29 @@ pub fn python_public_module_name(name: &str) -> String { to_snake_case(name) } +fn is_winrt_uint_suffix(token: &str) -> bool { + matches!(token, "int8" | "int16" | "int32" | "int64") +} + +fn collapse_winrt_uint_tokens(name: &str) -> String { + let tokens: Vec<_> = name.split('_').collect(); + let mut normalized = Vec::with_capacity(tokens.len()); + let mut index = 0; + while index < tokens.len() { + if tokens[index] == "u" + && index + 1 < tokens.len() + && is_winrt_uint_suffix(tokens[index + 1]) + { + normalized.push(format!("u{}", tokens[index + 1])); + index += 2; + } else { + normalized.push(tokens[index].to_string()); + index += 1; + } + } + normalized.join("_") +} + /// Convert PascalCase / camelCase to snake_case. pub(crate) fn to_snake_case(s: &str) -> String { if s.is_empty() { @@ -130,7 +153,7 @@ pub(crate) fn to_snake_case(s: &str) -> String { result.push(c); } } - let result = result.trim_start_matches('_').to_string(); + let result = collapse_winrt_uint_tokens(result.trim_start_matches('_')); if is_py_reserved(&result) { format!("{}_", result) } else { @@ -189,3 +212,57 @@ pub fn to_snake_case_filename(name: &str) -> String { .unwrap_or_else(|| to_snake_case(name)) }) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn snake_case_keeps_winrt_uint_tokens_together() { + assert_eq!(to_snake_case("UInt8"), "uint8"); + assert_eq!(to_snake_case("UInt16"), "uint16"); + assert_eq!(to_snake_case("UInt32"), "uint32"); + assert_eq!(to_snake_case("UInt64"), "uint64"); + assert_eq!(to_snake_case("CreateUInt8"), "create_uint8"); + assert_eq!(to_snake_case("CreateUInt32Value"), "create_uint32_value"); + assert_eq!(to_snake_case("IReference_UInt32"), "i_reference_uint32"); + assert_eq!( + to_snake_case_filename("IReference_UInt32"), + "i_reference_uint32" + ); + } + + #[test] + fn snake_case_only_collapses_uint_word_boundaries() { + assert_eq!(to_snake_case("MenuInt8"), "menu_int8"); + assert_eq!(to_snake_case("GpuInt32"), "gpu_int32"); + assert_eq!(to_snake_case("MenuUInt8"), "menu_uint8"); + } + + #[test] + fn snake_case_preserves_acronym_regressions() { + assert_eq!(to_snake_case("GUID"), "guid"); + assert_eq!(to_snake_case("IIDComponent"), "iid_component"); + assert_eq!(to_snake_case("HTMLParser"), "html_parser"); + } + + #[test] + fn module_layout_collision_detection_uses_normalized_names() { + let err = install_python_module_layout([ + PythonTypeIdentity { + namespace: "Example".into(), + name: "UInt32".into(), + }, + PythonTypeIdentity { + namespace: "Example".into(), + name: "Uint32".into(), + }, + ]) + .err() + .expect("normalized module name collision should fail"); + + assert!(err.contains("Example.UInt32"), "{err}"); + assert!(err.contains("Example.Uint32"), "{err}"); + assert!(err.contains("example__uint32.py"), "{err}"); + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/overloads.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/overloads.rs index 6ae5ea11..e16ed758 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/overloads.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/overloads.rs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::meta::MethodMeta; +use crate::codegen::winrt::shared::imports::get_in_params; +use crate::meta::{MethodMeta, ParamMeta}; +use std::cmp::Ordering; use std::collections::HashSet; use super::naming::to_snake_case; +use super::signature::py_dispatch_type_sort_key; pub(crate) fn grouped_methods<'a>( methods: impl IntoIterator, @@ -55,3 +58,67 @@ pub(crate) fn method_group_key(method: &MethodMeta, names: &HashSet) -> } } } + +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); + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs index c1488756..5232e7cd 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs @@ -29,6 +29,83 @@ fn py_runtime_namespaced_symbol(namespace: &str, type_name: &str, symbol_name: & ) } +fn py_enum_instance_guard(name: &str) -> String { + format!("isinstance({name}, __import__('enum').Enum)") +} + +fn py_exact_int_guard(name: &str) -> String { + format!( + "isinstance({name}, int) and not isinstance({name}, bool) and not {}", + py_enum_instance_guard(name) + ) +} + +fn py_real_number_guard(name: &str) -> String { + format!( + "isinstance({name}, (int, float)) and not isinstance({name}, bool) and not {}", + py_enum_instance_guard(name) + ) +} + +pub(crate) fn py_integer_bounds(typ: &TypeMeta) -> Option<(i128, i128)> { + match typ { + TypeMeta::I8 => Some((i8::MIN as i128, i8::MAX as i128)), + TypeMeta::U8 => Some((u8::MIN as i128, u8::MAX as i128)), + TypeMeta::I16 => Some((i16::MIN as i128, i16::MAX as i128)), + TypeMeta::U16 => Some((u16::MIN as i128, u16::MAX as i128)), + TypeMeta::I32 => Some((i32::MIN as i128, i32::MAX as i128)), + TypeMeta::U32 => Some((u32::MIN as i128, u32::MAX as i128)), + TypeMeta::I64 => Some((i64::MIN as i128, i64::MAX as i128)), + TypeMeta::U64 => Some((u64::MIN as i128, u64::MAX as i128)), + _ => None, + } +} + +/// 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 +/// canonical specificity order that does not depend on WinMD declaration order. +/// We prefer exact bool/char/string shapes first, then narrower integer ranges +/// (signed before unsigned when widths overlap), and finally float fallbacks. +/// Enums sort after numeric branches because numeric guards reject `Enum` +/// instances, so plain ints still prefer numeric overloads while generated +/// `IntEnum`/`IntFlag` values reach their exact enum branch. +/// `IReference` sorts just after `T` so concrete values prefer the +/// non-nullable overload while `None` still resolves to the nullable branch. +pub(crate) fn py_dispatch_type_sort_key(typ: &TypeMeta) -> (u8, u16, u8, u8, String) { + if let Some(inner) = ireference_inner_type(typ) { + let (category, width, detail, _, label) = py_dispatch_type_sort_key(inner); + let label = if label.is_empty() { + "IReference".to_string() + } else { + format!("IReference<{label}>") + }; + return (category, width, detail, 1, label); + } + + match typ { + TypeMeta::Bool => (0, 0, 0, 0, String::new()), + TypeMeta::Char16 => (1, 0, 0, 0, String::new()), + TypeMeta::String => (2, 0, 0, 0, String::new()), + TypeMeta::I8 => (3, 8, 0, 0, String::new()), + TypeMeta::U8 => (3, 8, 1, 0, String::new()), + TypeMeta::I16 => (3, 16, 0, 0, String::new()), + TypeMeta::U16 => (3, 16, 1, 0, String::new()), + TypeMeta::I32 => (3, 32, 0, 0, String::new()), + TypeMeta::U32 => (3, 32, 1, 0, String::new()), + TypeMeta::I64 => (3, 64, 0, 0, String::new()), + TypeMeta::U64 => (3, 64, 1, 0, String::new()), + // Python's native float is a C double, so prefer F64 when both float + // widths would otherwise accept the same runtime value. + TypeMeta::F64 => (4, 0, 0, 0, String::new()), + TypeMeta::F32 => (4, 1, 0, 0, String::new()), + TypeMeta::Enum { name, .. } => (5, 0, 0, 0, name.clone()), + TypeMeta::Guid => (6, 0, 0, 0, "Guid".to_string()), + TypeMeta::Array(_) => (7, 0, 0, 0, format!("{typ:?}")), + _ => (8, 0, 0, 0, format!("{typ:?}")), + } +} + // ====================================================================== // Python type expression // ====================================================================== @@ -439,22 +516,15 @@ pub(crate) fn py_type_guard(name: &str, typ: &TypeMeta, known_types: &HashSet format!("isinstance({name}, bool)"), - TypeMeta::I8 - | TypeMeta::U8 - | TypeMeta::I16 - | TypeMeta::U16 - | TypeMeta::I32 - | TypeMeta::U32 - | TypeMeta::I64 - | TypeMeta::U64 => { - format!("isinstance({name}, int) and not isinstance({name}, bool)") - } - TypeMeta::F32 | TypeMeta::F64 => { - format!("isinstance({name}, (int, float)) and not isinstance({name}, bool)") - } - TypeMeta::Char16 => format!("isinstance({name}, str) and len({name}) == 1"), + TypeMeta::F32 | TypeMeta::F64 => py_real_number_guard(name), + TypeMeta::Char16 => { + format!("isinstance({name}, str) and len({name}) == 1 and ord({name}) <= 65535") + } TypeMeta::String => format!("isinstance({name}, str)"), TypeMeta::Guid => format!("isinstance({name}, UUID)"), TypeMeta::Enum { @@ -465,9 +535,7 @@ pub(crate) fn py_type_guard(name: &str, typ: &TypeMeta, known_types: &HashSet { - format!("isinstance({name}, int) and not isinstance({name}, bool)") - } + TypeMeta::Enum { .. } => py_exact_int_guard(name), TypeMeta::Array(_) => format!( "isinstance({name}, (DynWinRTArray, bytes, bytearray, Sequence)) and not isinstance({name}, str)" ), @@ -768,6 +836,18 @@ pub(crate) fn py_generate_interface_registration(iface: &InterfaceMeta, var_name mod tests { use super::*; + fn enum_type(name: &str, is_flags: bool) -> TypeMeta { + TypeMeta::Enum { + namespace: "Contoso".into(), + name: name.into(), + underlying: Box::new(TypeMeta::I32), + members: Vec::new(), + is_flags, + doc: None, + deprecated: None, + } + } + fn geometry_type() -> TypeMeta { TypeMeta::RuntimeClass { namespace: "Microsoft.UI.Xaml.Media".into(), @@ -797,4 +877,50 @@ mod tests { )] ); } + + #[test] + fn python_numeric_overload_integer_guards_use_exact_ranges() { + let known = HashSet::new(); + assert_eq!( + py_type_guard("value", &TypeMeta::I8, &known), + "isinstance(value, int) and not isinstance(value, bool) and not isinstance(value, __import__('enum').Enum) and -128 <= value <= 127" + ); + assert_eq!( + py_type_guard("value", &TypeMeta::U8, &known), + "isinstance(value, int) and not isinstance(value, bool) and not isinstance(value, __import__('enum').Enum) and 0 <= value <= 255" + ); + assert_eq!( + py_type_guard("value", &TypeMeta::U64, &known), + format!( + "isinstance(value, int) and not isinstance(value, bool) and not isinstance(value, __import__('enum').Enum) and 0 <= value <= {}", + u64::MAX + ) + ); + } + + #[test] + fn python_numeric_overload_float_guards_reject_enum_instances() { + let known = HashSet::new(); + assert_eq!( + py_type_guard("value", &TypeMeta::F64, &known), + "isinstance(value, (int, float)) and not isinstance(value, bool) and not isinstance(value, __import__('enum').Enum)" + ); + } + + #[test] + fn python_known_enum_guards_are_precise_and_unknown_enums_fail_closed() { + let known = HashSet::from(["Mode".to_string()]); + assert_eq!( + py_type_guard("value", &enum_type("Mode", false), &known), + "isinstance(value, _dynwinrt_symbol('mode', 'Mode'))" + ); + assert_eq!( + py_type_guard("value", &enum_type("Mode", false), &HashSet::new()), + "isinstance(value, int) and not isinstance(value, bool) and not isinstance(value, __import__('enum').Enum)" + ); + assert_eq!( + py_type_guard("value", &enum_type("Options", true), &HashSet::new()), + "isinstance(value, int) and not isinstance(value, bool) and not isinstance(value, __import__('enum').Enum)" + ); + } } diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs index 44be3ede..07a8931d 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs @@ -7,7 +7,7 @@ use std::collections::HashSet; use crate::codegen::winrt::shared::imports::get_in_params; use crate::meta::MethodMeta; -use crate::types::{TypeKind, TypeMeta}; +use crate::types::{FieldMeta, TypeKind, TypeMeta}; use super::naming::{python_module_name, to_snake_case}; use super::native_types::{FoundationType, foundation_type}; @@ -74,10 +74,15 @@ pub(super) fn emit_struct_stub(s: &TypeMeta) -> String { }; let mut out = String::new(); let snake_name = to_snake_case(name); + let slot_names = fields.iter().map(py_struct_slot_name).collect::>(); out.push_str(&format!("\nclass {}:\n", name)); + out.push_str(&format!( + " __slots__ = {}\n", + py_string_tuple_literal(&slot_names) + )); if fields.is_empty() { - out.push_str(" pass\n"); + out.push_str(" def __init__(self) -> None: ...\n"); } else { let init_params: Vec = fields .iter() @@ -113,6 +118,8 @@ pub(super) fn emit_struct_stub(s: &TypeMeta) -> String { } } } + out.push_str(" def __eq__(self, other: object) -> bool: ...\n"); + out.push_str(" def __repr__(self) -> str: ...\n"); out.push('\n'); out.push_str(&format!( @@ -127,6 +134,30 @@ pub(super) fn emit_struct_stub(s: &TypeMeta) -> String { out } +fn py_struct_slot_name(field: &FieldMeta) -> String { + let snake = to_snake_case(&field.name); + if ireference_inner_type(&field.typ).is_some() { + format!("_{snake}") + } else { + snake + } +} + +fn py_string_tuple_literal(values: &[String]) -> String { + match values { + [] => "()".to_string(), + [value] => format!("('{value}',)"), + _ => format!( + "({})", + values + .iter() + .map(|value| format!("'{value}'")) + .collect::>() + .join(", ") + ), + } +} + pub(super) fn py_struct_field_stub_type(typ: &TypeMeta) -> String { if ireference_inner_type(typ).is_some() { return py_struct_field_type(typ); diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs index de749abe..7821bc10 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs @@ -915,6 +915,7 @@ fn emit_constructor_stubs( out.push_str(" def __new__(cls, _not_constructible: NoReturn) -> NoReturn: ...\n"); return out; } + overloads.sort_by(|left, right| super::overloads::cmp_python_dispatch_params(left, right)); let count = overloads.len(); for params in &overloads { @@ -999,9 +1000,13 @@ fn emit_instance_stub_group( event_has_remove: bool, property_has_getter: bool, ) -> String { - if methods.len() == 1 { + 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( - methods[0], + ordered_methods[0], known_types, delegate_type_names, indent_spaces, @@ -1009,10 +1014,10 @@ fn emit_instance_stub_group( property_has_getter, ); } - let names = super::overloads::method_names(methods.iter().copied()); - let public_name = super::overloads::method_group_key(methods[0], &names); + 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); - methods + ordered_methods .iter() .map(|method| { format!( @@ -1053,8 +1058,13 @@ fn emit_static_stub_group( known_types: &HashSet, delegate_type_names: &HashSet, ) -> String { - if methods.len() == 1 { - let (method, is_factory) = methods[0]; + 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, @@ -1063,9 +1073,9 @@ fn emit_static_stub_group( delegate_type_names, ); } - let names = super::overloads::method_names(methods.iter().map(|(method, _)| *method)); - let public_name = super::overloads::method_group_key(methods[0].0, &names); - methods + 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!( diff --git a/tools/dynwinrt-codegen/tests/element_factory_test.rs b/tools/dynwinrt-codegen/tests/element_factory_test.rs index f03fae64..4247a042 100644 --- a/tools/dynwinrt-codegen/tests/element_factory_test.rs +++ b/tools/dynwinrt-codegen/tests/element_factory_test.rs @@ -264,6 +264,7 @@ fn element_factory_projects_js_callback_constructor() { assert!(py.contains("element = elements.pop(native.identity_raw(), projected_element)")); assert!(py.contains("callback_state = [True]")); assert!(py.contains("callback_state[0] = False")); + assert!(py.contains("factory = IElementFactory._from_native(implementation.to_value())")); assert!( py.matches("IElementFactory callbacks have been released.") .count() diff --git a/tools/dynwinrt-codegen/tests/ireference_struct_field_test.rs b/tools/dynwinrt-codegen/tests/ireference_struct_field_test.rs index 042a32dc..cde78f4a 100644 --- a/tools/dynwinrt-codegen/tests/ireference_struct_field_test.rs +++ b/tools/dynwinrt-codegen/tests/ireference_struct_field_test.rs @@ -294,6 +294,22 @@ fn nested_struct_defaults_and_enum_fields_are_python_native() { let py = python::generate_class(&class, &known, &HashSet::new(), &HashSet::new()); let pyi = python_stub::generate_class_stub(&class, &known, &HashSet::new(), &HashSet::new()); + assert!( + py.contains("class Inner:\n __slots__ = ('count',)"), + "{py}" + ); + assert!( + py.contains("return (self.count,) == (other.count,)"), + "{py}" + ); + assert!( + py.contains("return f'{type(self).__name__}(count={self.count!r})'"), + "{py}" + ); + assert!( + py.contains("class Outer:\n __slots__ = ('mode', 'inner')"), + "{py}" + ); assert!( py.contains("def __init__(self, mode: 'Mode' = _dynwinrt_enum('mode', 'Mode', 0), inner: Inner | None = None):"), "{py}" @@ -309,7 +325,28 @@ fn nested_struct_defaults_and_enum_fields_are_python_native() { ); assert!(py.contains("s.set_u32(0, int(v.mode))"), "{py}"); assert!(py.contains("s.set_struct(1, _pack_inner(v.inner))"), "{py}"); + assert!( + py.contains("return (self.mode, self.inner) == (other.mode, other.inner)"), + "{py}" + ); + assert!( + py.contains("return f'{type(self).__name__}(mode={self.mode!r}, inner={self.inner!r})'"), + "{py}" + ); + assert!( + pyi.contains("class Inner:\n __slots__ = ('count',)"), + "{pyi}" + ); + assert!( + pyi.contains("def __eq__(self, other: object) -> bool: ..."), + "{pyi}" + ); + assert!(pyi.contains("def __repr__(self) -> str: ..."), "{pyi}"); + assert!( + pyi.contains("class Outer:\n __slots__ = ('mode', 'inner')"), + "{pyi}" + ); assert!( pyi.contains("def __init__(self, mode: 'Mode' = ..., inner: 'Inner' = ...) -> None: ..."), "{pyi}" @@ -318,6 +355,43 @@ fn nested_struct_defaults_and_enum_fields_are_python_native() { assert!(pyi.contains("inner: 'Inner'"), "{pyi}"); } +#[test] +fn empty_structs_emit_slots_and_value_semantics() { + let empty = TypeMeta::Struct { + namespace: "Synthetic".into(), + name: "EmptyMarker".into(), + fields: vec![], + }; + let class = class_with_struct("UsesEmptyMarker", empty); + let known = HashSet::from(["UsesEmptyMarker".to_string(), "EmptyMarker".to_string()]); + let py = python::generate_class(&class, &known, &HashSet::new(), &HashSet::new()); + let pyi = python_stub::generate_class_stub(&class, &known, &HashSet::new(), &HashSet::new()); + + assert!( + py.contains( + "class EmptyMarker:\n __slots__ = ()\n\n def __init__(self):\n pass" + ), + "{py}" + ); + assert!( + py.contains( + "def __eq__(self, other: object) -> bool:\n if type(other) is not type(self):\n return NotImplemented\n return True" + ), + "{py}" + ); + assert!( + py.contains("def __repr__(self) -> str:\n return f'{type(self).__name__}()'"), + "{py}" + ); + + assert!( + pyi.contains( + "class EmptyMarker:\n __slots__ = ()\n def __init__(self) -> None: ...\n def __eq__(self, other: object) -> bool: ...\n def __repr__(self) -> str: ..." + ), + "{pyi}" + ); +} + #[test] fn sdk_http_progress_ireference_u64_fields_are_native_optional_values() { if !std::path::Path::new(WINDOWS_WINMD).exists() { @@ -342,7 +416,7 @@ fn sdk_http_progress_ireference_u64_fields_are_native_optional_values() { "s.set_object(2, _dynwinrt_box_reference(v.total_bytes_to_send, DynWinRTType.u64_type(), lambda value: DynWinRTValue.from_u64(value)))" )); assert!(py.contains( - "None if value.is_null() else _dynwinrt_symbol('i_reference_u_int64', 'IReference_UInt64')(value).value" + "None if value.is_null() else _dynwinrt_symbol('i_reference_uint64', 'IReference_UInt64')(value).value" )); assert!( dts.contains("totalBytesToSend: bigint | null | IReference_UInt64;") diff --git a/tools/dynwinrt-codegen/tests/nullable_return_test.rs b/tools/dynwinrt-codegen/tests/nullable_return_test.rs index 7e3eea17..fab7a244 100644 --- a/tools/dynwinrt-codegen/tests/nullable_return_test.rs +++ b/tools/dynwinrt-codegen/tests/nullable_return_test.rs @@ -158,7 +158,7 @@ fn ireference_values_are_projected_as_native_nullable_values() { assert!(py.contains("def day(self) -> int | None:")); assert!(py.contains( - "None if value.is_null() else _dynwinrt_symbol('i_reference_u_int32', 'IReference_UInt32')(value).value" + "None if value.is_null() else _dynwinrt_symbol('i_reference_uint32', 'IReference_UInt32')(value).value" )); assert!(py.contains("def day(self, value: int | None | IReference_UInt32):")); assert!(py.contains( diff --git a/tools/dynwinrt-codegen/tests/observable_vector_test.rs b/tools/dynwinrt-codegen/tests/observable_vector_test.rs index c8df1408..510117cb 100644 --- a/tools/dynwinrt-codegen/tests/observable_vector_test.rs +++ b/tools/dynwinrt-codegen/tests/observable_vector_test.rs @@ -101,7 +101,7 @@ fn observable_vector_projects_python_mutable_sequence_and_typed_events() { "class IObservableVector_Object(_dynwinrt_symbol('i_vector_object', 'IVector_Object')):" )); assert!( - py.contains("_dynwinrt_symbol('i_vector_object', 'IVector_Object').__init__(self, obj)") + py.contains("_dynwinrt_symbol('i_vector_object', 'IVector_Object')._set_native(self, obj)") ); assert!(py.contains("self._observable_obj = obj.cast(IID_IObservableVector_Object)")); assert!(py.contains("def create(items: Iterable['DynWinRTValue'])")); diff --git a/tools/dynwinrt-codegen/tests/python_constructor_boundary_test.rs b/tools/dynwinrt-codegen/tests/python_constructor_boundary_test.rs index 3b1579e6..28d67b26 100644 --- a/tools/dynwinrt-codegen/tests/python_constructor_boundary_test.rs +++ b/tools/dynwinrt-codegen/tests/python_constructor_boundary_test.rs @@ -101,6 +101,67 @@ fn only_referenced_public_factory_metadata_becomes_a_constructor() { assert!(!pyi.contains("def __init__(self) -> None: ...")); } +#[test] +fn numeric_constructor_overloads_dispatch_by_specificity() { + let parameter = |typ| ParamMeta { + name: "value".into(), + typ, + direction: ParamDirection::In, + }; + let factory = InterfaceMeta { + name: "ISystemResultFactory".into(), + namespace: "Contoso".into(), + iid: "11111111-1111-1111-1111-111111111111".into(), + methods: vec![ + MethodMeta { + name: "Create2".into(), + raw_name: "Create2".into(), + vtable_index: 7, + params: vec![parameter(TypeMeta::I32)], + return_type: Some(runtime_class("SystemResult")), + ..Default::default() + }, + MethodMeta { + name: "Create".into(), + raw_name: "Create".into(), + vtable_index: 6, + params: vec![parameter(TypeMeta::I8)], + return_type: Some(runtime_class("SystemResult")), + ..Default::default() + }, + ], + ..Default::default() + }; + let class = ClassMeta { + name: "SystemResult".into(), + namespace: "Contoso".into(), + full_name: "Contoso.SystemResult".into(), + factory_interfaces: vec![factory], + constructors: vec![ConstructorMeta { + kind: ConstructorKind::FactoryActivation, + factory_interface: Some(TypeRef { + namespace: "Contoso".into(), + name: "ISystemResultFactory".into(), + kind: TypeKind::Interface, + }), + }], + ..Default::default() + }; + let known = HashSet::from(["SystemResult".into()]); + + let py = python::generate_class(&class, &known, &HashSet::new(), &HashSet::new()); + let narrow_guard = "-128 <= _bound[0] <= 127"; + let wide_guard = "-2147483648 <= _bound[0] <= 2147483647"; + assert!( + py.find(narrow_guard).expect("narrow constructor guard") + < py.find(wide_guard).expect("wide constructor guard"), + "{py}" + ); + + let pyi = python_stub::generate_class_stub(&class, &known, &HashSet::new(), &HashSet::new()); + assert_eq!(pyi.matches(" @overload\n").count(), 4, "{pyi}"); +} + #[test] fn protected_composition_is_not_public_construction() { let class = ClassMeta { diff --git a/tools/dynwinrt-codegen/tests/python_identity_cache_test.rs b/tools/dynwinrt-codegen/tests/python_identity_cache_test.rs new file mode 100644 index 00000000..6aa8d50a --- /dev/null +++ b/tools/dynwinrt-codegen/tests/python_identity_cache_test.rs @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::HashSet; + +use dynwinrt_codegen::codegen::python; +use dynwinrt_codegen::meta::{ + ClassMeta, ConstructorKind, ConstructorMeta, InterfaceMeta, MethodMeta, ParamDirection, + ParamMeta, +}; +use dynwinrt_codegen::types::{TypeKind, TypeMeta, TypeRef}; + +#[test] +fn runtime_class_generation_uses_projected_identity_cache() { + let class = ClassMeta { + name: "Widget".into(), + namespace: "Contoso".into(), + full_name: "Contoso.Widget".into(), + default_interface: Some(InterfaceMeta { + name: "IWidget".into(), + namespace: "Contoso".into(), + iid: "11111111-1111-1111-1111-111111111111".into(), + ..Default::default() + }), + ..Default::default() + }; + + let py = python::generate_class( + &class, + &HashSet::from(["Widget".to_string()]), + &HashSet::new(), + &HashSet::new(), + ); + + assert!( + py.contains("_dynwinrt_projected_from_native"), + "missing projected identity helper import:\n{py}" + ); + assert!( + py.contains("_dynwinrt_cache_projected"), + "missing projected cache helper import:\n{py}" + ); + assert!( + py.contains("def __new__(cls, *args, **kwargs):"), + "missing native-wrap __new__:\n{py}" + ); + assert!( + py.contains("return _dynwinrt_projected_from_native(cls, args[0], '_set_native')"), + "missing cached native-wrap path:\n{py}" + ); + assert!( + py.contains("self._dynwinrt_native_ready = True"), + "missing native initialization flag:\n{py}" + ); + assert!( + py.contains("_dynwinrt_cache_projected(self)"), + "missing projected cache registration:\n{py}" + ); + assert!( + py.contains("def _from_native(cls, obj: DynWinRTValue):\n return cls(obj)"), + "missing cached _from_native helper:\n{py}" + ); +} + +#[test] +fn runtime_class_public_constructor_registers_final_self() { + let class = ClassMeta { + name: "Widget".into(), + namespace: "Contoso".into(), + full_name: "Contoso.Widget".into(), + default_interface: Some(InterfaceMeta { + name: "IWidget".into(), + namespace: "Contoso".into(), + iid: "11111111-1111-1111-1111-111111111111".into(), + ..Default::default() + }), + factory_interfaces: vec![InterfaceMeta { + name: "IWidgetFactory".into(), + namespace: "Contoso".into(), + iid: "22222222-2222-2222-2222-222222222222".into(), + methods: vec![MethodMeta { + name: "CreateWidget".into(), + raw_name: "CreateWidget".into(), + vtable_index: 6, + params: vec![ParamMeta { + name: "name".into(), + typ: TypeMeta::String, + direction: ParamDirection::In, + }], + return_type: Some(TypeMeta::RuntimeClass { + namespace: "Contoso".into(), + name: "Widget".into(), + default_interface: None, + }), + ..Default::default() + }], + ..Default::default() + }], + constructors: vec![ConstructorMeta { + kind: ConstructorKind::FactoryActivation, + factory_interface: Some(TypeRef { + namespace: "Contoso".into(), + name: "IWidgetFactory".into(), + kind: TypeKind::Interface, + }), + }], + ..Default::default() + }; + + let py = python::generate_class( + &class, + &HashSet::from(["Widget".to_string()]), + &HashSet::new(), + &HashSet::new(), + ); + + assert!( + py.contains("return cls.create_widget(_bound[0])"), + "exact-class constructors must return the cached factory wrapper:\n{py}" + ); + assert!( + py.contains("self._set_native(type(self).create_widget(_bound[0])._obj)"), + "subclass constructors must retain the self-binding fallback:\n{py}" + ); + assert!( + py.contains("_dynwinrt_cache_projected(self)"), + "public constructors must register the final self:\n{py}" + ); +} + +#[test] +fn interface_generation_uses_projected_identity_cache() { + let iface = InterfaceMeta { + name: "IWidget".into(), + namespace: "Contoso".into(), + iid: "11111111-1111-1111-1111-111111111111".into(), + ..Default::default() + }; + + let py = python::generate_interface( + &iface, + &HashSet::from(["IWidget".to_string()]), + &HashSet::new(), + ); + + assert!( + py.contains("def __new__(cls, *args, **kwargs):"), + "missing native-wrap __new__:\n{py}" + ); + assert!( + py.contains("def _set_native(self, obj: DynWinRTValue):"), + "missing native initializer:\n{py}" + ); + assert!( + py.contains("_dynwinrt_cache_projected(self)"), + "interfaces should register cache entries for initialized wrappers:\n{py}" + ); + assert!( + py.contains( + "def _from_native(cls, obj: DynWinRTValue) -> 'IWidget':\n return cls(obj)" + ), + "missing cached _from_native helper:\n{py}" + ); + assert!( + py.contains("return IWidget._from_native(obj.cast(IID_IWidget))"), + "from_value should reuse the cached wrapper path:\n{py}" + ); +} diff --git a/tools/dynwinrt-codegen/tests/python_numeric_overload_dispatch_test.rs b/tools/dynwinrt-codegen/tests/python_numeric_overload_dispatch_test.rs new file mode 100644 index 00000000..52312e1f --- /dev/null +++ b/tools/dynwinrt-codegen/tests/python_numeric_overload_dispatch_test.rs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::HashSet; + +use dynwinrt_codegen::codegen::python_stub; +use dynwinrt_codegen::meta::{ClassMeta, InterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; +use dynwinrt_codegen::types::TypeMeta; + +fn overloaded_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_stubs_retain_typing_overload() { + let class = ClassMeta { + name: "Reader".into(), + namespace: "Contoso".into(), + full_name: "Contoso.Reader".into(), + default_interface: Some(InterfaceMeta { + name: "IReader".into(), + namespace: "Contoso".into(), + iid: "11111111-1111-1111-1111-111111111111".into(), + methods: vec![ + overloaded_method("Pick2", 7, TypeMeta::F64), + overloaded_method("Pick", 6, TypeMeta::I32), + ], + ..Default::default() + }), + ..Default::default() + }; + let known = HashSet::from(["Reader".into()]); + + let pyi = python_stub::generate_class_stub(&class, &known, &HashSet::new(), &HashSet::new()); + + assert!(pyi.contains("from typing import overload")); + assert_eq!(pyi.matches(" @overload\n").count(), 2); + assert!( + pyi.find("def pick(self, value: int)") < pyi.find("def pick(self, value: float)"), + "{pyi}" + ); +} diff --git a/tools/dynwinrt-codegen/tests/snapshot_test.rs b/tools/dynwinrt-codegen/tests/snapshot_test.rs index 1bdd1d50..5aeab484 100644 --- a/tools/dynwinrt-codegen/tests/snapshot_test.rs +++ b/tools/dynwinrt-codegen/tests/snapshot_test.rs @@ -296,6 +296,11 @@ fn snapshot_uri_py_class() { "Snapshot directory not found: {}", snapshot_dir.display() ); + if std::env::var_os("DYNWINRT_UPDATE_PY_SNAPSHOTS").is_some() { + for (filename, actual) in &generated { + fs::write(snapshot_dir.join(filename), actual).expect("write Python snapshot"); + } + } let mut mismatches = Vec::new(); for (filename, actual) in &generated { @@ -325,6 +330,66 @@ fn snapshot_uri_py_class() { } } +/// Snapshot a method-rich Python class outside Windows.Foundation. +#[test] +fn snapshot_data_writer_py_class() { + use dynwinrt_codegen::codegen::python; + + let classes = match meta::parse_class(WINDOWS_WINMD, "Windows.Storage.Streams", "DataWriter") { + Some(class) => vec![class], + None => { + eprintln!("Skipping snapshot test: Windows.winmd not found"); + return; + } + }; + let deps = meta::resolve_dependencies(WINDOWS_WINMD, &classes, &[], &[]); + let mut all_classes = classes; + all_classes.extend(deps.classes); + let interfaces = deps.interfaces; + let enums = deps.enums; + + let mut known_types = HashSet::new(); + known_types.extend(all_classes.iter().map(|class| class.name.clone())); + known_types.extend(interfaces.iter().map(|interface| interface.name.clone())); + known_types.extend(enums.iter().filter_map(|typ| match typ { + TypeMeta::Enum { name, .. } => Some(name.clone()), + _ => None, + })); + let delegate_type_names = interfaces + .iter() + .filter(|interface| { + interface + .methods + .iter() + .any(|method| method.name == ".ctor") + && interface + .methods + .iter() + .any(|method| method.name == "Invoke") + }) + .map(|interface| interface.name.clone()) + .collect::>(); + let class = all_classes + .iter() + .find(|class| class.name == "DataWriter") + .expect("DataWriter class"); + let actual = python::generate_class(class, &known_types, &delegate_type_names, &HashSet::new()); + + let snapshot_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots/data_writer_py"); + let snapshot_path = snapshot_dir.join("data_writer.py"); + if std::env::var_os("DYNWINRT_UPDATE_PY_SNAPSHOTS").is_some() { + fs::create_dir_all(&snapshot_dir).expect("create DataWriter snapshot directory"); + fs::write(&snapshot_path, &actual).expect("write DataWriter Python snapshot"); + } + let expected = fs::read_to_string(&snapshot_path).unwrap_or_else(|error| { + panic!( + "Failed to read snapshot {}: {error}. Set DYNWINRT_UPDATE_PY_SNAPSHOTS=1 to create it.", + snapshot_path.display() + ) + }); + assert_eq!(actual, expected, "DataWriter Python snapshot changed"); +} + /// Verify generated TypeScript for async (and async-with-progress) methods /// includes AbortSignal scaffolding: the `signal?: AbortSignal` parameter, /// `_op.cancel()` on abort, and `signal.reason` rethrow. 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 new file mode 100644 index 00000000..19574457 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/data_writer_py/data_writer.py @@ -0,0 +1,325 @@ +# Generated by dynwinrt-codegen — do not edit +from __future__ import annotations +from builtins import property as _property +from contextvars import copy_context as _copy_context +from functools import lru_cache +from importlib import import_module +from collections.abc import ( + Callable, Iterable, Iterator, Mapping, MutableMapping, MutableSequence, Sequence, +) +from datetime import datetime, timedelta +from typing import TYPE_CHECKING +from uuid import UUID +from weakref import ref as _weakref_ref +from dynwinrt import ( + DynWinRTType, DynWinRTMethodSig, DynWinRTValue, DynWinRTArray, + DynWinRTStruct, DynWinRtDelegate, DynWinRTOverrideInterface, WinGUID, +) +from dynwinrt.dynwinrt import ( + _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_datetime_to_ticks, _dynwinrt_guid, + _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, _dynwinrt_ticks_to_timedelta, + _dynwinrt_timedelta_to_ticks, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, + _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, +) + + +@lru_cache(maxsize=None) +def _dynwinrt_symbol(module, name): + return getattr(import_module(f'.{module}', __package__), name) + + +def _dynwinrt_wrap_values(module, name, values): + wrapper = _dynwinrt_symbol(module, name) + wrap = getattr(wrapper, '_from_native', wrapper) + return [None if value.is_null() else wrap(value) for value in values] + + +def _dynwinrt_enum(module, name, value): + enum_type = _dynwinrt_symbol(module, name) + try: + return enum_type(value) + except ValueError: + return value + + +def _dynwinrt_create_delegate(iid, parameter_types, callback): + context = _copy_context() + def invoke(*args): + return context.copy().run(callback, *args) + return DynWinRtDelegate.create(iid, parameter_types, invoke) + +def _dynwinrt_delegate(value, iid, parameter_types): + raw = getattr(value, '_obj', value) + if isinstance(raw, DynWinRTValue): + return raw + if not callable(value): + raise TypeError('delegate value must be callable or a DynWinRTValue') + return _dynwinrt_create_delegate(iid, parameter_types, value).to_value() + +from dynwinrt import WinRTAsync, WinRTAsyncWithProgress +from dynwinrt.dynwinrt import _DynWinRTAsync, _DynWinRTAsyncWithProgress + +if TYPE_CHECKING: + from .byte_order import ByteOrder # noqa: F401 + from .i_buffer import IID_IBuffer, IBuffer # noqa: F401 + from .i_output_stream import IID_IOutputStream, IOutputStream # noqa: F401 + from .unicode_encoding import UnicodeEncoding # noqa: F401 + +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') + +_IDataWriter = DynWinRTType.register_interface( + "IDataWriter", IID_IDataWriter) \ + .add_method("get_UnstoredBufferLength", DynWinRTMethodSig().add_out(DynWinRTType.u32_type())) \ + .add_method("get_UnicodeEncoding", DynWinRTMethodSig().add_out(DynWinRTType.enum_type('Windows.Storage.Streams.UnicodeEncoding', ['Utf8', 'Utf16LE', 'Utf16BE'], [0, 1, 2]))) \ + .add_method("put_UnicodeEncoding", DynWinRTMethodSig().add_in(DynWinRTType.enum_type('Windows.Storage.Streams.UnicodeEncoding', ['Utf8', 'Utf16LE', 'Utf16BE'], [0, 1, 2]))) \ + .add_method("get_ByteOrder", DynWinRTMethodSig().add_out(DynWinRTType.enum_type('Windows.Storage.Streams.ByteOrder', ['LittleEndian', 'BigEndian'], [0, 1]))) \ + .add_method("put_ByteOrder", DynWinRTMethodSig().add_in(DynWinRTType.enum_type('Windows.Storage.Streams.ByteOrder', ['LittleEndian', 'BigEndian'], [0, 1]))) \ + .add_method("WriteByte", DynWinRTMethodSig().add_in(DynWinRTType.u8_type())) \ + .add_method("WriteBytes", DynWinRTMethodSig().add_in(DynWinRTType.array_type(DynWinRTType.u8_type()))) \ + .add_method("WriteBuffer", DynWinRTMethodSig().add_in(DynWinRTType.interface(WinGUID.parse('905a0fe0-bc53-11df-8c49-001e4fc686da')))) \ + .add_method("WriteBufferRange", DynWinRTMethodSig().add_in(DynWinRTType.interface(WinGUID.parse('905a0fe0-bc53-11df-8c49-001e4fc686da'))).add_in(DynWinRTType.u32_type()).add_in(DynWinRTType.u32_type())) \ + .add_method("WriteBoolean", DynWinRTMethodSig().add_in(DynWinRTType.bool_type())) \ + .add_method("WriteGuid", DynWinRTMethodSig().add_in(DynWinRTType.guid_type())) \ + .add_method("WriteInt16", DynWinRTMethodSig().add_in(DynWinRTType.i16_type())) \ + .add_method("WriteInt32", DynWinRTMethodSig().add_in(DynWinRTType.i32_type())) \ + .add_method("WriteInt64", DynWinRTMethodSig().add_in(DynWinRTType.i64_type())) \ + .add_method("WriteUInt16", DynWinRTMethodSig().add_in(DynWinRTType.u16_type())) \ + .add_method("WriteUInt32", DynWinRTMethodSig().add_in(DynWinRTType.u32_type())) \ + .add_method("WriteUInt64", DynWinRTMethodSig().add_in(DynWinRTType.u64_type())) \ + .add_method("WriteSingle", DynWinRTMethodSig().add_in(DynWinRTType.f32_type())) \ + .add_method("WriteDouble", DynWinRTMethodSig().add_in(DynWinRTType.f64_type())) \ + .add_method("WriteDateTime", DynWinRTMethodSig().add_in(DynWinRTType.struct_type('Windows.Foundation.DateTime', [DynWinRTType.i64_type()]))) \ + .add_method("WriteTimeSpan", DynWinRTMethodSig().add_in(DynWinRTType.struct_type('Windows.Foundation.TimeSpan', [DynWinRTType.i64_type()]))) \ + .add_method("WriteString", DynWinRTMethodSig().add_in(DynWinRTType.hstring()).add_out(DynWinRTType.u32_type())) \ + .add_method("MeasureString", DynWinRTMethodSig().add_in(DynWinRTType.hstring()).add_out(DynWinRTType.u32_type())) \ + .add_method("StoreAsync", DynWinRTMethodSig().add_out(DynWinRTType.i_async_operation(DynWinRTType.u32_type()))) \ + .add_method("FlushAsync", DynWinRTMethodSig().add_out(DynWinRTType.i_async_operation(DynWinRTType.bool_type()))) \ + .add_method("DetachBuffer", DynWinRTMethodSig().add_out(DynWinRTType.interface(WinGUID.parse('905a0fe0-bc53-11df-8c49-001e4fc686da')))) \ + .add_method("DetachStream", DynWinRTMethodSig().add_out(DynWinRTType.interface(WinGUID.parse('905a0fe6-bc53-11df-8c49-001e4fc686da')))) + +_IDataWriterFactory = DynWinRTType.register_interface( + "IDataWriterFactory", IID_IDataWriterFactory) \ + .add_method("CreateDataWriter", DynWinRTMethodSig().add_in(DynWinRTType.interface(WinGUID.parse('905a0fe6-bc53-11df-8c49-001e4fc686da'))).add_out(DynWinRTType.runtime_class('Windows.Storage.Streams.DataWriter', DynWinRTType.interface(WinGUID.parse('64b89265-d341-4922-b38a-dd4af8808c4e'))))) + +_IClosable = DynWinRTType.register_interface( + "IClosable", IID_IClosable) \ + .add_method("Close", DynWinRTMethodSig()) + +_IActivationFactory = DynWinRTType.register_interface( + 'IActivationFactory', WinGUID.parse('00000035-0000-0000-c000-000000000046')) \ + .add_method('ActivateInstance', DynWinRTMethodSig().add_out(DynWinRTType.object())) + + +def unpack_date_time(v: DynWinRTValue) -> datetime: + return _dynwinrt_ticks_to_datetime(v.as_struct().get_i64(0)) +_unpack_date_time = unpack_date_time +DateTime_TYPE = DynWinRTType.struct_type('Windows.Foundation.DateTime', [DynWinRTType.i64_type()]) +_DateTime_TYPE = DateTime_TYPE + +def pack_date_time(v: datetime) -> DynWinRTStruct: + s = DynWinRTStruct.create(DateTime_TYPE) + s.set_i64(0, _dynwinrt_datetime_to_ticks(v)) + return s +_pack_date_time = pack_date_time + + +def unpack_time_span(v: DynWinRTValue) -> timedelta: + return _dynwinrt_ticks_to_timedelta(v.as_struct().get_i64(0)) +_unpack_time_span = unpack_time_span +TimeSpan_TYPE = DynWinRTType.struct_type('Windows.Foundation.TimeSpan', [DynWinRTType.i64_type()]) +_TimeSpan_TYPE = TimeSpan_TYPE + +def pack_time_span(v: timedelta) -> DynWinRTStruct: + s = DynWinRTStruct.create(TimeSpan_TYPE) + s.set_i64(0, _dynwinrt_timedelta_to_ticks(v)) + return s +_pack_time_span = pack_time_span + + +class DataWriter: + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native(cls, args[0], '_set_native') + if cls is DataWriter: + _bound = _dynwinrt_bind_overload((), args, kwargs) + if _bound is not None: + return cls.create_default() + _bound = _dynwinrt_bind_overload(('output_stream',), args, kwargs) + if _bound is not None and isinstance(_bound[0], _dynwinrt_symbol('i_output_stream', 'IOutputStream')): + return cls.create_data_writer(_bound[0]) + return super().__new__(cls) + + def _set_native(self, obj: DynWinRTValue): + self._obj = obj.cast(IID_IDataWriter) + self._closed = False + self._dynwinrt_native_ready = True + _dynwinrt_track_projected(self, 'Windows.Storage.Streams.DataWriter') + _dynwinrt_cache_projected(self) + + @classmethod + def _from_native(cls, obj: DynWinRTValue): + return cls(obj) + + def __init__(self, *args, **kwargs): + if getattr(self, '_dynwinrt_native_ready', False): + return + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + self._set_native(args[0]) + return + _bound = _dynwinrt_bind_overload((), args, kwargs) + if _bound is not None: + self._set_native(type(self).create_default()._obj) + return + _bound = _dynwinrt_bind_overload(('output_stream',), args, kwargs) + if _bound is not None and isinstance(_bound[0], _dynwinrt_symbol('i_output_stream', 'IOutputStream')): + self._set_native(type(self).create_data_writer(_bound[0])._obj) + return + raise TypeError("No matching constructor for DataWriter") + + @staticmethod + def _get_f_IDataWriterFactory(): + return DynWinRTValue.activation_factory('Windows.Storage.Streams.DataWriter').cast(IID_IDataWriterFactory) + + @staticmethod + def create_default() -> 'DataWriter': + return DataWriter._from_native(_IActivationFactory.method(6).invoke(DynWinRTValue.activation_factory('Windows.Storage.Streams.DataWriter'), [])) + + + @staticmethod + def create_data_writer(output_stream: 'IOutputStream') -> 'DataWriter': + return DataWriter._from_native(_IDataWriterFactory.method(6).invoke(DataWriter._get_f_IDataWriterFactory(), [getattr(output_stream, '_obj', output_stream)])) + + @_property + def unstored_buffer_length(self) -> int: + return _IDataWriter.method(6).invoke(self._obj, []).to_u32() + + @_property + def unicode_encoding(self) -> 'UnicodeEncoding': + return _dynwinrt_enum('unicode_encoding', 'UnicodeEncoding', _IDataWriter.method(7).invoke(self._obj, []).to_number()) + + @_property + def byte_order(self) -> 'ByteOrder': + return _dynwinrt_enum('byte_order', 'ByteOrder', _IDataWriter.method(9).invoke(self._obj, []).to_number()) + + def write_byte(self, value: int) -> None: + _IDataWriter.method(11).invoke(self._obj, [DynWinRTValue.from_u8(value)]) + + 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: + _IDataWriter.method(13).invoke(self._obj, [getattr(buffer, '_obj', buffer)]) + + def write_buffer_range(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_boolean(self, value: bool) -> None: + _IDataWriter.method(15).invoke(self._obj, [DynWinRTValue.from_bool(value)]) + + def write_guid(self, value: UUID) -> None: + _IDataWriter.method(16).invoke(self._obj, [DynWinRTValue.from_guid(_dynwinrt_guid(value))]) + + def write_int16(self, value: int) -> None: + _IDataWriter.method(17).invoke(self._obj, [DynWinRTValue.from_i16(value)]) + + def write_int32(self, value: int) -> None: + _IDataWriter.method(18).invoke(self._obj, [DynWinRTValue.from_i32(value)]) + + def write_int64(self, value: int) -> None: + _IDataWriter.method(19).invoke(self._obj, [DynWinRTValue.from_i64(value)]) + + def write_uint16(self, value: int) -> None: + _IDataWriter.method(20).invoke(self._obj, [DynWinRTValue.from_u16(value)]) + + def write_uint32(self, value: int) -> None: + _IDataWriter.method(21).invoke(self._obj, [DynWinRTValue.from_u32(value)]) + + def write_uint64(self, value: int) -> None: + _IDataWriter.method(22).invoke(self._obj, [DynWinRTValue.from_u64(value)]) + + def write_single(self, value: float) -> None: + _IDataWriter.method(23).invoke(self._obj, [DynWinRTValue.from_f32(value)]) + + def write_double(self, value: float) -> None: + _IDataWriter.method(24).invoke(self._obj, [DynWinRTValue.from_f64(value)]) + + def write_date_time(self, value: datetime) -> None: + _IDataWriter.method(25).invoke(self._obj, [_pack_date_time(value).to_value()]) + + def write_time_span(self, value: timedelta) -> None: + _IDataWriter.method(26).invoke(self._obj, [_pack_time_span(value).to_value()]) + + def write_string(self, value: str) -> int: + return _IDataWriter.method(27).invoke(self._obj, [DynWinRTValue.from_hstring(value)]).to_u32() + + def measure_string(self, value: str) -> int: + return _IDataWriter.method(28).invoke(self._obj, [DynWinRTValue.from_hstring(value)]).to_u32() + + def store_async(self) -> WinRTAsync[int]: + return _dynwinrt_track_projected(_DynWinRTAsync(_IDataWriter.method(29).invoke(self._obj, []), lambda value: value.to_u32()), 'WinRTAsync') + + def flush_async(self) -> WinRTAsync[bool]: + return _dynwinrt_track_projected(_DynWinRTAsync(_IDataWriter.method(30).invoke(self._obj, []), lambda value: value.to_bool()), 'WinRTAsync') + + def detach_buffer(self) -> IBuffer | None: + return (lambda value: None if value.is_null() else _dynwinrt_symbol('i_buffer', 'IBuffer')(value))(_IDataWriter.method(31).invoke(self._obj, [])) + + def detach_stream(self) -> IOutputStream | None: + return (lambda value: None if value.is_null() else _dynwinrt_symbol('i_output_stream', 'IOutputStream')(value))(_IDataWriter.method(32).invoke(self._obj, [])) + + @unicode_encoding.setter + def unicode_encoding(self, value: 'UnicodeEncoding'): + _IDataWriter.method(8).invoke(self._obj, [DynWinRTValue.enum_value(DynWinRTType.enum_type('Windows.Storage.Streams.UnicodeEncoding', ['Utf8', 'Utf16LE', 'Utf16BE'], [0, 1, 2]), int(value))]) + + @byte_order.setter + 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))]) + + def close(self): + if self._closed: + return + _dynwinrt_symbol('i_closable', 'IClosable').from_value(self._obj).close() + self._closed = True + + def __enter__(self): + if self._closed: + raise RuntimeError('cannot enter a closed WinRT object') + return self + + def __exit__(self, _exc_type, _exc_value, _traceback): + self.close() + return False + + def as_interface(self, interface_class): + return interface_class.from_value(self._obj) + + +class IClosable: + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native(cls, args[0], '_set_native') + return super().__new__(cls) + + def _set_native(self, obj: DynWinRTValue): + self._obj = obj.cast(IID_IClosable) + self._dynwinrt_native_ready = True + _dynwinrt_track_projected(self, 'Windows.Foundation.IClosable') + _dynwinrt_cache_projected(self) + + def __init__(self, obj: DynWinRTValue): + if getattr(self, '_dynwinrt_native_ready', False): + return + IClosable._set_native(self, obj) + + @classmethod + def _from_native(cls, obj: DynWinRTValue) -> 'IClosable': + return cls(obj) + + @staticmethod + def from_value(obj: DynWinRTValue) -> 'IClosable': + return IClosable._from_native(obj.cast(IID_IClosable)) + + def close(self) -> None: + _IClosable.method(6).invoke(self._obj, []) diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_iterator_i_www_form_url_decoder_entry.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_iterator_i_www_form_url_decoder_entry.py index fe72e1fa..fb643560 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_iterator_i_www_form_url_decoder_entry.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_iterator_i_www_form_url_decoder_entry.py @@ -18,7 +18,8 @@ from dynwinrt.dynwinrt import ( _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_datetime_to_ticks, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, _dynwinrt_ticks_to_timedelta, - _dynwinrt_timedelta_to_ticks, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, + _dynwinrt_timedelta_to_ticks, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, + _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, ) @@ -71,13 +72,29 @@ def _dynwinrt_delegate(value, iid, parameter_types): class IIterator_IWwwFormUrlDecoderEntry(_WinRTIteratorMixin): - def __init__(self, obj: DynWinRTValue): + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native(cls, args[0], '_set_native') + return super().__new__(cls) + + def _set_native(self, obj: DynWinRTValue): self._obj = obj.cast(IID_IIterator_IWwwFormUrlDecoderEntry) + self._dynwinrt_native_ready = True _dynwinrt_track_projected(self, 'Windows.Foundation.Collections.IIterator_IWwwFormUrlDecoderEntry') + _dynwinrt_cache_projected(self) + + def __init__(self, obj: DynWinRTValue): + if getattr(self, '_dynwinrt_native_ready', False): + return + IIterator_IWwwFormUrlDecoderEntry._set_native(self, obj) + + @classmethod + def _from_native(cls, obj: DynWinRTValue) -> 'IIterator_IWwwFormUrlDecoderEntry': + return cls(obj) @staticmethod def from_value(obj: DynWinRTValue) -> 'IIterator_IWwwFormUrlDecoderEntry': - return IIterator_IWwwFormUrlDecoderEntry(obj.cast(IID_IIterator_IWwwFormUrlDecoderEntry)) + return IIterator_IWwwFormUrlDecoderEntry._from_native(obj.cast(IID_IIterator_IWwwFormUrlDecoderEntry)) @_property diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_stringable.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_stringable.py index beee4fdb..abde8bf7 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_stringable.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_stringable.py @@ -18,7 +18,8 @@ from dynwinrt.dynwinrt import ( _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_datetime_to_ticks, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, _dynwinrt_ticks_to_timedelta, - _dynwinrt_timedelta_to_ticks, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, + _dynwinrt_timedelta_to_ticks, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, + _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, ) @@ -64,13 +65,29 @@ def _dynwinrt_delegate(value, iid, parameter_types): class IStringable: - def __init__(self, obj: DynWinRTValue): + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native(cls, args[0], '_set_native') + return super().__new__(cls) + + def _set_native(self, obj: DynWinRTValue): self._obj = obj + self._dynwinrt_native_ready = True _dynwinrt_track_projected(self, 'Windows.Foundation.IStringable') + _dynwinrt_cache_projected(self) + + def __init__(self, obj: DynWinRTValue): + if getattr(self, '_dynwinrt_native_ready', False): + return + IStringable._set_native(self, obj) + + @classmethod + def _from_native(cls, obj: DynWinRTValue) -> 'IStringable': + return cls(obj) @staticmethod def from_value(obj: DynWinRTValue) -> 'IStringable': - return IStringable(obj.cast(IID_IStringable)) + return IStringable._from_native(obj.cast(IID_IStringable)) def to_string(self) -> str: diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_uri_runtime_class_with_absolute_canonical_uri.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_uri_runtime_class_with_absolute_canonical_uri.py index 1cee8dbc..2e7358bb 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_uri_runtime_class_with_absolute_canonical_uri.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_uri_runtime_class_with_absolute_canonical_uri.py @@ -18,7 +18,8 @@ from dynwinrt.dynwinrt import ( _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_datetime_to_ticks, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, _dynwinrt_ticks_to_timedelta, - _dynwinrt_timedelta_to_ticks, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, + _dynwinrt_timedelta_to_ticks, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, + _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, ) @@ -65,13 +66,29 @@ def _dynwinrt_delegate(value, iid, parameter_types): class IUriRuntimeClassWithAbsoluteCanonicalUri: - def __init__(self, obj: DynWinRTValue): + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native(cls, args[0], '_set_native') + return super().__new__(cls) + + def _set_native(self, obj: DynWinRTValue): self._obj = obj + self._dynwinrt_native_ready = True _dynwinrt_track_projected(self, 'Windows.Foundation.IUriRuntimeClassWithAbsoluteCanonicalUri') + _dynwinrt_cache_projected(self) + + def __init__(self, obj: DynWinRTValue): + if getattr(self, '_dynwinrt_native_ready', False): + return + IUriRuntimeClassWithAbsoluteCanonicalUri._set_native(self, obj) + + @classmethod + def _from_native(cls, obj: DynWinRTValue) -> 'IUriRuntimeClassWithAbsoluteCanonicalUri': + return cls(obj) @staticmethod def from_value(obj: DynWinRTValue) -> 'IUriRuntimeClassWithAbsoluteCanonicalUri': - return IUriRuntimeClassWithAbsoluteCanonicalUri(obj.cast(IID_IUriRuntimeClassWithAbsoluteCanonicalUri)) + return IUriRuntimeClassWithAbsoluteCanonicalUri._from_native(obj.cast(IID_IUriRuntimeClassWithAbsoluteCanonicalUri)) @_property diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_www_form_url_decoder_entry.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_www_form_url_decoder_entry.py index 28e8306b..e805b391 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_www_form_url_decoder_entry.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_www_form_url_decoder_entry.py @@ -18,7 +18,8 @@ from dynwinrt.dynwinrt import ( _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_datetime_to_ticks, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, _dynwinrt_ticks_to_timedelta, - _dynwinrt_timedelta_to_ticks, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, + _dynwinrt_timedelta_to_ticks, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, + _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, ) @@ -65,13 +66,29 @@ def _dynwinrt_delegate(value, iid, parameter_types): class IWwwFormUrlDecoderEntry: - def __init__(self, obj: DynWinRTValue): + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native(cls, args[0], '_set_native') + return super().__new__(cls) + + def _set_native(self, obj: DynWinRTValue): self._obj = obj + self._dynwinrt_native_ready = True _dynwinrt_track_projected(self, 'Windows.Foundation.IWwwFormUrlDecoderEntry') + _dynwinrt_cache_projected(self) + + def __init__(self, obj: DynWinRTValue): + if getattr(self, '_dynwinrt_native_ready', False): + return + IWwwFormUrlDecoderEntry._set_native(self, obj) + + @classmethod + def _from_native(cls, obj: DynWinRTValue) -> 'IWwwFormUrlDecoderEntry': + return cls(obj) @staticmethod def from_value(obj: DynWinRTValue) -> 'IWwwFormUrlDecoderEntry': - return IWwwFormUrlDecoderEntry(obj.cast(IID_IWwwFormUrlDecoderEntry)) + return IWwwFormUrlDecoderEntry._from_native(obj.cast(IID_IWwwFormUrlDecoderEntry)) @_property diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/uri.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/uri.py index d90b0620..197fc071 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/uri.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/uri.py @@ -18,7 +18,8 @@ from dynwinrt.dynwinrt import ( _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_datetime_to_ticks, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, _dynwinrt_ticks_to_timedelta, - _dynwinrt_timedelta_to_ticks, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, + _dynwinrt_timedelta_to_ticks, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, + _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, ) @@ -107,17 +108,31 @@ def _dynwinrt_delegate(value, iid, parameter_types): class Uri: + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native(cls, args[0], '_set_native') + if cls is Uri: + _bound = _dynwinrt_bind_overload(('uri',), args, kwargs) + if _bound is not None and isinstance(_bound[0], str): + return cls.create_uri(_bound[0]) + _bound = _dynwinrt_bind_overload(('base_uri', 'relative_uri',), args, kwargs) + if _bound is not None and isinstance(_bound[0], str) and isinstance(_bound[1], str): + return cls.create_with_relative_uri(_bound[0], _bound[1]) + return super().__new__(cls) + def _set_native(self, obj: DynWinRTValue): self._obj = obj.cast(IID_IUriRuntimeClass) + self._dynwinrt_native_ready = True _dynwinrt_track_projected(self, 'Windows.Foundation.Uri') + _dynwinrt_cache_projected(self) @classmethod def _from_native(cls, obj: DynWinRTValue): - instance = cls.__new__(cls) - instance._set_native(obj) - return instance + return cls(obj) def __init__(self, *args, **kwargs): + if getattr(self, '_dynwinrt_native_ready', False): + return if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): self._set_native(args[0]) return @@ -244,13 +259,29 @@ def as_interface(self, interface_class): class IUriRuntimeClassWithAbsoluteCanonicalUri: - def __init__(self, obj: DynWinRTValue): + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native(cls, args[0], '_set_native') + return super().__new__(cls) + + def _set_native(self, obj: DynWinRTValue): self._obj = obj.cast(IID_IUriRuntimeClassWithAbsoluteCanonicalUri) + self._dynwinrt_native_ready = True _dynwinrt_track_projected(self, 'Windows.Foundation.IUriRuntimeClassWithAbsoluteCanonicalUri') + _dynwinrt_cache_projected(self) + + def __init__(self, obj: DynWinRTValue): + if getattr(self, '_dynwinrt_native_ready', False): + return + IUriRuntimeClassWithAbsoluteCanonicalUri._set_native(self, obj) + + @classmethod + def _from_native(cls, obj: DynWinRTValue) -> 'IUriRuntimeClassWithAbsoluteCanonicalUri': + return cls(obj) @staticmethod def from_value(obj: DynWinRTValue) -> 'IUriRuntimeClassWithAbsoluteCanonicalUri': - return IUriRuntimeClassWithAbsoluteCanonicalUri(obj.cast(IID_IUriRuntimeClassWithAbsoluteCanonicalUri)) + return IUriRuntimeClassWithAbsoluteCanonicalUri._from_native(obj.cast(IID_IUriRuntimeClassWithAbsoluteCanonicalUri)) @_property def absolute_canonical_uri(self) -> str: @@ -262,13 +293,29 @@ def display_iri(self) -> str: class IStringable: - def __init__(self, obj: DynWinRTValue): + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native(cls, args[0], '_set_native') + return super().__new__(cls) + + def _set_native(self, obj: DynWinRTValue): self._obj = obj.cast(IID_IStringable) + self._dynwinrt_native_ready = True _dynwinrt_track_projected(self, 'Windows.Foundation.IStringable') + _dynwinrt_cache_projected(self) + + def __init__(self, obj: DynWinRTValue): + if getattr(self, '_dynwinrt_native_ready', False): + return + IStringable._set_native(self, obj) + + @classmethod + def _from_native(cls, obj: DynWinRTValue) -> 'IStringable': + return cls(obj) @staticmethod def from_value(obj: DynWinRTValue) -> 'IStringable': - return IStringable(obj.cast(IID_IStringable)) + return IStringable._from_native(obj.cast(IID_IStringable)) def to_string(self) -> str: return _IStringable.method(6).invoke(self._obj, []).to_string() 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 69c49115..4435d5fe 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 @@ -18,7 +18,8 @@ from dynwinrt.dynwinrt import ( _dynwinrt_array, _dynwinrt_bind_overload, _dynwinrt_datetime_to_ticks, _dynwinrt_guid, _dynwinrt_map, _dynwinrt_new_vector, _dynwinrt_ticks_to_datetime, _dynwinrt_ticks_to_timedelta, - _dynwinrt_timedelta_to_ticks, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, + _dynwinrt_timedelta_to_ticks, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, + _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, ) @@ -87,18 +88,29 @@ def _dynwinrt_delegate(value, iid, parameter_types): class WwwFormUrlDecoder(_WinRTSequenceMixin): + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native(cls, args[0], '_set_native') + if cls is WwwFormUrlDecoder: + _bound = _dynwinrt_bind_overload(('query',), args, kwargs) + if _bound is not None and isinstance(_bound[0], str): + return cls.create_www_form_url_decoder(_bound[0]) + return super().__new__(cls) + def _set_native(self, obj: DynWinRTValue): self._obj = obj.cast(IID_IWwwFormUrlDecoderRuntimeClass) self._collection_obj = obj.cast(IID_IVectorView_IWwwFormUrlDecoderEntry) + self._dynwinrt_native_ready = True _dynwinrt_track_projected(self, 'Windows.Foundation.WwwFormUrlDecoder') + _dynwinrt_cache_projected(self) @classmethod def _from_native(cls, obj: DynWinRTValue): - instance = cls.__new__(cls) - instance._set_native(obj) - return instance + return cls(obj) def __init__(self, *args, **kwargs): + if getattr(self, '_dynwinrt_native_ready', False): + return if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): self._set_native(args[0]) return @@ -143,13 +155,29 @@ def as_interface(self, interface_class): class IVectorView_IWwwFormUrlDecoderEntry(_WinRTSequenceMixin): - def __init__(self, obj: DynWinRTValue): + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native(cls, args[0], '_set_native') + return super().__new__(cls) + + def _set_native(self, obj: DynWinRTValue): self._obj = obj.cast(IID_IVectorView_IWwwFormUrlDecoderEntry) + self._dynwinrt_native_ready = True _dynwinrt_track_projected(self, 'Windows.Foundation.Collections.IVectorView_IWwwFormUrlDecoderEntry') + _dynwinrt_cache_projected(self) + + def __init__(self, obj: DynWinRTValue): + if getattr(self, '_dynwinrt_native_ready', False): + return + IVectorView_IWwwFormUrlDecoderEntry._set_native(self, obj) + + @classmethod + def _from_native(cls, obj: DynWinRTValue) -> 'IVectorView_IWwwFormUrlDecoderEntry': + return cls(obj) @staticmethod def from_value(obj: DynWinRTValue) -> 'IVectorView_IWwwFormUrlDecoderEntry': - return IVectorView_IWwwFormUrlDecoderEntry(obj.cast(IID_IVectorView_IWwwFormUrlDecoderEntry)) + return IVectorView_IWwwFormUrlDecoderEntry._from_native(obj.cast(IID_IVectorView_IWwwFormUrlDecoderEntry)) @_property def size(self) -> int: @@ -168,13 +196,29 @@ def get_many(self, start_index: int, items: DynWinRTArray | Sequence['IWwwFormUr class IIterable_IWwwFormUrlDecoderEntry(_WinRTIterableMixin): - def __init__(self, obj: DynWinRTValue): + def __new__(cls, *args, **kwargs): + if len(args) == 1 and not kwargs and isinstance(args[0], DynWinRTValue): + return _dynwinrt_projected_from_native(cls, args[0], '_set_native') + return super().__new__(cls) + + def _set_native(self, obj: DynWinRTValue): self._obj = obj.cast(IID_IIterable_IWwwFormUrlDecoderEntry) + self._dynwinrt_native_ready = True _dynwinrt_track_projected(self, 'Windows.Foundation.Collections.IIterable_IWwwFormUrlDecoderEntry') + _dynwinrt_cache_projected(self) + + def __init__(self, obj: DynWinRTValue): + if getattr(self, '_dynwinrt_native_ready', False): + return + IIterable_IWwwFormUrlDecoderEntry._set_native(self, obj) + + @classmethod + def _from_native(cls, obj: DynWinRTValue) -> 'IIterable_IWwwFormUrlDecoderEntry': + return cls(obj) @staticmethod def from_value(obj: DynWinRTValue) -> 'IIterable_IWwwFormUrlDecoderEntry': - return IIterable_IWwwFormUrlDecoderEntry(obj.cast(IID_IIterable_IWwwFormUrlDecoderEntry)) + return IIterable_IWwwFormUrlDecoderEntry._from_native(obj.cast(IID_IIterable_IWwwFormUrlDecoderEntry)) def first(self) -> Iterator[IWwwFormUrlDecoderEntry | None] | None: return (lambda value: None if value.is_null() else _dynwinrt_symbol('i_iterator_i_www_form_url_decoder_entry', 'IIterator_IWwwFormUrlDecoderEntry')(value))(_IIterable_IWwwFormUrlDecoderEntry.method(6).invoke(self._obj, []))