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
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
- Generated `Object` positions stay raw `DynWinRTValue`s; `unbox_object(raw, preserve_type=False)` and `to_winrt_object(value, property_type=None)` convert boxed values explicitly, with tags, typed arrays, `Point`/`Size`/`Rect` and `PropertyType` in `dynwinrt.values` (never guess a WinRT type)

### 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
78 changes: 65 additions & 13 deletions bindings/py/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,27 +207,79 @@ 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
### Explicit boxing and unboxing of `Object` values

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`:
Generic WinRT `Object`/`IInspectable` results and parameters remain raw
`DynWinRTValue` instances: an `Object` may be any runtime object, not only a
boxed value. Convert explicitly where the application expects a boxed
`Windows.Foundation.IPropertyValue`, such as the values of
`DeviceInformation.properties` or a `PropertySet`:

```python
from dynwinrt import unbox_object
from datetime import datetime, timezone

from dynwinrt import to_winrt_object, unbox_object
from dynwinrt.values import PropertyType, UInt32

raw = device_information.properties["System.Devices.DeviceInstanceId"]
instance_id = unbox_object(raw)

properties.insert("count", to_winrt_object(5)) # Int32
properties.insert("port", to_winrt_object(UInt32(8080))) # UInt32
properties.insert("size", to_winrt_object(2**40, PropertyType.UInt64))
properties.insert("when", to_winrt_object(datetime.now(timezone.utc)))
```

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.
`unbox_object(value, *, preserve_type=False)` borrows its argument. `None` and
WinRT null return `None`. If the object does not implement `IPropertyValue`,
the exact same Python object is returned, so identity and later projection
remain intact. Boxed values map to Python values:

- numbers, Boolean, string and character values use `int`, `float`, `bool`
and `str`; GUIDs use `uuid.UUID`;
- `DateTime` uses a timezone-aware UTC `datetime` and `TimeSpan` a
`timedelta`, truncated to microseconds like other positions; a `DateTime`
beyond `datetime.max` raises `OverflowError`;
- `Point`, `Size` and `Rect` use the immutable `dynwinrt.values` types;
- `UInt8Array` uses `bytes`, other arrays use lists, and `InspectableArray`
elements are unboxed recursively by the same rules.

`Empty`, `Inspectable`, `OtherType` and `OtherTypeArray` boxes, including
nested ones, and native getter failures raise `OSError`.

With `preserve_type=True`, values whose plain Python form would box as a
different type come back as `dynwinrt.values` tags (`UInt8`, `Int16`,
`UInt16`, `UInt32`, `Int64`, `UInt64`, `Single`, `Char16`), and every array
except `bytes` comes back as a typed array such as `UInt32Array` or
`InspectableArray`. `to_winrt_object()` then restores the exact
`PropertyType` and value. It creates a new box, so the original box's COM
identity is not preserved.

`to_winrt_object(value, property_type=None)` returns a `DynWinRTValue` and
never guesses a WinRT type:

- `None` becomes WinRT null; a `DynWinRTValue` holding an object is returned
unchanged, and a projected wrapper returns its native `DynWinRTValue`;
- `bool`, `float`, `str`, a timezone-aware `datetime`, `timedelta`,
`uuid.UUID`/`WinGUID`, `bytes`/`bytearray`/`memoryview`, and
`dynwinrt.values` tags, typed arrays and `Point`/`Size`/`Rect` box as their
exact type; a naive `datetime` raises `ValueError`;
- a plain `int` boxes as `Int32` only; other values raise `OverflowError`;
- a list or tuple boxes only when all elements have the same unambiguous
type, for example all `str` (`StringArray`) or all in-range plain `int`
(`Int32Array`); empty and mixed lists raise `TypeError`;
- enum members raise `TypeError` unless an integer `property_type` is given.
Boxing a WinRT enum as `IReference<T>` is not supported yet.

`property_type` accepts a `dynwinrt.values.PropertyType` member or its
integer value and converts the value to exactly that type, validating its
range. `InspectableArray` elements, including `None`, follow the rules above.

The `dynwinrt.values` types behave like the plain value: tags compare, hash,
format, pickle and serialize to JSON like `int`, `float` or `str`, and
arithmetic returns plain numbers. They live in `dynwinrt.values` rather than
`dynwinrt`, so they never shadow generated `Windows.Foundation` names such as
`Point` or `PropertyType`.

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

Expand Down Expand Up @@ -52,6 +53,7 @@ __all__ = [
"project_as",
"release_projected",
"unbox_object",
"to_winrt_object",
"init_winappsdk",
"ro_initialize",
"ro_uninitialize",
Expand Down Expand Up @@ -131,27 +133,67 @@ def project_as(

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

# Shapes of dynwinrt.values.Point, Size and Rect. This stub also serves as a
# single-file module stub, so it cannot import the dynwinrt.values submodule.
class _WinRTPointValue(Protocol):
@property
def x(self) -> float: ...
@property
def y(self) -> float: ...

class _WinRTSizeValue(Protocol):
@property
def width(self) -> float: ...
@property
def height(self) -> float: ...

class _WinRTRectValue(Protocol):
@property
def x(self) -> float: ...
@property
def y(self) -> float: ...
@property
def width(self) -> float: ...
@property
def height(self) -> float: ...

_UnboxedPropertyValue = Union[
bool,
int,
float,
str,
UUID,
bytes,
datetime,
timedelta,
_WinRTPointValue,
_WinRTSizeValue,
_WinRTRectValue,
List[int],
List[float],
List[bool],
List[str],
List[UUID],
List[datetime],
List[timedelta],
List[_WinRTPointValue],
List[_WinRTSizeValue],
List[_WinRTRectValue],
# InspectableArray elements are unboxed recursively.
List[Any],
]

@overload
def unbox_object(value: None) -> None: ...
def unbox_object(value: None, *, preserve_type: bool = ...) -> None: ...
@overload
def unbox_object(
value: "DynWinRTValue",
value: "DynWinRTValue", *, preserve_type: bool = ...
) -> Union[_UnboxedPropertyValue, "DynWinRTValue", None]: ...

def to_winrt_object(
value: object, property_type: Optional[int] = ...
) -> "DynWinRTValue": ...


@final
class WinGUID:
Expand Down
1 change: 1 addition & 0 deletions bindings/py/python/dynwinrt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from . import dynwinrt as dynwinrt
from .dynwinrt import *
from . import values as values

__doc__ = dynwinrt.__doc__
__all__ = dynwinrt.__all__
Loading
Loading