diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6ce079b2..c4f1038a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -548,7 +548,7 @@ jobs: run: | $env:DYNWINRT_TEST_PYTHON = (Resolve-Path .\bindings\py\.venv\Scripts\python.exe).Path $env:DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME = '1' - cargo test -p dynwinrt-codegen --test implementation_naming_test + cargo test -p dynwinrt-codegen --test implementation_naming_test --test python_released_implementation_test - name: Run E2E tests run: .\tests\e2e\e2e_test.ps1 -SkipBuild -Codegen $env:DYNWINRT_CODEGEN # This optional-SDK behavioral smoke is separate from generated coverage diff --git a/README.md b/README.md index b54a8abb..95ddfb93 100644 --- a/README.md +++ b/README.md @@ -124,7 +124,7 @@ Save this as `example.py`: from dynwinrt import RoApartment, projected_lifetime_scope from generated_uri.windows.foundation import Uri -with RoApartment(1), projected_lifetime_scope(): +with RoApartment(), projected_lifetime_scope(): uri = Uri("https://example.com/path?q=1") print(uri.host) # "example.com" ``` @@ -133,8 +133,9 @@ with RoApartment(1), projected_lifetime_scope(): python .\example.py ``` -`RoApartment(1)` initializes WinRT on the current thread. The lifetime scope -releases generated wrappers before the apartment closes. +`RoApartment()` initializes WinRT on the current thread (multithreaded by +default). The lifetime scope releases generated wrappers before the apartment +closes. ## Examples diff --git a/bindings/py/README.md b/bindings/py/README.md index 2a1cb72c..dd432d19 100644 --- a/bindings/py/README.md +++ b/bindings/py/README.md @@ -231,8 +231,9 @@ inspectable, and other types) and native getter failures raise an exception. Use `wrapper.as_interface(InterfaceClass)` when converting an existing wrapper to an interface view. Use `InterfaceClass.from_value(raw)` for a raw -`DynWinRTValue`. Do not call the internal `_from_native()` method from -application code. +`DynWinRTValue`. `as_interface()` accepts generated interface classes only; +passing a runtime class raises `TypeError` that points to `project_as()`. Do not +call the internal `_from_native()` method from application code. ## COM apartments and cleanup @@ -240,16 +241,23 @@ Use `RoApartment` to initialize COM for a thread and balance every successful initialization: ```python -with RoApartment(0): # RO_INIT_SINGLETHREADED +from dynwinrt import RO_INIT_SINGLETHREADED, RoApartment + +with RoApartment(RO_INIT_SINGLETHREADED): use_winrt() ``` -Use `RoApartment(1)` for `RO_INIT_MULTITHREADED`. Nested contexts using the same +`RoApartment()` uses `RO_INIT_MULTITHREADED`, the same as +`RoApartment(RO_INIT_MULTITHREADED)`. Nested contexts using the same model are supported. Requesting a conflicting model raises `OSError` with `RPC_E_CHANGED_MODE`. The low-level `ro_initialize()` API remains available, but each successful call, including `S_FALSE`, must be paired with one `ro_uninitialize()` call on the same thread. +WinRT is never initialized implicitly. A call on a thread without an apartment +raises `OSError` with `CO_E_NOTINITIALIZED` in `error.winerror`; its message +explains how to open one. + Generated runtime classes that implement `IClosable` support `with` and an idempotent `close()` method. Prefer deterministic cleanup instead of relying on Python garbage collection. @@ -473,19 +481,26 @@ Use a projection lifetime scope inside the COM apartment so wrappers release their native values before `RoUninitialize`: ```python -from dynwinrt import RoApartment, projected_lifetime_scope +from dynwinrt import RO_INIT_SINGLETHREADED, RoApartment, projected_lifetime_scope -with RoApartment(0), projected_lifetime_scope(): +with RoApartment(RO_INIT_SINGLETHREADED), projected_lifetime_scope(): app = Application.create() # Create and use WinUI objects here. ``` Scopes nest in LIFO order. Wrappers that survive a closed scope remain Python -objects, but their native values are released and further WinRT calls fail. -Each scope is thread-affine: enter, use, and close it inside that thread's -`RoApartment`. Same-thread asyncio tasks inherit the active scope, while worker -threads must open their own ordered -`with RoApartment(...), projected_lifetime_scope():`. Native callbacks invoked +objects, but their native values are released: using one afterwards, as the +object of a call, as an argument, or inside a sequence, mapping, array, or +struct input, raises `RuntimeError` explaining that it was released, as it +does after `release_projected(wrapper)` or `DynWinRTValue.release()`. +Returning one from an interface implementation handler fails the native call +like any other handler error. `DynWinRTValue.is_released()` tells a released +value apart from a WinRT null reference: both report `is_null()`, but only the +null can still be passed. Each scope is thread-affine: enter, use, and close it +inside that thread's `RoApartment`. Same-thread asyncio tasks inherit the +active scope, while worker threads must open their own ordered +`with RoApartment(...), projected_lifetime_scope():`. +Native callbacks invoked on a foreign thread preserve other captured context but do not inherit the creator thread's lifetime scope. This includes generated delegates, raw progress handlers, and element-factory callbacks. Retained callback values remain diff --git a/bindings/py/dynwinrt.pyi b/bindings/py/dynwinrt.pyi index 9292a1a5..2d77c677 100644 --- a/bindings/py/dynwinrt.pyi +++ b/bindings/py/dynwinrt.pyi @@ -1,5 +1,5 @@ from collections.abc import Coroutine -from typing import Any, Awaitable, Callable, Generic, List, Literal, Mapping, Optional, Protocol, Sequence, TypeVar, Union, final, overload +from typing import Any, Awaitable, Callable, Final, Generic, List, Literal, Mapping, Optional, Protocol, Sequence, TypeVar, Union, final, overload from uuid import UUID _T = TypeVar("_T", covariant=True) @@ -24,6 +24,8 @@ class _DynWinRTRuntimeClass(_DynWinRTProjectableClass): ... __all__ = [ "WinAppSDKContext", "RoApartment", + "RO_INIT_SINGLETHREADED", + "RO_INIT_MULTITHREADED", "WinGUID", "DynWinRTType", "DynWinRTMethodSig", @@ -64,6 +66,12 @@ class WinAppSDKContext: def resource_pri_path(self) -> str: ... +# apartment_type values for RoApartment(...) and ro_initialize(...); +# RoApartment() uses RO_INIT_MULTITHREADED. +RO_INIT_SINGLETHREADED: Final = 0 +RO_INIT_MULTITHREADED: Final = 1 + + @final class RoApartment: def __new__( @@ -458,6 +466,9 @@ class DynWinRTValue: def to_guid(self) -> WinGUID: ... def to_bytes(self) -> bytes: ... def is_null(self) -> bool: ... + # True after release(), release_projected(), or a closing + # projected_lifetime_scope(); a WinRT null reference is not released. + def is_released(self) -> bool: ... def release(self) -> None: ... def as_raw(self) -> int: ... def identity_raw(self) -> int: ... diff --git a/bindings/py/src/async_runtime.rs b/bindings/py/src/async_runtime.rs index 24a1de57..f8d2ce24 100644 --- a/bindings/py/src/async_runtime.rs +++ b/bindings/py/src/async_runtime.rs @@ -474,7 +474,7 @@ impl ProgressDispatcher { return Ok(()); } - let raw = Py::new(py, DynWinRTValue(value))?; + let raw = Py::new(py, DynWinRTValue::new(value))?; let context = self.callback_context.call_method0(py, "copy")?; let context_run = context.getattr(py, "run")?; self.event_loop.call_method1( @@ -585,7 +585,7 @@ impl AsyncOperation { let raw_future = pyo3_async_runtimes::tokio::future_into_py(py, async move { let result = winrt_future.await; let result = result.map_err(map_dynwinrt_error)?; - Ok(DynWinRTValue(result)) + Ok(DynWinRTValue::new(result)) })?; let converter = self.converter.clone_ref(py); @@ -632,7 +632,7 @@ impl AsyncOperation { *state = ExecutionState::Idle; } - let raw = Py::new(py, DynWinRTValue(result?))?; + let raw = Py::new(py, DynWinRTValue::new(result?))?; self.converter.call1(py, (raw,)) } diff --git a/bindings/py/src/delegate_method.rs b/bindings/py/src/delegate_method.rs index fbfc14bb..f67c107e 100644 --- a/bindings/py/src/delegate_method.rs +++ b/bindings/py/src/delegate_method.rs @@ -5,8 +5,8 @@ use pyo3::exceptions::PyTypeError; use pyo3::prelude::*; use windows::core::{GUID, IInspectable, IUnknown, Interface}; -use crate::errors::{map_dynwinrt_error, map_windows_error}; -use crate::runtime::{DynWinRTMethodSig, DynWinRTValue, WinGUID}; +use crate::errors::map_windows_error; +use crate::runtime::{DynWinRTMethodSig, DynWinRTValue, WinGUID, native_arguments}; type DelegateCall = dyn Fn(&IUnknown, &[dynwinrt::WinRTValue]) -> windows::core::Result>; @@ -52,16 +52,16 @@ impl DynWinRTDelegateMethod { args: Vec, ) -> PyResult> { // Keep native pins, not a Python value borrow, across reentrant Invoke. - let value = value.try_borrow()?.0.clone(); - let delegate = value.cast(&self.iid).map_err(map_dynwinrt_error)?; + let value = value.try_borrow()?.clone(); + let delegate = value.query(&self.iid, "delegate Invoke()")?; let dynwinrt::WinRTValue::Object(object) = &delegate else { return Err(PyTypeError::new_err( "delegate invocation requires a managed WinRT delegate value", )); }; - let args = args.into_iter().map(|arg| arg.0).collect::>(); + let args = native_arguments("delegate Invoke()", args)?; (self.call.0)(object, &args) - .map(|outputs| outputs.into_iter().map(DynWinRTValue).collect()) + .map(|outputs| outputs.into_iter().map(DynWinRTValue::new).collect()) .map_err(map_windows_error) } } diff --git a/bindings/py/src/errors.rs b/bindings/py/src/errors.rs index f80ea216..340ece66 100644 --- a/bindings/py/src/errors.rs +++ b/bindings/py/src/errors.rs @@ -4,10 +4,72 @@ use pyo3::exceptions::asyncio::CancelledError as PyCancelledError; use pyo3::exceptions::{PyIndexError, PyOSError, PyRuntimeError}; use pyo3::prelude::*; +use windows::Win32::Foundation::CO_E_NOTINITIALIZED; +use windows::core::HRESULT; #[cfg(test)] pub(crate) static UNRAISABLE_HOOK_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); +/// Guidance appended to the Windows description of specific HRESULTs. +const HRESULT_HINTS: &[(HRESULT, &str)] = &[( + CO_E_NOTINITIALIZED, + "WinRT is not initialized on this thread; use `with dynwinrt.RoApartment():` \ + (or call `dynwinrt.ro_initialize(dynwinrt.RO_INIT_MULTITHREADED)`) before calling \ + WinRT APIs.", +)]; + +fn hresult_hint(code: HRESULT) -> Option<&'static str> { + HRESULT_HINTS + .iter() + .find_map(|&(hinted, hint)| (hinted == code).then_some(hint)) +} + +const RELEASED_REASON: &str = "has been released (its projected_lifetime_scope() exited, or \ + release_projected() / DynWinRTValue.release() was called) and can no longer be used."; + +/// A call on a value after `release()`, including release by its lifetime scope. +pub(crate) fn released_receiver_error() -> PyErr { + PyRuntimeError::new_err(format!("This WinRT object {RELEASED_REASON}")) +} + +/// Where a value was handed to native code, with a 0-based index. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum InputSlot { + Argument(usize), + Element(usize), + Key(usize), + Value(usize), + Field(usize), + /// A value a Python callback returned to its native caller. + Output(usize), +} + +impl std::fmt::Display for InputSlot { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let (slot, index) = match *self { + Self::Argument(index) => ("argument", index), + Self::Element(index) => ("element", index), + Self::Key(index) => ("key", index), + Self::Value(index) => ("value", index), + Self::Field(index) => ("field", index), + Self::Output(index) => ("output", index), + }; + write!(f, "{slot} {index}") + } +} + +/// A released value passed in `slot` of `operation`. +pub(crate) fn released_input_error(operation: &str, slot: InputSlot) -> PyErr { + PyRuntimeError::new_err(format!( + "This WinRT object ({slot} of {operation}) {RELEASED_REASON}" + )) +} + +/// A live value of `kind` used where `operation` needs a WinRT object. +pub(crate) fn non_object_receiver_error(operation: &str, kind: &str) -> PyErr { + PyRuntimeError::new_err(format!("{operation} requires an Object value, got {kind}")) +} + pub(crate) fn map_windows_error(error: windows::core::Error) -> PyErr { windows_error(error, None) } @@ -17,10 +79,18 @@ pub(crate) fn map_windows_error_with_context(error: windows::core::Error, contex } fn windows_error(error: windows::core::Error, context: Option<&str>) -> PyErr { - let message = match context { + let description = match context { Some(context) => format!("{context}: {}", error.message()), None => error.message(), }; + // A hint explains how to fix the failure; it never changes the error. + let message = match hresult_hint(error.code()) { + Some(hint) => match description.trim_end() { + "" => hint.to_owned(), + text => format!("{text} {hint}"), + }, + None => description, + }; // Match PyWinRT's OSError shape and preserve the signed HRESULT in winerror. PyOSError::new_err((0, message, Option::::None, error.code().0)) } @@ -47,3 +117,52 @@ pub(crate) fn map_dynwinrt_error_with_context(error: dynwinrt::Error, context: & other => PyRuntimeError::new_err(format!("{context}: {}", other.message())), } } + +#[cfg(test)] +mod tests { + use super::*; + use windows::Win32::Foundation::E_POINTER; + + fn os_error_fields(py: Python<'_>, error: PyErr) -> (i32, i32, String) { + let value = error.value(py); + assert!(value.is_instance_of::()); + ( + value.getattr("winerror").unwrap().extract().unwrap(), + value.getattr("errno").unwrap().extract().unwrap(), + value.getattr("strerror").unwrap().extract().unwrap(), + ) + } + + #[test] + fn hinted_hresults_keep_their_os_error_and_append_guidance() { + Python::initialize(); + Python::attach(|py| { + let hint = hresult_hint(CO_E_NOTINITIALIZED).expect("CO_E_NOTINITIALIZED hint"); + let not_initialized = windows::core::Error::from_hresult(CO_E_NOTINITIALIZED); + let (winerror, errno, message) = + os_error_fields(py, map_windows_error(not_initialized.clone())); + assert_eq!(winerror, CO_E_NOTINITIALIZED.0); + assert_eq!(errno, 22); + assert!(message.ends_with(hint), "{message}"); + assert_ne!(message, hint, "the Windows description must remain"); + + let (_, _, message) = os_error_fields( + py, + map_dynwinrt_error_with_context( + dynwinrt::Error::WindowsError(not_initialized), + "activation failed", + ), + ); + assert!(message.starts_with("activation failed: "), "{message}"); + assert!(message.ends_with(hint), "{message}"); + + assert_eq!(hresult_hint(E_POINTER), None); + let (winerror, _, message) = os_error_fields( + py, + map_dynwinrt_error(dynwinrt::Error::WindowsError(E_POINTER.into())), + ); + assert_eq!(winerror, E_POINTER.0); + assert!(!message.contains("RoApartment"), "{message}"); + }); + } +} diff --git a/bindings/py/src/implementation.rs b/bindings/py/src/implementation.rs index dbc65ced..1ca498c0 100644 --- a/bindings/py/src/implementation.rs +++ b/bindings/py/src/implementation.rs @@ -20,7 +20,7 @@ use windows::core::{Error, HRESULT}; use crate::errors::map_windows_error; use crate::runtime::{ DynWinRTMethodSig, DynWinRTType, DynWinRTValue, PYWINRT_E_UNRAISABLE_PYTHON_EXCEPTION, WinGUID, - wrap_python_callback_context, + native_outputs, wrap_python_callback_context, }; const RO_E_CLOSED: HRESULT = HRESULT(0x80000013_u32 as i32); @@ -206,15 +206,11 @@ impl CallbackCell { let result = (|| -> PyResult> { let inputs = args .iter() - .map(|value| Py::new(py, DynWinRTValue(value.clone()))) + .map(|value| Py::new(py, DynWinRTValue::new(value.clone()))) .collect::>>()?; let inputs = PyList::new(py, inputs)?; let outputs = callback.call1(py, (interface_index, vtable_index, inputs))?; - Ok(outputs - .extract::>(py)? - .into_iter() - .map(|value| value.0) - .collect()) + native_outputs("implementation callback", outputs.extract(py)?) })(); result.map_err(|error| { let message = format!( @@ -353,7 +349,7 @@ impl DynWinRTImplementation { self.with_native(|native| { native .to_value() - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(map_windows_error) }) } @@ -572,7 +568,7 @@ mod tests { globals .set_item( "result", - DynWinRTValue(dynwinrt::WinRTValue::HString("finished".into())), + DynWinRTValue::new(dynwinrt::WinRTValue::HString("finished".into())), ) .unwrap(); let function = py diff --git a/bindings/py/src/lib.rs b/bindings/py/src/lib.rs index f5bf9fcf..3113ceb9 100644 --- a/bindings/py/src/lib.rs +++ b/bindings/py/src/lib.rs @@ -581,6 +581,11 @@ _Coroutine.register(_DynWinRTAsyncWithProgress) None, )?; + // Constants + for (name, apartment_type) in super::runtime::APARTMENT_TYPE_CONSTANTS { + m.add(name, apartment_type.0)?; + } + // Functions m.add_function(wrap_pyfunction!(super::runtime::init_winappsdk, m)?)?; m.add_function(wrap_pyfunction!(super::runtime::ro_initialize, m)?)?; diff --git a/bindings/py/src/runtime.rs b/bindings/py/src/runtime.rs index 3b717856..618b7b5e 100644 --- a/bindings/py/src/runtime.rs +++ b/bindings/py/src/runtime.rs @@ -7,9 +7,15 @@ use dynwinrt; use pyo3::exceptions::{PyIndexError, PyOverflowError, PyRuntimeError, PyTypeError}; use pyo3::prelude::*; use pyo3::types::{PyBytes, PyDict, PyList}; +use windows::Win32::System::WinRT::{ + RO_INIT_MULTITHREADED, RO_INIT_SINGLETHREADED, RO_INIT_TYPE, RoInitialize, +}; use windows::core::{GUID, HSTRING, IUnknown, Interface}; -use crate::errors::{map_dynwinrt_error, map_dynwinrt_error_with_context, map_windows_error}; +use crate::errors::{ + InputSlot, map_dynwinrt_error, map_dynwinrt_error_with_context, map_windows_error, + non_object_receiver_error, released_input_error, released_receiver_error, +}; /// Shared MetadataTable — created once, used everywhere. static TABLE: std::sync::LazyLock> = @@ -128,6 +134,26 @@ pub struct RoApartment { active: bool, } +/// `apartment_type` used when Python omits it: the multithreaded apartment. +const DEFAULT_APARTMENT_TYPE: i32 = RO_INIT_MULTITHREADED.0; + +/// Module constants naming the `apartment_type` values Python passes to +/// `RoApartment(...)` and `ro_initialize(...)`. +pub(crate) const APARTMENT_TYPE_CONSTANTS: [(&str, RO_INIT_TYPE); 2] = [ + ("RO_INIT_SINGLETHREADED", RO_INIT_SINGLETHREADED), + ("RO_INIT_MULTITHREADED", RO_INIT_MULTITHREADED), +]; + +/// The `RoInitialize` model for a Python `apartment_type`. Values other than +/// `RO_INIT_SINGLETHREADED` keep their historical multithreaded meaning. +fn ro_init_type(apartment_type: i32) -> RO_INIT_TYPE { + if apartment_type == RO_INIT_SINGLETHREADED.0 { + RO_INIT_SINGLETHREADED + } else { + RO_INIT_MULTITHREADED + } +} + impl RoApartment { fn initialize(&mut self) -> PyResult<()> { if self.active { @@ -135,14 +161,7 @@ impl RoApartment { "the COM apartment context is already active", )); } - use windows::Win32::System::WinRT::{ - RO_INIT_MULTITHREADED, RO_INIT_SINGLETHREADED, RoInitialize, - }; - let init_type = match self.apartment_type { - 0 => RO_INIT_SINGLETHREADED, - _ => RO_INIT_MULTITHREADED, - }; - unsafe { RoInitialize(init_type) }.map_err(map_windows_error)?; + unsafe { RoInitialize(ro_init_type(self.apartment_type)) }.map_err(map_windows_error)?; self.active = true; Ok(()) } @@ -167,7 +186,7 @@ impl RoApartment { #[pyo3(signature = (apartment_type=None))] fn new(apartment_type: Option) -> Self { Self { - apartment_type: apartment_type.unwrap_or(1), + apartment_type: apartment_type.unwrap_or(DEFAULT_APARTMENT_TYPE), active: false, } } @@ -208,13 +227,7 @@ pub fn init_winappsdk(major: u32, minor: u32) -> PyResult { #[pyfunction] pub fn ro_initialize(apartment_type: Option) -> PyResult<()> { - use windows::Win32::System::WinRT::{ - RO_INIT_MULTITHREADED, RO_INIT_SINGLETHREADED, RoInitialize, - }; - let init_type = match apartment_type.unwrap_or(1) { - 0 => RO_INIT_SINGLETHREADED, - _ => RO_INIT_MULTITHREADED, - }; + let init_type = ro_init_type(apartment_type.unwrap_or(DEFAULT_APARTMENT_TYPE)); unsafe { RoInitialize(init_type) }.map_err(map_windows_error) } @@ -308,6 +321,8 @@ pub fn unbox_object(py: Python<'_>, value: &Bound<'_, PyAny>) -> PyResult>()?; + // A released value is not a WinRT null; do not unbox it as `None`. + raw.check_input("unbox_object()", InputSlot::Argument(0))?; dynwinrt::unbox_property_value(&raw.0).map_err(map_dynwinrt_error)? }; match result { @@ -953,16 +968,13 @@ impl DynWinRTMethodHandle { fn invoke(&self, obj: DynWinRTValue, args: Vec) -> PyResult { // Extraction retains the native object without holding a Python borrow // while an implementation callback may release the original wrapper. - let raw = match &obj.0 { - dynwinrt::WinRTValue::Object(o) => o.as_raw(), - _ => return Err(PyRuntimeError::new_err("invoke() requires an Object value")), - }; - let wrt_args: Vec = args.iter().map(|a| a.0.clone()).collect(); + let raw = obj.receiver("invoke()")?.as_raw(); + let wrt_args = native_arguments("invoke()", args)?; let results = self.0.invoke(raw, &wrt_args).map_err(map_dynwinrt_error)?; if results.is_empty() { - Ok(DynWinRTValue(dynwinrt::WinRTValue::I32(0))) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::I32(0))) } else { - Ok(DynWinRTValue(results.into_iter().next().unwrap())) + Ok(DynWinRTValue::new(results.into_iter().next().unwrap())) } } @@ -997,27 +1009,20 @@ impl DynWinRTMethodHandle { // Owned extraction ends the Python receiver borrow before dispatch. // Move its native pin into the call so reentrant disposal can release // the original wrapper without shortening the in-flight call lifetime. - let object = match obj.0 { - dynwinrt::WinRTValue::Object(object) => object, - _ => { - return Err(PyRuntimeError::new_err( - "invoke_detached() requires an Object value", - )); - } - }; + let object = obj.into_receiver("invoke_detached()")?; let call = SameThreadCall { method: self.0.clone(), object, - args: args.into_iter().map(|arg| arg.0).collect(), + args: native_arguments("invoke_detached()", args)?, }; let results = py .detach(move || call.run()) .0 .map_err(map_dynwinrt_error)?; if results.is_empty() { - Ok(DynWinRTValue(dynwinrt::WinRTValue::I32(0))) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::I32(0))) } else { - Ok(DynWinRTValue( + Ok(DynWinRTValue::new( results .into_iter() .next() @@ -1033,17 +1038,10 @@ impl DynWinRTMethodHandle { obj: DynWinRTValue, args: Vec, ) -> PyResult> { - let raw = match &obj.0 { - dynwinrt::WinRTValue::Object(o) => o.as_raw(), - _ => { - return Err(PyRuntimeError::new_err( - "invoke_all() requires an Object value", - )); - } - }; - let wrt_args: Vec = args.iter().map(|a| a.0.clone()).collect(); + let raw = obj.receiver("invoke_all()")?.as_raw(); + let wrt_args = native_arguments("invoke_all()", args)?; let results = self.0.invoke(raw, &wrt_args).map_err(map_dynwinrt_error)?; - Ok(results.into_iter().map(DynWinRTValue).collect()) + Ok(results.into_iter().map(DynWinRTValue::new).collect()) } /// Invoke a WinRT composable factory with a runtime-provided outer host. @@ -1056,13 +1054,8 @@ impl DynWinRTMethodHandle { instance_output_index: usize, agile: bool, ) -> PyResult { - let factory = factory.0.as_object().ok_or_else(|| { - PyRuntimeError::new_err("invoke_composed() requires an Object factory") - })?; - let args = args - .into_iter() - .map(|argument| argument.0) - .collect::>(); + let factory = factory.com_receiver("invoke_composed() factory")?; + let args = native_arguments("invoke_composed()", args)?; dynwinrt::compose_winrt( &factory, &self.0, @@ -1072,7 +1065,7 @@ impl DynWinRTMethodHandle { instance_output_index, agile, ) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(map_dynwinrt_error) } @@ -1099,13 +1092,8 @@ impl DynWinRTMethodHandle { agile, ); } - let factory = factory.0.as_object().ok_or_else(|| { - PyRuntimeError::new_err("invoke_composed_with_overrides() requires an Object factory") - })?; - let args = args - .into_iter() - .map(|argument| argument.0) - .collect::>(); + let factory = factory.com_receiver("invoke_composed_with_overrides() factory")?; + let args = native_arguments("invoke_composed_with_overrides()", args)?; let overrides = override_interfaces .iter() .map(|interface| interface.to_core(py)) @@ -1120,7 +1108,7 @@ impl DynWinRTMethodHandle { agile, overrides, ) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(map_dynwinrt_error) } @@ -1128,11 +1116,7 @@ impl DynWinRTMethodHandle { /// Getter → string (0 args, zero Vec allocation) fn get_string(&self, obj: DynWinRTValue) -> PyResult { - let raw = obj - .0 - .as_object() - .ok_or_else(|| PyRuntimeError::new_err("get_string: not an Object"))? - .as_raw(); + let raw = obj.com_receiver("get_string()")?.as_raw(); let hs = self .0 .call_getter_hstring(raw) @@ -1142,65 +1126,45 @@ impl DynWinRTMethodHandle { /// Getter → i32 (0 args, zero Vec allocation) fn get_i32(&self, obj: DynWinRTValue) -> PyResult { - let raw = obj - .0 - .as_object() - .ok_or_else(|| PyRuntimeError::new_err("get_i32: not an Object"))? - .as_raw(); + let raw = obj.com_receiver("get_i32()")?.as_raw(); self.0.call_getter_i32(raw).map_err(map_dynwinrt_error) } /// Getter → bool (0 args, zero Vec allocation) fn get_bool(&self, obj: DynWinRTValue) -> PyResult { - let raw = obj - .0 - .as_object() - .ok_or_else(|| PyRuntimeError::new_err("get_bool: not an Object"))? - .as_raw(); + let raw = obj.com_receiver("get_bool()")?.as_raw(); self.0.call_getter_bool(raw).map_err(map_dynwinrt_error) } /// Getter → DynWinRTValue object (0 args, zero Vec allocation) fn get_obj(&self, obj: DynWinRTValue) -> PyResult { - let raw = obj - .0 - .as_object() - .ok_or_else(|| PyRuntimeError::new_err("get_obj: not an Object"))? - .as_raw(); + let raw = obj.com_receiver("get_obj()")?.as_raw(); self.0 .call_getter_object(raw) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(map_dynwinrt_error) } /// 1-arg invoke with hstring input → DynWinRTValue result fn invoke_hstring(&self, obj: DynWinRTValue, arg: String) -> PyResult { - let raw = obj - .0 - .as_object() - .ok_or_else(|| PyRuntimeError::new_err("invoke_hstring: not an Object"))? - .as_raw(); + let raw = obj.com_receiver("invoke_hstring()")?.as_raw(); let results = self .0 .invoke(raw, &[dynwinrt::WinRTValue::HString(HSTRING::from(arg))]) .map_err(map_dynwinrt_error)?; - Ok(DynWinRTValue(results.into_iter().next().ok_or_else( + Ok(DynWinRTValue::new(results.into_iter().next().ok_or_else( || PyRuntimeError::new_err("invoke_hstring: no result"), )?)) } /// 1-arg invoke with i32 input → DynWinRTValue result fn invoke_i32(&self, obj: DynWinRTValue, arg: i32) -> PyResult { - let raw = obj - .0 - .as_object() - .ok_or_else(|| PyRuntimeError::new_err("invoke_i32: not an Object"))? - .as_raw(); + let raw = obj.com_receiver("invoke_i32()")?.as_raw(); let results = self .0 .invoke(raw, &[dynwinrt::WinRTValue::I32(arg)]) .map_err(map_dynwinrt_error)?; - Ok(DynWinRTValue(results.into_iter().next().ok_or_else( + Ok(DynWinRTValue::new(results.into_iter().next().ok_or_else( || PyRuntimeError::new_err("invoke_i32: no result"), )?)) } @@ -1212,7 +1176,142 @@ impl DynWinRTMethodHandle { #[pyclass(from_py_object)] #[derive(Clone)] -pub struct DynWinRTValue(pub(crate) dynwinrt::WinRTValue); +pub struct DynWinRTValue(pub(crate) dynwinrt::WinRTValue, Lifecycle); + +/// Whether a value still owns its native payload. `release()` is the only +/// transition and leaves `WinRTValue::Null` behind, so this state is what +/// distinguishes a released value from a WinRT null reference. +#[derive(Clone, Copy)] +enum Lifecycle { + Live, + Released, +} + +impl DynWinRTValue { + pub(crate) fn new(value: dynwinrt::WinRTValue) -> Self { + Self(value, Lifecycle::Live) + } + + /// The WinRT object receiving `operation`. + fn receiver(&self, operation: &str) -> PyResult<&IUnknown> { + match &self.0 { + dynwinrt::WinRTValue::Object(object) => Ok(object), + _ => Err(self.receiver_error(operation)), + } + } + + /// Like `receiver`, but moves the object out of this value. + fn into_receiver(self, operation: &str) -> PyResult { + match self.0 { + dynwinrt::WinRTValue::Object(object) => Ok(object), + _ => Err(self.receiver_error(operation)), + } + } + + /// The COM identity receiving `operation`. Unlike `receiver`, this also + /// accepts async operations, as the legacy convenience entry points do. + fn com_receiver(&self, operation: &str) -> PyResult { + self.0 + .as_object() + .ok_or_else(|| self.receiver_error(operation)) + } + + /// QueryInterface this value for `operation`. + pub(crate) fn query(&self, iid: &GUID, operation: &str) -> PyResult { + self.0.cast(iid).map_err(|error| match error { + dynwinrt::Error::ExpectObjectTypeError(_) => self.receiver_error(operation), + error => map_dynwinrt_error(error), + }) + } + + /// Why this value cannot receive `operation`. + fn receiver_error(&self, operation: &str) -> PyErr { + match self.ensure_live() { + Err(error) => error, + Ok(()) => non_object_receiver_error(operation, value_kind(&self.0)), + } + } + + /// Reject a released value before a native call that reports its own + /// payload errors, such as IBuffer access. + fn ensure_live(&self) -> PyResult<()> { + match self.1 { + Lifecycle::Live => Ok(()), + Lifecycle::Released => Err(released_receiver_error()), + } + } + + /// Reject this value if released; `slot` names where `operation` received it. + fn check_input(&self, operation: &str, slot: InputSlot) -> PyResult<()> { + match self.1 { + Lifecycle::Live => Ok(()), + Lifecycle::Released => Err(released_input_error(operation, slot)), + } + } +} + +/// The native values `operation` received, rejecting released values. `slot` +/// maps each position to where it was passed, such as an argument or element. +fn native_inputs( + operation: &str, + values: Vec, + slot: fn(usize) -> InputSlot, +) -> PyResult> { + values + .into_iter() + .enumerate() + .map(|(index, value)| { + value.check_input(operation, slot(index))?; + Ok(value.0) + }) + .collect() +} + +/// The native arguments of `operation`, rejecting released values. +pub(crate) fn native_arguments( + operation: &str, + args: Vec, +) -> PyResult> { + native_inputs(operation, args, InputSlot::Argument) +} + +/// The native values a Python `operation` callback returned, rejecting +/// released values instead of returning them as WinRT null. +pub(crate) fn native_outputs( + operation: &str, + outputs: Vec, +) -> PyResult> { + native_inputs(operation, outputs, InputSlot::Output) +} + +fn value_kind(value: &dynwinrt::WinRTValue) -> &'static str { + use dynwinrt::WinRTValue; + match value { + WinRTValue::Bool(_) => "Bool", + WinRTValue::I8(_) => "I8", + WinRTValue::U8(_) => "U8", + WinRTValue::I16(_) => "I16", + WinRTValue::U16(_) => "U16", + WinRTValue::I32(_) => "I32", + WinRTValue::U32(_) => "U32", + WinRTValue::I64(_) => "I64", + WinRTValue::U64(_) => "U64", + WinRTValue::F32(_) => "F32", + WinRTValue::F64(_) => "F64", + WinRTValue::Object(_) => "Object", + WinRTValue::Null => "null", + WinRTValue::HString(_) => "HString", + WinRTValue::HResult(_) => "HResult", + WinRTValue::Guid(_) => "Guid", + WinRTValue::RawPtr(_) => "RawPtr", + WinRTValue::OutValue(..) => "OutValue", + WinRTValue::Async(_) => "Async", + WinRTValue::ArrayOfIUnknown(_) => "ArrayOfIUnknown", + WinRTValue::Enum { .. } => "Enum", + WinRTValue::Struct(_) => "Struct", + WinRTValue::Array(_) => "Array", + } +} #[pymethods] impl DynWinRTValue { @@ -1220,7 +1319,7 @@ impl DynWinRTValue { fn activation_factory(name: String) -> PyResult { WINUI_MODULES .activation_factory(&HSTRING::from(name)) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(map_dynwinrt_error) } @@ -1237,7 +1336,7 @@ impl DynWinRTValue { )); }; dynwinrt::copy_to_ibuffer(&bytes) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(map_dynwinrt_error) } @@ -1265,7 +1364,7 @@ impl DynWinRTValue { .transpose()?; WINUI_MODULES .create_xaml_application(&provider, callback.as_ref()) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(map_dynwinrt_error) } @@ -1273,71 +1372,71 @@ impl DynWinRTValue { #[staticmethod] fn from_bool(value: bool) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Bool(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::Bool(value)) } #[staticmethod] fn from_i8(value: i32) -> PyResult { - Ok(DynWinRTValue(dynwinrt::WinRTValue::I8(checked_i8( + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::I8(checked_i8( value, "from_i8", )?))) } #[staticmethod] fn from_u8(value: u32) -> PyResult { - Ok(DynWinRTValue(dynwinrt::WinRTValue::U8(checked_u8( + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::U8(checked_u8( value, "from_u8", )?))) } #[staticmethod] fn from_i16(value: i32) -> PyResult { - Ok(DynWinRTValue(dynwinrt::WinRTValue::I16(checked_i16( + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::I16(checked_i16( value, "from_i16", )?))) } #[staticmethod] fn from_u16(value: u32) -> PyResult { - Ok(DynWinRTValue(dynwinrt::WinRTValue::U16(checked_u16( + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::U16(checked_u16( value, "from_u16", )?))) } #[staticmethod] fn from_i32(value: i32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::I32(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::I32(value)) } #[staticmethod] fn from_hresult(value: i32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::HResult(windows::core::HRESULT(value))) + DynWinRTValue::new(dynwinrt::WinRTValue::HResult(windows::core::HRESULT(value))) } #[staticmethod] fn from_u32(value: u32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::U32(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::U32(value)) } #[staticmethod] fn from_i64(value: i64) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::I64(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::I64(value)) } #[staticmethod] fn from_u64(value: u64) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::U64(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::U64(value)) } #[staticmethod] fn from_f32(value: f32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::F32(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::F32(value)) } #[staticmethod] fn from_f64(value: f64) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::F64(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::F64(value)) } #[staticmethod] fn from_hstring(value: String) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::HString(HSTRING::from(value))) + DynWinRTValue::new(dynwinrt::WinRTValue::HString(HSTRING::from(value))) } #[staticmethod] fn from_guid(value: &WinGUID) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Guid(value.0)) + DynWinRTValue::new(dynwinrt::WinRTValue::Guid(value.0)) } #[staticmethod] fn null_value() -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Null) + DynWinRTValue::new(dynwinrt::WinRTValue::Null) } /// Create an enum value within the enum's declared i32 or u32 range. @@ -1346,14 +1445,15 @@ impl DynWinRTValue { enum_type .0 .enum_value(value) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(map_dynwinrt_error) } #[staticmethod] fn box_reference(value: &DynWinRTValue, value_type: &DynWinRTType) -> PyResult { + value.check_input("DynWinRTValue.box_reference()", InputSlot::Argument(0))?; dynwinrt::box_ireference(value.0.clone(), value_type.0.clone()) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(map_dynwinrt_error) } @@ -1378,11 +1478,11 @@ impl DynWinRTValue { items: Vec, element_type: &DynWinRTType, ) -> PyResult { + let wrt_items = native_inputs("DynWinRTValue.create_vector()", items, InputSlot::Element)?; let iids = TABLE.vector_iids(&element_type.0); - let wrt_items: Vec = items.iter().map(|i| i.0.clone()).collect(); let vector = dynwinrt::vector::create_vector_from_values(&wrt_items, &element_type.0, iids) .map_err(map_dynwinrt_error)?; - Ok(DynWinRTValue(dynwinrt::WinRTValue::Object(vector))) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Object(vector))) } /// Create an IMap from parallel key/value lists. @@ -1398,26 +1498,26 @@ impl DynWinRTValue { "create_map: keys and values must have the same length", )); } + const OPERATION: &str = "DynWinRTValue.create_map()"; + let keys = native_inputs(OPERATION, keys, InputSlot::Key)?; + let values = native_inputs(OPERATION, values, InputSlot::Value)?; let iids = TABLE.map_iids(&key_type.0, &value_type.0); - let entries: Vec<(dynwinrt::WinRTValue, dynwinrt::WinRTValue)> = keys - .iter() - .zip(values.iter()) - .map(|(key, value)| (key.0.clone(), value.0.clone())) - .collect(); + let entries: Vec<(dynwinrt::WinRTValue, dynwinrt::WinRTValue)> = + keys.into_iter().zip(values).collect(); let map = dynwinrt::map::create_map_from_values(&entries, &key_type.0, &value_type.0, iids) .map_err(map_dynwinrt_error)?; - Ok(DynWinRTValue(dynwinrt::WinRTValue::Object(map))) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Object(map))) } /// Await an async WinRT operation (blocks the current thread). /// Releases the Python GIL while waiting so other threads can proceed. fn wait(&self, py: Python<'_>) -> PyResult { - super::async_runtime::wait_for_async(&self.0, py).map(DynWinRTValue) + super::async_runtime::wait_for_async(&self.0, py).map(DynWinRTValue::new) } fn _get_async_results(&self) -> PyResult { dynwinrt::get_async_results(&self.0) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(map_dynwinrt_error) } @@ -1458,7 +1558,7 @@ impl DynWinRTValue { let progress_cb: dynwinrt::ProgressCallback = Box::new(move |val: dynwinrt::WinRTValue| { Python::attach(|py| { let result = (|| -> PyResult<()> { - let py_val = Py::new(py, DynWinRTValue(val))?; + let py_val = Py::new(py, DynWinRTValue::new(val))?; callback.call1(py, (py_val,))?; Ok(()) })(); @@ -1611,6 +1711,7 @@ impl DynWinRTValue { /// Copy the initialized bytes from a WinRT IBuffer into Python bytes. fn to_bytes<'py>(&self, py: Python<'py>) -> PyResult> { + self.ensure_live()?; dynwinrt::copy_from_ibuffer(&self.0) .map(|bytes| pyo3::types::PyBytes::new(py, &bytes)) .map_err(map_dynwinrt_error) @@ -1620,42 +1721,39 @@ impl DynWinRTValue { self.0.is_null_object() } + /// Whether `release()` has run on this value, directly or through + /// `release_projected()` or a closing `projected_lifetime_scope()`. + /// + /// A released value is also `is_null()`; this tells it apart from a WinRT + /// null reference, which remains usable as a null input. + fn is_released(&self) -> bool { + matches!(self.1, Lifecycle::Released) + } + /// Release resources owned by this value and replace it with Null. /// /// This is idempotent so projected lifetime scopes can safely retry /// cleanup without double-releasing COM references. fn release(&mut self) { let value = std::mem::replace(&mut self.0, dynwinrt::WinRTValue::Null); + self.1 = Lifecycle::Released; drop(value); } fn as_raw(&self) -> PyResult { - match &self.0 { - dynwinrt::WinRTValue::Object(o) => Ok(o.as_raw() as i64), - _ => Err(PyRuntimeError::new_err( - "Cannot get raw pointer from non-object", - )), - } + Ok(self.receiver("as_raw()")?.as_raw() as i64) } fn identity_raw(&self) -> PyResult { - match &self.0 { - dynwinrt::WinRTValue::Object(object) => object - .cast::() - .map(|identity| identity.as_raw() as i64) - .map_err(map_windows_error), - _ => Err(PyRuntimeError::new_err( - "Cannot get COM identity from a non-object value", - )), - } + self.receiver("identity_raw()")? + .cast::() + .map(|identity| identity.as_raw() as i64) + .map_err(map_windows_error) } /// COM QueryInterface — cast to a different interface. fn cast(&self, iid: &WinGUID) -> PyResult { - self.0 - .cast(&iid.0) - .map(DynWinRTValue) - .map_err(map_dynwinrt_error) + self.query(&iid.0, "cast()").map(DynWinRTValue::new) } /// Invoke metadata-described Invoke on an IUnknown-rooted WinRT delegate. @@ -1674,13 +1772,9 @@ impl DynWinRTValue { let method = dynwinrt::MethodSignature::new(&*TABLE) .add_out(TABLE.object()) .build(6); - let raw = self - .0 - .as_object() - .ok_or_else(|| PyRuntimeError::new_err("activate: not an Object"))? - .as_raw(); + let raw = self.com_receiver("activate()")?.as_raw(); let result = method.call_dynamic(raw, &[]).map_err(map_windows_error)?; - Ok(DynWinRTValue(result.into_iter().next().ok_or_else( + Ok(DynWinRTValue::new(result.into_iter().next().ok_or_else( || PyRuntimeError::new_err("activate: no result"), )?)) } @@ -1692,15 +1786,11 @@ impl DynWinRTValue { let method = dynwinrt::MethodSignature::new(&*TABLE) .add_out(return_type.0.clone()) .build(method_index); - let obj_raw = self - .0 - .as_object() - .ok_or_else(|| PyRuntimeError::new_err("call_0 requires an Object value"))? - .as_raw(); + let obj_raw = self.com_receiver("call_0()")?.as_raw(); let result = method .call_dynamic(obj_raw, &[]) .map_err(map_windows_error)?; - Ok(DynWinRTValue(result.into_iter().next().unwrap())) + Ok(DynWinRTValue::new(result.into_iter().next().unwrap())) } /// Call a method with one arg and one out param. @@ -1710,20 +1800,17 @@ impl DynWinRTValue { return_type: &DynWinRTType, v1: &DynWinRTValue, ) -> PyResult { + let obj_raw = self.com_receiver("call_1()")?.as_raw(); + v1.check_input("call_1()", InputSlot::Argument(0))?; let in_type = TABLE.handle_from_kind(v1.0.get_type_kind()); let method = dynwinrt::MethodSignature::new(&*TABLE) .add_in(in_type) .add_out(return_type.0.clone()) .build(method_index); - let obj_raw = self - .0 - .as_object() - .ok_or_else(|| PyRuntimeError::new_err("call_1 requires an Object value"))? - .as_raw(); let result = method .call_dynamic(obj_raw, &[v1.0.clone()]) .map_err(map_windows_error)?; - Ok(DynWinRTValue(result.into_iter().next().unwrap())) + Ok(DynWinRTValue::new(result.into_iter().next().unwrap())) } /// General-purpose method call with explicit types and args. @@ -1740,10 +1827,8 @@ impl DynWinRTValue { } method = method.add_out(return_type.0.clone()); - let obj = match &self.0 { - dynwinrt::WinRTValue::Object(o) => o.as_raw(), - _ => return Err(PyRuntimeError::new_err("call() requires an Object value")), - }; + let obj = self.receiver("call()")?.as_raw(); + let winrt_args = native_arguments("call()", args)?; let mut iface = dynwinrt::InterfaceSignature::define_from_iinspectable("", Default::default(), &*TABLE); @@ -1753,15 +1838,14 @@ impl DynWinRTValue { } iface.add_method(method); - let winrt_args: Vec = args.iter().map(|a| a.0.clone()).collect(); let result = iface.methods[target_index] .call_dynamic(obj, &winrt_args) .map_err(map_windows_error)?; if result.is_empty() { - Ok(DynWinRTValue(dynwinrt::WinRTValue::I32(0))) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::I32(0))) } else { - Ok(DynWinRTValue(result.into_iter().next().unwrap())) + Ok(DynWinRTValue::new(result.into_iter().next().unwrap())) } } @@ -1798,6 +1882,20 @@ impl DynWinRTValue { #[derive(Clone)] pub struct DynWinRTArray(dynwinrt::ArrayData); +impl DynWinRTArray { + fn from_elements( + operation: &str, + values: Vec, + element_type: &DynWinRTType, + ) -> PyResult { + let values = native_inputs(operation, values, InputSlot::Element)?; + Ok(Self(dynwinrt::ArrayData::from_values( + element_type.0.clone(), + &values, + ))) + } +} + #[pymethods] impl DynWinRTArray { fn __len__(&self) -> usize { @@ -1809,14 +1907,14 @@ impl DynWinRTArray { let index = checked_index(index)?; self.0 .try_get(index) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(map_dynwinrt_error) } /// Convert all elements to a list of DynWinRTValue. fn to_values(&self) -> Vec { (0..self.0.len()) - .map(|i| DynWinRTValue(self.0.get(i))) + .map(|i| DynWinRTValue::new(self.0.get(i))) .collect() } @@ -2003,13 +2101,11 @@ impl DynWinRTArray { } #[staticmethod] - fn from_values(values: Vec, element_type: &DynWinRTType) -> DynWinRTArray { - let values: Vec = - values.iter().map(|value| value.0.clone()).collect(); - DynWinRTArray(dynwinrt::ArrayData::from_values( - element_type.0.clone(), - &values, - )) + fn from_values( + values: Vec, + element_type: &DynWinRTType, + ) -> PyResult { + Self::from_elements("DynWinRTArray.from_values()", values, element_type) } /// Build a DynWinRTArray of WinRT object/interface elements. @@ -2022,8 +2118,8 @@ impl DynWinRTArray { fn from_object_values( values: Vec, element_type: &DynWinRTType, - ) -> DynWinRTArray { - Self::from_values(values, element_type) + ) -> PyResult { + Self::from_elements("DynWinRTArray.from_object_values()", values, element_type) } /// Return the u8 array data as a Python `bytes` object. Safe for both @@ -2064,7 +2160,7 @@ impl DynWinRTArray { /// Wrap as DynWinRTValue::Array for passing to call(). fn to_value(&self) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Array(self.0.clone())) + DynWinRTValue::new(dynwinrt::WinRTValue::Array(self.0.clone())) } fn __repr__(&self) -> String { @@ -2285,13 +2381,14 @@ impl DynWinRTStruct { 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)), + Some(object) => Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Object(object))), + None => Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Null)), } } fn set_object(&mut self, index: i64, value: &DynWinRTValue) -> PyResult<()> { let index = checked_index(index)?; + value.check_input("DynWinRTStruct.set_object()", InputSlot::Field(index))?; match &value.0 { dynwinrt::WinRTValue::Object(obj) => self .0 @@ -2309,7 +2406,7 @@ impl DynWinRTStruct { /// Wrap as DynWinRTValue::Struct for passing to call(). fn to_value(&self) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Struct(self.0.clone())) + DynWinRTValue::new(dynwinrt::WinRTValue::Struct(self.0.clone())) } fn __repr__(&self) -> String { @@ -2339,7 +2436,7 @@ fn create_python_delegate( let py_args = args .iter() .map(|arg| { - Ok(DynWinRTValue(arg.clone()) + Ok(DynWinRTValue::new(arg.clone()) .into_pyobject(py)? .into_any() .unbind()) @@ -2383,7 +2480,7 @@ impl DynWinRtDelegate { /// Get the delegate as a DynWinRTValue for passing to WinRT methods. fn to_value(&self) -> DynWinRTValue { - DynWinRTValue(self.0.clone()) + DynWinRTValue::new(self.0.clone()) } fn __repr__(&self) -> String { @@ -2465,7 +2562,7 @@ impl DynWinRtElementFactory { ) }; let result = (|| -> PyResult { - let argument = Py::new(py, DynWinRTValue(args.clone()))?; + let argument = Py::new(py, DynWinRTValue::new(args.clone()))?; let result = callback.call1(py, (argument,))?; let value = result.extract::>(py)?; value.0.cast(&element_iid).map_err(map_dynwinrt_error) @@ -2497,7 +2594,7 @@ impl DynWinRtElementFactory { ) }; let result = (|| -> PyResult<()> { - let argument = Py::new(py, DynWinRTValue(args.clone()))?; + let argument = Py::new(py, DynWinRTValue::new(args.clone()))?; callback.call1(py, (argument,))?; Ok(()) })(); @@ -2518,7 +2615,7 @@ impl DynWinRtElementFactory { } fn to_value(&self) -> DynWinRTValue { - DynWinRTValue(self.value.clone()) + DynWinRTValue::new(self.value.clone()) } fn release_callbacks(&self) -> PyResult<()> { @@ -2660,7 +2757,7 @@ mod tests { None, ) .unwrap(); - let receiver = DynWinRTValue(owner.to_value().unwrap().cast(&iid).unwrap()); + let receiver = DynWinRTValue::new(owner.to_value().unwrap().cast(&iid).unwrap()); let method = DynWinRTMethodHandle(interface.method(6).unwrap()); let direct = method.invoke(receiver.clone(), vec![]).unwrap(); let detached = method.invoke_detached(py, receiver, vec![]).unwrap(); @@ -2671,7 +2768,7 @@ mod tests { )); } let invalid = method - .invoke_detached(py, DynWinRTValue(dynwinrt::WinRTValue::I32(0)), vec![]) + .invoke_detached(py, DynWinRTValue::new(dynwinrt::WinRTValue::I32(0)), vec![]) .err() .expect("non-object receiver must be rejected"); assert!(invalid.is_instance_of::(py)); diff --git a/bindings/py/tests/test_error_messages.py b/bindings/py/tests/test_error_messages.py new file mode 100644 index 00000000..245e185b --- /dev/null +++ b/bindings/py/tests/test_error_messages.py @@ -0,0 +1,502 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""Actionable errors for common runtime misuse.""" + +import errno +import json +import re +import subprocess +import sys +import textwrap +import threading +from uuid import uuid4 + +import pytest + +import dynwinrt +from dynwinrt import ( + RO_INIT_MULTITHREADED, + RO_INIT_SINGLETHREADED, + DynWinRTArray, + DynWinRTImplementation, + DynWinRTImplementationMethod, + DynWinRTInterfacePlan, + DynWinRTMethodSig, + DynWinRTStruct, + DynWinRTType, + DynWinRTValue, + DynWinRtDelegate, + RoApartment, + WinGUID, + projected_lifetime_scope, + release_projected, + ro_initialize, + ro_uninitialize, + unbox_object, +) +from dynwinrt.dynwinrt import ( + _dynwinrt_cache_projected, + _dynwinrt_projected_from_native, + _dynwinrt_track_projected, +) + +IID_IURI_FACTORY = WinGUID.parse("44A9796F-723E-4FDF-A218-033E75B0C084") +IID_IURI = WinGUID.parse("9E365E57-48B2-4160-956F-C7385120BBFC") +IID_ISTRINGABLE = WinGUID.parse("96369F54-8EB6-48F0-ABCE-C1B211E627C3") +IID_TEST_DELEGATE = WinGUID.parse("5A0F1C3E-7B24-4D69-8E1F-2C3B4A5D6E7F") +IID_IPROPERTY_VALUE_STATICS = WinGUID.parse("629BDBC8-D932-4FF4-96B9-8D96C5C1E858") +PYTHON_EXCEPTION = -1594998779 # 0xA0EE4005 +RPC_E_CHANGED_MODE = -2147417850 +CO_E_NOTINITIALIZED = -2147221008 +NOT_INITIALIZED_HINT = ( + "WinRT is not initialized on this thread; use `with dynwinrt.RoApartment():` " + "(or call `dynwinrt.ro_initialize(dynwinrt.RO_INIT_MULTITHREADED)`) before " + "calling WinRT APIs." +) +RELEASED_REASON = re.escape( + "has been released (its projected_lifetime_scope() exited, or " + "release_projected() / DynWinRTValue.release() was called) and can no longer " + "be used." +) + "$" +RELEASED = rf"^This WinRT object {RELEASED_REASON}" + +_URI_FACTORY = DynWinRTType.register_interface( + "ErrorGuidanceUriFactory", IID_IURI_FACTORY +).add_method( + "CreateUri", + DynWinRTMethodSig().add_in(DynWinRTType.hstring()).add_out(DynWinRTType.object()), +) +_URI = DynWinRTType.register_interface("ErrorGuidanceUri", IID_IURI).add_method( + "get_AbsoluteUri", DynWinRTMethodSig().add_out(DynWinRTType.hstring()) +) +_STRINGABLE = DynWinRTType.register_interface( + "ErrorGuidanceStringable", IID_ISTRINGABLE +).add_method("ToString", DynWinRTMethodSig().add_out(DynWinRTType.hstring())) + + +def released_input(slot, operation): + return ( + rf"^This WinRT object \({slot} of {re.escape(operation)}\) " + rf"{RELEASED_REASON}" + ) + + +def released_argument(position, operation): + return released_input(f"argument {position}", operation) + + +class ProjectedUri: + """The generated runtime-class wrapper shape, reduced to two members.""" + + 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(IID_IURI) + self._dynwinrt_native_ready = True + _dynwinrt_track_projected(self, "Windows.Foundation.Uri") + _dynwinrt_cache_projected(self) + + def __init__(self, obj): + if getattr(self, "_dynwinrt_native_ready", False): + return + self._set_native(obj) + + @classmethod + def create(cls, uri): + factory = DynWinRTValue.activation_factory("Windows.Foundation.Uri").cast( + IID_IURI_FACTORY + ) + try: + return cls( + _URI_FACTORY.method(6).invoke(factory, [DynWinRTValue.from_hstring(uri)]) + ) + finally: + factory.release() + + @property + def absolute_uri(self): + return _URI.method(6).invoke(self._obj, []).to_string() + + def to_string(self): + stringable = self._obj.cast(IID_ISTRINGABLE) + return _STRINGABLE.method(6).invoke(stringable, []).to_string() + + +def _assert_released(uri): + with pytest.raises(RuntimeError, match=RELEASED): + uri.absolute_uri + with pytest.raises(RuntimeError, match=RELEASED): + uri.to_string() + assert uri._obj.is_null() + + +def _released_uri_value(): + value = DynWinRTValue.activation_factory("Windows.Foundation.Uri") + value.release() + value.release() + assert value.is_null() + return value + + +def test_projection_used_after_scope_exit_explains_the_release(): + with RoApartment(): + with projected_lifetime_scope(): + uri = ProjectedUri.create("https://example.com/scoped") + assert uri.absolute_uri == "https://example.com/scoped" + + _assert_released(uri) + + +def test_projection_used_after_release_projected_explains_the_release(): + with RoApartment(): + uri = ProjectedUri.create("https://example.com/released") + assert uri.to_string() == "https://example.com/released" + release_projected(uri) + + _assert_released(uri) + + +def test_projection_used_after_direct_value_release_explains_the_release(): + with RoApartment(): + uri = ProjectedUri.create("https://example.com/direct") + uri._obj.release() + _assert_released(uri) + + value = DynWinRTValue.activation_factory("Windows.Foundation.Uri") + value.release() + with pytest.raises(RuntimeError, match=RELEASED) as caught: + value.cast(IID_IURI_FACTORY) + assert "DynWinRTValue.release()" in str(caught.value) + + +def test_receivers_report_released_values_null_and_value_kinds(): + method = _URI.method(6) + receivers = ( + ("invoke()", lambda value: method.invoke(value, [])), + ("invoke_all()", lambda value: method.invoke_all(value, [])), + ("invoke_detached()", lambda value: method.invoke_detached(value, [])), + ("get_string()", lambda value: method.get_string(value)), + ("call_0()", lambda value: value.call_0(6, DynWinRTType.hstring())), + ("call()", lambda value: value.call(6, DynWinRTType.hstring(), [], [])), + ("as_raw()", lambda value: value.as_raw()), + ("identity_raw()", lambda value: value.identity_raw()), + ("cast()", lambda value: value.cast(IID_IURI)), + ) + for value, kind in ( + (DynWinRTValue.from_i32(7), "I32"), + (DynWinRTValue.from_hstring("text"), "HString"), + (DynWinRTValue.null_value(), "null"), + ): + for operation, call in receivers: + expected = rf"^{re.escape(operation)} requires an Object value, got {kind}$" + with pytest.raises(RuntimeError, match=expected): + call(value) + + with RoApartment(): + released = _released_uri_value() + for _, call in receivers: + with pytest.raises(RuntimeError, match=RELEASED): + call(released) + + buffer = DynWinRTValue.from_bytes(b"released") + buffer.release() + with pytest.raises(RuntimeError, match=RELEASED): + buffer.to_bytes() + + +def test_released_arguments_are_rejected_by_position(): + with RoApartment(): + uri = ProjectedUri.create("https://example.com/argument") + other = ProjectedUri.create("https://example.com/argument") + released = _released_uri_value() + method = _URI.method(6) + live = DynWinRTValue.from_i32(1) + # IUriRuntimeClass.Equals(Uri) is vtable slot 21. + equals = (21, DynWinRTType.bool_type(), [DynWinRTType.object()]) + + assert uri._obj.call(*equals, [other._obj]).to_bool() + for operation, call in ( + ("invoke()", lambda: method.invoke(uri._obj, [live, released])), + ("invoke_all()", lambda: method.invoke_all(uri._obj, [live, released])), + ( + "invoke_detached()", + lambda: method.invoke_detached(uri._obj, [live, released]), + ), + ("call()", lambda: uri._obj.call(*equals, [live, released])), + ): + with pytest.raises(RuntimeError, match=released_argument(1, operation)): + call() + with pytest.raises(RuntimeError, match=released_argument(0, "call_1()")): + uri._obj.call_1(21, DynWinRTType.bool_type(), released) + + release_projected(uri) + release_projected(other) + + +def test_released_values_nested_in_inputs_are_rejected_by_position(): + with RoApartment(): + live = DynWinRTValue.activation_factory("Windows.Foundation.Uri") + released = _released_uri_value() + key = DynWinRTValue.from_hstring("key") + objects = DynWinRTType.object() + try: + vector = DynWinRTValue.create_vector([live, DynWinRTValue.null_value()], objects) + vector.release() + with pytest.raises( + RuntimeError, + match=released_input("element 1", "DynWinRTValue.create_vector()"), + ): + DynWinRTValue.create_vector([live, released], objects) + + strings = DynWinRTType.hstring() + mapping = DynWinRTValue.create_map([key], [live], strings, objects) + mapping.release() + with pytest.raises( + RuntimeError, match=released_input("key 0", "DynWinRTValue.create_map()") + ): + DynWinRTValue.create_map([released], [live], objects, objects) + with pytest.raises( + RuntimeError, match=released_input("value 1", "DynWinRTValue.create_map()") + ): + DynWinRTValue.create_map( + [key, DynWinRTValue.from_hstring("other")], + [live, released], + strings, + objects, + ) + + for operation, build in ( + ("DynWinRTArray.from_values()", DynWinRTArray.from_values), + ("DynWinRTArray.from_object_values()", DynWinRTArray.from_object_values), + ): + assert len(build([live, DynWinRTValue.null_value()], objects)) == 2 + with pytest.raises(RuntimeError, match=released_input("element 1", operation)): + build([live, released], objects) + + fields = DynWinRTStruct.create( + DynWinRTType.struct_type("Tests.ReleasedObjectField", [objects]) + ) + fields.set_object(0, live) + fields.set_object(0, DynWinRTValue.null_value()) + assert fields.get_object(0).is_null() + with pytest.raises( + RuntimeError, match=released_input("field 0", "DynWinRTStruct.set_object()") + ): + fields.set_object(0, released) + finally: + live.release() + + +def test_unbox_object_distinguishes_released_values_from_null(): + assert unbox_object(None) is None + assert unbox_object(DynWinRTValue.null_value()) is None + with RoApartment(): + statics = DynWinRTValue.activation_factory("Windows.Foundation.PropertyValue").cast( + IID_IPROPERTY_VALUE_STATICS + ) + # IPropertyValueStatics.CreateString is vtable slot 18. + boxed = statics.call( + 18, + DynWinRTType.object(), + [DynWinRTType.hstring()], + [DynWinRTValue.from_hstring("boxed")], + ) + statics.release() + assert unbox_object(boxed) == "boxed" + boxed.release() + with pytest.raises(RuntimeError, match=released_argument(0, "unbox_object()")): + unbox_object(boxed) + + +def test_is_released_tells_released_values_from_winrt_null(): + null = DynWinRTValue.null_value() + assert null.is_null() and not null.is_released() + value = DynWinRTValue.from_i32(1) + assert not value.is_released() + value.release() + value.release() + assert value.is_null() and value.is_released() + + +def test_box_reference_rejects_released_values(): + with RoApartment(): + boxed = DynWinRTValue.box_reference(DynWinRTValue.from_i32(7), DynWinRTType.i32_type()) + assert not boxed.is_null() + boxed.release() + released = DynWinRTValue.from_i32(7) + released.release() + with pytest.raises( + RuntimeError, match=released_argument(0, "DynWinRTValue.box_reference()") + ): + DynWinRTValue.box_reference(released, DynWinRTType.i32_type()) + + +def test_implementation_callbacks_reject_released_outputs(monkeypatch): + objects = DynWinRTType.object() + iid = WinGUID.parse(str(uuid4())) + # GetPair(out Object first) -> Object: one out parameter, then the result. + signature = DynWinRTMethodSig().add_out(objects).add_out(objects) + typ = DynWinRTType.register_interface("Tests.ReleasedOutputs", iid).add_method( + "GetPair", signature + ) + plan = DynWinRTInterfacePlan.create( + "Tests.ReleasedOutputs", typ, [DynWinRTImplementationMethod("GetPair", 6, signature)] + ) + unraisable = [] + monkeypatch.setattr(sys, "unraisablehook", unraisable.append) + with RoApartment(): + live = DynWinRTValue.activation_factory("Windows.Foundation.Uri") + outputs = [live, DynWinRTValue.null_value()] + owner = DynWinRTImplementation.create([plan], lambda _interface, _slot, _args: outputs) + canonical = owner.to_value() + view = canonical.cast(iid) + canonical.release() + try: + first, result = typ.method(6).invoke_all(view, []) + assert first.identity_raw() == live.identity_raw() and result.is_null() + first.release() + + for position in (0, 1): + outputs = [live, DynWinRTValue.null_value()] + outputs[position] = _released_uri_value() + with pytest.raises(OSError) as caught: + typ.method(6).invoke_all(view, []) + assert caught.value.winerror == PYTHON_EXCEPTION + expected = released_input(f"output {position}", "implementation callback") + assert re.match(expected, str(unraisable.pop().exc_value)) + assert f"(output {position} of implementation callback)" in owner.take_error() + assert not unraisable + + outputs = [DynWinRTValue.null_value(), DynWinRTValue.null_value()] + assert all(value.is_null() for value in typ.method(6).invoke_all(view, [])) + finally: + view.release() + owner.dispose() + live.release() + + +def test_delegate_invocation_rejects_released_receivers_and_arguments(): + signature = DynWinRTMethodSig().add_in(DynWinRTType.object()) + calls = [] + delegate = DynWinRtDelegate.create( + IID_TEST_DELEGATE, + [DynWinRTType.object()], + lambda argument: calls.append(argument.is_null()), + ).to_value() + try: + assert delegate.invoke_delegate( + IID_TEST_DELEGATE, signature, [DynWinRTValue.null_value()] + ) == [] + assert calls == [True] + + with RoApartment(): + released = _released_uri_value() + with pytest.raises( + RuntimeError, match=released_argument(0, "delegate Invoke()") + ): + delegate.invoke_delegate(IID_TEST_DELEGATE, signature, [released]) + assert calls == [True] + finally: + delegate.release() + + with pytest.raises(RuntimeError, match=RELEASED): + delegate.invoke_delegate( + IID_TEST_DELEGATE, signature, [DynWinRTValue.null_value()] + ) + with pytest.raises( + RuntimeError, match=r"^delegate Invoke\(\) requires an Object value, got null$" + ): + DynWinRTValue.null_value().invoke_delegate(IID_TEST_DELEGATE, signature, []) + + +def test_apartment_constants_name_the_ro_init_models(): + assert (RO_INIT_SINGLETHREADED, RO_INIT_MULTITHREADED) == (0, 1) + assert {"RO_INIT_SINGLETHREADED", "RO_INIT_MULTITHREADED"} <= set(dynwinrt.__all__) + assert repr(RoApartment()) == repr(RoApartment(RO_INIT_MULTITHREADED)) + observed = [] + errors = [] + + def worker(): + # Keep no traceback cycles: unsendable apartments must drop on this thread. + try: + ro_initialize(RO_INIT_MULTITHREADED) + ro_uninitialize() + with RoApartment(RO_INIT_SINGLETHREADED) as apartment: + observed.append(repr(apartment)) + try: + with RoApartment(RO_INIT_MULTITHREADED): + pass + except OSError as error: + observed.append(error.winerror) + except BaseException as error: + errors.append(repr(error)) + + thread = threading.Thread(target=worker) + thread.start() + thread.join() + assert not errors + assert observed == [ + "RoApartment(apartment_type=0, active=true)", + RPC_E_CHANGED_MODE, + ] + + +def test_uninitialized_thread_error_explains_apartment_setup(): + # Test modules initialize the process MTA at import, which makes every new + # thread an implicit MTA member. Observe a genuinely uninitialized thread + # in a fresh interpreter instead. + script = r''' + import json + import threading + from dynwinrt import DynWinRTValue + + caught = [] + + def call_without_apartment(): + try: + DynWinRTValue.activation_factory("Windows.Foundation.Uri") + except BaseException as error: + caught.append(error) + + thread = threading.Thread(target=call_without_apartment) + thread.start() + thread.join() + error = caught[0] + print(json.dumps({ + "type": type(error).__name__, + "winerror": getattr(error, "winerror", None), + "errno": getattr(error, "errno", None), + "strerror": getattr(error, "strerror", None), + "message": str(error), + })) + ''' + result = subprocess.run( + [sys.executable, "-c", textwrap.dedent(script)], + capture_output=True, + text=True, + timeout=60, + check=False, + ) + assert result.returncode == 0, result.stderr + report = json.loads(result.stdout) + + assert report["type"] == "OSError" + assert report["winerror"] == CO_E_NOTINITIALIZED + assert report["errno"] == errno.EINVAL + strerror = report["strerror"] + assert report["message"] == f"[WinError {CO_E_NOTINITIALIZED}] {strerror}" + assert strerror.endswith(f" {NOT_INITIALIZED_HINT}"), strerror + assert strerror[: -len(NOT_INITIALIZED_HINT)].strip(), "Windows text must remain" + + +def test_other_hresults_do_not_mention_apartment_setup(): + with RoApartment(): + with pytest.raises(OSError) as exc_info: + DynWinRTValue.activation_factory("Contoso.DynWinRT.MissingClass") + + assert exc_info.value.winerror != CO_E_NOTINITIALIZED + assert "RoApartment" not in str(exc_info.value) diff --git a/docs/guides/windows/winrt-interface-implementations.md b/docs/guides/windows/winrt-interface-implementations.md index 433b881d..7d5a4da4 100644 --- a/docs/guides/windows/winrt-interface-implementations.md +++ b/docs/guides/windows/winrt-interface-implementations.md @@ -71,7 +71,7 @@ class TextHandler: def to_string(self) -> str: return "Implemented in Python" -with RoApartment(1), IStringable.implement(TextHandler()) as impl: +with RoApartment(), IStringable.implement(TextHandler()) as impl: print(impl.value.to_string()) # Crosses the native vtable in both directions. ``` diff --git a/samples/python/app-lifecycle-single-instance/app.py b/samples/python/app-lifecycle-single-instance/app.py index 0d655305..4c5f0ea5 100644 --- a/samples/python/app-lifecycle-single-instance/app.py +++ b/samples/python/app-lifecycle-single-instance/app.py @@ -27,7 +27,7 @@ async def run_primary( ) -> None: runtime = init_winappsdk(major, minor) try: - with RoApartment(1), projected_lifetime_scope(): + with RoApartment(), projected_lifetime_scope(): instance = AppInstance.find_or_register_for_key(key) if instance is None or not instance.is_current: raise RuntimeError("Could not register the primary instance") @@ -64,7 +64,7 @@ def on_activated( async def redirect_to_primary(key: str, major: int, minor: int) -> None: runtime = init_winappsdk(major, minor) try: - with RoApartment(1), projected_lifetime_scope(): + with RoApartment(), projected_lifetime_scope(): current = AppInstance.get_current() target = AppInstance.find_or_register_for_key(key) if current is None or target is None: diff --git a/samples/python/app-notification/app.py b/samples/python/app-notification/app.py index d552f08b..693de3ae 100644 --- a/samples/python/app-notification/app.py +++ b/samples/python/app-notification/app.py @@ -42,7 +42,7 @@ async def run( ) -> None: runtime = init_winappsdk(major, minor) try: - with RoApartment(1), projected_lifetime_scope(): + with RoApartment(), projected_lifetime_scope(): supported = AppNotificationManager.is_supported() notification = build_notification() if smoke: diff --git a/samples/python/async-file-io/app.py b/samples/python/async-file-io/app.py index 6cd7111d..56ed5804 100644 --- a/samples/python/async-file-io/app.py +++ b/samples/python/async-file-io/app.py @@ -13,7 +13,7 @@ async def run() -> None: expected = "Hello from dynwinrt.\nAsync WinRT file I/O works." with tempfile.TemporaryDirectory(prefix="dynwinrt-python-") as directory: - with RoApartment(1), projected_lifetime_scope(): + with RoApartment(), projected_lifetime_scope(): folder = await StorageFolder.get_folder_from_path_async(directory) if folder is None: raise RuntimeError("StorageFolder returned no temporary folder") diff --git a/samples/python/cryptography/app.py b/samples/python/cryptography/app.py index e0a7c5c3..2ff36fee 100644 --- a/samples/python/cryptography/app.py +++ b/samples/python/cryptography/app.py @@ -7,7 +7,7 @@ def sha256(text: str) -> str: - with RoApartment(1), projected_lifetime_scope(): + with RoApartment(), projected_lifetime_scope(): provider = HashAlgorithmProvider.open_algorithm("SHA256") if provider is None: raise RuntimeError("SHA256 provider is unavailable") diff --git a/samples/python/device-watcher/app.py b/samples/python/device-watcher/app.py index 83f5578e..a1576d72 100644 --- a/samples/python/device-watcher/app.py +++ b/samples/python/device-watcher/app.py @@ -13,7 +13,7 @@ async def enumerate_devices(timeout: int, show_names: bool) -> None: - with RoApartment(1), projected_lifetime_scope(): + with RoApartment(), projected_lifetime_scope(): watcher = DeviceInformation.create_watcher() if watcher is None: raise RuntimeError("DeviceInformation returned no watcher") diff --git a/samples/python/interface-implementation/app.py b/samples/python/interface-implementation/app.py index 92234cdc..35da8586 100644 --- a/samples/python/interface-implementation/app.py +++ b/samples/python/interface-implementation/app.py @@ -65,7 +65,7 @@ def close(self) -> None: def main() -> None: - with RoApartment(1), projected_lifetime_scope(): + with RoApartment(), projected_lifetime_scope(): handlers = Task() with IBackgroundTaskInstance.implement(TaskInstance()) as instance_impl, IBackgroundTask.implement( handlers, interfaces=[(IStringable, handlers), (IClosable, handlers)] diff --git a/samples/python/ocr-image/app.py b/samples/python/ocr-image/app.py index 6cd4c9cc..90953ef8 100644 --- a/samples/python/ocr-image/app.py +++ b/samples/python/ocr-image/app.py @@ -15,7 +15,7 @@ def normalized_words(value: str) -> set[str]: async def recognize(path: Path) -> str: - with RoApartment(1), projected_lifetime_scope(): + with RoApartment(), projected_lifetime_scope(): file = await StorageFile.get_file_from_path_async(str(path.resolve())) if file is None: raise RuntimeError("StorageFile returned no image file") diff --git a/samples/python/text-to-speech/app.py b/samples/python/text-to-speech/app.py index c3267524..f6991911 100644 --- a/samples/python/text-to-speech/app.py +++ b/samples/python/text-to-speech/app.py @@ -10,7 +10,7 @@ async def speak(text: str, smoke: bool) -> None: - with RoApartment(1), projected_lifetime_scope(): + with RoApartment(), projected_lifetime_scope(): with SpeechSynthesizer() as synthesizer: stream = await synthesizer.synthesize_text_to_stream_async(text) if stream is None: diff --git a/samples/python/winui-hello-world/app.py b/samples/python/winui-hello-world/app.py index f3596534..9d1a2903 100644 --- a/samples/python/winui-hello-world/app.py +++ b/samples/python/winui-hello-world/app.py @@ -9,6 +9,7 @@ ) from dynwinrt import ( + RO_INIT_SINGLETHREADED, RoApartment, init_winappsdk, project_as, @@ -48,7 +49,7 @@ def run(smoke: bool, major: int, minor: int) -> None: subscriptions: list[Callable[[], None]] = [] try: - with RoApartment(0), projected_lifetime_scope(): + with RoApartment(RO_INIT_SINGLETHREADED), projected_lifetime_scope(): def initialize(_params: object) -> None: def launched() -> None: diff --git a/samples/python/winui-tic-tac-toe-code-only/app.py b/samples/python/winui-tic-tac-toe-code-only/app.py index 8c0cd9f9..6ba72213 100644 --- a/samples/python/winui-tic-tac-toe-code-only/app.py +++ b/samples/python/winui-tic-tac-toe-code-only/app.py @@ -6,7 +6,12 @@ ROOT / ".runtime" / "Microsoft.WindowsAppRuntime.Bootstrap.dll" ) -from dynwinrt import RoApartment, init_winappsdk, projected_lifetime_scope +from dynwinrt import ( + RO_INIT_SINGLETHREADED, + RoApartment, + init_winappsdk, + projected_lifetime_scope, +) from generated.microsoft.ui.xaml import ( Application, ApplicationTheme, @@ -61,7 +66,7 @@ def run() -> None: state: dict[str, object] = {} try: - with RoApartment(0), projected_lifetime_scope(): + with RoApartment(RO_INIT_SINGLETHREADED), projected_lifetime_scope(): def initialize(_params: object) -> None: def launched() -> None: diff --git a/samples/python/winui-tic-tac-toe/app.py b/samples/python/winui-tic-tac-toe/app.py index fb92b7f2..dd9e1eee 100644 --- a/samples/python/winui-tic-tac-toe/app.py +++ b/samples/python/winui-tic-tac-toe/app.py @@ -8,6 +8,7 @@ ) from dynwinrt import ( + RO_INIT_SINGLETHREADED, RoApartment, init_winappsdk, project_as, @@ -66,7 +67,7 @@ def run() -> None: state: dict[str, object] = {} try: - with RoApartment(0), projected_lifetime_scope(): + with RoApartment(RO_INIT_SINGLETHREADED), projected_lifetime_scope(): registration = StackPanel.register_xaml_runtime_class( "DynWinRT.Example.TicTacToePanel", TicTacToePanel, diff --git a/tests/e2e/e2e_specs.json b/tests/e2e/e2e_specs.json index 709bf04f..5a28ff8f 100644 --- a/tests/e2e/e2e_specs.json +++ b/tests/e2e/e2e_specs.json @@ -98,7 +98,18 @@ }, "checks": [ { "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" } + { "kind": "projection_identity", "member": "as_interface", "langs": ["py"], "interface_class": "IStringable" }, + { "kind": "as_interface_rejects_runtime_class", "member": "as_interface", "langs": ["py"] } + ] + }, + { + "id": "python_released_projection_error", + "namespace": "Windows.Foundation", + "class": "Uri", + "langs": ["py"], + "instantiate": { "kind": "none" }, + "checks": [ + { "kind": "released_projection_error", "member": "host", "args": ["https://example.com/released"] } ] }, { diff --git a/tests/e2e/e2e_specs.schema.json b/tests/e2e/e2e_specs.schema.json index fc2ecf9e..743f54f9 100644 --- a/tests/e2e/e2e_specs.schema.json +++ b/tests/e2e/e2e_specs.schema.json @@ -67,6 +67,8 @@ "nullable_object_array_roundtrip", "interface_cast", "projection_identity", + "as_interface_rejects_runtime_class", + "released_projection_error", "struct_roundtrip", "array_roundtrip", "static_string_length", diff --git a/tests/e2e/runners/implementation_py.py b/tests/e2e/runners/implementation_py.py index 6a7c5704..eb2dc2e0 100644 --- a/tests/e2e/runners/implementation_py.py +++ b/tests/e2e/runners/implementation_py.py @@ -1111,13 +1111,142 @@ def parse_double(self, text): dw.release_projected(view) +RELEASED = ( + r"has been released \(its projected_lifetime_scope\(\) exited, or " + r"release_projected\(\) / DynWinRTValue\.release\(\) was called\) and can no " + r"longer be used\." +) +INSPECTABLE_IID = "af86e2e0-b12d-4c6a-9c5a-d7aa65101e90" +PROPERTY_VALUE_NAMES = ( + "uint8", "int16", "uint16", "int32", "uint32", "int64", "uint64", "single", + "double", "char16", "boolean", "string", "guid", "date_time", "time_span", + "point", "size", "rect", +) + + +def released_references(g, dw, own): + """Released values stay released through generated implementation code.""" + + class Text: + def to_string(self): + return "released reference" + + text_owner = own(g.IStringable.implement(Text())) + live = text_owner.to_value() + + def released_value(): + value = text_owner.to_value() + value.release() + assert value.is_released() and value.is_null() + return value + + # A stored event handler invoked with a released sender. + callbacks = {} + + class Reference: + def get_capacity(self): + return 1 + + def add_closed(self, handler): + callbacks[len(callbacks) + 1] = handler + return g.EventRegistrationToken(value=len(callbacks)) + + def remove_closed(self, token): + callbacks.pop(token.value, None) + + def close(self): + pass + + reference = Reference() + reference_owner = own(g.IMemoryBufferReference.implement( + reference, g.IClosable.implementation(reference) + )) + sender = g.IMemoryBufferReference.from_implementation(reference_owner) + senders = [] + unsubscribe = sender.subscribe_closed(lambda value, _args: senders.append(value)) + handler = callbacks[1] + with dw.projected_lifetime_scope(): + scoped_sender = g.IMemoryBufferReference.from_value(sender._obj) + for released_sender in (released_value(), scoped_sender): + expect_error( + lambda: handler(released_sender, None), + r"\(argument 0 of delegate Invoke\(\)\) " + RELEASED, + ) + assert senders == [], "A released sender must not reach the native handler as null" + handler(None, None) + handler(dw.DynWinRTValue.null_value(), None) + assert senders == [None, None] + unsubscribe() + + # An element of an array result. + items = {"value": [live, None]} + methods = { + "get_type": not_implemented, + "get_is_numeric_scalar": not_implemented, + "get_inspectable_array": lambda self: items["value"], + } + for name in PROPERTY_VALUE_NAMES: + methods[f"get_{name}"] = methods[f"get_{name}_array"] = not_implemented + properties_owner = own(g.IPropertyValue.implement(type("Inspectables", (), methods)())) + properties = g.IPropertyValue.from_implementation(properties_owner) + result = properties.get_inspectable_array() + assert len(result) == 2 and result[1] is None + result[0].release() + items["value"] = [released_value(), live] + expect_hresult(properties.get_inspectable_array, PYTHON_CALLBACK_ERROR) + take_error( + properties_owner, + r"\(element 0 of DynWinRTArray\.from_object_values\(\)\) " + RELEASED, + ) + + # A direct result. + class Items: + item = None + get_size = index_of = first = not_implemented + + def get_at(self, index): + return self.item + + vector_handlers = Items() + vector_owner = own(g.IBindableVectorView.implement( + vector_handlers, g.IBindableIterable.implementation(vector_handlers) + )) + vector = g.IBindableVectorView.from_implementation(vector_owner) + assert vector.get_at(0) is None + vector_handlers.item = released_value() + expect_hresult(lambda: vector.get_at(0), PYTHON_CALLBACK_ERROR) + take_error(vector_owner, r"\(output 0 of implementation callback\) " + RELEASED) + vector_handlers.item = live + item = vector.get_at(0) + assert item.identity_raw() == live.identity_raw() + item.release() + + # Generated struct results store reference fields through the same helper. + runtime = importlib.import_module(f"{g.__name__}._runtime") + iid = dw.WinGUID.parse(INSPECTABLE_IID) + null = dw.DynWinRTValue.null_value() + assert runtime._implementation_reference(null, iid, "item") is null + released = released_value() + assert runtime._implementation_reference(released, iid, "item") is released + fields = dw.DynWinRTStruct.create(dw.DynWinRTType.struct_type( + "E2E.ReleasedReferenceField", [dw.DynWinRTType.object()] + )) + fields.set_object(0, runtime._implementation_reference(None, iid, "item")) + fields.set_object(0, runtime._implementation_reference(live, iid, "item")) + expect_error( + lambda: fields.set_object(0, runtime._implementation_reference(released, iid, "item")), + r"\(field 0 of DynWinRTStruct\.set_object\(\)\) " + RELEASED, + ) + live.release() + + CASES = { case.__name__: case for case in ( management_handle, property_views, background_task, multi_interface_lifetime, dispose_disconnects, reentrant_dispose, callback_error, async_handler_rejected, async_result_rejected, required_interfaces, memory_buffer_event, array_contracts, value_shapes, fill_array, fill_array_wrong_length, named_outputs, - nullable_reference_results, + nullable_reference_results, released_references, public_view_success_gc, public_view_failed_cast_gc, ) } diff --git a/tests/e2e/runners/py_runner.py b/tests/e2e/runners/py_runner.py index 9bde333f..1821530c 100644 --- a/tests/e2e/runners/py_runner.py +++ b/tests/e2e/runners/py_runner.py @@ -341,6 +341,86 @@ async def run_check( else: cr['pass'] = True + elif kind == 'as_interface_rejects_runtime_class': + try: + obj.as_interface(cls) + except TypeError as error: + expected = f'dynwinrt.project_as(obj, {cls.__name__})' + if expected not in str(error): + cr['error'] = f'TypeError did not suggest {expected}: {error}' + return cr + else: + cr['error'] = 'as_interface() accepted a runtime class' + return cr + # Other misuse keeps the AttributeError of the from_value lookup. + for target in (None, 42, object, obj): + try: + obj.as_interface(target) + except AttributeError: + continue + except Exception as error: + cr['error'] = ( + f'as_interface({target!r}) raised {type(error).__name__}, ' + 'expected AttributeError' + ) + return cr + cr['error'] = f'as_interface({target!r}) succeeded' + return cr + cr['pass'] = True + + elif kind == 'released_projection_error': + reason = ( + 'has been released (its projected_lifetime_scope() exited, or ' + 'release_projected() / DynWinRTValue.release() was called) and ' + 'can no longer be used.' + ) + receiver = f'This WinRT object {reason}' + args = [literal_arg(a) for a in check.get('args', [])] + with dw.projected_lifetime_scope(): + scoped = cls(*args) + released = cls(*args) + dw.release_projected(released) + value_released = cls(*args) + value_released._obj.release() + live = cls(*args) + property_value = generated_type(pkg_name, 'PropertyValue') + uses = ( + ('scope exit', lambda: getattr(scoped, member), receiver), + ('release_projected', lambda: getattr(released, member), receiver), + ( + 'DynWinRTValue.release()', + lambda: getattr(value_released, member), + receiver, + ), + ('interface cast', released.to_string, receiver), + # A runtime-class parameter is cast first, which receives it. + ('runtime-class argument', lambda: live.equals(released), receiver), + # An Object parameter reaches the native invocation unchanged. + ( + 'object argument', + lambda: property_value.create_inspectable(released), + f'This WinRT object (argument 0 of invoke()) {reason}', + ), + ( + 'array element', + lambda: property_value.create_inspectable_array([live, released]), + 'This WinRT object (element 1 of ' + f'DynWinRTArray.from_values()) {reason}', + ), + ) + for label, use, expected in uses: + try: + use() + except RuntimeError as error: + if str(error) != expected: + cr['error'] = f'{label}: expected {expected!r}, got {str(error)!r}' + return cr + else: + cr['error'] = f'{label}: released projection allowed a WinRT call' + return cr + dw.release_projected(live) + cr['pass'] = True + elif kind == 'narrow_integer_overflow': cases = ( ('create_uint8', (256,)), @@ -500,8 +580,43 @@ async def run_check( f'wrapper IReference roundtrip returned {actual!r}, ' f'expected {check["compatibility_value"]!r}' ) - else: - cr['pass'] = True + return cr + + # A released wrapper is neither passed nor unboxed as a null + # reference. Generated struct IReference field setters share the + # module's unbox helper. + reason = ( + 'has been released (its projected_lifetime_scope() exited, or ' + 'release_projected() / DynWinRTValue.release() was called) and ' + 'can no longer be used.' + ) + released_box = factory(check['compatibility_value']) + released = reference_cls.from_value(getattr(released_box, '_obj', released_box)) + dw.release_projected(released) + unbox = importlib.import_module( + implementation_module_name(pkg_name, namespace, cls.__name__) + )._dynwinrt_unbox_reference + if unbox(None) is not None or unbox(reference) != check['compatibility_value']: + cr['error'] = 'IReference unbox helper changed a null or live value' + return cr + for label, use, expected in ( + ( + 'argument', + lambda: setattr(obj, member, released), + f'This WinRT object (argument 0 of invoke()) {reason}', + ), + ('unbox', lambda: unbox(released), f'This WinRT object {reason}'), + ): + try: + use() + except RuntimeError as error: + if str(error) != expected: + cr['error'] = f'{label}: expected {expected!r}, got {str(error)!r}' + return cr + else: + cr['error'] = f'{label}: a released IReference was accepted' + return cr + cr['pass'] = True elif kind == 'struct_roundtrip': struct_module = check.get( diff --git a/tools/dynwinrt-codegen/python/README.md b/tools/dynwinrt-codegen/python/README.md index 2b16f617..f9dda50b 100644 --- a/tools/dynwinrt-codegen/python/README.md +++ b/tools/dynwinrt-codegen/python/README.md @@ -47,7 +47,7 @@ The generated package can then be imported normally: from dynwinrt import RoApartment, projected_lifetime_scope from generated_uri.windows.foundation import Uri -with RoApartment(1), projected_lifetime_scope(): +with RoApartment(), projected_lifetime_scope(): uri = Uri("https://example.com/path") print(uri.host) ``` 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 1a670f54..f109cd00 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs @@ -763,8 +763,7 @@ pub fn generate_class( || !class.required_interfaces.is_empty() { out.push('\n'); - out.push_str(" def as_interface(self, interface_class):\n"); - out.push_str(" return interface_class.from_value(self._obj)\n"); + out.push_str(&as_interface_method(context)); } if winui::is_dispatcher_queue(class) { @@ -890,8 +889,7 @@ pub fn generate_class( " return cls._from_native(obj.cast(IID_{symbol}))\n" )); out.push('\n'); - out.push_str(" def as_interface(self, interface_class):\n"); - out.push_str(" return interface_class.from_value(self._obj)\n"); + out.push_str(&as_interface_method(context)); for methods in crate::codegen::winrt::python::overloads::grouped_methods( reorder_getters_before_setters(&req_iface.methods), ) { 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 76351781..02a4fea5 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs @@ -41,6 +41,7 @@ const HEADER: &str = "# Generated by dynwinrt-codegen — do not edit\n"; const FUTURE_ANNOTATIONS: &str = "from __future__ import annotations\n"; fn import_line(context: &PythonProjectionContext) -> String { let object_input = context.support_symbol_import(PythonSupportSymbol::ObjectInput); + let as_interface = context.support_symbol_import(PythonSupportSymbol::AsInterface); format!( "\ from ._runtime import ( @@ -53,7 +54,7 @@ from ._runtime import ( _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _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, + {as_interface}, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, ) @@ -128,8 +129,34 @@ def _dynwinrt_can_cast(value, iid): return False projected.release() return True + + +def _dynwinrt_as_interface(native, interface_class): + if ( + isinstance(interface_class, type) + and ( + getattr(interface_class, '_dynwinrt_runtime_class_type', False) + or getattr(interface_class, '_dynwinrt_projectable_class_type', False) + ) + and not hasattr(interface_class, 'from_value') + ): + name = interface_class.__name__ + raise TypeError( + f'as_interface() requires a generated interface class, but {name} is a ' + f'runtime class. Use dynwinrt.project_as(obj, {name}) to cast to a runtime class.' + ) + return interface_class.from_value(native) \n"; +/// `as_interface()`, emitted for every generated class and interface view. +/// It references the module's allocated name for the runtime-support helper. +fn as_interface_method(context: &PythonProjectionContext) -> String { + format!( + " def as_interface(self, interface_class):\n return {}(self._obj, interface_class)\n", + context.support_symbol_reference(PythonSupportSymbol::AsInterface) + ) +} + pub fn generate_runtime_support_module() -> String { format!( "{HEADER}{FUTURE_ANNOTATIONS}{RUNTIME_SUPPORT_BODY}{}{}", @@ -156,7 +183,12 @@ def _dynwinrt_box_reference(value, value_type, wrap): def _dynwinrt_unbox_reference(value): raw = getattr(value, '_obj', None) if isinstance(raw, DynWinRTValue): - return None if raw.is_null() else value.value + # A released wrapper is not a null reference: reading its value raises. + # Runtimes without is_released() keep treating it as None. + is_released = getattr(raw, 'is_released', None) + if raw.is_null() and not (is_released is not None and is_released()): + return None + return value.value return value 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 4eb5dd40..c85f1e14 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs @@ -351,8 +351,7 @@ pub fn generate_interface(context: &PythonProjectionContext, iface: &InterfaceMe iface.name )); out.push('\n'); - out.push_str(" def as_interface(self, interface_class):\n"); - out.push_str(" return interface_class.from_value(self._obj)\n"); + out.push_str(&as_interface_method(context)); out.push('\n'); } diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/implementation.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/implementation.rs index 72dbf32d..ec9dee46 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/implementation.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/implementation.rs @@ -62,7 +62,9 @@ def _implementation_reference(value, iid, label): raw = getattr(value, '_obj', value) if not isinstance(raw, DynWinRTValue): raise TypeError(f'{label}: expected a managed WinRT value or None') - return DynWinRTValue.null_value() if raw.is_null() else raw.cast(iid) + # Return a null or released value itself: native marshaling rejects only + # the released one, which a fresh null_value() would have hidden. + return raw if raw.is_null() else raw.cast(iid) def _implementation_sync(value, label): diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs index 3dedf02b..ae2ab424 100644 --- a/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs @@ -17,12 +17,14 @@ pub type PythonTypeIdentity = TypeIdentity; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub(crate) enum PythonSupportSymbol { ObjectInput, + AsInterface, } impl PythonSupportSymbol { fn name(self) -> &'static str { match self { Self::ObjectInput => "_DynWinRTObject", + Self::AsInterface => "_dynwinrt_as_interface", } } } @@ -912,7 +914,10 @@ impl PythonProjectionContext { } } // Support imports yield to metadata declarations and their allocated roles. - for helper in [PythonSupportSymbol::ObjectInput] { + for helper in [ + PythonSupportSymbol::ObjectInput, + PythonSupportSymbol::AsInterface, + ] { let preferred = helper.name(); let mut name = preferred.to_string(); let mut index = 2; @@ -1433,27 +1438,32 @@ pub fn to_snake_case_filename(name: &str) -> String { mod tests { use super::*; - #[test] - fn object_input_helper_yields_to_visible_roles_without_renaming_metadata() { - let helper = PythonSupportSymbol::ObjectInput; + fn assert_support_helper_yields_to_visible_roles(helper: PythonSupportSymbol) { + let name = helper.name(); + let [second, third] = [format!("{name}_2"), format!("{name}_3")]; + let others = [ + PythonSupportSymbol::ObjectInput, + PythonSupportSymbol::AsInterface, + ] + .into_iter() + .filter(|other| *other != helper) + .collect::>(); for kind in [ TypeIdentityKind::Class, TypeIdentityKind::Interface, TypeIdentityKind::Struct, ] { for packaged in [false, true] { - let owner = TypeIdentity::named(kind, "Audit", "_DynWinRTObject"); - let peer = - TypeIdentity::named(TypeIdentityKind::Enum, "Audit", "_DynWinRTObject_2"); - let unused = - TypeIdentity::named(TypeIdentityKind::Enum, "Unused", "_DynWinRTObject_3"); + let owner = TypeIdentity::named(kind, "Audit", name); + let peer = TypeIdentity::named(TypeIdentityKind::Enum, "Audit", &second); + let unused = TypeIdentity::named(TypeIdentityKind::Enum, "Unused", &third); let context = PythonProjectionContext::new([owner.clone(), peer.clone(), unused], packaged) .unwrap(); let structs = if kind == TypeIdentityKind::Struct { vec![TypeMeta::Struct { namespace: "Audit".into(), - name: "_DynWinRTObject".into(), + name: name.into(), fields: vec![], }] } else { @@ -1465,25 +1475,35 @@ mod tests { [(peer.clone(), PythonSymbol::Type)], [], ); - assert_eq!(module.reference_name(&owner), "_DynWinRTObject"); - assert_eq!(module.reference_name(&peer), "_DynWinRTObject_2"); - assert_eq!(module.support_symbol_reference(helper), "_DynWinRTObject_3"); + assert_eq!(module.reference_name(&owner), name); + assert_eq!(module.reference_name(&peer), second); + assert_eq!(module.support_symbol_reference(helper), third); assert_eq!( module.support_symbol_import(helper), - "_DynWinRTObject as _DynWinRTObject_3" + format!("{name} as {third}") ); + for other in &others { + assert_eq!(module.support_symbol_import(*other), other.name()); + } let isolated = context.with_local_types(Some(owner), &structs, [], []); - assert_eq!( - isolated.support_symbol_reference(helper), - "_DynWinRTObject_2" - ); + assert_eq!(isolated.support_symbol_reference(helper), second); let control = context.with_local_types(Some(peer), &[], [], []); - assert_eq!(control.support_symbol_import(helper), "_DynWinRTObject"); - assert_eq!(context.support_symbol_import(helper), "_DynWinRTObject"); + assert_eq!(control.support_symbol_import(helper), name); + assert_eq!(context.support_symbol_import(helper), name); } } } + #[test] + fn object_input_helper_yields_to_visible_roles_without_renaming_metadata() { + assert_support_helper_yields_to_visible_roles(PythonSupportSymbol::ObjectInput); + } + + #[test] + fn as_interface_helper_yields_to_visible_roles_without_renaming_metadata() { + assert_support_helper_yields_to_visible_roles(PythonSupportSymbol::AsInterface); + } + #[test] fn companion_aliases_freeze_roles_and_use_only_visible_symbols() { let owner = TypeIdentity::named(TypeIdentityKind::Class, "Audit", "Widget"); diff --git a/tools/dynwinrt-codegen/tests/python_as_interface_test.rs b/tools/dynwinrt-codegen/tests/python_as_interface_test.rs new file mode 100644 index 00000000..be2e8b18 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/python_as_interface_test.rs @@ -0,0 +1,474 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod common; + +use std::collections::HashSet; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Output}; +use std::sync::atomic::{AtomicU64, Ordering}; + +use dynwinrt_codegen::codegen::python::{self, generate_runtime_support_module}; +use dynwinrt_codegen::meta::{self, ClassMeta, InterfaceMeta}; +use dynwinrt_codegen::types::TypeMeta; +use windows_metadata::{ + FieldAttributes, MethodAttributes, MethodCallAttributes, MethodImplAttributes, ParamAttributes, + Signature, Type, TypeAttributes, Value, writer, +}; + +const AS_INTERFACE: &str = " def as_interface(self, interface_class): + return _dynwinrt_as_interface(self._obj, interface_class) +"; + +fn interface(name: &str, iid: &str) -> InterfaceMeta { + InterfaceMeta { + name: name.into(), + namespace: "Contoso".into(), + iid: iid.into(), + ..Default::default() + } +} + +fn assert_imports_helper(module: &str) { + let (_, imports) = module + .split_once("from ._runtime import (") + .expect("runtime support import"); + let (imports, _) = imports.split_once(')').expect("closed import list"); + assert!( + imports + .split(',') + .any(|name| name.trim() == "_dynwinrt_as_interface"), + "{module}" + ); +} + +#[test] +fn every_as_interface_delegates_to_the_runtime_support_helper() { + let class = ClassMeta { + name: "Widget".into(), + namespace: "Contoso".into(), + full_name: "Contoso.Widget".into(), + default_interface: Some(interface("IWidget", "11111111-1111-1111-1111-111111111111")), + required_interfaces: vec![interface("IExtra", "22222222-2222-2222-2222-222222222222")], + ..Default::default() + }; + let known = HashSet::from(["Widget".to_string()]); + let py = common::generate_class(&class, &known, &HashSet::new(), &HashSet::new()); + assert_imports_helper(&py); + let (runtime_class, embedded_interface) = py + .split_once("\nclass IExtra:") + .expect("embedded runtime interface"); + assert!( + runtime_class.contains("_dynwinrt_runtime_class_type = True") + && runtime_class.contains(AS_INTERFACE), + "{runtime_class}" + ); + assert!( + embedded_interface.contains(AS_INTERFACE), + "{embedded_interface}" + ); + + let py = common::generate_interface( + &interface("IWidget", "11111111-1111-1111-1111-111111111111"), + &HashSet::from(["IWidget".to_string()]), + &HashSet::new(), + ); + assert_imports_helper(&py); + assert!(py.contains(AS_INTERFACE), "{py}"); +} + +#[test] +fn as_interface_helper_changes_only_runtime_class_misuse() { + let runtime = generate_runtime_support_module(); + let helper = runtime + .split_once("def _dynwinrt_as_interface(native, interface_class):\n") + .map(|(_, helper)| helper.split("\n\n\n").next().unwrap_or(helper)) + .expect("as_interface helper"); + // Only a generated runtime class without from_value raises TypeError. + let (guard, fallback) = helper + .split_once(" raise TypeError(") + .expect("runtime-class TypeError"); + for expected in [ + "isinstance(interface_class, type)", + "_dynwinrt_runtime_class_type", + "_dynwinrt_projectable_class_type", + "not hasattr(interface_class, 'from_value')", + ] { + assert!(guard.contains(expected), "missing {expected:?}:\n{helper}"); + } + assert!( + fallback.contains("Use dynwinrt.project_as(obj, {name}) to cast to a runtime class."), + "{helper}" + ); + // Every other target keeps the original from_value lookup and its errors. + assert!( + fallback + .trim_end() + .ends_with("return interface_class.from_value(native)"), + "{helper}" + ); + assert_eq!(helper.matches("raise ").count(), 1, "{helper}"); +} + +/// A metadata struct whose declaration takes the helper's module name. +const COLLIDING: &str = "_dynwinrt_as_interface"; +const ALIASED_IMPORT: &str = "_dynwinrt_as_interface as _dynwinrt_as_interface_2"; +const ALIASED_AS_INTERFACE: &str = " def as_interface(self, interface_class): + return _dynwinrt_as_interface_2(self._obj, interface_class) +"; +// Windows.Foundation.Uri implements both, so the generated module can cast a +// real object: IUriRuntimeClass and IUriRuntimeClassWithAbsoluteCanonicalUri. +const URI_RUNTIME_CLASS: (u32, u16, u16, [u8; 8]) = ( + 0x9e365e57, + 0x48b2, + 0x4160, + [0x95, 0x6f, 0xc7, 0x38, 0x51, 0x20, 0xbb, 0xfc], +); +const URI_ABSOLUTE_CANONICAL: (u32, u16, u16, [u8; 8]) = ( + 0x758d9661, + 0x221c, + 0x480f, + [0xa3, 0x39, 0x50, 0x65, 0x66, 0x73, 0xf4, 0x6f], +); + +static NEXT: AtomicU64 = AtomicU64::new(0); + +struct Fixture(PathBuf); + +impl Fixture { + fn new() -> Self { + let directory = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("target") + .join(format!( + "python-as-interface-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed), + )); + fs::create_dir_all(&directory).unwrap(); + Self(directory) + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn success(output: Output) { + assert!( + output.status.success(), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); +} + +fn python() -> PathBuf { + std::env::var_os("DYNWINRT_TEST_PYTHON") + .map(PathBuf::from) + .unwrap_or_else(|| { + let venv = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("bindings") + .join("py") + .join(".venv") + .join("Scripts") + .join("python.exe"); + if venv.is_file() { + venv + } else { + PathBuf::from("python") + } + }) +} + +fn runtime_available() -> bool { + let available = Command::new(python()) + .args([ + "-c", + "import dynwinrt; assert hasattr(dynwinrt, 'RO_INIT_MULTITHREADED')", + ]) + .output() + .is_ok_and(|output| output.status.success()); + assert!( + available || std::env::var("DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME").as_deref() != Ok("1"), + "the as_interface collision probe requires the current Python binding" + ); + if !available { + eprintln!("Skipping the as_interface runtime probe; set DYNWINRT_TEST_PYTHON."); + } + available +} + +fn guid(file: &mut writer::File, definition: writer::TypeDef, value: (u32, u16, u16, [u8; 8])) { + let attribute = file.TypeRef("Windows.Foundation.Metadata", "GuidAttribute"); + let mut types = vec![Type::U32, Type::U16, Type::U16]; + types.extend(std::iter::repeat_n(Type::U8, 8)); + let constructor = file.MemberRef( + ".ctor", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::Void, + types, + }, + writer::MemberRefParent::TypeRef(attribute), + ); + let (data1, data2, data3, data4) = value; + let mut arguments = vec![Value::U32(data1), Value::U16(data2), Value::U16(data3)]; + arguments.extend(data4.map(Value::U8)); + file.Attribute( + writer::HasAttribute::TypeDef(definition), + writer::AttributeType::MemberRef(constructor), + &arguments + .into_iter() + .map(|argument| (String::new(), argument)) + .collect::>(), + ); +} + +fn declare_interface(file: &mut writer::File, name: &str, value: (u32, u16, u16, [u8; 8])) { + let definition = file.TypeDef( + "Audit", + name, + writer::TypeDefOrRef::default(), + TypeAttributes::Public + | TypeAttributes::Interface + | TypeAttributes::Abstract + | TypeAttributes::WindowsRuntime, + ); + guid(file, definition, value); +} + +fn declare_method(file: &mut writer::File, name: &str, result: Type, params: &[(&str, Type)]) { + file.MethodDef( + name, + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: result, + types: params.iter().map(|(_, typ)| typ.clone()).collect(), + }, + MethodAttributes::Public + | MethodAttributes::Abstract + | MethodAttributes::Virtual + | MethodAttributes::NewSlot, + MethodImplAttributes::default(), + ); + for (index, (name, _)) in params.iter().enumerate() { + file.Param(name, index as u16 + 1, ParamAttributes::In); + } +} + +/// `Audit.Widget` casts between two real Uri interfaces, while its default +/// interface also uses a struct declared with the helper's name. +fn colliding_metadata(path: &Path) { + let mut file = writer::File::new("PythonAsInterfaceCollision"); + let value_type = file.TypeRef("System", "ValueType"); + file.TypeDef( + "Audit", + COLLIDING, + writer::TypeDefOrRef::TypeRef(value_type), + TypeAttributes::Public + | TypeAttributes::Sealed + | TypeAttributes::SequentialLayout + | TypeAttributes::WindowsRuntime, + ); + file.Field("Value", &Type::I32, FieldAttributes::Public); + declare_interface(&mut file, "IWidget", URI_RUNTIME_CLASS); + declare_method(&mut file, "ReadAbsoluteUri", Type::String, &[]); + let colliding = Type::named("Audit", COLLIDING); + declare_method( + &mut file, + "Echo", + colliding.clone(), + &[("value", colliding)], + ); + declare_interface(&mut file, "IExtra", URI_ABSOLUTE_CANONICAL); + declare_method(&mut file, "ReadCanonicalUri", Type::String, &[]); + + let object = file.TypeRef("System", "Object"); + let class = file.TypeDef( + "Audit", + "Widget", + writer::TypeDefOrRef::TypeRef(object), + TypeAttributes::Public | TypeAttributes::Sealed | TypeAttributes::WindowsRuntime, + ); + let default = file.InterfaceImpl(class, &Type::named("Audit", "IWidget")); + file.InterfaceImpl(class, &Type::named("Audit", "IExtra")); + let attribute = file.TypeRef("Windows.Foundation.Metadata", "DefaultAttribute"); + let constructor = file.MemberRef( + ".ctor", + &Signature { + flags: MethodCallAttributes::HASTHIS, + return_type: Type::Void, + types: vec![], + }, + writer::MemberRefParent::TypeRef(attribute), + ); + file.Attribute( + writer::HasAttribute::InterfaceImpl(default), + writer::AttributeType::MemberRef(constructor), + &[], + ); + fs::create_dir_all(path.parent().unwrap()).unwrap(); + fs::write(path, file.into_stream()).unwrap(); +} + +/// Assert that a module which also declares or imports `COLLIDING` binds the +/// helper under an alias, and that every `as_interface()` calls that alias. +fn assert_aliased_as_interface(module: &str, expected_methods: usize) { + let (_, imports) = module + .split_once("from ._runtime import (") + .expect("runtime support import"); + let (imports, _) = imports.split_once(')').expect("closed import list"); + let imports = imports.split(',').map(str::trim).collect::>(); + assert!(imports.contains(&ALIASED_IMPORT), "{module}"); + assert!(!imports.contains(&COLLIDING), "{module}"); + assert_eq!( + module.matches("def as_interface(").count(), + expected_methods, + "{module}" + ); + assert_eq!( + module.matches(ALIASED_AS_INTERFACE).count(), + expected_methods, + "{module}" + ); + assert!( + !module.contains("return _dynwinrt_as_interface(self._obj"), + "{module}" + ); +} + +fn probe(directory: &Path, module: &str) { + fs::write( + directory.join("as_interface_probe.py"), + format!( + r#" +import importlib +import dynwinrt as dw + +support = importlib.import_module("pyviews._runtime") +module = importlib.import_module("pyviews.{module}") +Widget, IExtra = module.Widget, module.IExtra +assert module._dynwinrt_as_interface_2 is support._dynwinrt_as_interface +assert getattr(module, "{COLLIDING}", None) is not support._dynwinrt_as_interface + +factory_iid = dw.WinGUID.parse("44a9796f-723e-4fdf-a218-033e75b0c084") +factory_type = dw.DynWinRTType.register_interface( + "AsInterfaceCollisionFactory", factory_iid +).add_method( + "CreateUri", + dw.DynWinRTMethodSig().add_in(dw.DynWinRTType.hstring()).add_out(dw.DynWinRTType.object()), +) +uri = "https://example.com/as-interface" +with dw.RoApartment(), dw.projected_lifetime_scope(): + factory = dw.DynWinRTValue.activation_factory("Windows.Foundation.Uri").cast(factory_iid) + widget = Widget._from_native( + factory_type.method(6).invoke(factory, [dw.DynWinRTValue.from_hstring(uri)]) + ) + factory.release() + assert widget.read_absolute_uri() == uri + extra = widget.as_interface(IExtra) + assert type(extra) is IExtra + assert extra.read_canonical_uri() == uri + assert extra.as_interface(IExtra) is extra + try: + widget.as_interface(Widget) + except TypeError as error: + assert "dynwinrt.project_as(obj, Widget)" in str(error), error + else: + raise AssertionError("as_interface() accepted a runtime class") + + class NoIidInterface: + _dynwinrt_interface_type = True + + # Other misuse keeps its original AttributeError from the from_value lookup. + for target in (None, 42, object, NoIidInterface, widget): + try: + widget.as_interface(target) + except AttributeError as error: + assert "from_value" in str(error), error + else: + raise AssertionError(f"as_interface() accepted {{target!r}}") +print("as-interface-collision-ok") +"# + ), + ) + .unwrap(); + let output = Command::new(python()) + .args(["-B", "as_interface_probe.py"]) + .current_dir(directory) + .output() + .unwrap(); + assert!( + String::from_utf8_lossy(&output.stdout).contains("as-interface-collision-ok"), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + success(output); +} + +#[test] +fn colliding_metadata_declarations_alias_the_as_interface_helper() { + let fixture = Fixture::new(); + let winmd = fixture.0.join("metadata").join("Collision.winmd"); + colliding_metadata(&winmd); + let run_runtime = runtime_available(); + + // Standalone modules declare the struct inline, next to the class and its + // embedded IExtra view. + let class = meta::parse_class(winmd.to_str().unwrap(), "Audit", "Widget").unwrap(); + assert_eq!(class.required_interfaces.len(), 1); + let structs = python::package_structs(std::slice::from_ref(&class), &[]); + let identities = [TypeMeta::RuntimeClass { + namespace: "Audit".into(), + name: "Widget".into(), + default_interface: None, + } + .type_identity()] + .into_iter() + .chain(class.all_interfaces().map(InterfaceMeta::type_identity)) + .chain(structs.iter().map(TypeMeta::type_identity)); + let context = python::PythonProjectionContext::new(identities, false).unwrap(); + let source = python::generate_class(&context, &class, &Default::default()); + assert!( + source.contains(&format!("\nclass {COLLIDING}:")), + "{source}" + ); + assert_aliased_as_interface(&source, 2); + let standalone = fixture.0.join("standalone"); + let package = standalone.join("pyviews"); + fs::create_dir_all(&package).unwrap(); + fs::write(package.join("__init__.py"), "").unwrap(); + fs::write( + package.join("_runtime.py"), + generate_runtime_support_module(), + ) + .unwrap(); + fs::write(package.join("audit__widget.py"), &source).unwrap(); + if run_runtime { + probe(&standalone, "audit__widget"); + } + + // Packaged output resolves the struct from its own module. + let packaged = fixture.0.join("packaged"); + success( + Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args(["generate", "--winmd"]) + .arg(&winmd) + .arg("--output") + .arg(packaged.join("pyviews")) + .args(["--lang", "py", "--class-name", "Audit.Widget"]) + .output() + .unwrap(), + ); + let source = fs::read_to_string(packaged.join("pyviews").join("audit__widget.py")).unwrap(); + assert_aliased_as_interface(&source, 2); + if run_runtime { + probe(&packaged, "audit__widget"); + } +} diff --git a/tools/dynwinrt-codegen/tests/python_released_implementation_test.rs b/tools/dynwinrt-codegen/tests/python_released_implementation_test.rs new file mode 100644 index 00000000..3152bd66 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/python_released_implementation_test.rs @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::sync::atomic::{AtomicU64, Ordering}; + +use dynwinrt_codegen::codegen::{python, python_stub}; +use dynwinrt_codegen::meta::{InterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; +use dynwinrt_codegen::types::{FieldMeta, TypeMeta}; + +static NEXT: AtomicU64 = AtomicU64::new(0); + +struct Fixture(PathBuf); + +impl Fixture { + fn new() -> Self { + let path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("target") + .join(format!( + "python-released-implementation-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed) + )); + fs::create_dir_all(&path).unwrap(); + Self(path) + } +} + +impl Drop for Fixture { + fn drop(&mut self) { + let _ = fs::remove_dir_all(&self.0); + } +} + +fn python() -> PathBuf { + std::env::var_os("DYNWINRT_TEST_PYTHON") + .map(PathBuf::from) + .unwrap_or_else(|| { + let venv = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("..") + .join("..") + .join("bindings") + .join("py") + .join(".venv") + .join("Scripts") + .join("python.exe"); + if venv.is_file() { + venv + } else { + PathBuf::from("python") + } + }) +} + +fn runtime_available() -> bool { + let available = Command::new(python()) + .args([ + "-c", + "import dynwinrt; assert hasattr(dynwinrt.DynWinRTValue, 'is_released')", + ]) + .output() + .is_ok_and(|output| output.status.success()); + assert!( + available || std::env::var("DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME").as_deref() != Ok("1"), + "the released implementation probe requires the current Python binding" + ); + if !available { + eprintln!("Skipping the released implementation probe; set DYNWINRT_TEST_PYTHON."); + } + available +} + +/// `Contoso.ISource` returns a struct with an Object field, and an Object +/// through an out parameter plus the result. +fn source() -> (InterfaceMeta, TypeMeta) { + let holder = TypeMeta::Struct { + namespace: "Contoso".into(), + name: "Holder".into(), + fields: vec![FieldMeta { + name: "Item".into(), + typ: TypeMeta::Object, + }], + }; + let interface = InterfaceMeta { + namespace: "Contoso".into(), + name: "ISource".into(), + iid: "3d1f0b7e-5c2a-4e8b-9f6d-1a2b3c4d5e6f".into(), + methods: vec![ + MethodMeta { + name: "GetHolder".into(), + vtable_index: 6, + return_type: Some(holder.clone()), + ..Default::default() + }, + MethodMeta { + name: "GetPair".into(), + vtable_index: 7, + params: vec![ParamMeta { + name: "first".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }], + return_type: Some(TypeMeta::Object), + ..Default::default() + }, + ], + ..Default::default() + }; + (interface, holder) +} + +#[test] +fn generated_implementation_results_reject_released_references() { + let (interface, holder) = source(); + let context = python::PythonProjectionContext::packaged([ + interface.type_identity(), + holder.type_identity(), + ]) + .unwrap(); + let source_module = context.implementation_module_for_interface(&interface); + let holder_module = context.implementation_module_for_type(&holder); + let generated = python::generate_interface(&context, &interface); + // Reference results, including struct fields, share the helper that keeps + // a released value released instead of substituting a fresh null. + assert!( + generated.contains("s.set_object(0, _implementation_reference(value.item, "), + "{generated}" + ); + let runtime = python::generate_runtime_support_module(); + assert!( + runtime.contains(" return raw if raw.is_null() else raw.cast(iid)\n"), + "{runtime}" + ); + assert!( + !runtime.contains("null_value() if raw.is_null()"), + "{runtime}" + ); + if !runtime_available() { + return; + } + + let fixture = Fixture::new(); + let package = fixture.0.join("pyviews"); + fs::create_dir_all(&package).unwrap(); + for (name, source) in [ + ("__init__.py".to_string(), String::new()), + ("_runtime.py".to_string(), runtime), + ( + "_runtime.pyi".to_string(), + python_stub::generate_runtime_support_stub(), + ), + (format!("{source_module}.py"), generated), + ( + format!("{holder_module}.py"), + python::generate_struct(&context, &holder).unwrap(), + ), + ] { + fs::write(package.join(name), source).unwrap(); + } + fs::write( + fixture.0.join("probe.py"), + format!( + r#" +import importlib +import sys +import dynwinrt as dw + +ISource = importlib.import_module("pyviews.{source_module}").ISource +Holder = importlib.import_module("pyviews.{holder_module}").Holder +PYTHON_EXCEPTION = -1594998779 +RELEASED = ( + "has been released (its projected_lifetime_scope() exited, or release_projected() / " + "DynWinRTValue.release() was called) and can no longer be used." +) +state = {{"item": None, "pair": (None, None)}} + + +class Handlers: + def get_holder(self): + return Holder(item=state["item"]) + + def get_pair(self): + first, result = state["pair"] + return {{"first": first, "result": result}} + + +sys.unraisablehook = lambda _args: None +with dw.RoApartment(), dw.projected_lifetime_scope(): + live = dw.DynWinRTValue.activation_factory("Windows.Foundation.Uri") + + def released(): + value = live.cast(dw.WinGUID.parse("af86e2e0-b12d-4c6a-9c5a-d7aa65101e90")) + value.release() + return value + + with ISource.implement(Handlers()) as impl: + view = impl.value + # Real nulls and live values still cross every result path. An Object + # struct field reads back as a raw value, so a null one is is_null(). + assert view.get_holder().item.is_null() + state["item"] = live + assert not view.get_holder().item.is_null() + state["pair"] = (None, live) + view.get_pair() + state["pair"] = (live, None) + view.get_pair() + for call, update, slot in ( + (view.get_holder, {{"item": released()}}, "field 0 of DynWinRTStruct.set_object()"), + (view.get_pair, {{"item": None, "pair": (released(), None)}}, "output 0 of implementation callback"), + (view.get_pair, {{"pair": (None, released())}}, "output 1 of implementation callback"), + ): + state.update(update) + try: + call() + except OSError as error: + assert error.winerror == PYTHON_EXCEPTION, error + else: + raise AssertionError(f"{{slot}}: a released reference was returned") + message = impl.take_error() + assert f"This WinRT object ({{slot}}) {{RELEASED}}" in message, message + live.release() +print("released-implementation-ok") +"# + ), + ) + .unwrap(); + let output = Command::new(python()) + .args(["-B", "probe.py"]) + .current_dir(&fixture.0) + .output() + .unwrap(); + assert!( + output.status.success() + && String::from_utf8_lossy(&output.stdout).contains("released-implementation-ok"), + "{}\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); +} diff --git a/tools/dynwinrt-codegen/tests/snapshots/data_writer_py/data_writer.py b/tools/dynwinrt-codegen/tests/snapshots/data_writer_py/data_writer.py index 42b7b420..050fcba2 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/data_writer_py/data_writer.py +++ b/tools/dynwinrt-codegen/tests/snapshots/data_writer_py/data_writer.py @@ -10,7 +10,7 @@ _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _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_as_interface, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, ) @@ -252,7 +252,7 @@ def __exit__(self, _exc_type, _exc_value, _traceback): return False def as_interface(self, interface_class): - return interface_class.from_value(self._obj) + return _dynwinrt_as_interface(self._obj, interface_class) class IClosable: @@ -283,7 +283,7 @@ def from_value(cls, obj: DynWinRTValue) -> 'IClosable': return cls._from_native(obj.cast(IID_IClosable)) def as_interface(self, interface_class): - return interface_class.from_value(self._obj) + return _dynwinrt_as_interface(self._obj, interface_class) 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 2937caea..484ad085 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 @@ -10,7 +10,7 @@ _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _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_as_interface, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, ) @@ -66,7 +66,7 @@ def from_value(cls, obj: DynWinRTValue) -> 'IIterator_IWwwFormUrlDecoderEntry': return cls._from_native(obj.cast(IID_IIterator_IWwwFormUrlDecoderEntry)) def as_interface(self, interface_class): - return interface_class.from_value(self._obj) + return _dynwinrt_as_interface(self._obj, interface_class) @_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 d1b2576a..1de16db4 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_stringable.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/i_stringable.py @@ -10,7 +10,7 @@ _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _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_as_interface, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, ) @@ -113,7 +113,7 @@ def from_value(cls, obj: DynWinRTValue) -> 'IStringable': return cls._from_native(obj.cast(IID_IStringable)) def as_interface(self, interface_class): - return interface_class.from_value(self._obj) + return _dynwinrt_as_interface(self._obj, interface_class) 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 64c0fed9..c343c075 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 @@ -10,7 +10,7 @@ _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _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_as_interface, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, ) @@ -122,7 +122,7 @@ def from_value(cls, obj: DynWinRTValue) -> 'IUriRuntimeClassWithAbsoluteCanonica return cls._from_native(obj.cast(IID_IUriRuntimeClassWithAbsoluteCanonicalUri)) def as_interface(self, interface_class): - return interface_class.from_value(self._obj) + return _dynwinrt_as_interface(self._obj, interface_class) @_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 d255ba1d..ed4851fa 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 @@ -10,7 +10,7 @@ _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _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_as_interface, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, ) @@ -122,7 +122,7 @@ def from_value(cls, obj: DynWinRTValue) -> 'IWwwFormUrlDecoderEntry': return cls._from_native(obj.cast(IID_IWwwFormUrlDecoderEntry)) def as_interface(self, interface_class): - return interface_class.from_value(self._obj) + return _dynwinrt_as_interface(self._obj, interface_class) @_property diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri_py/uri.py b/tools/dynwinrt-codegen/tests/snapshots/uri_py/uri.py index 70d15aa3..83ae20d3 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri_py/uri.py +++ b/tools/dynwinrt-codegen/tests/snapshots/uri_py/uri.py @@ -10,7 +10,7 @@ _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _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_as_interface, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, ) @@ -214,7 +214,7 @@ def __repr__(self) -> str: return f'{type(self).__name__}({self.__str__()!r})' def as_interface(self, interface_class): - return interface_class.from_value(self._obj) + return _dynwinrt_as_interface(self._obj, interface_class) class IUriRuntimeClassWithAbsoluteCanonicalUri: @@ -245,7 +245,7 @@ def from_value(cls, obj: DynWinRTValue) -> 'IUriRuntimeClassWithAbsoluteCanonica return cls._from_native(obj.cast(IID_IUriRuntimeClassWithAbsoluteCanonicalUri)) def as_interface(self, interface_class): - return interface_class.from_value(self._obj) + return _dynwinrt_as_interface(self._obj, interface_class) @_property def absolute_canonical_uri(self) -> str: @@ -284,7 +284,7 @@ def from_value(cls, obj: DynWinRTValue) -> 'IStringable': return cls._from_native(obj.cast(IID_IStringable)) def as_interface(self, interface_class): - return interface_class.from_value(self._obj) + return _dynwinrt_as_interface(self._obj, interface_class) 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 f9baae09..1d714c6f 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 @@ -10,7 +10,7 @@ _dynwinrt_datetime_to_ticks, _dynwinrt_delegate, _dynwinrt_enum, _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_as_interface, _dynwinrt_cache_projected, _dynwinrt_projected_from_native, _dynwinrt_symbol, _dynwinrt_track_projected, _dynwinrt_uuid, _dynwinrt_vector, _dynwinrt_wrap_values, ) @@ -110,7 +110,7 @@ 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.cast(IID_IIterable_IWwwFormUrlDecoderEntry), [])) def as_interface(self, interface_class): - return interface_class.from_value(self._obj) + return _dynwinrt_as_interface(self._obj, interface_class) class IVectorView_IWwwFormUrlDecoderEntry(_WinRTSequenceMixin): @@ -141,7 +141,7 @@ def from_value(cls, obj: DynWinRTValue) -> 'IVectorView_IWwwFormUrlDecoderEntry' return cls._from_native(obj.cast(IID_IVectorView_IWwwFormUrlDecoderEntry)) def as_interface(self, interface_class): - return interface_class.from_value(self._obj) + return _dynwinrt_as_interface(self._obj, interface_class) @_property def size(self) -> int: @@ -187,7 +187,7 @@ def from_value(cls, obj: DynWinRTValue) -> 'IIterable_IWwwFormUrlDecoderEntry': return cls._from_native(obj.cast(IID_IIterable_IWwwFormUrlDecoderEntry)) def as_interface(self, interface_class): - return interface_class.from_value(self._obj) + return _dynwinrt_as_interface(self._obj, interface_class) 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, []))