Implement WinRT interfaces in Node.js and Python

September 10, 2026 ยท View on GitHub

Generated WinRT interfaces can create standalone native objects backed by synchronous JavaScript or Python handlers. The objects support IInspectable, QueryInterface, and real native vtable calls. They do not need WinUI composition, a package identity, or background-task registration.

Use a runtime and code generator from the same version. Generate the complete interfaces you need, including their required interfaces. The generated handler declaration describes exactly which methods and results to provide.

Construction and typed interface views

Interface.implement(handlers) creates a native implementation instance and returns a management handle: DynWinRtImplementationHandle<Interface> in Node or DynWinRTImplementationHandle[Interface] in Python. Its read-only .value is the generated primary interface, not the handler object or a raw native value. Calling it traverses the ordinary native WinRT ABI.

The primary view is created lazily and cached: repeated .value accesses return the same object. release() releases both that view, if created, and the handle's native-owner reference. dispose() also disconnects future callbacks object-wide. After either operation .value raises; it never recreates a released view. The common pattern needs no separate view-release call. Lifecycle methods live on the handle, so interface members named close, release, or dispose do not collide.

Node.js

Generate IStringable and IClosable:

cargo run -p dynwinrt-codegen -- generate `
  --class-name "Windows.Foundation.IStringable,Windows.Foundation.IClosable" `
  --output .\generated
import { roInitialize } from '@microsoft/dynwinrt';
import { IStringable } from './generated/index.js';

roInitialize(1);
const impl = IStringable.implement({ toString: () => 'Implemented in JavaScript' });
try {
    console.log(impl.value.toString()); // Native call, then reverse callback.
} finally {
    impl.dispose();
}

Use the generated IStringableHandlers and other handler declarations to type handler objects in TypeScript. Handlers must be actual synchronous functions; an async function or a returned Promise/thenable is not a WinRT synchronous result.

Python

Generate the same interfaces with --lang py:

cargo run -p dynwinrt-codegen -- generate `
  --class-name "Windows.Foundation.IStringable,Windows.Foundation.IClosable" `
  --lang py --output .\generated
from dynwinrt import RoApartment
from generated.windows.foundation import IStringable

class TextHandler:
    def to_string(self) -> str:
        return "Implemented in Python"

with RoApartment(1), IStringable.implement(TextHandler()) as impl:
    print(impl.value.to_string())  # Crosses the native vtable in both directions.

Python handler objects may use ordinary bound methods. Generated .pyi declarations describe their protocol. Coroutine functions and returned awaitables are rejected, not run in the background or awaited on the callback thread. The implementation owner's context-manager exit performs dispose, not merely reference release.

Python implementations currently require the main CPython interpreter; subinterpreter-owned callbacks are rejected at construction. Captured contextvars follow the existing Python callback-context convention.

Multiple interfaces and advanced views

Routine multi-interface construction keeps the primary type inferred:

const impl = IBackgroundTask.implement(taskHandlers, {
    interfaces: [[IStringable, textHandlers]],
});
impl.value.run(instanceView);
with IBackgroundTask.implement(handler, interfaces=[(IStringable, text_handler)]) as impl:
    impl.value.run(instance_view)

Each entry explicitly supplies a generated interface and its handlers. Complete metadata validation, duplicate IIDs, and required-interface checks happen before native publication. Existing positional .implementation(...) descriptors are still supported, and may be preferable for reusable heterogeneous compositions. Python statically checks heterogeneous convenience lists for interfaces in the same generated package. For heterogeneous interfaces from different generated packages, use typed positional .implementation(...) descriptors; runtime list acceptance is unchanged. Homogeneous lists retain their generic typing.

Interface.fromImplementation(impl) / Interface.from_implementation(impl) remains the advanced independent-view path; it accepts either a typed handle or the low-level native owner. Each call owns a separate QI reference that is released with releaseProjected(view) / release_projected(view). Independent views survive handle release(), but not object-wide dispose(). Python views remain tracked by their projection lifetime scope; the ordinary from_value cache behavior is unchanged. Scope exit can release a handle's cached primary view; accessing it does not silently create a new one.

