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
39 changes: 38 additions & 1 deletion bindings/py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,7 +505,44 @@ with RoApartment(RO_INIT_SINGLETHREADED):
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.
`ro_uninitialize()` call on the same thread. `ro_uninitialize()` rejects calls
without a matching `dynwinrt.ro_initialize()` on that thread, including calls
intended to balance an initialization made by another library.

Do not close the final managed apartment from a synchronous native-to-Python
callback (for example, a `PropertySet.map_changed` handler). `close()`,
`__exit__()`, and `ro_uninitialize()` raise `RuntimeError` **before**
`RoUninitialize` in that situation. A named `RoApartment` remains active; wait
for both the callback and its outer native call to return, release any retained
native callback values, then retry `apartment.close()` on the owner thread.
Non-final nested initializations may still be balanced inside the callback.
`ro_uninitialize()` cannot consume a `RoApartment` initialization.

If a final `RoApartment` is dropped during a callback (including an unnamed
`with RoApartment():` whose `__exit__` failed), its initialization stays in a
same-thread pending lease rather than being uninitialized inside the native
stack. After the native call returns, recover the lease explicitly:

```python
apartment = RoApartment.recover_pending() # on the original OS thread
# Release any remaining native PropertySet / callback references first.
apartment.close()
```

`recover_pending()` raises if called inside a callback or if no lease is
pending on that thread; calling it from another thread cannot consume the
owner's lease. It does not close or release anything automatically. Retain a
named apartment when possible so `.close()` can simply be retried. An implicit
drop without an earlier failed close emits a native stderr diagnostic.

An apartment is bound to the OS thread on which `RoApartment()` was created.
Calling `__enter__()`, `close()`, or `__exit__()` on another thread raises
`RuntimeError` without changing its state; retry on the creating thread.
If its last Python reference is instead dropped on a different thread, native
uninitialization is **not** attempted there: a native stderr diagnostic identifies
the pending lease, which the creating thread can explicitly retrieve using
`RoApartment.recover_pending()` and close after native references are released.
Do not depend on Python garbage collection to close an apartment.

WinRT is never initialized implicitly. A call on a thread without an apartment
raises `OSError` with `CO_E_NOTINITIALIZED` in `error.winerror`; its message
Expand Down
2 changes: 2 additions & 0 deletions bindings/py/dynwinrt.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ class RoApartment:
self, exc_type: object, exc_value: object, traceback: object
) -> Literal[False]: ...
def close(self) -> None: ...
@staticmethod
def recover_pending() -> RoApartment: ...
def __repr__(self) -> str: ...

@final
Expand Down
3 changes: 2 additions & 1 deletion bindings/py/src/async_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::sync::{Arc, Mutex, MutexGuard};
use crate::errors::{
map_dynwinrt_error, map_dynwinrt_error_with_context, map_windows_error_with_context,
};
use crate::runtime::DynWinRTValue;
use crate::runtime::{DynWinRTValue, NativeCallbackGuard};
use pyo3::exceptions::{PyRuntimeError, PyTypeError};
use pyo3::prelude::*;
use pyo3::types::PyList;
Expand Down Expand Up @@ -870,6 +870,7 @@ impl DynWinRTAsyncWithProgress {
let weak_dispatcher = Arc::downgrade(&dispatcher);

let progress_callback: dynwinrt::ProgressCallback = Box::new(move |value| {
let _callback_guard = NativeCallbackGuard::enter();
Python::attach(|py| {
let Some(dispatcher) = weak_dispatcher.upgrade() else {
return;
Expand Down
5 changes: 3 additions & 2 deletions bindings/py/src/implementation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ use windows::core::{Error, HRESULT};

use crate::errors::map_windows_error;
use crate::runtime::{
DynWinRTMethodSig, DynWinRTType, DynWinRTValue, PYWINRT_E_UNRAISABLE_PYTHON_EXCEPTION, WinGUID,
native_outputs, wrap_python_callback_context,
DynWinRTMethodSig, DynWinRTType, DynWinRTValue, NativeCallbackGuard,
PYWINRT_E_UNRAISABLE_PYTHON_EXCEPTION, WinGUID, native_outputs, wrap_python_callback_context,
};

const RO_E_CLOSED: HRESULT = HRESULT(0x80000013_u32 as i32);
Expand Down Expand Up @@ -192,6 +192,7 @@ impl CallbackCell {
if self.interpreter.stopping.load(Ordering::Acquire) {
return Err(closed_error());
}
let _callback_guard = NativeCallbackGuard::enter();
Python::try_attach(|py| {
if self.interpreter.stopping.load(Ordering::Acquire) {
return Err(closed_error());
Expand Down
Loading
Loading