Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"
```
Expand All @@ -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

Expand Down
37 changes: 26 additions & 11 deletions bindings/py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,25 +231,33 @@ 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

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.
Expand Down Expand Up @@ -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
Expand Down
13 changes: 12 additions & 1 deletion bindings/py/dynwinrt.pyi
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -24,6 +24,8 @@ class _DynWinRTRuntimeClass(_DynWinRTProjectableClass): ...
__all__ = [
"WinAppSDKContext",
"RoApartment",
"RO_INIT_SINGLETHREADED",
"RO_INIT_MULTITHREADED",
"WinGUID",
"DynWinRTType",
"DynWinRTMethodSig",
Expand Down Expand Up @@ -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__(
Expand Down Expand Up @@ -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: ...
Expand Down
6 changes: 3 additions & 3 deletions bindings/py/src/async_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,))
}

Expand Down
12 changes: 6 additions & 6 deletions bindings/py/src/delegate_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<dynwinrt::WinRTValue>>;
Expand Down Expand Up @@ -52,16 +52,16 @@ impl DynWinRTDelegateMethod {
args: Vec<DynWinRTValue>,
) -> PyResult<Vec<DynWinRTValue>> {
// 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::<Vec<_>>();
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)
}
}
Expand Down
121 changes: 120 additions & 1 deletion bindings/py/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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::<String>::None, error.code().0))
}
Expand All @@ -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::<PyOSError>());
(
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}");
});
}
}
14 changes: 5 additions & 9 deletions bindings/py/src/implementation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -206,15 +206,11 @@ impl CallbackCell {
let result = (|| -> PyResult<Vec<dynwinrt::WinRTValue>> {
let inputs = args
.iter()
.map(|value| Py::new(py, DynWinRTValue(value.clone())))
.map(|value| Py::new(py, DynWinRTValue::new(value.clone())))
.collect::<PyResult<Vec<_>>>()?;
let inputs = PyList::new(py, inputs)?;
let outputs = callback.call1(py, (interface_index, vtable_index, inputs))?;
Ok(outputs
.extract::<Vec<DynWinRTValue>>(py)?
.into_iter()
.map(|value| value.0)
.collect())
native_outputs("implementation callback", outputs.extract(py)?)
})();
result.map_err(|error| {
let message = format!(
Expand Down Expand Up @@ -353,7 +349,7 @@ impl DynWinRTImplementation {
self.with_native(|native| {
native
.to_value()
.map(DynWinRTValue)
.map(DynWinRTValue::new)
.map_err(map_windows_error)
})
}
Expand Down Expand Up @@ -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
Expand Down
Loading
Loading