Clarify common Python runtime errors and name apartment constants - #188
Open
leileizhang (lei9444) wants to merge 12 commits into
Open
leileizhang (lei9444) wants to merge 12 commits into
leileizhang (lei9444) wants to merge 12 commits into
Conversation
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>
Mixed-language test coverageWorkflow status: ✅ Passed
|
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Several common mistakes in the Python binding produced errors that were confusing, or no error at all:
projected_lifetime_scope()exited raisedRuntimeError: invoke() requires an Object value. A released object passed as an argument was silently marshalled as null.CO_E_NOTINITIALIZEDOSError.obj.as_interface(SomeRuntimeClass)failed with an unrelatedAttributeError: ... has no attribute 'from_value'.RoApartment(1).Key changes
DynWinRTValuenow tracks an explicit lifecycle (live or released), exposed asDynWinRTValue.is_released(). One validator handles receivers, arguments, nested elements and implementation outputs, so released values raise a clearRuntimeErrorinstead of being used or passed as null.errors.rsgains an HRESULT hint table.CO_E_NOTINITIALIZEDstill raises the sameOSError,winerroranderrno, and the message now explains how to initialize WinRT.as_interface()goes through one_runtime.pyhelper. For runtime classes it raisesTypeErrorpointing todynwinrt.project_as().dynwinrt.RO_INIT_SINGLETHREADEDandRO_INIT_MULTITHREADEDconstants, backed by a single shared apartment mapping. Docs and samples are updated.Notes
as_interface()now raisesTypeErrorinstead ofAttributeError.