Supported contracts

Support is determined from the entire interface, not only from the method an application intends to call. Missing handlers, unresolved required interfaces, duplicate views, invalid slots, or unsupported signatures fail before a native object is published. Validation retains original metadata reference qualifiers: an input scalar BYREF, a by-reference logical return, or a by-reference struct field is not silently treated as a value. Normal scalar outputs and WinRT pass/fill/receive array contracts remain supported.

ContractSupport
Ordinary methodsTyped inputs, void results, return values, and multiple outputs
PropertiesExplicit synchronous getter and setter handlers
EventsAdd/remove handlers, native delegate values, and registration tokens
Primitive valuesBooleans, integer widths, floating-point values, Char16, enums, HRESULT values
GUID and HSTRINGManaged values with native layout/string ownership
WinRT structsKnown layouts, including supported nested fields
Object/interface/class referencesNullable managed references; output QueryInterface checks the declared IID
Closed generic and nullable-reference valuesEstablished value conversions, not generic implementations
ArraysInput, receive, and fill arrays of supported elements
Native async objectsPass-through where the existing projection supports the declared type

Handler accessor names and output field names are given in the generated handler type. Do not infer them from a property's displayed name or a language object's shape. When a method has multiple logical outputs, the generated result has named fields; every field must be present and valid. The ABI order is explicit output parameters in metadata order followed by the method's logical return value.

Node handlers with no logical outputs use normal TypeScript void semantics: synchronous expression results such as closes++, an assignment, or Map.delete(...) are ignored. Promises and thenables are still rejected, and thrown exceptions still fail the native call. Python void handlers return None.

Handler, delegate-callable, and result helpers have names allocated with their owning interface identity. If a helper would shadow a real metadata type or another helper, codegen qualifies the helper name; real types keep their public identity. Use factory type inference or the generated exports instead of constructing helper names by concatenating strings.

A fill-array handler receives a capacity, not a writable borrowed native buffer. Return exactly that many elements. Receive-array storage and element references are allocated and transferred by the runtime. An invalid element, wrong length, missing result field, or failed output QueryInterface fails the whole call before any output ownership is published.

Required interfaces have their own handler descriptors and native views. For example, IMemoryBufferReference requires IClosable. Supply its descriptor alongside the memory-reference handlers; do not merge an IClosable method into the memory-reference vtable.

For its Closed event, the implementation handlers are getCapacity, addClosed, and removeClosed in Node, or get_capacity, add_closed, and remove_closed in Python. The add handler receives an owned, typed callable native delegate. Retain it to deliver the event by calling handler(senderView, null) / handler(sender_view, None). Return a token { value: 1n } in Node or EventRegistrationToken(value=1) in Python. The remove handler receives that token and should remove and release its stored delegate. These calls invoke the delegate's native slot 3; they are not direct calls to the original subscriber's language function. In Python, these received native-delegate callables take positional-only arguments, matching their generated Protocols. Ordinary projected methods keep their existing keyword-argument support. An implementation-side handler that rejects registration without retaining the received delegate can release that visible owned view with releaseProjected(handler) before throwing, for deterministic cleanup instead of waiting for JavaScript GC. This does not transfer responsibility for the generator's hidden subscription temporaries to application code.

Generated Node subscriptions release their creator and temporary delegate references after the native add method returns. The native event source owns the subscription reference until removal. Keeping an unsubscribe function reachable after removal does not keep the native delegate alive. A caller using DynWinRtDelegate.create directly must similarly release its delegate owner and its separate toValue() reference when they are no longer needed. Neither release disconnects a native-retained reference.

Existing metadata visibility rules remain in force: an ExclusiveTo static or factory interface is an implementation detail of its runtime class, not a new public implementation entrypoint. The array examples in the generated integration suite use public IDataWriter, IPropertyValue, and IDataReader interfaces rather than exposing private factory interfaces.

Ownership, disposal, and errors

