Skip to content
Closed
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
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ output, runtime behavior, and the `@microsoft/dynwinrt` root API.
- GUID: `WinGUID.parse('...')`
- Method call: `method_handle.invoke(obj, [args])` → returns single `DynWinRTValue`
- `IReference<T>` values project as native values plus `None`; generated `IReference_*` wrappers remain accepted for input compatibility
- `Object` (IInspectable) positions convert only through `to_winrt_object` / `from_winrt_object` (`bindings/py/src/object_value.rs`, experimental): boxed `IPropertyValue` payloads become Python values, tagged (`dynwinrt.UInt32`, …) where the plain write rule would change their type; other objects stay the same `DynWinRTValue`. Codegen emits them only via `py_to_winrt_object` / `py_from_winrt_object` in `python/signature.rs`

### Common Issues
- `test_initialize` is `#[ignore]` — requires `WINAPPSDK_BOOTSTRAP_DLL_PATH` env var
Expand Down
5 changes: 4 additions & 1 deletion bindings/js/__test__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1486,7 +1486,10 @@ test('explicitly unbox WinRT property values without changing raw objects', (t)
const dateTime = DynWinRtStruct.create(DynWinRtType.structType('Windows.Foundation.DateTime', [DynWinRtType.i64()]))
dateTime.setI64(0, 0n)
const unsupported = staticsType.methodByName('CreateDateTime').invoke(factory, [dateTime.toValue()])
t.throws(() => unboxObject(unsupported), { message: /Unsupported WinRT IPropertyValue type/ })
// The core now models DateTime; JavaScript keeps its exact pre-existing error.
t.throws(() => unboxObject(unsupported), {
message: '0x80004001: Unsupported WinRT IPropertyValue type: 14 (0x80004001)',
})

const keyType = DynWinRtType.hstring()
const propertyMap = DynWinRtValue.createMap(
Expand Down
25 changes: 25 additions & 0 deletions bindings/js/src/property_value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ fn null_unknown<'env>(env: Env) -> napi::Result<Unknown<'env>> {
unsafe { Unknown::from_napi_value(env.raw(), value) }
}

/// JavaScript does not project these PropertyTypes yet. Report them with the
/// exact error the core raised before it modeled every PropertyType, so the
/// JavaScript contract stays unchanged.
fn unsupported_property_type(property_type: windows::Foundation::PropertyType) -> napi::Error {
let error = dynwinrt::Error::WindowsError(windows::core::Error::new(
windows::core::HRESULT(0x80004001_u32 as i32),
format!("Unsupported WinRT IPropertyValue type: {}", property_type.0),
));
napi::Error::from_reason(error.message())
}

fn property_value_to_javascript<'env>(
env: Env,
value: dynwinrt::PropertyValueData,
Expand Down Expand Up @@ -104,6 +115,17 @@ fn property_value_to_javascript<'env>(
.map(property_value_guid)
.collect::<Vec<_>>(),
),
value @ (PropertyValueData::DateTime(_)
| PropertyValueData::TimeSpan(_)
| PropertyValueData::Point(_)
| PropertyValueData::Size(_)
| PropertyValueData::Rect(_)
| PropertyValueData::InspectableArray(_)
| PropertyValueData::DateTimeArray(_)
| PropertyValueData::TimeSpanArray(_)
| PropertyValueData::PointArray(_)
| PropertyValueData::SizeArray(_)
| PropertyValueData::RectArray(_)) => Err(unsupported_property_type(value.property_type())),
}
}

