Skip to content

Windows: dotnet.invoke cannot marshal projected WinRT objects, and awaits Task-returning methods on the caller thread #11616

Open

Description

@aleclarson

Two verified limitations in the pinned Windows runtime's .NET bridge, blocking any JS-side use of APIs that need managed interop — most concretely, WinRT pickers that require IInitializeWithWindow owner HWND setup.

Real-world impact: Octane Xplat's files.pick/pickMultiple/export on Windows needed a fully managed workaround (app-compiled helper + DispatcherQueue enqueue) because neither projected WinRT objects nor async picker calls can cross the bridge. Verified on packaged MSIX 1.0.0.224–227, runtime @nativescript/windows@0.1.0-alpha.144.

1. Projected WinRT objects marshal as System.String, never RCWs

Every candidate carrier handed to dotnet.invoke arrives in managed code as System.String:

  • the picker proxy itself → System.String
  • picker.handle, interop.handleOf(picker).handle → Cannot convert object to primitive value
  • dotnet.wrap(picker), interop.pointer(picker) → System.String

ServiceInterop.Describe (a managed helper taking object) reports System.String|IsInitWithWindow=False, so neither a ComImport IInitializeWithWindow cast nor WinRT.Interop.InitializeWithWindow can run — every call fails InvalidCastException: Failed to create a CCW for object of type 'System.String'. The bridge's CoerceBin has a WinRtRef path (tag 0x0A → Marshal.GetObjectForIUnknown) but no JS value appears to serialize to it.

2. Task-returning methods are awaited on the caller thread

dotnet.invoke on a Task<T>-returning method blocks the JS/UI thread until the task completes (AwaitIfTask in Bridge.BinaryDispatch.cs). For a modal picker this is a guaranteed deadlock: the dialog needs the same thread's message pump. Verified deterministically — a Task<string> PickSingle helper hung the packaged app on two consecutive runs; enqueueing the same work on the captured DispatcherQueue and polling a string state works.

Workaround shipped: an app-compiled ServiceInterop helper does construct + InitializeWithWindow + await in-process on the dispatcher; JS only passes long/string and polls PickState. Packaged evidence: a real FileOpenPicker/FileSavePicker dialog opened, accepted a typed-path selection, and committed a save — while every JS-side picker invoke still rejects WinRT async operation failed.

Activity

  1. self-assigned this
    on Nov 5, 2026
  2. triniwiz commented on Nov 6, 2026

    @triniwiz
    Member
    1. Projected objects arriving as System.String: confirmed and fixed. WinRT wrappers never exposed __native_ptr, so they fell through to toString(). The next runtime release passes WinRT objects as WinRT refs, and the bridge projects them through C#/WinRT when it's loaded. A FileOpenPicker now reaches managed code as a FileOpenPicker, and WinRT.Interop.InitializeWithWindow.Initialize(picker, hwnd) works on it.

    The runtime also already calls IInitializeWithWindow.Initialize when you construct a picker, but it used the console window, which a packaged app doesn't have. It now uses the app's top-level window, so new Windows.Storage.Pickers.FileOpenPicker() from JS should no longer need managed owner setup.

    1. Task methods blocking the caller: this was fixed after the version you're on (0.1.0-alpha.144, 2026-09-28). Task/ValueTask results are no longer awaited on the calling thread: dotnet.invoke returns right away with an awaitable result, so await it (or use NSWinRT.toPromise(result)) instead of polling. Please move to the beta dist-tag (@nativescript/windows@1.0.0-beta.18 or later).

    try updating core to the pkg.pr.new 298c76d and the runtime to @nativescript/windows@1.0.0-beta.19

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions