Skip to content

[Experimental] Automatic boxing and unboxing for WinRT Object values in Python - #192

Closed
leileizhang (lei9444) wants to merge 3 commits into
mainfrom
lei9444-python-auto-boxing-prototype
Closed

leileizhang (lei9444) wants to merge 3 commits into
mainfrom
lei9444-python-auto-boxing-prototype

Conversation

@lei9444

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

Copy link
Copy Markdown
Contributor

Experimental draft for evaluation. Not for merge.

Problem

WinRT Object (IInspectable) values are awkward in the Python projection:

  • Writing a plain value fails. ps["n"] = 5 raises TypeError, so callers have to write PropertyValue.create_int32(5).
  • Reading gives back an opaque DynWinRTValue that has to be passed through dynwinrt.unbox_object(). DateTime, TimeSpan, Point, Size and Rect cannot be unboxed at all.
  • A read-then-write round trip loses the WinRT type. For example, a UInt32 comes back as a plain int and is written back as Int32.

This draft tries full automatic boxing and unboxing, so the effect can be judged.

Key changes

  • One core model in crates/dynwinrt/src/property_value.rs. It covers 37 of the 41 PropertyTypes, unboxes without raising, and adds a new box_property_value.
  • One Rust conversion layer in bindings/py/src/object_value.rs: to_winrt_object / from_winrt_object. unbox_object becomes the same function.
  • Public value types in dynwinrt so that read-then-write keeps the exact WinRT type:
    • type-tagged numbers (UInt32, Int64, Single, …);
    • typed arrays;
    • immutable Point, Size and Rect.
  • Codegen routes every Object position through one hook per direction: params, returns, properties, collection elements, async results and event args.
  • JS gets a compile-only adapter; its behavior is unchanged.
  • Measured overhead: Object reads are about 2% slower (1000 Int32 values) and about 10% slower for mixed values. Writes are about 2.7× faster than the old create_int32 path.

How it looks

# before
ps["n"] = PropertyValue.create_int32(5)   # ps["n"] = 5 raised TypeError
n = dynwinrt.unbox_object(ps["n"])        # DateTime/Point/... raised

# after
ps["n"] = 5
ps["when"] = datetime.now(timezone.utc)
ps["n"]                                   # 5
size = props["System.Size"]               # dynwinrt.UInt64(11)
copy["System.Size"] = size                # written back as UInt64

Open questions

  • Should mixed lists be boxed as InspectableArray (current behavior), or raise TypeError?
  • A plain int defaults to Int32. APIs that expect another numeric type need an explicit tag.
  • Should the generated Point, Size and Rect alias the runtime value types?
  • Behavior change: PropertyValue.create_* results are now unboxed automatically, so IPropertyValue.from_value(PropertyValue.create_x(...)) no longer works.

EXPERIMENTAL prototype for design evaluation; not for merge.

Core (crates/dynwinrt/src/property_value.rs) now models every
Windows.Foundation.PropertyType that has a payload (37 of 41): adds
DateTime, TimeSpan, Point, Size, Rect, their arrays and InspectableArray.
unbox_property_value classifies payload-less boxes as Unsupported instead
of failing, and box_property_value creates system PropertyValue boxes.
The JS binding keeps its exact previous behavior through a compile-only
adapter.

The Python binding gains one conversion layer (src/object_value.rs):
to_winrt_object / from_winrt_object, with unbox_object as the same
function. A public value model (python/dynwinrt/_values.py) adds
PropertyType-tagged scalars (UInt8 ... Char16), typed arrays, immutable
Point/Size/Rect and the WinRTObjectValue / WinRTObjectInput aliases, so
values read from Object positions write back with their exact WinRT type.

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

EXPERIMENTAL prototype for design evaluation; not for merge.

python/signature.rs owns the single codegen hook per direction,
py_to_winrt_object / py_from_winrt_object. Every TypeMeta::Object arm uses
them: parameters and setters, collection and array elements, returns,
getters, out params, async results, event arguments, and the inputs and
outputs of Python-implemented interfaces. Object no longer shares its
return arm with unknown runtime classes and interfaces.

type_helpers::py_object_annotation is the single annotation hook; Object
positions read WinRTObjectValue | None and accept WinRTObjectInput | None
through the shared py_optional_type. The aliases are collision-safe support
symbols. Generated enums carry an IReference<Enum> boxing marker and
generated Windows.Foundation Point/Size/Rect carry a PropertyType marker.
Composable factory outers keep their raw object-reference projection.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
EXPERIMENTAL prototype for design evaluation; not for merge.

test_object_values_generated.py generates Windows SDK bindings and runs
each scenario in a fresh process: a PropertyValue.create_X round trip for
every PropertyType, plain-value writes, PropertySet/ValueSet/StringMap
mapping behavior, identity and project_as, generated enums and geometry
structs, DeviceInformation and StorageFile property stores, and
DeviceWatcher.EnumerationCompleted arguments typed Object.

samples/python/object-boxing-demo shows the natural code, times the
conversion, and is a mypy --strict / pyright typing consumer.

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

Copy link
Copy Markdown

Mixed-language test coverage

Workflow status: ✅ Passed

Layer Lines Functions Branches/regions
Rust, including native .pyd/.node 86.51% 81.75% 86.17% regions
Python aggregate 71.73% n/a 38.73% branches
Python runtime 98.53% n/a 95.59% branches
Generated Python WinRT projections 70.07% n/a 26.96% branches
Generated Python WinRT implementations 72.32% n/a 46.92% 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

@lei9444

Copy link
Copy Markdown
Contributor Author

Closing this experiment. We will keep Object as native IInspectable by default and add Python value conversion only at boundaries that explicitly opt into value semantics:

  1. Complete explicit conversion first: to_winrt_object(value, property_type=...), WinRT type tags for ambiguous numbers (no magnitude-based guessing), full unbox_object type coverage, and an optional type-preserving unbox.
  2. Then add an opt-in value view for Object-valued maps.
  3. Only widen the scope based on real usage.

This draft stays as a reference implementation; its core pieces will be reused.

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