Expand All @@ -129,6 +151,9 @@ pub fn unbox_object<'env>(env: Env, value: Unknown<'env>) -> napi::Result<Unknow
{
dynwinrt::PropertyValueUnboxResult::Null => null_unknown(env),
dynwinrt::PropertyValueUnboxResult::NotPropertyValue => Ok(value),
dynwinrt::PropertyValueUnboxResult::Unsupported(property_type) => {
Err(unsupported_property_type(property_type))
}
dynwinrt::PropertyValueUnboxResult::Value(value) => {
property_value_to_javascript(env, value)
}
Expand Down
79 changes: 64 additions & 15 deletions bindings/py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,27 +207,76 @@ produce a wrapper unless the input actually implements that default interface.
Incompatible types raise the ordinary WinRT `OSError`. Static-only metadata
classes with no instance surface are not projection targets.

### Explicit boxed-value unboxing
### WinRT `Object` values: automatic boxing and unboxing (experimental)

Generic WinRT `Object`/`IInspectable` results remain raw `DynWinRTValue`
instances. Use `unbox_object()` only where the application expects a boxed
`Windows.Foundation.IPropertyValue`, such as values from
`DeviceInformation.properties`:
> **Experimental prototype.** This section describes the draft automatic
> boxing model under evaluation; the design, names and rules may change.

Every metadata position typed `Object` (`IInspectable`) — parameters, property
setters and getters, returns, out parameters, collection keys, values and
elements, arrays, async results, event arguments, and the inputs and outputs of
Python-implemented interfaces — converts through one pair of functions:
`to_winrt_object(value)` on the way in and `from_winrt_object(value)` on the
way out. Values boxed as `Windows.Foundation.IPropertyValue` therefore read and
write as ordinary Python values:

```python
from dynwinrt import unbox_object
from dynwinrt import UInt32

properties = PropertySet()
properties["count"] = 5 # boxed as Int32
properties["tags"] = ["a", "b"] # boxed as StringArray
properties["id"] = UInt32(7) # boxed as UInt32
assert properties["count"] == 5
assert dict(properties) == {"count": 5, "tags": ["a", "b"], "id": 7}

raw = device_information.properties["System.Devices.DeviceInstanceId"]
instance_id = unbox_object(raw)
size = file_properties["System.Size"] # dynwinrt.UInt64(11)
copy["System.Size"] = size # written back as UInt64, not Int32
```

The helper borrows its argument. It maps supported numeric, Boolean, string,
character, GUID, and corresponding array property types to Python values;
64-bit integers use `int`, GUIDs use `uuid.UUID`, and `UInt8Array` uses `bytes`.
`None` stays `None`. If the object does not implement `IPropertyValue`, the
exact same Python object is returned, so identity and later projection remain
intact. Unsupported property types (including `DateTime`, `TimeSpan`, geometry,
inspectable, and other types) and native getter failures raise an exception.
Writing (`to_winrt_object`) applies these rules in order:

| Python value | WinRT value |
|---|---|
| `None` | null |
| `DynWinRTValue` object, projected wrapper | passed through unchanged (wrappers as `_obj`) |
| `DynWinRTValue` holding a primitive, string, GUID, enum or foundation struct | boxed by its exact kind |
| `bool` | `Boolean` |
| `dynwinrt.UInt8` … `dynwinrt.Char16` tag | exactly that `PropertyType` |
| generated WinRT enum member | `IReference<Enum>` (as C#) |
| `dynwinrt.Point/Size/Rect`, generated `Windows.Foundation.Point/Size/Rect` | `Point` / `Size` / `Rect` |
| other `int` (including unmarked `IntEnum`) | `Int32`, else `Int64`, else `UInt64`, else `OverflowError` |
| `float` | `Double` |
| `str` | `String` |
| timezone-aware `datetime` / `timedelta` / `uuid.UUID` | `DateTime` / `TimeSpan` / `Guid` (naive `datetime` raises `ValueError`) |
| `bytes`, `bytearray`, `memoryview` | `UInt8Array` |
| `dynwinrt.<Type>Array` | that array type |
| `list` / `tuple` | inferred array: homogeneous items give the matching array; plain ints the smallest of `Int32Array`/`Int64Array`/`UInt64Array`; ints mixed with floats `DoubleArray`; plain numbers adopt a single numeric tag present; anything else `InspectableArray` (items converted one by one, `None` as null); empty sequences raise `TypeError` |
| anything else | `TypeError` listing the supported types |

Reading (`from_winrt_object`) returns `None` for null, the same `DynWinRTValue`
for objects that are not boxed values, and otherwise:

| `PropertyType` | Python value |
|---|---|
| `Boolean`, `Int32`, `Double`, `String`, `Guid` | `bool`, `int`, `float`, `str`, `uuid.UUID` |
| `UInt8`, `Int16`, `UInt16`, `UInt32`, `Int64`, `UInt64`, `Single`, `Char16` | tagged `dynwinrt.UInt8` … `dynwinrt.Char16` (`int`/`float`/`str` subclasses) |
| `DateTime`, `TimeSpan` | timezone-aware UTC `datetime`, `timedelta` (microsecond resolution) |
| `Point`, `Size`, `Rect` | immutable `dynwinrt.Point`, `dynwinrt.Size`, `dynwinrt.Rect` |
| `UInt8Array` | `bytes` |
| other arrays | `dynwinrt.<Type>Array` list subclasses with tagged elements; `InspectableArray` items are converted recursively |
| `Empty`, `Inspectable`, `OtherType`, `OtherTypeArray`, unrepresentable values | the same `DynWinRTValue` (never an exception) |

A value that is read and written back keeps its `PropertyType` (`DateTime` and
`TimeSpan` keep microseconds). Tags behave like the plain value for `==`,
`hash`, `str`, `format`, `json` and `pickle`; only `repr` shows the tag, and
arithmetic returns plain numbers. Boxes have value semantics: a write creates a
new box, so the COM identity of a boxed value is not preserved; every other
object keeps its identity and still works with `project_as()`.
`unbox_object()` is the same function as `from_winrt_object()`: it is
idempotent and returns plain Python values unchanged. Generated annotations use
`dynwinrt.WinRTObjectValue | None` for outputs and
`dynwinrt.WinRTObjectInput | None` for inputs.

Use `wrapper.as_interface(InterfaceClass)` when converting an existing
wrapper to an interface view. Use `InterfaceClass.from_value(raw)` for a raw
Expand Down
195 changes: 177 additions & 18 deletions bindings/py/dynwinrt.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from collections.abc import Coroutine
from typing import Any, Awaitable, Callable, Generic, List, Literal, Mapping, Optional, Protocol, Sequence, TypeVar, Union, final, overload
from datetime import datetime, timedelta
from typing import Any, Awaitable, Callable, ClassVar, Generic, List, Literal, Mapping, Optional, Protocol, Sequence, SupportsFloat, SupportsIndex, TypeAlias, TypeVar, Union, final, overload
from typing import Self
from uuid import UUID

_T = TypeVar("_T", covariant=True)
Expand Down Expand Up @@ -49,7 +51,44 @@ __all__ = [
"projected_lifetime_scope",
"project_as",
"release_projected",
"to_winrt_object",
"from_winrt_object",
"unbox_object",
"WinRTScalar",
"UInt8",
"Int16",
"UInt16",
"Int32",
"UInt32",
"Int64",
"UInt64",
"Single",
"Double",
"Char16",
"WinRTArray",
"Int16Array",
"UInt16Array",
"Int32Array",
"UInt32Array",
"Int64Array",
"UInt64Array",
"SingleArray",
"DoubleArray",
"Char16Array",
"BooleanArray",
"StringArray",
"InspectableArray",
"DateTimeArray",
"TimeSpanArray",
"GuidArray",
"PointArray",
"SizeArray",
"RectArray",
"Point",
"Size",
"Rect",
"WinRTObjectValue",
"WinRTObjectInput",
"init_winappsdk",
"ro_initialize",
"ro_uninitialize",
Expand Down Expand Up @@ -123,26 +162,146 @@ def project_as(

def release_projected(value: object) -> None: ...

_UnboxedPropertyValue = Union[
bool,
int,
float,
str,
UUID,
bytes,
List[int],
List[float],
List[bool],
List[str],
List[UUID],
]
# ----------------------------------------------------------------------
# WinRT Object value model (see python/dynwinrt/_values.py)
# ----------------------------------------------------------------------

class WinRTScalar:
property_type: ClassVar[str]

class UInt8(WinRTScalar, int):
def __new__(cls, value: SupportsIndex = ..., /) -> Self: ...

class Int16(WinRTScalar, int):
def __new__(cls, value: SupportsIndex = ..., /) -> Self: ...

class UInt16(WinRTScalar, int):
def __new__(cls, value: SupportsIndex = ..., /) -> Self: ...

class Int32(WinRTScalar, int):
def __new__(cls, value: SupportsIndex = ..., /) -> Self: ...

class UInt32(WinRTScalar, int):
def __new__(cls, value: SupportsIndex = ..., /) -> Self: ...

class Int64(WinRTScalar, int):
def __new__(cls, value: SupportsIndex = ..., /) -> Self: ...

class UInt64(WinRTScalar, int):
def __new__(cls, value: SupportsIndex = ..., /) -> Self: ...

class Single(WinRTScalar, float):
def __new__(cls, value: SupportsFloat | SupportsIndex = ..., /) -> Self: ...

class Double(WinRTScalar, float):
def __new__(cls, value: SupportsFloat | SupportsIndex = ..., /) -> Self: ...

class Char16(WinRTScalar, str):
def __new__(cls, value: str, /) -> Self: ...

_ArrayItem = TypeVar("_ArrayItem")

class WinRTArray(list[_ArrayItem]):
property_type: ClassVar[str]
def copy(self) -> Self: ...

class Int16Array(WinRTArray[int]): ...
class UInt16Array(WinRTArray[int]): ...
class Int32Array(WinRTArray[int]): ...
class UInt32Array(WinRTArray[int]): ...
class Int64Array(WinRTArray[int]): ...
class UInt64Array(WinRTArray[int]): ...
class SingleArray(WinRTArray[float]): ...
class DoubleArray(WinRTArray[float]): ...
class Char16Array(WinRTArray[str]): ...
class BooleanArray(WinRTArray[bool]): ...
class StringArray(WinRTArray[str]): ...
class InspectableArray(WinRTArray["WinRTObjectInput | None"]): ...
class DateTimeArray(WinRTArray[datetime]): ...
class TimeSpanArray(WinRTArray[timedelta]): ...
class GuidArray(WinRTArray[UUID]): ...
class PointArray(WinRTArray["Point | _WinRTPointLike"]): ...
class SizeArray(WinRTArray["Size | _WinRTSizeLike"]): ...
class RectArray(WinRTArray["Rect | _WinRTRectLike"]): ...

class Point:
__match_args__ = ("x", "y")
property_type: ClassVar[str]
def __init__(self, x: float = ..., y: float = ...) -> None: ...
@property
def x(self) -> float: ...
@property
def y(self) -> float: ...
def __hash__(self) -> int: ...

class Size:
__match_args__ = ("width", "height")
property_type: ClassVar[str]
def __init__(self, width: float = ..., height: float = ...) -> None: ...
@property
def width(self) -> float: ...
@property
def height(self) -> float: ...
def __hash__(self) -> int: ...

class Rect:
__match_args__ = ("x", "y", "width", "height")
property_type: ClassVar[str]
def __init__(
self, x: float = ..., y: float = ..., width: float = ..., height: float = ...
) -> None: ...
@property
def x(self) -> float: ...
@property
def y(self) -> float: ...
@property
def width(self) -> float: ...
@property
def height(self) -> float: ...
def __hash__(self) -> int: ...

class _WinRTObjectWrapper(Protocol):
@property
def _obj(self) -> DynWinRTValue: ...

# Generated windows.foundation Point/Size/Rect structs.
class _WinRTPointLike(Protocol):
x: float
y: float

class _WinRTSizeLike(Protocol):
width: float
height: float

class _WinRTRectLike(Protocol):
x: float
y: float
width: float
height: float

# Values read from WinRT Object positions. Generated signatures add `| None`.
WinRTObjectValue: TypeAlias = (
bool | int | float | str | UUID | datetime | timedelta | bytes
| Point | Size | Rect | list[Any] | DynWinRTValue
)
# Values accepted by WinRT Object positions besides None; generated enums are
# int subclasses.
WinRTObjectInput: TypeAlias = (
WinRTObjectValue | _WinRTObjectWrapper | tuple[Any, ...] | bytearray | memoryview
| _WinRTPointLike | _WinRTSizeLike | _WinRTRectLike
)

_Passthrough = TypeVar("_Passthrough")

def to_winrt_object(value: WinRTObjectInput | None, /) -> DynWinRTValue: ...
@overload
def from_winrt_object(value: None, /) -> None: ...
@overload
def unbox_object(value: None) -> None: ...
def from_winrt_object(value: DynWinRTValue, /) -> WinRTObjectValue | None: ...
@overload
def unbox_object(
value: "DynWinRTValue",
) -> Union[_UnboxedPropertyValue, "DynWinRTValue", None]: ...
def from_winrt_object(value: _Passthrough, /) -> _Passthrough: ...

unbox_object = from_winrt_object


@final
Expand Down
Loading
Loading