Skip to content

Clarify common Python runtime errors and name apartment constants - #188

Open
leileizhang (lei9444) wants to merge 12 commits into
mainfrom
lei9444-clearer-python-runtime-errors
Open

leileizhang (lei9444) wants to merge 12 commits into
mainfrom
lei9444-clearer-python-runtime-errors

Conversation

@lei9444

@lei9444 leileizhang (lei9444) commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Several common mistakes in the Python binding produced errors that were confusing, or no error at all:

  • Using a projected object after its projected_lifetime_scope() exited raised RuntimeError: invoke() requires an Object value. A released object passed as an argument was silently marshalled as null.
  • Calling WinRT without initializing an apartment raised a bare CO_E_NOTINITIALIZED OSError.
  • obj.as_interface(SomeRuntimeClass) failed with an unrelated AttributeError: ... has no attribute 'from_value'.
  • Docs and samples used the magic number RoApartment(1).

Key changes

  • DynWinRTValue now tracks an explicit lifecycle (live or released), exposed as DynWinRTValue.is_released(). One validator handles receivers, arguments, nested elements and implementation outputs, so released values raise a clear RuntimeError instead of being used or passed as null.
  • errors.rs gains an HRESULT hint table. CO_E_NOTINITIALIZED still raises the same OSError, winerror and errno, and the message now explains how to initialize WinRT.
  • Generated as_interface() goes through one _runtime.py helper. For runtime classes it raises TypeError pointing to dynwinrt.project_as().
  • New dynwinrt.RO_INIT_SINGLETHREADED and RO_INIT_MULTITHREADED constants, backed by a single shared apartment mapping. Docs and samples are updated.

Notes

  • Exception types are unchanged, except that passing a runtime class to as_interface() now raises TypeError instead of AttributeError.
  • The WinUI samples use the new constants, so they need a runtime that includes this change.

Behavior-preserving groundwork for clearer runtime errors:

- Route every DynWinRTValue construction through DynWinRTValue::new so
  the value can carry lifecycle state without touching each call site
  again.
- Resolve apartment_type through one ro_init_type() mapping and a named
  default, shared by RoApartment and ro_initialize, instead of two
  inline 0/1 matches.
- Emit as_interface() from a single codegen template instead of three
  copies. Generated output is unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A projected object used after its projected_lifetime_scope() exited, or
after release_projected(), failed with "RuntimeError: invoke() requires
an Object value". release() replaced the payload with WinRTValue::Null,
so a released value was indistinguishable from a WinRT null, and each
entry point validated its receiver with its own match and message.

DynWinRTValue now records its lifecycle (Live/Released). release() is
the only transition. Every native entry point validates its receiver
through one classifier, which reports a released value, a WinRT null, or
the actual value kind:

- receiver(): Object only (invoke, invoke_all, invoke_detached, call,
  as_raw, identity_raw)
- com_receiver(): the existing as_object() policy, which also accepts
  async operations (fast-path getters, call_0, call_1, activate, and
  composed factories)
- query(): cast and delegate Invoke

Released values passed as arguments to invoke, invoke_all,
invoke_detached, the composed factories, call, call_1 and delegate
Invoke raise the same error, naming the argument position, instead of
marshalling as null. Exception types are unchanged (RuntimeError).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
RoApartment(1) and ro_initialize(1) relied on magic numbers. Export
dynwinrt.RO_INIT_SINGLETHREADED (0) and dynwinrt.RO_INIT_MULTITHREADED
(1), sourced from the same Win32 RO_INIT_TYPE constants the apartment
mapping uses, and declare them as Final in the stub.

Docs and samples now use RoApartment() for the default multithreaded
apartment and RoApartment(RO_INIT_SINGLETHREADED) for WinUI. Accepted
values and their mapping are unchanged.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Calling WinRT on a thread without an apartment surfaced only the
localized CoInitialize text. windows_error() now consults one
HRESULT_HINTS table and appends its guidance to the Windows description.
Its first entry covers CO_E_NOTINITIALIZED and points to
`with dynwinrt.RoApartment():` or ro_initialize(RO_INIT_MULTITHREADED).

OSError, winerror and errno are unchanged, and WinRT is still never
initialized implicitly. The async runtime's CO_E_NOTINITIALIZED check
inspects the native error before mapping, so it is unaffected.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
obj.as_interface(StorageFile) failed with "AttributeError: type object
'StorageFile' has no attribute 'from_value'" because each generated
as_interface() duck-typed on from_value.

Every generated as_interface() now delegates to one codegen-owned
_runtime.py helper, _dynwinrt_as_interface. It validates the declared
_DynWinRTProjector contract (from_value) and raises TypeError. For
generated runtime classes, the message points to
dynwinrt.project_as(obj, StorageFile). Valid targets behave exactly as
before, and stubs are unchanged.

The helper is imported through the shared import list, so the seven
Python snapshots change in their import header and as_interface()
bodies.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Mixed-language test coverage

Workflow status: ✅ Passed

Layer Lines Functions Branches/regions
Rust, including native .pyd/.node 86.52% 81.8% 86.18% regions
Python aggregate 71.1% n/a 38% branches
Python runtime 99.12% n/a 97.37% branches
Generated Python WinRT projections 70.09% n/a 27.28% branches
Generated Python WinRT implementations 72.35% n/a 47.05% branches
JavaScript aggregate 21.91% 25.18% 57.61% branches
JavaScript runtime 44.27% 45.76% 78.99% branches
Generated WinRT projections 22.8% 18.7% 54.84% branches
Generated WinRT implementations 45.99% 59.19% 60.97% branches
Generated Classic COM projections 11.88% 23.97% 53.4% branches