OperationMeaning
toValue() / to_value()Create an owned low-level native value for explicit interop
Public typed view conversionCreate an owned typed view without consuming the owner
releaseProjected(view) / release_projected(view)Release that view without disposing the controller or other views
impl.valueStable, handle-managed primary interface view
release() on the typed handleRelease its primary view and native-owner reference, without disconnecting other native owners
disconnect()Disconnect callbacks for the whole implemented object
dispose()Disconnect the whole object, then release the owner's reference
takeError() / take_error()Retrieve and clear the last contextual native callback error

Native references keep handlers alive after the original wrapper leaves scope. An independent view or native consumer's reference is not disconnected merely because the owner was released or collected. Low-level values and typed views have independent reference ownership and must follow their normal projected lifetime rules. JavaScript's releaseProjected is exported by the generated package; Python's release_projected is exported by dynwinrt.

Explicit disposal is different: every later method call through any retained view fails with RO_E_CLOSED. A callback that had already entered before disposal may finish and return its validated outputs. Its native object, dispatch storage, and callback roots remain alive until it returns.

If a handler retains its own owner or native view, the resulting native/language cycle may require explicit disposal. Python GC integration is conservative when external native references exist: collecting a still-native-owned handler would be incorrect. Avoid strong self/native cycles where practical, and use a try/finally or the Python owner context manager for deterministic disconnection. Environment/interpreter shutdown also closes callback state; it does not leave callable pointers into a destroyed language runtime.

Python can collect an owner-only callback cycle when that owner holds the sole native reference. Native aliases or native weak-reference use are treated conservatively and may require explicit disposal. Native reference cleanup can run on another thread without granting permission to invoke handlers there. This includes a handler that captures its typed handle after the primary view has been created: that view is another native reference. Use dispose() or Python's handle context manager to break such cycles deterministically. When a handle is collected without a capture cycle, its native owner and unreferenced primary view are collected normally; independently retained native references remain callable.

Language exceptions are reported as real failing HRESULTs. The owner's error accessor provides interface/slot context; Python also reports the original exception using its native-callback unraisable-exception convention. Outputs are initialized on failure, and prepared strings, references, and buffers are released if any output conversion fails. Ordinary JavaScript handler exceptions map to E_FAIL (0x80004005); ordinary Python handler exceptions map to 0xA0EE4005. In particular, throwing NotImplementedError does not request native E_NOTIMPL, and arbitrary properties attached to a JavaScript Error do not override its HRESULT.

Threading

Create and call implementations on their owner thread. They are explicitly non-agile, including when created in an MTA. Native identity and reference-count operations remain thread-safe, but a method callback from a foreign thread fails with RPC_E_WRONG_THREAD before entering JavaScript or Python.

This release does not provide apartment dispatch, cross-thread callback marshaling, implicit Promise/coroutine conversion, or event-loop blocking to wait for a synchronous result. Existing native WinRT async objects and deferrals may be used explicitly through their own APIs.

IBackgroundTask and deployment

IBackgroundTask uses the same general implementation engine as IStringable, IClosable, and other supported interfaces. A controlled native consumer can call its Run method with a real IBackgroundTaskInstance interface without registering an operating-system task.

The Node sample and Python sample provide runnable task-instance fixtures and demonstrate native Run calls, nested native property callbacks, multiple interface views, and owner release.

Creating such an object does not register a background task or COM server, make a CLSID activatable, configure a Windows trigger, install a package, or grant a package identity. Windows background activation and deployment are separate future work. GetRuntimeClassName is descriptive metadata, not activation registration.

Arbitrary user-defined generic interface implementations, nested arrays, unknown native layouts, and incomplete ownership contracts are not supported. Where a signature requires a dynamic libffi closure, process restrictions on executable-memory allocation can also make construction fail.

For implementation details, see the native architecture.

The repository's focused generated/native scenarios can be run after building the matching bindings and generator:

.\tests\e2e\e2e_test.ps1 -SkipBuild -Suite implementations `
  -Lang py,ts -CargoProfile dev -Python .\bindings\py\.venv\Scripts\python.exe

They exercise real vtables, properties, native event delegates, pass/receive/ fill arrays, scalar/GUID/struct/reference outputs, named multiple outputs, independent views, exception recovery, and retention/disposal. Use -Suite standard for the existing WinRT/Classic COM compatibility scenarios.