View workflow run and download full HTML/LCOV/XML reports

Generated modules imported _dynwinrt_as_interface unconditionally. A
metadata declaration with the same module name, such as a struct named
_dynwinrt_as_interface, shadowed the import, and as_interface() then
called that type instead of casting.

The helper is now a PythonSupportSymbol like _DynWinRTObject. The
module symbol allocator yields it to metadata declarations and their
allocated roles, import_line() binds it through support_symbol_import(),
and every generated as_interface() calls support_symbol_reference(), for
example `_dynwinrt_as_interface as _dynwinrt_as_interface_2`. Output
without a collision is unchanged.

A generation test builds metadata that declares the colliding struct
next to Audit.Widget. It checks the aliased import and every
as_interface() body in standalone and packaged output, and casts a real
Windows.Foundation.Uri through the generated modules when the runtime is
available.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The released error blamed only projected_lifetime_scope() and
release_projected(), but the public DynWinRTValue.release() reaches the
same state. `raw.release(); raw.cast(iid)` pointed at two operations
that never happened.

Receiver and argument errors now share one accurate reason: "has been
released (its projected_lifetime_scope() exited, or release_projected() /
DynWinRTValue.release() was called) and can no longer be used." Tests
and the E2E check assert the full text and cover a direct
DynWinRTValue.release() on raw and projected values.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
_dynwinrt_as_interface raised TypeError for every target without
from_value. Previously None, an int, an arbitrary class, a runtime-class
instance, or a generated interface without an IID raised AttributeError
from the from_value lookup, so they changed exception type too.

Only a generated runtime class (a type carrying a runtime-class or
projectable-class marker, without from_value) now raises the TypeError
pointing to dynwinrt.project_as(). Every other target calls
interface_class.from_value(native) exactly as before. The codegen test
pins the helper shape. The generated-module probe and the E2E check
assert TypeError for runtime classes and AttributeError for the other
targets.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Released wrappers inside Python sequences, mappings, arrays or struct
fields still reached native code as WinRT null. Generated conversions
pass each element's _obj through DynWinRTValue.create_vector,
create_map, DynWinRTArray.from_values / from_object_values and
DynWinRTStruct.set_object, which copied the payload without a lifecycle
check. For example, obj.method([released_view]) for an IIterable<IFoo>
parameter sent a vector containing null.

These constructors now validate every input through the same check as
native_arguments(): vector and array elements, map keys and values, and
struct object fields. The error names the operation and the slot, e.g.
"This WinRT object (element 1 of DynWinRTArray.from_values()) has been
released (...)". unbox_object() now raises for a released value
instead of returning None, so released and WinRT null stay distinct.
Live values and real WinRT nulls are unaffected.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The "argument" case called live.equals(released). Its runtime-class
parameter is cast before invocation, so the error came from cast()'s
receiver check, and the case would still pass without the
released-argument check.

The check now pins the exact message for each use:
- the runtime-class parameter keeps its receiver-style error;
- PropertyValue.create_inspectable(released) passes an Object parameter
  without a cast and must report "(argument 0 of invoke())";
- create_inspectable_array([live, released]) must report "(element 1 of
  DynWinRTArray.from_values())".

With the argument check reverted, the check fails: native code
receives null and returns E_INVALIDARG.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
The generated _implementation_reference() returned a fresh
DynWinRTValue.null_value() whenever raw.is_null(). is_null() is also
true after release(), so a released wrapper became a live null and
passed every lifecycle check. This reached native code as null through:
- arguments to generated delegate callables inside implementations,
  e.g. a stored event handler invoked with a released sender;
- array elements and struct object fields in implementation results;
- direct return and out values, which the binding extracted without a
  lifecycle check;
- DynWinRTValue.box_reference(released, t);
- generated struct IReference field setters, whose
  _dynwinrt_unbox_reference() mapped a released wrapper to None.

Fixes:
- _implementation_reference() returns a null or released raw value
  itself, so a real null stays a live null and a released value stays
  released.
- Implementation callback outputs pass through native_outputs(). A
  released output fails the native call like any other handler error:
  PYWINRT_E_UNRAISABLE_PYTHON_EXCEPTION, sys.unraisablehook, and
  take_error(), e.g. "(output 1 of implementation callback)".
- box_reference() checks its input.
- DynWinRTValue.is_released() tells a released value from a WinRT null.
  _dynwinrt_unbox_reference() calls it through getattr, so generated
  code still works with runtimes that lack it. A released wrapper then
  raises when its value is read.

Tests cover each path and that real nulls still work:
- pytest: is_released(), box_reference(), and an implementation that
  returns released out and result values;
- a new implementation E2E case: stored delegate, array element, direct
  result, and the struct field helper;
- the E2E IReference check: property setter and unbox helper;
- a codegen test that runs a generated implementation whose struct
  field, out parameter and result are released.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
python_released_implementation_test runs a generated implementation
natively only when the Python runtime is installed. The test-rust job
has no runtime, so CI ran only its static assertions. Add it to the
e2e-runtime step that already runs implementation_naming_test against
the freshly built wheel with DYNWINRT_REQUIRE_IMPLEMENTATION_RUNTIME=1.
There, a missing runtime fails the test instead of skipping the probe.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant