From c146764f65aec5b4ffb01eedfcef3139b8160997 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Wed, 22 Jul 2026 15:11:30 +0800 Subject: [PATCH 01/28] Classic-COM vertical: runtime + classic/interop codegen + SMTC/DTM + shared napi plumbing Reorganizes the Win32/COM work into a self-contained classic-vertical that pairs the classic-COM runtime (call.rs RawPtr, classic_com.rs, signature::define_from_iunknown), classic-COM/interop codegen (codegen::com, main.rs --class-name COM path), and the shared napi plumbing (coCreateInstance, registerInterfaceUnknown, pointer, iidPointer, asPointerBigint, u64 Either fix, createTestHwnd) needed by the ITaskbarList3 / DTM / SMTC E2Es. Flat-Win32 pieces (flat_call.rs, codegen::flat, flatInvoke napi, Apis/DllImport meta) are intentionally absent from this branch and are layered back on top in reorg/flat-vertical. The classic E2Es (taskbarlist.mjs, dtm.mjs, smtc.mjs) acquire a process-owned HWND through a new tiny napi helper createTestHwnd() (delegates to CreateWindowExW via windows-rs) instead of the flatInvoke-based path used in the fully-integrated reference. A small hwnd.mjs helper module encapsulates the call. Gauntlet (green): - cargo test -p dynwinrt: 83 passed + 1 winrt_regression - cargo test -p dynwinrt-codegen: all suites green - napi build (release): OK - Node E2Es: taskbarlist.mjs / dtm.mjs / smtc.mjs PASS - tests\e2e_test.ps1 -SkipBuild: py 29/29, ts 28/28 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .gitignore | 5 +- bindings/js/e2e/DataTransferManager.d.ts | 13 + bindings/js/e2e/DataTransferManager.js | 32 + .../js/e2e/IDataTransferManagerInterop.d.ts | 16 + .../js/e2e/IDataTransferManagerInterop.js | 36 + .../ISystemMediaTransportControlsInterop.d.ts | 15 + .../ISystemMediaTransportControlsInterop.js | 32 + bindings/js/e2e/ITaskbarList3.js | 97 + .../js/e2e/SystemMediaTransportControls.d.ts | 13 + .../js/e2e/SystemMediaTransportControls.js | 32 + bindings/js/e2e/TBPFLAG.js | 8 + bindings/js/e2e/dtm.mjs | 54 + bindings/js/e2e/hwnd.mjs | 28 + bindings/js/e2e/package.json | 4 + bindings/js/e2e/smtc.mjs | 147 ++ bindings/js/e2e/taskbarlist.mjs | 83 + bindings/js/src/lib.rs | 360 ++- crates/dynwinrt/Cargo.toml | 3 + crates/dynwinrt/src/call.rs | 1 + crates/dynwinrt/src/classic_com.rs | 304 +++ crates/dynwinrt/src/lib.rs | 1 + crates/dynwinrt/src/metadata_table/arena.rs | 56 +- crates/dynwinrt/src/metadata_table/mod.rs | 13 + crates/dynwinrt/src/signature.rs | 91 + crates/dynwinrt/tests/winrt_regression.rs | 404 ++++ tools/dynwinrt-codegen/src/codegen/com.rs | 1925 +++++++++++++++++ tools/dynwinrt-codegen/src/codegen/mod.rs | 1 + tools/dynwinrt-codegen/src/main.rs | 95 +- tools/dynwinrt-codegen/src/meta.rs | 344 ++- .../DataTransferManager.d.ts | 13 + .../DataTransferManager.js | 32 + .../IDataTransferManagerInterop.d.ts | 16 + .../IDataTransferManagerInterop.js | 36 + .../itaskbarlist3/ITaskbarList3.d.ts | 38 + .../snapshots/itaskbarlist3/ITaskbarList3.js | 97 + .../snapshots/itaskbarlist3/TBPFLAG.d.ts | 8 + .../tests/snapshots/itaskbarlist3/TBPFLAG.js | 8 + .../tests/win32_com_interop_test.rs | 467 ++++ .../dynwinrt-codegen/tests/win32_com_test.rs | 641 ++++++ 39 files changed, 5545 insertions(+), 24 deletions(-) create mode 100644 bindings/js/e2e/DataTransferManager.d.ts create mode 100644 bindings/js/e2e/DataTransferManager.js create mode 100644 bindings/js/e2e/IDataTransferManagerInterop.d.ts create mode 100644 bindings/js/e2e/IDataTransferManagerInterop.js create mode 100644 bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts create mode 100644 bindings/js/e2e/ISystemMediaTransportControlsInterop.js create mode 100644 bindings/js/e2e/ITaskbarList3.js create mode 100644 bindings/js/e2e/SystemMediaTransportControls.d.ts create mode 100644 bindings/js/e2e/SystemMediaTransportControls.js create mode 100644 bindings/js/e2e/TBPFLAG.js create mode 100644 bindings/js/e2e/dtm.mjs create mode 100644 bindings/js/e2e/hwnd.mjs create mode 100644 bindings/js/e2e/package.json create mode 100644 bindings/js/e2e/smtc.mjs create mode 100644 bindings/js/e2e/taskbarlist.mjs create mode 100644 crates/dynwinrt/src/classic_com.rs create mode 100644 crates/dynwinrt/tests/winrt_regression.rs create mode 100644 tools/dynwinrt-codegen/src/codegen/com.rs create mode 100644 tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.d.ts create mode 100644 tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.js create mode 100644 tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts create mode 100644 tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js create mode 100644 tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts create mode 100644 tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js create mode 100644 tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.d.ts create mode 100644 tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.js create mode 100644 tools/dynwinrt-codegen/tests/win32_com_interop_test.rs create mode 100644 tools/dynwinrt-codegen/tests/win32_com_test.rs diff --git a/.gitignore b/.gitignore index 06356cd9..c884ca33 100644 --- a/.gitignore +++ b/.gitignore @@ -437,4 +437,7 @@ bench-electron/out/ **/build # Claude Code local settings -**/settings.local.json \ No newline at end of file +**/settings.local.json +# Generated E2E projection fixtures (regenerated by codegen; not committed to keep PRs reviewable) +bindings/js/e2e/smtc-projected/ +bindings/js/e2e/generated/ diff --git a/bindings/js/e2e/DataTransferManager.d.ts b/bindings/js/e2e/DataTransferManager.d.ts new file mode 100644 index 00000000..567a7435 --- /dev/null +++ b/bindings/js/e2e/DataTransferManager.d.ts @@ -0,0 +1,13 @@ +// Generated by dynwinrt-codegen — do not edit + +/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +export type HWND = bigint | Buffer; + +export declare class DataTransferManager { + /** Wrap an existing native COM pointer (for QueryInterface bridging). */ + static _fromNative(obj: unknown): DataTransferManager; + /** Get a `DataTransferManager` for the given HWND (projected from `Windows.ApplicationModel.DataTransfer.DataTransferManager`). */ + static getForWindow(appWindow: HWND): DataTransferManager; + /** IInspectable::GetRuntimeClassName — the projected class name. */ + get runtimeClassName(): string; +} diff --git a/bindings/js/e2e/DataTransferManager.js b/bindings/js/e2e/DataTransferManager.js new file mode 100644 index 00000000..564d1164 --- /dev/null +++ b/bindings/js/e2e/DataTransferManager.js @@ -0,0 +1,32 @@ +// Generated by dynwinrt-codegen — do not edit +import { DynWinRtType, DynWinRtMethodSig, WinGuid } from '../dist/index.js'; +import { IDataTransferManagerInterop } from './IDataTransferManagerInterop.js'; + +const IID_IInspectable = WinGuid.parse('af86e2e0-b12d-4c6a-9c5a-d7aa65101e90'); + +let _IInspectableCache; +const _IInspectable = new Proxy({}, { + get(_target, prop) { + _IInspectableCache ??= DynWinRtType.registerInterfaceUnknown('IInspectable_projected', IID_IInspectable) + .addMethod('GetIids', new DynWinRtMethodSig().addOut(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())) + .addMethod('GetRuntimeClassName', new DynWinRtMethodSig().addOut(DynWinRtType.hstring())) + .addMethod('GetTrustLevel', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type())); + const value = _IInspectableCache[prop]; + return typeof value === 'function' ? value.bind(_IInspectableCache) : value; + }, +}); + +export class DataTransferManager { + _obj; + constructor(obj) { this._obj = obj; } + static _fromNative(obj) { return new DataTransferManager(obj); } + /** Get a `DataTransferManager` for the given HWND via the IDataTransferManagerInterop interop. */ + static getForWindow(appWindow) { + const interop = IDataTransferManagerInterop.create(); + return interop.getForWindow(appWindow); + } + /** IInspectable::GetRuntimeClassName — the projected class name. */ + get runtimeClassName() { + return _IInspectable.method(4).getString(this._obj); + } +} diff --git a/bindings/js/e2e/IDataTransferManagerInterop.d.ts b/bindings/js/e2e/IDataTransferManagerInterop.d.ts new file mode 100644 index 00000000..83b9e2bc --- /dev/null +++ b/bindings/js/e2e/IDataTransferManagerInterop.d.ts @@ -0,0 +1,16 @@ +// Generated by dynwinrt-codegen — do not edit +import { DataTransferManager } from './DataTransferManager.js'; + +/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +export type HWND = bigint | Buffer; + +export declare const IID_IDataTransferManagerInterop: unknown; + +export declare class IDataTransferManagerInterop { + /** Activate the projected WinRT class and QI to the interop. */ + static create(): IDataTransferManagerInterop; + /** Wrap an existing native COM pointer (for QueryInterface bridging). */ + static _fromNative(obj: unknown): IDataTransferManagerInterop; + getForWindow(appWindow: HWND): DataTransferManager; + showShareUIForWindow(appWindow: HWND): void; +} diff --git a/bindings/js/e2e/IDataTransferManagerInterop.js b/bindings/js/e2e/IDataTransferManagerInterop.js new file mode 100644 index 00000000..c5c0f3be --- /dev/null +++ b/bindings/js/e2e/IDataTransferManagerInterop.js @@ -0,0 +1,36 @@ +// Generated by dynwinrt-codegen — do not edit +import { DynWinRtType, DynWinRtMethodSig, DynWinRtValue, WinGuid } from '../dist/index.js'; +import { DataTransferManager } from './DataTransferManager.js'; + +export const IID_IDataTransferManagerInterop = WinGuid.parse('3a3dcd6c-3eab-43dc-bcde-45671ce800c8'); +const IID_DataTransferManager_default = WinGuid.parse('a5caee9b-8708-49d1-8d36-67d25a8da00c'); + +let _IDataTransferManagerInteropCache; +const _IDataTransferManagerInterop = new Proxy({}, { + get(_target, prop) { + _IDataTransferManagerInteropCache ??= DynWinRtType.registerInterfaceUnknown('IDataTransferManagerInterop', IID_IDataTransferManagerInterop) + .addMethod('GetForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())) + .addMethod('ShowShareUIForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())); + const value = _IDataTransferManagerInteropCache[prop]; + return typeof value === 'function' ? value.bind(_IDataTransferManagerInteropCache) : value; + }, +}); + +export class IDataTransferManagerInterop { + _obj; + constructor(obj) { this._obj = obj; } + static _fromNative(obj) { return new IDataTransferManagerInterop(obj); } + /** Create a new `IDataTransferManagerInterop` by activating the `Windows.ApplicationModel.DataTransfer.DataTransferManager` factory and QI'ing to the interop. */ + static create() { + const factory = DynWinRtValue.activationFactory('Windows.ApplicationModel.DataTransfer.DataTransferManager'); + const _obj = factory.cast(IID_IDataTransferManagerInterop); + return new IDataTransferManagerInterop(_obj); + } + getForWindow(appWindow) { + const _out = _IDataTransferManagerInterop.method(3).invoke(this._obj, [DynWinRtValue.pointer(appWindow), DynWinRtValue.iidPointer(IID_DataTransferManager_default)]); + return DataTransferManager._fromNative(_out); + } + showShareUIForWindow(appWindow) { + _IDataTransferManagerInterop.method(4).invoke(this._obj, [DynWinRtValue.pointer(appWindow)]); + } +} diff --git a/bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts b/bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts new file mode 100644 index 00000000..b15b539a --- /dev/null +++ b/bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts @@ -0,0 +1,15 @@ +// Generated by dynwinrt-codegen — do not edit +import { SystemMediaTransportControls } from './SystemMediaTransportControls.js'; + +/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +export type HWND = bigint | Buffer; + +export declare const IID_ISystemMediaTransportControlsInterop: unknown; + +export declare class ISystemMediaTransportControlsInterop { + /** Activate the projected WinRT class and QI to the interop. */ + static create(): ISystemMediaTransportControlsInterop; + /** Wrap an existing native COM pointer (for QueryInterface bridging). */ + static _fromNative(obj: unknown): ISystemMediaTransportControlsInterop; + getForWindow(appWindow: HWND): SystemMediaTransportControls; +} diff --git a/bindings/js/e2e/ISystemMediaTransportControlsInterop.js b/bindings/js/e2e/ISystemMediaTransportControlsInterop.js new file mode 100644 index 00000000..01710a9f --- /dev/null +++ b/bindings/js/e2e/ISystemMediaTransportControlsInterop.js @@ -0,0 +1,32 @@ +// Generated by dynwinrt-codegen — do not edit +import { DynWinRtType, DynWinRtMethodSig, DynWinRtValue, WinGuid } from '../dist/index.js'; +import { SystemMediaTransportControls } from './SystemMediaTransportControls.js'; + +export const IID_ISystemMediaTransportControlsInterop = WinGuid.parse('ddb0472d-c911-4a1f-86d9-dc3d71a95f5a'); +const IID_SystemMediaTransportControls_default = WinGuid.parse('99fa3ff4-1742-42a6-902e-087d41f965ec'); + +let _ISystemMediaTransportControlsInteropCache; +const _ISystemMediaTransportControlsInterop = new Proxy({}, { + get(_target, prop) { + _ISystemMediaTransportControlsInteropCache ??= DynWinRtType.registerInterface('ISystemMediaTransportControlsInterop', IID_ISystemMediaTransportControlsInterop) + .addMethod('GetForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())); + const value = _ISystemMediaTransportControlsInteropCache[prop]; + return typeof value === 'function' ? value.bind(_ISystemMediaTransportControlsInteropCache) : value; + }, +}); + +export class ISystemMediaTransportControlsInterop { + _obj; + constructor(obj) { this._obj = obj; } + static _fromNative(obj) { return new ISystemMediaTransportControlsInterop(obj); } + /** Create a new `ISystemMediaTransportControlsInterop` by activating the `Windows.Media.SystemMediaTransportControls` factory and QI'ing to the interop. */ + static create() { + const factory = DynWinRtValue.activationFactory('Windows.Media.SystemMediaTransportControls'); + const _obj = factory.cast(IID_ISystemMediaTransportControlsInterop); + return new ISystemMediaTransportControlsInterop(_obj); + } + getForWindow(appWindow) { + const _out = _ISystemMediaTransportControlsInterop.method(6).invoke(this._obj, [DynWinRtValue.pointer(appWindow), DynWinRtValue.iidPointer(IID_SystemMediaTransportControls_default)]); + return SystemMediaTransportControls._fromNative(_out); + } +} diff --git a/bindings/js/e2e/ITaskbarList3.js b/bindings/js/e2e/ITaskbarList3.js new file mode 100644 index 00000000..36fd793e --- /dev/null +++ b/bindings/js/e2e/ITaskbarList3.js @@ -0,0 +1,97 @@ +// Generated by dynwinrt-codegen — do not edit +import { DynWinRtType, DynWinRtMethodSig, DynWinRtValue, WinGuid } from '../dist/index.js'; +import { TBPFLAG } from './TBPFLAG.js'; + +export const IID_ITaskbarList3 = WinGuid.parse('ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf'); + +let _ITaskbarList3Cache; +const _ITaskbarList3 = new Proxy({}, { + get(_target, prop) { + _ITaskbarList3Cache ??= DynWinRtType.registerInterfaceUnknown('ITaskbarList3', IID_ITaskbarList3) + .addMethod('HrInit', new DynWinRtMethodSig()) + .addMethod('AddTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) + .addMethod('DeleteTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) + .addMethod('ActivateTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) + .addMethod('SetActiveAlt', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) + .addMethod('MarkFullscreenWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) + .addMethod('SetProgressValue', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u64Type()).addIn(DynWinRtType.u64Type())) + .addMethod('SetProgressState', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) + .addMethod('RegisterTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) + .addMethod('UnregisterTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) + .addMethod('SetTabOrder', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) + .addMethod('SetTabActive', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type())) + .addMethod('ThumbBarAddButtons', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type()).addIn(DynWinRtType.pointer())) + .addMethod('ThumbBarUpdateButtons', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type()).addIn(DynWinRtType.pointer())) + .addMethod('ThumbBarSetImageList', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) + .addMethod('SetOverlayIcon', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) + .addMethod('SetThumbnailTooltip', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) + .addMethod('SetThumbnailClip', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())); + const value = _ITaskbarList3Cache[prop]; + return typeof value === 'function' ? value.bind(_ITaskbarList3Cache) : value; + }, +}); + +export class ITaskbarList3 { + _obj; + constructor(obj) { this._obj = obj; } + static _fromNative(obj) { return new ITaskbarList3(obj); } + /** Create a new `ITaskbarList3` via `CoCreateInstance` on `CLSID_TaskbarList`. */ + static create() { + const _obj = DynWinRtValue.coCreateInstance('56fdf344-fd6d-11d0-958a-006097c9a090', IID_ITaskbarList3); + return new ITaskbarList3(_obj); + } + hrInit() { + _ITaskbarList3.method(3).invoke(this._obj, []); + } + addTab(hwnd) { + _ITaskbarList3.method(4).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + } + deleteTab(hwnd) { + _ITaskbarList3.method(5).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + } + activateTab(hwnd) { + _ITaskbarList3.method(6).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + } + setActiveAlt(hwnd) { + _ITaskbarList3.method(7).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + } + markFullscreenWindow(hwnd, fFullscreen) { + _ITaskbarList3.method(8).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.i32(fFullscreen ? 1 : 0)]); + } + setProgressValue(hwnd, ullCompleted, ullTotal) { + _ITaskbarList3.method(9).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.u64(BigInt(ullCompleted)), DynWinRtValue.u64(BigInt(ullTotal))]); + } + setProgressState(hwnd, tbpFlags) { + _ITaskbarList3.method(10).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.i32(tbpFlags)]); + } + registerTab(tab, mDI) { + _ITaskbarList3.method(11).invoke(this._obj, [DynWinRtValue.pointer(tab), DynWinRtValue.pointer(mDI)]); + } + unregisterTab(tab) { + _ITaskbarList3.method(12).invoke(this._obj, [DynWinRtValue.pointer(tab)]); + } + setTabOrder(tab, insertBefore) { + _ITaskbarList3.method(13).invoke(this._obj, [DynWinRtValue.pointer(tab), DynWinRtValue.pointer(insertBefore)]); + } + setTabActive(tab, mDI, reserved) { + _ITaskbarList3.method(14).invoke(this._obj, [DynWinRtValue.pointer(tab), DynWinRtValue.pointer(mDI), DynWinRtValue.u32(reserved)]); + } + thumbBarAddButtons(hwnd, cButtons, pButton) { + _ITaskbarList3.method(15).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.u32(cButtons), DynWinRtValue.pointer(pButton)]); + } + thumbBarUpdateButtons(hwnd, cButtons, pButton) { + _ITaskbarList3.method(16).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.u32(cButtons), DynWinRtValue.pointer(pButton)]); + } + thumbBarSetImageList(hwnd, himl) { + _ITaskbarList3.method(17).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(himl)]); + } + setOverlayIcon(hwnd, hIcon, description) { + _ITaskbarList3.method(18).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(hIcon), DynWinRtValue.pointer(description)]); + } + setThumbnailTooltip(hwnd, tip) { + _ITaskbarList3.method(19).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(tip)]); + } + setThumbnailClip(hwnd, prcClip) { + _ITaskbarList3.method(20).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(prcClip)]); + } +} diff --git a/bindings/js/e2e/SystemMediaTransportControls.d.ts b/bindings/js/e2e/SystemMediaTransportControls.d.ts new file mode 100644 index 00000000..1eff241e --- /dev/null +++ b/bindings/js/e2e/SystemMediaTransportControls.d.ts @@ -0,0 +1,13 @@ +// Generated by dynwinrt-codegen — do not edit + +/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +export type HWND = bigint | Buffer; + +export declare class SystemMediaTransportControls { + /** Wrap an existing native COM pointer (for QueryInterface bridging). */ + static _fromNative(obj: unknown): SystemMediaTransportControls; + /** Get a `SystemMediaTransportControls` for the given HWND (projected from `Windows.Media.SystemMediaTransportControls`). */ + static getForWindow(appWindow: HWND): SystemMediaTransportControls; + /** IInspectable::GetRuntimeClassName — the projected class name. */ + get runtimeClassName(): string; +} diff --git a/bindings/js/e2e/SystemMediaTransportControls.js b/bindings/js/e2e/SystemMediaTransportControls.js new file mode 100644 index 00000000..84af0b33 --- /dev/null +++ b/bindings/js/e2e/SystemMediaTransportControls.js @@ -0,0 +1,32 @@ +// Generated by dynwinrt-codegen — do not edit +import { DynWinRtType, DynWinRtMethodSig, WinGuid } from '../dist/index.js'; +import { ISystemMediaTransportControlsInterop } from './ISystemMediaTransportControlsInterop.js'; + +const IID_IInspectable = WinGuid.parse('af86e2e0-b12d-4c6a-9c5a-d7aa65101e90'); + +let _IInspectableCache; +const _IInspectable = new Proxy({}, { + get(_target, prop) { + _IInspectableCache ??= DynWinRtType.registerInterfaceUnknown('IInspectable_projected', IID_IInspectable) + .addMethod('GetIids', new DynWinRtMethodSig().addOut(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())) + .addMethod('GetRuntimeClassName', new DynWinRtMethodSig().addOut(DynWinRtType.hstring())) + .addMethod('GetTrustLevel', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type())); + const value = _IInspectableCache[prop]; + return typeof value === 'function' ? value.bind(_IInspectableCache) : value; + }, +}); + +export class SystemMediaTransportControls { + _obj; + constructor(obj) { this._obj = obj; } + static _fromNative(obj) { return new SystemMediaTransportControls(obj); } + /** Get a `SystemMediaTransportControls` for the given HWND via the ISystemMediaTransportControlsInterop interop. */ + static getForWindow(appWindow) { + const interop = ISystemMediaTransportControlsInterop.create(); + return interop.getForWindow(appWindow); + } + /** IInspectable::GetRuntimeClassName — the projected class name. */ + get runtimeClassName() { + return _IInspectable.method(4).getString(this._obj); + } +} diff --git a/bindings/js/e2e/TBPFLAG.js b/bindings/js/e2e/TBPFLAG.js new file mode 100644 index 00000000..58af8cf8 --- /dev/null +++ b/bindings/js/e2e/TBPFLAG.js @@ -0,0 +1,8 @@ +// Generated by dynwinrt-codegen — do not edit +export const TBPFLAG = Object.freeze({ + TBPF_NOPROGRESS: 0, + TBPF_INDETERMINATE: 1, + TBPF_NORMAL: 2, + TBPF_ERROR: 4, + TBPF_PAUSED: 8, +}); diff --git a/bindings/js/e2e/dtm.mjs b/bindings/js/e2e/dtm.mjs new file mode 100644 index 00000000..d0495b7f --- /dev/null +++ b/bindings/js/e2e/dtm.mjs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// E2E: real Node.js proof that the generated natural DataTransferManager +// wrapper drives live WinRT via the *Interop* HWND pattern: +// IDataTransferManagerInterop::GetForWindow(HWND, REFIID, void**) +// The test uses ONLY the high-level generated wrapper — no low-level +// `registerInterfaceUnknown` / `coCreateInstance` / QI plumbing in the test. +// +// Run: node bindings/js/e2e/dtm.mjs + +import { DynWinRtValue } from '../dist/index.js'; +import { DataTransferManager } from './DataTransferManager.js'; +import { acquireHwndBigInt } from './hwnd.mjs'; + +function fail(msg) { + console.error(`[e2e] FAIL: ${msg}`); + process.exit(1); +} + +console.log('[e2e] step 1: acquiring a process-owned HWND via napi createTestHwnd()'); +// The classic-vertical does not bundle flat-Win32, so we obtain a +// process-owned HWND via a small napi helper (`createTestHwnd`) instead of +// `flatInvoke(user32!CreateWindowExW, ...)`. This keeps the E2E +// self-contained with respect to the classic vertical's surface area. +const hwndBig = acquireHwndBigInt(); +console.log(`[e2e] HWND → 0x${hwndBig.toString(16)}`); +if (hwndBig === 0n) fail('acquireHwndBigInt returned NULL'); + +console.log('[e2e] step 2: DataTransferManager.getForWindow(hwnd) [HIGH-LEVEL WRAPPER]'); +let dtm; +try { + dtm = DataTransferManager.getForWindow(hwndBig); +} catch (e) { + fail(`DataTransferManager.getForWindow threw: ${e && e.message ? e.message : e}`); +} + +if (dtm == null) fail('DataTransferManager.getForWindow returned null'); +console.log(`[e2e] got DataTransferManager instance = ${dtm}`); + +console.log('[e2e] step 3: MEANINGFUL — read live member `runtimeClassName` (via IInspectable::GetRuntimeClassName)'); +let name; +try { + name = dtm.runtimeClassName; +} catch (e) { + fail(`dtm.runtimeClassName threw: ${e && e.message ? e.message : e}`); +} +console.log(`[e2e] runtimeClassName = ${JSON.stringify(name)}`); + +const expected = 'Windows.ApplicationModel.DataTransfer.DataTransferManager'; +if (name !== expected) fail(`expected runtimeClassName='${expected}', got '${name}'`); + +console.log('PASS'); +process.exit(0); diff --git a/bindings/js/e2e/hwnd.mjs b/bindings/js/e2e/hwnd.mjs new file mode 100644 index 00000000..e2ff9a9c --- /dev/null +++ b/bindings/js/e2e/hwnd.mjs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// Classic-COM/interop E2E helper to obtain a process-owned Win32 HWND +// without relying on flat-Win32 codegen. +// +// Interop APIs like `IDataTransferManagerInterop::GetForWindow` and +// `ISystemMediaTransportControlsInterop::GetForWindow` require an HWND +// that is OWNED BY THE CALLING PROCESS (they return E_ACCESSDENIED for +// desktop / shell / cross-process HWNDs). This helper delegates to the +// napi `createTestHwnd()` export, which creates a hidden `WS_POPUP` +// window in the Node process using the pre-registered `STATIC` class. + +import { DynWinRtValue } from '../dist/index.js'; + +/** + * Return a valid Win32 HWND owned by the current process, as a bigint. + * Throws if window creation fails. + */ +export function acquireHwndBigInt() { + const hwnd = DynWinRtValue.createTestHwnd(); + // napi BigInt → JS bigint. + const n = typeof hwnd === 'bigint' ? hwnd : BigInt(hwnd); + if (n === 0n) { + throw new Error('acquireHwndBigInt: createTestHwnd returned 0'); + } + return n; +} diff --git a/bindings/js/e2e/package.json b/bindings/js/e2e/package.json new file mode 100644 index 00000000..96b1890a --- /dev/null +++ b/bindings/js/e2e/package.json @@ -0,0 +1,4 @@ +{ + "type": "module", + "private": true +} diff --git a/bindings/js/e2e/smtc.mjs b/bindings/js/e2e/smtc.mjs new file mode 100644 index 00000000..dc9a5942 --- /dev/null +++ b/bindings/js/e2e/smtc.mjs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// E2E: real Node.js proof that the generated natural SystemMediaTransportControls +// wrapper drives live WinRT via the IInspectable-rooted (+6) *Interop* HWND +// pattern: +// ISystemMediaTransportControlsInterop::GetForWindow(HWND, REFIID, void**) +// The test uses ONLY high-level generated wrappers — no low-level +// `registerInterface` / `coCreateInstance` / QI plumbing in the test. +// +// This is the companion of dtm.mjs. DTM proves the IUnknown-rooted (+3) +// interop; this proves the IInspectable-rooted (+6) interop AND exercises a +// real SMTC member (isPlayEnabled) to prove the returned object is a live, +// usable SystemMediaTransportControls, not just a valid IInspectable pointer. +// +// Run: node bindings/js/e2e/smtc.mjs + +import { existsSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, resolve } from 'node:path'; +import { DynWinRtValue } from '../dist/index.js'; +// Classic-COM interop wrapper: gets the SMTC pointer from an HWND. +import { ISystemMediaTransportControlsInterop } from './ISystemMediaTransportControlsInterop.js'; +import { acquireHwndBigInt } from './hwnd.mjs'; + +// The SMTC full-WinRT projection under ./smtc-projected/ is a bulky generated +// fixture and is intentionally gitignored. On a clean checkout it must be +// regenerated before this test can run — otherwise a static import below would +// fail with an opaque module-not-found error. Fail early with a helpful +// message that spells out the exact regeneration command. +const __dirname_smtc = dirname(fileURLToPath(import.meta.url)); +const SMTC_FIXTURE = resolve( + __dirname_smtc, + 'smtc-projected/SystemMediaTransportControls.js' +); +if (!existsSync(SMTC_FIXTURE)) { + console.error(`[e2e] FAIL: SMTC projection fixture not found: ${SMTC_FIXTURE}`); + console.error(`[e2e] This fixture is gitignored — regenerate it with:`); + console.error(` cargo run -p dynwinrt-codegen -- generate \\`); + console.error(` --namespace Windows.Media \\`); + console.error(` --class-name SystemMediaTransportControls \\`); + console.error(` --output bindings/js/e2e/smtc-projected \\`); + console.error(` --import-name ../../dist/index.js`); + process.exit(1); +} +// Full WinRT natural projection: exercises real SMTC members via the same +// underlying COM pointer. Both wrappers are generated by dynwinrt-codegen. +const { SystemMediaTransportControls: SmtcProjected } = + await import('./smtc-projected/SystemMediaTransportControls.js'); +const { MediaPlaybackStatus } = + await import('./smtc-projected/MediaPlaybackStatus.js'); + +function fail(msg) { + console.error(`[e2e] FAIL: ${msg}`); + process.exit(1); +} + +// SMTC (unlike DTM) is documented as requiring a real top-level window that +// owns a media session. The classic-vertical `createTestHwnd()` helper +// creates a hidden `WS_POPUP` window owned by this process; that has been +// sufficient for interop dispatch on tested Windows builds. + +console.log('[e2e] step 1: acquiring a process-owned HWND via napi createTestHwnd()'); +const hwndBig = acquireHwndBigInt(); +console.log(`[e2e] HWND → 0x${hwndBig.toString(16)}`); +if (hwndBig === 0n) fail('acquireHwndBigInt returned NULL'); + +console.log('[e2e] step 2: ISystemMediaTransportControlsInterop.getForWindow(hwnd) [HIGH-LEVEL WRAPPER, IInspectable-rooted +6]'); +let smtcStub; +try { + const interop = ISystemMediaTransportControlsInterop.create(); + smtcStub = interop.getForWindow(hwndBig); +} catch (e) { + fail(`ISystemMediaTransportControlsInterop.getForWindow threw: ${e && e.message ? e.message : e}`); +} +if (smtcStub == null) fail('getForWindow returned null'); +console.log(`[e2e] got SystemMediaTransportControls (companion stub) = ${smtcStub}`); +if (!smtcStub._obj) fail('companion stub is missing native _obj'); + +console.log('[e2e] step 3: prove liveness via IInspectable::GetRuntimeClassName (companion stub property)'); +let name; +try { + name = smtcStub.runtimeClassName; +} catch (e) { + fail(`smtcStub.runtimeClassName threw: ${e && e.message ? e.message : e}`); +} +console.log(`[e2e] runtimeClassName = ${JSON.stringify(name)}`); +const expected = 'Windows.Media.SystemMediaTransportControls'; +if (name !== expected) fail(`expected runtimeClassName='${expected}', got '${name}'`); + +console.log('[e2e] step 4: MEANINGFUL — exercise real SMTC members through the natural WinRT wrapper'); +// Re-wrap the SAME native pointer with the full WinRT projection. +// This is still 100% "generated wrapper" code — no manual registerInterface. +const smtc = SmtcProjected._fromNative(smtcStub._obj); + +// (a) round-trip a boolean property. +console.log('[e2e] set isPlayEnabled = true'); +try { + smtc.isPlayEnabled = true; +} catch (e) { + fail(`smtc.isPlayEnabled = true threw: ${e && e.message ? e.message : e}`); +} +let readBack; +try { + readBack = smtc.isPlayEnabled; +} catch (e) { + fail(`get smtc.isPlayEnabled threw: ${e && e.message ? e.message : e}`); +} +console.log(`[e2e] get isPlayEnabled → ${readBack}`); +if (readBack !== true) fail(`isPlayEnabled round-trip: expected true, got ${readBack}`); + +// Flip it back to prove get/set really goes to the COM object. +console.log('[e2e] set isPlayEnabled = false'); +smtc.isPlayEnabled = false; +const readBack2 = smtc.isPlayEnabled; +console.log(`[e2e] get isPlayEnabled → ${readBack2}`); +if (readBack2 !== false) fail(`isPlayEnabled round-trip #2: expected false, got ${readBack2}`); + +// (b) round-trip an enum property. +console.log('[e2e] set playbackStatus = MediaPlaybackStatus.Playing (3)'); +try { + smtc.playbackStatus = MediaPlaybackStatus.Playing; +} catch (e) { + fail(`set playbackStatus threw: ${e && e.message ? e.message : e}`); +} +let status; +try { + status = smtc.playbackStatus; +} catch (e) { + fail(`get playbackStatus threw: ${e && e.message ? e.message : e}`); +} +console.log(`[e2e] get playbackStatus → ${status} (${status === MediaPlaybackStatus.Playing ? 'Playing' : 'other'})`); +if (status !== MediaPlaybackStatus.Playing) fail(`playbackStatus round-trip: expected ${MediaPlaybackStatus.Playing} (Playing), got ${status}`); + +// (c) reach a nested COM object. +console.log('[e2e] get displayUpdater (IInspectable child object)'); +let updater; +try { + updater = smtc.displayUpdater; +} catch (e) { + fail(`get displayUpdater threw: ${e && e.message ? e.message : e}`); +} +if (updater == null || !updater._obj) fail('displayUpdater returned null / no _obj'); +console.log(`[e2e] displayUpdater is a live SystemMediaTransportControlsDisplayUpdater`); + +console.log('PASS'); +process.exit(0); diff --git a/bindings/js/e2e/taskbarlist.mjs b/bindings/js/e2e/taskbarlist.mjs new file mode 100644 index 00000000..3c8bb088 --- /dev/null +++ b/bindings/js/e2e/taskbarlist.mjs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// Phase 2 E2E: real Node.js proof that the generated natural ITaskbarList3 +// wrapper drives live Windows classic COM (ITaskbarList3) via CoCreateInstance. +// +// Run: node bindings/js/e2e/taskbarlist.mjs + +import { DynWinRtValue, WinGuid } from '../dist/index.js'; +import { ITaskbarList3 } from './ITaskbarList3.js'; +import { TBPFLAG } from './TBPFLAG.js'; +import { acquireHwndBigInt } from './hwnd.mjs'; + +function fail(msg) { + console.error(`[e2e] FAIL: ${msg}`); + process.exit(1); +} + +console.log('[e2e] step 1: acquiring a process-owned HWND via napi createTestHwnd()'); +// The classic-vertical does not bundle flat-Win32, so we obtain a +// process-owned HWND via a small napi helper (`createTestHwnd`) instead of +// `flatInvoke(user32!CreateWindowExW, ...)`. This keeps the E2E +// self-contained with respect to the classic vertical's surface area. +const hwndBig = acquireHwndBigInt(); +console.log(`[e2e] HWND → 0x${hwndBig.toString(16)}`); +if (hwndBig === 0n) fail('acquireHwndBigInt returned NULL'); + +console.log('[e2e] step 2: CoCreateInstance(CLSID_TaskbarList, IID_ITaskbarList3)'); + +let t; +try { + t = ITaskbarList3.create(); +} catch (e) { + fail(`ITaskbarList3.create() threw: ${e && e.message ? e.message : e}`); +} +console.log(`[e2e] ITaskbarList3 = ${t}`); + +console.log('[e2e] step 3: HrInit() (vtable slot 3)'); +try { + t.hrInit(); +} catch (e) { + fail(`HrInit() threw: ${e && e.message ? e.message : e}`); +} + +// ITaskbarList3 accepts arbitrary HWNDs — SetProgressState / SetProgressValue on +// non-owned windows do not fail; they simply have no visible effect if the +// window is not a top-level shell window. What we need is: the call returns +// without an HRESULT error being thrown. + +console.log('[e2e] step 4: SetProgressState(hwnd, TBPF_NORMAL) (vtable slot 10)'); +try { + t.setProgressState(hwndBig, TBPFLAG.TBPF_NORMAL); +} catch (e) { + fail(`SetProgressState(TBPF_NORMAL) threw: ${e && e.message ? e.message : e}`); +} + +console.log('[e2e] step 5: SetProgressValue(hwnd, 30n, 100n) (vtable slot 9, u64 args)'); +try { + t.setProgressValue(hwndBig, 30n, 100n); +} catch (e) { + fail(`SetProgressValue(30, 100) threw: ${e && e.message ? e.message : e}`); +} + +console.log('[e2e] step 6: SetProgressState(hwnd, TBPF_NOPROGRESS)'); +try { + t.setProgressState(hwndBig, TBPFLAG.TBPF_NOPROGRESS); +} catch (e) { + fail(`SetProgressState(TBPF_NOPROGRESS) threw: ${e && e.message ? e.message : e}`); +} + +// Prove the BOOL → i32 codegen fix: markFullscreenWindow historically emitted +// `DynWinRtValue.pointer(fFullscreen)` and typed `fFullscreen: BOOL = bigint | Buffer`, +// so passing a plain `false` threw at napi. After the fix, BOOL projects as an +// i32 with a `boolean` surface, and this natural-JS call round-trips. +console.log('[e2e] step 7: MarkFullscreenWindow(hwnd, false) — proves BOOL→i32 codegen fix'); +try { + t.markFullscreenWindow(hwndBig, false); +} catch (e) { + fail(`MarkFullscreenWindow(hwnd, false) threw: ${e && e.message ? e.message : e}`); +} + +console.log('PASS'); +process.exit(0); diff --git a/bindings/js/src/lib.rs b/bindings/js/src/lib.rs index a23905c4..3859494a 100644 --- a/bindings/js/src/lib.rs +++ b/bindings/js/src/lib.rs @@ -7,7 +7,9 @@ use std::sync::{Arc, OnceLock}; use dynwinrt; +use napi::JsValue; use napi::bindgen_prelude::BigInt; +use napi::bindgen_prelude::Either; use napi::threadsafe_function::ThreadsafeFunctionCallMode; use napi_derive::napi; use windows::core::{IUnknown, Interface, HSTRING}; @@ -270,14 +272,62 @@ impl DynWinRTType { DynWinRTType(TABLE.register_interface(&name, iid.0)) } + /// Register a classic-COM (IUnknown-based) interface. + /// Returns self (Interface TypeHandle) for chaining `.addMethod()`. + /// User methods start at vtable slot 3 (QueryInterface/AddRef/Release are 0/1/2). + #[napi] + pub fn register_interface_unknown(name: String, iid: &WinGUID) -> Self { + DynWinRTType(TABLE.register_interface_iunknown(&name, iid.0)) + } + + /// Type-only alias for `object()` used by the classic-COM codegen. Any + /// pointer/handle (HWND, PWSTR, void*, function pointer, ...) is passed by + /// its raw ABI value; the value factory `DynWinRtValue.pointer(...)` builds + /// the matching `WinRTValue::RawPtr`. + #[napi] + pub fn pointer() -> Self { + DynWinRTType(TABLE.object()) + } + + /// Alias for `i32()` — matches the `xxxType()` naming used by codegen. + #[napi] + pub fn i32_type() -> Self { + DynWinRTType(TABLE.i32_type()) + } + + /// Alias for `u32()` — matches the `xxxType()` naming used by codegen. + #[napi] + pub fn u32_type() -> Self { + DynWinRTType(TABLE.u32_type()) + } + + /// Alias for `i64()` — matches the `xxxType()` naming used by codegen. + #[napi] + pub fn i64_type() -> Self { + DynWinRTType(TABLE.i64_type()) + } + + /// Alias for `u64()` — matches the `xxxType()` naming used by codegen. + #[napi] + pub fn u64_type() -> Self { + DynWinRTType(TABLE.u64_type()) + } + /// Add a method to this interface using a MethodSignature. - /// Methods are numbered starting at vtable index 6. + /// For IInspectable-based (WinRT) interfaces registered via + /// `register_interface`, methods start at vtable slot 6 (after + /// IUnknown 0-2 and IInspectable 3-5). For classic COM interfaces + /// registered via `register_interface_unknown`, methods start at + /// vtable slot 3 (after IUnknown 0-2 only). #[napi] pub fn add_method(&self, name: String, sig: &DynWinRTMethodSig) -> DynWinRTType { DynWinRTType(self.0.clone().add_method(&name, sig.0.clone())) } - /// Get a MethodHandle by vtable index (6 = first user method). + /// Get a MethodHandle by vtable index. For IInspectable-based interfaces + /// (WinRT / `registerInterface`), the first user method is at slot 6. For + /// classic COM interfaces (`registerInterfaceUnknown`), the first user + /// method is at slot 3. #[napi] pub fn method(&self, vtable_index: i32) -> napi::Result { self @@ -541,6 +591,12 @@ impl DynWinRTValue { #[napi] pub fn activation_factory(name: String) -> napi::Result { + // WinRT's RoGetActivationFactory requires the thread apartment to be + // initialized. Node's main thread is not COM-initialized by default, so + // do it lazily on the first call (same behaviour as `coCreateInstance`). + dynwinrt::classic_com::ensure_com_initialized().map_err(|e| { + napi::Error::from_reason(format!("ensure_com_initialized: {}", e.message())) + })?; let factory = dynwinrt::ro_get_activation_factory_2(&HSTRING::from(&name)).map_err(|e| { napi::Error::from_reason(format!("ActivationFactory '{}': {}", name, e.message())) })?; @@ -571,6 +627,233 @@ impl DynWinRTValue { }) } + /// Create a classic-COM instance via `CoCreateInstance(clsid, CLSCTX_INPROC_SERVER)` and QI to `iid`. + #[napi] + pub fn co_create_instance(clsid_str: String, iid: &WinGUID) -> napi::Result { + let clsid = windows::core::GUID::try_from(clsid_str.as_str()) + .map_err(|_| napi::Error::from_reason(format!("Invalid CLSID: '{}'", clsid_str)))?; + dynwinrt::classic_com::co_create_instance(clsid, iid.0) + .map(DynWinRTValue) + .map_err(|e| { + napi::Error::from_reason(format!( + "CoCreateInstance({}, {}) failed: {}", + clsid_str, + iid.to_string(), + e.message() + )) + }) + } + + /// Create a hidden top-level HWND owned by this (Node) process, for use + /// with classic-COM/WinRT interop APIs that require a process-owned window + /// (e.g. `IDataTransferManagerInterop::GetForWindow`, + /// `ISystemMediaTransportControlsInterop::GetForWindow`). + /// + /// The window is a hidden `WS_POPUP` window using the pre-registered + /// `STATIC` class; it is intentionally leaked (never destroyed) because + /// tests are short-lived and cleanup is unnecessary. Returns the HWND + /// as a `bigint`. + /// + /// This lives in classic-vertical because it is the classic-COM/interop + /// vertical's own way to obtain a process-owned HWND for testing — it + /// avoids taking a flat-Win32 dependency for the classic tests. + #[napi] + pub fn create_test_hwnd() -> napi::Result { + use windows::Win32::UI::WindowsAndMessaging::{CreateWindowExW, WINDOW_EX_STYLE, WS_POPUP}; + let class_name: Vec = "STATIC".encode_utf16().chain(std::iter::once(0)).collect(); + let title: Vec = "dynwinrt-test-hwnd\0".encode_utf16().collect(); + let hwnd = unsafe { + CreateWindowExW( + WINDOW_EX_STYLE(0), + windows::core::PCWSTR(class_name.as_ptr()), + windows::core::PCWSTR(title.as_ptr()), + WS_POPUP, + 0, + 0, + 1, + 1, + None, + None, + None, + None, + ) + } + .map_err(|e| napi::Error::from_reason(format!("CreateWindowExW: {}", e)))?; + Ok(BigInt::from(hwnd.0 as u64)) + } + + /// Wrap a pointer/handle (BigInt, Buffer, or another `DynWinRtValue` holding + /// an object/raw pointer) as a `WinRTValue::RawPtr` for classic-COM calls + /// with `void*` / HWND / PWSTR / function-pointer parameters. + /// + /// Accepts: + /// - BigInt: interpreted as a raw pointer value (u64 on x64). + /// - Buffer: uses the buffer's byte-pointer directly (does not clone). + /// Caller keeps the Buffer alive for the duration of the COM call. + /// - DynWinRtValue: reuses its underlying pointer (Object/RawPtr) or + /// handles Null. + /// - null/undefined: null pointer. + #[napi] + pub fn pointer( + #[napi( + ts_arg_type = "bigint | number | Buffer | Uint8Array | DynWinRtValue | null | undefined" + )] + value: napi::bindgen_prelude::Unknown, + ) -> napi::Result { + use napi::bindgen_prelude::FromNapiValue; + use napi::sys; + + let raw_env = value.value().env; + let raw_val = value.value().value; + + // Fast path 1: null / undefined → null pointer + let mut val_type = sys::ValueType::napi_undefined; + unsafe { sys::napi_typeof(raw_env, raw_val, &mut val_type) }; + if val_type == sys::ValueType::napi_null || val_type == sys::ValueType::napi_undefined { + return Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( + std::ptr::null_mut(), + ))); + } + + // Fast path 2: BigInt → parse as u64 pointer bits. + // + // BigInt::get_u64() returns (sign_bit, magnitude, lossless). The tuple + // silently swallows negative values (sign=true is dropped) and values + // that don't fit in u64 (lossless=false → magnitude wraps). Validate + // both so that DynWinRtValue.pointer(-1n) or a >2^64 bigint produce a + // clean error instead of a fabricated pointer. + if val_type == sys::ValueType::napi_bigint { + let bi = + unsafe { napi::bindgen_prelude::BigInt::from_napi_value(raw_env, raw_val) }?; + let (sign_bit, n, lossless) = bi.get_u64(); + if sign_bit { + return Err(napi::Error::from_reason( + "pointer(): bigint must be non-negative (pointer values are unsigned)", + )); + } + if !lossless { + return Err(napi::Error::from_reason( + "pointer(): bigint exceeds u64 range; pointer values must fit in u64", + )); + } + if (n as usize as u64) != n { + return Err(napi::Error::from_reason( + "pointer(): bigint exceeds usize range on this platform", + )); + } + return Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( + n as usize as *mut std::ffi::c_void, + ))); + } + + // Fast path 3: Number → cast to usize (handy for HWNDs that fit in a + // JS number; the caller can also pass BigInt for safety). + // + // A float→int cast in Rust saturates and silently accepts NaN, negative, + // fractional, and >2^53 values — any of which could produce a bogus + // pointer. Validate that the value is a finite, non-negative safe + // integer that fits in usize, and require BigInt otherwise. + if val_type == sys::ValueType::napi_number { + let mut d: f64 = 0.0; + unsafe { sys::napi_get_value_double(raw_env, raw_val, &mut d) }; + if !d.is_finite() { + return Err(napi::Error::from_reason( + "pointer(): number must be finite (got NaN or Infinity); use bigint for arbitrary pointer values", + )); + } + if d < 0.0 { + return Err(napi::Error::from_reason( + "pointer(): number must be non-negative; use bigint for arbitrary pointer values", + )); + } + if d.fract() != 0.0 { + return Err(napi::Error::from_reason( + "pointer(): number must be an integer; use bigint for arbitrary pointer values", + )); + } + // JS Number can only faithfully represent integers up to 2^53 - 1. + const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; // (1 << 53) - 1 + if d > MAX_SAFE_INTEGER { + return Err(napi::Error::from_reason( + "pointer(): number exceeds Number.MAX_SAFE_INTEGER; use bigint for arbitrary pointer values", + )); + } + let bits = d as u64; + if (bits as usize as u64) != bits { + return Err(napi::Error::from_reason( + "pointer(): number exceeds usize range on this platform; use bigint", + )); + } + return Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( + bits as usize as *mut std::ffi::c_void, + ))); + } + + // Fast path 4: Buffer / Uint8Array → base data pointer. + if let Ok(buf) = + unsafe { napi::bindgen_prelude::Buffer::from_napi_value(raw_env, raw_val) } + { + let slice: &[u8] = buf.as_ref(); + return Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( + slice.as_ptr() as *mut std::ffi::c_void, + ))); + } + + // Fast path 4b: plain Uint8Array (NOT a Node.js Buffer subclass) → + // base data pointer. Buffer::from_napi_value above rejects raw + // Uint8Array views even though the TS surface (`ts_arg_type`) advertises + // Uint8Array. Handle it explicitly with the same semantics as Buffer. + if let Ok(arr) = + unsafe { napi::bindgen_prelude::Uint8Array::from_napi_value(raw_env, raw_val) } + { + let slice: &[u8] = arr.as_ref(); + return Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( + slice.as_ptr() as *mut std::ffi::c_void, + ))); + } + + // Fast path 5: existing DynWinRtValue → reuse its pointer. + if let Ok(v) = unsafe { <&DynWinRTValue>::from_napi_value(raw_env, raw_val) } { + return match &v.0 { + dynwinrt::WinRTValue::Object(o) => Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( + o.as_raw(), + ))), + dynwinrt::WinRTValue::RawPtr(p) => { + Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr(*p))) + } + dynwinrt::WinRTValue::Null => Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( + std::ptr::null_mut(), + ))), + _ => Err(napi::Error::from_reason( + "pointer(): DynWinRtValue must wrap an object or raw pointer", + )), + }; + } + + Err(napi::Error::from_reason( + "pointer(): expected bigint, number, Buffer, Uint8Array, DynWinRtValue, null, or undefined", + )) + } + + /// Get the underlying pointer of an Object/RawPtr value as a BigInt. + /// Useful for turning a pointer result (e.g. HWND from + /// `GetConsoleWindow`) into a bigint you can then feed into other calls. + #[napi] + pub fn as_pointer_bigint(&self) -> napi::Result { + let bits: usize = match &self.0 { + dynwinrt::WinRTValue::Object(o) => o.as_raw() as usize, + dynwinrt::WinRTValue::RawPtr(p) => *p as usize, + dynwinrt::WinRTValue::Null => 0, + _ => { + return Err(napi::Error::from_reason(format!( + "asPointerBigint: not a pointer/object value ({:?})", + self.0.get_type_kind() + ))); + } + }; + Ok(BigInt::from(bits as u64)) + } + #[napi] pub fn bool_value(value: bool) -> DynWinRTValue { DynWinRTValue(dynwinrt::WinRTValue::Bool(value)) @@ -603,9 +886,53 @@ impl DynWinRTValue { pub fn i64(value: i64) -> DynWinRTValue { DynWinRTValue(dynwinrt::WinRTValue::I64(value)) } - #[napi] - pub fn u64(value: i64) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::U64(value as u64)) + /// Create a `u64` `WinRTValue`. Accepts either a JS `BigInt` (classic-COM + /// codegen emits `DynWinRtValue.u64(BigInt(v))`) or a plain JS `number` + /// (existing WinRT codegen emits `DynWinRtValue.u64(value)` for `UInt64` + /// params like stream seek/size). Accepting both keeps the WinRT path + /// working while supporting the 64-bit classic-COM path. + /// + /// Negative values, values > u64::MAX (bigint), or negative numbers (JS + /// number) are rejected up front; silent truncation used to be possible + /// via BigInt::get_u64()'s sign/lossless flags and via `i64 as u64` on + /// the number path. + #[napi(ts_args_type = "value: bigint | number")] + pub fn u64(value: Either) -> napi::Result { + let n = match value { + Either::A(big) => { + let (sign_bit, n, lossless) = big.get_u64(); + if sign_bit { + return Err(napi::Error::from_reason( + "u64(): bigint must be non-negative", + )); + } + if !lossless { + return Err(napi::Error::from_reason( + "u64(): bigint exceeds u64::MAX", + )); + } + n + } + Either::B(num) => { + if num < 0 { + return Err(napi::Error::from_reason( + "u64(): number must be non-negative; use bigint for the full u64 range", + )); + } + // JS Number can only faithfully represent integers up to 2^53 - 1; + // anything above that has already been rounded by the time napi + // converts to i64. Refuse it explicitly so callers switch to bigint + // instead of silently marshalling a lossy value. + const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991; // (1 << 53) - 1 + if num > MAX_SAFE_INTEGER { + return Err(napi::Error::from_reason( + "u64(): number exceeds Number.MAX_SAFE_INTEGER; use bigint for the full u64 range", + )); + } + num as u64 + } + }; + Ok(DynWinRTValue(dynwinrt::WinRTValue::U64(n))) } #[napi] pub fn f32(value: f64) -> DynWinRTValue { @@ -660,6 +987,29 @@ impl DynWinRTValue { pub fn guid(value: &WinGUID) -> DynWinRTValue { DynWinRTValue(dynwinrt::WinRTValue::Guid(value.0)) } + /// Return a raw pointer to a stable GUID (for `REFIID` parameters). + /// The GUID is boxed and cached per-unique-value; the box outlives the process. + #[napi] + pub fn iid_pointer(value: &WinGUID) -> DynWinRTValue { + use std::collections::HashMap; + use std::sync::{Mutex, OnceLock}; + static CACHE: OnceLock>> = OnceLock::new(); + let g = value.0; + // Compose a stable u128 key from the GUID fields. + let mut key: u128 = 0; + key |= (g.data1 as u128) << 96; + key |= (g.data2 as u128) << 80; + key |= (g.data3 as u128) << 64; + for (i, b) in g.data4.iter().enumerate() { + key |= (*b as u128) << (56 - i as u32 * 8); + } + let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new())); + let mut map = cache.lock().unwrap(); + let addr = *map.entry(key).or_insert_with(|| { + Box::into_raw(Box::new(g)) as usize + }); + DynWinRTValue(dynwinrt::WinRTValue::RawPtr(addr as *mut std::ffi::c_void)) + } #[napi] pub fn null_value() -> DynWinRTValue { DynWinRTValue(dynwinrt::WinRTValue::Null) diff --git a/crates/dynwinrt/Cargo.toml b/crates/dynwinrt/Cargo.toml index ddbaf326..ed876eda 100644 --- a/crates/dynwinrt/Cargo.toml +++ b/crates/dynwinrt/Cargo.toml @@ -21,6 +21,7 @@ windows-metadata = "0.59.0" version = ">=0.59, <=0.62" features = [ "ApplicationModel", + "ApplicationModel_DataTransfer", "Data_Xml_Dom", "Devices_Geolocation", "Storage_Streams", @@ -30,6 +31,8 @@ features = [ "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_System_WinRT", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging", "Win32_Storage_Packaging_Appx", "Win32_UI_HiDpi", "Management_Deployment", diff --git a/crates/dynwinrt/src/call.rs b/crates/dynwinrt/src/call.rs index 829fce31..f7acde2f 100644 --- a/crates/dynwinrt/src/call.rs +++ b/crates/dynwinrt/src/call.rs @@ -74,6 +74,7 @@ macro_rules! dispatch_scalar { WinRTValue::F64(v) => $call(*v), WinRTValue::Object(o) => $call(o.as_raw()), WinRTValue::Null => $call(std::ptr::null_mut::()), + WinRTValue::RawPtr(p) => $call(*p), WinRTValue::Guid(g) => $call(*g), _ => panic!("dispatch_scalar: unsupported type {:?}", $in_val), } diff --git a/crates/dynwinrt/src/classic_com.rs b/crates/dynwinrt/src/classic_com.rs new file mode 100644 index 00000000..bd662899 --- /dev/null +++ b/crates/dynwinrt/src/classic_com.rs @@ -0,0 +1,304 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use core::ffi::c_void; +use std::cell::RefCell; + +use windows::Win32::System::Com::{ + CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize, +}; +use windows_core::{GUID, IUnknown, Interface}; + +use crate::{MethodSignature, WinRTValue, result}; + +const RPC_E_CHANGED_MODE: windows_core::HRESULT = windows_core::HRESULT(0x80010106u32 as i32); + +struct ComApartment; + +impl Drop for ComApartment { + fn drop(&mut self) { + unsafe { CoUninitialize() }; + } +} + +enum ComInitialization { + Unknown, + Owned(ComApartment), + ExistingApartment, +} + +thread_local! { + static COM_INITIALIZATION: RefCell = + const { RefCell::new(ComInitialization::Unknown) }; +} + +pub fn ensure_com_initialized() -> result::Result<()> { + COM_INITIALIZATION.with(|state| { + if !matches!(*state.borrow(), ComInitialization::Unknown) { + return Ok(()); + } + + let hr = unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }; + if hr.is_ok() { + *state.borrow_mut() = ComInitialization::Owned(ComApartment); + Ok(()) + } else if hr == RPC_E_CHANGED_MODE { + *state.borrow_mut() = ComInitialization::ExistingApartment; + Ok(()) + } else { + Err(result::Error::WindowsError( + windows_core::Error::from_hresult(hr), + )) + } + }) +} + +pub fn co_create_instance(clsid: GUID, iid: GUID) -> result::Result { + ensure_com_initialized()?; + + let unknown: IUnknown = unsafe { CoCreateInstance(&clsid, None, CLSCTX_INPROC_SERVER) } + .map_err(result::Error::WindowsError)?; + let mut result = std::ptr::null_mut(); + unsafe { unknown.query(&iid, &mut result) } + .ok() + .map_err(result::Error::WindowsError)?; + Ok(WinRTValue::Object(unsafe { IUnknown::from_raw(result) })) +} + +pub fn call_method( + vtable_index: usize, + obj: *mut c_void, + signature: MethodSignature, + args: &[WinRTValue], +) -> result::Result> { + signature + .build(vtable_index) + .call_dynamic(obj, args) + .map_err(result::Error::WindowsError) +} + +#[cfg(test)] +fn call_method_1_ptr( + vtable_index: usize, + obj: *mut c_void, + ptr: *const c_void, +) -> result::Result<()> { + crate::call::call_winrt_method_1(vtable_index, obj, ptr) + .ok() + .map_err(result::Error::WindowsError) +} + +#[cfg(test)] +fn call_method_2_ptr_i32( + vtable_index: usize, + obj: *mut c_void, + ptr: *mut c_void, + value: i32, +) -> result::Result<()> { + crate::call::call_winrt_method_2(vtable_index, obj, ptr, value) + .ok() + .map_err(result::Error::WindowsError) +} + +#[cfg(test)] +fn wide_null(text: &str) -> Vec { + text.encode_utf16().chain(std::iter::once(0)).collect() +} + +#[cfg(test)] +fn wide_buffer(characters: usize) -> Vec { + vec![0; characters] +} + +#[cfg(test)] +fn wide_to_string(buffer: &[u16]) -> String { + let end = buffer + .iter() + .position(|ch| *ch == 0) + .unwrap_or(buffer.len()); + String::from_utf16_lossy(&buffer[..end]) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + InterfaceSignature, MetadataTable, com_helpers::E_NOINTERFACE, ro_get_activation_factory_2, + roapi::query_interface, + }; + use windows::{ + ApplicationModel::DataTransfer::DataTransferManager, + Win32::{ + UI::Shell::IDataTransferManagerInterop, + UI::WindowsAndMessaging::{ + CreateWindowExW, DestroyWindow, WINDOW_EX_STYLE, WS_OVERLAPPED, + }, + }, + }; + use windows_core::{HSTRING, Interface, w}; + + const CLSID_SHELL_LINK: GUID = GUID::from_u128(0x00021401_0000_0000_c000_000000000046); + const IID_ISHELL_LINK_W: GUID = GUID::from_u128(0x000214f9_0000_0000_c000_000000000046); + const REGDB_E_CLASSNOTREG: windows_core::HRESULT = windows_core::HRESULT(0x80040154u32 as i32); + + fn shell_link() -> result::Result { + co_create_instance(CLSID_SHELL_LINK, IID_ISHELL_LINK_W) + } + + fn shell_link_signature(table: &std::sync::Arc) -> InterfaceSignature { + let mut iface = + InterfaceSignature::define_from_iunknown("IShellLinkW", IID_ISHELL_LINK_W, table); + iface + .add_method(MethodSignature::new(table)) // 3 GetPath + .add_method(MethodSignature::new(table)) // 4 GetIDList + .add_method(MethodSignature::new(table)) // 5 SetIDList + .add_method(MethodSignature::new(table)) // 6 GetDescription + .add_method(MethodSignature::new(table)) // 7 SetDescription + .add_method(MethodSignature::new(table)) // 8 GetWorkingDirectory + .add_method(MethodSignature::new(table)) // 9 SetWorkingDirectory + .add_method(MethodSignature::new(table)) // 10 GetArguments + .add_method(MethodSignature::new(table)) // 11 SetArguments + .add_method(MethodSignature::new(table).add_out(table.u16_type())) // 12 GetHotkey + .add_method(MethodSignature::new(table).add_in(table.u16_type())) // 13 SetHotkey + .add_method(MethodSignature::new(table).add_out(table.i32_type())) // 14 GetShowCmd + .add_method(MethodSignature::new(table).add_in(table.i32_type())); // 15 SetShowCmd + iface + } + + #[test] + fn shell_link_set_get_show_cmd_round_trips_via_classic_com_vtable() -> result::Result<()> { + let shell_link = shell_link()?.as_object().unwrap(); + let table = MetadataTable::new(); + let iface = shell_link_signature(&table); + + iface.methods[15].call_dynamic(shell_link.as_raw(), &[WinRTValue::I32(3)])?; + let result = iface.methods[14].call_dynamic(shell_link.as_raw(), &[])?; + + assert_eq!(result[0].as_i32().unwrap(), 3); + Ok(()) + } + + #[test] + fn shell_link_set_get_hotkey_round_trips_u16() -> result::Result<()> { + let shell_link = shell_link()?.as_object().unwrap(); + let table = MetadataTable::new(); + let iface = shell_link_signature(&table); + + iface.methods[13].call_dynamic(shell_link.as_raw(), &[WinRTValue::U16(0x0141)])?; + let result = iface.methods[12].call_dynamic(shell_link.as_raw(), &[])?; + + assert_eq!(result[0].as_i32().unwrap() as u16, 0x0141); + Ok(()) + } + + #[test] + fn shell_link_set_get_description_round_trips_wide_string() -> result::Result<()> { + let shell_link = shell_link()?.as_object().unwrap(); + let expected = "dynwinrt classic COM"; + let wide = wide_null(expected); + + call_method_1_ptr(7, shell_link.as_raw(), wide.as_ptr() as *const c_void)?; + + let mut buffer = wide_buffer(128); + call_method_2_ptr_i32( + 6, + shell_link.as_raw(), + buffer.as_mut_ptr() as *mut c_void, + buffer.len() as i32, + )?; + + assert_eq!(wide_to_string(&buffer), expected); + Ok(()) + } + + #[test] + fn co_create_instance_with_bogus_clsid_returns_error() -> result::Result<()> { + let bogus = GUID::from_u128(0xaaaaaaaa_bbbb_cccc_dddd_eeeeeeeeeeee); + + let err = co_create_instance(bogus, IID_ISHELL_LINK_W).unwrap_err(); + match err { + result::Error::WindowsError(err) => assert_eq!(err.code(), REGDB_E_CLASSNOTREG), + err => panic!("expected REGDB_E_CLASSNOTREG, got {err:?}"), + } + Ok(()) + } + + #[test] + fn query_interface_with_unsupported_iid_returns_error() -> result::Result<()> { + let shell_link = shell_link()?; + let bogus = GUID::from_u128(0xbbbbbbbb_cccc_dddd_eeee_ffffffffffff); + + let err = shell_link.cast(&bogus).unwrap_err(); + match err { + result::Error::WindowsError(err) => assert_eq!(err.code(), E_NOINTERFACE), + err => panic!("expected E_NOINTERFACE, got {err:?}"), + } + Ok(()) + } + + #[test] + fn data_transfer_manager_interop_get_for_window_returns_winrt_object_via_dynamic_iunknown_vtable() + -> result::Result<()> { + ensure_com_initialized()?; + + let hwnd = unsafe { + CreateWindowExW( + WINDOW_EX_STYLE(0), + w!("STATIC"), + w!("dynwinrt data transfer interop test"), + WS_OVERLAPPED, + 0, + 0, + 1, + 1, + None, + None, + None, + None, + ) + } + .map_err(result::Error::WindowsError)?; + struct WindowGuard(windows::Win32::Foundation::HWND); + impl Drop for WindowGuard { + fn drop(&mut self) { + let _ = unsafe { DestroyWindow(self.0) }; + } + } + let _window = WindowGuard(hwnd); + + let factory = ro_get_activation_factory_2(&HSTRING::from( + "Windows.ApplicationModel.DataTransfer.DataTransferManager", + ))?; + let interop = query_interface(factory, &IDataTransferManagerInterop::IID) + .map_err(result::Error::WindowsError)? + .as_object() + .unwrap(); + + let table = MetadataTable::new(); + let mut iface = InterfaceSignature::define_from_iunknown( + "IDataTransferManagerInterop", + IDataTransferManagerInterop::IID, + &table, + ); + iface.add_method( + MethodSignature::new(&table) + .add_in(table.object()) + .add_in(table.object()) + .add_out(table.object()), + ); + + let target_iid = DataTransferManager::IID; + let result = iface.methods[3].call_dynamic( + interop.as_raw(), + &[ + WinRTValue::RawPtr(hwnd.0 as *mut c_void), + WinRTValue::RawPtr(&target_iid as *const GUID as *mut c_void), + ], + )?; + + let manager = result[0].as_object().expect("GetForWindow returned null"); + assert!(!manager.as_raw().is_null()); + let _typed: DataTransferManager = manager.cast().map_err(result::Error::WindowsError)?; + Ok(()) + } +} diff --git a/crates/dynwinrt/src/lib.rs b/crates/dynwinrt/src/lib.rs index ae33bdd1..07b22887 100644 --- a/crates/dynwinrt/src/lib.rs +++ b/crates/dynwinrt/src/lib.rs @@ -5,6 +5,7 @@ use windows::core::*; mod abi; mod call; +pub mod classic_com; mod interfaces; mod result; mod roapi; diff --git a/crates/dynwinrt/src/metadata_table/arena.rs b/crates/dynwinrt/src/metadata_table/arena.rs index 16b5dd23..ed5c5362 100644 --- a/crates/dynwinrt/src/metadata_table/arena.rs +++ b/crates/dynwinrt/src/metadata_table/arena.rs @@ -40,6 +40,9 @@ pub(super) struct EnumData { pub(super) struct InterfaceMethodTable { pub(super) method_names: Vec, pub(super) method_indices: Vec, + /// First user-method vtable slot for this interface. + /// 6 for IInspectable-based (WinRT) interfaces, 3 for IUnknown-based (classic COM). + pub(super) base_slot: usize, } // =========================================================================== @@ -114,14 +117,39 @@ impl MetadataTable { /// Create an interface method table. Called only when dedup already checked by caller. pub(super) fn create_interface_method_table(&self, iid: GUID) { - self.interface_methods - .write() - .unwrap() - .entry(iid) - .or_insert_with(|| InterfaceMethodTable { - method_names: Vec::new(), - method_indices: Vec::new(), - }); + self.create_interface_method_table_with_base(iid, 6); + } + + /// Create an interface method table with a specific base vtable slot. + /// 6 = IInspectable-based (WinRT), 3 = IUnknown-based (classic COM). + /// + /// If a method table for this IID already exists, its `base_slot` MUST + /// match `base_slot`; otherwise subsequent method registrations for the + /// IID would compute wrong vtable indices for one of the callers. + /// Failing loudly is safer than silently keeping the first-registered + /// base slot (as `or_insert_with` would). + pub(super) fn create_interface_method_table_with_base(&self, iid: GUID, base_slot: usize) { + let mut tables = self.interface_methods.write().unwrap(); + match tables.entry(iid) { + std::collections::hash_map::Entry::Occupied(existing) => { + let existing_base = existing.get().base_slot; + assert_eq!( + existing_base, base_slot, + "interface IID {:?} registered twice with conflicting base slots \ + (existing={}, new={}). This would silently produce wrong vtable \ + indices; each IID must be registered with a single base_slot \ + (3 for IUnknown-based classic COM, 6 for IInspectable/WinRT).", + iid, existing_base, base_slot, + ); + } + std::collections::hash_map::Entry::Vacant(v) => { + v.insert(InterfaceMethodTable { + method_names: Vec::new(), + method_indices: Vec::new(), + base_slot, + }); + } + } } /// Add a method to an interface's method table. Returns the vtable index. @@ -134,10 +162,10 @@ impl MetadataTable { // Dedup: if method name already registered, return existing vtable index if let Some(pos) = table.method_names.iter().position(|n| n == name) { - return (6 + pos) as u32; + return (table.base_slot + pos) as u32; } - let vtable_index = 6 + table.method_indices.len(); + let vtable_index = table.base_slot + table.method_indices.len(); let method = sig.build(vtable_index); let arena_index = self.methods.push(method); table.method_names.push(name.to_string()); @@ -234,12 +262,12 @@ impl MetadataTable { iid: &GUID, vtable_index: usize, ) -> Option { - if vtable_index < 6 { - return None; - } - let local_index = vtable_index - 6; let iface_methods = self.interface_methods.read().unwrap(); let table = iface_methods.get(iid)?; + if vtable_index < table.base_slot { + return None; + } + let local_index = vtable_index - table.base_slot; table.method_indices.get(local_index).copied() } diff --git a/crates/dynwinrt/src/metadata_table/mod.rs b/crates/dynwinrt/src/metadata_table/mod.rs index 8ab9eb5f..edea6ccb 100644 --- a/crates/dynwinrt/src/metadata_table/mod.rs +++ b/crates/dynwinrt/src/metadata_table/mod.rs @@ -241,6 +241,19 @@ impl MetadataTable { self.make(kind) } + /// Register a named IUnknown-based (classic COM) interface. User methods + /// start at vtable slot 3 (QI/AddRef/Release occupy 0/1/2), rather than the + /// WinRT default of 6 (IInspectable adds three more slots at 3/4/5). + pub fn register_interface_iunknown(self: &Arc, name: &str, iid: GUID) -> TypeHandle { + if let Some(kind) = self.get_named_type(name) { + return self.make(kind); + } + self.create_interface_method_table_with_base(iid, 3); + let kind = TypeKind::Interface(iid); + self.insert_named_type(name, kind); + self.make(kind) + } + /// Register a named struct with dedup. If already registered, returns /// the existing TypeHandle. pub fn struct_type(self: &Arc, name: &str, fields: &[TypeHandle]) -> TypeHandle { diff --git a/crates/dynwinrt/src/signature.rs b/crates/dynwinrt/src/signature.rs index 105a1b24..5f91026d 100644 --- a/crates/dynwinrt/src/signature.rs +++ b/crates/dynwinrt/src/signature.rs @@ -288,9 +288,39 @@ fn coerce_input_object( let Some(iid) = expected_object_iid(expected) else { return Ok(None); }; + // Null objects are always allowed (`WinRTValue::Null` and the + // Object-typed null variant both project as "no coercion needed" — the + // ABI receives a null pointer directly). if value.is_null_object() { return Ok(None); } + // A `WinRTValue::RawPtr` is a raw ABI pointer supplied by the caller + // (e.g. `DynWinRtValue.pointer(hwnd)`). It is legitimate ONLY when the + // parameter is untyped `TypeKind::Object` (the codegen's `pointer()` + // alias, used for HWND / PWSTR / void* / function-pointer slots). + // + // For a TYPED interface / delegate / runtime class / async parameter + // the runtime would otherwise blindly forward the caller's pointer bits + // into the vtable dispatch, without QI'ing to the required IID. If + // the pointer wasn't actually a live COM object with the expected + // vtable layout the dispatch would read a bogus vtable → crash / UB. + // Reject up-front with E_INVALIDARG so callers pass a real `Object` + // (or an explicitly `.cast()`-ed one) instead. + if matches!(value, WinRTValue::RawPtr(_)) { + if matches!(expected.kind(), TypeKind::Object) { + return Ok(None); + } + return Err(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Refusing to pass a raw pointer as a typed COM parameter ({}). \ + Only untyped Object / void* / handle parameters accept \ + DynWinRtValue.pointer(...); for a concrete interface pass a \ + real object (or one obtained via `.cast(IID)`).", + expected.signature_string(), + ), + )); + } let object = value.as_object().ok_or_else(|| { windows_core::Error::new( @@ -795,6 +825,67 @@ mod tests { Ok(()) } + /// Regression: `coerce_input_object` must accept `WinRTValue::RawPtr` + /// ONLY when the expected parameter type is the untyped + /// `TypeKind::Object` (the codegen's `pointer()` alias used for HWND / + /// void* / handle slots). Passing a raw pointer where a *typed* + /// interface / delegate / runtime class / async parameter is expected + /// must be rejected up-front with `E_INVALIDARG`, so we don't + /// blindly forward pointer bits into a vtable dispatch that would + /// then read a bogus vtable and crash / UB. + #[test] + fn raw_pointer_only_accepted_for_untyped_object_params() { + let table = MetadataTable::new(); + let bogus = WinRTValue::RawPtr(0xDEADBEEF as *mut std::ffi::c_void); + + // Legitimate case: `TypeKind::Object` (a.k.a. codegen's `pointer()` / + // HWND / void*) accepts RawPtr — bypass coercion so the raw ABI + // pointer is forwarded to the callee unchanged. + let object_ty = table.object(); + assert!( + matches!(object_ty.kind(), TypeKind::Object), + "sanity: table.object() must be TypeKind::Object" + ); + assert!( + coerce_input_object(&object_ty, &bogus) + .expect("RawPtr into TypeKind::Object must be allowed") + .is_none(), + "RawPtr into TypeKind::Object should bypass coercion (Ok(None))", + ); + + // Unsafe case: RawPtr into a typed `TypeKind::Interface(IID)` + // must FAIL with E_INVALIDARG, not silently succeed. + let iface_ty = table.interface(IStringable::IID); + let err = coerce_input_object(&iface_ty, &bogus) + .expect_err("RawPtr into a typed interface must be rejected"); + assert_eq!( + err.code().0, + 0x80070057u32 as i32, + "typed-interface RawPtr rejection must use E_INVALIDARG (got {:?})", + err + ); + let msg = err.message(); + assert!( + msg.contains("raw pointer") && msg.contains("typed"), + "rejection error must explain the constraint, got: {}", + msg + ); + + // Null objects remain allowed for both untyped and typed slots + // (a null pointer is a valid COM null-object). + let null_object = WinRTValue::Null; + assert!( + coerce_input_object(&object_ty, &null_object) + .expect("null into TypeKind::Object must be allowed") + .is_none(), + ); + assert!( + coerce_input_object(&iface_ty, &null_object) + .expect("null into typed interface must be allowed") + .is_none(), + ); + } + #[test] fn coerces_object_array_elements_to_the_expected_interface() -> windows_core::Result<()> { let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; diff --git a/crates/dynwinrt/tests/winrt_regression.rs b/crates/dynwinrt/tests/winrt_regression.rs new file mode 100644 index 00000000..9619f49f --- /dev/null +++ b/crates/dynwinrt/tests/winrt_regression.rs @@ -0,0 +1,404 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use dynwinrt::{InterfaceSignature, MetadataTable, MethodSignature, WinRTValue}; +use windows::Devices::Geolocation::{BasicGeoposition, Geopoint, IGeopoint, IGeopointFactory}; +use windows::Foundation::{IPropertyValue, IUriRuntimeClass, IUriRuntimeClassFactory}; +use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize}; +use windows_core::{GUID, HRESULT, HSTRING, Interface}; + +fn init_winrt() { + unsafe { RoInitialize(RO_INIT_MULTITHREADED) }.expect("RoInitialize should succeed"); +} + +fn assert_hstring(value: &WinRTValue, expected: &str) { + assert_eq!(value.as_hstring().expect("expected HSTRING"), expected); +} + +fn assert_bool(value: &WinRTValue, expected: bool) { + match value { + WinRTValue::Bool(actual) => assert_eq!(*actual, expected), + other => panic!("expected Bool({expected}), got {other:?}"), + } +} + +fn uri_runtime_class_signature(reg: &std::sync::Arc) -> InterfaceSignature { + let mut iface = InterfaceSignature::define_from_iinspectable( + "IUriRuntimeClass", + IUriRuntimeClass::IID, + reg, + ); + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 6 AbsoluteUri + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 7 DisplayUri + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 8 Domain + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 9 Extension + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 10 Fragment + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 11 Host + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 12 Password + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 13 Path + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 14 Query + iface.add_method(MethodSignature::new(reg).add_out(reg.object())); // 15 QueryParsed + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 16 RawUri + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 17 SchemeName + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 18 UserName + iface.add_method(MethodSignature::new(reg).add_out(reg.i32_type())); // 19 Port + iface.add_method(MethodSignature::new(reg)); // 20 Suspicious (unused) + iface +} + +fn create_uri_dynamic( + reg: &std::sync::Arc, + raw: &str, +) -> windows_core::Result { + let factory = WinRTValue::from_activation_factory(&HSTRING::from("Windows.Foundation.Uri")) + .expect("Windows.Foundation.Uri activation factory"); + let uri_factory = factory + .cast(&IUriRuntimeClassFactory::IID) + .expect("IUriRuntimeClassFactory"); + let mut iface = InterfaceSignature::define_from_iinspectable( + "IUriRuntimeClassFactory", + IUriRuntimeClassFactory::IID, + reg, + ); + iface.add_method( + MethodSignature::new(reg) + .add_in(reg.hstring()) + .add_out(reg.object()), + ); + let uri_factory_obj = uri_factory.as_object().expect("factory object"); + let result = iface.methods[6].call_dynamic( + uri_factory_obj.as_raw(), + &[WinRTValue::HString(HSTRING::from(raw))], + )?; + Ok(result[0].clone()) +} + +fn property_value_statics_signature(reg: &std::sync::Arc) -> InterfaceSignature { + let statics_iid = GUID::from_u128(0x629BDBC8_D932_4FF4_96B9_8D96C5C1E858); + let mut iface = + InterfaceSignature::define_from_iinspectable("IPropertyValueStatics", statics_iid, reg); + for _ in 0..4 { + iface.add_method(MethodSignature::new(reg)); // 6 CreateEmpty through 9 CreateUInt16 + } + iface.add_method( + MethodSignature::new(reg) + .add_in(reg.i32_type()) + .add_out(reg.object()), + ); // 10 CreateInt32 + for _ in 0..6 { + iface.add_method(MethodSignature::new(reg)); // 11 CreateUInt32 through 16 CreateChar16 + } + iface.add_method( + MethodSignature::new(reg) + .add_in(reg.bool_type()) + .add_out(reg.object()), + ); // 17 CreateBoolean + iface.add_method( + MethodSignature::new(reg) + .add_in(reg.hstring()) + .add_out(reg.object()), + ); // 18 CreateString + iface +} + +fn property_value_signature(reg: &std::sync::Arc) -> InterfaceSignature { + let ipv_iid = GUID::from_u128(0x4BD682DD_7554_40E9_9A9B_82654EDE7E62); + let mut iface = InterfaceSignature::define_from_iinspectable("IPropertyValue", ipv_iid, reg); + iface.add_method(MethodSignature::new(reg).add_out(reg.i32_type())); // 6 get_Type + iface.add_method(MethodSignature::new(reg).add_out(reg.bool_type())); // 7 get_IsNumericScalar + for _ in 0..3 { + iface.add_method(MethodSignature::new(reg)); // 8 GetUInt8 through 10 GetUInt16 + } + iface.add_method(MethodSignature::new(reg).add_out(reg.i32_type())); // 11 GetInt32 + for _ in 0..6 { + iface.add_method(MethodSignature::new(reg)); // 12 GetUInt32 through 17 GetChar16 + } + iface.add_method(MethodSignature::new(reg).add_out(reg.bool_type())); // 18 GetBoolean + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 19 GetString + iface +} + +fn create_property_value( + statics: &WinRTValue, + iface: &InterfaceSignature, + vtable_index: usize, + arg: WinRTValue, +) -> windows_core::Result { + let statics_obj = statics.as_object().expect("statics object"); + Ok(iface.methods[vtable_index].call_dynamic(statics_obj.as_raw(), &[arg])?[0].clone()) +} + +fn as_property_value(value: &WinRTValue) -> WinRTValue { + value + .cast(&IPropertyValue::IID) + .expect("IPropertyValue interface") +} + +fn check_winrt_uri_factory_dynamic_properties_are_golden() -> windows_core::Result<()> { + let reg = MetadataTable::new(); + let uri = create_uri_dynamic(®, "https://www.example.com/a/b?q=2#frag")?; + let uri_obj = uri.as_object().expect("uri object"); + let iface = uri_runtime_class_signature(®); + + assert_hstring( + &iface.methods[6].call_dynamic(uri_obj.as_raw(), &[])?[0], + "https://www.example.com/a/b?q=2#frag", + ); + assert_hstring( + &iface.methods[8].call_dynamic(uri_obj.as_raw(), &[])?[0], + "example.com", + ); + assert_hstring( + &iface.methods[10].call_dynamic(uri_obj.as_raw(), &[])?[0], + "#frag", + ); + assert_hstring( + &iface.methods[11].call_dynamic(uri_obj.as_raw(), &[])?[0], + "www.example.com", + ); + assert_hstring( + &iface.methods[13].call_dynamic(uri_obj.as_raw(), &[])?[0], + "/a/b", + ); + assert_hstring( + &iface.methods[14].call_dynamic(uri_obj.as_raw(), &[])?[0], + "?q=2", + ); + assert_hstring( + &iface.methods[16].call_dynamic(uri_obj.as_raw(), &[])?[0], + "https://www.example.com/a/b?q=2#frag", + ); + assert_hstring( + &iface.methods[17].call_dynamic(uri_obj.as_raw(), &[])?[0], + "https", + ); + assert_eq!( + iface.methods[19].call_dynamic(uri_obj.as_raw(), &[])?[0] + .as_i32() + .unwrap(), + 443 + ); + + Ok(()) +} + +fn check_winrt_uri_empty_path_is_golden() -> windows_core::Result<()> { + let reg = MetadataTable::new(); + let uri = create_uri_dynamic(®, "https://www.example.com")?; + let uri_obj = uri.as_object().expect("uri object"); + let iface = uri_runtime_class_signature(®); + + assert_hstring( + &iface.methods[6].call_dynamic(uri_obj.as_raw(), &[])?[0], + "https://www.example.com/", + ); + assert_hstring( + &iface.methods[13].call_dynamic(uri_obj.as_raw(), &[])?[0], + "/", + ); + assert_hstring( + &iface.methods[14].call_dynamic(uri_obj.as_raw(), &[])?[0], + "", + ); + assert_hstring( + &iface.methods[17].call_dynamic(uri_obj.as_raw(), &[])?[0], + "https", + ); + assert_hstring( + &iface.methods[18].call_dynamic(uri_obj.as_raw(), &[])?[0], + "", + ); + assert_eq!( + iface.methods[19].call_dynamic(uri_obj.as_raw(), &[])?[0] + .as_i32() + .unwrap(), + 443 + ); + + Ok(()) +} + +fn check_property_value_dynamic_scalar_round_trips_are_golden() -> windows_core::Result<()> { + let reg = MetadataTable::new(); + let statics = + WinRTValue::from_activation_factory(&HSTRING::from("Windows.Foundation.PropertyValue")) + .expect("PropertyValue activation factory") + .cast(&GUID::from_u128(0x629BDBC8_D932_4FF4_96B9_8D96C5C1E858)) + .expect("IPropertyValueStatics"); + let statics_iface = property_value_statics_signature(®); + let value_iface = property_value_signature(®); + + let int_value = as_property_value(&create_property_value( + &statics, + &statics_iface, + 10, + WinRTValue::I32(-12345), + )?); + let int_obj = int_value.as_object().expect("IPropertyValue int object"); + assert_eq!( + value_iface.methods[6].call_dynamic(int_obj.as_raw(), &[])?[0] + .as_i32() + .unwrap(), + 4 + ); + assert_bool( + &value_iface.methods[7].call_dynamic(int_obj.as_raw(), &[])?[0], + false, + ); + assert_eq!( + value_iface.methods[11].call_dynamic(int_obj.as_raw(), &[])?[0] + .as_i32() + .unwrap(), + -12345 + ); + + let bool_value = as_property_value(&create_property_value( + &statics, + &statics_iface, + 17, + WinRTValue::Bool(true), + )?); + let bool_obj = bool_value.as_object().expect("IPropertyValue bool object"); + assert_eq!( + value_iface.methods[6].call_dynamic(bool_obj.as_raw(), &[])?[0] + .as_i32() + .unwrap(), + 11 + ); + assert_bool( + &value_iface.methods[7].call_dynamic(bool_obj.as_raw(), &[])?[0], + false, + ); + assert_bool( + &value_iface.methods[18].call_dynamic(bool_obj.as_raw(), &[])?[0], + true, + ); + + let string_value = as_property_value(&create_property_value( + &statics, + &statics_iface, + 18, + WinRTValue::HString(HSTRING::from("dynwinrt regression")), + )?); + let string_obj = string_value + .as_object() + .expect("IPropertyValue string object"); + assert_eq!( + value_iface.methods[6].call_dynamic(string_obj.as_raw(), &[])?[0] + .as_i32() + .unwrap(), + 12 + ); + assert_bool( + &value_iface.methods[7].call_dynamic(string_obj.as_raw(), &[])?[0], + false, + ); + assert_hstring( + &value_iface.methods[19].call_dynamic(string_obj.as_raw(), &[])?[0], + "dynwinrt regression", + ); + + Ok(()) +} + +fn check_property_value_dynamic_type_mismatch_returns_golden_error() -> windows_core::Result<()> { + let reg = MetadataTable::new(); + let statics = + WinRTValue::from_activation_factory(&HSTRING::from("Windows.Foundation.PropertyValue")) + .expect("PropertyValue activation factory") + .cast(&GUID::from_u128(0x629BDBC8_D932_4FF4_96B9_8D96C5C1E858)) + .expect("IPropertyValueStatics"); + let statics_iface = property_value_statics_signature(®); + let value_iface = property_value_signature(®); + let int_value = as_property_value(&create_property_value( + &statics, + &statics_iface, + 10, + WinRTValue::I32(7), + )?); + let int_obj = int_value.as_object().expect("IPropertyValue int object"); + + let err = value_iface.methods[19] + .call_dynamic(int_obj.as_raw(), &[]) + .expect_err("GetString on an Int32 PropertyValue should fail"); + assert_eq!(err.code(), HRESULT(0x80028CA0u32 as i32)); + + Ok(()) +} + +fn check_geopoint_struct_layout_and_dynamic_position_round_trip_are_golden() +-> windows_core::Result<()> { + let reg = MetadataTable::new(); + let f64_type = reg.f64_type(); + let geo_type = reg.struct_type( + "Windows.Devices.Geolocation.BasicGeoposition", + &[f64_type.clone(), f64_type.clone(), f64_type], + ); + assert_eq!(geo_type.size_of(), 24); + assert_eq!(geo_type.align_of(), 8); + assert_eq!(geo_type.field_offset(0), 0); + assert_eq!(geo_type.field_offset(1), 8); + assert_eq!(geo_type.field_offset(2), 16); + + let mut geo_value = geo_type.default_value(); + assert_eq!(geo_value.get_field::(0), 0.0); + assert_eq!(geo_value.get_field::(1), 0.0); + assert_eq!(geo_value.get_field::(2), 0.0); + geo_value.set_field(0, 47.643); + geo_value.set_field(1, -122.131); + geo_value.set_field(2, 100.5); + + let projected = Geopoint::Create(BasicGeoposition { + Latitude: 47.643, + Longitude: -122.131, + Altitude: 100.5, + })?; + let projected_position = projected.Position()?; + assert!((projected_position.Latitude - 47.643).abs() < 1e-6); + assert!((projected_position.Longitude + 122.131).abs() < 1e-6); + assert!((projected_position.Altitude - 100.5).abs() < 1e-6); + + let factory = + WinRTValue::from_activation_factory(&HSTRING::from("Windows.Devices.Geolocation.Geopoint")) + .expect("Geopoint activation factory") + .cast(&IGeopointFactory::IID) + .expect("IGeopointFactory"); + let mut factory_iface = InterfaceSignature::define_from_iinspectable( + "IGeopointFactory", + IGeopointFactory::IID, + ®, + ); + factory_iface.add_method( + MethodSignature::new(®) + .add_in(geo_type.clone()) + .add_out(reg.object()), + ); + let factory_obj = factory.as_object().expect("factory object"); + let created = factory_iface.methods[6] + .call_dynamic(factory_obj.as_raw(), &[WinRTValue::Struct(geo_value)])?; + let geopoint: IGeopoint = created[0].as_object().expect("Geopoint object").cast()?; + + let mut geopoint_iface = + InterfaceSignature::define_from_iinspectable("IGeopoint", IGeopoint::IID, ®); + geopoint_iface.add_method(MethodSignature::new(®).add_out(geo_type)); + let position = geopoint_iface.methods[6].call_dynamic(geopoint.as_raw(), &[])?; + let data = position[0].as_struct().expect("BasicGeoposition struct"); + assert!((data.get_field::(0) - 47.643).abs() < 1e-6); + assert!((data.get_field::(1) + 122.131).abs() < 1e-6); + assert!((data.get_field::(2) - 100.5).abs() < 1e-6); + + Ok(()) +} + +#[test] +fn winrt_regression_harness_golden_behaviors() -> windows_core::Result<()> { + init_winrt(); + + check_winrt_uri_factory_dynamic_properties_are_golden()?; + check_winrt_uri_empty_path_is_golden()?; + check_property_value_dynamic_scalar_round_trips_are_golden()?; + check_property_value_dynamic_type_mismatch_returns_golden_error()?; + check_geopoint_struct_layout_and_dynamic_position_round_trip_are_golden()?; + + Ok(()) +} diff --git a/tools/dynwinrt-codegen/src/codegen/com.rs b/tools/dynwinrt-codegen/src/codegen/com.rs new file mode 100644 index 00000000..f22223ed --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com.rs @@ -0,0 +1,1925 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Classic-COM (option A) code generation. +//! +//! This module generates natural TypeScript/JS wrappers for IUnknown-rooted +//! Win32 COM interfaces described in Windows.Win32.winmd. It intentionally +//! keeps a **separate pipeline** from the WinRT projection: classic COM has +//! meaningfully different semantics (IUnknown base offset of 3 vs 6, HRESULT +//! throw-on-failure, `CoCreateInstance` activation, no IReference/async +//! projection) so mixing them into the existing IR would obscure both paths. +//! +//! What we emit today (phase 1): +//! - `.js`: registration via `DynWinRtType.registerInterfaceUnknown` +//! + a natural class with camelCase methods and static `create()` / +//! `_fromNative()`. +//! - `.d.ts`: PascalCase class, camelCase methods, opaque +//! handle typedefs (HWND etc.) as `bigint | Buffer`, HRESULT returns +//! projected to `void` (throwing on failure via the runtime). +//! - Per-enum sibling files for each enum referenced by any method parameter. + +use std::collections::BTreeSet; + +use crate::meta::{ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; +use crate::types::TypeMeta; + +/// A rendered classic-COM output: primary `.js` + `.d.ts` for the interface, +/// plus zero or more sibling files (one `.js` + `.d.ts` per referenced enum). +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ComGeneratedOutput { + pub js: String, + pub dts: String, + /// Additional files (filename → content). Includes each enum's `.js` and + /// `.d.ts`. Stable-sorted by filename for deterministic output. + pub extra_files: Vec<(String, String)>, +} + +// --------------------------------------------------------------------------- +// Public entry +// --------------------------------------------------------------------------- + +/// Generate the `.js` + `.d.ts` for a classic-COM interface. +/// +/// `winmd_paths` is the semicolon-separated list of `.winmd` files loaded by +/// the generator. Interop `*Interop` interfaces consult these winmds FIRST +/// to resolve the projected WinRT runtime class's default IID; if that fails +/// (e.g. the caller only passed Win32 metadata), the generator falls back to +/// the NEWEST installed `UnionMetadata\\Windows.winmd`. If the target +/// IID still cannot be resolved for a confirmed interop shape, generation +/// **fails loudly** with `Err(...)` — the generator must never emit a NULL +/// riid that would silently break the wrapper at runtime. +pub fn generate_com_interface_files( + meta: &ComInterfaceMeta, + winmd_paths: &str, +) -> Result { + // Detect whether this is a `*Interop` interface whose every method has the + // `(HWND, [HSTRING…,] REFIID, out void**)` GetForWindow shape. When so, we + // emit natural signatures that hide the REFIID + void** — the caller only + // supplies the natural in-params, and the wrapper returns the projected + // WinRT object. We also emit a companion runtime-class file that provides + // the ergonomic `.getForWindow(hwnd)` static surface. + let interop = detect_interop(meta, winmd_paths)?; + + let js = render_js(meta, interop.as_ref()); + let dts = render_dts(meta, interop.as_ref()); + + // Per-enum sibling files (referenced by parameter types). + let mut extra_files: Vec<(String, String)> = Vec::new(); + for en in &meta.referenced_enums { + if let TypeMeta::Enum { name, .. } = en { + let (enum_js, enum_dts) = render_enum_files(en); + extra_files.push((format!("{}.js", name), enum_js)); + extra_files.push((format!("{}.d.ts", name), enum_dts)); + } + } + + // Companion projected-class files: only when the interop resolved to a + // real WinRT runtime class. This emits a natural `.js`/.d.ts + // with a static `getForWindow(hwnd)` and a `.runtimeClassName` getter, + // giving the E2E a MEANINGFUL surface to exercise on the returned object. + if let Some(ref info) = interop { + if let Some((cjs, cdts)) = render_projected_class_files(meta, info) { + extra_files.push((format!("{}.js", info.class_name), cjs)); + extra_files.push((format!("{}.d.ts", info.class_name), cdts)); + } + } + + extra_files.sort_by(|a, b| a.0.cmp(&b.0)); + + Ok(ComGeneratedOutput { js, dts, extra_files }) +} + +// --------------------------------------------------------------------------- +// Interop detection +// --------------------------------------------------------------------------- + +/// Metadata for a single method within a `*Interop` interface. Each method +/// is EITHER interop-shaped (`riid + void**` trailing pair to hide) OR plain +/// (no special handling — HWND setter etc.). +#[derive(Debug, Clone)] +struct InteropMethod { + /// Original method name (PascalCase, e.g. "GetForWindow"). + name: String, + /// camelCase method name for JS/TS emission. + camel: String, + /// Absolute vtable slot. + vtable_index: usize, + /// `Some(natural_params)` when the method has the interop shape (last two + /// ABI params are `(REFIID, out void**)`), i.e. the surface should hide + /// them. `None` means "plain" — emit like a normal classic-COM method. + natural_params: Option>, + /// For plain methods, the underlying `MethodMeta` so we can reuse the + /// existing emission path. + plain: Option, + /// Underlying method's docstring, if any. + _doc: Option, +} + +/// Interop-level metadata for the whole interface. +#[derive(Debug, Clone)] +struct InteropInfo { + /// Every method — some tagged interop-shape, some plain. + methods: Vec, + /// The projected WinRT runtime-class name (derived from the interop + /// interface: `ISystemMediaTransportControlsInterop` → + /// `SystemMediaTransportControls`). + class_name: String, + /// Full namespace of the projected runtime class in the WinRT metadata + /// (e.g. `"Windows.Media"`). Empty when auto-resolution failed. + class_namespace: String, + /// Default interface IID of the projected runtime class, used as the + /// REFIID in the interop call. Empty when auto-resolution failed. + target_iid: String, +} + +/// Recognise an interop method: last two ABI parameters are +/// `(In: REFIID /* Guid* */, Out: Object /* void** */)`, HRESULT return. +/// +/// The trailing in-param is treated as a hidden REFIID **only when we're +/// confident it's actually one** — either its metadata type projects to +/// `TypeMeta::Guid` (System.Guid) OR its parameter name (case-insensitive) +/// is exactly `riid` / `iid`. A method whose last in-param is a real +/// application-level Object (a live COM interface pointer) MUST NOT be +/// interpreted as interop-shaped, since dropping that argument would silently +/// break the wrapper. See Fix 3 in the accompanying code-review notes. +fn method_is_interop_shape(m: &MethodMeta) -> Option> { + // Must return HRESULT + match &m.return_type { + Some(t) if is_hresult(t) => {} + _ => return None, + } + // Enforce the exact structural shape in the ORIGINAL parameter order: + // [in]... [in REFIID] [out void**] + // i.e. every param except the last is [in], the last is the sole [out], + // and the second-to-last [in] is the REFIID. Filtering into direction + // buckets would have lost this ordering and could misclassify methods + // where the [out] param appears mid-signature or where the REFIID is + // not at the tail of the in-list. + if m.params.len() < 2 { + return None; + } + let last_idx = m.params.len() - 1; + let out_param = &m.params[last_idx]; + if out_param.direction != ParamDirection::Out { + return None; + } + if !matches!(out_param.typ, TypeMeta::Object) { + return None; + } + // All preceding params must be [in]. + for p in &m.params[..last_idx] { + if p.direction != ParamDirection::In { + return None; + } + } + // The last of those [in] params is the REFIID. + let riid = &m.params[last_idx - 1]; + let is_riid = match &riid.typ { + TypeMeta::Guid => true, + TypeMeta::Object => { + let n = riid.name.to_ascii_lowercase(); + n == "riid" || n == "iid" + } + _ => false, + }; + if !is_riid { + return None; + } + // Natural params: every [in] EXCEPT the trailing REFIID, preserving + // original order. + let natural: Vec = m.params[..last_idx - 1].iter().cloned().collect(); + Some(natural) +} + +/// Best-effort detection: an interface qualifies as an "interop" iff +/// (a) its name ends with `"Interop"`, and +/// (b) at least ONE method matches the interop shape. +/// +/// Any interop-shape methods get natural signatures (hide riid + void**); +/// the rest fall back to the normal classic-COM emission. +/// +/// Returns: +/// - `Ok(None)` — not an interop interface. +/// - `Ok(Some(info))` — an interop interface with a resolved target IID. +/// - `Err(msg)` — an interop interface was detected but the projected WinRT +/// runtime class's default IID could not be resolved from either the +/// passed winmds or the newest installed Windows SDK. This is a hard +/// failure by design: silently emitting a NULL riid would produce a +/// generated wrapper that fails only at runtime, on a machine the +/// developer may not have. +fn detect_interop( + meta: &ComInterfaceMeta, + winmd_paths: &str, +) -> Result, String> { + let iface = &meta.interface; + if !iface.name.ends_with("Interop") { + return Ok(None); + } + if iface.methods.is_empty() { + return Ok(None); + } + let mut methods = Vec::with_capacity(iface.methods.len()); + let mut has_interop_method = false; + for m in &iface.methods { + match method_is_interop_shape(m) { + Some(natural) => { + has_interop_method = true; + methods.push(InteropMethod { + name: m.name.clone(), + camel: camel_case(&m.name), + vtable_index: m.vtable_index, + natural_params: Some(natural), + plain: None, + _doc: m.doc.clone(), + }); + } + None => { + methods.push(InteropMethod { + name: m.name.clone(), + camel: camel_case(&m.name), + vtable_index: m.vtable_index, + natural_params: None, + plain: Some(m.clone()), + _doc: m.doc.clone(), + }); + } + } + } + if !has_interop_method { + return Ok(None); + } + + // Derive the WinRT runtime-class simple name from the interop name: + // strip leading `I` and trailing `Interop`. + let stripped_i = iface.name.strip_prefix('I').unwrap_or(&iface.name); + let class_name = stripped_i + .strip_suffix("Interop") + .unwrap_or(stripped_i) + .to_string(); + + // Auto-resolve the projected class's default interface IID. Try the winmds + // the generator was actually given FIRST (portable — respects an integrator + // who pinned a specific SDK via --ref); if that fails, discover the newest + // installed Windows SDK winmd. If BOTH fail, we cannot generate a working + // interop wrapper — fail loudly rather than emit a NULL riid. + let (class_namespace, target_iid) = match resolve_projected_default_iid( + winmd_paths, + &class_name, + ) { + Some((ns, _iface_name, iid)) => (ns, iid), + None => { + return Err(format!( + "Classic-COM interop generator: cannot resolve default IID for the projected \ + WinRT runtime class `{cls}` (derived from `{iface}`). \ + Neither the winmds passed to the generator ({paths:?}) nor the newest installed \ + `C:\\Program Files (x86)\\Windows Kits\\10\\UnionMetadata\\\\Windows.winmd` \ + contains a WinRT runtime class of that name with a resolvable default interface. \ + Pass the correct Windows.winmd via --ref or install a recent Windows SDK.", + cls = class_name, + iface = iface.name, + paths = winmd_paths, + )); + } + }; + + Ok(Some(InteropInfo { + methods, + class_name, + class_namespace, + target_iid, + })) +} + +/// Auto-resolve the target class + IID for interop projection. +/// +/// Consults, in order: +/// 1. The winmd paths currently loaded by the generator (`winmd_paths`). +/// 2. The NEWEST installed `Windows Kits\10\UnionMetadata\\Windows.winmd` +/// (dynamically discovered — NOT pinned to a specific SDK version). +/// +/// Returns `None` when the class cannot be found in either source. +fn resolve_projected_default_iid( + winmd_paths: &str, + simple_class_name: &str, +) -> Option<(String, String, String)> { + // First: try the winmds the generator was given. When integrators pass + // pinned Windows metadata via --ref/--ref-list this preserves reproducibility. + if !winmd_paths.is_empty() { + if let Some(result) = + crate::meta::find_runtime_class_default_iid(winmd_paths, simple_class_name) + { + return Some(result); + } + } + // Fallback: newest installed SDK. This makes the generator portable across + // machines that have any recent SDK installed, not just `10.0.26100.0`. + let sdk_winmd = crate::meta::discover_newest_windows_winmd()?; + // Avoid re-loading if the SDK path was already among the passed winmds. + if winmd_paths + .split(';') + .any(|p| p.eq_ignore_ascii_case(&sdk_winmd)) + { + return None; + } + crate::meta::find_runtime_class_default_iid(&sdk_winmd, simple_class_name) +} + + +// --------------------------------------------------------------------------- +// .js rendering +// --------------------------------------------------------------------------- + +fn render_js(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { + let iface = &meta.interface; + let iid = &iface.iid; + let name = &iface.name; + + let mut out = String::new(); + out.push_str("// Generated by dynwinrt-codegen — do not edit\n"); + + // Imports (runtime + any referenced enums) + out.push_str(&format!( + "import {{ DynWinRtType, DynWinRtMethodSig, DynWinRtValue, WinGuid }} from '{}';\n", + crate::codegen::project::get_import_name() + )); + for en in enum_import_names(meta) { + out.push_str(&format!("import {{ {} }} from './{}.js';\n", en, en)); + } + // Interop: import the projected class so we can wrap the returned object. + if let Some(info) = interop { + if !info.target_iid.is_empty() { + out.push_str(&format!( + "import {{ {cls} }} from './{cls}.js';\n", + cls = info.class_name + )); + } + } + out.push('\n'); + + out.push_str(&format!( + "export const IID_{name} = WinGuid.parse('{iid}');\n", + name = name, + iid = iid + )); + // For interop wrappers with a resolved target IID, also emit the target + // interface IID as a private constant used by the getForWindow call. + if let Some(info) = interop { + if !info.target_iid.is_empty() { + out.push_str(&format!( + "const IID_{cls}_default = WinGuid.parse('{iid}');\n", + cls = info.class_name, + iid = info.target_iid, + )); + } + } + out.push('\n'); + + // Interface registration (lazy). Base-aware: IUnknown-rooted uses + // registerInterfaceUnknown (first user slot = 3); IInspectable-rooted + // uses registerInterface (first user slot = 6). + let register_fn = if meta.is_iunknown_rooted { + "registerInterfaceUnknown" + } else { + "registerInterface" + }; + let cache_var = format!("_{name}Cache", name = name); + let iface_var = format!("_{name}", name = name); + out.push_str(&format!("let {cache_var};\n", cache_var = cache_var)); + out.push_str(&format!( + "const {iface_var} = new Proxy({{}}, {{\n get(_target, prop) {{\n {cache_var} ??= DynWinRtType.{register_fn}('{name}', IID_{name})\n", + iface_var = iface_var, + cache_var = cache_var, + name = name, + register_fn = register_fn, + )); + for m in &iface.methods { + out.push_str(&format!( + " .addMethod('{}', {})\n", + m.name, + build_method_sig_js(m) + )); + } + // Trim trailing newline before closing the block, then close. + if out.ends_with('\n') { + out.truncate(out.len() - 1); + } + out.push_str(";\n"); + out.push_str(&format!( + " const value = {cache_var}[prop];\n return typeof value === 'function' ? value.bind({cache_var}) : value;\n }},\n}});\n", + cache_var = cache_var, + )); + out.push('\n'); + + // Class body + out.push_str(&format!("export class {name} {{\n", name = name)); + out.push_str(" _obj;\n"); + out.push_str(" constructor(obj) { this._obj = obj; }\n"); + out.push_str(&format!( + " static _fromNative(obj) {{ return new {name}(obj); }}\n", + name = name + )); + + if let Some(ref clsid) = meta.coclass_clsid { + // static create() — classic COM CLSID-based activation. + out.push_str(&format!( + " /** Create a new `{name}` via `CoCreateInstance` on `CLSID_{cc}`. */\n", + name = name, + cc = meta.coclass_name.as_deref().unwrap_or("Coclass") + )); + out.push_str(&format!( + " static create() {{\n const _obj = DynWinRtValue.coCreateInstance('{clsid}', IID_{name});\n return new {name}(_obj);\n }}\n", + clsid = clsid, + name = name, + )); + } else if let Some(info) = interop { + if !info.class_namespace.is_empty() { + // static create() — interop activation: activate the projected + // WinRT runtime class's factory, then QI to the interop IID. + let full_class_name = format!("{}.{}", info.class_namespace, info.class_name); + out.push_str(&format!( + " /** Create a new `{name}` by activating the `{full_class_name}` factory and QI'ing to the interop. */\n", + name = name, + full_class_name = full_class_name, + )); + out.push_str(&format!( + " static create() {{\n const factory = DynWinRtValue.activationFactory('{full_class_name}');\n const _obj = factory.cast(IID_{name});\n return new {name}(_obj);\n }}\n", + full_class_name = full_class_name, + name = name, + )); + } + } + + // Emit methods: natural interop shape when available, otherwise pass-through. + if let Some(info) = interop { + for im in &info.methods { + emit_interop_method_js(&mut out, im, &iface_var, info); + } + } else { + for m in &iface.methods { + emit_method_js(&mut out, m, &iface_var); + } + } + out.push_str("}\n"); + out +} + +fn build_method_sig_js(m: &MethodMeta) -> String { + let mut parts = Vec::new(); + for p in &m.params { + if p.direction == ParamDirection::In { + parts.push(format!(".addIn({})", ts_type_expr_js(&p.typ))); + } else if p.direction == ParamDirection::Out { + parts.push(format!(".addOut({})", ts_type_expr_js(&p.typ))); + } else if p.direction == ParamDirection::OutFill { + parts.push(format!(".addOutFill({})", ts_type_expr_js(&p.typ))); + } + } + // Return type of a classic-COM HRESULT method is NOT part of the sig — + // the runtime swallows HRESULT and throws on failure. Only non-HRESULT + // returns are recorded (rare; e.g. IClassFactory::CreateInstance uses + // HRESULT, so most Win32 methods land here). + if let Some(ref rt) = m.return_type { + if !is_hresult(rt) { + parts.push(format!(".addOut({})", ts_type_expr_js(rt))); + } + } + if parts.is_empty() { + "new DynWinRtMethodSig()".to_string() + } else { + format!("new DynWinRtMethodSig(){}", parts.join("")) + } +} + +fn emit_method_js(out: &mut String, m: &MethodMeta, iface_var: &str) { + let camel = camel_case(&m.name); + let in_params: Vec<&ParamMeta> = m + .params + .iter() + .filter(|p| p.direction == ParamDirection::In) + .collect(); + let out_params: Vec<&ParamMeta> = m + .params + .iter() + .filter(|p| p.direction == ParamDirection::Out) + .collect(); + let has_outfill = m + .params + .iter() + .any(|p| p.direction == ParamDirection::OutFill); + + let param_list: Vec = in_params + .iter() + .enumerate() + .map(|(i, p)| js_param_name(&p.name, i)) + .collect(); + + let args_exprs: Vec = in_params + .iter() + .enumerate() + .map(|(i, p)| wrap_arg_js(&p.typ, &js_param_name(&p.name, i))) + .collect(); + + out.push_str(&format!( + " {camel}({params}) {{\n", + camel = camel, + params = param_list.join(", ") + )); + // Project trailing `[out]` params as JS return values, mirroring how the + // WinRT codegen already handles out-params (see + // `codegen/javascript/project/methods.rs` — `is_multi_output` / `invokeAll`). + // OutFill (caller-allocated buffers, e.g. GetPath(LPWSTR, cchMax)) are + // NOT projected — see the TODO note below. + if has_outfill { + out.push_str(" // TODO: caller-allocated [out, sizeis] buffers are not yet projected as returns.\n"); + } + match out_params.len() { + 0 => { + out.push_str(&format!( + " {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", + iface_var = iface_var, + slot = m.vtable_index, + args = args_exprs.join(", ") + )); + } + 1 => { + out.push_str(&format!( + " const _out = {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", + iface_var = iface_var, + slot = m.vtable_index, + args = args_exprs.join(", ") + )); + out.push_str(&format!( + " return {};\n", + unwrap_return_js(&out_params[0].typ, "_out") + )); + } + _ => { + out.push_str(&format!( + " const _r = {iface_var}.method({slot}).invokeAll(this._obj, [{args}]);\n", + iface_var = iface_var, + slot = m.vtable_index, + args = args_exprs.join(", ") + )); + let items: Vec = out_params + .iter() + .enumerate() + .map(|(i, p)| unwrap_return_js(&p.typ, &format!("_r[{i}]"))) + .collect(); + out.push_str(&format!(" return [{}];\n", items.join(", "))); + } + } + out.push_str(" }\n"); +} + +/// Unwrap the `DynWinRtValue` result of a method invocation into a natural JS +/// value, according to the `[out]` param's declared type. Mirrors the WinRT +/// codegen's `convert_return` for the primitive/GUID/enum/handle cases; +/// Object/Interface/RuntimeClass currently return the raw `DynWinRtValue` +/// (caller can `.cast(IID)` to bridge to another wrapper). +fn unwrap_return_js(t: &TypeMeta, expr: &str) -> String { + if is_win32_bool(t) { + // Win32 BOOL marshals as i32 at the ABI; project as JS boolean. + return format!("({expr}.toNumber() !== 0)"); + } + if handle_type_name(t).is_some() { + // Opaque Win32 handle (HWND, PWSTR, etc.) → raw pointer as bigint. + return format!("{expr}.toI64()"); + } + match t { + TypeMeta::Bool => format!("{expr}.toBool()"), + TypeMeta::I8 + | TypeMeta::U8 + | TypeMeta::I16 + | TypeMeta::U16 + | TypeMeta::I32 + | TypeMeta::U32 + | TypeMeta::Char16 => format!("{expr}.toNumber()"), + TypeMeta::I64 | TypeMeta::U64 => format!("{expr}.toI64()"), + TypeMeta::F32 | TypeMeta::F64 => format!("{expr}.toF64()"), + TypeMeta::Guid => format!("{expr}.toGuid().toString()"), + TypeMeta::Enum { underlying, .. } => unwrap_return_js(underlying, expr), + TypeMeta::String => format!("{expr}.toString()"), + // Object / Interface / RuntimeClass / Struct pointer / etc. + // Return the raw DynWinRtValue and let the caller decide (e.g. cast). + _ => expr.to_string(), + } +} + +/// `.d.ts` return-type text for a classic-COM plain method whose HRESULT is +/// swallowed. Projects `[out]` params as the natural return type: 0 outs → +/// `void`, 1 out → that type, N outs → a tuple. +fn dts_return_type_for_outs(m: &MethodMeta) -> String { + let out_params: Vec<&ParamMeta> = m + .params + .iter() + .filter(|p| p.direction == ParamDirection::Out) + .collect(); + match out_params.len() { + 0 => "void".to_string(), + 1 => ts_type_expr_dts(&out_params[0].typ), + _ => { + let items: Vec = out_params + .iter() + .map(|p| ts_type_expr_dts(&p.typ)) + .collect(); + format!("[{}]", items.join(", ")) + } + } +} + +/// Emit an interop method: either natural (hide trailing REFIID + void**) or +/// plain (fall back to the normal classic-COM emission). +fn emit_interop_method_js(out: &mut String, im: &InteropMethod, iface_var: &str, info: &InteropInfo) { + let Some(natural_params) = &im.natural_params else { + // Plain method — reuse the existing pass-through emission. + if let Some(m) = &im.plain { + emit_method_js(out, m, iface_var); + } + return; + }; + let param_list: Vec = natural_params + .iter() + .enumerate() + .map(|(i, p)| js_param_name(&p.name, i)) + .collect(); + + let mut arg_exprs: Vec = natural_params + .iter() + .enumerate() + .map(|(i, p)| wrap_arg_js(&p.typ, &js_param_name(&p.name, i))) + .collect(); + + // The synthesised REFIID pointer. When we have a resolved target IID we + // pass the cached pointer; otherwise the method is unusable (still emitted + // for completeness so `.d.ts` doesn't lie about the surface). + let riid_arg = if !info.target_iid.is_empty() { + format!("DynWinRtValue.iidPointer(IID_{}_default)", info.class_name) + } else { + "DynWinRtValue.pointer(0n)".to_string() + }; + arg_exprs.push(riid_arg); + + out.push_str(&format!( + " {camel}({params}) {{\n", + camel = im.camel, + params = param_list.join(", "), + )); + if !info.target_iid.is_empty() { + out.push_str(&format!( + " const _out = {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", + iface_var = iface_var, + slot = im.vtable_index, + args = arg_exprs.join(", "), + )); + out.push_str(&format!( + " return {cls}._fromNative(_out);\n", + cls = info.class_name, + )); + } else { + // Fallback: no projection available. Return the raw object. + out.push_str(&format!( + " return {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", + iface_var = iface_var, + slot = im.vtable_index, + args = arg_exprs.join(", "), + )); + } + out.push_str(" }\n"); +} + + +// --------------------------------------------------------------------------- +// .d.ts rendering +// --------------------------------------------------------------------------- + +fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { + let iface = &meta.interface; + let name = &iface.name; + + let mut out = String::new(); + out.push_str("// Generated by dynwinrt-codegen — do not edit\n"); + // Import Buffer type hint via `import type` from Node ambient — HWND uses `Buffer`. + // Node.js `Buffer` is a global type; no import needed. We DO import enum types. + for en in enum_import_names(meta) { + out.push_str(&format!("import {{ {} }} from './{}.js';\n", en, en)); + } + // Interop: import the projected class declaration so return types resolve. + if let Some(info) = interop { + if !info.target_iid.is_empty() { + out.push_str(&format!( + "import {{ {cls} }} from './{cls}.js';\n", + cls = info.class_name + )); + } + } + out.push('\n'); + + // Emit typedef aliases for handles seen in method parameters. + let handle_aliases = collect_handle_aliases(meta); + for h in &handle_aliases { + out.push_str(&format!( + "/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */\nexport type {h} = bigint | Buffer;\n", + h = h + )); + } + if !handle_aliases.is_empty() { + out.push('\n'); + } + + out.push_str(&format!("export declare const IID_{name}: unknown;\n\n", name = name)); + + out.push_str(&format!("export declare class {name} {{\n", name = name)); + if meta.coclass_clsid.is_some() { + out.push_str(" /** Create a new instance via the coclass activation path. */\n"); + out.push_str(&format!(" static create(): {name};\n", name = name)); + } else if let Some(info) = interop { + if !info.class_namespace.is_empty() { + out.push_str(&format!( + " /** Activate the projected WinRT class and QI to the interop. */\n static create(): {name};\n", + name = name + )); + } + } + out.push_str(&format!( + " /** Wrap an existing native COM pointer (for QueryInterface bridging). */\n static _fromNative(obj: unknown): {name};\n", + name = name + )); + + if let Some(info) = interop { + // Interop methods: NATURAL signatures for interop-shape methods (no + // riid, no void**). Plain methods fall through to the normal + // classic-COM emission. + for im in &info.methods { + match (&im.natural_params, &im.plain) { + (Some(natural), _) => { + let ts_params: Vec = natural + .iter() + .enumerate() + .map(|(i, p)| { + format!("{}: {}", js_param_name(&p.name, i), ts_type_expr_dts(&p.typ)) + }) + .collect(); + let ret = if !info.target_iid.is_empty() { + info.class_name.clone() + } else { + "unknown".to_string() + }; + out.push_str(&format!( + " {camel}({params}): {ret};\n", + camel = im.camel, + params = ts_params.join(", "), + ret = ret, + )); + } + (None, Some(m)) => { + let camel = camel_case(&m.name); + let in_params: Vec<&ParamMeta> = m + .params + .iter() + .filter(|p| p.direction == ParamDirection::In) + .collect(); + let ts_params: Vec = in_params + .iter() + .enumerate() + .map(|(i, p)| { + format!("{}: {}", js_param_name(&p.name, i), ts_type_expr_dts(&p.typ)) + }) + .collect(); + let ret = match &m.return_type { + None => "void".to_string(), + // HRESULT is swallowed by the runtime (throw on failure). + // Project `[out]` params as the natural return instead. + Some(t) if is_hresult(t) => dts_return_type_for_outs(m), + Some(t) => ts_type_expr_dts(t), + }; + out.push_str(&format!( + " {camel}({params}): {ret};\n", + camel = camel, + params = ts_params.join(", "), + ret = ret, + )); + } + _ => {} + } + } + } else { + for m in &iface.methods { + let camel = camel_case(&m.name); + let in_params: Vec<&ParamMeta> = m + .params + .iter() + .filter(|p| p.direction == ParamDirection::In) + .collect(); + let ts_params: Vec = in_params + .iter() + .enumerate() + .map(|(i, p)| { + format!("{}: {}", js_param_name(&p.name, i), ts_type_expr_dts(&p.typ)) + }) + .collect(); + let ret = match &m.return_type { + None => "void".to_string(), + // HRESULT is swallowed by the runtime (throw on failure). + // Project `[out]` params as the natural return instead. + Some(t) if is_hresult(t) => dts_return_type_for_outs(m), + Some(t) => ts_type_expr_dts(t), + }; + out.push_str(&format!( + " {camel}({params}): {ret};\n", + camel = camel, + params = ts_params.join(", "), + ret = ret, + )); + } + } + out.push_str("}\n"); + out +} + +/// Emit the companion `.js` + `.d.ts` for the projected WinRT +/// runtime class. Provides: +/// - a `static getForWindow(hwnd)` that opens the interop and calls it, +/// returning a natural `` wrapper; +/// - an internal constructor that stores the live COM object; +/// - a `runtimeClassName` getter (via IInspectable::GetRuntimeClassName) — +/// the E2E's proof that the returned object is a live WinRT instance. +fn render_projected_class_files( + meta: &ComInterfaceMeta, + info: &InteropInfo, +) -> Option<(String, String)> { + if info.target_iid.is_empty() || info.class_namespace.is_empty() { + return None; + } + // The interop wrapper file is named after the interface (e.g. + // `IDataTransferManagerInterop.js`). We import from it. + let interop_module = &meta.interface.name; + let full_class_name = format!("{}.{}", info.class_namespace, info.class_name); + + // Pick the primary interop method to expose as the `static getForWindow`. + // Prefer one whose PascalCase name equals "GetForWindow"; otherwise take + // the first interop-shape method. + let primary = info + .methods + .iter() + .find(|im| im.name == "GetForWindow" && im.natural_params.is_some()) + .or_else(|| info.methods.iter().find(|im| im.natural_params.is_some()))?; + let primary_natural = primary.natural_params.as_ref()?; + + // The IInspectable IID is a fixed WinRT constant. + const IID_IINSPECTABLE: &str = "af86e2e0-b12d-4c6a-9c5a-d7aa65101e90"; + + // -- .js -- + let mut js = String::new(); + js.push_str("// Generated by dynwinrt-codegen — do not edit\n"); + js.push_str(&format!( + "import {{ DynWinRtType, DynWinRtMethodSig, WinGuid }} from '{}';\n", + crate::codegen::project::get_import_name() + )); + js.push_str(&format!( + "import {{ {interop} }} from './{interop}.js';\n", + interop = interop_module, + )); + js.push('\n'); + + js.push_str(&format!( + "const IID_IInspectable = WinGuid.parse('{iid}');\n\n", + iid = IID_IINSPECTABLE + )); + // IInspectable registration (lazy) — used to reach GetRuntimeClassName. + // IInspectable is the base itself; its methods live at absolute vtable + // slots 3, 4, 5 (right after IUnknown). Register with the +3 base so that + // `.method(4)` resolves to `GetRuntimeClassName` at the real absolute slot. + js.push_str("let _IInspectableCache;\n"); + js.push_str("const _IInspectable = new Proxy({}, {\n get(_target, prop) {\n"); + js.push_str(" _IInspectableCache ??= DynWinRtType.registerInterfaceUnknown('IInspectable_projected', IID_IInspectable)\n"); + js.push_str(" .addMethod('GetIids', new DynWinRtMethodSig().addOut(DynWinRtType.pointer()).addOut(DynWinRtType.pointer()))\n"); + js.push_str(" .addMethod('GetRuntimeClassName', new DynWinRtMethodSig().addOut(DynWinRtType.hstring()))\n"); + js.push_str(" .addMethod('GetTrustLevel', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type()));\n"); + js.push_str(" const value = _IInspectableCache[prop];\n"); + js.push_str(" return typeof value === 'function' ? value.bind(_IInspectableCache) : value;\n"); + js.push_str(" },\n});\n\n"); + + js.push_str(&format!("export class {cls} {{\n", cls = info.class_name)); + js.push_str(" _obj;\n"); + js.push_str(" constructor(obj) { this._obj = obj; }\n"); + js.push_str(&format!( + " static _fromNative(obj) {{ return new {cls}(obj); }}\n", + cls = info.class_name, + )); + + // Static getForWindow(hwnd) — the high-level natural surface. + let param_list: Vec = primary_natural + .iter() + .enumerate() + .map(|(i, p)| js_param_name(&p.name, i)) + .collect(); + js.push_str(&format!( + " /** Get a `{cls}` for the given HWND via the {interop} interop. */\n", + cls = info.class_name, + interop = interop_module, + )); + js.push_str(&format!( + " static {camel}({params}) {{\n", + camel = primary.camel, + params = param_list.join(", "), + )); + js.push_str(&format!( + " const interop = {interop}.create();\n", + interop = interop_module, + )); + // Call interop.(...naturalArgs) — this returns a + // `` already wrapped via `_fromNative`. + js.push_str(&format!( + " return interop.{camel}({params});\n", + camel = primary.camel, + params = param_list.join(", "), + )); + js.push_str(" }\n"); + + // runtimeClassName getter — IInspectable slot 4 (absolute vtable index). + js.push_str(" /** IInspectable::GetRuntimeClassName — the projected class name. */\n"); + js.push_str(" get runtimeClassName() {\n"); + js.push_str(" return _IInspectable.method(4).getString(this._obj);\n"); + js.push_str(" }\n"); + + js.push_str("}\n"); + + // -- .d.ts -- + let mut dts = String::new(); + dts.push_str("// Generated by dynwinrt-codegen — do not edit\n\n"); + // Handle typedef for HWND (needed for the static getForWindow signature). + let handle_aliases = collect_handle_aliases(meta); + for h in &handle_aliases { + dts.push_str(&format!( + "/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */\nexport type {h} = bigint | Buffer;\n", + h = h + )); + } + if !handle_aliases.is_empty() { + dts.push('\n'); + } + dts.push_str(&format!("export declare class {cls} {{\n", cls = info.class_name)); + dts.push_str(&format!( + " /** Wrap an existing native COM pointer (for QueryInterface bridging). */\n static _fromNative(obj: unknown): {cls};\n", + cls = info.class_name, + )); + let ts_params: Vec = primary_natural + .iter() + .enumerate() + .map(|(i, p)| format!("{}: {}", js_param_name(&p.name, i), ts_type_expr_dts(&p.typ))) + .collect(); + dts.push_str(&format!( + " /** Get a `{cls}` for the given HWND (projected from `{full_class_name}`). */\n", + cls = info.class_name, + full_class_name = full_class_name, + )); + dts.push_str(&format!( + " static {camel}({params}): {cls};\n", + camel = primary.camel, + params = ts_params.join(", "), + cls = info.class_name, + )); + dts.push_str(" /** IInspectable::GetRuntimeClassName — the projected class name. */\n"); + dts.push_str(" get runtimeClassName(): string;\n"); + dts.push_str("}\n"); + + Some((js, dts)) +} + + +fn collect_handle_aliases(meta: &ComInterfaceMeta) -> Vec { + let mut set = BTreeSet::new(); + for m in &meta.interface.methods { + for p in &m.params { + if let Some(h) = handle_type_name(&p.typ) { + set.insert(h); + } + } + } + set.into_iter().collect() +} + +// --------------------------------------------------------------------------- +// Enum sibling files +// --------------------------------------------------------------------------- + +fn render_enum_files(en: &TypeMeta) -> (String, String) { + let (name, members) = match en { + TypeMeta::Enum { name, members, .. } => (name.as_str(), members), + _ => unreachable!(), + }; + + // .js: a frozen object. + let mut js = String::new(); + js.push_str("// Generated by dynwinrt-codegen — do not edit\n"); + js.push_str(&format!("export const {name} = Object.freeze({{\n", name = name)); + for m in members { + js.push_str(&format!(" {}: {},\n", m.name, m.value)); + } + js.push_str("});\n"); + + // .d.ts: a proper enum declaration. + let mut dts = String::new(); + dts.push_str("// Generated by dynwinrt-codegen — do not edit\n"); + dts.push_str(&format!("export declare const enum {name} {{\n", name = name)); + for m in members { + dts.push_str(&format!(" {} = {},\n", m.name, m.value)); + } + dts.push_str("}\n"); + + (js, dts) +} + +fn enum_import_names(meta: &ComInterfaceMeta) -> Vec { + meta.referenced_enums + .iter() + .filter_map(|e| match e { + TypeMeta::Enum { name, .. } => Some(name.clone()), + _ => None, + }) + .collect() +} + +// --------------------------------------------------------------------------- +// Type mapping helpers +// --------------------------------------------------------------------------- + +/// TS type expression for the `.d.ts` surface. +fn ts_type_expr_dts(t: &TypeMeta) -> String { + // Win32 BOOL is a struct with a single `Value: I32` field — the same shape + // as an opaque handle. Special-case it to the natural boolean surface so + // callers can just pass `true`/`false` rather than a bigint. + if is_win32_bool(t) { + return "boolean".into(); + } + if let Some(h) = handle_type_name(t) { + return h; + } + match t { + TypeMeta::Bool => "boolean".into(), + TypeMeta::I8 | TypeMeta::U8 | TypeMeta::I16 | TypeMeta::U16 | TypeMeta::I32 | TypeMeta::U32 + | TypeMeta::F32 | TypeMeta::F64 | TypeMeta::Char16 => "number".into(), + TypeMeta::I64 | TypeMeta::U64 => "bigint".into(), + TypeMeta::String => "string".into(), + TypeMeta::Guid => "string".into(), + TypeMeta::Enum { name, .. } => name.clone(), + TypeMeta::Struct { name, .. } => name.clone(), + // Pointer-to-struct or unknown — opaque bigint|Buffer at the surface. + _ => "bigint | Buffer".into(), + } +} + +/// Runtime type expression for `DynWinRtMethodSig` calls in `.js`. +fn ts_type_expr_js(t: &TypeMeta) -> String { + // Win32 BOOL marshals as a 32-bit int at the ABI (Win32 BOOL is `int`), + // NOT an opaque pointer. Mirrors how enums map to their underlying i32. + if is_win32_bool(t) { + return "DynWinRtType.i32Type()".into(); + } + if handle_type_name(t).is_some() { + return "DynWinRtType.pointer()".into(); + } + match t { + TypeMeta::Bool => "DynWinRtType.boolType()".into(), + TypeMeta::I8 => "DynWinRtType.i8Type()".into(), + TypeMeta::U8 => "DynWinRtType.u8Type()".into(), + TypeMeta::I16 => "DynWinRtType.i16Type()".into(), + TypeMeta::U16 => "DynWinRtType.u16Type()".into(), + TypeMeta::I32 => "DynWinRtType.i32Type()".into(), + TypeMeta::U32 => "DynWinRtType.u32Type()".into(), + TypeMeta::I64 => "DynWinRtType.i64Type()".into(), + TypeMeta::U64 => "DynWinRtType.u64Type()".into(), + TypeMeta::F32 => "DynWinRtType.f32Type()".into(), + TypeMeta::F64 => "DynWinRtType.f64Type()".into(), + TypeMeta::Char16 => "DynWinRtType.char16()".into(), + TypeMeta::String => "DynWinRtType.pointer()".into(), // PCWSTR/PWSTR → opaque + TypeMeta::Guid => "DynWinRtType.guidType()".into(), + TypeMeta::Enum { underlying, .. } => ts_type_expr_js(underlying), + _ => "DynWinRtType.pointer()".into(), + } +} + +fn wrap_arg_js(t: &TypeMeta, var: &str) -> String { + // Win32 BOOL: accept `boolean`/`number`/`bigint` on the surface and + // narrow to an i32 (0/1) at the ABI. Truthy → 1, falsy → 0. Non-nullish + // numerics are preserved so callers passing `1`/`0` still work. + if is_win32_bool(t) { + return format!("DynWinRtValue.i32({var} ? 1 : 0)", var = var); + } + if handle_type_name(t).is_some() { + return format!("DynWinRtValue.pointer({var})", var = var); + } + match t { + TypeMeta::Bool => format!("DynWinRtValue.boolValue({var})", var = var), + TypeMeta::I8 => format!("DynWinRtValue.i8Value({var})", var = var), + TypeMeta::U8 => format!("DynWinRtValue.u8Value({var})", var = var), + TypeMeta::I16 => format!("DynWinRtValue.i16Value({var})", var = var), + TypeMeta::U16 => format!("DynWinRtValue.u16Value({var})", var = var), + TypeMeta::I32 => format!("DynWinRtValue.i32({var})", var = var), + TypeMeta::U32 => format!("DynWinRtValue.u32({var})", var = var), + TypeMeta::I64 => format!("DynWinRtValue.i64(BigInt({var}))", var = var), + TypeMeta::U64 => format!("DynWinRtValue.u64(BigInt({var}))", var = var), + TypeMeta::F32 => format!("DynWinRtValue.f32({var})", var = var), + TypeMeta::F64 => format!("DynWinRtValue.f64({var})", var = var), + TypeMeta::Char16 => format!("DynWinRtValue.char16({var})", var = var), + TypeMeta::String => format!("DynWinRtValue.pointer({var})", var = var), + TypeMeta::Guid => format!("DynWinRtValue.guid(WinGuid.parse({var}))", var = var), + TypeMeta::Enum { underlying, .. } => wrap_arg_js(underlying, var), + _ => format!("DynWinRtValue.pointer({var})", var = var), + } +} + +/// Returns `Some("HWND")` etc. when the given type is a Win32 opaque handle +/// (a struct in `Windows.Win32.Foundation` or similar handle-namespace with a +/// single pointer-shaped `Value` field). Also returns handle names for +/// PWSTR/PCWSTR/HRESULT-family types encountered as parameters (except +/// HRESULT itself which is treated as `void`). +fn handle_type_name(t: &TypeMeta) -> Option { + // BOOL is NOT a handle even though it shape-matches (`{ Value: I32 }`). + // The natural surface is `boolean` (see `is_win32_bool`). + if is_win32_bool(t) { + return None; + } + match t { + TypeMeta::Struct { namespace, name, fields } => { + if !is_win32_handle_namespace(namespace) { + return None; + } + if is_hresult_by_name(namespace, name) { + return None; // HRESULT is not a "handle" — never surface it as one + } + // Handle heuristic: exactly one field named `Value`, of pointer/int type. + if fields.len() == 1 + && fields[0].name == "Value" + && matches!( + fields[0].typ, + TypeMeta::Object + | TypeMeta::U64 + | TypeMeta::I64 + | TypeMeta::U32 + | TypeMeta::I32 + ) + { + return Some(name.clone()); + } + None + } + _ => None, + } +} + +fn is_win32_handle_namespace(ns: &str) -> bool { + ns.starts_with("Windows.Win32.") +} + +fn is_hresult(t: &TypeMeta) -> bool { + matches!( + t, + TypeMeta::Struct { namespace, name, .. } + if is_hresult_by_name(namespace, name) + ) +} + +fn is_hresult_by_name(ns: &str, name: &str) -> bool { + ns == "Windows.Win32.Foundation" && name == "HRESULT" +} + +/// Recognise the Win32 `BOOL` struct (`Windows.Win32.Foundation.BOOL`) — a +/// `{ Value: I32 }` struct whose natural surface is a JS `boolean` but whose +/// ABI is a 32-bit int. Kept as a distinct helper so the surface remains +/// obvious and greppable. +fn is_win32_bool(t: &TypeMeta) -> bool { + matches!( + t, + TypeMeta::Struct { namespace, name, .. } + if namespace == "Windows.Win32.Foundation" && name == "BOOL" + ) +} + +// --------------------------------------------------------------------------- +// Naming helpers +// --------------------------------------------------------------------------- + +fn camel_case(name: &str) -> String { + if name.is_empty() { + return String::new(); + } + let chars: Vec = name.chars().collect(); + // Count the leading uppercase run. + let mut run = 0usize; + while run < chars.len() && chars[run].is_ascii_uppercase() { + run += 1; + } + let mut result = String::with_capacity(name.len()); + if run == 0 { + // Already starts lowercase — return unchanged. + return name.to_string(); + } + if run == chars.len() { + // Fully uppercase (e.g. "URL") — lowercase everything. + for c in &chars { + result.push(c.to_ascii_lowercase()); + } + return result; + } + if run == 1 { + // Simple case: lowercase first char, keep the rest. + result.push(chars[0].to_ascii_lowercase()); + for c in &chars[1..] { + result.push(*c); + } + return result; + } + // Multi-char uppercase run followed by lowercase: last uppercase char is + // the start of the next word. E.g. "IOHandle" -> "ioHandle". + for c in &chars[..run - 1] { + result.push(c.to_ascii_lowercase()); + } + for c in &chars[run - 1..] { + result.push(*c); + } + result +} + +fn js_param_name(raw: &str, index: usize) -> String { + let base = if raw.is_empty() { + format!("arg{}", index) + } else { + raw.to_string() + }; + // Camelize (strip common Hungarian prefixes lightly for prettier surface): + // dwFoo -> foo, pFoo -> foo, lpszFoo -> foo, cbFoo -> foo, iFoo -> foo, hFoo -> foo, hwndFoo -> foo. + let stripped = strip_hungarian(&base); + let mut out = String::with_capacity(stripped.len()); + let mut chars = stripped.chars(); + if let Some(first) = chars.next() { + out.push(first.to_ascii_lowercase()); + } + for c in chars { + out.push(c); + } + // Guard against JS reserved words. + match out.as_str() { + "class" | "return" | "function" | "default" | "this" | "new" | "delete" + | "let" | "const" | "var" | "if" | "else" | "for" | "while" | "do" | "switch" + | "case" | "break" | "continue" | "true" | "false" | "null" | "undefined" + | "in" | "of" | "typeof" | "instanceof" | "throw" | "try" | "catch" | "finally" + | "yield" | "async" | "await" | "with" | "void" | "public" | "private" | "protected" + | "package" | "static" | "import" | "export" | "extends" | "super" | "arguments" => { + format!("{}_", out) + } + _ => out, + } +} + +fn strip_hungarian(s: &str) -> &str { + // Only strip common **multi-character** Hungarian prefixes, and only when + // followed by an uppercase letter (word boundary). Single-letter prefixes + // like `h`, `p`, `i` cause too many false positives on real method-param + // names (e.g. `hwnd` starts with `h` but isn't Hungarian; `pButton` is). + let prefixes = [ + "lpwsz", "pwsz", "lpsz", "psz", "lpsz", "pwstr", "pcwstr", + "hwnd", "dw", "sz", "cb", "cx", "cy", "cw", "ch", "cn", "cc", + "lp", "np", "ph", "pd", "pf", "pv", "ppv", "pp", "wsz", + ]; + for p in prefixes { + if let Some(rest) = s.strip_prefix(p) { + if rest + .chars() + .next() + .map(|c| c.is_ascii_uppercase()) + .unwrap_or(false) + { + return rest; + } + } + } + s +} + +// --------------------------------------------------------------------------- +// Unit tests (fast, no winmd — pure logic) +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn camel_case_basic() { + assert_eq!(camel_case("HrInit"), "hrInit"); + assert_eq!(camel_case("SetProgressValue"), "setProgressValue"); + assert_eq!(camel_case("AddTab"), "addTab"); + assert_eq!(camel_case("URL"), "url"); + assert_eq!(camel_case("IOHandle"), "ioHandle"); + } + + #[test] + fn strip_hungarian_only_at_word_boundary() { + assert_eq!(strip_hungarian("dwReserved"), "Reserved"); + assert_eq!(strip_hungarian("hwndTab"), "Tab"); + // "hwnd" alone must NOT be stripped (no uppercase follow-up). + assert_eq!(strip_hungarian("hwnd"), "hwnd"); + } + + #[test] + fn handle_type_name_recognizes_hwnd_shape() { + let hwnd = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + assert_eq!(handle_type_name(&hwnd).as_deref(), Some("HWND")); + } + + #[test] + fn hresult_is_not_a_handle() { + let hr = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HRESULT".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::I32, + }], + }; + assert!(handle_type_name(&hr).is_none()); + assert!(is_hresult(&hr)); + } + + #[test] + fn non_win32_struct_is_not_a_handle() { + let rect = TypeMeta::Struct { + namespace: "Windows.Foundation".into(), + name: "Rect".into(), + fields: vec![ + crate::types::FieldMeta { name: "X".into(), typ: TypeMeta::F32 }, + crate::types::FieldMeta { name: "Y".into(), typ: TypeMeta::F32 }, + crate::types::FieldMeta { name: "Width".into(), typ: TypeMeta::F32 }, + crate::types::FieldMeta { name: "Height".into(), typ: TypeMeta::F32 }, + ], + }; + assert!(handle_type_name(&rect).is_none()); + } + + // ---- Fix 2 (BOOL → boolean/i32) ---- + + fn win32_bool_struct() -> TypeMeta { + TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "BOOL".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::I32, + }], + } + } + + #[test] + fn win32_bool_is_not_a_handle() { + let b = win32_bool_struct(); + // Sanity: it's the exact shape of a handle (single Value: I32) — the + // special-case must WIN over the generic handle heuristic. + assert!(handle_type_name(&b).is_none(), + "BOOL must not be emitted as an opaque handle typedef"); + } + + #[test] + fn win32_bool_projects_as_boolean_and_i32() { + let b = win32_bool_struct(); + // .d.ts surface: boolean (not `BOOL` or `bigint | Buffer`) + assert_eq!(ts_type_expr_dts(&b), "boolean"); + // .js registration: i32 type (not pointer) + assert_eq!(ts_type_expr_js(&b), "DynWinRtType.i32Type()"); + // .js argument marshalling: truthy→1, falsy→0 as an i32 (not pointer) + assert_eq!( + wrap_arg_js(&b, "fFullscreen"), + "DynWinRtValue.i32(fFullscreen ? 1 : 0)" + ); + } + + // ---- Fix 3 (REFIID-guarded interop heuristic) ---- + + /// Helper: construct a MethodMeta with HRESULT return type. + fn make_hresult() -> TypeMeta { + TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HRESULT".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::I32, + }], + } + } + + #[test] + fn interop_shape_accepts_riid_named_object_trailing_in() { + // Real Windows.Win32 shape: `HRESULT GetForWindow(HWND appWindow, REFIID riid, out void** ppv)`. + // REFIID typically projects to TypeMeta::Object with name "riid". + let m = MethodMeta { + name: "GetForWindow".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "appWindow".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "ppv".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let natural = method_is_interop_shape(&m).expect( + "REFIID-shaped trailing in-param named `riid` must be recognised as interop", + ); + // Natural in-params = every in EXCEPT the trailing REFIID. + assert_eq!(natural.len(), 1); + assert_eq!(natural[0].name, "appWindow"); + } + + #[test] + fn interop_shape_accepts_guid_typed_trailing_in() { + // Some winmds project REFIID as TypeMeta::Guid rather than Object. + let m = MethodMeta { + name: "GetSomething".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "target".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + // Deliberately NOT named "riid" — the type alone is sufficient. + name: "interfaceId".into(), + typ: TypeMeta::Guid, + direction: ParamDirection::In, + }, + ParamMeta { + name: "out".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let natural = method_is_interop_shape(&m) + .expect("System.Guid-typed trailing in-param must be recognised as interop"); + assert_eq!(natural.len(), 1); + assert_eq!(natural[0].name, "target"); + } + + /// FIX 3 REGRESSION: a method returning HRESULT with an [out] Object and a + /// trailing In-Object whose name is NOT `riid`/`iid` (e.g. a real application + /// COM interface pointer like `original`) must NOT be mis-classified as + /// interop-shape. Otherwise the codegen would silently drop the caller's + /// meaningful argument. + #[test] + fn interop_shape_rejects_non_refiid_trailing_object() { + let m = MethodMeta { + name: "CloneWithOriginal".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "context".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + // NOT `riid`/`iid`, NOT Guid — a real COM pointer in-param. + name: "original".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "cloned".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + assert!( + method_is_interop_shape(&m).is_none(), + "trailing in-param `original` is a real Object argument, NOT a REFIID — \ + it must not be dropped by the interop heuristic" + ); + } + + #[test] + fn interop_shape_rejects_iid_named_non_object_param() { + // A parameter named `riid` but typed as a plain I32 is not a REFIID — + // reject rather than silently drop. + let m = MethodMeta { + name: "Weird".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "hwnd".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ParamMeta { + name: "out".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + assert!( + method_is_interop_shape(&m).is_none(), + "an I32 named `riid` is not a REFIID — must be rejected" + ); + } + + // ---- Fix 1 (winmd-derived interop IID, fail-loud on unresolved) ---- + + /// Build a fully synthetic ComInterfaceMeta for an `IFooInterop`-style + /// interface whose derived projected class name (`Foo`) does NOT exist + /// anywhere reachable. The generator must FAIL LOUDLY rather than emit + /// a NULL riid. + #[test] + fn interop_generation_fails_when_target_iid_unresolvable() { + use crate::meta::{ComInterfaceMeta, InterfaceMeta}; + + let iface = InterfaceMeta { + name: "IThisRuntimeClassDoesNotExist_DynWinrtInterop".into(), + namespace: "Windows.Win32.System.WinRT".into(), + iid: "00000000-0000-0000-0000-000000000000".into(), + methods: vec![MethodMeta { + name: "GetForWindow".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "appWindow".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "ppv".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }], + generic_piid: None, + generic_args: Vec::new(), + doc: None, + deprecated: None, + }; + let com = ComInterfaceMeta { + interface: iface, + base_offset: 3, + is_iunknown_rooted: true, + base_chain: vec!["IUnknown".into()], + coclass_clsid: None, + coclass_name: None, + own_methods_start: 3, + referenced_enums: Vec::new(), + }; + // Pass empty winmd_paths — even with the newest-SDK fallback, the + // synthetic class name won't be found anywhere. + let result = generate_com_interface_files(&com, ""); + assert!( + result.is_err(), + "generator must fail loudly when the projected runtime-class IID \ + cannot be resolved; got Ok(_)" + ); + let err = result.unwrap_err(); + assert!( + err.contains("ThisRuntimeClassDoesNotExist_Dynwinrt") + || err.contains("ThisRuntimeClassDoesNotExist_DynWinrt"), + "error must name the class it failed to resolve: {}", + err + ); + assert!( + !err.is_empty(), + "error message must be non-empty (fail-loud contract)" + ); + } + + #[test] + fn non_interop_iunknown_interface_still_generates_without_winmd_lookup() { + // A vanilla IUnknown-rooted interface with no coclass and no + // interop shape must succeed even when we pass empty winmd paths. + use crate::meta::{ComInterfaceMeta, InterfaceMeta}; + let iface = InterfaceMeta { + name: "IMyPlainClassicCom".into(), + namespace: "Windows.Win32.System.Com".into(), + iid: "11111111-2222-3333-4444-555555555555".into(), + methods: vec![MethodMeta { + name: "DoStuff".into(), + vtable_index: 3, + params: vec![], + return_type: Some(make_hresult()), + ..Default::default() + }], + generic_piid: None, + generic_args: Vec::new(), + doc: None, + deprecated: None, + }; + let com = ComInterfaceMeta { + interface: iface, + base_offset: 3, + is_iunknown_rooted: true, + base_chain: vec!["IUnknown".into()], + coclass_clsid: None, + coclass_name: None, + own_methods_start: 3, + referenced_enums: Vec::new(), + }; + let out = generate_com_interface_files(&com, "") + .expect("plain classic-COM codegen must succeed with no winmds"); + assert!(out.js.contains("registerInterfaceUnknown")); + assert!(out.js.contains("method(3)")); + } + + // ---- Fix 4 (classic-COM plain `[out]` param → return-value projection) ---- + + fn plain_iface_with_method(m: MethodMeta) -> crate::meta::ComInterfaceMeta { + use crate::meta::{ComInterfaceMeta, InterfaceMeta}; + let iface = InterfaceMeta { + name: "IHasOut".into(), + namespace: "Windows.Win32.System.Com".into(), + iid: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee".into(), + methods: vec![m], + generic_piid: None, + generic_args: Vec::new(), + doc: None, + deprecated: None, + }; + ComInterfaceMeta { + interface: iface, + base_offset: 3, + is_iunknown_rooted: true, + base_chain: vec!["IUnknown".into()], + coclass_clsid: None, + coclass_name: None, + own_methods_start: 3, + referenced_enums: Vec::new(), + } + } + + #[test] + fn plain_method_single_out_scalar_projects_as_return() { + // Model: `HRESULT GetShowCmd([out] int* pcmd)` — the classic single-out + // int shape. The out-int must become the method's return value. + let m = MethodMeta { + name: "GetShowCmd".into(), + vtable_index: 8, + params: vec![ParamMeta { + name: "pcmd".into(), + typ: TypeMeta::I32, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + // .js: must capture `_out` and return it as a JS number. + assert!( + js.contains("const _out = _IHasOut.method(8).invoke(this._obj, [])"), + ".js must capture invoke() result into _out:\n{}", + js + ); + assert!( + js.contains("return _out.toNumber();"), + ".js must unwrap the I32 out as _out.toNumber():\n{}", + js + ); + // .d.ts: return type must be `number`, not `void`. + assert!( + dts.contains("getShowCmd(): number;"), + ".d.ts must project single-out I32 as `number`:\n{}", + dts + ); + } + + #[test] + fn plain_method_single_out_guid_projects_as_string() { + // Model: `HRESULT GetClassID([out] GUID* pClassID)` (IPersist shape). + let m = MethodMeta { + name: "GetClassID".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "pClassID".into(), + typ: TypeMeta::Guid, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains("const _out = _IHasOut.method(3).invoke(this._obj, [])"), + ".js must capture invoke() result into _out:\n{}", + js + ); + assert!( + js.contains("return _out.toGuid().toString();"), + ".js must unwrap GUID out via .toGuid().toString():\n{}", + js + ); + assert!( + dts.contains("getClassID(): string;"), + ".d.ts must project single-out GUID as `string`:\n{}", + dts + ); + } + + #[test] + fn plain_method_single_out_enum_projects_as_underlying() { + // Model: `HRESULT GetKind([out] MyKind* pk)` where MyKind is an I32 + // enum. Underlying-scalar unwrap → `.toNumber()`; .d.ts uses the enum + // type name. + let m = MethodMeta { + name: "GetKind".into(), + vtable_index: 5, + params: vec![ParamMeta { + name: "pk".into(), + typ: TypeMeta::Enum { + namespace: "Windows.Win32.System.Com".into(), + name: "MyKind".into(), + underlying: Box::new(TypeMeta::I32), + members: Vec::new(), + doc: None, + deprecated: None, + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains("return _out.toNumber();"), + ".js must unwrap enum out via underlying scalar (.toNumber()):\n{}", + js + ); + assert!( + dts.contains("getKind(): MyKind;"), + ".d.ts must project enum out under the enum's declared name:\n{}", + dts + ); + } + + #[test] + fn plain_method_multi_out_uses_invoke_all_and_tuple_return() { + // Model: `HRESULT Q([out] uint32_t* a, [out] BOOL* found)` — two + // trailing out params must flip to `.invokeAll()` and a tuple return. + let m = MethodMeta { + name: "Q".into(), + vtable_index: 6, + params: vec![ + ParamMeta { + name: "a".into(), + typ: TypeMeta::U32, + direction: ParamDirection::Out, + }, + ParamMeta { + name: "found".into(), + typ: TypeMeta::Bool, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains("const _r = _IHasOut.method(6).invokeAll(this._obj, [])"), + ".js multi-out must use .invokeAll():\n{}", + js + ); + assert!( + js.contains("return [_r[0].toNumber(), _r[1].toBool()];"), + ".js multi-out must return a tuple with each out unwrapped:\n{}", + js + ); + assert!( + dts.contains("q(): [number, boolean];"), + ".d.ts multi-out must project a tuple type:\n{}", + dts + ); + } + + #[test] + fn plain_method_zero_out_still_discards_result() { + // No out params: existing behavior — invoke and discard. + let m = MethodMeta { + name: "DoIt".into(), + vtable_index: 4, + params: vec![ParamMeta { + name: "arg".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + !js.contains("const _out ="), + ".js zero-out must not capture invoke() result:\n{}", + js + ); + assert!( + !js.contains("invokeAll"), + ".js zero-out must not use .invokeAll():\n{}", + js + ); + assert!( + js.contains("_IHasOut.method(4).invoke(this._obj,"), + ".js zero-out must call plain .invoke():\n{}", + js + ); + assert!( + dts.contains("doIt(arg: number): void;"), + ".d.ts zero-out must still be `void`:\n{}", + dts + ); + } + + #[test] + fn plain_method_outfill_stays_void_with_todo() { + // Caller-allocated `[out, sizeis]` buffers are NOT yet projected — + // emit a `TODO` comment and keep the surface as `void` so we don't + // half-break anything. + let m = MethodMeta { + name: "GetPath".into(), + vtable_index: 2, + params: vec![ + ParamMeta { + name: "pszFile".into(), + typ: TypeMeta::String, // PWSTR buffer, caller-allocated + direction: ParamDirection::OutFill, + }, + ParamMeta { + name: "cch".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains("TODO: caller-allocated [out, sizeis] buffers"), + ".js OutFill must include a TODO comment:\n{}", + js + ); + assert!( + !js.contains("return _out") && !js.contains("return _r") && !js.contains("return [") , + ".js OutFill must not return anything (avoid half-broken projection):\n{}", + js + ); + assert!( + dts.contains("getPath(cch: number): void;"), + ".d.ts OutFill must stay `void`:\n{}", + dts + ); + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/mod.rs b/tools/dynwinrt-codegen/src/codegen/mod.rs index b70c5a6a..9704d9d1 100644 --- a/tools/dynwinrt-codegen/src/codegen/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/mod.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +pub mod com; pub mod common; pub mod javascript; pub mod python; diff --git a/tools/dynwinrt-codegen/src/main.rs b/tools/dynwinrt-codegen/src/main.rs index f340258d..b8c21038 100644 --- a/tools/dynwinrt-codegen/src/main.rs +++ b/tools/dynwinrt-codegen/src/main.rs @@ -7,6 +7,7 @@ use std::path::Path; use clap::{Parser, Subcommand}; +use dynwinrt_codegen::codegen::com; use dynwinrt_codegen::codegen::python; use dynwinrt_codegen::codegen::render_package_json; use dynwinrt_codegen::codegen::typescript; @@ -268,16 +269,108 @@ fn run() -> Result<(), String> { .map(|s| s.trim()) .filter(|s| !s.is_empty()) .collect(); + + // First: partition into WinRT classes and classic-COM interfaces. let mut classes = Vec::new(); + let mut com_interfaces: Vec = Vec::new(); for cls in &class_names { + if let Some(com_iface) = meta::parse_com_interface(&winmd, ns, cls) { + // Route through classic-COM path when: + // 1) The interface is IUnknown-rooted (base +3), OR + // 2) It is a `*Interop` bridge (name ends with "Interop") — even + // if IInspectable-rooted (base +6), because the emitter + // handles that via `registerInterface`. + if com_iface.is_iunknown_rooted || cls.ends_with("Interop") { + com_interfaces.push(com_iface); + continue; + } + // The type exists as an interface but is IInspectable-rooted and + // not `*Interop` — it's a plain WinRT interface. Those still need + // to go through the WinRT projection pipeline via `parse_class`, + // which will find it if it's the projected surface of a runtime + // class. If not, give a targeted error rather than the misleading + // "Class not found". + if meta::parse_class(&winmd, ns, cls).is_none() { + return Err(format!( + "{}.{} is an IInspectable-rooted WinRT interface, not a runtime class \ + or classic-COM interface. `--class-name` expects a WinRT runtime class, \ + an IUnknown-rooted classic COM interface, or a `*Interop` bridge. \ + If you meant to project a WinRT interface directly, use the full \ + namespace-projection mode (no `--class-name`).", + ns, cls + )); + } + } match meta::parse_class(&winmd, ns, cls) { Some(mut c) => { doc_table.apply_to_class(&mut c); classes.push(c); } - None => return Err(format!("Class {}.{} not found in {}", ns, cls, winmd)), + None => { + return Err(format!("Class {}.{} not found in {}", ns, cls, winmd)); + } } } + + // Fail loud: classic-COM codegen only emits `.js` + `.d.ts` + // today. If the user asked for a different language + // (e.g. `--lang py`) but any of the requested `--class-name` + // inputs resolved to a classic-COM interface, silently writing + // JS files into a Python output directory would produce the + // wrong artifact types with no diagnostic. Reject the + // combination up front. + if lang != "js" && !com_interfaces.is_empty() { + let mut offenders: Vec = Vec::new(); + for ci in &com_interfaces { + offenders.push(format!("{}.{} (classic-COM interface)", + ci.interface.namespace, ci.interface.name)); + } + return Err(format!( + "`--lang {}` is not supported for classic-COM interfaces \ + (they emit only `.js` + `.d.ts` today). \ + Offending inputs: {}. Re-run with `--lang js`, or split the \ + invocation so the WinRT classes are generated with `--lang {}` and \ + the COM classes with `--lang js`.", + lang, + offenders.join(", "), + lang + )); + } + + // Emit classic-COM interfaces (standalone; not wired into WinRT index/barrel). + if !com_interfaces.is_empty() { + for com_iface in &com_interfaces { + let out = com::generate_com_interface_files(com_iface, &winmd) + .map_err(|e| format!("Classic-COM codegen for {} failed: {}", com_iface.interface.name, e))?; + let js_name = format!("{}.js", com_iface.interface.name); + let dts_name = format!("{}.d.ts", com_iface.interface.name); + if !dry_run { + fs::write(output_dir.join(&js_name), &out.js).map_err(|e| { + format!("Failed to write {}: {}", js_name, e) + })?; + fs::write(output_dir.join(&dts_name), &out.dts).map_err(|e| { + format!("Failed to write {}: {}", dts_name, e) + })?; + for (name, content) in &out.extra_files { + fs::write(output_dir.join(name), content).map_err(|e| { + format!("Failed to write {}: {}", name, e) + })?; + } + println!("Generated {} ({} .js/.d.ts + {} extras)", + com_iface.interface.name, + 2, + out.extra_files.len()); + } else { + println!("[dry-run] Would generate {}", com_iface.interface.name); + } + } + // If we only had classic-COM interfaces requested, return early — + // no WinRT index/barrel work to do. + if classes.is_empty() { + return Ok(()); + } + } + add_implicit_js_types(&winmd, &lang, &mut classes); generate_for_types( &winmd, diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index 8182a655..c5dd6783 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -156,6 +156,97 @@ pub fn parse_class(winmd_paths: &str, namespace: &str, name: &str) -> Option Option<(String, String, String)> { + let index = load_index(winmd_paths)?; + // Iterate ALL TypeDefs looking for a runtime class matching `simple_name`. + for def in index.all() { + if def.name() != simple_name { + continue; + } + // A WinRT runtime class extends System.Object AND carries the + // WindowsRuntime flag on its type. Interfaces extend nothing; + // classes extend Object/etc. We filter to actual runtime classes. + if !def.flags().contains(windows_metadata::TypeAttributes::WindowsRuntime) { + continue; + } + // Must be a class (not interface/enum/struct). + if def.flags().contains(windows_metadata::TypeAttributes::Interface) { + continue; + } + let namespace = def.namespace().to_string(); + // Look for the default interface via DefaultAttribute. + for iface_impl in def.interface_impls() { + if !iface_impl.has_attribute("DefaultAttribute") { + continue; + } + let iface_ty = iface_impl.interface(&[]); + let windows_metadata::Type::Name(tn) = &iface_ty else { continue }; + // Resolve concrete (non-generic) interface's IID from its TypeDef. + if !tn.generics.is_empty() { + // Skip generic default interfaces — interop projections don't + // hit them in practice, and the parameterized IID would need + // separate computation. + continue; + } + let iface_def = index.get(&tn.namespace, &tn.name).next()?; + let iid = extract_iid(&iface_def); + if iid.is_empty() { + continue; + } + return Some((namespace, tn.name.clone(), iid)); + } + } + None +} + +/// Discover the NEWEST installed Windows SDK `Windows.winmd` by enumerating the +/// versioned directories under `C:\Program Files (x86)\Windows Kits\10\UnionMetadata` +/// and picking the highest version that actually contains a readable file. +/// +/// Used as a portable fallback by the classic-COM interop code generator when +/// the winmds explicitly loaded for generation don't contain the projected +/// WinRT runtime class. Returns `None` when no SDK is installed. +pub fn discover_newest_windows_winmd() -> Option { + let base = std::path::Path::new(r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata"); + if !base.exists() { + return None; + } + let mut versions: Vec = std::fs::read_dir(base) + .ok()? + .filter_map(|e| e.ok()) + .filter(|e| e.path().is_dir()) + .map(|e| e.file_name().to_string_lossy().to_string()) + .filter(|name| name.starts_with("10.")) + .collect(); + // Sort by dotted-version tuple so `10.0.26100.0` beats `10.0.19041.0`. + versions.sort_by(|a, b| { + let pa: Vec = a.split('.').filter_map(|s| s.parse().ok()).collect(); + let pb: Vec = b.split('.').filter_map(|s| s.parse().ok()).collect(); + pa.cmp(&pb) + }); + for version in versions.iter().rev() { + let winmd_path = base.join(version).join("Windows.winmd"); + if winmd_path.exists() { + return Some(winmd_path.to_string_lossy().to_string()); + } + } + None +} + /// Parse all RuntimeClasses in a given namespace. pub fn parse_namespace(winmd_paths: &str, namespace: &str) -> Vec { let index = match load_index(winmd_paths) { @@ -931,9 +1022,250 @@ fn split_full_name(full_name: &str) -> Option<(&str, &str)> { fn parse_interface(index: &reader::Index, namespace: &str, name: &str) -> Option { let def = index.get(namespace, name).next()?; let iid = extract_iid(&def); - parse_interface_methods(index, &def, name, namespace, &iid, &[]) + parse_interface_methods(index, &def, name, namespace, &iid, &[], 6) +} + +// ========================================================================== +// Classic-COM (option A) support +// ========================================================================== + +/// Rich metadata for a classic-COM interface discovered by walking the +/// `interface_impls()` chain. The `interface.methods` list is the *flattened* +/// method set (own + all inherited, excluding IUnknown's QI/AddRef/Release) +/// with absolute vtable indices — so the codegen renderer never has to think +/// about inheritance again. +/// +/// This is entirely separate from the WinRT `parse_class`/`parse_interface` +/// path so we do not risk regressing IInspectable-based generation. +#[derive(Debug, Clone)] +pub struct ComInterfaceMeta { + /// Flattened interface with own + inherited methods, absolute vtable indices. + pub interface: InterfaceMeta, + /// The vtable index of the first user method in the flattened list: + /// - `3` for any IUnknown-rooted interface (QI/AddRef/Release occupy 0..2). + /// - `6` for any IInspectable-rooted interface (WinRT projection layout). + pub base_offset: usize, + /// `true` iff the inheritance chain terminates at IUnknown. + /// `false` iff it terminates at IInspectable (WinRT-projected classic COM). + pub is_iunknown_rooted: bool, + /// Ordered list of base names from immediate parent up to the root + /// (e.g. `["ITaskbarList2", "ITaskbarList", "IUnknown"]`). + pub base_chain: Vec, + /// If a Win32 coclass matches this interface, the coclass GUID (=CLSID). + pub coclass_clsid: Option, + /// The name of the discovered coclass, e.g. `"TaskbarList"`. + pub coclass_name: Option, + /// The absolute vtable slot of this leaf interface's first *own* method + /// (i.e. the number of methods contributed by all bases plus the root + /// offset). Renderer helper — not core metadata. + pub own_methods_start: usize, + /// Enum types referenced by this interface's methods (directly resolved + /// during metadata parsing so codegen can emit them without a second + /// resolve_dependencies pass over the whole namespace). + pub referenced_enums: Vec, +} + +/// Parse a classic-COM interface (IUnknown-rooted) by name, walking the +/// `interface_impls()` chain to compute absolute vtable slots and flatten +/// inherited methods. +/// +/// Returns `None` if the type isn't found. Unlike `parse_interface`, this +/// function also handles interfaces that inherit from other classic-COM +/// interfaces via `interface_impls()` (the Windows.Win32 winmd doesn't +/// use `[NativeInheritance]` attributes — it uses actual InterfaceImpl rows). +pub fn parse_com_interface( + winmd_paths: &str, + namespace: &str, + name: &str, +) -> Option { + let index = load_index(winmd_paths)?; + parse_com_interface_from_index(&index, namespace, name) +} + +fn parse_com_interface_from_index( + index: &reader::Index, + namespace: &str, + name: &str, +) -> Option { + let def = index.get(namespace, name).next()?; + + // Walk the interface_impls chain: for each base, collect its own method + // count, and stop at IUnknown or IInspectable. Traverse from the leaf up + // so we can compute cumulative offsets. + let mut base_chain: Vec<(String, String, usize)> = Vec::new(); // (ns, name, own_method_count) + let mut cur_ns = namespace.to_string(); + let mut cur_name = name.to_string(); + let mut is_iunknown_rooted = false; + + // Walk up to 32 levels deep as a safety limit (real chains are 3-4 deep). + for _ in 0..32 { + let cur_def = match index.get(&cur_ns, &cur_name).next() { + Some(d) => d, + None => break, + }; + // Find the (single) base via interface_impls. + let base_ii = cur_def.interface_impls().next(); + let base_type = base_ii.map(|ii| ii.interface(&[])); + let base = match base_type { + Some(windows_metadata::Type::Name(tn)) => (tn.namespace.clone(), tn.name.clone()), + _ => break, + }; + // Terminate at IUnknown or IInspectable. + if base.1 == "IUnknown" { + is_iunknown_rooted = true; + base_chain.push(("Windows.Win32.System.Com".to_string(), "IUnknown".to_string(), 0)); + break; + } + if base.1 == "IInspectable" { + base_chain.push(("Windows.Foundation".to_string(), "IInspectable".to_string(), 0)); + break; + } + // Otherwise this base is a real classic-COM interface — count its methods. + let base_def = match index.get(&base.0, &base.1).next() { + Some(d) => d, + None => break, + }; + let own_count = base_def.methods().count(); + base_chain.push((base.0.clone(), base.1.clone(), own_count)); + cur_ns = base.0; + cur_name = base.1; + } + + // Compute root offset (3 for IUnknown, 6 for IInspectable) and the + // absolute vtable slot at which THIS leaf interface's own methods start. + let root_offset = if is_iunknown_rooted { 3 } else { 6 }; + let intermediate_methods: usize = base_chain + .iter() + .filter(|(_, name, _)| name != "IUnknown" && name != "IInspectable") + .map(|(_, _, c)| *c) + .sum(); + let own_methods_start = root_offset + intermediate_methods; + + // Build a flattened method list: iterate the chain top-down (from root + // toward the leaf, i.e. reverse `base_chain`), assigning consecutive + // vtable slots. Base interfaces contribute their own methods first. + // + // Vtable layout: [IUnknown 0..2] [base_N 3..] [base_{N-1} ...] ... [leaf's own]. + let mut methods: Vec = Vec::new(); + + let mut slot_cursor = root_offset; + // Reverse: iterate from the outermost base (closest to IUnknown) down + // toward the immediate parent. + let mut chain_top_down: Vec<&(String, String, usize)> = base_chain.iter().rev().collect(); + // Filter out the root (IUnknown/IInspectable, which contribute 0 own methods to the vtable + // *from the user-visible perspective* — their slots are already counted in `root_offset`). + chain_top_down.retain(|(_, n, _)| n != "IUnknown" && n != "IInspectable"); + + for (base_ns, base_name, _own_count) in chain_top_down { + if let Some(base_iface) = parse_interface_with_offset(index, base_ns, base_name, slot_cursor) { + slot_cursor += base_iface.methods.len(); + methods.extend(base_iface.methods); + } else { + eprintln!( + "warning: could not parse base classic-COM interface {}.{}", + base_ns, base_name + ); + } + } + // Assert the invariant that we lined up correctly. + debug_assert_eq!(slot_cursor, own_methods_start, + "vtable cursor {} != computed own_methods_start {}", slot_cursor, own_methods_start); + + // Now the leaf's own methods + let iid = extract_iid(&def); + let own = parse_interface_methods(index, &def, name, namespace, &iid, &[], slot_cursor)?; + methods.extend(own.methods); + + // Build a mostly-standard InterfaceMeta wrapping the flattened method list. + let interface = InterfaceMeta { + name: name.to_string(), + namespace: namespace.to_string(), + iid: iid.clone(), + methods, + generic_piid: None, + generic_args: Vec::new(), + doc: None, + deprecated: None, + }; + + // Discover coclass CLSID. Heuristic: strip leading `I` from the interface + // name, then strip trailing digits (e.g. `ITaskbarList3` → `TaskbarList3` + // → `TaskbarList`). Return the first coclass matching either variant that + // has a GuidAttribute AND `extends System.ValueType`. + let mut candidates: Vec = Vec::new(); + if let Some(stripped) = name.strip_prefix('I') { + candidates.push(stripped.to_string()); + // Also try trimming trailing digits: TaskbarList3 → TaskbarList + let trimmed: String = stripped + .trim_end_matches(|c: char| c.is_ascii_digit()) + .to_string(); + if trimmed != stripped { + candidates.push(trimmed); + } + } + let mut coclass_clsid: Option = None; + let mut coclass_name: Option = None; + for cand in &candidates { + if let Some(cc_def) = index.get(namespace, cand).next() { + let ext = cc_def.extends(); + let is_coclass_shape = matches!( + ext.map(|e| (e.namespace().to_string(), e.name().to_string())), + Some((ref ns, ref n)) if ns == "System" && n == "ValueType" + ); + if !is_coclass_shape { + continue; + } + let cc_iid = extract_iid(&cc_def); + if !cc_iid.is_empty() { + coclass_clsid = Some(cc_iid); + coclass_name = Some(cand.clone()); + break; + } + } + } + + // Collect enum types referenced in methods' parameters (direct only). + let mut referenced_enums: Vec = Vec::new(); + let mut seen_enum_names: HashSet = HashSet::new(); + for m in &interface.methods { + for p in &m.params { + if let TypeMeta::Enum { .. } = &p.typ { + if let TypeMeta::Enum { name: en, .. } = &p.typ { + if seen_enum_names.insert(en.clone()) { + referenced_enums.push(p.typ.clone()); + } + } + } + } + } + + Some(ComInterfaceMeta { + interface, + base_offset: root_offset, + is_iunknown_rooted, + base_chain: base_chain.into_iter().map(|(_, n, _)| n).collect(), + coclass_clsid, + coclass_name, + own_methods_start, + referenced_enums, + }) +} + +/// Parse an interface's OWN methods (no inheritance flattening) with a caller- +/// supplied base offset. Used by `parse_com_interface_from_index` to lay out +/// base-class methods at the correct absolute vtable slots. +fn parse_interface_with_offset( + index: &reader::Index, + namespace: &str, + name: &str, + base_offset: usize, +) -> Option { + let def = index.get(namespace, name).next()?; + let iid = extract_iid(&def); + parse_interface_methods(index, &def, name, namespace, &iid, &[], base_offset) } + fn parse_interface_type( index: &reader::Index, interface_type: &windows_metadata::Type, @@ -976,10 +1308,15 @@ fn parse_parameterized_interface( ) -> Option { let trimmed_name = generic_name.split('`').next().unwrap_or(generic_name); let def = index.get(namespace, trimmed_name).next()?; - parse_interface_methods(index, &def, concrete_name, namespace, piid, generic_args) + parse_interface_methods(index, &def, concrete_name, namespace, piid, generic_args, 6) } /// Core interface parsing: extract methods from a TypeDef, optionally substituting generics. +/// +/// `base_offset` is the vtable index of the first user method: +/// - `6` for WinRT (IInspectable-rooted: QI/AddRef/Release + GetIids/GetRuntimeClassName/GetTrustLevel). +/// - `3` for classic-COM IUnknown-rooted interfaces (QI/AddRef/Release only). +/// - Or any absolute offset for a base-aware slot in a chained classic-COM interface. fn parse_interface_methods( index: &reader::Index, def: &reader::TypeDef, @@ -987,13 +1324,14 @@ fn parse_interface_methods( namespace: &str, iid: &str, generic_args: &[TypeMeta], + base_offset: usize, ) -> Option { let winmd_generics: Vec = generic_args.iter().map(type_meta_to_winmd_type).collect(); let mut methods = Vec::new(); for (i, method) in def.methods().enumerate() { - let vtable_index = 6 + i; + let vtable_index = base_offset + i; let sig = method.signature(&winmd_generics); let raw_name = method.name().to_string(); diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.d.ts b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.d.ts new file mode 100644 index 00000000..567a7435 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.d.ts @@ -0,0 +1,13 @@ +// Generated by dynwinrt-codegen — do not edit + +/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +export type HWND = bigint | Buffer; + +export declare class DataTransferManager { + /** Wrap an existing native COM pointer (for QueryInterface bridging). */ + static _fromNative(obj: unknown): DataTransferManager; + /** Get a `DataTransferManager` for the given HWND (projected from `Windows.ApplicationModel.DataTransfer.DataTransferManager`). */ + static getForWindow(appWindow: HWND): DataTransferManager; + /** IInspectable::GetRuntimeClassName — the projected class name. */ + get runtimeClassName(): string; +} diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.js b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.js new file mode 100644 index 00000000..c41630e8 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.js @@ -0,0 +1,32 @@ +// Generated by dynwinrt-codegen — do not edit +import { DynWinRtType, DynWinRtMethodSig, WinGuid } from '@microsoft/dynwinrt'; +import { IDataTransferManagerInterop } from './IDataTransferManagerInterop.js'; + +const IID_IInspectable = WinGuid.parse('af86e2e0-b12d-4c6a-9c5a-d7aa65101e90'); + +let _IInspectableCache; +const _IInspectable = new Proxy({}, { + get(_target, prop) { + _IInspectableCache ??= DynWinRtType.registerInterfaceUnknown('IInspectable_projected', IID_IInspectable) + .addMethod('GetIids', new DynWinRtMethodSig().addOut(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())) + .addMethod('GetRuntimeClassName', new DynWinRtMethodSig().addOut(DynWinRtType.hstring())) + .addMethod('GetTrustLevel', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type())); + const value = _IInspectableCache[prop]; + return typeof value === 'function' ? value.bind(_IInspectableCache) : value; + }, +}); + +export class DataTransferManager { + _obj; + constructor(obj) { this._obj = obj; } + static _fromNative(obj) { return new DataTransferManager(obj); } + /** Get a `DataTransferManager` for the given HWND via the IDataTransferManagerInterop interop. */ + static getForWindow(appWindow) { + const interop = IDataTransferManagerInterop.create(); + return interop.getForWindow(appWindow); + } + /** IInspectable::GetRuntimeClassName — the projected class name. */ + get runtimeClassName() { + return _IInspectable.method(4).getString(this._obj); + } +} diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts new file mode 100644 index 00000000..83b9e2bc --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts @@ -0,0 +1,16 @@ +// Generated by dynwinrt-codegen — do not edit +import { DataTransferManager } from './DataTransferManager.js'; + +/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +export type HWND = bigint | Buffer; + +export declare const IID_IDataTransferManagerInterop: unknown; + +export declare class IDataTransferManagerInterop { + /** Activate the projected WinRT class and QI to the interop. */ + static create(): IDataTransferManagerInterop; + /** Wrap an existing native COM pointer (for QueryInterface bridging). */ + static _fromNative(obj: unknown): IDataTransferManagerInterop; + getForWindow(appWindow: HWND): DataTransferManager; + showShareUIForWindow(appWindow: HWND): void; +} diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js new file mode 100644 index 00000000..59b064b1 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js @@ -0,0 +1,36 @@ +// Generated by dynwinrt-codegen — do not edit +import { DynWinRtType, DynWinRtMethodSig, DynWinRtValue, WinGuid } from '@microsoft/dynwinrt'; +import { DataTransferManager } from './DataTransferManager.js'; + +export const IID_IDataTransferManagerInterop = WinGuid.parse('3a3dcd6c-3eab-43dc-bcde-45671ce800c8'); +const IID_DataTransferManager_default = WinGuid.parse('a5caee9b-8708-49d1-8d36-67d25a8da00c'); + +let _IDataTransferManagerInteropCache; +const _IDataTransferManagerInterop = new Proxy({}, { + get(_target, prop) { + _IDataTransferManagerInteropCache ??= DynWinRtType.registerInterfaceUnknown('IDataTransferManagerInterop', IID_IDataTransferManagerInterop) + .addMethod('GetForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())) + .addMethod('ShowShareUIForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())); + const value = _IDataTransferManagerInteropCache[prop]; + return typeof value === 'function' ? value.bind(_IDataTransferManagerInteropCache) : value; + }, +}); + +export class IDataTransferManagerInterop { + _obj; + constructor(obj) { this._obj = obj; } + static _fromNative(obj) { return new IDataTransferManagerInterop(obj); } + /** Create a new `IDataTransferManagerInterop` by activating the `Windows.ApplicationModel.DataTransfer.DataTransferManager` factory and QI'ing to the interop. */ + static create() { + const factory = DynWinRtValue.activationFactory('Windows.ApplicationModel.DataTransfer.DataTransferManager'); + const _obj = factory.cast(IID_IDataTransferManagerInterop); + return new IDataTransferManagerInterop(_obj); + } + getForWindow(appWindow) { + const _out = _IDataTransferManagerInterop.method(3).invoke(this._obj, [DynWinRtValue.pointer(appWindow), DynWinRtValue.iidPointer(IID_DataTransferManager_default)]); + return DataTransferManager._fromNative(_out); + } + showShareUIForWindow(appWindow) { + _IDataTransferManagerInterop.method(4).invoke(this._obj, [DynWinRtValue.pointer(appWindow)]); + } +} diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts new file mode 100644 index 00000000..2c255bed --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts @@ -0,0 +1,38 @@ +// Generated by dynwinrt-codegen — do not edit +import { TBPFLAG } from './TBPFLAG.js'; + +/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +export type HICON = bigint | Buffer; +/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +export type HIMAGELIST = bigint | Buffer; +/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +export type HWND = bigint | Buffer; +/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +export type PWSTR = bigint | Buffer; + +export declare const IID_ITaskbarList3: unknown; + +export declare class ITaskbarList3 { + /** Create a new instance via the coclass activation path. */ + static create(): ITaskbarList3; + /** Wrap an existing native COM pointer (for QueryInterface bridging). */ + static _fromNative(obj: unknown): ITaskbarList3; + hrInit(): void; + addTab(hwnd: HWND): void; + deleteTab(hwnd: HWND): void; + activateTab(hwnd: HWND): void; + setActiveAlt(hwnd: HWND): void; + markFullscreenWindow(hwnd: HWND, fFullscreen: boolean): void; + setProgressValue(hwnd: HWND, ullCompleted: bigint, ullTotal: bigint): void; + setProgressState(hwnd: HWND, tbpFlags: TBPFLAG): void; + registerTab(tab: HWND, mDI: HWND): void; + unregisterTab(tab: HWND): void; + setTabOrder(tab: HWND, insertBefore: HWND): void; + setTabActive(tab: HWND, mDI: HWND, reserved: number): void; + thumbBarAddButtons(hwnd: HWND, cButtons: number, pButton: bigint | Buffer): void; + thumbBarUpdateButtons(hwnd: HWND, cButtons: number, pButton: bigint | Buffer): void; + thumbBarSetImageList(hwnd: HWND, himl: HIMAGELIST): void; + setOverlayIcon(hwnd: HWND, hIcon: HICON, description: PWSTR): void; + setThumbnailTooltip(hwnd: HWND, tip: PWSTR): void; + setThumbnailClip(hwnd: HWND, prcClip: bigint | Buffer): void; +} diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js new file mode 100644 index 00000000..e4b4b684 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js @@ -0,0 +1,97 @@ +// Generated by dynwinrt-codegen — do not edit +import { DynWinRtType, DynWinRtMethodSig, DynWinRtValue, WinGuid } from '@microsoft/dynwinrt'; +import { TBPFLAG } from './TBPFLAG.js'; + +export const IID_ITaskbarList3 = WinGuid.parse('ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf'); + +let _ITaskbarList3Cache; +const _ITaskbarList3 = new Proxy({}, { + get(_target, prop) { + _ITaskbarList3Cache ??= DynWinRtType.registerInterfaceUnknown('ITaskbarList3', IID_ITaskbarList3) + .addMethod('HrInit', new DynWinRtMethodSig()) + .addMethod('AddTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) + .addMethod('DeleteTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) + .addMethod('ActivateTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) + .addMethod('SetActiveAlt', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) + .addMethod('MarkFullscreenWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) + .addMethod('SetProgressValue', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u64Type()).addIn(DynWinRtType.u64Type())) + .addMethod('SetProgressState', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) + .addMethod('RegisterTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) + .addMethod('UnregisterTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) + .addMethod('SetTabOrder', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) + .addMethod('SetTabActive', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type())) + .addMethod('ThumbBarAddButtons', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type()).addIn(DynWinRtType.pointer())) + .addMethod('ThumbBarUpdateButtons', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type()).addIn(DynWinRtType.pointer())) + .addMethod('ThumbBarSetImageList', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) + .addMethod('SetOverlayIcon', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) + .addMethod('SetThumbnailTooltip', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) + .addMethod('SetThumbnailClip', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())); + const value = _ITaskbarList3Cache[prop]; + return typeof value === 'function' ? value.bind(_ITaskbarList3Cache) : value; + }, +}); + +export class ITaskbarList3 { + _obj; + constructor(obj) { this._obj = obj; } + static _fromNative(obj) { return new ITaskbarList3(obj); } + /** Create a new `ITaskbarList3` via `CoCreateInstance` on `CLSID_TaskbarList`. */ + static create() { + const _obj = DynWinRtValue.coCreateInstance('56fdf344-fd6d-11d0-958a-006097c9a090', IID_ITaskbarList3); + return new ITaskbarList3(_obj); + } + hrInit() { + _ITaskbarList3.method(3).invoke(this._obj, []); + } + addTab(hwnd) { + _ITaskbarList3.method(4).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + } + deleteTab(hwnd) { + _ITaskbarList3.method(5).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + } + activateTab(hwnd) { + _ITaskbarList3.method(6).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + } + setActiveAlt(hwnd) { + _ITaskbarList3.method(7).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + } + markFullscreenWindow(hwnd, fFullscreen) { + _ITaskbarList3.method(8).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.i32(fFullscreen ? 1 : 0)]); + } + setProgressValue(hwnd, ullCompleted, ullTotal) { + _ITaskbarList3.method(9).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.u64(BigInt(ullCompleted)), DynWinRtValue.u64(BigInt(ullTotal))]); + } + setProgressState(hwnd, tbpFlags) { + _ITaskbarList3.method(10).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.i32(tbpFlags)]); + } + registerTab(tab, mDI) { + _ITaskbarList3.method(11).invoke(this._obj, [DynWinRtValue.pointer(tab), DynWinRtValue.pointer(mDI)]); + } + unregisterTab(tab) { + _ITaskbarList3.method(12).invoke(this._obj, [DynWinRtValue.pointer(tab)]); + } + setTabOrder(tab, insertBefore) { + _ITaskbarList3.method(13).invoke(this._obj, [DynWinRtValue.pointer(tab), DynWinRtValue.pointer(insertBefore)]); + } + setTabActive(tab, mDI, reserved) { + _ITaskbarList3.method(14).invoke(this._obj, [DynWinRtValue.pointer(tab), DynWinRtValue.pointer(mDI), DynWinRtValue.u32(reserved)]); + } + thumbBarAddButtons(hwnd, cButtons, pButton) { + _ITaskbarList3.method(15).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.u32(cButtons), DynWinRtValue.pointer(pButton)]); + } + thumbBarUpdateButtons(hwnd, cButtons, pButton) { + _ITaskbarList3.method(16).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.u32(cButtons), DynWinRtValue.pointer(pButton)]); + } + thumbBarSetImageList(hwnd, himl) { + _ITaskbarList3.method(17).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(himl)]); + } + setOverlayIcon(hwnd, hIcon, description) { + _ITaskbarList3.method(18).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(hIcon), DynWinRtValue.pointer(description)]); + } + setThumbnailTooltip(hwnd, tip) { + _ITaskbarList3.method(19).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(tip)]); + } + setThumbnailClip(hwnd, prcClip) { + _ITaskbarList3.method(20).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(prcClip)]); + } +} diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.d.ts b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.d.ts new file mode 100644 index 00000000..46cebb65 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.d.ts @@ -0,0 +1,8 @@ +// Generated by dynwinrt-codegen — do not edit +export declare const enum TBPFLAG { + TBPF_NOPROGRESS = 0, + TBPF_INDETERMINATE = 1, + TBPF_NORMAL = 2, + TBPF_ERROR = 4, + TBPF_PAUSED = 8, +} diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.js b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.js new file mode 100644 index 00000000..58af8cf8 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.js @@ -0,0 +1,8 @@ +// Generated by dynwinrt-codegen — do not edit +export const TBPFLAG = Object.freeze({ + TBPF_NOPROGRESS: 0, + TBPF_INDETERMINATE: 1, + TBPF_NORMAL: 2, + TBPF_ERROR: 4, + TBPF_PAUSED: 8, +}); diff --git a/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs b/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs new file mode 100644 index 00000000..fb499ead --- /dev/null +++ b/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs @@ -0,0 +1,467 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! TDD tests for the *Interop HWND pattern in classic-COM code generation. +//! +//! These tests drive the `getForWindow(hwnd, REFIID, out void**)` special case: +//! - IUnknown-rooted interop (e.g. `IDataTransferManagerInterop`, base=+3) +//! - IInspectable-rooted interop (e.g. `ISystemMediaTransportControlsInterop`, base=+6) +//! +//! The interop shape is: last two params are `(riid: In, out_ptr: Out)`, plus +//! zero or more natural in-params (HWND, HSTRING, …). The generated wrapper +//! MUST hide the REFIID + void** — the caller only supplies the natural +//! parameters, and the wrapper returns the projected WinRT object. +//! +//! Windows.winmd is auto-discovered from the newest installed Windows SDK by +//! the classic-COM interop codegen (see `com::resolve_projected_default_iid`), +//! so these tests do not require a specific SDK version — they only need any +//! recent SDK to be installed AND the Windows.Win32 metadata at `WIN32_WINMD`. + +use std::fs; +use std::path::{Path, PathBuf}; + +use dynwinrt_codegen::codegen::com; +use dynwinrt_codegen::meta; + +const WIN32_WINMD: &str = r"C:\s\win32metadata\Windows.Win32.winmd"; + +fn win32_available() -> bool { + Path::new(WIN32_WINMD).exists() +} + +/// Ensure any recent installed Windows SDK is present so the interop generator +/// can auto-resolve the projected class IID. Uses the SAME discovery logic the +/// codegen itself uses — no pinned version. +fn newest_windows_winmd_available() -> bool { + meta::discover_newest_windows_winmd().is_some() +} + +/// 1. IDataTransferManagerInterop parses cleanly, is IUnknown-rooted (+3), +/// and its `GetForWindow` is at slot 3. +#[test] +fn parse_data_transfer_manager_interop() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com = meta::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .expect("IDataTransferManagerInterop must exist"); + assert!(com.is_iunknown_rooted); + assert_eq!(com.base_offset, 3); + let get_for_window = com + .interface + .methods + .iter() + .find(|m| m.name == "GetForWindow") + .expect("GetForWindow method must exist"); + assert_eq!(get_for_window.vtable_index, 3); + // Last two params must be (In riid, Out out_ptr) — the interop shape. + assert_eq!(get_for_window.params.len(), 3, "HWND + riid + out"); +} + +/// 2. ISystemMediaTransportControlsInterop parses cleanly, is IInspectable-rooted (+6), +/// `GetForWindow` at slot 6. +#[test] +fn parse_smtc_interop() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com = meta::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.System.WinRT", + "ISystemMediaTransportControlsInterop", + ) + .expect("ISystemMediaTransportControlsInterop must exist"); + assert!(!com.is_iunknown_rooted, "SMTC interop derives from IInspectable, not IUnknown"); + assert_eq!(com.base_offset, 6); + let get_for_window = com + .interface + .methods + .iter() + .find(|m| m.name == "GetForWindow") + .expect("GetForWindow method must exist"); + assert_eq!(get_for_window.vtable_index, 6); +} + +/// 3. Codegen recognises the interop shape and emits a natural +/// `getForWindow(hwnd)` — hiding both the REFIID and the void** out-ptr. +#[test] +fn interop_dts_hides_riid_and_out_ptr_for_datatransfermanager() { + if !win32_available() || !newest_windows_winmd_available() { + eprintln!("Skipping: winmd(s) not available"); + return; + } + let com = meta::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com, WIN32_WINMD).expect("interop codegen must succeed when winmds are present"); + let dts = out.dts.as_str(); + + // The natural signature: hwnd only, NO riid, NO out-ptr. + // Accept either single-arg or single-arg + optional projection hint. + // The signature must contain `getForWindow(` followed by a SINGLE + // typed parameter (HWND-like) and NO `riid`/`REFIID` mention. + assert!( + dts.contains("getForWindow"), + ".d.ts must expose getForWindow (camelCased):\n{}", + dts + ); + assert!( + !dts.contains("riid") && !dts.contains("REFIID"), + "REFIID/riid must not appear in .d.ts:\n{}", + dts + ); + assert!( + !dts.contains("void**") && !dts.to_lowercase().contains("out_ptr"), + "void**/out_ptr must not appear in .d.ts:\n{}", + dts + ); + + // Return type — must be a NATURAL WinRT type name, not `bigint | Buffer` + // and not the raw `unknown` fallback. + // For IDataTransferManagerInterop → DataTransferManager. + assert!( + dts.contains("DataTransferManager"), + ".d.ts must project the return type as DataTransferManager:\n{}", + dts + ); + assert!( + !dts.contains("getForWindow(hwnd: bigint | Buffer, riid"), + "riid must not leak into the natural signature:\n{}", + dts + ); +} + +/// 4. The generated JS synthesises the target IID (default interface IID of +/// the WinRT runtime class) INSIDE the method body — the caller supplies +/// only the HWND. +#[test] +fn interop_js_synthesizes_target_iid_for_datatransfermanager() { + if !win32_available() || !newest_windows_winmd_available() { + eprintln!("Skipping: winmd(s) not available"); + return; + } + let com = meta::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com, WIN32_WINMD).expect("interop codegen must succeed when winmds are present"); + let js = out.js.as_str(); + + // The IDataTransferManager default interface IID must be embedded in .js + // (it's the runtime class's default interface's IID: + // a5caee9b-8708-49d1-8d36-67d25a8da00c). + assert!( + js.contains("a5caee9b-8708-49d1-8d36-67d25a8da00c"), + ".js must embed the IDataTransferManager default interface IID:\n{}", + js + ); + // The interop's own IID must also be present. + assert!( + js.contains("3a3dcd6c-3eab-43dc-bcde-45671ce800c8"), + ".js must embed the IDataTransferManagerInterop IID:\n{}", + js + ); + + // GetForWindow lives at vtable slot 3 (IUnknown+3). + assert!( + js.contains("method(3)"), + ".js must invoke slot 3 for GetForWindow:\n{}", + js + ); + + // Activation: uses activationFactory (WinRT) for the projected class + // + QI to the interop IID — NOT CoCreateInstance (which is for classic COM CLSIDs). + assert!( + js.contains("activationFactory") || js.contains("activation_factory"), + ".js must use activationFactory to reach the interop:\n{}", + js + ); + assert!( + !js.contains("coCreateInstance"), + "interop must NOT use coCreateInstance (only WinRT interop path):\n{}", + js + ); +} + +/// 5. SMTC-specific: the SMTC interop generates a wrapper whose registration +/// uses the +6 (IInspectable) base, and its GetForWindow invokes slot 6. +#[test] +fn smtc_interop_js_uses_inspectable_base_slot_6() { + if !win32_available() || !newest_windows_winmd_available() { + eprintln!("Skipping: winmd(s) not available"); + return; + } + let com = meta::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.System.WinRT", + "ISystemMediaTransportControlsInterop", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com, WIN32_WINMD).expect("interop codegen must succeed when winmds are present"); + let js = out.js.as_str(); + + // IInspectable-rooted → register with the WinRT base (registerInterface), + // not registerInterfaceUnknown. + assert!( + js.contains("registerInterface(") && !js.contains("registerInterfaceUnknown("), + ".js for an IInspectable-rooted interop must use registerInterface \ + (base_slot=6), got:\n{}", + js + ); + assert!( + js.contains("method(6)"), + ".js must invoke slot 6 for GetForWindow:\n{}", + js + ); + + // Return type = SystemMediaTransportControls; default interface IID + // (ISystemMediaTransportControls = 99fa3ff4-1742-42a6-902e-087d41f965ec). + assert!( + js.contains("99fa3ff4-1742-42a6-902e-087d41f965ec"), + ".js must embed the ISystemMediaTransportControls default interface IID:\n{}", + js + ); +} + +/// 6. The interop wrapper's return object exposes `runtimeClassName` — a +/// natural, meaningful property that reads via IInspectable::GetRuntimeClassName. +/// This is what the E2E asserts to prove the returned object is a live WinRT +/// object (not just a non-null pointer). +#[test] +fn interop_return_type_exposes_runtime_class_name() { + if !win32_available() || !newest_windows_winmd_available() { + eprintln!("Skipping: winmd(s) not available"); + return; + } + let com = meta::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com, WIN32_WINMD).expect("interop codegen must succeed when winmds are present"); + + // The projected class `DataTransferManager` is emitted as a separate + // sibling file (own .js + .d.ts), NOT inside the interop wrapper's .d.ts. + let projected_dts = out + .extra_files + .iter() + .find(|(name, _)| name == "DataTransferManager.d.ts") + .map(|(_, content)| content.as_str()) + .expect( + "DataTransferManager.d.ts must be emitted as a projected companion \ + (via Windows.winmd default-interface lookup)", + ); + + assert!( + projected_dts.contains("runtimeClassName"), + "DataTransferManager.d.ts must declare a `runtimeClassName` getter:\n{}", + projected_dts + ); + assert!( + projected_dts.contains("class DataTransferManager"), + "DataTransferManager.d.ts must declare `class DataTransferManager`:\n{}", + projected_dts + ); + assert!( + projected_dts.contains("static getForWindow"), + "DataTransferManager.d.ts must expose `static getForWindow(hwnd)`:\n{}", + projected_dts + ); + + // Also confirm the interop's own .d.ts references DataTransferManager as + // the natural return type (verified via import). + assert!( + out.dts.contains("DataTransferManager"), + "interop .d.ts must reference the projected return type:\n{}", + out.dts + ); +} + +/// 7. Interop generation is deterministic (byte-identical across two runs). +#[test] +fn interop_generation_is_deterministic() { + if !win32_available() || !newest_windows_winmd_available() { + eprintln!("Skipping: winmd(s) not available"); + return; + } + let mk = || { + let com = meta::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .unwrap(); + com::generate_com_interface_files(&com, WIN32_WINMD).expect("interop codegen must succeed when winmds are present") + }; + let a = mk(); + let b = mk(); + assert_eq!(a.js, b.js); + assert_eq!(a.dts, b.dts); + assert_eq!(a.extra_files, b.extra_files); +} + +/// 8. Snapshot: lock the generated IDataTransferManagerInterop files +/// against committed reference files. +#[test] +fn snapshot_datatransfermanager_interop() { + if !win32_available() || !newest_windows_winmd_available() { + eprintln!("Skipping: winmd(s) not available"); + return; + } + let com = meta::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com, WIN32_WINMD).expect("interop codegen must succeed when winmds are present"); + + let snapshot_dir: PathBuf = + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots/idatatransfermanagerinterop"); + assert!( + snapshot_dir.exists(), + "Snapshot directory not found: {}", + snapshot_dir.display() + ); + + let mut generated: Vec<(String, String)> = Vec::new(); + generated.push(("IDataTransferManagerInterop.js".into(), out.js.clone())); + generated.push(("IDataTransferManagerInterop.d.ts".into(), out.dts.clone())); + for (name, content) in &out.extra_files { + generated.push((name.clone(), content.clone())); + } + + let mut mismatches = Vec::new(); + for (name, actual) in &generated { + let path = snapshot_dir.join(name); + if !path.exists() { + mismatches.push(format!(" missing snapshot: {}", name)); + continue; + } + let expected = fs::read_to_string(&path).unwrap(); + if actual.trim_end() != expected.trim_end() { + mismatches.push(format!(" differs: {}", name)); + } + } + if let Ok(entries) = fs::read_dir(&snapshot_dir) { + let names: std::collections::HashSet = + generated.iter().map(|(n, _)| n.clone()).collect(); + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if !names.contains(&name) { + mismatches.push(format!(" extra snapshot not generated: {}", name)); + } + } + } + + if !mismatches.is_empty() { + panic!( + "IDataTransferManagerInterop snapshot mismatch!\n{}\n\n\ + To update, re-run the generator or copy the actual output.", + mismatches.join("\n") + ); + } +} + +// ------------------------------------------------------------------------- +// Fix 1 (portability): interop IID resolution +// ------------------------------------------------------------------------- + +/// FIX 1 (portability): the interop generator MUST NOT depend on a specific +/// SDK-versioned `Windows.winmd` path. On this box (and any developer/CI +/// machine with the Win32 metadata + a recent Windows SDK installed), the +/// generator resolves the projected class IID correctly, and the tests +/// actively assert that IID rather than self-skipping. +#[test] +fn fix1_interop_iid_resolution_is_portable_and_asserted() { + if !win32_available() { + eprintln!("Skipping fix1_interop_iid_resolution_is_portable_and_asserted: Win32 winmd not available at {}", WIN32_WINMD); + return; + } + if !newest_windows_winmd_available() { + eprintln!("Skipping fix1_interop_iid_resolution_is_portable_and_asserted: no Windows SDK Windows.winmd discoverable"); + return; + } + + // 1. IDataTransferManager: default interface IID must resolve to the + // well-known value regardless of which SDK version is installed. + let (ns_dtm, _iface_dtm, iid_dtm) = + meta::find_runtime_class_default_iid( + &meta::discover_newest_windows_winmd().unwrap(), + "DataTransferManager", + ) + .expect("DataTransferManager must resolve via discovered SDK winmd"); + assert_eq!(ns_dtm, "Windows.ApplicationModel.DataTransfer"); + assert_eq!(iid_dtm, "a5caee9b-8708-49d1-8d36-67d25a8da00c"); + + // 2. SystemMediaTransportControls: same portability contract. + let (ns_smtc, _iface_smtc, iid_smtc) = + meta::find_runtime_class_default_iid( + &meta::discover_newest_windows_winmd().unwrap(), + "SystemMediaTransportControls", + ) + .expect("SystemMediaTransportControls must resolve via discovered SDK winmd"); + assert_eq!(ns_smtc, "Windows.Media"); + assert_eq!(iid_smtc, "99fa3ff4-1742-42a6-902e-087d41f965ec"); + + // 3. End-to-end: the classic-COM interop wrapper embeds the correct IID. + // Test intentionally passes ONLY the Win32 winmd (no Windows.winmd in + // winmd_paths) to exercise the newest-SDK fallback path. + let com = meta::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .expect("IDataTransferManagerInterop must exist"); + let out = com::generate_com_interface_files(&com, WIN32_WINMD) + .expect("interop codegen must resolve IID via newest-SDK fallback"); + assert!( + out.js.contains(&iid_dtm), + "generated .js must embed the resolved DataTransferManager IID `{}`:\n{}", + iid_dtm, + out.js + ); + // Must NEVER emit the silent NULL riid sentinel that the pre-fix code + // could produce when resolution failed. + assert!( + !out.js.contains("DynWinRtValue.pointer(0n)"), + "generator must not emit a NULL riid — indicates silent failure:\n{}", + out.js + ); +} + +/// FIX 1 (portability): the generator MUST prefer the winmd paths passed to +/// it OVER the auto-discovered SDK winmd. This preserves reproducibility for +/// integrators who pin a specific SDK via `--ref`. +#[test] +fn fix1_interop_iid_prefers_passed_winmds_over_sdk() { + if !win32_available() || !newest_windows_winmd_available() { + eprintln!("Skipping: winmd(s) not available"); + return; + } + let sdk = meta::discover_newest_windows_winmd().unwrap(); + // Pass Windows.winmd as part of winmd_paths — the generator should find + // the runtime class immediately without hitting the fallback path. + let combined = format!("{};{}", WIN32_WINMD, sdk); + let com = meta::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com, &combined) + .expect("interop codegen must succeed when Windows.winmd is in winmd_paths"); + assert!(out.js.contains("a5caee9b-8708-49d1-8d36-67d25a8da00c")); +} diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs new file mode 100644 index 00000000..b80cd278 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -0,0 +1,641 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! TDD tests for classic-COM (option A) code generation from Windows.Win32.winmd. +//! +//! These tests drive the implementation of: +//! - Base-aware vtable slot computation (walks interface_impls chain) +//! - IUnknown vs IInspectable base offset (3 vs 6) +//! - Coclass CLSID discovery for `create()` activation +//! - Natural TS/JS wrapper generation for classic-COM interfaces +//! +//! Tests are skipped (with an `eprintln!` note) when the Win32 winmd is not +//! present at the well-known path. + +use std::fs; +use std::path::{Path, PathBuf}; + +use dynwinrt_codegen::codegen::com; +use dynwinrt_codegen::codegen::project::{get_import_name, set_import_name}; +use dynwinrt_codegen::meta; + +const WIN32_WINMD: &str = r"C:\s\win32metadata\Windows.Win32.winmd"; + +fn win32_available() -> bool { + Path::new(WIN32_WINMD).exists() +} + +/// Resolve a `Windows.winmd` from the newest installed Windows SDK, matching +/// the discovery logic the codegen itself uses. Returns `None` if no SDK is +/// installed on this machine (the test that calls this should skip in that +/// case, consistent with other tests in this module). +fn discovered_windows_winmd() -> Option { + meta::discover_newest_windows_winmd() +} + +// ------------------------------------------------------------------------- +// NORMAL tests +// ------------------------------------------------------------------------- + +/// 1. Parse ITaskbarList3 from Win32 metadata → correct IID. +#[test] +fn parse_itaskbarlist3_iid() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available at {}", WIN32_WINMD); + return; + } + let com_iface = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .expect("ITaskbarList3 must exist in Win32 metadata"); + assert_eq!(com_iface.interface.name, "ITaskbarList3"); + assert_eq!(com_iface.interface.namespace, "Windows.Win32.UI.Shell"); + assert_eq!(com_iface.interface.iid, "ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf"); +} + +/// 2. Base-aware vtable slots: full interface_impls chain determines absolute slots. +/// ITaskbarList3 inherits: IUnknown (3 methods) + ITaskbarList (5) + ITaskbarList2 (1). +/// So HrInit = 3, SetProgressValue = 9, SetProgressState = 10. +#[test] +fn parse_itaskbarlist3_vtable_slots() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .expect("ITaskbarList3 must exist"); + + let by_name = |n: &str| -> usize { + com_iface + .interface + .methods + .iter() + .find(|m| m.name == n) + .unwrap_or_else(|| panic!("method {} not found (methods: {:?})", n, com_iface.interface.methods.iter().map(|m| &m.name).collect::>())) + .vtable_index + }; + + assert_eq!(by_name("HrInit"), 3, "HrInit is the first ITaskbarList method after IUnknown"); + assert_eq!(by_name("AddTab"), 4); + assert_eq!(by_name("DeleteTab"), 5); + assert_eq!(by_name("ActivateTab"), 6); + assert_eq!(by_name("SetActiveAlt"), 7); + assert_eq!(by_name("MarkFullscreenWindow"), 8, "ITaskbarList2's only method"); + assert_eq!(by_name("SetProgressValue"), 9); + assert_eq!(by_name("SetProgressState"), 10); +} + +/// 3. Base detection: ITaskbarList3 is IUnknown-rooted → base offset (first user +/// method slot) is 3, NOT 6. +#[test] +fn itaskbarlist3_is_iunknown_rooted() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .unwrap(); + assert_eq!(com_iface.base_offset, 3); + assert!(com_iface.is_iunknown_rooted); + // Base chain should include ITaskbarList2, ITaskbarList (and stop at IUnknown) + let base_names: Vec<&str> = com_iface.base_chain.iter().map(|s| s.as_str()).collect(); + assert_eq!( + base_names, + ["ITaskbarList2", "ITaskbarList", "IUnknown"], + "base chain order matters" + ); +} + +/// 4. CLSID resolution: ITaskbarList3 → TaskbarList coclass → CLSID +/// 56fdf344-fd6d-11d0-958a-006097c9a090 +#[test] +fn itaskbarlist3_clsid_resolution() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .unwrap(); + assert_eq!( + com_iface.coclass_clsid.as_deref(), + Some("56fdf344-fd6d-11d0-958a-006097c9a090") + ); + assert_eq!(com_iface.coclass_name.as_deref(), Some("TaskbarList")); +} + +/// 5. Param type mapping: HWND → pointer/handle, TBPFLAG → enum, HRESULT → void. +#[test] +fn param_type_mapping() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .unwrap(); + + // Generate wrapper as a text bundle we can inspect for the mapping decisions + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD).expect("codegen must succeed for classic-COM interface"); + + let dts = out.dts.as_str(); + let js = out.js.as_str(); + + // HWND is a handle type → bigint | Buffer surface + assert!( + dts.contains("bigint | Buffer") || dts.contains("bigint|Buffer"), + "HWND should be projected as `bigint | Buffer` in .d.ts, got:\n{}", dts + ); + + // ULONGLONG (U64) → bigint + // setProgressValue's completed/total params are U64 + assert!( + dts.contains("bigint"), + "U64 params should surface as bigint" + ); + + // TBPFLAG enum → surfaced by name (either an enum decl or a union) + assert!( + dts.contains("TBPFLAG") || dts.contains("TbpFlag"), + ".d.ts must reference the TBPFLAG enum:\n{}", dts + ); + + // HRESULT-returning methods project to `void` (throw on failure); no HRESULT surface + assert!( + !dts.contains(": HRESULT") + && !dts.contains("-> HRESULT") + && !dts.contains("Promise"), + "HRESULT must not leak into the .d.ts surface:\n{}", dts + ); + + // JS body: the SetProgressState signature must include u32 (TBPFLAG's underlying) for the enum arg + // Look for slot 10 invocation: + assert!( + js.contains("method(10)"), + ".js must call vtable slot 10 for SetProgressState" + ); + assert!( + js.contains("method(9)"), + ".js must call vtable slot 9 for SetProgressValue" + ); +} + +/// 6. Partial generation: generating a single class-name yields ONLY that +/// interface plus its immediate deps (enum, coclass metadata), NOT the +/// entire Windows.Win32.UI.Shell namespace. +#[test] +fn partial_generation_only_emits_target_interface() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .unwrap(); + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD).expect("codegen must succeed for classic-COM interface"); + + // Expected files: ITaskbarList3.js, ITaskbarList3.d.ts, TBPFLAG.js, TBPFLAG.d.ts + let file_names: Vec<&str> = out.extra_files.iter().map(|(n, _)| n.as_str()).collect(); + + // Should NOT include unrelated Shell types like IShellItem or IApplicationActivationManager + assert!( + !file_names.iter().any(|n| n.starts_with("IShellItem")), + "Partial generation must not include IShellItem: {:?}", file_names + ); + assert!( + !file_names.iter().any(|n| n.starts_with("IApplicationActivationManager")), + "Partial generation must not include unrelated types: {:?}", file_names + ); + + // Should include TBPFLAG (a direct dep) + let has_tbpflag = file_names.iter().any(|n| n.starts_with("TBPFLAG")); + assert!(has_tbpflag, "TBPFLAG (direct enum dep) must be included: {:?}", file_names); +} + +/// 7. Generated `.d.ts` has PascalCase type + camelCase methods and +/// no raw IID/vtable-index/CoCreateInstance leaked into the TYPED surface. +#[test] +fn dts_surface_is_natural_and_clean() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .unwrap(); + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD).expect("codegen must succeed for classic-COM interface"); + let dts = out.dts.as_str(); + + // PascalCase class name + assert!( + dts.contains("class ITaskbarList3"), + ".d.ts must export class ITaskbarList3, got:\n{}", dts + ); + + // camelCase methods + for cc in &["hrInit", "setProgressValue", "setProgressState", "addTab"] { + assert!( + dts.contains(cc), + ".d.ts must declare camelCase method `{}`, got:\n{}", cc, dts + ); + } + // No PascalCase leaked method names + for pc in &["HrInit(", "SetProgressValue(", "SetProgressState(", "AddTab("] { + assert!( + !dts.contains(pc), + ".d.ts must not expose PascalCase method `{}`, got:\n{}", pc, dts + ); + } + + // No raw IID leak in .d.ts + assert!( + !dts.contains("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf"), + "raw IID must not leak into .d.ts:\n{}", dts + ); + // No raw CLSID leak + assert!( + !dts.contains("56fdf344-fd6d-11d0-958a-006097c9a090"), + "raw CLSID must not leak into .d.ts:\n{}", dts + ); + // No CoCreateInstance leak + assert!( + !dts.contains("CoCreateInstance") && !dts.contains("coCreateInstance"), + "CoCreateInstance must not leak into .d.ts:\n{}", dts + ); + // No vtable index leak in .d.ts + for slot in &["method(3)", "method(9)", "method(10)", "vtable"] { + assert!( + !dts.contains(slot), + "vtable detail `{}` must not appear in .d.ts:\n{}", slot, dts + ); + } +} + +/// 8. Generated `.js`: activation uses a CoCreateInstance path with CLSID + IID; +/// methods invoke at the correct base-aware slots. +#[test] +fn js_body_uses_cocreateinstance_and_correct_slots() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .unwrap(); + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD).expect("codegen must succeed for classic-COM interface"); + let js = out.js.as_str(); + + // CLSID + IID appear in .js + assert!( + js.contains("56fdf344-fd6d-11d0-958a-006097c9a090"), + ".js must embed the CLSID:\n{}", js + ); + assert!( + js.contains("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf"), + ".js must embed the IID:\n{}", js + ); + + // Activation via coCreateInstance + assert!( + js.contains("coCreateInstance"), + ".js must use coCreateInstance for activation:\n{}", js + ); + + // registerInterfaceUnknown (not registerInterface) since IUnknown-based + assert!( + js.contains("registerInterfaceUnknown"), + ".js must use registerInterfaceUnknown for classic COM:\n{}", js + ); + + // Base-aware slots + assert!(js.contains("method(3)"), "HrInit slot 3"); + assert!(js.contains("method(9)"), "SetProgressValue slot 9"); + assert!(js.contains("method(10)"), "SetProgressState slot 10"); + + // Should NOT contain WinRT `.method(6)` for a user method (that would be + // the IInspectable-rooted slot for the first user method). + // HrInit at slot 6 would be the failing case — we accept `method(6)` only + // if that's ActivateTab (slot 6). ActivateTab IS at 6, so it's a valid + // occurrence. Just check the file doesn't say something like `HrInit ... method(6)`. + // This is covered by the exact per-method assertion above. +} + +// ------------------------------------------------------------------------- +// CORNER tests +// ------------------------------------------------------------------------- + +/// 9. Regression: WinRT-style (IInspectable-based) interfaces still compute +/// base offset 6 (i.e. the existing WinRT path is unaffected). +#[test] +fn winrt_interfaces_still_use_offset_6() { + // Parse a well-known WinRT interface (Windows.Foundation.IUriRuntimeClass or similar) + // via the existing WinRT path — its first method should still have vtable_index = 6. + let Some(windows_winmd) = discovered_windows_winmd() else { + eprintln!("Skipping winrt_interfaces_still_use_offset_6: no Windows SDK Windows.winmd discoverable"); + return; + }; + // Take Windows.Foundation.Uri's default interface — pick one that has methods. + let class = meta::parse_class(&windows_winmd, "Windows.Foundation", "Uri") + .expect("Windows.Foundation.Uri must be present"); + let default_iface = class + .default_interface + .as_ref() + .expect("Uri must have a default interface"); + + // Its first method's vtable_index must still be 6 (unchanged from existing + // WinRT behavior); classic-COM support must not regress this. + let first_slot = default_iface + .methods + .first() + .map(|m| m.vtable_index) + .expect("Uri default interface must have methods"); + assert_eq!(first_slot, 6, "WinRT interfaces retain the IInspectable base offset of 6"); +} + +/// 10. Interface-not-found is a clean Option::None, not a panic. +#[test] +fn interface_not_found_is_clean_none() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let missing = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "IDoesNotExist_XYZ"); + assert!(missing.is_none()); +} + +/// 11. QI-only interface (no coclass) → wrapper emitted WITHOUT `create()`, +/// only a static from-raw / QI entry point. +#[test] +fn qi_only_interface_has_no_create() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + // IPersist is IUnknown-rooted (has 1 own method: GetClassID) and has NO + // "Persist" coclass anywhere in the metadata — verified via probe. + let com_iface = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.System.Com", "IPersist") + .expect("IPersist must exist in Win32 metadata"); + assert!( + com_iface.coclass_clsid.is_none(), + "IPersist has no associated coclass CLSID" + ); + + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD).expect("codegen must succeed for classic-COM interface"); + let js = out.js.as_str(); + let dts = out.dts.as_str(); + + // No `create()` in either surface + assert!( + !dts.contains("static create()") && !dts.contains("static create(): "), + "QI-only interface must not expose static create() in .d.ts:\n{}", dts + ); + assert!( + !js.contains("coCreateInstance"), + "QI-only interface must not call coCreateInstance in .js:\n{}", js + ); + + // Must still have a fromNative / QI-only entry + assert!( + js.contains("_fromNative") || js.contains("fromRaw"), + "QI-only interface must expose a from-raw entry:\n{}", js + ); + + // Slot 3 for GetClassID (only method, IUnknown-rooted) + assert!( + js.contains("method(3)"), + "IPersist.GetClassID must invoke slot 3:\n{}", js + ); +} + +/// 12. Determinism: regenerating ITaskbarList3 twice produces byte-identical output. +#[test] +fn generation_is_deterministic() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let a = { + let com = meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .unwrap(); + com::generate_com_interface_files(&com, WIN32_WINMD).expect("codegen must succeed for classic-COM interface") + }; + let b = { + let com = meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .unwrap(); + com::generate_com_interface_files(&com, WIN32_WINMD).expect("codegen must succeed for classic-COM interface") + }; + assert_eq!(a.js, b.js); + assert_eq!(a.dts, b.dts); + assert_eq!(a.extra_files, b.extra_files); +} + +// ------------------------------------------------------------------------- +// SNAPSHOT test +// ------------------------------------------------------------------------- + +/// Snapshot test: lock generated ITaskbarList3 .js + .d.ts against committed files. +/// +/// To update snapshots after an intentional change: +/// cargo run -p dynwinrt-codegen -- generate \ +/// --winmd C:\s\win32metadata\Windows.Win32.winmd \ +/// --namespace Windows.Win32.UI.Shell \ +/// --class-name ITaskbarList3 \ +/// --output tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3 +#[test] +fn snapshot_itaskbarlist3() { + if !win32_available() { + eprintln!("Skipping snapshot test: Win32 winmd not available"); + return; + } + let com_iface = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .expect("ITaskbarList3 must exist"); + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD).expect("codegen must succeed for classic-COM interface"); + + let snapshot_dir: PathBuf = + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots/itaskbarlist3"); + assert!( + snapshot_dir.exists(), + "Snapshot directory not found: {}", snapshot_dir.display() + ); + + let mut generated: Vec<(String, String)> = Vec::new(); + generated.push(("ITaskbarList3.js".into(), out.js.clone())); + generated.push(("ITaskbarList3.d.ts".into(), out.dts.clone())); + for (name, content) in &out.extra_files { + generated.push((name.clone(), content.clone())); + } + + let mut mismatches = Vec::new(); + for (name, actual) in &generated { + let path = snapshot_dir.join(name); + if !path.exists() { + mismatches.push(format!(" missing snapshot: {}", name)); + continue; + } + let expected = fs::read_to_string(&path).unwrap(); + if actual.trim_end() != expected.trim_end() { + mismatches.push(format!(" differs: {}", name)); + } + } + + // Any extra snapshot file not produced by the generator is also a mismatch. + if let Ok(entries) = fs::read_dir(&snapshot_dir) { + let names: std::collections::HashSet = + generated.iter().map(|(n, _)| n.clone()).collect(); + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if !names.contains(&name) { + mismatches.push(format!(" extra snapshot not generated: {}", name)); + } + } + } + + if !mismatches.is_empty() { + panic!( + "ITaskbarList3 snapshot mismatch!\n{}\n\n\ + To update, re-run the generator or copy the actual output into the snapshot dir.", + mismatches.join("\n") + ); + } +} + +// ------------------------------------------------------------------------- +// --import-name honored by classic-COM path +// ------------------------------------------------------------------------- + +/// Regression test for a bug where the classic-COM generator hardcoded the +/// runtime import as `'@microsoft/dynwinrt'`, ignoring the `--import-name` +/// CLI flag (which the WinRT path already honored via +/// `codegen::project::set_import_name`). Fixing this makes it possible to +/// regenerate the Node E2E wrappers from `Windows.Win32.winmd` without +/// hand-patching the import line. +/// +/// The test uses the same thread-local as `set_import_name`, so it +/// save/restores the default around the assertion to avoid contaminating +/// other tests that assume the `@microsoft/dynwinrt` default (notably the +/// snapshot tests). `#[serial]` is intentionally NOT used — because +/// `RUNTIME_IMPORT_NAME` is a `thread_local!`, cargo's parallel test runner +/// gives each thread its own copy; restoring on the same thread is enough. +#[test] +fn import_name_flag_is_honored_by_com_path() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let previous = get_import_name(); + set_import_name("../dist/index.js"); + + let result = std::panic::catch_unwind(|| { + let com_iface = meta::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .expect("ITaskbarList3 must exist"); + com::generate_com_interface_files(&com_iface, WIN32_WINMD) + .expect("codegen must succeed for classic-COM interface") + }); + + // Always restore before propagating any assertion failure. + set_import_name(&previous); + + let out = result.unwrap_or_else(|e| std::panic::resume_unwind(e)); + + // Custom import must appear on the runtime import line... + assert!( + out.js.contains("from '../dist/index.js'"), + "classic-COM .js must honor --import-name (expected `from '../dist/index.js'`):\n{}", + out.js + ); + // ...and the hardcoded default must NOT be present in the generated body. + assert!( + !out.js.contains("'@microsoft/dynwinrt'"), + "classic-COM .js must NOT hardcode '@microsoft/dynwinrt' when --import-name is set:\n{}", + out.js + ); + + // Sanity: after restoring the default, subsequent generation reverts. + let default_out = { + let com_iface = meta::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .expect("ITaskbarList3 must exist"); + com::generate_com_interface_files(&com_iface, WIN32_WINMD) + .expect("codegen must succeed for classic-COM interface") + }; + assert!( + default_out.js.contains("from '@microsoft/dynwinrt'"), + "after restoring, default import name must be back to '@microsoft/dynwinrt':\n{}", + default_out.js + ); +} + +/// Same test for the *interop wrapper* generation path (the second hardcoded +/// site in `com.rs`) — regenerating `IDataTransferManagerInterop` with a +/// custom import name should thread through to the emitted +/// `DataTransferManager.js` companion. +#[test] +fn import_name_flag_is_honored_by_interop_wrapper() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + if discovered_windows_winmd().is_none() { + eprintln!("Skipping: no Windows SDK Windows.winmd discoverable (needed for interop resolution)"); + return; + } + + let previous = get_import_name(); + set_import_name("../dist/index.js"); + + let result = std::panic::catch_unwind(|| { + let com_iface = meta::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .expect("IDataTransferManagerInterop must exist"); + com::generate_com_interface_files(&com_iface, WIN32_WINMD) + .expect("codegen must succeed for classic-COM interop interface") + }); + + set_import_name(&previous); + let out = result.unwrap_or_else(|e| std::panic::resume_unwind(e)); + + // The interop .js itself must honor the flag. + assert!( + out.js.contains("from '../dist/index.js'"), + "interop .js must honor --import-name:\n{}", + out.js + ); + assert!( + !out.js.contains("'@microsoft/dynwinrt'"), + "interop .js must NOT hardcode '@microsoft/dynwinrt':\n{}", + out.js + ); + + // And the projected companion class file must honor it too. + let companion = out + .extra_files + .iter() + .find(|(name, _)| name == "DataTransferManager.js") + .map(|(_, content)| content.as_str()) + .expect("DataTransferManager.js companion must be emitted"); + assert!( + companion.contains("from '../dist/index.js'"), + "projected companion .js must honor --import-name:\n{}", + companion + ); + assert!( + !companion.contains("'@microsoft/dynwinrt'"), + "projected companion .js must NOT hardcode '@microsoft/dynwinrt':\n{}", + companion + ); +} From 197a84fe8964b51c4b591b5e6370691f97745987 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Wed, 22 Jul 2026 15:45:34 +0800 Subject: [PATCH 02/28] codegen(com): emit enums as const-object + companion type, not `const enum` Mirror the WinRT enum generator (render_enum_dts in codegen/javascript/render/declarations.rs) so classic-COM enum .d.ts stays consumable under TS isolatedModules and matches the JS Object.freeze runtime shape. Snapshot updated (tests/snapshots/itaskbarlist3/TBPFLAG.d.ts); the .js output is unchanged so the taskbarlist E2E still works via TBPFLAG.TBPF_NORMAL member access. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/dynwinrt-codegen/src/codegen/com.rs | 15 +++++++++++---- .../tests/snapshots/itaskbarlist3/TBPFLAG.d.ts | 15 ++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/tools/dynwinrt-codegen/src/codegen/com.rs b/tools/dynwinrt-codegen/src/codegen/com.rs index f22223ed..68c1ea02 100644 --- a/tools/dynwinrt-codegen/src/codegen/com.rs +++ b/tools/dynwinrt-codegen/src/codegen/com.rs @@ -1019,14 +1019,21 @@ fn render_enum_files(en: &TypeMeta) -> (String, String) { } js.push_str("});\n"); - // .d.ts: a proper enum declaration. + // .d.ts: emit a const object + companion type — matches the JS `Object.freeze({...})` + // runtime shape and mirrors the WinRT enum generator (see + // `codegen::javascript::render::declarations::render_enum_dts`). Using `const enum` + // breaks under TS `isolatedModules`, so we intentionally avoid it. let mut dts = String::new(); dts.push_str("// Generated by dynwinrt-codegen — do not edit\n"); - dts.push_str(&format!("export declare const enum {name} {{\n", name = name)); + dts.push_str(&format!( + "export type {name} = (typeof {name})[keyof typeof {name}];\n", + name = name + )); + dts.push_str(&format!("export declare const {name}: {{\n", name = name)); for m in members { - dts.push_str(&format!(" {} = {},\n", m.name, m.value)); + dts.push_str(&format!(" readonly {}: {};\n", m.name, m.value)); } - dts.push_str("}\n"); + dts.push_str("};\n"); (js, dts) } diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.d.ts b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.d.ts index 46cebb65..cad22793 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.d.ts +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.d.ts @@ -1,8 +1,9 @@ // Generated by dynwinrt-codegen — do not edit -export declare const enum TBPFLAG { - TBPF_NOPROGRESS = 0, - TBPF_INDETERMINATE = 1, - TBPF_NORMAL = 2, - TBPF_ERROR = 4, - TBPF_PAUSED = 8, -} +export type TBPFLAG = (typeof TBPFLAG)[keyof typeof TBPFLAG]; +export declare const TBPFLAG: { + readonly TBPF_NOPROGRESS: 0; + readonly TBPF_INDETERMINATE: 1; + readonly TBPF_NORMAL: 2; + readonly TBPF_ERROR: 4; + readonly TBPF_PAUSED: 8; +}; From f9fb23e802c91b1fac7a9b69b592f5832212218c Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Wed, 22 Jul 2026 16:13:32 +0800 Subject: [PATCH 03/28] codegen(com): fail closed on unknown base-chain shape + fix handle out-param projection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - meta::parse_com_interface_from_index now tracks explicit termination at IUnknown or IInspectable and returns None if the base-chain walk exits without reaching either, instead of silently defaulting to IInspectable (base offset 6). This prevents wrong absolute vtable slots when a winmd has an unexpected inheritance shape or missing interface_impls. - unwrap_return_js: for opaque Win32 handle out-params (HWND, PWSTR, ...) emit `.asPointerBigint()` instead of `.toI64()`. The runtime may produce WinRTValue::Object/RawPtr/Null when the handle's inner `Value` field is a void*-shaped type, and `.toI64()` panics on those variants (its fallback `.toNumber()` panics for non-numeric variants). `.asPointerBigint()` cleanly handles all three pointer representations. All snapshots and E2Es unaffected: current interfaces have no [out] handle params, and the two rooted tests (ITaskbarList3 → IUnknown, SMTC interop → IInspectable) still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/dynwinrt-codegen/src/codegen/com.rs | 8 +++++++- tools/dynwinrt-codegen/src/meta.rs | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/tools/dynwinrt-codegen/src/codegen/com.rs b/tools/dynwinrt-codegen/src/codegen/com.rs index 68c1ea02..229f6d99 100644 --- a/tools/dynwinrt-codegen/src/codegen/com.rs +++ b/tools/dynwinrt-codegen/src/codegen/com.rs @@ -584,7 +584,13 @@ fn unwrap_return_js(t: &TypeMeta, expr: &str) -> String { } if handle_type_name(t).is_some() { // Opaque Win32 handle (HWND, PWSTR, etc.) → raw pointer as bigint. - return format!("{expr}.toI64()"); + // Use `asPointerBigint` (not `toI64`): the runtime may return the + // handle as a `WinRTValue::Object`/`RawPtr`/`Null` when the handle's + // inner `Value` field is a `void*`-shaped type, and `toI64` panics + // on those variants (it falls back to `toNumber`, which explicitly + // panics for non-numeric WinRTValues). `asPointerBigint` cleanly + // handles Object/RawPtr/Null and preserves all 64 pointer bits. + return format!("{expr}.asPointerBigint()"); } match t { TypeMeta::Bool => format!("{expr}.toBool()"), diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index c5dd6783..efb96c0f 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -1096,6 +1096,13 @@ fn parse_com_interface_from_index( let mut cur_ns = namespace.to_string(); let mut cur_name = name.to_string(); let mut is_iunknown_rooted = false; + // Explicit-termination flag: set only when the walk reaches a well-known + // COM/WinRT root (IUnknown or IInspectable). If we exit the loop without + // this being set — malformed/incomplete winmd, missing `interface_impls`, + // or a depth-limit overrun — the offset-3 vs. offset-6 decision below + // would be guesswork. In that case we return None rather than emit code + // with silently-wrong vtable slots. + let mut terminated_at_known_root = false; // Walk up to 32 levels deep as a safety limit (real chains are 3-4 deep). for _ in 0..32 { @@ -1113,10 +1120,12 @@ fn parse_com_interface_from_index( // Terminate at IUnknown or IInspectable. if base.1 == "IUnknown" { is_iunknown_rooted = true; + terminated_at_known_root = true; base_chain.push(("Windows.Win32.System.Com".to_string(), "IUnknown".to_string(), 0)); break; } if base.1 == "IInspectable" { + terminated_at_known_root = true; base_chain.push(("Windows.Foundation".to_string(), "IInspectable".to_string(), 0)); break; } @@ -1131,6 +1140,18 @@ fn parse_com_interface_from_index( cur_name = base.1; } + // Refuse to guess a root offset when the walk didn't terminate cleanly: + // an unknown-shape base chain would produce wrong absolute vtable slots + // and therefore wrong method dispatch. Callers see `None` and can log / + // surface a clearer error than a silent mis-generation. + if !terminated_at_known_root { + eprintln!( + "warning: base-chain walk for {}.{} did not terminate at IUnknown or IInspectable — refusing to guess vtable root offset", + namespace, name + ); + return None; + } + // Compute root offset (3 for IUnknown, 6 for IInspectable) and the // absolute vtable slot at which THIS leaf interface's own methods start. let root_offset = if is_iunknown_rooted { 3 } else { 6 }; From 3fb57a96e753622a266d271e19c5fa982fd21151 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Wed, 22 Jul 2026 16:22:41 +0800 Subject: [PATCH 04/28] codegen(com): reject non-interface TypeDefs in parse_com_interface_from_index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add an early guard that returns None unless the resolved TypeDef has `TypeAttributes::Interface` set. Prevents WinRT runtime classes, structs, enums, and delegates from being mis-parsed by walking their `interface_impls()` and flattening a bogus method list — which could have quietly routed `--class-name *Interop` runtime classes through the classic-COM code path in `main.rs`. Callers see `None` and fall through to the correct WinRT path. All existing tests + node/py/ts E2Es unaffected (real interfaces still have the Interface attribute set). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/dynwinrt-codegen/src/meta.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index efb96c0f..80889ce4 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -1089,6 +1089,19 @@ fn parse_com_interface_from_index( ) -> Option { let def = index.get(namespace, name).next()?; + // Guard: refuse to treat non-interface TypeDefs (WinRT runtime classes, + // enums, structs, delegates) as classic-COM interfaces. Without this, + // routing a name that happens to resolve to e.g. a `*Interop` runtime + // class through this path would walk its `interface_impls()` and produce + // a bogus flattened method list. Callers see `None` and can fall through + // to the correct WinRT code path in `main.rs`. + if !def + .flags() + .contains(windows_metadata::TypeAttributes::Interface) + { + return None; + } + // Walk the interface_impls chain: for each base, collect its own method // count, and stop at IUnknown or IInspectable. Traverse from the leaf up // so we can compute cumulative offsets. From b4150112dd54c62fc77ef7429a3b40a8f37493b9 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Wed, 22 Jul 2026 08:37:47 +0000 Subject: [PATCH 05/28] Round-5 review fixes: hwnd cache, doc wording, RC ambiguity, u64 f64 path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - napi::create_test_hwnd() now caches the HWND in an AtomicUsize so repeated calls in a long-lived Node process (tests, REPL, Electron) don't accumulate window handles. - codegen(com): handle typedef comments now say "Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR)" instead of just "Opaque Win32 handle" — the aliases cover both handles and pointer newtypes like PWSTR/PCWSTR. Snapshots updated. - meta::find_runtime_class_default_iid now collects all runtime-class matches for a simple name and refuses to pick when they resolve to distinct default-interface IIDs (cross-namespace collisions). Emits an explicit warning listing the candidates and returns None so callers fall through instead of silently generating interop wrappers with the wrong IID. - DynWinRTValue.u64 number branch now takes f64 (not i64) so we can reject NaN, +/-Infinity, and fractional values explicitly. napi's previous i64 coercion silently truncated fractions and mis-handled non-finite inputs. Bigint path and MAX_SAFE_INTEGER bound unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bindings/js/src/lib.rs | 57 ++++++++++++++++--- tools/dynwinrt-codegen/src/codegen/com.rs | 4 +- tools/dynwinrt-codegen/src/meta.rs | 31 +++++++++- .../DataTransferManager.d.ts | 2 +- .../IDataTransferManagerInterop.d.ts | 2 +- .../itaskbarlist3/ITaskbarList3.d.ts | 8 +-- 6 files changed, 84 insertions(+), 20 deletions(-) diff --git a/bindings/js/src/lib.rs b/bindings/js/src/lib.rs index 3859494a..78a7000b 100644 --- a/bindings/js/src/lib.rs +++ b/bindings/js/src/lib.rs @@ -657,9 +657,28 @@ impl DynWinRTValue { /// This lives in classic-vertical because it is the classic-COM/interop /// vertical's own way to obtain a process-owned HWND for testing — it /// avoids taking a flat-Win32 dependency for the classic tests. + /// Create a small process-owned HWND for use by the classic-COM E2E + /// tests. Returns the same cached HWND on subsequent calls to avoid + /// leaking window handles in long-lived Node processes (test runners, + /// REPLs, Electron). Marshalled as a `bigint`; on the way back into a + /// classic-COM call, wrap with `DynWinRtValue.pointer(bigint)`. + /// + /// Kept as a napi export (not a Node-side test helper) because it + /// avoids taking a flat-Win32 dependency for the classic tests. #[napi] pub fn create_test_hwnd() -> napi::Result { use windows::Win32::UI::WindowsAndMessaging::{CreateWindowExW, WINDOW_EX_STYLE, WS_POPUP}; + + // Guard: return the previously-created HWND on repeat calls. Storing + // the pointer bits as an `AtomicUsize` (rather than a full HWND) keeps + // the static Send/Sync without needing an unsafe impl. + use std::sync::atomic::{AtomicUsize, Ordering}; + static CACHED_HWND: AtomicUsize = AtomicUsize::new(0); + let cached = CACHED_HWND.load(Ordering::Acquire); + if cached != 0 { + return Ok(BigInt::from(cached as u64)); + } + let class_name: Vec = "STATIC".encode_utf16().chain(std::iter::once(0)).collect(); let title: Vec = "dynwinrt-test-hwnd\0".encode_utf16().collect(); let hwnd = unsafe { @@ -679,7 +698,13 @@ impl DynWinRTValue { ) } .map_err(|e| napi::Error::from_reason(format!("CreateWindowExW: {}", e)))?; - Ok(BigInt::from(hwnd.0 as u64)) + let bits = hwnd.0 as usize; + // Only publish to the cache if creation succeeded. Losing a race here + // is harmless: one of the racers wins, the losers' HWND is used once + // and then never destroyed — the cache guarantees at most O(#racers) + // leaked windows, not O(#calls). + CACHED_HWND.store(bits, Ordering::Release); + Ok(BigInt::from(bits as u64)) } /// Wrap a pointer/handle (BigInt, Buffer, or another `DynWinRtValue` holding @@ -892,12 +917,16 @@ impl DynWinRTValue { /// params like stream seek/size). Accepting both keeps the WinRT path /// working while supporting the 64-bit classic-COM path. /// - /// Negative values, values > u64::MAX (bigint), or negative numbers (JS - /// number) are rejected up front; silent truncation used to be possible - /// via BigInt::get_u64()'s sign/lossless flags and via `i64 as u64` on - /// the number path. + /// Bigint path: rejects negative bigints and values > u64::MAX. + /// + /// Number path: takes `f64` (not `i64`) so we can detect and reject + /// NaN / Infinity / fractional values explicitly — coercing through + /// napi's `i64` conversion would silently truncate fractions and + /// mishandle non-finite inputs. Bounded above by + /// `Number.MAX_SAFE_INTEGER` (2^53 - 1); larger values must come in as + /// a bigint. #[napi(ts_args_type = "value: bigint | number")] - pub fn u64(value: Either) -> napi::Result { + pub fn u64(value: Either) -> napi::Result { let n = match value { Either::A(big) => { let (sign_bit, n, lossless) = big.get_u64(); @@ -914,16 +943,26 @@ impl DynWinRTValue { n } Either::B(num) => { - if num < 0 { + if !num.is_finite() { + return Err(napi::Error::from_reason( + "u64(): number must be finite (got NaN or Infinity); use bigint for arbitrary values", + )); + } + if num.fract() != 0.0 { + return Err(napi::Error::from_reason( + "u64(): number must be an integer (got a fractional value); use Math.trunc/round or bigint", + )); + } + if num < 0.0 { return Err(napi::Error::from_reason( "u64(): number must be non-negative; use bigint for the full u64 range", )); } // JS Number can only faithfully represent integers up to 2^53 - 1; // anything above that has already been rounded by the time napi - // converts to i64. Refuse it explicitly so callers switch to bigint + // converts to f64. Refuse it explicitly so callers switch to bigint // instead of silently marshalling a lossy value. - const MAX_SAFE_INTEGER: i64 = 9_007_199_254_740_991; // (1 << 53) - 1 + const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; // (1 << 53) - 1 if num > MAX_SAFE_INTEGER { return Err(napi::Error::from_reason( "u64(): number exceeds Number.MAX_SAFE_INTEGER; use bigint for the full u64 range", diff --git a/tools/dynwinrt-codegen/src/codegen/com.rs b/tools/dynwinrt-codegen/src/codegen/com.rs index 229f6d99..da1dc011 100644 --- a/tools/dynwinrt-codegen/src/codegen/com.rs +++ b/tools/dynwinrt-codegen/src/codegen/com.rs @@ -725,7 +725,7 @@ fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String let handle_aliases = collect_handle_aliases(meta); for h in &handle_aliases { out.push_str(&format!( - "/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */\nexport type {h} = bigint | Buffer;\n", + "/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */\nexport type {h} = bigint | Buffer;\n", h = h )); } @@ -958,7 +958,7 @@ fn render_projected_class_files( let handle_aliases = collect_handle_aliases(meta); for h in &handle_aliases { dts.push_str(&format!( - "/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */\nexport type {h} = bigint | Buffer;\n", + "/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */\nexport type {h} = bigint | Buffer;\n", h = h )); } diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index 80889ce4..bfe6251e 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -172,7 +172,14 @@ pub fn find_runtime_class_default_iid( simple_name: &str, ) -> Option<(String, String, String)> { let index = load_index(winmd_paths)?; - // Iterate ALL TypeDefs looking for a runtime class matching `simple_name`. + // Collect *all* runtime classes with this simple name so we can detect + // cross-namespace collisions (e.g. two runtime classes both called + // `SomeThing` in different namespaces). Returning the first match blindly + // would silently drive interop codegen with the wrong default-interface + // IID → wrappers that call `GetForWindow(riid=…, ppv)` for a different + // interface than the caller expects. + let mut found: Option<(String, String, String)> = None; + let mut collisions: Vec<(String, String, String)> = Vec::new(); for def in index.all() { if def.name() != simple_name { continue; @@ -207,10 +214,28 @@ pub fn find_runtime_class_default_iid( if iid.is_empty() { continue; } - return Some((namespace, tn.name.clone(), iid)); + let candidate = (namespace.clone(), tn.name.clone(), iid); + match &found { + None => found = Some(candidate), + Some(prev) if prev == &candidate => { + // Exact duplicate — same namespace + same IID means the + // same TypeDef, harmless. + } + Some(_) => collisions.push(candidate), + } + break; // stop looking at this class's other interface_impls } } - None + if !collisions.is_empty() { + let mut all = vec![found.clone().unwrap()]; + all.extend(collisions); + eprintln!( + "warning: find_runtime_class_default_iid({}): multiple runtime classes with this simple name resolve to distinct default IIDs — refusing to guess. Candidates: {:?}", + simple_name, all + ); + return None; + } + found } /// Discover the NEWEST installed Windows SDK `Windows.winmd` by enumerating the diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.d.ts b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.d.ts index 567a7435..dd52ede7 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.d.ts +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.d.ts @@ -1,6 +1,6 @@ // Generated by dynwinrt-codegen — do not edit -/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ export type HWND = bigint | Buffer; export declare class DataTransferManager { diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts index 83b9e2bc..7ad9ce06 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts @@ -1,7 +1,7 @@ // Generated by dynwinrt-codegen — do not edit import { DataTransferManager } from './DataTransferManager.js'; -/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ export type HWND = bigint | Buffer; export declare const IID_IDataTransferManagerInterop: unknown; diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts index 2c255bed..d4cf0607 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts @@ -1,13 +1,13 @@ // Generated by dynwinrt-codegen — do not edit import { TBPFLAG } from './TBPFLAG.js'; -/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ export type HICON = bigint | Buffer; -/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ export type HIMAGELIST = bigint | Buffer; -/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ export type HWND = bigint | Buffer; -/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ export type PWSTR = bigint | Buffer; export declare const IID_ITaskbarList3: unknown; From cbe661095d10ee10869d51e31d8d547eb5fdcd07 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Wed, 22 Jul 2026 16:46:09 +0800 Subject: [PATCH 06/28] codegen(com): fail closed when a base classic-COM interface can't be parsed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parse_com_interface_from_index previously logged a warning and continued when parse_interface_with_offset returned None for a base interface. That left slot_cursor unadvanced and produced a truncated flattened method list, so the leaf interface's absolute vtable indices would be off by however many base methods were missing. The debug_assert_eq! below caught this in debug builds, but in release it was silently compiled out — so codegen would emit wrappers that dispatch to the wrong COM methods. Now return None on any base-parse failure (with a warning naming both the missing base and the leaf we're refusing to emit), so callers see a clean skip rather than misgeneration. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- tools/dynwinrt-codegen/src/meta.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index bfe6251e..6fc7a0ef 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -1216,14 +1216,24 @@ fn parse_com_interface_from_index( chain_top_down.retain(|(_, n, _)| n != "IUnknown" && n != "IInspectable"); for (base_ns, base_name, _own_count) in chain_top_down { - if let Some(base_iface) = parse_interface_with_offset(index, base_ns, base_name, slot_cursor) { - slot_cursor += base_iface.methods.len(); - methods.extend(base_iface.methods); - } else { - eprintln!( - "warning: could not parse base classic-COM interface {}.{}", - base_ns, base_name - ); + match parse_interface_with_offset(index, base_ns, base_name, slot_cursor) { + Some(base_iface) => { + slot_cursor += base_iface.methods.len(); + methods.extend(base_iface.methods); + } + None => { + // Fail loud: if we can't parse a base interface's methods, + // the flattened method list would be missing entries and the + // leaf's absolute vtable indices would be wrong. In release + // the `debug_assert_eq!` below is compiled out, so we'd + // silently emit wrappers that dispatch to the wrong COM + // methods. Return None so callers surface a clear error. + eprintln!( + "warning: could not parse base classic-COM interface {}.{} — refusing to emit {}.{} with a truncated vtable", + base_ns, base_name, namespace, name + ); + return None; + } } } // Assert the invariant that we lined up correctly. From a935328988cc0e4be588b6fb4bf4cdcb44d6b084 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Wed, 22 Jul 2026 16:57:27 +0800 Subject: [PATCH 07/28] Round-7 review fixes: harden base_slot dedup + robust default-IID lookup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MetadataTable::register_interface and register_interface_iunknown now route through create_interface_method_table*(iid, base_slot) BEFORE checking the name cache. That call is already assertive on base_slot mismatch (arena.rs:131). Previously a first-time register_interface(name, iid) with base_slot=6 would let a later register_interface_iunknown(name, iid) — expecting base_slot=3 — silently reuse the WinRT-shaped vtable and dispatch to the wrong absolute slots. Now the mismatch panics loudly. - meta::find_runtime_class_default_iid: replaced the `?` on the default-interface TypeDef lookup with a `let-else { continue }`. A missing/unreadable interface TypeDef for one candidate no longer aborts the whole search — other matching runtime classes (or other DefaultAttribute impls on the same class) can still resolve. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- crates/dynwinrt/src/metadata_table/mod.rs | 15 +++++++++++++-- tools/dynwinrt-codegen/src/meta.rs | 9 ++++++++- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/dynwinrt/src/metadata_table/mod.rs b/crates/dynwinrt/src/metadata_table/mod.rs index edea6ccb..22d0b011 100644 --- a/crates/dynwinrt/src/metadata_table/mod.rs +++ b/crates/dynwinrt/src/metadata_table/mod.rs @@ -232,10 +232,14 @@ impl MetadataTable { /// Register a named interface. Creates an IID → method table. /// Returns a TypeHandle for chaining `.add_method()`. pub fn register_interface(self: &Arc, name: &str, iid: GUID) -> TypeHandle { + // See `register_interface_iunknown` for the rationale: always route + // through the assertive method-table creator so a stale registration + // with a mismatched base_slot fails loudly instead of silently + // returning the wrong vtable. + self.create_interface_method_table(iid); if let Some(kind) = self.get_named_type(name) { return self.make(kind); } - self.create_interface_method_table(iid); let kind = TypeKind::Interface(iid); self.insert_named_type(name, kind); self.make(kind) @@ -245,10 +249,17 @@ impl MetadataTable { /// start at vtable slot 3 (QI/AddRef/Release occupy 0/1/2), rather than the /// WinRT default of 6 (IInspectable adds three more slots at 3/4/5). pub fn register_interface_iunknown(self: &Arc, name: &str, iid: GUID) -> TypeHandle { + // Even if the name already resolves to a TypeKind, still route through + // `create_interface_method_table_with_base(iid, 3)`. That call is + // idempotent when the IID's method table already exists with the same + // base_slot, and panics loudly (see arena.rs:131) if a prior + // `register_interface` created it with base_slot=6. This closes the + // window where callers would otherwise silently reuse a WinRT-shaped + // vtable for classic-COM dispatch and get wrong absolute slots. + self.create_interface_method_table_with_base(iid, 3); if let Some(kind) = self.get_named_type(name) { return self.make(kind); } - self.create_interface_method_table_with_base(iid, 3); let kind = TypeKind::Interface(iid); self.insert_named_type(name, kind); self.make(kind) diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index 6fc7a0ef..72c62544 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -209,7 +209,14 @@ pub fn find_runtime_class_default_iid( // separate computation. continue; } - let iface_def = index.get(&tn.namespace, &tn.name).next()?; + let Some(iface_def) = index.get(&tn.namespace, &tn.name).next() else { + // Unreadable/missing TypeDef for this DefaultAttribute impl + // — skip *this* candidate rather than aborting the whole + // lookup. Other matching runtime classes (or other + // DefaultAttribute impls on the same class) can still resolve + // successfully. + continue; + }; let iid = extract_iid(&iface_def); if iid.is_empty() { continue; From 84919b1d05454f51d59816f2d104d880065fe88d Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Wed, 22 Jul 2026 22:33:59 +0800 Subject: [PATCH 08/28] Fix: set is_flags on new classic-COM test enum construction (merge origin/main) --- tools/dynwinrt-codegen/src/codegen/com.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/dynwinrt-codegen/src/codegen/com.rs b/tools/dynwinrt-codegen/src/codegen/com.rs index da1dc011..7867318f 100644 --- a/tools/dynwinrt-codegen/src/codegen/com.rs +++ b/tools/dynwinrt-codegen/src/codegen/com.rs @@ -1789,6 +1789,7 @@ mod tests { name: "MyKind".into(), underlying: Box::new(TypeMeta::I32), members: Vec::new(), + is_flags: false, doc: None, deprecated: None, }, From bc0c8bbb9e1558e4905238a5d839ae3f27dac9d3 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Thu, 23 Jul 2026 09:19:49 +0800 Subject: [PATCH 09/28] Classic-COM codegen fixes: u16Type + HRESULT-in + out-string buffers + COM pointer adoption Fixes 4 gaps surfaced by a systematic Windows.Win32 winmd exploration sweep: - C4: add napi DynWinRtType.u16Type() (+ i16Type/u8Type/f32Type/f64Type aliases). - C1: [in] HRESULT params project as number / i32Type() / DynWinRtValue.i32(hr). - C2: caller-owned [out] PWSTR + cch string buffers (IShellLinkW.GetPath/ GetDescription) generate a real wrapper via ParamDirection::OutStringBuffer; narrow detector (direct PWSTR/PSTR + adjacent char-count; PWSTR* not matched; cb byte-counts excluded for PWSTR; PSTR fails loud). Previously crashed. - C3: adoptComPointer(ptr, iid?) adopts an AddRef-owned returned COM pointer via IUnknown::from_raw (no extra AddRef) + optional QI-validate; codegen wraps directly-named TypeMeta::Interface out-params as typed wrappers. Tests: TDD unit tests each; refcount-correct native adoption test; e2e/shelllink-buffer.mjs proves SetPath/GetPath + SetDescription/GetDescription round-trips on live classic COM. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bindings/js/e2e/IShellLinkW.d.ts | 32 ++ bindings/js/e2e/IShellLinkW.js | 123 +++++ bindings/js/e2e/SHOW_WINDOW_CMD.d.ts | 19 + bindings/js/e2e/SHOW_WINDOW_CMD.js | 18 + bindings/js/e2e/shelllink-buffer.mjs | 23 + bindings/js/src/lib.rs | 179 +++++- crates/dynwinrt/src/classic_com.rs | 34 ++ tools/dynwinrt-codegen/src/codegen/com.rs | 516 +++++++++++++++--- .../src/codegen/javascript/project/methods.rs | 4 +- .../src/codegen/python/type_helpers.rs | 3 +- .../src/codegen/shared/imports.rs | 6 +- tools/dynwinrt-codegen/src/main.rs | 36 +- tools/dynwinrt-codegen/src/meta.rs | 192 ++++++- .../tests/win32_com_interop_test.rs | 54 +- .../dynwinrt-codegen/tests/win32_com_test.rs | 216 +++++--- 15 files changed, 1253 insertions(+), 202 deletions(-) create mode 100644 bindings/js/e2e/IShellLinkW.d.ts create mode 100644 bindings/js/e2e/IShellLinkW.js create mode 100644 bindings/js/e2e/SHOW_WINDOW_CMD.d.ts create mode 100644 bindings/js/e2e/SHOW_WINDOW_CMD.js create mode 100644 bindings/js/e2e/shelllink-buffer.mjs diff --git a/bindings/js/e2e/IShellLinkW.d.ts b/bindings/js/e2e/IShellLinkW.d.ts new file mode 100644 index 00000000..b55dab60 --- /dev/null +++ b/bindings/js/e2e/IShellLinkW.d.ts @@ -0,0 +1,32 @@ +// Generated by dynwinrt-codegen — do not edit +import { SHOW_WINDOW_CMD } from './SHOW_WINDOW_CMD.js'; + +/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ +export type HWND = bigint | Buffer; +/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ +export type PWSTR = bigint | Buffer; + +export declare const IID_IShellLinkW: unknown; + +export declare class IShellLinkW { + /** Wrap an existing native COM pointer (for QueryInterface bridging). */ + static _fromNative(obj: unknown): IShellLinkW; + getPath(cch?: number, fFlags?: number): string; + getIDList(): bigint | Buffer; + setIDList(pidl: bigint | Buffer): void; + getDescription(cch?: number): string; + setDescription(name: PWSTR): void; + getWorkingDirectory(cch?: number): string; + setWorkingDirectory(dir: PWSTR): void; + getArguments(cch?: number): string; + setArguments(args: PWSTR): void; + getHotkey(): bigint | Buffer; + setHotkey(wHotkey: number): void; + getShowCmd(): bigint | Buffer; + setShowCmd(iShowCmd: SHOW_WINDOW_CMD): void; + getIconLocation(cch?: number): string; + setIconLocation(iconPath: PWSTR, iIcon: number): void; + setRelativePath(pathRel: PWSTR, reserved: number): void; + resolve(hwnd: HWND, fFlags: number): void; + setPath(file: PWSTR): void; +} diff --git a/bindings/js/e2e/IShellLinkW.js b/bindings/js/e2e/IShellLinkW.js new file mode 100644 index 00000000..646be59d --- /dev/null +++ b/bindings/js/e2e/IShellLinkW.js @@ -0,0 +1,123 @@ +// Generated by dynwinrt-codegen — do not edit +import { DynWinRtType, DynWinRtMethodSig, DynWinRtValue, WinGuid } from '../dist/index.js'; +import { SHOW_WINDOW_CMD } from './SHOW_WINDOW_CMD.js'; + +function _normalizeStringBufferCount(value, name) { + if (!Number.isInteger(value) || value <= 0) throw new TypeError(`${name} must be a positive integer`); + return value; +} +function _decodeWideString(buffer) { + let end = 0; + while (end + 1 < buffer.length && buffer.readUInt16LE(end) !== 0) end += 2; + return buffer.subarray(0, end).toString('utf16le'); +} + +export const IID_IShellLinkW = WinGuid.parse('000214f9-0000-0000-c000-000000000046'); + +let _IShellLinkWCache; +const _IShellLinkW = new Proxy({}, { + get(_target, prop) { + _IShellLinkWCache ??= DynWinRtType.registerInterfaceUnknown('IShellLinkW', IID_IShellLinkW) + .addMethod('GetPath', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type()).addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type())) + .addMethod('GetIDList', new DynWinRtMethodSig().addOut(DynWinRtType.pointer())) + .addMethod('SetIDList', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) + .addMethod('GetDescription', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) + .addMethod('SetDescription', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) + .addMethod('GetWorkingDirectory', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) + .addMethod('SetWorkingDirectory', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) + .addMethod('GetArguments', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) + .addMethod('SetArguments', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) + .addMethod('GetHotkey', new DynWinRtMethodSig().addOut(DynWinRtType.pointer())) + .addMethod('SetHotkey', new DynWinRtMethodSig().addIn(DynWinRtType.u16Type())) + .addMethod('GetShowCmd', new DynWinRtMethodSig().addOut(DynWinRtType.pointer())) + .addMethod('SetShowCmd', new DynWinRtMethodSig().addIn(DynWinRtType.i32Type())) + .addMethod('GetIconLocation', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type()).addOut(DynWinRtType.pointer())) + .addMethod('SetIconLocation', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) + .addMethod('SetRelativePath', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type())) + .addMethod('Resolve', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type())) + .addMethod('SetPath', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())); + const value = _IShellLinkWCache[prop]; + return typeof value === 'function' ? value.bind(_IShellLinkWCache) : value; + }, +}); + +export class IShellLinkW { + _obj; + constructor(obj) { this._obj = obj; } + static _fromNative(obj) { return new IShellLinkW(obj); } + getPath(cch = 260, fFlags = 0) { + cch = _normalizeStringBufferCount(cch, 'cch'); + const _buffer = Buffer.alloc(cch * 2); + _IShellLinkW.method(3).invoke(this._obj, [DynWinRtValue.pointer(_buffer), DynWinRtValue.i32(cch), DynWinRtValue.pointer(0n), DynWinRtValue.u32(fFlags)]); + return _decodeWideString(_buffer); + } + getIDList() { + const _out = _IShellLinkW.method(4).invoke(this._obj, []); + // TODO: raw COM interface pointer adoption requires preserved pointee metadata. + return _out; + } + setIDList(pidl) { + _IShellLinkW.method(5).invoke(this._obj, [DynWinRtValue.pointer(pidl)]); + } + getDescription(cch = 260) { + cch = _normalizeStringBufferCount(cch, 'cch'); + const _buffer = Buffer.alloc(cch * 2); + _IShellLinkW.method(6).invoke(this._obj, [DynWinRtValue.pointer(_buffer), DynWinRtValue.i32(cch)]); + return _decodeWideString(_buffer); + } + setDescription(name) { + _IShellLinkW.method(7).invoke(this._obj, [DynWinRtValue.pointer(name)]); + } + getWorkingDirectory(cch = 260) { + cch = _normalizeStringBufferCount(cch, 'cch'); + const _buffer = Buffer.alloc(cch * 2); + _IShellLinkW.method(8).invoke(this._obj, [DynWinRtValue.pointer(_buffer), DynWinRtValue.i32(cch)]); + return _decodeWideString(_buffer); + } + setWorkingDirectory(dir) { + _IShellLinkW.method(9).invoke(this._obj, [DynWinRtValue.pointer(dir)]); + } + getArguments(cch = 260) { + cch = _normalizeStringBufferCount(cch, 'cch'); + const _buffer = Buffer.alloc(cch * 2); + _IShellLinkW.method(10).invoke(this._obj, [DynWinRtValue.pointer(_buffer), DynWinRtValue.i32(cch)]); + return _decodeWideString(_buffer); + } + setArguments(args) { + _IShellLinkW.method(11).invoke(this._obj, [DynWinRtValue.pointer(args)]); + } + getHotkey() { + const _out = _IShellLinkW.method(12).invoke(this._obj, []); + // TODO: raw COM interface pointer adoption requires preserved pointee metadata. + return _out; + } + setHotkey(wHotkey) { + _IShellLinkW.method(13).invoke(this._obj, [DynWinRtValue.u16Value(wHotkey)]); + } + getShowCmd() { + const _out = _IShellLinkW.method(14).invoke(this._obj, []); + // TODO: raw COM interface pointer adoption requires preserved pointee metadata. + return _out; + } + setShowCmd(iShowCmd) { + _IShellLinkW.method(15).invoke(this._obj, [DynWinRtValue.i32(iShowCmd)]); + } + getIconLocation(cch = 260) { + cch = _normalizeStringBufferCount(cch, 'cch'); + const _buffer = Buffer.alloc(cch * 2); + _IShellLinkW.method(16).invoke(this._obj, [DynWinRtValue.pointer(_buffer), DynWinRtValue.i32(cch)]); + return _decodeWideString(_buffer); + } + setIconLocation(iconPath, iIcon) { + _IShellLinkW.method(17).invoke(this._obj, [DynWinRtValue.pointer(iconPath), DynWinRtValue.i32(iIcon)]); + } + setRelativePath(pathRel, reserved) { + _IShellLinkW.method(18).invoke(this._obj, [DynWinRtValue.pointer(pathRel), DynWinRtValue.u32(reserved)]); + } + resolve(hwnd, fFlags) { + _IShellLinkW.method(19).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.u32(fFlags)]); + } + setPath(file) { + _IShellLinkW.method(20).invoke(this._obj, [DynWinRtValue.pointer(file)]); + } +} diff --git a/bindings/js/e2e/SHOW_WINDOW_CMD.d.ts b/bindings/js/e2e/SHOW_WINDOW_CMD.d.ts new file mode 100644 index 00000000..956c018a --- /dev/null +++ b/bindings/js/e2e/SHOW_WINDOW_CMD.d.ts @@ -0,0 +1,19 @@ +// Generated by dynwinrt-codegen — do not edit +export type SHOW_WINDOW_CMD = (typeof SHOW_WINDOW_CMD)[keyof typeof SHOW_WINDOW_CMD]; +export declare const SHOW_WINDOW_CMD: { + readonly SW_HIDE: 0; + readonly SW_SHOWNORMAL: 1; + readonly SW_NORMAL: 1; + readonly SW_SHOWMINIMIZED: 2; + readonly SW_SHOWMAXIMIZED: 3; + readonly SW_MAXIMIZE: 3; + readonly SW_SHOWNOACTIVATE: 4; + readonly SW_SHOW: 5; + readonly SW_MINIMIZE: 6; + readonly SW_SHOWMINNOACTIVE: 7; + readonly SW_SHOWNA: 8; + readonly SW_RESTORE: 9; + readonly SW_SHOWDEFAULT: 10; + readonly SW_FORCEMINIMIZE: 11; + readonly SW_MAX: 11; +}; diff --git a/bindings/js/e2e/SHOW_WINDOW_CMD.js b/bindings/js/e2e/SHOW_WINDOW_CMD.js new file mode 100644 index 00000000..87f174b7 --- /dev/null +++ b/bindings/js/e2e/SHOW_WINDOW_CMD.js @@ -0,0 +1,18 @@ +// Generated by dynwinrt-codegen — do not edit +export const SHOW_WINDOW_CMD = Object.freeze({ + SW_HIDE: 0, + SW_SHOWNORMAL: 1, + SW_NORMAL: 1, + SW_SHOWMINIMIZED: 2, + SW_SHOWMAXIMIZED: 3, + SW_MAXIMIZE: 3, + SW_SHOWNOACTIVATE: 4, + SW_SHOW: 5, + SW_MINIMIZE: 6, + SW_SHOWMINNOACTIVE: 7, + SW_SHOWNA: 8, + SW_RESTORE: 9, + SW_SHOWDEFAULT: 10, + SW_FORCEMINIMIZE: 11, + SW_MAX: 11, +}); diff --git a/bindings/js/e2e/shelllink-buffer.mjs b/bindings/js/e2e/shelllink-buffer.mjs new file mode 100644 index 00000000..a38fbe60 --- /dev/null +++ b/bindings/js/e2e/shelllink-buffer.mjs @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import { DynWinRtValue } from '../dist/index.js'; +import { IShellLinkW, IID_IShellLinkW } from './IShellLinkW.js'; + +const CLSID_SHELL_LINK = '00021401-0000-0000-c000-000000000046'; + +function wide(text) { + return Buffer.from(`${text}\0`, 'utf16le'); +} + +const link = IShellLinkW._fromNative( + DynWinRtValue.coCreateInstance(CLSID_SHELL_LINK, IID_IShellLinkW), +); + +const expectedPath = 'C:\\Windows\\explorer.exe'; +link.setPath(wide(expectedPath)); +assert.equal(link.getPath(260, 0).toLowerCase(), expectedPath.toLowerCase()); + +const expectedDescription = 'dynwinrt shelllink buffer'; +link.setDescription(wide(expectedDescription)); +assert.equal(link.getDescription(), expectedDescription); + +console.log('shelllink-buffer ok'); diff --git a/bindings/js/src/lib.rs b/bindings/js/src/lib.rs index cf39a051..b9c38504 100644 --- a/bindings/js/src/lib.rs +++ b/bindings/js/src/lib.rs @@ -7,10 +7,10 @@ use std::sync::{Arc, Mutex, OnceLock}; use dynwinrt; -use napi::JsValue; use napi::bindgen_prelude::BigInt; use napi::bindgen_prelude::Either; use napi::threadsafe_function::ThreadsafeFunctionCallMode; +use napi::JsValue; use napi_derive::napi; use windows::core::{IUnknown, Interface, HSTRING}; @@ -159,11 +159,36 @@ impl DynWinRTType { DynWinRTType(TABLE.i16_type()) } + #[napi] + pub fn i16_type() -> Self { + DynWinRTType(TABLE.i16_type()) + } + #[napi] pub fn u16() -> Self { DynWinRTType(TABLE.u16_type()) } + #[napi] + pub fn u16_type() -> Self { + DynWinRTType(TABLE.u16_type()) + } + + #[napi] + pub fn u8_type() -> Self { + DynWinRTType(TABLE.u8_type()) + } + + #[napi] + pub fn f32_type() -> Self { + DynWinRTType(TABLE.f32_type()) + } + + #[napi] + pub fn f64_type() -> Self { + DynWinRTType(TABLE.f64_type()) + } + #[napi] pub fn bool_type() -> Self { DynWinRTType(TABLE.bool_type()) @@ -594,9 +619,8 @@ impl DynWinRTValue { // WinRT's RoGetActivationFactory requires the thread apartment to be // initialized. Node's main thread is not COM-initialized by default, so // do it lazily on the first call (same behaviour as `coCreateInstance`). - dynwinrt::classic_com::ensure_com_initialized().map_err(|e| { - napi::Error::from_reason(format!("ensure_com_initialized: {}", e.message())) - })?; + dynwinrt::classic_com::ensure_com_initialized() + .map_err(|e| napi::Error::from_reason(format!("ensure_com_initialized: {}", e.message())))?; let factory = dynwinrt::ro_get_activation_factory_2(&HSTRING::from(&name)).map_err(|e| { napi::Error::from_reason(format!("ActivationFactory '{}': {}", name, e.message())) })?; @@ -721,7 +745,7 @@ impl DynWinRTValue { #[napi] pub fn pointer( #[napi( - ts_arg_type = "bigint | number | Buffer | Uint8Array | DynWinRtValue | null | undefined" + ts_arg_type = "bigint | number | Buffer | Uint8Array | DynWinRtValue | null | undefined" )] value: napi::bindgen_prelude::Unknown, ) -> napi::Result { @@ -748,8 +772,7 @@ impl DynWinRTValue { // both so that DynWinRtValue.pointer(-1n) or a >2^64 bigint produce a // clean error instead of a fabricated pointer. if val_type == sys::ValueType::napi_bigint { - let bi = - unsafe { napi::bindgen_prelude::BigInt::from_napi_value(raw_env, raw_val) }?; + let bi = unsafe { napi::bindgen_prelude::BigInt::from_napi_value(raw_env, raw_val) }?; let (sign_bit, n, lossless) = bi.get_u64(); if sign_bit { return Err(napi::Error::from_reason( @@ -815,12 +838,10 @@ impl DynWinRTValue { } // Fast path 4: Buffer / Uint8Array → base data pointer. - if let Ok(buf) = - unsafe { napi::bindgen_prelude::Buffer::from_napi_value(raw_env, raw_val) } - { + if let Ok(buf) = unsafe { napi::bindgen_prelude::Buffer::from_napi_value(raw_env, raw_val) } { let slice: &[u8] = buf.as_ref(); return Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( - slice.as_ptr() as *mut std::ffi::c_void, + slice.as_ptr() as *mut std::ffi::c_void ))); } @@ -828,24 +849,21 @@ impl DynWinRTValue { // base data pointer. Buffer::from_napi_value above rejects raw // Uint8Array views even though the TS surface (`ts_arg_type`) advertises // Uint8Array. Handle it explicitly with the same semantics as Buffer. - if let Ok(arr) = - unsafe { napi::bindgen_prelude::Uint8Array::from_napi_value(raw_env, raw_val) } + if let Ok(arr) = unsafe { napi::bindgen_prelude::Uint8Array::from_napi_value(raw_env, raw_val) } { let slice: &[u8] = arr.as_ref(); return Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( - slice.as_ptr() as *mut std::ffi::c_void, + slice.as_ptr() as *mut std::ffi::c_void ))); } // Fast path 5: existing DynWinRtValue → reuse its pointer. if let Ok(v) = unsafe { <&DynWinRTValue>::from_napi_value(raw_env, raw_val) } { return match &v.0 { - dynwinrt::WinRTValue::Object(o) => Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( - o.as_raw(), - ))), - dynwinrt::WinRTValue::RawPtr(p) => { - Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr(*p))) + dynwinrt::WinRTValue::Object(o) => { + Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr(o.as_raw()))) } + dynwinrt::WinRTValue::RawPtr(p) => Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr(*p))), dynwinrt::WinRTValue::Null => Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( std::ptr::null_mut(), ))), @@ -860,6 +878,30 @@ impl DynWinRTValue { )) } + /// Adopt an AddRef-owned COM interface pointer as a managed Object value. + /// This takes ownership of the caller's reference and must not be used for + /// borrowed pointers. Existing DynWinRtValue inputs are intentionally + /// rejected to avoid adopting a borrowed pointer from an owned wrapper. + #[napi] + pub fn adopt_com_pointer( + #[napi(ts_arg_type = "bigint | number | Buffer | Uint8Array | null | undefined")] + value: napi::bindgen_prelude::Unknown, + iid: Option<&WinGUID>, + ) -> napi::Result { + let ptr = raw_pointer_from_unknown(value, "adoptComPointer")?; + let adopted = unsafe { dynwinrt::classic_com::adopt_com_pointer(ptr) }; + if let Some(iid) = iid { + adopted.cast(&iid.0).map(DynWinRTValue).map_err(|e| { + napi::Error::from_reason(format!( + "adoptComPointer QueryInterface failed: {}", + e.message() + )) + }) + } else { + Ok(DynWinRTValue(adopted)) + } + } + /// Get the underlying pointer of an Object/RawPtr value as a BigInt. /// Useful for turning a pointer result (e.g. HWND from /// `GetConsoleWindow`) into a bigint you can then feed into other calls. @@ -936,9 +978,7 @@ impl DynWinRTValue { )); } if !lossless { - return Err(napi::Error::from_reason( - "u64(): bigint exceeds u64::MAX", - )); + return Err(napi::Error::from_reason("u64(): bigint exceeds u64::MAX")); } n } @@ -1044,9 +1084,9 @@ impl DynWinRTValue { } let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new())); let mut map = cache.lock().unwrap(); - let addr = *map.entry(key).or_insert_with(|| { - Box::into_raw(Box::new(g)) as usize - }); + let addr = *map + .entry(key) + .or_insert_with(|| Box::into_raw(Box::new(g)) as usize); DynWinRTValue(dynwinrt::WinRTValue::RawPtr(addr as *mut std::ffi::c_void)) } #[napi] @@ -1285,6 +1325,81 @@ impl DynWinRTValue { } } +fn raw_pointer_from_unknown( + value: napi::bindgen_prelude::Unknown, + context: &str, +) -> napi::Result<*mut std::ffi::c_void> { + use napi::bindgen_prelude::FromNapiValue; + use napi::sys; + + let raw_env = value.value().env; + let raw_val = value.value().value; + let mut val_type = sys::ValueType::napi_undefined; + unsafe { sys::napi_typeof(raw_env, raw_val, &mut val_type) }; + + if val_type == sys::ValueType::napi_null || val_type == sys::ValueType::napi_undefined { + return Ok(std::ptr::null_mut()); + } + + if val_type == sys::ValueType::napi_bigint { + let bi = unsafe { napi::bindgen_prelude::BigInt::from_napi_value(raw_env, raw_val) }?; + let (sign_bit, n, lossless) = bi.get_u64(); + if sign_bit { + return Err(napi::Error::from_reason(format!( + "{context}: bigint must be non-negative" + ))); + } + if !lossless { + return Err(napi::Error::from_reason(format!( + "{context}: bigint exceeds u64 range" + ))); + } + if (n as usize as u64) != n { + return Err(napi::Error::from_reason(format!( + "{context}: bigint exceeds usize range on this platform" + ))); + } + return Ok(n as usize as *mut std::ffi::c_void); + } + + if val_type == sys::ValueType::napi_number { + let mut d: f64 = 0.0; + unsafe { sys::napi_get_value_double(raw_env, raw_val, &mut d) }; + if !d.is_finite() || d < 0.0 || d.fract() != 0.0 { + return Err(napi::Error::from_reason(format!( + "{context}: number must be a finite, non-negative integer" + ))); + } + const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; + if d > MAX_SAFE_INTEGER { + return Err(napi::Error::from_reason(format!( + "{context}: number exceeds Number.MAX_SAFE_INTEGER; use bigint" + ))); + } + let bits = d as u64; + if (bits as usize as u64) != bits { + return Err(napi::Error::from_reason(format!( + "{context}: number exceeds usize range on this platform" + ))); + } + return Ok(bits as usize as *mut std::ffi::c_void); + } + + if let Ok(buf) = unsafe { napi::bindgen_prelude::Buffer::from_napi_value(raw_env, raw_val) } { + let slice: &[u8] = buf.as_ref(); + return Ok(slice.as_ptr() as *mut std::ffi::c_void); + } + + if let Ok(arr) = unsafe { napi::bindgen_prelude::Uint8Array::from_napi_value(raw_env, raw_val) } { + let slice: &[u8] = arr.as_ref(); + return Ok(slice.as_ptr() as *mut std::ffi::c_void); + } + + Err(napi::Error::from_reason(format!( + "{context}: expected bigint, number, Buffer, Uint8Array, null, or undefined" + ))) +} + // ====================================================================== // Array binding — blittable fast path via typed Vec, generic fallback // ====================================================================== @@ -2428,3 +2543,17 @@ pub fn raw_get_i32(method: &DynWinRTMethodHandle, obj: &DynWinRTValue) -> napi:: .call_getter_i32(raw) .map_err(|e| napi::Error::from_reason(e.message())) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn codegen_type_alias_constructors_exist() { + let _ = DynWinRTType::u16_type(); + let _ = DynWinRTType::i16_type(); + let _ = DynWinRTType::u8_type(); + let _ = DynWinRTType::f32_type(); + let _ = DynWinRTType::f64_type(); + } +} diff --git a/crates/dynwinrt/src/classic_com.rs b/crates/dynwinrt/src/classic_com.rs index bd662899..69bba0f2 100644 --- a/crates/dynwinrt/src/classic_com.rs +++ b/crates/dynwinrt/src/classic_com.rs @@ -65,6 +65,19 @@ pub fn co_create_instance(clsid: GUID, iid: GUID) -> result::Result Ok(WinRTValue::Object(unsafe { IUnknown::from_raw(result) })) } +/// Adopt an AddRef-owned COM interface pointer into a managed Object value. +/// +/// The pointer must represent a caller-owned COM reference (+1). This function +/// takes ownership with `IUnknown::from_raw` and must not be used for borrowed +/// pointers. +pub unsafe fn adopt_com_pointer(ptr: *mut c_void) -> WinRTValue { + if ptr.is_null() { + WinRTValue::Null + } else { + WinRTValue::Object(unsafe { IUnknown::from_raw(ptr) }) + } +} + pub fn call_method( vtable_index: usize, obj: *mut c_void, @@ -211,6 +224,27 @@ mod tests { Ok(()) } + #[test] + fn adopt_com_pointer_accepts_addref_owned_pointer() -> result::Result<()> { + let shell_link = shell_link()?.as_object().unwrap(); + let shell_link_raw = shell_link.as_raw(); + let borrowed = unsafe { IUnknown::from_raw_borrowed(&shell_link_raw) }.unwrap(); + let addref_owned = borrowed.clone(); + let raw = addref_owned.as_raw(); + std::mem::forget(addref_owned); + + let adopted = unsafe { adopt_com_pointer(raw) }; + let adopted = adopted.as_object().expect("adopted value must be Object"); + let table = MetadataTable::new(); + let iface = shell_link_signature(&table); + + iface.methods[15].call_dynamic(adopted.as_raw(), &[WinRTValue::I32(7)])?; + let result = iface.methods[14].call_dynamic(adopted.as_raw(), &[])?; + + assert_eq!(result[0].as_i32().unwrap(), 7); + Ok(()) + } + #[test] fn co_create_instance_with_bogus_clsid_returns_error() -> result::Result<()> { let bogus = GUID::from_u128(0xaaaaaaaa_bbbb_cccc_dddd_eeeeeeeeeeee); diff --git a/tools/dynwinrt-codegen/src/codegen/com.rs b/tools/dynwinrt-codegen/src/codegen/com.rs index 7867318f..f61806a5 100644 --- a/tools/dynwinrt-codegen/src/codegen/com.rs +++ b/tools/dynwinrt-codegen/src/codegen/com.rs @@ -87,7 +87,11 @@ pub fn generate_com_interface_files( extra_files.sort_by(|a, b| a.0.cmp(&b.0)); - Ok(ComGeneratedOutput { js, dts, extra_files }) + Ok(ComGeneratedOutput { + js, + dts, + extra_files, + }) } // --------------------------------------------------------------------------- @@ -325,7 +329,6 @@ fn resolve_projected_default_iid( crate::meta::find_runtime_class_default_iid(&sdk_winmd, simple_class_name) } - // --------------------------------------------------------------------------- // .js rendering // --------------------------------------------------------------------------- @@ -346,6 +349,11 @@ fn render_js(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { for en in enum_import_names(meta) { out.push_str(&format!("import {{ {} }} from './{}.js';\n", en, en)); } + for iface in returned_interface_import_names(meta) { + if iface != *name { + out.push_str(&format!("import {{ {iface} }} from './{iface}.js';\n")); + } + } // Interop: import the projected class so we can wrap the returned object. if let Some(info) = interop { if !info.target_iid.is_empty() { @@ -357,6 +365,20 @@ fn render_js(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { } out.push('\n'); + if has_string_buffer_method(meta) { + out.push_str("function _normalizeStringBufferCount(value, name) {\n"); + out.push_str(" if (!Number.isInteger(value) || value <= 0) throw new TypeError(`${name} must be a positive integer`);\n"); + out.push_str(" return value;\n"); + out.push_str("}\n"); + out.push_str("function _decodeWideString(buffer) {\n"); + out.push_str(" let end = 0;\n"); + out.push_str( + " while (end + 1 < buffer.length && buffer.readUInt16LE(end) !== 0) end += 2;\n", + ); + out.push_str(" return buffer.subarray(0, end).toString('utf16le');\n"); + out.push_str("}\n\n"); + } + out.push_str(&format!( "export const IID_{name} = WinGuid.parse('{iid}');\n", name = name, @@ -466,11 +488,20 @@ fn render_js(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { fn build_method_sig_js(m: &MethodMeta) -> String { let mut parts = Vec::new(); - for p in &m.params { + let string_buffer = string_buffer_pattern(m); + for (idx, p) in m.params.iter().enumerate() { if p.direction == ParamDirection::In { parts.push(format!(".addIn({})", ts_type_expr_js(&p.typ))); + } else if matches!(p.direction, ParamDirection::OutStringBuffer { .. }) { + parts.push(".addIn(DynWinRtType.pointer())".to_string()); } else if p.direction == ParamDirection::Out { - parts.push(format!(".addOut({})", ts_type_expr_js(&p.typ))); + if string_buffer.is_some_and(|(_, count_idx, _)| { + idx > count_idx && is_optional_find_data_out_after_string_count(p) + }) { + parts.push(".addIn(DynWinRtType.pointer())".to_string()); + } else { + parts.push(format!(".addOut({})", ts_type_expr_js(&p.typ))); + } } else if p.direction == ParamDirection::OutFill { parts.push(format!(".addOutFill({})", ts_type_expr_js(&p.typ))); } @@ -493,10 +524,11 @@ fn build_method_sig_js(m: &MethodMeta) -> String { fn emit_method_js(out: &mut String, m: &MethodMeta, iface_var: &str) { let camel = camel_case(&m.name); - let in_params: Vec<&ParamMeta> = m + let in_params: Vec<(usize, &ParamMeta)> = m .params .iter() - .filter(|p| p.direction == ParamDirection::In) + .enumerate() + .filter(|(_, p)| p.direction == ParamDirection::In) .collect(); let out_params: Vec<&ParamMeta> = m .params @@ -511,13 +543,24 @@ fn emit_method_js(out: &mut String, m: &MethodMeta, iface_var: &str) { let param_list: Vec = in_params .iter() .enumerate() - .map(|(i, p)| js_param_name(&p.name, i)) + .map(|(surface_i, (idx, p))| { + let name = js_param_name(&p.name, surface_i); + if let Some((_, count_idx, _)) = string_buffer_pattern(m) { + if *idx == count_idx { + return format!("{name} = 260"); + } + if *idx > count_idx { + return format!("{name} = 0"); + } + } + name + }) .collect(); let args_exprs: Vec = in_params .iter() .enumerate() - .map(|(i, p)| wrap_arg_js(&p.typ, &js_param_name(&p.name, i))) + .map(|(i, (_, p))| wrap_arg_js(&p.typ, &js_param_name(&p.name, i))) .collect(); out.push_str(&format!( @@ -525,6 +568,55 @@ fn emit_method_js(out: &mut String, m: &MethodMeta, iface_var: &str) { camel = camel, params = param_list.join(", ") )); + if let Some((buffer_idx, count_idx, encoding)) = string_buffer_pattern(m) { + let count_surface_idx = in_params + .iter() + .position(|(idx, _)| *idx == count_idx) + .expect("count param must be an input"); + let count_name = js_param_name(&m.params[count_idx].name, count_surface_idx); + if encoding == StringEncoding::Ansi { + out.push_str( + " throw new Error('PSTR out buffers are not yet decoded safely');\n", + ); + out.push_str(" }\n"); + return; + } + let args: Vec = m + .params + .iter() + .enumerate() + .filter_map(|(idx, p)| { + if idx == buffer_idx { + Some("DynWinRtValue.pointer(_buffer)".to_string()) + } else if p.direction == ParamDirection::In { + let surface_idx = in_params + .iter() + .position(|(param_idx, _)| *param_idx == idx) + .expect("input param must have a surface index"); + Some(wrap_arg_js(&p.typ, &js_param_name(&p.name, surface_idx))) + } else if idx > count_idx && is_optional_find_data_out_after_string_count(p) { + Some("DynWinRtValue.pointer(0n)".to_string()) + } else { + None + } + }) + .collect(); + out.push_str(&format!( + " {count_name} = _normalizeStringBufferCount({count_name}, '{count_name}');\n" + )); + out.push_str(&format!( + " const _buffer = Buffer.alloc({count_name} * 2);\n" + )); + out.push_str(&format!( + " {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", + iface_var = iface_var, + slot = m.vtable_index, + args = args.join(", ") + )); + out.push_str(" return _decodeWideString(_buffer);\n"); + out.push_str(" }\n"); + return; + } // Project trailing `[out]` params as JS return values, mirroring how the // WinRT codegen already handles out-params (see // `codegen/javascript/project/methods.rs` — `is_multi_output` / `invokeAll`). @@ -549,6 +641,9 @@ fn emit_method_js(out: &mut String, m: &MethodMeta, iface_var: &str) { slot = m.vtable_index, args = args_exprs.join(", ") )); + if matches!(out_params[0].typ, TypeMeta::Object) { + out.push_str(" // TODO: raw COM interface pointer adoption requires preserved pointee metadata.\n"); + } out.push_str(&format!( " return {};\n", unwrap_return_js(&out_params[0].typ, "_out") @@ -606,6 +701,9 @@ fn unwrap_return_js(t: &TypeMeta, expr: &str) -> String { TypeMeta::Guid => format!("{expr}.toGuid().toString()"), TypeMeta::Enum { underlying, .. } => unwrap_return_js(underlying, expr), TypeMeta::String => format!("{expr}.toString()"), + TypeMeta::Interface { name, iid, .. } if !iid.is_empty() => { + format!("{name}._fromNative({expr})") + } // Object / Interface / RuntimeClass / Struct pointer / etc. // Return the raw DynWinRtValue and let the caller decide (e.g. cast). _ => expr.to_string(), @@ -616,6 +714,9 @@ fn unwrap_return_js(t: &TypeMeta, expr: &str) -> String { /// swallowed. Projects `[out]` params as the natural return type: 0 outs → /// `void`, 1 out → that type, N outs → a tuple. fn dts_return_type_for_outs(m: &MethodMeta) -> String { + if string_buffer_pattern(m).is_some() { + return "string".to_string(); + } let out_params: Vec<&ParamMeta> = m .params .iter() @@ -634,9 +735,33 @@ fn dts_return_type_for_outs(m: &MethodMeta) -> String { } } +fn dts_params_for_method(m: &MethodMeta) -> Vec { + let string_buffer = string_buffer_pattern(m); + m.params + .iter() + .enumerate() + .filter(|(_, p)| p.direction == ParamDirection::In) + .enumerate() + .map(|(surface_i, (idx, p))| { + let mut name = js_param_name(&p.name, surface_i); + if let Some((_, count_idx, _)) = string_buffer { + if idx >= count_idx { + name.push('?'); + } + } + format!("{}: {}", name, ts_type_expr_dts(&p.typ)) + }) + .collect() +} + /// Emit an interop method: either natural (hide trailing REFIID + void**) or /// plain (fall back to the normal classic-COM emission). -fn emit_interop_method_js(out: &mut String, im: &InteropMethod, iface_var: &str, info: &InteropInfo) { +fn emit_interop_method_js( + out: &mut String, + im: &InteropMethod, + iface_var: &str, + info: &InteropInfo, +) { let Some(natural_params) = &im.natural_params else { // Plain method — reuse the existing pass-through emission. if let Some(m) = &im.plain { @@ -694,7 +819,6 @@ fn emit_interop_method_js(out: &mut String, im: &InteropMethod, iface_var: &str, out.push_str(" }\n"); } - // --------------------------------------------------------------------------- // .d.ts rendering // --------------------------------------------------------------------------- @@ -710,6 +834,11 @@ fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String for en in enum_import_names(meta) { out.push_str(&format!("import {{ {} }} from './{}.js';\n", en, en)); } + for iface in returned_interface_import_names(meta) { + if iface != *name { + out.push_str(&format!("import {{ {iface} }} from './{iface}.js';\n")); + } + } // Interop: import the projected class declaration so return types resolve. if let Some(info) = interop { if !info.target_iid.is_empty() { @@ -733,7 +862,10 @@ fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String out.push('\n'); } - out.push_str(&format!("export declare const IID_{name}: unknown;\n\n", name = name)); + out.push_str(&format!( + "export declare const IID_{name}: unknown;\n\n", + name = name + )); out.push_str(&format!("export declare class {name} {{\n", name = name)); if meta.coclass_clsid.is_some() { @@ -763,7 +895,11 @@ fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String .iter() .enumerate() .map(|(i, p)| { - format!("{}: {}", js_param_name(&p.name, i), ts_type_expr_dts(&p.typ)) + format!( + "{}: {}", + js_param_name(&p.name, i), + ts_type_expr_dts(&p.typ) + ) }) .collect(); let ret = if !info.target_iid.is_empty() { @@ -780,18 +916,7 @@ fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String } (None, Some(m)) => { let camel = camel_case(&m.name); - let in_params: Vec<&ParamMeta> = m - .params - .iter() - .filter(|p| p.direction == ParamDirection::In) - .collect(); - let ts_params: Vec = in_params - .iter() - .enumerate() - .map(|(i, p)| { - format!("{}: {}", js_param_name(&p.name, i), ts_type_expr_dts(&p.typ)) - }) - .collect(); + let ts_params = dts_params_for_method(m); let ret = match &m.return_type { None => "void".to_string(), // HRESULT is swallowed by the runtime (throw on failure). @@ -812,18 +937,7 @@ fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String } else { for m in &iface.methods { let camel = camel_case(&m.name); - let in_params: Vec<&ParamMeta> = m - .params - .iter() - .filter(|p| p.direction == ParamDirection::In) - .collect(); - let ts_params: Vec = in_params - .iter() - .enumerate() - .map(|(i, p)| { - format!("{}: {}", js_param_name(&p.name, i), ts_type_expr_dts(&p.typ)) - }) - .collect(); + let ts_params = dts_params_for_method(m); let ret = match &m.return_type { None => "void".to_string(), // HRESULT is swallowed by the runtime (throw on failure). @@ -903,7 +1017,9 @@ fn render_projected_class_files( js.push_str(" .addMethod('GetRuntimeClassName', new DynWinRtMethodSig().addOut(DynWinRtType.hstring()))\n"); js.push_str(" .addMethod('GetTrustLevel', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type()));\n"); js.push_str(" const value = _IInspectableCache[prop];\n"); - js.push_str(" return typeof value === 'function' ? value.bind(_IInspectableCache) : value;\n"); + js.push_str( + " return typeof value === 'function' ? value.bind(_IInspectableCache) : value;\n", + ); js.push_str(" },\n});\n\n"); js.push_str(&format!("export class {cls} {{\n", cls = info.class_name)); @@ -965,7 +1081,10 @@ fn render_projected_class_files( if !handle_aliases.is_empty() { dts.push('\n'); } - dts.push_str(&format!("export declare class {cls} {{\n", cls = info.class_name)); + dts.push_str(&format!( + "export declare class {cls} {{\n", + cls = info.class_name + )); dts.push_str(&format!( " /** Wrap an existing native COM pointer (for QueryInterface bridging). */\n static _fromNative(obj: unknown): {cls};\n", cls = info.class_name, @@ -973,7 +1092,13 @@ fn render_projected_class_files( let ts_params: Vec = primary_natural .iter() .enumerate() - .map(|(i, p)| format!("{}: {}", js_param_name(&p.name, i), ts_type_expr_dts(&p.typ))) + .map(|(i, p)| { + format!( + "{}: {}", + js_param_name(&p.name, i), + ts_type_expr_dts(&p.typ) + ) + }) .collect(); dts.push_str(&format!( " /** Get a `{cls}` for the given HWND (projected from `{full_class_name}`). */\n", @@ -993,7 +1118,6 @@ fn render_projected_class_files( Some((js, dts)) } - fn collect_handle_aliases(meta: &ComInterfaceMeta) -> Vec { let mut set = BTreeSet::new(); for m in &meta.interface.methods { @@ -1019,7 +1143,10 @@ fn render_enum_files(en: &TypeMeta) -> (String, String) { // .js: a frozen object. let mut js = String::new(); js.push_str("// Generated by dynwinrt-codegen — do not edit\n"); - js.push_str(&format!("export const {name} = Object.freeze({{\n", name = name)); + js.push_str(&format!( + "export const {name} = Object.freeze({{\n", + name = name + )); for m in members { js.push_str(&format!(" {}: {},\n", m.name, m.value)); } @@ -1054,10 +1181,85 @@ fn enum_import_names(meta: &ComInterfaceMeta) -> Vec { .collect() } +fn returned_interface_import_names(meta: &ComInterfaceMeta) -> Vec { + let mut set = BTreeSet::new(); + for m in &meta.interface.methods { + for p in &m.params { + if p.direction == ParamDirection::Out { + if let TypeMeta::Interface { name, iid, .. } = &p.typ { + if !iid.is_empty() { + set.insert(name.clone()); + } + } + } + } + } + set.into_iter().collect() +} + // --------------------------------------------------------------------------- // Type mapping helpers // --------------------------------------------------------------------------- +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum StringEncoding { + Wide, + Ansi, +} + +fn has_string_buffer_method(meta: &ComInterfaceMeta) -> bool { + meta.interface + .methods + .iter() + .any(|m| string_buffer_pattern(m).is_some()) +} + +fn string_buffer_pattern(m: &MethodMeta) -> Option<(usize, usize, StringEncoding)> { + for (idx, p) in m.params.iter().enumerate() { + let ParamDirection::OutStringBuffer { count_param_index } = p.direction else { + continue; + }; + let encoding = string_buffer_encoding(&p.typ)?; + if m.params + .get(count_param_index) + .is_some_and(|count| count.direction == ParamDirection::In) + { + return Some((idx, count_param_index, encoding)); + } + } + None +} + +fn string_buffer_encoding(t: &TypeMeta) -> Option { + match t { + TypeMeta::Struct { + namespace, name, .. + } if namespace == "Windows.Win32.Foundation" && name == "PWSTR" => { + Some(StringEncoding::Wide) + } + TypeMeta::Struct { + namespace, name, .. + } if namespace == "Windows.Win32.Foundation" && name == "PSTR" => { + Some(StringEncoding::Ansi) + } + _ => None, + } +} + +fn is_optional_find_data_out_after_string_count(p: &ParamMeta) -> bool { + if p.direction != ParamDirection::Out { + return false; + } + let n = p.name.to_ascii_lowercase(); + if n == "pfd" || n.contains("finddata") || n.contains("find_data") { + return true; + } + matches!( + &p.typ, + TypeMeta::Struct { name, .. } if name == "WIN32_FIND_DATAW" || name == "WIN32_FIND_DATAA" + ) +} + /// TS type expression for the `.d.ts` surface. fn ts_type_expr_dts(t: &TypeMeta) -> String { // Win32 BOOL is a struct with a single `Value: I32` field — the same shape @@ -1066,16 +1268,27 @@ fn ts_type_expr_dts(t: &TypeMeta) -> String { if is_win32_bool(t) { return "boolean".into(); } + if is_hresult(t) { + return "number".into(); + } if let Some(h) = handle_type_name(t) { return h; } match t { TypeMeta::Bool => "boolean".into(), - TypeMeta::I8 | TypeMeta::U8 | TypeMeta::I16 | TypeMeta::U16 | TypeMeta::I32 | TypeMeta::U32 - | TypeMeta::F32 | TypeMeta::F64 | TypeMeta::Char16 => "number".into(), + TypeMeta::I8 + | TypeMeta::U8 + | TypeMeta::I16 + | TypeMeta::U16 + | TypeMeta::I32 + | TypeMeta::U32 + | TypeMeta::F32 + | TypeMeta::F64 + | TypeMeta::Char16 => "number".into(), TypeMeta::I64 | TypeMeta::U64 => "bigint".into(), TypeMeta::String => "string".into(), TypeMeta::Guid => "string".into(), + TypeMeta::Interface { name, iid, .. } if !iid.is_empty() => name.clone(), TypeMeta::Enum { name, .. } => name.clone(), TypeMeta::Struct { name, .. } => name.clone(), // Pointer-to-struct or unknown — opaque bigint|Buffer at the surface. @@ -1090,6 +1303,9 @@ fn ts_type_expr_js(t: &TypeMeta) -> String { if is_win32_bool(t) { return "DynWinRtType.i32Type()".into(); } + if is_hresult(t) { + return "DynWinRtType.i32Type()".into(); + } if handle_type_name(t).is_some() { return "DynWinRtType.pointer()".into(); } @@ -1120,6 +1336,9 @@ fn wrap_arg_js(t: &TypeMeta, var: &str) -> String { if is_win32_bool(t) { return format!("DynWinRtValue.i32({var} ? 1 : 0)", var = var); } + if is_hresult(t) { + return format!("DynWinRtValue.i32({var})", var = var); + } if handle_type_name(t).is_some() { return format!("DynWinRtValue.pointer({var})", var = var); } @@ -1155,7 +1374,11 @@ fn handle_type_name(t: &TypeMeta) -> Option { return None; } match t { - TypeMeta::Struct { namespace, name, fields } => { + TypeMeta::Struct { + namespace, + name, + fields, + } => { if !is_win32_handle_namespace(namespace) { return None; } @@ -1274,12 +1497,12 @@ fn js_param_name(raw: &str, index: usize) -> String { } // Guard against JS reserved words. match out.as_str() { - "class" | "return" | "function" | "default" | "this" | "new" | "delete" - | "let" | "const" | "var" | "if" | "else" | "for" | "while" | "do" | "switch" - | "case" | "break" | "continue" | "true" | "false" | "null" | "undefined" - | "in" | "of" | "typeof" | "instanceof" | "throw" | "try" | "catch" | "finally" - | "yield" | "async" | "await" | "with" | "void" | "public" | "private" | "protected" - | "package" | "static" | "import" | "export" | "extends" | "super" | "arguments" => { + "class" | "return" | "function" | "default" | "this" | "new" | "delete" | "let" + | "const" | "var" | "if" | "else" | "for" | "while" | "do" | "switch" | "case" + | "break" | "continue" | "true" | "false" | "null" | "undefined" | "in" | "of" + | "typeof" | "instanceof" | "throw" | "try" | "catch" | "finally" | "yield" | "async" + | "await" | "with" | "void" | "public" | "private" | "protected" | "package" | "static" + | "import" | "export" | "extends" | "super" | "arguments" => { format!("{}_", out) } _ => out, @@ -1292,9 +1515,8 @@ fn strip_hungarian(s: &str) -> &str { // like `h`, `p`, `i` cause too many false positives on real method-param // names (e.g. `hwnd` starts with `h` but isn't Hungarian; `pButton` is). let prefixes = [ - "lpwsz", "pwsz", "lpsz", "psz", "lpsz", "pwstr", "pcwstr", - "hwnd", "dw", "sz", "cb", "cx", "cy", "cw", "ch", "cn", "cc", - "lp", "np", "ph", "pd", "pf", "pv", "ppv", "pp", "wsz", + "lpwsz", "pwsz", "lpsz", "psz", "lpsz", "pwstr", "pcwstr", "hwnd", "dw", "sz", "cb", "cx", + "cy", "cw", "ch", "cn", "cc", "lp", "np", "ph", "pd", "pf", "pv", "ppv", "pp", "wsz", ]; for p in prefixes { if let Some(rest) = s.strip_prefix(p) { @@ -1369,10 +1591,22 @@ mod tests { namespace: "Windows.Foundation".into(), name: "Rect".into(), fields: vec![ - crate::types::FieldMeta { name: "X".into(), typ: TypeMeta::F32 }, - crate::types::FieldMeta { name: "Y".into(), typ: TypeMeta::F32 }, - crate::types::FieldMeta { name: "Width".into(), typ: TypeMeta::F32 }, - crate::types::FieldMeta { name: "Height".into(), typ: TypeMeta::F32 }, + crate::types::FieldMeta { + name: "X".into(), + typ: TypeMeta::F32, + }, + crate::types::FieldMeta { + name: "Y".into(), + typ: TypeMeta::F32, + }, + crate::types::FieldMeta { + name: "Width".into(), + typ: TypeMeta::F32, + }, + crate::types::FieldMeta { + name: "Height".into(), + typ: TypeMeta::F32, + }, ], }; assert!(handle_type_name(&rect).is_none()); @@ -1396,8 +1630,10 @@ mod tests { let b = win32_bool_struct(); // Sanity: it's the exact shape of a handle (single Value: I32) — the // special-case must WIN over the generic handle heuristic. - assert!(handle_type_name(&b).is_none(), - "BOOL must not be emitted as an opaque handle typedef"); + assert!( + handle_type_name(&b).is_none(), + "BOOL must not be emitted as an opaque handle typedef" + ); } #[test] @@ -1414,6 +1650,56 @@ mod tests { ); } + #[test] + fn hresult_input_projects_as_number_and_i32_value() { + let hr = make_hresult(); + assert_eq!(ts_type_expr_dts(&hr), "number"); + assert_eq!(ts_type_expr_js(&hr), "DynWinRtType.i32Type()"); + assert_eq!(wrap_arg_js(&hr, "hr"), "DynWinRtValue.i32(hr)"); + + let m = MethodMeta { + name: "Close".into(), + vtable_index: 4, + params: vec![ParamMeta { + name: "hr".into(), + typ: hr, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains( + ".addMethod('Close', new DynWinRtMethodSig().addIn(DynWinRtType.i32Type()))" + ), + ".js must register HRESULT in-param as i32:\n{}", + js + ); + assert!( + js.contains("DynWinRtValue.i32(hr)"), + ".js must pass HRESULT by value as i32:\n{}", + js + ); + assert!( + !js.contains("DynWinRtValue.pointer(hr)"), + ".js must not pass HRESULT as a pointer:\n{}", + js + ); + assert!( + dts.contains("close(hr: number): void;"), + ".d.ts must type HRESULT in-param as number:\n{}", + dts + ); + assert!( + !dts.contains("HRESULT"), + ".d.ts must not expose an undefined HRESULT alias:\n{}", + dts + ); + } + // ---- Fix 3 (REFIID-guarded interop heuristic) ---- /// Helper: construct a MethodMeta with HRESULT return type. @@ -1455,9 +1741,8 @@ mod tests { return_type: Some(make_hresult()), ..Default::default() }; - let natural = method_is_interop_shape(&m).expect( - "REFIID-shaped trailing in-param named `riid` must be recognised as interop", - ); + let natural = method_is_interop_shape(&m) + .expect("REFIID-shaped trailing in-param named `riid` must be recognised as interop"); // Natural in-params = every in EXCEPT the trailing REFIID. assert_eq!(natural.len(), 1); assert_eq!(natural[0].name, "appWindow"); @@ -1926,7 +2211,7 @@ mod tests { js ); assert!( - !js.contains("return _out") && !js.contains("return _r") && !js.contains("return [") , + !js.contains("return _out") && !js.contains("return _r") && !js.contains("return ["), ".js OutFill must not return anything (avoid half-broken projection):\n{}", js ); @@ -1936,4 +2221,109 @@ mod tests { dts ); } + + fn pwstr_struct() -> TypeMeta { + TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "PWSTR".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + } + } + + #[test] + fn out_string_buffer_allocates_decodes_and_returns_string() { + let m = MethodMeta { + name: "GetDescription".into(), + vtable_index: 6, + params: vec![ + ParamMeta { + name: "pszName".into(), + typ: pwstr_struct(), + direction: ParamDirection::OutStringBuffer { + count_param_index: 1, + }, + }, + ParamMeta { + name: "cch".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains("function _normalizeStringBufferCount"), + ".js must emit string buffer validation helper:\n{}", + js + ); + assert!( + js.contains(".addMethod('GetDescription', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type()))"), + ".js must register string buffer as an input pointer:\n{}", + js + ); + assert!( + js.contains("getDescription(cch = 260)") && js.contains("Buffer.alloc(cch * 2)"), + ".js must default cch and allocate a UTF-16 buffer:\n{}", + js + ); + assert!( + js.contains("return _decodeWideString(_buffer);"), + ".js must return the decoded wide string:\n{}", + js + ); + assert!( + dts.contains("getDescription(cch?: number): string;"), + ".d.ts must expose optional count and string return:\n{}", + dts + ); + } + + #[test] + fn interface_out_param_projects_as_typed_wrapper() { + let m = MethodMeta { + name: "GetThing".into(), + vtable_index: 7, + params: vec![ParamMeta { + name: "thing".into(), + typ: TypeMeta::Interface { + namespace: "Windows.Win32.System.Com".into(), + name: "IThing".into(), + iid: "11111111-2222-3333-4444-555555555555".into(), + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains("import { IThing } from './IThing.js';"), + ".js must import the returned interface wrapper:\n{}", + js + ); + assert!( + js.contains("return IThing._fromNative(_out);"), + ".js must wrap the returned COM object in the typed interface wrapper:\n{}", + js + ); + assert!( + dts.contains("import { IThing } from './IThing.js';"), + ".d.ts must import the returned interface type:\n{}", + dts + ); + assert!( + dts.contains("getThing(): IThing;"), + ".d.ts must return the typed interface wrapper:\n{}", + dts + ); + } } diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/project/methods.rs b/tools/dynwinrt-codegen/src/codegen/javascript/project/methods.rs index c1a741b5..ed494d16 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/project/methods.rs +++ b/tools/dynwinrt-codegen/src/codegen/javascript/project/methods.rs @@ -10,7 +10,9 @@ fn projected_method_outputs(method: &MethodMeta) -> Vec<(usize, &TypeMeta)> { let mut outputs = Vec::new(); for param in &method.params { match param.direction { - ParamDirection::Out | ParamDirection::OutFill => { + ParamDirection::Out + | ParamDirection::OutFill + | ParamDirection::OutStringBuffer { .. } => { outputs.push((result_index, ¶m.typ)); result_index += 1; } diff --git a/tools/dynwinrt-codegen/src/codegen/python/type_helpers.rs b/tools/dynwinrt-codegen/src/codegen/python/type_helpers.rs index ffedcd14..59ced174 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/type_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/python/type_helpers.rs @@ -207,7 +207,8 @@ pub(super) fn py_method_outputs(method: &MethodMeta) -> Vec<(usize, &TypeMeta)> for param in &method.params { match param.direction { - crate::meta::ParamDirection::Out => { + crate::meta::ParamDirection::Out + | crate::meta::ParamDirection::OutStringBuffer { .. } => { outputs.push((result_index, ¶m.typ)); result_index += 1; } diff --git a/tools/dynwinrt-codegen/src/codegen/shared/imports.rs b/tools/dynwinrt-codegen/src/codegen/shared/imports.rs index cf8e4047..9a821a7c 100644 --- a/tools/dynwinrt-codegen/src/codegen/shared/imports.rs +++ b/tools/dynwinrt-codegen/src/codegen/shared/imports.rs @@ -218,7 +218,9 @@ pub(crate) fn method_abi_output_count(method: &MethodMeta) -> usize { .filter(|param| { matches!( param.direction, - ParamDirection::Out | ParamDirection::OutFill + ParamDirection::Out + | ParamDirection::OutFill + | ParamDirection::OutStringBuffer { .. } ) }) .count() @@ -229,7 +231,7 @@ pub(crate) fn fill_array_output_index(method: &MethodMeta) -> Option { let mut result_index = 0; for param in &method.params { match param.direction { - ParamDirection::Out => result_index += 1, + ParamDirection::Out | ParamDirection::OutStringBuffer { .. } => result_index += 1, ParamDirection::OutFill => return Some(result_index), ParamDirection::In => {} } diff --git a/tools/dynwinrt-codegen/src/main.rs b/tools/dynwinrt-codegen/src/main.rs index b8c21038..a062dd50 100644 --- a/tools/dynwinrt-codegen/src/main.rs +++ b/tools/dynwinrt-codegen/src/main.rs @@ -322,8 +322,10 @@ fn run() -> Result<(), String> { if lang != "js" && !com_interfaces.is_empty() { let mut offenders: Vec = Vec::new(); for ci in &com_interfaces { - offenders.push(format!("{}.{} (classic-COM interface)", - ci.interface.namespace, ci.interface.name)); + offenders.push(format!( + "{}.{} (classic-COM interface)", + ci.interface.namespace, ci.interface.name + )); } return Err(format!( "`--lang {}` is not supported for classic-COM interfaces \ @@ -340,26 +342,30 @@ fn run() -> Result<(), String> { // Emit classic-COM interfaces (standalone; not wired into WinRT index/barrel). if !com_interfaces.is_empty() { for com_iface in &com_interfaces { - let out = com::generate_com_interface_files(com_iface, &winmd) - .map_err(|e| format!("Classic-COM codegen for {} failed: {}", com_iface.interface.name, e))?; + let out = + com::generate_com_interface_files(com_iface, &winmd).map_err(|e| { + format!( + "Classic-COM codegen for {} failed: {}", + com_iface.interface.name, e + ) + })?; let js_name = format!("{}.js", com_iface.interface.name); let dts_name = format!("{}.d.ts", com_iface.interface.name); if !dry_run { - fs::write(output_dir.join(&js_name), &out.js).map_err(|e| { - format!("Failed to write {}: {}", js_name, e) - })?; - fs::write(output_dir.join(&dts_name), &out.dts).map_err(|e| { - format!("Failed to write {}: {}", dts_name, e) - })?; + fs::write(output_dir.join(&js_name), &out.js) + .map_err(|e| format!("Failed to write {}: {}", js_name, e))?; + fs::write(output_dir.join(&dts_name), &out.dts) + .map_err(|e| format!("Failed to write {}: {}", dts_name, e))?; for (name, content) in &out.extra_files { - fs::write(output_dir.join(name), content).map_err(|e| { - format!("Failed to write {}: {}", name, e) - })?; + fs::write(output_dir.join(name), content) + .map_err(|e| format!("Failed to write {}: {}", name, e))?; } - println!("Generated {} ({} .js/.d.ts + {} extras)", + println!( + "Generated {} ({} .js/.d.ts + {} extras)", com_iface.interface.name, 2, - out.extra_files.len()); + out.extra_files.len() + ); } else { println!("[dry-run] Would generate {}", com_iface.interface.name); } diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index a47901ca..0b11f4ba 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -15,6 +15,11 @@ pub enum ParamDirection { Out, /// FillArray: caller allocates buffer, callee fills it. OutFill, + /// Caller-owned classic-COM string buffer, immediately sized by another + /// input parameter. + OutStringBuffer { + count_param_index: usize, + }, } /// A single method parameter. @@ -187,11 +192,17 @@ pub fn find_runtime_class_default_iid( // A WinRT runtime class extends System.Object AND carries the // WindowsRuntime flag on its type. Interfaces extend nothing; // classes extend Object/etc. We filter to actual runtime classes. - if !def.flags().contains(windows_metadata::TypeAttributes::WindowsRuntime) { + if !def + .flags() + .contains(windows_metadata::TypeAttributes::WindowsRuntime) + { continue; } // Must be a class (not interface/enum/struct). - if def.flags().contains(windows_metadata::TypeAttributes::Interface) { + if def + .flags() + .contains(windows_metadata::TypeAttributes::Interface) + { continue; } let namespace = def.namespace().to_string(); @@ -201,7 +212,9 @@ pub fn find_runtime_class_default_iid( continue; } let iface_ty = iface_impl.interface(&[]); - let windows_metadata::Type::Name(tn) = &iface_ty else { continue }; + let windows_metadata::Type::Name(tn) = &iface_ty else { + continue; + }; // Resolve concrete (non-generic) interface's IID from its TypeDef. if !tn.generics.is_empty() { // Skip generic default interfaces — interop projections don't @@ -1166,12 +1179,20 @@ fn parse_com_interface_from_index( if base.1 == "IUnknown" { is_iunknown_rooted = true; terminated_at_known_root = true; - base_chain.push(("Windows.Win32.System.Com".to_string(), "IUnknown".to_string(), 0)); + base_chain.push(( + "Windows.Win32.System.Com".to_string(), + "IUnknown".to_string(), + 0, + )); break; } if base.1 == "IInspectable" { terminated_at_known_root = true; - base_chain.push(("Windows.Foundation".to_string(), "IInspectable".to_string(), 0)); + base_chain.push(( + "Windows.Foundation".to_string(), + "IInspectable".to_string(), + 0, + )); break; } // Otherwise this base is a real classic-COM interface — count its methods. @@ -1244,8 +1265,11 @@ fn parse_com_interface_from_index( } } // Assert the invariant that we lined up correctly. - debug_assert_eq!(slot_cursor, own_methods_start, - "vtable cursor {} != computed own_methods_start {}", slot_cursor, own_methods_start); + debug_assert_eq!( + slot_cursor, own_methods_start, + "vtable cursor {} != computed own_methods_start {}", + slot_cursor, own_methods_start + ); // Now the leaf's own methods let iid = extract_iid(&def); @@ -1341,7 +1365,6 @@ fn parse_interface_with_offset( parse_interface_methods(index, &def, name, namespace, &iid, &[], base_offset) } - fn parse_interface_type( index: &reader::Index, interface_type: &windows_metadata::Type, @@ -1447,6 +1470,7 @@ fn parse_interface_methods( }); } } + mark_caller_owned_string_buffers(&mut params); let return_type = if sig.return_type == windows_metadata::Type::Void { None @@ -1704,6 +1728,54 @@ fn parse_enum_def(def: &reader::TypeDef) -> TypeMeta { } } +fn mark_caller_owned_string_buffers(params: &mut [ParamMeta]) { + if params.len() < 2 { + return; + } + for idx in 0..params.len() - 1 { + if params[idx].direction == ParamDirection::Out + && is_direct_win32_string_buffer(¶ms[idx].typ) + && params[idx + 1].direction == ParamDirection::In + && is_string_buffer_count_param(¶ms[idx].typ, ¶ms[idx + 1]) + { + params[idx].direction = ParamDirection::OutStringBuffer { + count_param_index: idx + 1, + }; + } + } +} + +fn is_direct_win32_string_buffer(t: &TypeMeta) -> bool { + matches!( + t, + TypeMeta::Struct { namespace, name, .. } + if namespace == "Windows.Win32.Foundation" && (name == "PWSTR" || name == "PSTR") + ) +} + +fn is_pwstr_type(t: &TypeMeta) -> bool { + matches!( + t, + TypeMeta::Struct { + namespace, name, .. + } if namespace == "Windows.Win32.Foundation" && name == "PWSTR" + ) +} + +fn is_string_buffer_count_param(buffer_type: &TypeMeta, p: &ParamMeta) -> bool { + let n = p.name.to_ascii_lowercase(); + matches!(p.typ, TypeMeta::I32 | TypeMeta::U32) + && (n.starts_with("cch") + || (!is_pwstr_type(buffer_type) && n.starts_with("cb")) + || n == "len" + || n == "length" + || n == "size" + || n == "max" + || n == "count" + || n.starts_with("max") + || n.starts_with("size")) +} + fn map_winmd_type(ty: &windows_metadata::Type, index: &reader::Index) -> TypeMeta { map_winmd_type_with_generics(ty, index, &[]) } @@ -1917,6 +1989,110 @@ mod tests { assert_eq!(name, "IIterable_IVector_Int32"); } + fn pwstr_type() -> TypeMeta { + TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "PWSTR".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + } + } + + #[test] + fn marks_direct_pwstr_plus_adjacent_count_as_out_string_buffer() { + let mut params = vec![ + ParamMeta { + name: "pszName".into(), + typ: pwstr_type(), + direction: ParamDirection::Out, + }, + ParamMeta { + name: "cch".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ]; + mark_caller_owned_string_buffers(&mut params); + assert_eq!( + params[0].direction, + ParamDirection::OutStringBuffer { + count_param_index: 1 + } + ); + } + + #[test] + fn does_not_mark_pwstr_plus_byte_count_as_out_string_buffer() { + let mut params = vec![ + ParamMeta { + name: "pszName".into(), + typ: pwstr_type(), + direction: ParamDirection::Out, + }, + ParamMeta { + name: "cbSize".into(), + typ: TypeMeta::U32, + direction: ParamDirection::In, + }, + ]; + mark_caller_owned_string_buffers(&mut params); + assert_eq!(params[0].direction, ParamDirection::Out); + } + + #[test] + fn does_not_mark_callee_allocated_pwstr_pointer_object() { + let mut params = vec![ParamMeta { + name: "ppszName".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }]; + mark_caller_owned_string_buffers(&mut params); + assert_eq!(params[0].direction, ParamDirection::Out); + } + + #[test] + fn does_not_mark_generic_object_plus_size() { + let mut params = vec![ + ParamMeta { + name: "buffer".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ParamMeta { + name: "size".into(), + typ: TypeMeta::U32, + direction: ParamDirection::In, + }, + ]; + mark_caller_owned_string_buffers(&mut params); + assert_eq!(params[0].direction, ParamDirection::Out); + } + + #[test] + fn does_not_mark_non_adjacent_count() { + let mut params = vec![ + ParamMeta { + name: "pszName".into(), + typ: pwstr_type(), + direction: ParamDirection::Out, + }, + ParamMeta { + name: "flags".into(), + typ: TypeMeta::U32, + direction: ParamDirection::In, + }, + ParamMeta { + name: "cch".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ]; + mark_caller_owned_string_buffers(&mut params); + assert_eq!(params[0].direction, ParamDirection::Out); + } + #[test] fn class_all_interfaces_iterates_all() { let mk_iface = |n: &str| InterfaceMeta { diff --git a/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs b/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs index fb499ead..fbf41ad8 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs @@ -77,7 +77,10 @@ fn parse_smtc_interop() { "ISystemMediaTransportControlsInterop", ) .expect("ISystemMediaTransportControlsInterop must exist"); - assert!(!com.is_iunknown_rooted, "SMTC interop derives from IInspectable, not IUnknown"); + assert!( + !com.is_iunknown_rooted, + "SMTC interop derives from IInspectable, not IUnknown" + ); assert_eq!(com.base_offset, 6); let get_for_window = com .interface @@ -102,7 +105,8 @@ fn interop_dts_hides_riid_and_out_ptr_for_datatransfermanager() { "IDataTransferManagerInterop", ) .unwrap(); - let out = com::generate_com_interface_files(&com, WIN32_WINMD).expect("interop codegen must succeed when winmds are present"); + let out = com::generate_com_interface_files(&com, WIN32_WINMD) + .expect("interop codegen must succeed when winmds are present"); let dts = out.dts.as_str(); // The natural signature: hwnd only, NO riid, NO out-ptr. @@ -155,7 +159,8 @@ fn interop_js_synthesizes_target_iid_for_datatransfermanager() { "IDataTransferManagerInterop", ) .unwrap(); - let out = com::generate_com_interface_files(&com, WIN32_WINMD).expect("interop codegen must succeed when winmds are present"); + let out = com::generate_com_interface_files(&com, WIN32_WINMD) + .expect("interop codegen must succeed when winmds are present"); let js = out.js.as_str(); // The IDataTransferManager default interface IID must be embedded in .js @@ -208,7 +213,8 @@ fn smtc_interop_js_uses_inspectable_base_slot_6() { "ISystemMediaTransportControlsInterop", ) .unwrap(); - let out = com::generate_com_interface_files(&com, WIN32_WINMD).expect("interop codegen must succeed when winmds are present"); + let out = com::generate_com_interface_files(&com, WIN32_WINMD) + .expect("interop codegen must succeed when winmds are present"); let js = out.js.as_str(); // IInspectable-rooted → register with the WinRT base (registerInterface), @@ -250,7 +256,8 @@ fn interop_return_type_exposes_runtime_class_name() { "IDataTransferManagerInterop", ) .unwrap(); - let out = com::generate_com_interface_files(&com, WIN32_WINMD).expect("interop codegen must succeed when winmds are present"); + let out = com::generate_com_interface_files(&com, WIN32_WINMD) + .expect("interop codegen must succeed when winmds are present"); // The projected class `DataTransferManager` is emitted as a separate // sibling file (own .js + .d.ts), NOT inside the interop wrapper's .d.ts. @@ -303,7 +310,8 @@ fn interop_generation_is_deterministic() { "IDataTransferManagerInterop", ) .unwrap(); - com::generate_com_interface_files(&com, WIN32_WINMD).expect("interop codegen must succeed when winmds are present") + com::generate_com_interface_files(&com, WIN32_WINMD) + .expect("interop codegen must succeed when winmds are present") }; let a = mk(); let b = mk(); @@ -326,7 +334,8 @@ fn snapshot_datatransfermanager_interop() { "IDataTransferManagerInterop", ) .unwrap(); - let out = com::generate_com_interface_files(&com, WIN32_WINMD).expect("interop codegen must succeed when winmds are present"); + let out = com::generate_com_interface_files(&com, WIN32_WINMD) + .expect("interop codegen must succeed when winmds are present"); let snapshot_dir: PathBuf = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots/idatatransfermanagerinterop"); @@ -387,32 +396,35 @@ fn snapshot_datatransfermanager_interop() { #[test] fn fix1_interop_iid_resolution_is_portable_and_asserted() { if !win32_available() { - eprintln!("Skipping fix1_interop_iid_resolution_is_portable_and_asserted: Win32 winmd not available at {}", WIN32_WINMD); + eprintln!( + "Skipping fix1_interop_iid_resolution_is_portable_and_asserted: Win32 winmd not available at {}", + WIN32_WINMD + ); return; } if !newest_windows_winmd_available() { - eprintln!("Skipping fix1_interop_iid_resolution_is_portable_and_asserted: no Windows SDK Windows.winmd discoverable"); + eprintln!( + "Skipping fix1_interop_iid_resolution_is_portable_and_asserted: no Windows SDK Windows.winmd discoverable" + ); return; } // 1. IDataTransferManager: default interface IID must resolve to the // well-known value regardless of which SDK version is installed. - let (ns_dtm, _iface_dtm, iid_dtm) = - meta::find_runtime_class_default_iid( - &meta::discover_newest_windows_winmd().unwrap(), - "DataTransferManager", - ) - .expect("DataTransferManager must resolve via discovered SDK winmd"); + let (ns_dtm, _iface_dtm, iid_dtm) = meta::find_runtime_class_default_iid( + &meta::discover_newest_windows_winmd().unwrap(), + "DataTransferManager", + ) + .expect("DataTransferManager must resolve via discovered SDK winmd"); assert_eq!(ns_dtm, "Windows.ApplicationModel.DataTransfer"); assert_eq!(iid_dtm, "a5caee9b-8708-49d1-8d36-67d25a8da00c"); // 2. SystemMediaTransportControls: same portability contract. - let (ns_smtc, _iface_smtc, iid_smtc) = - meta::find_runtime_class_default_iid( - &meta::discover_newest_windows_winmd().unwrap(), - "SystemMediaTransportControls", - ) - .expect("SystemMediaTransportControls must resolve via discovered SDK winmd"); + let (ns_smtc, _iface_smtc, iid_smtc) = meta::find_runtime_class_default_iid( + &meta::discover_newest_windows_winmd().unwrap(), + "SystemMediaTransportControls", + ) + .expect("SystemMediaTransportControls must resolve via discovered SDK winmd"); assert_eq!(ns_smtc, "Windows.Media"); assert_eq!(iid_smtc, "99fa3ff4-1742-42a6-902e-087d41f965ec"); diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs index b80cd278..1e92b71f 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -49,7 +49,10 @@ fn parse_itaskbarlist3_iid() { .expect("ITaskbarList3 must exist in Win32 metadata"); assert_eq!(com_iface.interface.name, "ITaskbarList3"); assert_eq!(com_iface.interface.namespace, "Windows.Win32.UI.Shell"); - assert_eq!(com_iface.interface.iid, "ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf"); + assert_eq!( + com_iface.interface.iid, + "ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf" + ); } /// 2. Base-aware vtable slots: full interface_impls chain determines absolute slots. @@ -71,16 +74,35 @@ fn parse_itaskbarlist3_vtable_slots() { .methods .iter() .find(|m| m.name == n) - .unwrap_or_else(|| panic!("method {} not found (methods: {:?})", n, com_iface.interface.methods.iter().map(|m| &m.name).collect::>())) + .unwrap_or_else(|| { + panic!( + "method {} not found (methods: {:?})", + n, + com_iface + .interface + .methods + .iter() + .map(|m| &m.name) + .collect::>() + ) + }) .vtable_index }; - assert_eq!(by_name("HrInit"), 3, "HrInit is the first ITaskbarList method after IUnknown"); + assert_eq!( + by_name("HrInit"), + 3, + "HrInit is the first ITaskbarList method after IUnknown" + ); assert_eq!(by_name("AddTab"), 4); assert_eq!(by_name("DeleteTab"), 5); assert_eq!(by_name("ActivateTab"), 6); assert_eq!(by_name("SetActiveAlt"), 7); - assert_eq!(by_name("MarkFullscreenWindow"), 8, "ITaskbarList2's only method"); + assert_eq!( + by_name("MarkFullscreenWindow"), + 8, + "ITaskbarList2's only method" + ); assert_eq!(by_name("SetProgressValue"), 9); assert_eq!(by_name("SetProgressState"), 10); } @@ -94,8 +116,7 @@ fn itaskbarlist3_is_iunknown_rooted() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") - .unwrap(); + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3").unwrap(); assert_eq!(com_iface.base_offset, 3); assert!(com_iface.is_iunknown_rooted); // Base chain should include ITaskbarList2, ITaskbarList (and stop at IUnknown) @@ -116,8 +137,7 @@ fn itaskbarlist3_clsid_resolution() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") - .unwrap(); + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3").unwrap(); assert_eq!( com_iface.coclass_clsid.as_deref(), Some("56fdf344-fd6d-11d0-958a-006097c9a090") @@ -133,11 +153,11 @@ fn param_type_mapping() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") - .unwrap(); + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3").unwrap(); // Generate wrapper as a text bundle we can inspect for the mapping decisions - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD).expect("codegen must succeed for classic-COM interface"); + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + .expect("codegen must succeed for classic-COM interface"); let dts = out.dts.as_str(); let js = out.js.as_str(); @@ -145,7 +165,8 @@ fn param_type_mapping() { // HWND is a handle type → bigint | Buffer surface assert!( dts.contains("bigint | Buffer") || dts.contains("bigint|Buffer"), - "HWND should be projected as `bigint | Buffer` in .d.ts, got:\n{}", dts + "HWND should be projected as `bigint | Buffer` in .d.ts, got:\n{}", + dts ); // ULONGLONG (U64) → bigint @@ -158,7 +179,8 @@ fn param_type_mapping() { // TBPFLAG enum → surfaced by name (either an enum decl or a union) assert!( dts.contains("TBPFLAG") || dts.contains("TbpFlag"), - ".d.ts must reference the TBPFLAG enum:\n{}", dts + ".d.ts must reference the TBPFLAG enum:\n{}", + dts ); // HRESULT-returning methods project to `void` (throw on failure); no HRESULT surface @@ -166,7 +188,8 @@ fn param_type_mapping() { !dts.contains(": HRESULT") && !dts.contains("-> HRESULT") && !dts.contains("Promise"), - "HRESULT must not leak into the .d.ts surface:\n{}", dts + "HRESULT must not leak into the .d.ts surface:\n{}", + dts ); // JS body: the SetProgressState signature must include u32 (TBPFLAG's underlying) for the enum arg @@ -191,9 +214,9 @@ fn partial_generation_only_emits_target_interface() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") - .unwrap(); - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD).expect("codegen must succeed for classic-COM interface"); + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3").unwrap(); + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + .expect("codegen must succeed for classic-COM interface"); // Expected files: ITaskbarList3.js, ITaskbarList3.d.ts, TBPFLAG.js, TBPFLAG.d.ts let file_names: Vec<&str> = out.extra_files.iter().map(|(n, _)| n.as_str()).collect(); @@ -201,16 +224,24 @@ fn partial_generation_only_emits_target_interface() { // Should NOT include unrelated Shell types like IShellItem or IApplicationActivationManager assert!( !file_names.iter().any(|n| n.starts_with("IShellItem")), - "Partial generation must not include IShellItem: {:?}", file_names + "Partial generation must not include IShellItem: {:?}", + file_names ); assert!( - !file_names.iter().any(|n| n.starts_with("IApplicationActivationManager")), - "Partial generation must not include unrelated types: {:?}", file_names + !file_names + .iter() + .any(|n| n.starts_with("IApplicationActivationManager")), + "Partial generation must not include unrelated types: {:?}", + file_names ); // Should include TBPFLAG (a direct dep) let has_tbpflag = file_names.iter().any(|n| n.starts_with("TBPFLAG")); - assert!(has_tbpflag, "TBPFLAG (direct enum dep) must be included: {:?}", file_names); + assert!( + has_tbpflag, + "TBPFLAG (direct enum dep) must be included: {:?}", + file_names + ); } /// 7. Generated `.d.ts` has PascalCase type + camelCase methods and @@ -222,52 +253,67 @@ fn dts_surface_is_natural_and_clean() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") - .unwrap(); - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD).expect("codegen must succeed for classic-COM interface"); + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3").unwrap(); + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + .expect("codegen must succeed for classic-COM interface"); let dts = out.dts.as_str(); // PascalCase class name assert!( dts.contains("class ITaskbarList3"), - ".d.ts must export class ITaskbarList3, got:\n{}", dts + ".d.ts must export class ITaskbarList3, got:\n{}", + dts ); // camelCase methods for cc in &["hrInit", "setProgressValue", "setProgressState", "addTab"] { assert!( dts.contains(cc), - ".d.ts must declare camelCase method `{}`, got:\n{}", cc, dts + ".d.ts must declare camelCase method `{}`, got:\n{}", + cc, + dts ); } // No PascalCase leaked method names - for pc in &["HrInit(", "SetProgressValue(", "SetProgressState(", "AddTab("] { + for pc in &[ + "HrInit(", + "SetProgressValue(", + "SetProgressState(", + "AddTab(", + ] { assert!( !dts.contains(pc), - ".d.ts must not expose PascalCase method `{}`, got:\n{}", pc, dts + ".d.ts must not expose PascalCase method `{}`, got:\n{}", + pc, + dts ); } // No raw IID leak in .d.ts assert!( !dts.contains("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf"), - "raw IID must not leak into .d.ts:\n{}", dts + "raw IID must not leak into .d.ts:\n{}", + dts ); // No raw CLSID leak assert!( !dts.contains("56fdf344-fd6d-11d0-958a-006097c9a090"), - "raw CLSID must not leak into .d.ts:\n{}", dts + "raw CLSID must not leak into .d.ts:\n{}", + dts ); // No CoCreateInstance leak assert!( !dts.contains("CoCreateInstance") && !dts.contains("coCreateInstance"), - "CoCreateInstance must not leak into .d.ts:\n{}", dts + "CoCreateInstance must not leak into .d.ts:\n{}", + dts ); // No vtable index leak in .d.ts for slot in &["method(3)", "method(9)", "method(10)", "vtable"] { assert!( !dts.contains(slot), - "vtable detail `{}` must not appear in .d.ts:\n{}", slot, dts + "vtable detail `{}` must not appear in .d.ts:\n{}", + slot, + dts ); } } @@ -281,31 +327,35 @@ fn js_body_uses_cocreateinstance_and_correct_slots() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") - .unwrap(); - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD).expect("codegen must succeed for classic-COM interface"); + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3").unwrap(); + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + .expect("codegen must succeed for classic-COM interface"); let js = out.js.as_str(); // CLSID + IID appear in .js assert!( js.contains("56fdf344-fd6d-11d0-958a-006097c9a090"), - ".js must embed the CLSID:\n{}", js + ".js must embed the CLSID:\n{}", + js ); assert!( js.contains("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf"), - ".js must embed the IID:\n{}", js + ".js must embed the IID:\n{}", + js ); // Activation via coCreateInstance assert!( js.contains("coCreateInstance"), - ".js must use coCreateInstance for activation:\n{}", js + ".js must use coCreateInstance for activation:\n{}", + js ); // registerInterfaceUnknown (not registerInterface) since IUnknown-based assert!( js.contains("registerInterfaceUnknown"), - ".js must use registerInterfaceUnknown for classic COM:\n{}", js + ".js must use registerInterfaceUnknown for classic COM:\n{}", + js ); // Base-aware slots @@ -332,7 +382,9 @@ fn winrt_interfaces_still_use_offset_6() { // Parse a well-known WinRT interface (Windows.Foundation.IUriRuntimeClass or similar) // via the existing WinRT path — its first method should still have vtable_index = 6. let Some(windows_winmd) = discovered_windows_winmd() else { - eprintln!("Skipping winrt_interfaces_still_use_offset_6: no Windows SDK Windows.winmd discoverable"); + eprintln!( + "Skipping winrt_interfaces_still_use_offset_6: no Windows SDK Windows.winmd discoverable" + ); return; }; // Take Windows.Foundation.Uri's default interface — pick one that has methods. @@ -350,7 +402,10 @@ fn winrt_interfaces_still_use_offset_6() { .first() .map(|m| m.vtable_index) .expect("Uri default interface must have methods"); - assert_eq!(first_slot, 6, "WinRT interfaces retain the IInspectable base offset of 6"); + assert_eq!( + first_slot, 6, + "WinRT interfaces retain the IInspectable base offset of 6" + ); } /// 10. Interface-not-found is a clean Option::None, not a panic. @@ -375,38 +430,42 @@ fn qi_only_interface_has_no_create() { } // IPersist is IUnknown-rooted (has 1 own method: GetClassID) and has NO // "Persist" coclass anywhere in the metadata — verified via probe. - let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.System.Com", "IPersist") - .expect("IPersist must exist in Win32 metadata"); + let com_iface = meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.System.Com", "IPersist") + .expect("IPersist must exist in Win32 metadata"); assert!( com_iface.coclass_clsid.is_none(), "IPersist has no associated coclass CLSID" ); - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD).expect("codegen must succeed for classic-COM interface"); + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + .expect("codegen must succeed for classic-COM interface"); let js = out.js.as_str(); let dts = out.dts.as_str(); // No `create()` in either surface assert!( !dts.contains("static create()") && !dts.contains("static create(): "), - "QI-only interface must not expose static create() in .d.ts:\n{}", dts + "QI-only interface must not expose static create() in .d.ts:\n{}", + dts ); assert!( !js.contains("coCreateInstance"), - "QI-only interface must not call coCreateInstance in .js:\n{}", js + "QI-only interface must not call coCreateInstance in .js:\n{}", + js ); // Must still have a fromNative / QI-only entry assert!( js.contains("_fromNative") || js.contains("fromRaw"), - "QI-only interface must expose a from-raw entry:\n{}", js + "QI-only interface must expose a from-raw entry:\n{}", + js ); // Slot 3 for GetClassID (only method, IUnknown-rooted) assert!( js.contains("method(3)"), - "IPersist.GetClassID must invoke slot 3:\n{}", js + "IPersist.GetClassID must invoke slot 3:\n{}", + js ); } @@ -420,12 +479,14 @@ fn generation_is_deterministic() { let a = { let com = meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") .unwrap(); - com::generate_com_interface_files(&com, WIN32_WINMD).expect("codegen must succeed for classic-COM interface") + com::generate_com_interface_files(&com, WIN32_WINMD) + .expect("codegen must succeed for classic-COM interface") }; let b = { let com = meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") .unwrap(); - com::generate_com_interface_files(&com, WIN32_WINMD).expect("codegen must succeed for classic-COM interface") + com::generate_com_interface_files(&com, WIN32_WINMD) + .expect("codegen must succeed for classic-COM interface") }; assert_eq!(a.js, b.js); assert_eq!(a.dts, b.dts); @@ -453,13 +514,15 @@ fn snapshot_itaskbarlist3() { let com_iface = meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") .expect("ITaskbarList3 must exist"); - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD).expect("codegen must succeed for classic-COM interface"); + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + .expect("codegen must succeed for classic-COM interface"); let snapshot_dir: PathBuf = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots/itaskbarlist3"); assert!( snapshot_dir.exists(), - "Snapshot directory not found: {}", snapshot_dir.display() + "Snapshot directory not found: {}", + snapshot_dir.display() ); let mut generated: Vec<(String, String)> = Vec::new(); @@ -531,12 +594,9 @@ fn import_name_flag_is_honored_by_com_path() { set_import_name("../dist/index.js"); let result = std::panic::catch_unwind(|| { - let com_iface = meta::parse_com_interface( - WIN32_WINMD, - "Windows.Win32.UI.Shell", - "ITaskbarList3", - ) - .expect("ITaskbarList3 must exist"); + let com_iface = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .expect("ITaskbarList3 must exist"); com::generate_com_interface_files(&com_iface, WIN32_WINMD) .expect("codegen must succeed for classic-COM interface") }); @@ -561,12 +621,9 @@ fn import_name_flag_is_honored_by_com_path() { // Sanity: after restoring the default, subsequent generation reverts. let default_out = { - let com_iface = meta::parse_com_interface( - WIN32_WINMD, - "Windows.Win32.UI.Shell", - "ITaskbarList3", - ) - .expect("ITaskbarList3 must exist"); + let com_iface = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .expect("ITaskbarList3 must exist"); com::generate_com_interface_files(&com_iface, WIN32_WINMD) .expect("codegen must succeed for classic-COM interface") }; @@ -588,7 +645,9 @@ fn import_name_flag_is_honored_by_interop_wrapper() { return; } if discovered_windows_winmd().is_none() { - eprintln!("Skipping: no Windows SDK Windows.winmd discoverable (needed for interop resolution)"); + eprintln!( + "Skipping: no Windows SDK Windows.winmd discoverable (needed for interop resolution)" + ); return; } @@ -639,3 +698,28 @@ fn import_name_flag_is_honored_by_interop_wrapper() { companion ); } + +#[test] +fn shellitem_getdisplayname_is_not_classified_as_caller_owned_string_buffer() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let com_iface = meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "IShellItem") + .expect("IShellItem must exist"); + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + .expect("codegen must succeed for IShellItem"); + + assert!( + out.js.contains(".addMethod('GetDisplayName', new DynWinRtMethodSig().addIn(DynWinRtType.i32Type()).addOut(DynWinRtType.pointer()))"), + "PWSTR* callee-allocated output must remain addOut(pointer), not caller-owned buffer:\n{}", + out.js + ); + assert!( + !out.js.contains("getDisplayName(sigdnName = 260)") + && !out.js.contains("_decodeWideString"), + "IShellItem.GetDisplayName must not allocate/decode a caller-owned buffer:\n{}", + out.js + ); +} From a4a26c2f80062faa416de885a57ae22c309ace02 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Thu, 23 Jul 2026 10:17:19 +0800 Subject: [PATCH 10/28] Fix u16/i16 arg-wrapper codegen: emit DynWinRtValue.u16()/i16() not *Value Copilot review caught a real runtime bug: classic-COM codegen emitted DynWinRtValue.u16Value(...)/i16Value(...) for [in] u16/i16 params, but the napi binding exports u16()/i16() (only bool/i8/u8 use the *Value suffix). Any method with a u16/i16 input (e.g. IShellLinkW.SetHotkey) threw a TypeError at runtime. Fix the two wrong match arms in com.rs wrap_arg_js to the ctor names that exist. Adds a codegen regression test (u16 param must wrap via DynWinRtValue.u16, never u16Value/i16Value) and a live setHotkey no-throw assertion in shelllink-buffer.mjs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bindings/js/e2e/IShellLinkW.js | 2 +- bindings/js/e2e/shelllink-buffer.mjs | 5 ++++ tools/dynwinrt-codegen/src/codegen/com.rs | 4 +-- .../dynwinrt-codegen/tests/win32_com_test.rs | 29 +++++++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/bindings/js/e2e/IShellLinkW.js b/bindings/js/e2e/IShellLinkW.js index 646be59d..dcb08d92 100644 --- a/bindings/js/e2e/IShellLinkW.js +++ b/bindings/js/e2e/IShellLinkW.js @@ -92,7 +92,7 @@ export class IShellLinkW { return _out; } setHotkey(wHotkey) { - _IShellLinkW.method(13).invoke(this._obj, [DynWinRtValue.u16Value(wHotkey)]); + _IShellLinkW.method(13).invoke(this._obj, [DynWinRtValue.u16(wHotkey)]); } getShowCmd() { const _out = _IShellLinkW.method(14).invoke(this._obj, []); diff --git a/bindings/js/e2e/shelllink-buffer.mjs b/bindings/js/e2e/shelllink-buffer.mjs index a38fbe60..7c1d1560 100644 --- a/bindings/js/e2e/shelllink-buffer.mjs +++ b/bindings/js/e2e/shelllink-buffer.mjs @@ -20,4 +20,9 @@ const expectedDescription = 'dynwinrt shelllink buffer'; link.setDescription(wide(expectedDescription)); assert.equal(link.getDescription(), expectedDescription); +// Proves the u16 arg-wrapper codegen fix: setHotkey takes a [in] u16 (WORD). +// Before the fix, codegen emitted the non-existent DynWinRtValue.u16Value(...) +// and this call threw a TypeError. It must now complete without throwing. +assert.doesNotThrow(() => link.setHotkey(0x0341)); // Ctrl+Alt+'A' + console.log('shelllink-buffer ok'); diff --git a/tools/dynwinrt-codegen/src/codegen/com.rs b/tools/dynwinrt-codegen/src/codegen/com.rs index f61806a5..9e6fd40f 100644 --- a/tools/dynwinrt-codegen/src/codegen/com.rs +++ b/tools/dynwinrt-codegen/src/codegen/com.rs @@ -1346,8 +1346,8 @@ fn wrap_arg_js(t: &TypeMeta, var: &str) -> String { TypeMeta::Bool => format!("DynWinRtValue.boolValue({var})", var = var), TypeMeta::I8 => format!("DynWinRtValue.i8Value({var})", var = var), TypeMeta::U8 => format!("DynWinRtValue.u8Value({var})", var = var), - TypeMeta::I16 => format!("DynWinRtValue.i16Value({var})", var = var), - TypeMeta::U16 => format!("DynWinRtValue.u16Value({var})", var = var), + TypeMeta::I16 => format!("DynWinRtValue.i16({var})", var = var), + TypeMeta::U16 => format!("DynWinRtValue.u16({var})", var = var), TypeMeta::I32 => format!("DynWinRtValue.i32({var})", var = var), TypeMeta::U32 => format!("DynWinRtValue.u32({var})", var = var), TypeMeta::I64 => format!("DynWinRtValue.i64(BigInt({var}))", var = var), diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs index 1e92b71f..50341111 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -723,3 +723,32 @@ fn shellitem_getdisplayname_is_not_classified_as_caller_owned_string_buffer() { out.js ); } + +#[test] +fn u16_input_param_uses_existing_u16_value_ctor_not_u16value() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + // IShellLinkW.SetHotkey takes a [in] u16 (WORD). The napi value ctor is + // DynWinRtValue.u16(...) — there is no `u16Value`/`i16Value`. Regression + // guard: the arg-wrapper must emit the ctor that actually exists, or the + // generated call throws at runtime. + let com_iface = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "IShellLinkW") + .expect("IShellLinkW must exist"); + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + .expect("codegen must succeed for IShellLinkW"); + + assert!( + out.js.contains("DynWinRtValue.u16(wHotkey)"), + "u16 input param must wrap via the existing DynWinRtValue.u16(...):\n{}", + out.js + ); + assert!( + !out.js.contains("u16Value(") && !out.js.contains("i16Value("), + "codegen must not emit non-existent DynWinRtValue.u16Value/i16Value:\n{}", + out.js + ); +} From 0f3379eefca5a74eac0d7e2eced61a75a8d445b4 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 23 Jul 2026 10:35:59 +0800 Subject: [PATCH 11/28] codegen(com): qualify interface registration names --- bindings/js/e2e/IDataTransferManagerInterop.js | 2 +- bindings/js/e2e/IShellLinkW.js | 2 +- bindings/js/e2e/ISystemMediaTransportControlsInterop.js | 2 +- bindings/js/e2e/ITaskbarList3.js | 2 +- tools/dynwinrt-codegen/src/codegen/com.rs | 4 +++- .../IDataTransferManagerInterop.js | 2 +- .../tests/snapshots/itaskbarlist3/ITaskbarList3.js | 2 +- tools/dynwinrt-codegen/tests/win32_com_test.rs | 1 + 8 files changed, 10 insertions(+), 7 deletions(-) diff --git a/bindings/js/e2e/IDataTransferManagerInterop.js b/bindings/js/e2e/IDataTransferManagerInterop.js index c5c0f3be..6c59a7c6 100644 --- a/bindings/js/e2e/IDataTransferManagerInterop.js +++ b/bindings/js/e2e/IDataTransferManagerInterop.js @@ -8,7 +8,7 @@ const IID_DataTransferManager_default = WinGuid.parse('a5caee9b-8708-49d1-8d36-6 let _IDataTransferManagerInteropCache; const _IDataTransferManagerInterop = new Proxy({}, { get(_target, prop) { - _IDataTransferManagerInteropCache ??= DynWinRtType.registerInterfaceUnknown('IDataTransferManagerInterop', IID_IDataTransferManagerInterop) + _IDataTransferManagerInteropCache ??= DynWinRtType.registerInterfaceUnknown('Windows.Win32.UI.Shell.IDataTransferManagerInterop', IID_IDataTransferManagerInterop) .addMethod('GetForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())) .addMethod('ShowShareUIForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())); const value = _IDataTransferManagerInteropCache[prop]; diff --git a/bindings/js/e2e/IShellLinkW.js b/bindings/js/e2e/IShellLinkW.js index 646be59d..48db4a8b 100644 --- a/bindings/js/e2e/IShellLinkW.js +++ b/bindings/js/e2e/IShellLinkW.js @@ -17,7 +17,7 @@ export const IID_IShellLinkW = WinGuid.parse('000214f9-0000-0000-c000-0000000000 let _IShellLinkWCache; const _IShellLinkW = new Proxy({}, { get(_target, prop) { - _IShellLinkWCache ??= DynWinRtType.registerInterfaceUnknown('IShellLinkW', IID_IShellLinkW) + _IShellLinkWCache ??= DynWinRtType.registerInterfaceUnknown('Windows.Win32.UI.Shell.IShellLinkW', IID_IShellLinkW) .addMethod('GetPath', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type()).addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type())) .addMethod('GetIDList', new DynWinRtMethodSig().addOut(DynWinRtType.pointer())) .addMethod('SetIDList', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) diff --git a/bindings/js/e2e/ISystemMediaTransportControlsInterop.js b/bindings/js/e2e/ISystemMediaTransportControlsInterop.js index 01710a9f..191d05e0 100644 --- a/bindings/js/e2e/ISystemMediaTransportControlsInterop.js +++ b/bindings/js/e2e/ISystemMediaTransportControlsInterop.js @@ -8,7 +8,7 @@ const IID_SystemMediaTransportControls_default = WinGuid.parse('99fa3ff4-1742-42 let _ISystemMediaTransportControlsInteropCache; const _ISystemMediaTransportControlsInterop = new Proxy({}, { get(_target, prop) { - _ISystemMediaTransportControlsInteropCache ??= DynWinRtType.registerInterface('ISystemMediaTransportControlsInterop', IID_ISystemMediaTransportControlsInterop) + _ISystemMediaTransportControlsInteropCache ??= DynWinRtType.registerInterface('Windows.Win32.System.WinRT.ISystemMediaTransportControlsInterop', IID_ISystemMediaTransportControlsInterop) .addMethod('GetForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())); const value = _ISystemMediaTransportControlsInteropCache[prop]; return typeof value === 'function' ? value.bind(_ISystemMediaTransportControlsInteropCache) : value; diff --git a/bindings/js/e2e/ITaskbarList3.js b/bindings/js/e2e/ITaskbarList3.js index 36fd793e..12c6f315 100644 --- a/bindings/js/e2e/ITaskbarList3.js +++ b/bindings/js/e2e/ITaskbarList3.js @@ -7,7 +7,7 @@ export const IID_ITaskbarList3 = WinGuid.parse('ea1afb91-9e28-4b86-90e9-9e9f8a5e let _ITaskbarList3Cache; const _ITaskbarList3 = new Proxy({}, { get(_target, prop) { - _ITaskbarList3Cache ??= DynWinRtType.registerInterfaceUnknown('ITaskbarList3', IID_ITaskbarList3) + _ITaskbarList3Cache ??= DynWinRtType.registerInterfaceUnknown('Windows.Win32.UI.Shell.ITaskbarList3', IID_ITaskbarList3) .addMethod('HrInit', new DynWinRtMethodSig()) .addMethod('AddTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) .addMethod('DeleteTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) diff --git a/tools/dynwinrt-codegen/src/codegen/com.rs b/tools/dynwinrt-codegen/src/codegen/com.rs index f61806a5..ceaf3742 100644 --- a/tools/dynwinrt-codegen/src/codegen/com.rs +++ b/tools/dynwinrt-codegen/src/codegen/com.rs @@ -405,14 +405,16 @@ fn render_js(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { } else { "registerInterface" }; + let registration_name = format!("{}.{}", iface.namespace, name); let cache_var = format!("_{name}Cache", name = name); let iface_var = format!("_{name}", name = name); out.push_str(&format!("let {cache_var};\n", cache_var = cache_var)); out.push_str(&format!( - "const {iface_var} = new Proxy({{}}, {{\n get(_target, prop) {{\n {cache_var} ??= DynWinRtType.{register_fn}('{name}', IID_{name})\n", + "const {iface_var} = new Proxy({{}}, {{\n get(_target, prop) {{\n {cache_var} ??= DynWinRtType.{register_fn}('{registration_name}', IID_{name})\n", iface_var = iface_var, cache_var = cache_var, name = name, + registration_name = registration_name, register_fn = register_fn, )); for m in &iface.methods { diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js index 59b064b1..b30de7e3 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js @@ -8,7 +8,7 @@ const IID_DataTransferManager_default = WinGuid.parse('a5caee9b-8708-49d1-8d36-6 let _IDataTransferManagerInteropCache; const _IDataTransferManagerInterop = new Proxy({}, { get(_target, prop) { - _IDataTransferManagerInteropCache ??= DynWinRtType.registerInterfaceUnknown('IDataTransferManagerInterop', IID_IDataTransferManagerInterop) + _IDataTransferManagerInteropCache ??= DynWinRtType.registerInterfaceUnknown('Windows.Win32.UI.Shell.IDataTransferManagerInterop', IID_IDataTransferManagerInterop) .addMethod('GetForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())) .addMethod('ShowShareUIForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())); const value = _IDataTransferManagerInteropCache[prop]; diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js index e4b4b684..f7ec570a 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js @@ -7,7 +7,7 @@ export const IID_ITaskbarList3 = WinGuid.parse('ea1afb91-9e28-4b86-90e9-9e9f8a5e let _ITaskbarList3Cache; const _ITaskbarList3 = new Proxy({}, { get(_target, prop) { - _ITaskbarList3Cache ??= DynWinRtType.registerInterfaceUnknown('ITaskbarList3', IID_ITaskbarList3) + _ITaskbarList3Cache ??= DynWinRtType.registerInterfaceUnknown('Windows.Win32.UI.Shell.ITaskbarList3', IID_ITaskbarList3) .addMethod('HrInit', new DynWinRtMethodSig()) .addMethod('AddTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) .addMethod('DeleteTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs index 1e92b71f..7f4709ef 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -357,6 +357,7 @@ fn js_body_uses_cocreateinstance_and_correct_slots() { ".js must use registerInterfaceUnknown for classic COM:\n{}", js ); + assert!(js.contains("Windows.Win32.UI.Shell.ITaskbarList3")); // Base-aware slots assert!(js.contains("method(3)"), "HrInit slot 3"); From fdb9391bd70edee8b8c221fae5bfe92c944b6e78 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Thu, 23 Jul 2026 14:19:38 +0800 Subject: [PATCH 12/28] Fix IMapView\2 parameterized IID constant (was inconsistent with codegen) Copilot review found metadata_table/type_kind.rs IMAP_VIEW was e9bdaaf0-cbf6-4c39-de49-316b34326a17, but the code generator uses the correct WinRT IMapView\2 PIID e480ce40-a338-4ada-adcf-272272e48cb9 in four places (javascript/{method,project/mod,signature}.rs, python/collections.rs). MetadataTable::map_iids() feeds IMAP_VIEW into compute_parameterized_iid (metadata_table/mod.rs:378), so the mismatch produced wrong IMapView IIDs at runtime and would break map-view projections on QueryInterface. Latent (no test exercised IMapView). Aligns the runtime constant to the canonical PIID and adds a regression guard test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../dynwinrt/src/metadata_table/type_kind.rs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/dynwinrt/src/metadata_table/type_kind.rs b/crates/dynwinrt/src/metadata_table/type_kind.rs index df915e2d..2b079252 100644 --- a/crates/dynwinrt/src/metadata_table/type_kind.rs +++ b/crates/dynwinrt/src/metadata_table/type_kind.rs @@ -18,7 +18,7 @@ pub const IVECTOR_VIEW: GUID = GUID::from_u128(0xbbe1fa4c_b0e3_4583_baef_1f1b2e4 pub const IITERABLE: GUID = GUID::from_u128(0xfaa585ea_6214_4217_afda_7f46de5869b3); pub const IITERATOR: GUID = GUID::from_u128(0x6a79e863_4300_459a_9966_cbb660963ee1); pub const IMAP: GUID = GUID::from_u128(0x3c2925fe_8519_45c1_aa79_197b6718c1c1); -pub const IMAP_VIEW: GUID = GUID::from_u128(0xe9bdaaf0_cbf6_4c39_de49_316b34326a17); +pub const IMAP_VIEW: GUID = GUID::from_u128(0xe480ce40_a338_4ada_adcf_272272e48cb9); pub const IKEY_VALUE_PAIR: GUID = GUID::from_u128(0x02b51929_c1c4_4a7e_8940_0312b5c18500); pub const IOBSERVABLE_VECTOR: GUID = GUID::from_u128(0x5917eb53_50b4_4a0d_b309_65862b3f1dbc); pub const VECTOR_CHANGED_EVENT_HANDLER: GUID = @@ -247,3 +247,22 @@ pub(crate) fn pinterface_signature_from_strings(piid_sig: &str, arg_sigs: &[Stri s.push(')'); s } + +#[cfg(test)] +mod tests { + use super::*; + + /// The WinRT `IMapView`2` parameterized-interface IID. This MUST equal the + /// value the code generator uses (`IMAP_VIEW_PIID = + /// "e480ce40-a338-4ada-adcf-272272e48cb9"`), or `MetadataTable::map_iids()` + /// computes wrong `IMapView` IIDs at runtime and map-view projections + /// fail to QueryInterface. Regression guard against the prior mismatched + /// constant. + #[test] + fn imap_view_piid_is_canonical() { + assert_eq!( + IMAP_VIEW, + GUID::from_u128(0xe480ce40_a338_4ada_adcf_272272e48cb9) + ); + } +} From 73f09f16a26ddcfd5bb908d256b2e2b54208e1b2 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Thu, 23 Jul 2026 14:51:42 +0800 Subject: [PATCH 13/28] Classic-COM codegen: project scalar [out] pointer params as scalar out-values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copilot review (PR #1): methods with a scalar [out] pointer (IShellLinkW.GetShowCmd [out] int*, GetHotkey [out] WORD*, GetIconLocation [out] int* piIcon) were generated as .addOut(pointer()) and returned the raw DynWinRtValue with a bogus COM-pointer adoption TODO — an unusable result. Root cause: meta.rs collapsed scalar [out] pointers to TypeMeta::Object, losing the pointee type. Preserve the scalar pointee (GetShowCmd -> SHOW_WINDOW_CMD enum(I32), GetHotkey -> U16, GetIconLocation.piIcon -> I32) and project such out-params as .addOut(Type()) returning a JS number via .toNumber(). Interface out-params (pointer()+_fromNative) and string out-buffers (OutStringBuffer) are unaffected. Adds codegen regression tests and a live getShowCmd/getHotkey round-trip in shelllink-buffer.mjs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bindings/js/e2e/IShellLinkW.d.ts | 4 +- bindings/js/e2e/IShellLinkW.js | 12 ++-- bindings/js/e2e/shelllink-buffer.mjs | 8 ++- tools/dynwinrt-codegen/src/codegen/com.rs | 49 +++++++++++++ tools/dynwinrt-codegen/src/meta.rs | 63 ++++++++++++++++- .../dynwinrt-codegen/tests/win32_com_test.rs | 68 +++++++++++++++++++ 6 files changed, 193 insertions(+), 11 deletions(-) diff --git a/bindings/js/e2e/IShellLinkW.d.ts b/bindings/js/e2e/IShellLinkW.d.ts index b55dab60..6cfba6bb 100644 --- a/bindings/js/e2e/IShellLinkW.d.ts +++ b/bindings/js/e2e/IShellLinkW.d.ts @@ -20,9 +20,9 @@ export declare class IShellLinkW { setWorkingDirectory(dir: PWSTR): void; getArguments(cch?: number): string; setArguments(args: PWSTR): void; - getHotkey(): bigint | Buffer; + getHotkey(): number; setHotkey(wHotkey: number): void; - getShowCmd(): bigint | Buffer; + getShowCmd(): SHOW_WINDOW_CMD; setShowCmd(iShowCmd: SHOW_WINDOW_CMD): void; getIconLocation(cch?: number): string; setIconLocation(iconPath: PWSTR, iIcon: number): void; diff --git a/bindings/js/e2e/IShellLinkW.js b/bindings/js/e2e/IShellLinkW.js index 8f260043..4773f302 100644 --- a/bindings/js/e2e/IShellLinkW.js +++ b/bindings/js/e2e/IShellLinkW.js @@ -27,11 +27,11 @@ const _IShellLinkW = new Proxy({}, { .addMethod('SetWorkingDirectory', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) .addMethod('GetArguments', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) .addMethod('SetArguments', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('GetHotkey', new DynWinRtMethodSig().addOut(DynWinRtType.pointer())) + .addMethod('GetHotkey', new DynWinRtMethodSig().addOut(DynWinRtType.u16Type())) .addMethod('SetHotkey', new DynWinRtMethodSig().addIn(DynWinRtType.u16Type())) - .addMethod('GetShowCmd', new DynWinRtMethodSig().addOut(DynWinRtType.pointer())) + .addMethod('GetShowCmd', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type())) .addMethod('SetShowCmd', new DynWinRtMethodSig().addIn(DynWinRtType.i32Type())) - .addMethod('GetIconLocation', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type()).addOut(DynWinRtType.pointer())) + .addMethod('GetIconLocation', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type()).addOut(DynWinRtType.i32Type())) .addMethod('SetIconLocation', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) .addMethod('SetRelativePath', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type())) .addMethod('Resolve', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type())) @@ -88,16 +88,14 @@ export class IShellLinkW { } getHotkey() { const _out = _IShellLinkW.method(12).invoke(this._obj, []); - // TODO: raw COM interface pointer adoption requires preserved pointee metadata. - return _out; + return _out.toNumber(); } setHotkey(wHotkey) { _IShellLinkW.method(13).invoke(this._obj, [DynWinRtValue.u16(wHotkey)]); } getShowCmd() { const _out = _IShellLinkW.method(14).invoke(this._obj, []); - // TODO: raw COM interface pointer adoption requires preserved pointee metadata. - return _out; + return _out.toNumber(); } setShowCmd(iShowCmd) { _IShellLinkW.method(15).invoke(this._obj, [DynWinRtValue.i32(iShowCmd)]); diff --git a/bindings/js/e2e/shelllink-buffer.mjs b/bindings/js/e2e/shelllink-buffer.mjs index 7c1d1560..2a9f4082 100644 --- a/bindings/js/e2e/shelllink-buffer.mjs +++ b/bindings/js/e2e/shelllink-buffer.mjs @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; import { DynWinRtValue } from '../dist/index.js'; import { IShellLinkW, IID_IShellLinkW } from './IShellLinkW.js'; +import { SHOW_WINDOW_CMD } from './SHOW_WINDOW_CMD.js'; const CLSID_SHELL_LINK = '00021401-0000-0000-c000-000000000046'; @@ -23,6 +24,11 @@ assert.equal(link.getDescription(), expectedDescription); // Proves the u16 arg-wrapper codegen fix: setHotkey takes a [in] u16 (WORD). // Before the fix, codegen emitted the non-existent DynWinRtValue.u16Value(...) // and this call threw a TypeError. It must now complete without throwing. -assert.doesNotThrow(() => link.setHotkey(0x0341)); // Ctrl+Alt+'A' +const expectedHotkey = 0x0341; // Ctrl+Alt+'A' +assert.doesNotThrow(() => link.setHotkey(expectedHotkey)); +assert.equal(link.getHotkey(), expectedHotkey); + +link.setShowCmd(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED); +assert.equal(link.getShowCmd(), SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED); console.log('shelllink-buffer ok'); diff --git a/tools/dynwinrt-codegen/src/codegen/com.rs b/tools/dynwinrt-codegen/src/codegen/com.rs index 0f2b467c..d93c4bdc 100644 --- a/tools/dynwinrt-codegen/src/codegen/com.rs +++ b/tools/dynwinrt-codegen/src/codegen/com.rs @@ -2008,6 +2008,16 @@ mod tests { let com = plain_iface_with_method(m); let js = render_js(&com, None); let dts = render_dts(&com, None); + assert!( + js.contains(".addMethod('GetShowCmd', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type()))"), + ".js must register I32 out-param as i32, not pointer:\n{}", + js + ); + assert!( + !js.contains(".addMethod('GetShowCmd', new DynWinRtMethodSig().addOut(DynWinRtType.pointer()))"), + ".js must not register scalar out-param as pointer:\n{}", + js + ); // .js: must capture `_out` and return it as a JS number. assert!( js.contains("const _out = _IHasOut.method(8).invoke(this._obj, [])"), @@ -2027,6 +2037,45 @@ mod tests { ); } + #[test] + fn plain_method_single_out_u16_projects_as_return() { + // Model: `HRESULT GetHotkey([out] WORD* pwHotkey)`. + let m = MethodMeta { + name: "GetHotkey".into(), + vtable_index: 9, + params: vec![ParamMeta { + name: "pwHotkey".into(), + typ: TypeMeta::U16, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains(".addMethod('GetHotkey', new DynWinRtMethodSig().addOut(DynWinRtType.u16Type()))"), + ".js must register U16 out-param as u16, not pointer:\n{}", + js + ); + assert!( + js.contains("return _out.toNumber();"), + ".js must unwrap the U16 out as _out.toNumber():\n{}", + js + ); + assert!( + !js.contains("raw COM interface pointer adoption"), + ".js must not emit COM-pointer adoption TODO for scalar outs:\n{}", + js + ); + assert!( + dts.contains("getHotkey(): number;"), + ".d.ts must project single-out U16 as `number`:\n{}", + dts + ); + } + #[test] fn plain_method_single_out_guid_projects_as_string() { // Model: `HRESULT GetClassID([out] GUID* pClassID)` (IPersist shape). diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index 0b11f4ba..85887bd3 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -1449,10 +1449,14 @@ fn parse_interface_methods( for (j, param_def) in param_defs.iter().enumerate() { if j < sig.types.len() { clr_sig_types.push(clr_type_name(&sig.types[j])); - let typ = map_winmd_type_with_generics(&sig.types[j], index, generic_args); let is_out = param_def .flags() .contains(windows_metadata::ParamAttributes::Out); + let typ = if is_out { + map_winmd_out_param_type(&sig.types[j], index, generic_args) + } else { + map_winmd_type_with_generics(&sig.types[j], index, generic_args) + }; let direction = if is_out { if matches!(sig.types[j], windows_metadata::Type::Array(_)) { // [out] Array = FillArray (caller allocates buffer, callee fills) @@ -1823,6 +1827,42 @@ fn map_winmd_type_with_generics( } } +fn map_winmd_out_param_type( + ty: &windows_metadata::Type, + index: &reader::Index, + generic_args: &[TypeMeta], +) -> TypeMeta { + match ty { + windows_metadata::Type::PtrMut(inner, _) | windows_metadata::Type::PtrConst(inner, _) => { + let pointee = map_winmd_type_with_generics(inner, index, generic_args); + if is_scalar_out_pointee(&pointee) { + pointee + } else { + map_winmd_type_with_generics(ty, index, generic_args) + } + } + _ => map_winmd_type_with_generics(ty, index, generic_args), + } +} + +fn is_scalar_out_pointee(t: &TypeMeta) -> bool { + matches!( + t, + TypeMeta::Bool + | TypeMeta::I8 + | TypeMeta::U8 + | TypeMeta::I16 + | TypeMeta::U16 + | TypeMeta::I32 + | TypeMeta::U32 + | TypeMeta::I64 + | TypeMeta::U64 + | TypeMeta::F32 + | TypeMeta::F64 + | TypeMeta::Enum { .. } + ) +} + fn resolve_named_type( namespace: &str, name: &str, @@ -2093,6 +2133,27 @@ mod tests { assert_eq!(params[0].direction, ParamDirection::Out); } + #[test] + fn out_pointer_to_scalar_preserves_pointee_type() { + let index = reader::Index::new(vec![]); + assert_eq!( + map_winmd_out_param_type( + &windows_metadata::Type::PtrMut(Box::new(windows_metadata::Type::I32), 1), + &index, + &[], + ), + TypeMeta::I32 + ); + assert_eq!( + map_winmd_out_param_type( + &windows_metadata::Type::PtrMut(Box::new(windows_metadata::Type::U16), 1), + &index, + &[], + ), + TypeMeta::U16 + ); + } + #[test] fn class_all_interfaces_iterates_all() { let mk_iface = |n: &str| InterfaceMeta { diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs index 07bc32ee..d69bb504 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -18,6 +18,7 @@ use std::path::{Path, PathBuf}; use dynwinrt_codegen::codegen::com; use dynwinrt_codegen::codegen::project::{get_import_name, set_import_name}; use dynwinrt_codegen::meta; +use dynwinrt_codegen::types::TypeMeta; const WIN32_WINMD: &str = r"C:\s\win32metadata\Windows.Win32.winmd"; @@ -204,6 +205,73 @@ fn param_type_mapping() { ); } +#[test] +fn shelllink_scalar_out_pointers_preserve_pointees_and_codegen_as_scalars() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = + meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "IShellLinkW").unwrap(); + + let get_show_cmd = com_iface + .interface + .methods + .iter() + .find(|m| m.name == "GetShowCmd") + .expect("GetShowCmd"); + assert!(matches!( + &get_show_cmd.params[0].typ, + TypeMeta::Enum { name, underlying, .. } + if name == "SHOW_WINDOW_CMD" && matches!(**underlying, TypeMeta::I32) + )); + + let get_hotkey = com_iface + .interface + .methods + .iter() + .find(|m| m.name == "GetHotkey") + .expect("GetHotkey"); + assert_eq!(get_hotkey.params[0].typ, TypeMeta::U16); + + let get_icon_location = com_iface + .interface + .methods + .iter() + .find(|m| m.name == "GetIconLocation") + .expect("GetIconLocation"); + assert_eq!(get_icon_location.params[2].typ, TypeMeta::I32); + + let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + .expect("codegen must succeed for IShellLinkW"); + assert!( + out.js + .contains(".addMethod('GetHotkey', new DynWinRtMethodSig().addOut(DynWinRtType.u16Type()))"), + "GetHotkey must register WORD* out as u16:\n{}", + out.js + ); + assert!( + out.js + .contains(".addMethod('GetShowCmd', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type()))"), + "GetShowCmd must register SHOW_WINDOW_CMD* out as i32:\n{}", + out.js + ); + assert!( + out.js + .contains(".addMethod('GetIconLocation', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type()).addOut(DynWinRtType.i32Type()))"), + "GetIconLocation's trailing int* out must register as i32:\n{}", + out.js + ); + assert!(out.js.contains("getHotkey() {\n const _out")); + assert!(out.js.contains("getShowCmd() {\n const _out")); + assert!(out.js.contains("return _out.toNumber();")); + assert!( + !out.js.contains("getHotkey() {\n const _out = _IShellLinkW.method(12).invoke(this._obj, []);\n // TODO: raw COM interface pointer adoption"), + "GetHotkey must not get the COM-pointer TODO:\n{}", + out.js + ); +} + /// 6. Partial generation: generating a single class-name yields ONLY that /// interface plus its immediate deps (enum, coclass metadata), NOT the /// entire Windows.Win32.UI.Shell namespace. From 459ebc9fe8e2068b5786c9fdc5501b88c0a4a169 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 23 Jul 2026 17:32:11 +0800 Subject: [PATCH 14/28] refactor(com): isolate classic COM from WinRT Move classic-COM metadata, ABI signatures, pointer ownership, codegen, and JS bindings into parallel COM-specific layers while preserving existing WinRT models and APIs. --- bindings/js/e2e/DataTransferManager.d.ts | 13 - bindings/js/e2e/DataTransferManager.js | 32 - .../js/e2e/IDataTransferManagerInterop.d.ts | 6 +- .../js/e2e/IDataTransferManagerInterop.js | 16 +- bindings/js/e2e/IShellLinkW.d.ts | 7 +- bindings/js/e2e/IShellLinkW.js | 94 +- .../ISystemMediaTransportControlsInterop.d.ts | 6 +- .../ISystemMediaTransportControlsInterop.js | 12 +- bindings/js/e2e/ITaskbarList3.js | 76 +- .../js/e2e/SystemMediaTransportControls.d.ts | 13 - .../js/e2e/SystemMediaTransportControls.js | 32 - bindings/js/e2e/dtm.mjs | 30 +- bindings/js/e2e/hwnd.mjs | 6 +- bindings/js/e2e/shelllink-buffer.mjs | 8 +- bindings/js/e2e/smtc.mjs | 26 +- bindings/js/e2e/taskbarlist.mjs | 3 +- bindings/js/src/com.rs | 698 +++++++ bindings/js/src/lib.rs | 626 +----- crates/dynwinrt/src/call.rs | 123 +- crates/dynwinrt/src/classic_com.rs | 338 ---- crates/dynwinrt/src/com.rs | 639 ++++++ crates/dynwinrt/src/lib.rs | 2 +- crates/dynwinrt/src/metadata_table/arena.rs | 25 +- crates/dynwinrt/src/metadata_table/mod.rs | 31 +- .../src/metadata_table/type_handle.rs | 25 +- crates/dynwinrt/src/signature.rs | 407 +++- tools/dynwinrt-codegen/src/codegen/com/mod.rs | 11 + .../src/codegen/com/naming.rs | 85 + .../src/codegen/com/projection.rs | 224 +++ .../src/codegen/{com.rs => com/render.rs} | 1726 ++++++----------- .../src/codegen/com/type_mapping.rs | 457 +++++ .../src/codegen/javascript/project/methods.rs | 4 +- .../src/codegen/python/type_helpers.rs | 3 +- .../src/codegen/shared/imports.rs | 6 +- tools/dynwinrt-codegen/src/com_metadata.rs | 576 ++++++ tools/dynwinrt-codegen/src/lib.rs | 1 + tools/dynwinrt-codegen/src/main.rs | 5 +- tools/dynwinrt-codegen/src/meta.rs | 665 +------ .../DataTransferManager.d.ts | 13 - .../DataTransferManager.js | 32 - .../IDataTransferManagerInterop.d.ts | 4 +- .../IDataTransferManagerInterop.js | 16 +- .../snapshots/itaskbarlist3/ITaskbarList3.js | 76 +- .../tests/win32_com_interop_test.rs | 93 +- .../dynwinrt-codegen/tests/win32_com_test.rs | 193 +- 45 files changed, 4193 insertions(+), 3291 deletions(-) delete mode 100644 bindings/js/e2e/DataTransferManager.d.ts delete mode 100644 bindings/js/e2e/DataTransferManager.js delete mode 100644 bindings/js/e2e/SystemMediaTransportControls.d.ts delete mode 100644 bindings/js/e2e/SystemMediaTransportControls.js create mode 100644 bindings/js/src/com.rs delete mode 100644 crates/dynwinrt/src/classic_com.rs create mode 100644 crates/dynwinrt/src/com.rs create mode 100644 tools/dynwinrt-codegen/src/codegen/com/mod.rs create mode 100644 tools/dynwinrt-codegen/src/codegen/com/naming.rs create mode 100644 tools/dynwinrt-codegen/src/codegen/com/projection.rs rename tools/dynwinrt-codegen/src/codegen/{com.rs => com/render.rs} (52%) create mode 100644 tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs create mode 100644 tools/dynwinrt-codegen/src/com_metadata.rs delete mode 100644 tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.d.ts delete mode 100644 tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.js diff --git a/bindings/js/e2e/DataTransferManager.d.ts b/bindings/js/e2e/DataTransferManager.d.ts deleted file mode 100644 index 567a7435..00000000 --- a/bindings/js/e2e/DataTransferManager.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit - -/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ -export type HWND = bigint | Buffer; - -export declare class DataTransferManager { - /** Wrap an existing native COM pointer (for QueryInterface bridging). */ - static _fromNative(obj: unknown): DataTransferManager; - /** Get a `DataTransferManager` for the given HWND (projected from `Windows.ApplicationModel.DataTransfer.DataTransferManager`). */ - static getForWindow(appWindow: HWND): DataTransferManager; - /** IInspectable::GetRuntimeClassName — the projected class name. */ - get runtimeClassName(): string; -} diff --git a/bindings/js/e2e/DataTransferManager.js b/bindings/js/e2e/DataTransferManager.js deleted file mode 100644 index 564d1164..00000000 --- a/bindings/js/e2e/DataTransferManager.js +++ /dev/null @@ -1,32 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit -import { DynWinRtType, DynWinRtMethodSig, WinGuid } from '../dist/index.js'; -import { IDataTransferManagerInterop } from './IDataTransferManagerInterop.js'; - -const IID_IInspectable = WinGuid.parse('af86e2e0-b12d-4c6a-9c5a-d7aa65101e90'); - -let _IInspectableCache; -const _IInspectable = new Proxy({}, { - get(_target, prop) { - _IInspectableCache ??= DynWinRtType.registerInterfaceUnknown('IInspectable_projected', IID_IInspectable) - .addMethod('GetIids', new DynWinRtMethodSig().addOut(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())) - .addMethod('GetRuntimeClassName', new DynWinRtMethodSig().addOut(DynWinRtType.hstring())) - .addMethod('GetTrustLevel', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type())); - const value = _IInspectableCache[prop]; - return typeof value === 'function' ? value.bind(_IInspectableCache) : value; - }, -}); - -export class DataTransferManager { - _obj; - constructor(obj) { this._obj = obj; } - static _fromNative(obj) { return new DataTransferManager(obj); } - /** Get a `DataTransferManager` for the given HWND via the IDataTransferManagerInterop interop. */ - static getForWindow(appWindow) { - const interop = IDataTransferManagerInterop.create(); - return interop.getForWindow(appWindow); - } - /** IInspectable::GetRuntimeClassName — the projected class name. */ - get runtimeClassName() { - return _IInspectable.method(4).getString(this._obj); - } -} diff --git a/bindings/js/e2e/IDataTransferManagerInterop.d.ts b/bindings/js/e2e/IDataTransferManagerInterop.d.ts index 83b9e2bc..76f1798e 100644 --- a/bindings/js/e2e/IDataTransferManagerInterop.d.ts +++ b/bindings/js/e2e/IDataTransferManagerInterop.d.ts @@ -1,7 +1,7 @@ // Generated by dynwinrt-codegen — do not edit -import { DataTransferManager } from './DataTransferManager.js'; +import type { DynWinRtValue } from '../dist/index.js'; -/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ export type HWND = bigint | Buffer; export declare const IID_IDataTransferManagerInterop: unknown; @@ -11,6 +11,6 @@ export declare class IDataTransferManagerInterop { static create(): IDataTransferManagerInterop; /** Wrap an existing native COM pointer (for QueryInterface bridging). */ static _fromNative(obj: unknown): IDataTransferManagerInterop; - getForWindow(appWindow: HWND): DataTransferManager; + getForWindow(appWindow: HWND): DynWinRtValue; showShareUIForWindow(appWindow: HWND): void; } diff --git a/bindings/js/e2e/IDataTransferManagerInterop.js b/bindings/js/e2e/IDataTransferManagerInterop.js index 6c59a7c6..a2edf89e 100644 --- a/bindings/js/e2e/IDataTransferManagerInterop.js +++ b/bindings/js/e2e/IDataTransferManagerInterop.js @@ -1,6 +1,5 @@ // Generated by dynwinrt-codegen — do not edit -import { DynWinRtType, DynWinRtMethodSig, DynWinRtValue, WinGuid } from '../dist/index.js'; -import { DataTransferManager } from './DataTransferManager.js'; +import { DynCom, DynComMethodSig, DynWinRtValue, WinGuid } from '../dist/index.js'; export const IID_IDataTransferManagerInterop = WinGuid.parse('3a3dcd6c-3eab-43dc-bcde-45671ce800c8'); const IID_DataTransferManager_default = WinGuid.parse('a5caee9b-8708-49d1-8d36-67d25a8da00c'); @@ -8,9 +7,9 @@ const IID_DataTransferManager_default = WinGuid.parse('a5caee9b-8708-49d1-8d36-6 let _IDataTransferManagerInteropCache; const _IDataTransferManagerInterop = new Proxy({}, { get(_target, prop) { - _IDataTransferManagerInteropCache ??= DynWinRtType.registerInterfaceUnknown('Windows.Win32.UI.Shell.IDataTransferManagerInterop', IID_IDataTransferManagerInterop) - .addMethod('GetForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())) - .addMethod('ShowShareUIForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())); + _IDataTransferManagerInteropCache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.IDataTransferManagerInterop', IID_IDataTransferManagerInterop) + .addMethod('GetForWindow', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addOut(DynCom.pointerType())) + .addMethod('ShowShareUIForWindow', new DynComMethodSig().addIn(DynCom.pointerType())); const value = _IDataTransferManagerInteropCache[prop]; return typeof value === 'function' ? value.bind(_IDataTransferManagerInteropCache) : value; }, @@ -27,10 +26,11 @@ export class IDataTransferManagerInterop { return new IDataTransferManagerInterop(_obj); } getForWindow(appWindow) { - const _out = _IDataTransferManagerInterop.method(3).invoke(this._obj, [DynWinRtValue.pointer(appWindow), DynWinRtValue.iidPointer(IID_DataTransferManager_default)]); - return DataTransferManager._fromNative(_out); + const _raw = _IDataTransferManagerInterop.method(3).invoke(this._obj, [DynCom.pointer(appWindow), DynCom.iidPointer(IID_DataTransferManager_default)]); + const _out = DynCom.adoptComPointer(_raw, IID_DataTransferManager_default); + return _out; } showShareUIForWindow(appWindow) { - _IDataTransferManagerInterop.method(4).invoke(this._obj, [DynWinRtValue.pointer(appWindow)]); + _IDataTransferManagerInterop.method(4).invoke(this._obj, [DynCom.pointer(appWindow)]); } } diff --git a/bindings/js/e2e/IShellLinkW.d.ts b/bindings/js/e2e/IShellLinkW.d.ts index 6cfba6bb..5435019c 100644 --- a/bindings/js/e2e/IShellLinkW.d.ts +++ b/bindings/js/e2e/IShellLinkW.d.ts @@ -1,5 +1,6 @@ // Generated by dynwinrt-codegen — do not edit import { SHOW_WINDOW_CMD } from './SHOW_WINDOW_CMD.js'; +import type { DynWinRtValue } from '../dist/index.js'; /** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ export type HWND = bigint | Buffer; @@ -11,8 +12,8 @@ export declare const IID_IShellLinkW: unknown; export declare class IShellLinkW { /** Wrap an existing native COM pointer (for QueryInterface bridging). */ static _fromNative(obj: unknown): IShellLinkW; - getPath(cch?: number, fFlags?: number): string; - getIDList(): bigint | Buffer; + getPath(cch?: number, pfd?: bigint | Buffer, fFlags?: number): string; + getIDList(): DynWinRtValue; setIDList(pidl: bigint | Buffer): void; getDescription(cch?: number): string; setDescription(name: PWSTR): void; @@ -24,7 +25,7 @@ export declare class IShellLinkW { setHotkey(wHotkey: number): void; getShowCmd(): SHOW_WINDOW_CMD; setShowCmd(iShowCmd: SHOW_WINDOW_CMD): void; - getIconLocation(cch?: number): string; + getIconLocation(cch?: number): [string, number]; setIconLocation(iconPath: PWSTR, iIcon: number): void; setRelativePath(pathRel: PWSTR, reserved: number): void; resolve(hwnd: HWND, fFlags: number): void; diff --git a/bindings/js/e2e/IShellLinkW.js b/bindings/js/e2e/IShellLinkW.js index 4773f302..c2199386 100644 --- a/bindings/js/e2e/IShellLinkW.js +++ b/bindings/js/e2e/IShellLinkW.js @@ -1,5 +1,5 @@ // Generated by dynwinrt-codegen — do not edit -import { DynWinRtType, DynWinRtMethodSig, DynWinRtValue, WinGuid } from '../dist/index.js'; +import { DynCom, DynComMethodSig, WinGuid } from '../dist/index.js'; import { SHOW_WINDOW_CMD } from './SHOW_WINDOW_CMD.js'; function _normalizeStringBufferCount(value, name) { @@ -17,25 +17,25 @@ export const IID_IShellLinkW = WinGuid.parse('000214f9-0000-0000-c000-0000000000 let _IShellLinkWCache; const _IShellLinkW = new Proxy({}, { get(_target, prop) { - _IShellLinkWCache ??= DynWinRtType.registerInterfaceUnknown('Windows.Win32.UI.Shell.IShellLinkW', IID_IShellLinkW) - .addMethod('GetPath', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type()).addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type())) - .addMethod('GetIDList', new DynWinRtMethodSig().addOut(DynWinRtType.pointer())) - .addMethod('SetIDList', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('GetDescription', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) - .addMethod('SetDescription', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('GetWorkingDirectory', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) - .addMethod('SetWorkingDirectory', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('GetArguments', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) - .addMethod('SetArguments', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('GetHotkey', new DynWinRtMethodSig().addOut(DynWinRtType.u16Type())) - .addMethod('SetHotkey', new DynWinRtMethodSig().addIn(DynWinRtType.u16Type())) - .addMethod('GetShowCmd', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type())) - .addMethod('SetShowCmd', new DynWinRtMethodSig().addIn(DynWinRtType.i32Type())) - .addMethod('GetIconLocation', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type()).addOut(DynWinRtType.i32Type())) - .addMethod('SetIconLocation', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) - .addMethod('SetRelativePath', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type())) - .addMethod('Resolve', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type())) - .addMethod('SetPath', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())); + _IShellLinkWCache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.IShellLinkW', IID_IShellLinkW) + .addMethod('GetPath', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type()).addIn(DynCom.pointerType()).addIn(DynCom.u32Type())) + .addMethod('GetIDList', new DynComMethodSig().addOut(DynCom.pointerType())) + .addMethod('SetIDList', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('GetDescription', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) + .addMethod('SetDescription', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('GetWorkingDirectory', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) + .addMethod('SetWorkingDirectory', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('GetArguments', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) + .addMethod('SetArguments', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('GetHotkey', new DynComMethodSig().addOut(DynCom.u16Type())) + .addMethod('SetHotkey', new DynComMethodSig().addIn(DynCom.u16Type())) + .addMethod('GetShowCmd', new DynComMethodSig().addOut(DynCom.i32Type())) + .addMethod('SetShowCmd', new DynComMethodSig().addIn(DynCom.i32Type())) + .addMethod('GetIconLocation', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type()).addOut(DynCom.i32Type())) + .addMethod('SetIconLocation', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) + .addMethod('SetRelativePath', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u32Type())) + .addMethod('Resolve', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u32Type())) + .addMethod('SetPath', new DynComMethodSig().addIn(DynCom.pointerType())); const value = _IShellLinkWCache[prop]; return typeof value === 'function' ? value.bind(_IShellLinkWCache) : value; }, @@ -45,77 +45,81 @@ export class IShellLinkW { _obj; constructor(obj) { this._obj = obj; } static _fromNative(obj) { return new IShellLinkW(obj); } - getPath(cch = 260, fFlags = 0) { + getPath(cch = 260, pfd = 0, fFlags = 0) { cch = _normalizeStringBufferCount(cch, 'cch'); const _buffer = Buffer.alloc(cch * 2); - _IShellLinkW.method(3).invoke(this._obj, [DynWinRtValue.pointer(_buffer), DynWinRtValue.i32(cch), DynWinRtValue.pointer(0n), DynWinRtValue.u32(fFlags)]); - return _decodeWideString(_buffer); + _IShellLinkW.method(3).invoke(this._obj, [DynCom.pointer(_buffer), DynCom.i32(cch), DynCom.pointer(pfd), DynCom.u32(fFlags)]); + const _text = _decodeWideString(_buffer); + return _text; } getIDList() { const _out = _IShellLinkW.method(4).invoke(this._obj, []); - // TODO: raw COM interface pointer adoption requires preserved pointee metadata. - return _out; + return DynCom.adoptCoTaskMemPointer(_out); } setIDList(pidl) { - _IShellLinkW.method(5).invoke(this._obj, [DynWinRtValue.pointer(pidl)]); + _IShellLinkW.method(5).invoke(this._obj, [DynCom.pointer(pidl)]); } getDescription(cch = 260) { cch = _normalizeStringBufferCount(cch, 'cch'); const _buffer = Buffer.alloc(cch * 2); - _IShellLinkW.method(6).invoke(this._obj, [DynWinRtValue.pointer(_buffer), DynWinRtValue.i32(cch)]); - return _decodeWideString(_buffer); + _IShellLinkW.method(6).invoke(this._obj, [DynCom.pointer(_buffer), DynCom.i32(cch)]); + const _text = _decodeWideString(_buffer); + return _text; } setDescription(name) { - _IShellLinkW.method(7).invoke(this._obj, [DynWinRtValue.pointer(name)]); + _IShellLinkW.method(7).invoke(this._obj, [DynCom.pointer(name)]); } getWorkingDirectory(cch = 260) { cch = _normalizeStringBufferCount(cch, 'cch'); const _buffer = Buffer.alloc(cch * 2); - _IShellLinkW.method(8).invoke(this._obj, [DynWinRtValue.pointer(_buffer), DynWinRtValue.i32(cch)]); - return _decodeWideString(_buffer); + _IShellLinkW.method(8).invoke(this._obj, [DynCom.pointer(_buffer), DynCom.i32(cch)]); + const _text = _decodeWideString(_buffer); + return _text; } setWorkingDirectory(dir) { - _IShellLinkW.method(9).invoke(this._obj, [DynWinRtValue.pointer(dir)]); + _IShellLinkW.method(9).invoke(this._obj, [DynCom.pointer(dir)]); } getArguments(cch = 260) { cch = _normalizeStringBufferCount(cch, 'cch'); const _buffer = Buffer.alloc(cch * 2); - _IShellLinkW.method(10).invoke(this._obj, [DynWinRtValue.pointer(_buffer), DynWinRtValue.i32(cch)]); - return _decodeWideString(_buffer); + _IShellLinkW.method(10).invoke(this._obj, [DynCom.pointer(_buffer), DynCom.i32(cch)]); + const _text = _decodeWideString(_buffer); + return _text; } setArguments(args) { - _IShellLinkW.method(11).invoke(this._obj, [DynWinRtValue.pointer(args)]); + _IShellLinkW.method(11).invoke(this._obj, [DynCom.pointer(args)]); } getHotkey() { const _out = _IShellLinkW.method(12).invoke(this._obj, []); - return _out.toNumber(); + return DynCom.toNumber(_out); } setHotkey(wHotkey) { - _IShellLinkW.method(13).invoke(this._obj, [DynWinRtValue.u16(wHotkey)]); + _IShellLinkW.method(13).invoke(this._obj, [DynCom.u16(wHotkey)]); } getShowCmd() { const _out = _IShellLinkW.method(14).invoke(this._obj, []); - return _out.toNumber(); + return DynCom.toNumber(_out); } setShowCmd(iShowCmd) { - _IShellLinkW.method(15).invoke(this._obj, [DynWinRtValue.i32(iShowCmd)]); + _IShellLinkW.method(15).invoke(this._obj, [DynCom.i32(iShowCmd)]); } getIconLocation(cch = 260) { cch = _normalizeStringBufferCount(cch, 'cch'); const _buffer = Buffer.alloc(cch * 2); - _IShellLinkW.method(16).invoke(this._obj, [DynWinRtValue.pointer(_buffer), DynWinRtValue.i32(cch)]); - return _decodeWideString(_buffer); + const _out = _IShellLinkW.method(16).invoke(this._obj, [DynCom.pointer(_buffer), DynCom.i32(cch)]); + const _text = _decodeWideString(_buffer); + return [_text, DynCom.toNumber(_out)]; } setIconLocation(iconPath, iIcon) { - _IShellLinkW.method(17).invoke(this._obj, [DynWinRtValue.pointer(iconPath), DynWinRtValue.i32(iIcon)]); + _IShellLinkW.method(17).invoke(this._obj, [DynCom.pointer(iconPath), DynCom.i32(iIcon)]); } setRelativePath(pathRel, reserved) { - _IShellLinkW.method(18).invoke(this._obj, [DynWinRtValue.pointer(pathRel), DynWinRtValue.u32(reserved)]); + _IShellLinkW.method(18).invoke(this._obj, [DynCom.pointer(pathRel), DynCom.u32(reserved)]); } resolve(hwnd, fFlags) { - _IShellLinkW.method(19).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.u32(fFlags)]); + _IShellLinkW.method(19).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.u32(fFlags)]); } setPath(file) { - _IShellLinkW.method(20).invoke(this._obj, [DynWinRtValue.pointer(file)]); + _IShellLinkW.method(20).invoke(this._obj, [DynCom.pointer(file)]); } } diff --git a/bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts b/bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts index b15b539a..ff870b13 100644 --- a/bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts +++ b/bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts @@ -1,7 +1,7 @@ // Generated by dynwinrt-codegen — do not edit -import { SystemMediaTransportControls } from './SystemMediaTransportControls.js'; +import type { DynWinRtValue } from '../dist/index.js'; -/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ +/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ export type HWND = bigint | Buffer; export declare const IID_ISystemMediaTransportControlsInterop: unknown; @@ -11,5 +11,5 @@ export declare class ISystemMediaTransportControlsInterop { static create(): ISystemMediaTransportControlsInterop; /** Wrap an existing native COM pointer (for QueryInterface bridging). */ static _fromNative(obj: unknown): ISystemMediaTransportControlsInterop; - getForWindow(appWindow: HWND): SystemMediaTransportControls; + getForWindow(appWindow: HWND): DynWinRtValue; } diff --git a/bindings/js/e2e/ISystemMediaTransportControlsInterop.js b/bindings/js/e2e/ISystemMediaTransportControlsInterop.js index 191d05e0..6c1ab1d4 100644 --- a/bindings/js/e2e/ISystemMediaTransportControlsInterop.js +++ b/bindings/js/e2e/ISystemMediaTransportControlsInterop.js @@ -1,6 +1,5 @@ // Generated by dynwinrt-codegen — do not edit -import { DynWinRtType, DynWinRtMethodSig, DynWinRtValue, WinGuid } from '../dist/index.js'; -import { SystemMediaTransportControls } from './SystemMediaTransportControls.js'; +import { DynCom, DynComMethodSig, DynWinRtValue, WinGuid } from '../dist/index.js'; export const IID_ISystemMediaTransportControlsInterop = WinGuid.parse('ddb0472d-c911-4a1f-86d9-dc3d71a95f5a'); const IID_SystemMediaTransportControls_default = WinGuid.parse('99fa3ff4-1742-42a6-902e-087d41f965ec'); @@ -8,8 +7,8 @@ const IID_SystemMediaTransportControls_default = WinGuid.parse('99fa3ff4-1742-42 let _ISystemMediaTransportControlsInteropCache; const _ISystemMediaTransportControlsInterop = new Proxy({}, { get(_target, prop) { - _ISystemMediaTransportControlsInteropCache ??= DynWinRtType.registerInterface('Windows.Win32.System.WinRT.ISystemMediaTransportControlsInterop', IID_ISystemMediaTransportControlsInterop) - .addMethod('GetForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())); + _ISystemMediaTransportControlsInteropCache ??= DynCom.registerIInspectableInterface('Windows.Win32.System.WinRT.ISystemMediaTransportControlsInterop', IID_ISystemMediaTransportControlsInterop) + .addMethod('GetForWindow', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addOut(DynCom.pointerType())); const value = _ISystemMediaTransportControlsInteropCache[prop]; return typeof value === 'function' ? value.bind(_ISystemMediaTransportControlsInteropCache) : value; }, @@ -26,7 +25,8 @@ export class ISystemMediaTransportControlsInterop { return new ISystemMediaTransportControlsInterop(_obj); } getForWindow(appWindow) { - const _out = _ISystemMediaTransportControlsInterop.method(6).invoke(this._obj, [DynWinRtValue.pointer(appWindow), DynWinRtValue.iidPointer(IID_SystemMediaTransportControls_default)]); - return SystemMediaTransportControls._fromNative(_out); + const _raw = _ISystemMediaTransportControlsInterop.method(6).invoke(this._obj, [DynCom.pointer(appWindow), DynCom.iidPointer(IID_SystemMediaTransportControls_default)]); + const _out = DynCom.adoptComPointer(_raw, IID_SystemMediaTransportControls_default); + return _out; } } diff --git a/bindings/js/e2e/ITaskbarList3.js b/bindings/js/e2e/ITaskbarList3.js index 12c6f315..f6f19f54 100644 --- a/bindings/js/e2e/ITaskbarList3.js +++ b/bindings/js/e2e/ITaskbarList3.js @@ -1,5 +1,5 @@ // Generated by dynwinrt-codegen — do not edit -import { DynWinRtType, DynWinRtMethodSig, DynWinRtValue, WinGuid } from '../dist/index.js'; +import { DynCom, DynComMethodSig, WinGuid } from '../dist/index.js'; import { TBPFLAG } from './TBPFLAG.js'; export const IID_ITaskbarList3 = WinGuid.parse('ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf'); @@ -7,25 +7,25 @@ export const IID_ITaskbarList3 = WinGuid.parse('ea1afb91-9e28-4b86-90e9-9e9f8a5e let _ITaskbarList3Cache; const _ITaskbarList3 = new Proxy({}, { get(_target, prop) { - _ITaskbarList3Cache ??= DynWinRtType.registerInterfaceUnknown('Windows.Win32.UI.Shell.ITaskbarList3', IID_ITaskbarList3) - .addMethod('HrInit', new DynWinRtMethodSig()) - .addMethod('AddTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('DeleteTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('ActivateTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('SetActiveAlt', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('MarkFullscreenWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) - .addMethod('SetProgressValue', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u64Type()).addIn(DynWinRtType.u64Type())) - .addMethod('SetProgressState', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) - .addMethod('RegisterTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) - .addMethod('UnregisterTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('SetTabOrder', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) - .addMethod('SetTabActive', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type())) - .addMethod('ThumbBarAddButtons', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type()).addIn(DynWinRtType.pointer())) - .addMethod('ThumbBarUpdateButtons', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type()).addIn(DynWinRtType.pointer())) - .addMethod('ThumbBarSetImageList', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) - .addMethod('SetOverlayIcon', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) - .addMethod('SetThumbnailTooltip', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) - .addMethod('SetThumbnailClip', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())); + _ITaskbarList3Cache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.ITaskbarList3', IID_ITaskbarList3) + .addMethod('HrInit', new DynComMethodSig()) + .addMethod('AddTab', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('DeleteTab', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('ActivateTab', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('SetActiveAlt', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('MarkFullscreenWindow', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) + .addMethod('SetProgressValue', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u64Type()).addIn(DynCom.u64Type())) + .addMethod('SetProgressState', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) + .addMethod('RegisterTab', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('UnregisterTab', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('SetTabOrder', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('SetTabActive', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addIn(DynCom.u32Type())) + .addMethod('ThumbBarAddButtons', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u32Type()).addIn(DynCom.pointerType())) + .addMethod('ThumbBarUpdateButtons', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u32Type()).addIn(DynCom.pointerType())) + .addMethod('ThumbBarSetImageList', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('SetOverlayIcon', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('SetThumbnailTooltip', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('SetThumbnailClip', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())); const value = _ITaskbarList3Cache[prop]; return typeof value === 'function' ? value.bind(_ITaskbarList3Cache) : value; }, @@ -37,61 +37,61 @@ export class ITaskbarList3 { static _fromNative(obj) { return new ITaskbarList3(obj); } /** Create a new `ITaskbarList3` via `CoCreateInstance` on `CLSID_TaskbarList`. */ static create() { - const _obj = DynWinRtValue.coCreateInstance('56fdf344-fd6d-11d0-958a-006097c9a090', IID_ITaskbarList3); + const _obj = DynCom.coCreateInstance('56fdf344-fd6d-11d0-958a-006097c9a090', IID_ITaskbarList3); return new ITaskbarList3(_obj); } hrInit() { _ITaskbarList3.method(3).invoke(this._obj, []); } addTab(hwnd) { - _ITaskbarList3.method(4).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + _ITaskbarList3.method(4).invoke(this._obj, [DynCom.pointer(hwnd)]); } deleteTab(hwnd) { - _ITaskbarList3.method(5).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + _ITaskbarList3.method(5).invoke(this._obj, [DynCom.pointer(hwnd)]); } activateTab(hwnd) { - _ITaskbarList3.method(6).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + _ITaskbarList3.method(6).invoke(this._obj, [DynCom.pointer(hwnd)]); } setActiveAlt(hwnd) { - _ITaskbarList3.method(7).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + _ITaskbarList3.method(7).invoke(this._obj, [DynCom.pointer(hwnd)]); } markFullscreenWindow(hwnd, fFullscreen) { - _ITaskbarList3.method(8).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.i32(fFullscreen ? 1 : 0)]); + _ITaskbarList3.method(8).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.i32(fFullscreen ? 1 : 0)]); } setProgressValue(hwnd, ullCompleted, ullTotal) { - _ITaskbarList3.method(9).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.u64(BigInt(ullCompleted)), DynWinRtValue.u64(BigInt(ullTotal))]); + _ITaskbarList3.method(9).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.u64(BigInt(ullCompleted)), DynCom.u64(BigInt(ullTotal))]); } setProgressState(hwnd, tbpFlags) { - _ITaskbarList3.method(10).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.i32(tbpFlags)]); + _ITaskbarList3.method(10).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.i32(tbpFlags)]); } registerTab(tab, mDI) { - _ITaskbarList3.method(11).invoke(this._obj, [DynWinRtValue.pointer(tab), DynWinRtValue.pointer(mDI)]); + _ITaskbarList3.method(11).invoke(this._obj, [DynCom.pointer(tab), DynCom.pointer(mDI)]); } unregisterTab(tab) { - _ITaskbarList3.method(12).invoke(this._obj, [DynWinRtValue.pointer(tab)]); + _ITaskbarList3.method(12).invoke(this._obj, [DynCom.pointer(tab)]); } setTabOrder(tab, insertBefore) { - _ITaskbarList3.method(13).invoke(this._obj, [DynWinRtValue.pointer(tab), DynWinRtValue.pointer(insertBefore)]); + _ITaskbarList3.method(13).invoke(this._obj, [DynCom.pointer(tab), DynCom.pointer(insertBefore)]); } setTabActive(tab, mDI, reserved) { - _ITaskbarList3.method(14).invoke(this._obj, [DynWinRtValue.pointer(tab), DynWinRtValue.pointer(mDI), DynWinRtValue.u32(reserved)]); + _ITaskbarList3.method(14).invoke(this._obj, [DynCom.pointer(tab), DynCom.pointer(mDI), DynCom.u32(reserved)]); } thumbBarAddButtons(hwnd, cButtons, pButton) { - _ITaskbarList3.method(15).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.u32(cButtons), DynWinRtValue.pointer(pButton)]); + _ITaskbarList3.method(15).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.u32(cButtons), DynCom.pointer(pButton)]); } thumbBarUpdateButtons(hwnd, cButtons, pButton) { - _ITaskbarList3.method(16).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.u32(cButtons), DynWinRtValue.pointer(pButton)]); + _ITaskbarList3.method(16).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.u32(cButtons), DynCom.pointer(pButton)]); } thumbBarSetImageList(hwnd, himl) { - _ITaskbarList3.method(17).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(himl)]); + _ITaskbarList3.method(17).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(himl)]); } setOverlayIcon(hwnd, hIcon, description) { - _ITaskbarList3.method(18).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(hIcon), DynWinRtValue.pointer(description)]); + _ITaskbarList3.method(18).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(hIcon), DynCom.pointer(description)]); } setThumbnailTooltip(hwnd, tip) { - _ITaskbarList3.method(19).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(tip)]); + _ITaskbarList3.method(19).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(tip)]); } setThumbnailClip(hwnd, prcClip) { - _ITaskbarList3.method(20).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(prcClip)]); + _ITaskbarList3.method(20).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(prcClip)]); } } diff --git a/bindings/js/e2e/SystemMediaTransportControls.d.ts b/bindings/js/e2e/SystemMediaTransportControls.d.ts deleted file mode 100644 index 1eff241e..00000000 --- a/bindings/js/e2e/SystemMediaTransportControls.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit - -/** Opaque Win32 handle. Accepts either a raw pointer as `bigint` or a `Buffer`. */ -export type HWND = bigint | Buffer; - -export declare class SystemMediaTransportControls { - /** Wrap an existing native COM pointer (for QueryInterface bridging). */ - static _fromNative(obj: unknown): SystemMediaTransportControls; - /** Get a `SystemMediaTransportControls` for the given HWND (projected from `Windows.Media.SystemMediaTransportControls`). */ - static getForWindow(appWindow: HWND): SystemMediaTransportControls; - /** IInspectable::GetRuntimeClassName — the projected class name. */ - get runtimeClassName(): string; -} diff --git a/bindings/js/e2e/SystemMediaTransportControls.js b/bindings/js/e2e/SystemMediaTransportControls.js deleted file mode 100644 index 84af0b33..00000000 --- a/bindings/js/e2e/SystemMediaTransportControls.js +++ /dev/null @@ -1,32 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit -import { DynWinRtType, DynWinRtMethodSig, WinGuid } from '../dist/index.js'; -import { ISystemMediaTransportControlsInterop } from './ISystemMediaTransportControlsInterop.js'; - -const IID_IInspectable = WinGuid.parse('af86e2e0-b12d-4c6a-9c5a-d7aa65101e90'); - -let _IInspectableCache; -const _IInspectable = new Proxy({}, { - get(_target, prop) { - _IInspectableCache ??= DynWinRtType.registerInterfaceUnknown('IInspectable_projected', IID_IInspectable) - .addMethod('GetIids', new DynWinRtMethodSig().addOut(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())) - .addMethod('GetRuntimeClassName', new DynWinRtMethodSig().addOut(DynWinRtType.hstring())) - .addMethod('GetTrustLevel', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type())); - const value = _IInspectableCache[prop]; - return typeof value === 'function' ? value.bind(_IInspectableCache) : value; - }, -}); - -export class SystemMediaTransportControls { - _obj; - constructor(obj) { this._obj = obj; } - static _fromNative(obj) { return new SystemMediaTransportControls(obj); } - /** Get a `SystemMediaTransportControls` for the given HWND via the ISystemMediaTransportControlsInterop interop. */ - static getForWindow(appWindow) { - const interop = ISystemMediaTransportControlsInterop.create(); - return interop.getForWindow(appWindow); - } - /** IInspectable::GetRuntimeClassName — the projected class name. */ - get runtimeClassName() { - return _IInspectable.method(4).getString(this._obj); - } -} diff --git a/bindings/js/e2e/dtm.mjs b/bindings/js/e2e/dtm.mjs index d0495b7f..d4e1f7bb 100644 --- a/bindings/js/e2e/dtm.mjs +++ b/bindings/js/e2e/dtm.mjs @@ -1,16 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. // -// E2E: real Node.js proof that the generated natural DataTransferManager -// wrapper drives live WinRT via the *Interop* HWND pattern: +// E2E: real Node.js proof that IDataTransferManagerInterop returns a live +// WinRT object through the HWND interop pattern: // IDataTransferManagerInterop::GetForWindow(HWND, REFIID, void**) -// The test uses ONLY the high-level generated wrapper — no low-level -// `registerInterfaceUnknown` / `coCreateInstance` / QI plumbing in the test. -// // Run: node bindings/js/e2e/dtm.mjs -import { DynWinRtValue } from '../dist/index.js'; -import { DataTransferManager } from './DataTransferManager.js'; +import { DynCom, DynComMethodSig, WinGuid } from '../dist/index.js'; +import { IDataTransferManagerInterop } from './IDataTransferManagerInterop.js'; import { acquireHwndBigInt } from './hwnd.mjs'; function fail(msg) { @@ -27,23 +24,30 @@ const hwndBig = acquireHwndBigInt(); console.log(`[e2e] HWND → 0x${hwndBig.toString(16)}`); if (hwndBig === 0n) fail('acquireHwndBigInt returned NULL'); -console.log('[e2e] step 2: DataTransferManager.getForWindow(hwnd) [HIGH-LEVEL WRAPPER]'); +console.log('[e2e] step 2: IDataTransferManagerInterop.getForWindow(hwnd)'); let dtm; try { - dtm = DataTransferManager.getForWindow(hwndBig); + dtm = IDataTransferManagerInterop.create().getForWindow(hwndBig); } catch (e) { - fail(`DataTransferManager.getForWindow threw: ${e && e.message ? e.message : e}`); + fail(`getForWindow threw: ${e && e.message ? e.message : e}`); } -if (dtm == null) fail('DataTransferManager.getForWindow returned null'); +if (dtm == null) fail('getForWindow returned null'); console.log(`[e2e] got DataTransferManager instance = ${dtm}`); console.log('[e2e] step 3: MEANINGFUL — read live member `runtimeClassName` (via IInspectable::GetRuntimeClassName)'); +const inspectable = DynCom.registerIUnknownInterface( + 'IInspectable_e2e', + WinGuid.parse('af86e2e0-b12d-4c6a-9c5a-d7aa65101e90'), +) + .addMethod('GetIids', new DynComMethodSig().addOut(DynCom.pointerType()).addOut(DynCom.pointerType())) + .addMethod('GetRuntimeClassName', new DynComMethodSig().addOut(DynCom.hstringType())) + .addMethod('GetTrustLevel', new DynComMethodSig().addOut(DynCom.i32Type())); let name; try { - name = dtm.runtimeClassName; + name = inspectable.method(4).getString(dtm); } catch (e) { - fail(`dtm.runtimeClassName threw: ${e && e.message ? e.message : e}`); + fail(`GetRuntimeClassName threw: ${e && e.message ? e.message : e}`); } console.log(`[e2e] runtimeClassName = ${JSON.stringify(name)}`); diff --git a/bindings/js/e2e/hwnd.mjs b/bindings/js/e2e/hwnd.mjs index e2ff9a9c..52ffd3f1 100644 --- a/bindings/js/e2e/hwnd.mjs +++ b/bindings/js/e2e/hwnd.mjs @@ -11,14 +11,16 @@ // napi `createTestHwnd()` export, which creates a hidden `WS_POPUP` // window in the Node process using the pre-registered `STATIC` class. -import { DynWinRtValue } from '../dist/index.js'; +import { DynCom, roInitialize } from '../dist/index.js'; + +roInitialize(1); /** * Return a valid Win32 HWND owned by the current process, as a bigint. * Throws if window creation fails. */ export function acquireHwndBigInt() { - const hwnd = DynWinRtValue.createTestHwnd(); + const hwnd = DynCom.createTestHwnd(); // napi BigInt → JS bigint. const n = typeof hwnd === 'bigint' ? hwnd : BigInt(hwnd); if (n === 0n) { diff --git a/bindings/js/e2e/shelllink-buffer.mjs b/bindings/js/e2e/shelllink-buffer.mjs index 2a9f4082..c6f5595f 100644 --- a/bindings/js/e2e/shelllink-buffer.mjs +++ b/bindings/js/e2e/shelllink-buffer.mjs @@ -1,21 +1,25 @@ import assert from 'node:assert/strict'; -import { DynWinRtValue } from '../dist/index.js'; +import { DynCom } from '../dist/index.js'; import { IShellLinkW, IID_IShellLinkW } from './IShellLinkW.js'; import { SHOW_WINDOW_CMD } from './SHOW_WINDOW_CMD.js'; const CLSID_SHELL_LINK = '00021401-0000-0000-c000-000000000046'; +DynCom.initialize(1); function wide(text) { return Buffer.from(`${text}\0`, 'utf16le'); } const link = IShellLinkW._fromNative( - DynWinRtValue.coCreateInstance(CLSID_SHELL_LINK, IID_IShellLinkW), + DynCom.coCreateInstance(CLSID_SHELL_LINK, IID_IShellLinkW), ); const expectedPath = 'C:\\Windows\\explorer.exe'; link.setPath(wide(expectedPath)); assert.equal(link.getPath(260, 0).toLowerCase(), expectedPath.toLowerCase()); +const pidl = link.getIDList(); +assert.equal(pidl.isNull(), false); +pidl.release(); const expectedDescription = 'dynwinrt shelllink buffer'; link.setDescription(wide(expectedDescription)); diff --git a/bindings/js/e2e/smtc.mjs b/bindings/js/e2e/smtc.mjs index dc9a5942..f4606be1 100644 --- a/bindings/js/e2e/smtc.mjs +++ b/bindings/js/e2e/smtc.mjs @@ -18,7 +18,7 @@ import { existsSync } from 'node:fs'; import { fileURLToPath } from 'node:url'; import { dirname, resolve } from 'node:path'; -import { DynWinRtValue } from '../dist/index.js'; +import { DynCom, DynComMethodSig, WinGuid } from '../dist/index.js'; // Classic-COM interop wrapper: gets the SMTC pointer from an HWND. import { ISystemMediaTransportControlsInterop } from './ISystemMediaTransportControlsInterop.js'; import { acquireHwndBigInt } from './hwnd.mjs'; @@ -66,23 +66,29 @@ console.log(`[e2e] HWND → 0x${hwndBig.toString(16)}`); if (hwndBig === 0n) fail('acquireHwndBigInt returned NULL'); console.log('[e2e] step 2: ISystemMediaTransportControlsInterop.getForWindow(hwnd) [HIGH-LEVEL WRAPPER, IInspectable-rooted +6]'); -let smtcStub; +let smtcRaw; try { const interop = ISystemMediaTransportControlsInterop.create(); - smtcStub = interop.getForWindow(hwndBig); + smtcRaw = interop.getForWindow(hwndBig); } catch (e) { fail(`ISystemMediaTransportControlsInterop.getForWindow threw: ${e && e.message ? e.message : e}`); } -if (smtcStub == null) fail('getForWindow returned null'); -console.log(`[e2e] got SystemMediaTransportControls (companion stub) = ${smtcStub}`); -if (!smtcStub._obj) fail('companion stub is missing native _obj'); +if (smtcRaw == null) fail('getForWindow returned null'); +console.log(`[e2e] got SystemMediaTransportControls pointer = ${smtcRaw}`); -console.log('[e2e] step 3: prove liveness via IInspectable::GetRuntimeClassName (companion stub property)'); +console.log('[e2e] step 3: prove liveness via IInspectable::GetRuntimeClassName'); +const inspectable = DynCom.registerIUnknownInterface( + 'IInspectable_smtc_e2e', + WinGuid.parse('af86e2e0-b12d-4c6a-9c5a-d7aa65101e90'), +) + .addMethod('GetIids', new DynComMethodSig().addOut(DynCom.pointerType()).addOut(DynCom.pointerType())) + .addMethod('GetRuntimeClassName', new DynComMethodSig().addOut(DynCom.hstringType())) + .addMethod('GetTrustLevel', new DynComMethodSig().addOut(DynCom.i32Type())); let name; try { - name = smtcStub.runtimeClassName; + name = inspectable.method(4).getString(smtcRaw); } catch (e) { - fail(`smtcStub.runtimeClassName threw: ${e && e.message ? e.message : e}`); + fail(`GetRuntimeClassName threw: ${e && e.message ? e.message : e}`); } console.log(`[e2e] runtimeClassName = ${JSON.stringify(name)}`); const expected = 'Windows.Media.SystemMediaTransportControls'; @@ -91,7 +97,7 @@ if (name !== expected) fail(`expected runtimeClassName='${expected}', got '${nam console.log('[e2e] step 4: MEANINGFUL — exercise real SMTC members through the natural WinRT wrapper'); // Re-wrap the SAME native pointer with the full WinRT projection. // This is still 100% "generated wrapper" code — no manual registerInterface. -const smtc = SmtcProjected._fromNative(smtcStub._obj); +const smtc = SmtcProjected._fromNative(smtcRaw); // (a) round-trip a boolean property. console.log('[e2e] set isPlayEnabled = true'); diff --git a/bindings/js/e2e/taskbarlist.mjs b/bindings/js/e2e/taskbarlist.mjs index 3c8bb088..9fb330a9 100644 --- a/bindings/js/e2e/taskbarlist.mjs +++ b/bindings/js/e2e/taskbarlist.mjs @@ -6,7 +6,6 @@ // // Run: node bindings/js/e2e/taskbarlist.mjs -import { DynWinRtValue, WinGuid } from '../dist/index.js'; import { ITaskbarList3 } from './ITaskbarList3.js'; import { TBPFLAG } from './TBPFLAG.js'; import { acquireHwndBigInt } from './hwnd.mjs'; @@ -69,7 +68,7 @@ try { } // Prove the BOOL → i32 codegen fix: markFullscreenWindow historically emitted -// `DynWinRtValue.pointer(fFullscreen)` and typed `fFullscreen: BOOL = bigint | Buffer`, +// `DynCom.pointer(fFullscreen)` and typed `fFullscreen: BOOL = bigint | Buffer`, // so passing a plain `false` threw at napi. After the fix, BOOL projects as an // i32 with a `boolean` surface, and this natural-JS call round-trips. console.log('[e2e] step 7: MarkFullscreenWindow(hwnd, false) — proves BOOL→i32 codegen fix'); diff --git a/bindings/js/src/com.rs b/bindings/js/src/com.rs new file mode 100644 index 00000000..56c6d6a4 --- /dev/null +++ b/bindings/js/src/com.rs @@ -0,0 +1,698 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use napi::bindgen_prelude::{BigInt, FromNapiValue, Unknown}; +use napi::JsValue; +use napi_derive::napi; +use windows::core::{IUnknown, Interface as _}; + +use super::{DynWinRTValue, WinGUID, TABLE}; + +#[allow(dead_code)] +pub(super) enum NativePointerOwner { + Buffer(napi::bindgen_prelude::Buffer), + Uint8Array(napi::bindgen_prelude::Uint8Array), + ComObject(IUnknown), + CoTaskMem(*mut std::ffi::c_void), +} + +impl Drop for NativePointerOwner { + fn drop(&mut self) { + if let Self::CoTaskMem(ptr) = self { + if !ptr.is_null() { + unsafe { windows::Win32::System::Com::CoTaskMemFree(Some(*ptr)) }; + *ptr = std::ptr::null_mut(); + } + } + } +} + +fn co_create_instance(clsid: String, iid: &WinGUID) -> napi::Result { + let parsed = windows::core::GUID::try_from(clsid.as_str()) + .map_err(|_| napi::Error::from_reason(format!("Invalid CLSID: '{clsid}'")))?; + dynwinrt::com::co_create_instance(parsed, iid.0) + .map(DynWinRTValue::new) + .map_err(|error| napi::Error::from_reason(error.message())) +} + +fn create_test_hwnd() -> napi::Result { + use std::sync::atomic::{AtomicUsize, Ordering}; + use windows::Win32::UI::WindowsAndMessaging::{CreateWindowExW, WINDOW_EX_STYLE, WS_POPUP}; + + static CACHED_HWND: AtomicUsize = AtomicUsize::new(0); + let cached = CACHED_HWND.load(Ordering::Acquire); + if cached != 0 { + return Ok(BigInt::from(cached as u64)); + } + let class_name: Vec = "STATIC".encode_utf16().chain(Some(0)).collect(); + let title: Vec = "dynwinrt-test-hwnd\0".encode_utf16().collect(); + let hwnd = unsafe { + CreateWindowExW( + WINDOW_EX_STYLE(0), + windows::core::PCWSTR(class_name.as_ptr()), + windows::core::PCWSTR(title.as_ptr()), + WS_POPUP, + 0, + 0, + 1, + 1, + None, + None, + None, + None, + ) + } + .map_err(|error| napi::Error::from_reason(format!("CreateWindowExW: {error}")))?; + let bits = hwnd.0 as usize; + CACHED_HWND.store(bits, Ordering::Release); + Ok(BigInt::from(bits as u64)) +} + +fn pointer(value: Unknown) -> napi::Result { + use napi::sys; + + let env = value.value().env; + let raw = value.value().value; + let mut value_type = sys::ValueType::napi_undefined; + unsafe { sys::napi_typeof(env, raw, &mut value_type) }; + if matches!( + value_type, + sys::ValueType::napi_null | sys::ValueType::napi_undefined + ) { + return Ok(DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr( + std::ptr::null_mut(), + ))); + } + if value_type == sys::ValueType::napi_bigint { + let bigint = unsafe { BigInt::from_napi_value(env, raw) }?; + let (negative, bits, lossless) = bigint.get_u64(); + if negative || !lossless || bits as usize as u64 != bits { + return Err(napi::Error::from_reason( + "pointer(): bigint must fit in an unsigned pointer", + )); + } + return Ok(DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr( + bits as usize as *mut std::ffi::c_void, + ))); + } + if value_type == sys::ValueType::napi_number { + let mut number = 0.0; + unsafe { sys::napi_get_value_double(env, raw, &mut number) }; + if !number.is_finite() + || number < 0.0 + || number.fract() != 0.0 + || number > 9_007_199_254_740_991.0 + || number as u64 as usize as u64 != number as u64 + { + return Err(napi::Error::from_reason( + "pointer(): number must be a non-negative safe integer that fits in a pointer", + )); + } + return Ok(DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr( + number as usize as *mut std::ffi::c_void, + ))); + } + if let Ok(buffer) = unsafe { napi::bindgen_prelude::Buffer::from_napi_value(env, raw) } { + let ptr = buffer.as_ref().as_ptr() as *mut std::ffi::c_void; + return Ok(DynWinRTValue::with_pointer_owner( + dynwinrt::WinRTValue::RawPtr(ptr), + NativePointerOwner::Buffer(buffer), + )); + } + if let Ok(array) = unsafe { napi::bindgen_prelude::Uint8Array::from_napi_value(env, raw) } { + let ptr = array.as_ref().as_ptr() as *mut std::ffi::c_void; + return Ok(DynWinRTValue::with_pointer_owner( + dynwinrt::WinRTValue::RawPtr(ptr), + NativePointerOwner::Uint8Array(array), + )); + } + if let Ok(existing) = unsafe { <&DynWinRTValue>::from_napi_value(env, raw) } { + return match &existing.0 { + dynwinrt::WinRTValue::Object(object) => Ok(DynWinRTValue::with_pointer_owner( + dynwinrt::WinRTValue::RawPtr(object.as_raw()), + NativePointerOwner::ComObject(object.clone()), + )), + dynwinrt::WinRTValue::RawPtr(ptr) if existing.1.is_none() => { + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr(*ptr))) + } + dynwinrt::WinRTValue::Null => Ok(DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr( + std::ptr::null_mut(), + ))), + _ => Err(napi::Error::from_reason( + "pointer(): expected an object or unowned pointer value", + )), + }; + } + Err(napi::Error::from_reason( + "pointer(): expected bigint, number, Buffer, Uint8Array, object, null, or undefined", + )) +} + +fn adopt_com_pointer( + value: &mut DynWinRTValue, + iid: Option<&WinGUID>, +) -> napi::Result { + let ptr = take_raw_pointer(value, "COM interface")?; + let adopted = unsafe { dynwinrt::com::adopt_com_pointer(ptr) }; + match iid { + Some(iid) => adopted + .cast(&iid.0) + .map(DynWinRTValue::new) + .map_err(|error| napi::Error::from_reason(error.message())), + None => Ok(DynWinRTValue::new(adopted)), + } +} + +fn adopt_co_task_mem_pointer(value: &mut DynWinRTValue) -> napi::Result { + let ptr = take_raw_pointer(value, "CoTaskMem allocation")?; + if ptr.is_null() { + return Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Null)); + } + Ok(DynWinRTValue::with_pointer_owner( + dynwinrt::WinRTValue::RawPtr(ptr), + NativePointerOwner::CoTaskMem(ptr), + )) +} + +fn as_pointer_bigint(value: &DynWinRTValue) -> napi::Result { + let bits = match &value.0 { + dynwinrt::WinRTValue::Object(object) => object.as_raw() as usize, + dynwinrt::WinRTValue::RawPtr(ptr) => *ptr as usize, + dynwinrt::WinRTValue::Null => 0, + _ => { + return Err(napi::Error::from_reason( + "Value is not a pointer or COM object", + )) + } + }; + Ok(BigInt::from(bits as u64)) +} + +fn take_co_task_mem_wide_string(value: &mut DynWinRTValue) -> napi::Result { + let ptr = take_raw_pointer(value, "wide-string")?; + if ptr.is_null() { + return Ok(String::new()); + } + let result = unsafe { windows::core::PCWSTR(ptr.cast()).to_string() } + .map_err(|error| napi::Error::from_reason(error.to_string())); + unsafe { windows::Win32::System::Com::CoTaskMemFree(Some(ptr)) }; + result +} + +fn take_co_task_mem_ansi_string(value: &mut DynWinRTValue) -> napi::Result { + let ptr = take_raw_pointer(value, "ANSI-string")?; + if ptr.is_null() { + return Ok(String::new()); + } + let result = unsafe { windows::core::PCSTR(ptr.cast()).to_string() } + .map_err(|error| napi::Error::from_reason(error.to_string())); + unsafe { windows::Win32::System::Com::CoTaskMemFree(Some(ptr)) }; + result +} + +fn take_raw_pointer( + value: &mut DynWinRTValue, + description: &str, +) -> napi::Result<*mut std::ffi::c_void> { + if value.1.is_some() { + return Err(napi::Error::from_reason(format!( + "Cannot consume an owner-backed {description} pointer" + ))); + } + match std::mem::replace(&mut value.0, dynwinrt::WinRTValue::Null) { + dynwinrt::WinRTValue::RawPtr(ptr) => Ok(ptr), + dynwinrt::WinRTValue::Null => Ok(std::ptr::null_mut()), + other => { + value.0 = other; + Err(napi::Error::from_reason(format!( + "Expected a CoTaskMem-allocated {description} pointer" + ))) + } + } +} + +fn iid_pointer(value: &WinGUID) -> DynWinRTValue { + use std::collections::HashMap; + use std::sync::{Mutex, OnceLock}; + + static CACHE: OnceLock>> = OnceLock::new(); + let guid = value.0; + let key = u128::from_le_bytes(unsafe { std::mem::transmute(guid) }); + let address = *CACHE + .get_or_init(|| Mutex::new(HashMap::new())) + .lock() + .unwrap() + .entry(key) + .or_insert_with(|| Box::into_raw(Box::new(guid)) as usize); + DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr(address as *mut _)) +} + +#[napi] +pub struct DynComType(dynwinrt::com::Type); + +#[napi] +pub struct DynComMethodSig(dynwinrt::com::MethodSignature); + +#[napi] +impl DynComMethodSig { + #[napi(constructor)] + pub fn new() -> Self { + Self(dynwinrt::com::MethodSignature::new(&TABLE)) + } + + #[napi] + pub fn add_in(&self, typ: &DynComType) -> Self { + Self(self.0.clone().add_in(typ.0.clone())) + } + + #[napi] + pub fn add_out(&self, typ: &DynComType) -> Self { + Self(self.0.clone().add_out(typ.0.clone())) + } + + #[napi] + pub fn add_in_out(&self, typ: &DynComType) -> Self { + Self(self.0.clone().add_in_out(typ.0.clone())) + } + + #[napi] + pub fn add_out_fill(&self, typ: &DynComType) -> Self { + Self(self.0.clone().add_out_fill(typ.0.clone())) + } + + #[napi] + pub fn returns(&self, typ: &DynComType) -> Self { + Self(self.0.clone().returns(typ.0.clone())) + } + + #[napi] + pub fn returns_void(&self) -> Self { + Self(self.0.clone().returns_void()) + } +} + +#[napi] +pub struct DynComInterface(dynwinrt::com::Interface); + +#[napi] +impl DynComInterface { + #[napi] + pub fn add_method(&self, name: String, signature: &DynComMethodSig) -> Self { + Self(self.0.clone().add_method(&name, signature.0.clone())) + } + + #[napi] + pub fn method(&self, vtable_index: i32) -> napi::Result { + self + .0 + .method(vtable_index as usize) + .map(DynComMethodHandle) + .ok_or_else(|| { + napi::Error::from_reason(format!("No COM method at vtable index {vtable_index}")) + }) + } +} + +#[napi] +pub struct DynComMethodHandle(dynwinrt::MethodHandle); + +#[napi] +impl DynComMethodHandle { + #[napi] + pub fn get_string(&self, obj: &DynWinRTValue) -> napi::Result { + let raw = obj + .0 + .as_object() + .ok_or_else(|| napi::Error::from_reason("getString() requires a COM object"))? + .as_raw(); + self + .0 + .call_getter_hstring(raw) + .map(|value| value.to_string()) + .map_err(|error| napi::Error::from_reason(error.message())) + } + + #[napi] + pub fn invoke( + &self, + obj: &DynWinRTValue, + args: Vec<&DynWinRTValue>, + ) -> napi::Result { + let raw = obj + .0 + .as_object() + .ok_or_else(|| napi::Error::from_reason("invoke() requires a COM object"))? + .as_raw(); + let args = args.iter().map(|arg| arg.0.clone()).collect::>(); + let results = self + .0 + .invoke(raw, &args) + .map_err(|error| napi::Error::from_reason(error.message()))?; + Ok(DynWinRTValue::new( + results + .into_iter() + .next() + .unwrap_or(dynwinrt::WinRTValue::I32(0)), + )) + } + + #[napi] + pub fn invoke_all( + &self, + obj: &DynWinRTValue, + args: Vec<&DynWinRTValue>, + ) -> napi::Result> { + let raw = obj + .0 + .as_object() + .ok_or_else(|| napi::Error::from_reason("invokeAll() requires a COM object"))? + .as_raw(); + let args = args.iter().map(|arg| arg.0.clone()).collect::>(); + self + .0 + .invoke(raw, &args) + .map(|results| results.into_iter().map(DynWinRTValue::new).collect()) + .map_err(|error| napi::Error::from_reason(error.message())) + } +} + +#[napi] +pub struct DynCom; + +#[napi] +impl DynCom { + #[napi] + pub fn initialize(apartment_type: Option) -> napi::Result<()> { + let apartment_type = match apartment_type.unwrap_or(1) { + 0 => dynwinrt::com::ApartmentType::SingleThreaded, + _ => dynwinrt::com::ApartmentType::MultiThreaded, + }; + dynwinrt::com::initialize_apartment(apartment_type) + .map_err(|error| napi::Error::from_reason(error.message())) + } + + #[napi(js_name = "registerIUnknownInterface")] + pub fn register_iunknown_interface(name: String, iid: &WinGUID) -> DynComInterface { + DynComInterface(dynwinrt::com::register_interface( + &TABLE, + &name, + iid.0, + dynwinrt::com::InterfaceBase::IUnknown, + )) + } + + #[napi(js_name = "registerIInspectableInterface")] + pub fn register_iinspectable_interface(name: String, iid: &WinGUID) -> DynComInterface { + DynComInterface(dynwinrt::com::register_interface( + &TABLE, + &name, + iid.0, + dynwinrt::com::InterfaceBase::IInspectable, + )) + } + + #[napi] + pub fn bool_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.bool_type())) + } + + #[napi] + pub fn i8_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.i8_type())) + } + + #[napi] + pub fn u8_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.u8_type())) + } + + #[napi] + pub fn i16_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.i16_type())) + } + + #[napi] + pub fn u16_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.u16_type())) + } + + #[napi] + pub fn i32_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.i32_type())) + } + + #[napi] + pub fn u32_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.u32_type())) + } + + #[napi] + pub fn i64_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.i64_type())) + } + + #[napi] + pub fn u64_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.u64_type())) + } + + #[napi] + pub fn f32_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.f32_type())) + } + + #[napi] + pub fn f64_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.f64_type())) + } + + #[napi] + pub fn char16_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.char16_type())) + } + + #[napi] + pub fn guid_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.guid_type())) + } + + #[napi] + pub fn hstring_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.hstring())) + } + + #[napi] + pub fn pointer_type() -> DynComType { + DynComType(dynwinrt::com::Type::pointer()) + } + + #[napi] + pub fn interface_type(iid: &WinGUID) -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.interface(iid.0))) + } + + #[napi] + pub fn bool_value(value: bool) -> DynWinRTValue { + DynWinRTValue::bool_value(value) + } + + #[napi] + pub fn i8_value(value: i32) -> DynWinRTValue { + DynWinRTValue::i8_value(value) + } + + #[napi] + pub fn u8_value(value: u32) -> DynWinRTValue { + DynWinRTValue::u8_value(value) + } + + #[napi] + pub fn i16(value: i32) -> DynWinRTValue { + DynWinRTValue::i16(value) + } + + #[napi] + pub fn u16(value: u32) -> DynWinRTValue { + DynWinRTValue::u16(value) + } + + #[napi] + pub fn i32(value: i32) -> DynWinRTValue { + DynWinRTValue::i32(value) + } + + #[napi] + pub fn u32(value: u32) -> DynWinRTValue { + DynWinRTValue::u32(value) + } + + #[napi] + pub fn i64(value: BigInt) -> napi::Result { + let (value, lossless) = value.get_i64(); + if !lossless { + return Err(napi::Error::from_reason( + "DynCom.i64(): value must fit in a signed 64-bit integer", + )); + } + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::I64(value))) + } + + #[napi] + pub fn u64(value: BigInt) -> napi::Result { + let (negative, value, lossless) = value.get_u64(); + if negative || !lossless { + return Err(napi::Error::from_reason( + "DynCom.u64(): value must fit in an unsigned 64-bit integer", + )); + } + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::U64(value))) + } + + #[napi] + pub fn f32(value: f64) -> DynWinRTValue { + DynWinRTValue::f32(value) + } + + #[napi] + pub fn f64(value: f64) -> DynWinRTValue { + DynWinRTValue::f64(value) + } + + #[napi] + pub fn char16(value: u32) -> DynWinRTValue { + DynWinRTValue::new(dynwinrt::WinRTValue::U16(value as u16)) + } + + #[napi] + pub fn guid(value: &WinGUID) -> DynWinRTValue { + DynWinRTValue::guid(value) + } + + #[napi] + pub fn co_create_instance(clsid: String, iid: &WinGUID) -> napi::Result { + self::co_create_instance(clsid, iid) + } + + #[napi] + pub fn pointer( + #[napi( + ts_arg_type = "bigint | number | Buffer | Uint8Array | DynWinRtValue | null | undefined" + )] + value: Unknown, + ) -> napi::Result { + self::pointer(value) + } + + #[napi] + pub fn iid_pointer(value: &WinGUID) -> DynWinRTValue { + self::iid_pointer(value) + } + + #[napi] + pub fn adopt_com_pointer( + value: &mut DynWinRTValue, + iid: Option<&WinGUID>, + ) -> napi::Result { + self::adopt_com_pointer(value, iid) + } + + #[napi] + pub fn adopt_co_task_mem_pointer(value: &mut DynWinRTValue) -> napi::Result { + self::adopt_co_task_mem_pointer(value) + } + + #[napi] + pub fn as_pointer_bigint(value: &DynWinRTValue) -> napi::Result { + self::as_pointer_bigint(value) + } + + #[napi] + pub fn to_number(value: &DynWinRTValue) -> i32 { + value.to_number() + } + + #[napi] + pub fn to_bool(value: &DynWinRTValue) -> bool { + value.to_bool() + } + + #[napi] + pub fn to_f64(value: &DynWinRTValue) -> f64 { + value.to_f64() + } + + #[napi] + pub fn to_guid_string(value: &DynWinRTValue) -> napi::Result { + value.to_guid().map(|guid| guid.to_string()) + } + + #[napi] + pub fn take_co_task_mem_wide_string(value: &mut DynWinRTValue) -> napi::Result { + self::take_co_task_mem_wide_string(value) + } + + #[napi] + pub fn take_co_task_mem_ansi_string(value: &mut DynWinRTValue) -> napi::Result { + self::take_co_task_mem_ansi_string(value) + } + + #[napi] + pub fn to_u32(value: &DynWinRTValue) -> napi::Result { + match &value.0 { + dynwinrt::WinRTValue::U32(value) => Ok(*value), + _ => Err(napi::Error::from_reason("Value is not a u32")), + } + } + + #[napi] + pub fn to_i64_bigint(value: &DynWinRTValue) -> napi::Result { + match &value.0 { + dynwinrt::WinRTValue::I64(value) => Ok(BigInt::from(*value)), + _ => Err(napi::Error::from_reason("Value is not an i64")), + } + } + + #[napi] + pub fn to_u64_bigint(value: &DynWinRTValue) -> napi::Result { + match &value.0 { + dynwinrt::WinRTValue::U64(value) => Ok(BigInt::from(*value)), + _ => Err(napi::Error::from_reason("Value is not a u64")), + } + } + + #[napi] + pub fn create_test_hwnd() -> napi::Result { + self::create_test_hwnd() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn takes_and_clears_cotaskmem_wide_string() { + let text = "dynwinrt"; + let wide = text.encode_utf16().chain(Some(0)).collect::>(); + let bytes = wide.len() * std::mem::size_of::(); + let ptr = unsafe { windows::Win32::System::Com::CoTaskMemAlloc(bytes) }; + assert!(!ptr.is_null()); + unsafe { + std::ptr::copy_nonoverlapping(wide.as_ptr(), ptr.cast::(), wide.len()); + } + let mut value = DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr(ptr)); + + assert_eq!(take_co_task_mem_wide_string(&mut value).unwrap(), text); + assert!(matches!(value.0, dynwinrt::WinRTValue::Null)); + } + + #[test] + fn consuming_raw_pointer_clears_source_value() { + let ptr = 0x1234usize as *mut std::ffi::c_void; + let mut value = DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr(ptr)); + + assert_eq!(take_raw_pointer(&mut value, "test").unwrap(), ptr); + assert!(matches!(value.0, dynwinrt::WinRTValue::Null)); + assert!(take_raw_pointer(&mut value, "test").unwrap().is_null()); + } +} diff --git a/bindings/js/src/lib.rs b/bindings/js/src/lib.rs index b9c38504..d6fb1843 100644 --- a/bindings/js/src/lib.rs +++ b/bindings/js/src/lib.rs @@ -8,12 +8,13 @@ use std::sync::{Arc, Mutex, OnceLock}; use dynwinrt; use napi::bindgen_prelude::BigInt; -use napi::bindgen_prelude::Either; use napi::threadsafe_function::ThreadsafeFunctionCallMode; -use napi::JsValue; use napi_derive::napi; use windows::core::{IUnknown, Interface, HSTRING}; +mod com; +pub use com::{DynCom, DynComInterface, DynComMethodHandle, DynComMethodSig, DynComType}; + /// Shared MetadataTable — created once, used everywhere. static TABLE: std::sync::LazyLock> = std::sync::LazyLock::new(|| dynwinrt::MetadataTable::new()); @@ -159,36 +160,11 @@ impl DynWinRTType { DynWinRTType(TABLE.i16_type()) } - #[napi] - pub fn i16_type() -> Self { - DynWinRTType(TABLE.i16_type()) - } - #[napi] pub fn u16() -> Self { DynWinRTType(TABLE.u16_type()) } - #[napi] - pub fn u16_type() -> Self { - DynWinRTType(TABLE.u16_type()) - } - - #[napi] - pub fn u8_type() -> Self { - DynWinRTType(TABLE.u8_type()) - } - - #[napi] - pub fn f32_type() -> Self { - DynWinRTType(TABLE.f32_type()) - } - - #[napi] - pub fn f64_type() -> Self { - DynWinRTType(TABLE.f64_type()) - } - #[napi] pub fn bool_type() -> Self { DynWinRTType(TABLE.bool_type()) @@ -297,62 +273,14 @@ impl DynWinRTType { DynWinRTType(TABLE.register_interface(&name, iid.0)) } - /// Register a classic-COM (IUnknown-based) interface. - /// Returns self (Interface TypeHandle) for chaining `.addMethod()`. - /// User methods start at vtable slot 3 (QueryInterface/AddRef/Release are 0/1/2). - #[napi] - pub fn register_interface_unknown(name: String, iid: &WinGUID) -> Self { - DynWinRTType(TABLE.register_interface_iunknown(&name, iid.0)) - } - - /// Type-only alias for `object()` used by the classic-COM codegen. Any - /// pointer/handle (HWND, PWSTR, void*, function pointer, ...) is passed by - /// its raw ABI value; the value factory `DynWinRtValue.pointer(...)` builds - /// the matching `WinRTValue::RawPtr`. - #[napi] - pub fn pointer() -> Self { - DynWinRTType(TABLE.object()) - } - - /// Alias for `i32()` — matches the `xxxType()` naming used by codegen. - #[napi] - pub fn i32_type() -> Self { - DynWinRTType(TABLE.i32_type()) - } - - /// Alias for `u32()` — matches the `xxxType()` naming used by codegen. - #[napi] - pub fn u32_type() -> Self { - DynWinRTType(TABLE.u32_type()) - } - - /// Alias for `i64()` — matches the `xxxType()` naming used by codegen. - #[napi] - pub fn i64_type() -> Self { - DynWinRTType(TABLE.i64_type()) - } - - /// Alias for `u64()` — matches the `xxxType()` naming used by codegen. - #[napi] - pub fn u64_type() -> Self { - DynWinRTType(TABLE.u64_type()) - } - /// Add a method to this interface using a MethodSignature. - /// For IInspectable-based (WinRT) interfaces registered via - /// `register_interface`, methods start at vtable slot 6 (after - /// IUnknown 0-2 and IInspectable 3-5). For classic COM interfaces - /// registered via `register_interface_unknown`, methods start at - /// vtable slot 3 (after IUnknown 0-2 only). + /// Methods are numbered starting at vtable index 6. #[napi] pub fn add_method(&self, name: String, sig: &DynWinRTMethodSig) -> DynWinRTType { DynWinRTType(self.0.clone().add_method(&name, sig.0.clone())) } - /// Get a MethodHandle by vtable index. For IInspectable-based interfaces - /// (WinRT / `registerInterface`), the first user method is at slot 6. For - /// classic COM interfaces (`registerInterfaceUnknown`), the first user - /// method is at slot 3. + /// Get a MethodHandle by vtable index (6 = first user method). #[napi] pub fn method(&self, vtable_index: i32) -> napi::Result { self @@ -471,9 +399,9 @@ impl DynWinRTMethodHandle { .invoke(raw, &wrt_args) .map_err(|e| napi::Error::from_reason(e.message()))?; if results.is_empty() { - Ok(DynWinRTValue(dynwinrt::WinRTValue::I32(0))) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::I32(0))) } else { - Ok(DynWinRTValue(results.into_iter().next().ok_or_else( + Ok(DynWinRTValue::new(results.into_iter().next().ok_or_else( || napi::Error::from_reason("invoke: method returned no results"), )?)) } @@ -500,7 +428,7 @@ impl DynWinRTMethodHandle { .0 .invoke(raw, &wrt_args) .map_err(|e| napi::Error::from_reason(e.message()))?; - Ok(results.into_iter().map(DynWinRTValue).collect()) + Ok(results.into_iter().map(DynWinRTValue::new).collect()) } // --- Fast paths: skip Vec alloc + skip DynWinRTValue wrapping for result --- @@ -559,7 +487,7 @@ impl DynWinRTMethodHandle { self .0 .call_getter_object(raw) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(|e| napi::Error::from_reason(e.message())) } @@ -575,7 +503,7 @@ impl DynWinRTMethodHandle { .0 .invoke(raw, &[dynwinrt::WinRTValue::HString(HSTRING::from(arg))]) .map_err(|e| napi::Error::from_reason(e.message()))?; - Ok(DynWinRTValue(results.into_iter().next().ok_or_else( + Ok(DynWinRTValue::new(results.into_iter().next().ok_or_else( || napi::Error::from_reason("invoke_hstring: no result"), )?)) } @@ -592,7 +520,7 @@ impl DynWinRTMethodHandle { .0 .invoke(raw, &[dynwinrt::WinRTValue::I32(arg)]) .map_err(|e| napi::Error::from_reason(e.message()))?; - Ok(DynWinRTValue(results.into_iter().next().ok_or_else( + Ok(DynWinRTValue::new(results.into_iter().next().ok_or_else( || napi::Error::from_reason("invoke_i32: no result"), )?)) } @@ -603,28 +531,34 @@ impl DynWinRTMethodHandle { // ====================================================================== #[napi] -pub struct DynWinRTValue(dynwinrt::WinRTValue); +pub struct DynWinRTValue(dynwinrt::WinRTValue, Option); unsafe impl Send for DynWinRTValue {} unsafe impl Sync for DynWinRTValue {} +impl DynWinRTValue { + fn new(value: dynwinrt::WinRTValue) -> Self { + Self(value, None) + } + + fn with_pointer_owner(value: dynwinrt::WinRTValue, owner: com::NativePointerOwner) -> Self { + Self(value, Some(owner)) + } +} + #[napi] impl DynWinRTValue { #[napi] pub fn release(&mut self) { self.0 = dynwinrt::WinRTValue::Null; + self.1 = None; } #[napi] pub fn activation_factory(name: String) -> napi::Result { - // WinRT's RoGetActivationFactory requires the thread apartment to be - // initialized. Node's main thread is not COM-initialized by default, so - // do it lazily on the first call (same behaviour as `coCreateInstance`). - dynwinrt::classic_com::ensure_com_initialized() - .map_err(|e| napi::Error::from_reason(format!("ensure_com_initialized: {}", e.message())))?; let factory = dynwinrt::ro_get_activation_factory_2(&HSTRING::from(&name)).map_err(|e| { napi::Error::from_reason(format!("ActivationFactory '{}': {}", name, e.message())) })?; - Ok(DynWinRTValue(factory)) + Ok(DynWinRTValue::new(factory)) } /// Create a composed WinUI Application that forwards IXamlMetadataProvider @@ -645,386 +579,60 @@ impl DynWinRTValue { }) .transpose()?; dynwinrt::create_xaml_application(&provider, callback.as_ref()) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(|e| { napi::Error::from_reason(format!("createXamlApplication failed: {}", e.message())) }) } - /// Create a classic-COM instance via `CoCreateInstance(clsid, CLSCTX_INPROC_SERVER)` and QI to `iid`. - #[napi] - pub fn co_create_instance(clsid_str: String, iid: &WinGUID) -> napi::Result { - let clsid = windows::core::GUID::try_from(clsid_str.as_str()) - .map_err(|_| napi::Error::from_reason(format!("Invalid CLSID: '{}'", clsid_str)))?; - dynwinrt::classic_com::co_create_instance(clsid, iid.0) - .map(DynWinRTValue) - .map_err(|e| { - napi::Error::from_reason(format!( - "CoCreateInstance({}, {}) failed: {}", - clsid_str, - iid.to_string(), - e.message() - )) - }) - } - - /// Create a hidden top-level HWND owned by this (Node) process, for use - /// with classic-COM/WinRT interop APIs that require a process-owned window - /// (e.g. `IDataTransferManagerInterop::GetForWindow`, - /// `ISystemMediaTransportControlsInterop::GetForWindow`). - /// - /// The window is a hidden `WS_POPUP` window using the pre-registered - /// `STATIC` class; it is intentionally leaked (never destroyed) because - /// tests are short-lived and cleanup is unnecessary. Returns the HWND - /// as a `bigint`. - /// - /// This lives in classic-vertical because it is the classic-COM/interop - /// vertical's own way to obtain a process-owned HWND for testing — it - /// avoids taking a flat-Win32 dependency for the classic tests. - /// Create a small process-owned HWND for use by the classic-COM E2E - /// tests. Returns the same cached HWND on subsequent calls to avoid - /// leaking window handles in long-lived Node processes (test runners, - /// REPLs, Electron). Marshalled as a `bigint`; on the way back into a - /// classic-COM call, wrap with `DynWinRtValue.pointer(bigint)`. - /// - /// Kept as a napi export (not a Node-side test helper) because it - /// avoids taking a flat-Win32 dependency for the classic tests. - #[napi] - pub fn create_test_hwnd() -> napi::Result { - use windows::Win32::UI::WindowsAndMessaging::{CreateWindowExW, WINDOW_EX_STYLE, WS_POPUP}; - - // Guard: return the previously-created HWND on repeat calls. Storing - // the pointer bits as an `AtomicUsize` (rather than a full HWND) keeps - // the static Send/Sync without needing an unsafe impl. - use std::sync::atomic::{AtomicUsize, Ordering}; - static CACHED_HWND: AtomicUsize = AtomicUsize::new(0); - let cached = CACHED_HWND.load(Ordering::Acquire); - if cached != 0 { - return Ok(BigInt::from(cached as u64)); - } - - let class_name: Vec = "STATIC".encode_utf16().chain(std::iter::once(0)).collect(); - let title: Vec = "dynwinrt-test-hwnd\0".encode_utf16().collect(); - let hwnd = unsafe { - CreateWindowExW( - WINDOW_EX_STYLE(0), - windows::core::PCWSTR(class_name.as_ptr()), - windows::core::PCWSTR(title.as_ptr()), - WS_POPUP, - 0, - 0, - 1, - 1, - None, - None, - None, - None, - ) - } - .map_err(|e| napi::Error::from_reason(format!("CreateWindowExW: {}", e)))?; - let bits = hwnd.0 as usize; - // Only publish to the cache if creation succeeded. Losing a race here - // is harmless: one of the racers wins, the losers' HWND is used once - // and then never destroyed — the cache guarantees at most O(#racers) - // leaked windows, not O(#calls). - CACHED_HWND.store(bits, Ordering::Release); - Ok(BigInt::from(bits as u64)) - } - - /// Wrap a pointer/handle (BigInt, Buffer, or another `DynWinRtValue` holding - /// an object/raw pointer) as a `WinRTValue::RawPtr` for classic-COM calls - /// with `void*` / HWND / PWSTR / function-pointer parameters. - /// - /// Accepts: - /// - BigInt: interpreted as a raw pointer value (u64 on x64). - /// - Buffer: uses the buffer's byte-pointer directly (does not clone). - /// Caller keeps the Buffer alive for the duration of the COM call. - /// - DynWinRtValue: reuses its underlying pointer (Object/RawPtr) or - /// handles Null. - /// - null/undefined: null pointer. - #[napi] - pub fn pointer( - #[napi( - ts_arg_type = "bigint | number | Buffer | Uint8Array | DynWinRtValue | null | undefined" - )] - value: napi::bindgen_prelude::Unknown, - ) -> napi::Result { - use napi::bindgen_prelude::FromNapiValue; - use napi::sys; - - let raw_env = value.value().env; - let raw_val = value.value().value; - - // Fast path 1: null / undefined → null pointer - let mut val_type = sys::ValueType::napi_undefined; - unsafe { sys::napi_typeof(raw_env, raw_val, &mut val_type) }; - if val_type == sys::ValueType::napi_null || val_type == sys::ValueType::napi_undefined { - return Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( - std::ptr::null_mut(), - ))); - } - - // Fast path 2: BigInt → parse as u64 pointer bits. - // - // BigInt::get_u64() returns (sign_bit, magnitude, lossless). The tuple - // silently swallows negative values (sign=true is dropped) and values - // that don't fit in u64 (lossless=false → magnitude wraps). Validate - // both so that DynWinRtValue.pointer(-1n) or a >2^64 bigint produce a - // clean error instead of a fabricated pointer. - if val_type == sys::ValueType::napi_bigint { - let bi = unsafe { napi::bindgen_prelude::BigInt::from_napi_value(raw_env, raw_val) }?; - let (sign_bit, n, lossless) = bi.get_u64(); - if sign_bit { - return Err(napi::Error::from_reason( - "pointer(): bigint must be non-negative (pointer values are unsigned)", - )); - } - if !lossless { - return Err(napi::Error::from_reason( - "pointer(): bigint exceeds u64 range; pointer values must fit in u64", - )); - } - if (n as usize as u64) != n { - return Err(napi::Error::from_reason( - "pointer(): bigint exceeds usize range on this platform", - )); - } - return Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( - n as usize as *mut std::ffi::c_void, - ))); - } - - // Fast path 3: Number → cast to usize (handy for HWNDs that fit in a - // JS number; the caller can also pass BigInt for safety). - // - // A float→int cast in Rust saturates and silently accepts NaN, negative, - // fractional, and >2^53 values — any of which could produce a bogus - // pointer. Validate that the value is a finite, non-negative safe - // integer that fits in usize, and require BigInt otherwise. - if val_type == sys::ValueType::napi_number { - let mut d: f64 = 0.0; - unsafe { sys::napi_get_value_double(raw_env, raw_val, &mut d) }; - if !d.is_finite() { - return Err(napi::Error::from_reason( - "pointer(): number must be finite (got NaN or Infinity); use bigint for arbitrary pointer values", - )); - } - if d < 0.0 { - return Err(napi::Error::from_reason( - "pointer(): number must be non-negative; use bigint for arbitrary pointer values", - )); - } - if d.fract() != 0.0 { - return Err(napi::Error::from_reason( - "pointer(): number must be an integer; use bigint for arbitrary pointer values", - )); - } - // JS Number can only faithfully represent integers up to 2^53 - 1. - const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; // (1 << 53) - 1 - if d > MAX_SAFE_INTEGER { - return Err(napi::Error::from_reason( - "pointer(): number exceeds Number.MAX_SAFE_INTEGER; use bigint for arbitrary pointer values", - )); - } - let bits = d as u64; - if (bits as usize as u64) != bits { - return Err(napi::Error::from_reason( - "pointer(): number exceeds usize range on this platform; use bigint", - )); - } - return Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( - bits as usize as *mut std::ffi::c_void, - ))); - } - - // Fast path 4: Buffer / Uint8Array → base data pointer. - if let Ok(buf) = unsafe { napi::bindgen_prelude::Buffer::from_napi_value(raw_env, raw_val) } { - let slice: &[u8] = buf.as_ref(); - return Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( - slice.as_ptr() as *mut std::ffi::c_void - ))); - } - - // Fast path 4b: plain Uint8Array (NOT a Node.js Buffer subclass) → - // base data pointer. Buffer::from_napi_value above rejects raw - // Uint8Array views even though the TS surface (`ts_arg_type`) advertises - // Uint8Array. Handle it explicitly with the same semantics as Buffer. - if let Ok(arr) = unsafe { napi::bindgen_prelude::Uint8Array::from_napi_value(raw_env, raw_val) } - { - let slice: &[u8] = arr.as_ref(); - return Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( - slice.as_ptr() as *mut std::ffi::c_void - ))); - } - - // Fast path 5: existing DynWinRtValue → reuse its pointer. - if let Ok(v) = unsafe { <&DynWinRTValue>::from_napi_value(raw_env, raw_val) } { - return match &v.0 { - dynwinrt::WinRTValue::Object(o) => { - Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr(o.as_raw()))) - } - dynwinrt::WinRTValue::RawPtr(p) => Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr(*p))), - dynwinrt::WinRTValue::Null => Ok(DynWinRTValue(dynwinrt::WinRTValue::RawPtr( - std::ptr::null_mut(), - ))), - _ => Err(napi::Error::from_reason( - "pointer(): DynWinRtValue must wrap an object or raw pointer", - )), - }; - } - - Err(napi::Error::from_reason( - "pointer(): expected bigint, number, Buffer, Uint8Array, DynWinRtValue, null, or undefined", - )) - } - - /// Adopt an AddRef-owned COM interface pointer as a managed Object value. - /// This takes ownership of the caller's reference and must not be used for - /// borrowed pointers. Existing DynWinRtValue inputs are intentionally - /// rejected to avoid adopting a borrowed pointer from an owned wrapper. - #[napi] - pub fn adopt_com_pointer( - #[napi(ts_arg_type = "bigint | number | Buffer | Uint8Array | null | undefined")] - value: napi::bindgen_prelude::Unknown, - iid: Option<&WinGUID>, - ) -> napi::Result { - let ptr = raw_pointer_from_unknown(value, "adoptComPointer")?; - let adopted = unsafe { dynwinrt::classic_com::adopt_com_pointer(ptr) }; - if let Some(iid) = iid { - adopted.cast(&iid.0).map(DynWinRTValue).map_err(|e| { - napi::Error::from_reason(format!( - "adoptComPointer QueryInterface failed: {}", - e.message() - )) - }) - } else { - Ok(DynWinRTValue(adopted)) - } - } - - /// Get the underlying pointer of an Object/RawPtr value as a BigInt. - /// Useful for turning a pointer result (e.g. HWND from - /// `GetConsoleWindow`) into a bigint you can then feed into other calls. - #[napi] - pub fn as_pointer_bigint(&self) -> napi::Result { - let bits: usize = match &self.0 { - dynwinrt::WinRTValue::Object(o) => o.as_raw() as usize, - dynwinrt::WinRTValue::RawPtr(p) => *p as usize, - dynwinrt::WinRTValue::Null => 0, - _ => { - return Err(napi::Error::from_reason(format!( - "asPointerBigint: not a pointer/object value ({:?})", - self.0.get_type_kind() - ))); - } - }; - Ok(BigInt::from(bits as u64)) - } - #[napi] pub fn bool_value(value: bool) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Bool(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::Bool(value)) } #[napi] pub fn i8_value(value: i32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::I8(value as i8)) + DynWinRTValue::new(dynwinrt::WinRTValue::I8(value as i8)) } #[napi] pub fn u8_value(value: u32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::U8(value as u8)) + DynWinRTValue::new(dynwinrt::WinRTValue::U8(value as u8)) } #[napi] pub fn i16(value: i32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::I16(value as i16)) + DynWinRTValue::new(dynwinrt::WinRTValue::I16(value as i16)) } #[napi] pub fn u16(value: u32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::U16(value as u16)) + DynWinRTValue::new(dynwinrt::WinRTValue::U16(value as u16)) } #[napi] pub fn i32(value: i32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::I32(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::I32(value)) } #[napi] pub fn u32(value: u32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::U32(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::U32(value)) } #[napi] pub fn i64(value: i64) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::I64(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::I64(value)) } - /// Create a `u64` `WinRTValue`. Accepts either a JS `BigInt` (classic-COM - /// codegen emits `DynWinRtValue.u64(BigInt(v))`) or a plain JS `number` - /// (existing WinRT codegen emits `DynWinRtValue.u64(value)` for `UInt64` - /// params like stream seek/size). Accepting both keeps the WinRT path - /// working while supporting the 64-bit classic-COM path. - /// - /// Bigint path: rejects negative bigints and values > u64::MAX. - /// - /// Number path: takes `f64` (not `i64`) so we can detect and reject - /// NaN / Infinity / fractional values explicitly — coercing through - /// napi's `i64` conversion would silently truncate fractions and - /// mishandle non-finite inputs. Bounded above by - /// `Number.MAX_SAFE_INTEGER` (2^53 - 1); larger values must come in as - /// a bigint. - #[napi(ts_args_type = "value: bigint | number")] - pub fn u64(value: Either) -> napi::Result { - let n = match value { - Either::A(big) => { - let (sign_bit, n, lossless) = big.get_u64(); - if sign_bit { - return Err(napi::Error::from_reason( - "u64(): bigint must be non-negative", - )); - } - if !lossless { - return Err(napi::Error::from_reason("u64(): bigint exceeds u64::MAX")); - } - n - } - Either::B(num) => { - if !num.is_finite() { - return Err(napi::Error::from_reason( - "u64(): number must be finite (got NaN or Infinity); use bigint for arbitrary values", - )); - } - if num.fract() != 0.0 { - return Err(napi::Error::from_reason( - "u64(): number must be an integer (got a fractional value); use Math.trunc/round or bigint", - )); - } - if num < 0.0 { - return Err(napi::Error::from_reason( - "u64(): number must be non-negative; use bigint for the full u64 range", - )); - } - // JS Number can only faithfully represent integers up to 2^53 - 1; - // anything above that has already been rounded by the time napi - // converts to f64. Refuse it explicitly so callers switch to bigint - // instead of silently marshalling a lossy value. - const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; // (1 << 53) - 1 - if num > MAX_SAFE_INTEGER { - return Err(napi::Error::from_reason( - "u64(): number exceeds Number.MAX_SAFE_INTEGER; use bigint for the full u64 range", - )); - } - num as u64 - } - }; - Ok(DynWinRTValue(dynwinrt::WinRTValue::U64(n))) + #[napi] + pub fn u64(value: i64) -> DynWinRTValue { + DynWinRTValue::new(dynwinrt::WinRTValue::U64(value as u64)) } #[napi] pub fn f32(value: f64) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::F32(value as f32)) + DynWinRTValue::new(dynwinrt::WinRTValue::F32(value as f32)) } #[napi] pub fn f64(value: f64) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::F64(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::F64(value)) } /// Create an enum value from an i32. The type_handle must be an enum type. #[napi] pub fn enum_value(enum_type: &DynWinRTType, value: i32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Enum { + DynWinRTValue::new(dynwinrt::WinRTValue::Enum { value, type_handle: enum_type.0.clone(), }) @@ -1036,7 +644,7 @@ impl DynWinRTValue { value_type: &DynWinRTType, ) -> napi::Result { dynwinrt::box_ireference(value.0.clone(), value_type.0.clone()) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(|e| napi::Error::from_reason(e.message())) } @@ -1060,38 +668,15 @@ impl DynWinRTValue { #[napi] pub fn hstring(value: String) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::HString(HSTRING::from(value))) + DynWinRTValue::new(dynwinrt::WinRTValue::HString(HSTRING::from(value))) } #[napi] pub fn guid(value: &WinGUID) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Guid(value.0)) - } - /// Return a raw pointer to a stable GUID (for `REFIID` parameters). - /// The GUID is boxed and cached per-unique-value; the box outlives the process. - #[napi] - pub fn iid_pointer(value: &WinGUID) -> DynWinRTValue { - use std::collections::HashMap; - use std::sync::{Mutex, OnceLock}; - static CACHE: OnceLock>> = OnceLock::new(); - let g = value.0; - // Compose a stable u128 key from the GUID fields. - let mut key: u128 = 0; - key |= (g.data1 as u128) << 96; - key |= (g.data2 as u128) << 80; - key |= (g.data3 as u128) << 64; - for (i, b) in g.data4.iter().enumerate() { - key |= (*b as u128) << (56 - i as u32 * 8); - } - let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new())); - let mut map = cache.lock().unwrap(); - let addr = *map - .entry(key) - .or_insert_with(|| Box::into_raw(Box::new(g)) as usize); - DynWinRTValue(dynwinrt::WinRTValue::RawPtr(addr as *mut std::ffi::c_void)) + DynWinRTValue::new(dynwinrt::WinRTValue::Guid(value.0)) } #[napi] pub fn null_value() -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Null) + DynWinRTValue::new(dynwinrt::WinRTValue::Null) } /// Create an IVector from items. The element_type is used for IID computation. @@ -1105,7 +690,7 @@ impl DynWinRTValue { let wrt_items: Vec = items.iter().map(|i| i.0.clone()).collect(); let vector = dynwinrt::vector::create_vector_from_values(&wrt_items, &element_type.0, iids) .map_err(|error| napi::Error::from_reason(error.message()))?; - Ok(DynWinRTValue(dynwinrt::WinRTValue::Object(vector))) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Object(vector))) } /// Create an IMap from parallel key/value arrays. @@ -1130,7 +715,7 @@ impl DynWinRTValue { .collect(); let map = dynwinrt::map::create_map_from_values(&entries, &key_type.0, &value_type.0, iids) .map_err(|error| napi::Error::from_reason(error.message()))?; - Ok(DynWinRTValue(dynwinrt::WinRTValue::Object(map))) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Object(map))) } #[napi] @@ -1139,7 +724,7 @@ impl DynWinRTValue { dynwinrt::Error::Canceled => napi::Error::from_reason("Async operation was canceled"), other => napi::Error::from_reason(format!("Async operation failed: {}", other.message())), })?; - Ok(DynWinRTValue(v)) + Ok(DynWinRTValue::new(v)) } /// Cancel the underlying WinRT async operation (calls `IAsyncInfo::Cancel`). @@ -1182,7 +767,10 @@ impl DynWinRTValue { .weak::() .build()?; let progress_cb: dynwinrt::ProgressCallback = Box::new(move |val: dynwinrt::WinRTValue| { - tsfn.call(DynWinRTValue(val), ThreadsafeFunctionCallMode::NonBlocking); + tsfn.call( + DynWinRTValue::new(val), + ThreadsafeFunctionCallMode::NonBlocking, + ); }); let handler = dynwinrt::create_progress_handler(handler_iid, progress_type, progress_cb); @@ -1224,7 +812,7 @@ impl DynWinRTValue { .0 .cast(&iid.0) .map_err(|e| napi::Error::from_reason(format!("QueryInterface failed: {}", e.message())))?; - Ok(DynWinRTValue(result)) + Ok(DynWinRTValue::new(result)) } #[napi] @@ -1325,81 +913,6 @@ impl DynWinRTValue { } } -fn raw_pointer_from_unknown( - value: napi::bindgen_prelude::Unknown, - context: &str, -) -> napi::Result<*mut std::ffi::c_void> { - use napi::bindgen_prelude::FromNapiValue; - use napi::sys; - - let raw_env = value.value().env; - let raw_val = value.value().value; - let mut val_type = sys::ValueType::napi_undefined; - unsafe { sys::napi_typeof(raw_env, raw_val, &mut val_type) }; - - if val_type == sys::ValueType::napi_null || val_type == sys::ValueType::napi_undefined { - return Ok(std::ptr::null_mut()); - } - - if val_type == sys::ValueType::napi_bigint { - let bi = unsafe { napi::bindgen_prelude::BigInt::from_napi_value(raw_env, raw_val) }?; - let (sign_bit, n, lossless) = bi.get_u64(); - if sign_bit { - return Err(napi::Error::from_reason(format!( - "{context}: bigint must be non-negative" - ))); - } - if !lossless { - return Err(napi::Error::from_reason(format!( - "{context}: bigint exceeds u64 range" - ))); - } - if (n as usize as u64) != n { - return Err(napi::Error::from_reason(format!( - "{context}: bigint exceeds usize range on this platform" - ))); - } - return Ok(n as usize as *mut std::ffi::c_void); - } - - if val_type == sys::ValueType::napi_number { - let mut d: f64 = 0.0; - unsafe { sys::napi_get_value_double(raw_env, raw_val, &mut d) }; - if !d.is_finite() || d < 0.0 || d.fract() != 0.0 { - return Err(napi::Error::from_reason(format!( - "{context}: number must be a finite, non-negative integer" - ))); - } - const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991.0; - if d > MAX_SAFE_INTEGER { - return Err(napi::Error::from_reason(format!( - "{context}: number exceeds Number.MAX_SAFE_INTEGER; use bigint" - ))); - } - let bits = d as u64; - if (bits as usize as u64) != bits { - return Err(napi::Error::from_reason(format!( - "{context}: number exceeds usize range on this platform" - ))); - } - return Ok(bits as usize as *mut std::ffi::c_void); - } - - if let Ok(buf) = unsafe { napi::bindgen_prelude::Buffer::from_napi_value(raw_env, raw_val) } { - let slice: &[u8] = buf.as_ref(); - return Ok(slice.as_ptr() as *mut std::ffi::c_void); - } - - if let Ok(arr) = unsafe { napi::bindgen_prelude::Uint8Array::from_napi_value(raw_env, raw_val) } { - let slice: &[u8] = arr.as_ref(); - return Ok(slice.as_ptr() as *mut std::ffi::c_void); - } - - Err(napi::Error::from_reason(format!( - "{context}: expected bigint, number, Buffer, Uint8Array, null, or undefined" - ))) -} - // ====================================================================== // Array binding — blittable fast path via typed Vec, generic fallback // ====================================================================== @@ -1419,14 +932,14 @@ impl DynWinRTArray { /// Per-element access (works for all element types). #[napi] pub fn get(&self, index: u32) -> DynWinRTValue { - DynWinRTValue(self.0.get(index as usize)) + DynWinRTValue::new(self.0.get(index as usize)) } /// Convert all elements to DynWinRTValue array. #[napi] pub fn to_values(&self) -> Vec { (0..self.0.len()) - .map(|i| DynWinRTValue(self.0.get(i))) + .map(|i| DynWinRTValue::new(self.0.get(i))) .collect() } @@ -1658,7 +1171,7 @@ impl DynWinRTArray { /// Wrap as DynWinRTValue::Array for passing to call(). #[napi] pub fn to_value(&self) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Array(self.0.clone())) + DynWinRTValue::new(dynwinrt::WinRTValue::Array(self.0.clone())) } } @@ -1832,12 +1345,12 @@ impl DynWinRTStruct { let inner = self.0.get_field_struct(index as usize); let raw = unsafe { *(inner.as_ptr() as *const *mut std::ffi::c_void) }; if raw.is_null() { - Ok(DynWinRTValue(dynwinrt::WinRTValue::Null)) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Null)) } else { let obj = unsafe { IUnknown::from_raw_borrowed(&raw) } .ok_or_else(|| napi::Error::from_reason("null COM pointer"))? .clone(); - Ok(DynWinRTValue(dynwinrt::WinRTValue::Object(obj))) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Object(obj))) } } @@ -1869,7 +1382,7 @@ impl DynWinRTStruct { /// Wrap as DynWinRTValue::Struct for passing to call(). #[napi] pub fn to_value(&self) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Struct(self.0.clone())) + DynWinRTValue::new(dynwinrt::WinRTValue::Struct(self.0.clone())) } } @@ -2140,7 +1653,8 @@ impl DynWinRtDelegate { const E_UNEXPECTED: windows::core::HRESULT = windows::core::HRESULT(0x8000FFFFu32 as i32); let current_tid = unsafe { GetCurrentThreadId() }; - let js_args: Vec = args.iter().map(|a| DynWinRTValue(a.clone())).collect(); + let js_args: Vec = + args.iter().map(|a| DynWinRTValue::new(a.clone())).collect(); if current_tid == register_tid { // Same-thread synchronous direct invocation. Bypass the TSFN because @@ -2240,7 +1754,7 @@ impl DynWinRtDelegate { /// Get the delegate as a DynWinRtValue for passing to WinRT methods. #[napi] pub fn to_value(&self) -> DynWinRTValue { - DynWinRTValue(self.0.clone()) + DynWinRTValue::new(self.0.clone()) } } @@ -2348,7 +1862,7 @@ impl DynWinRtElementFactory { Err(_) => return Err(E_FAIL), }; let raw_env = get_env.0; - let js_arg = DynWinRTValue(args.clone()); + let js_arg = DynWinRTValue::new(args.clone()); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe( || -> napi::Result { unsafe { @@ -2425,7 +1939,7 @@ impl DynWinRtElementFactory { Err(_) => return E_FAIL, }; let raw_env = recycle_env.0; - let js_arg = DynWinRTValue(args.clone()); + let js_arg = DynWinRTValue::new(args.clone()); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| -> napi::Result<()> { unsafe { @@ -2488,7 +2002,7 @@ impl DynWinRtElementFactory { #[napi] pub fn to_value(&self) -> DynWinRTValue { - DynWinRTValue(self.value.clone()) + DynWinRTValue::new(self.value.clone()) } #[napi] @@ -2543,17 +2057,3 @@ pub fn raw_get_i32(method: &DynWinRTMethodHandle, obj: &DynWinRTValue) -> napi:: .call_getter_i32(raw) .map_err(|e| napi::Error::from_reason(e.message())) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn codegen_type_alias_constructors_exist() { - let _ = DynWinRTType::u16_type(); - let _ = DynWinRTType::i16_type(); - let _ = DynWinRTType::u8_type(); - let _ = DynWinRTType::f32_type(); - let _ = DynWinRTType::f64_type(); - } -} diff --git a/crates/dynwinrt/src/call.rs b/crates/dynwinrt/src/call.rs index f7acde2f..2635bb3d 100644 --- a/crates/dynwinrt/src/call.rs +++ b/crates/dynwinrt/src/call.rs @@ -5,7 +5,11 @@ use core::ffi::c_void; use libffi::middle::{Arg, arg}; use windows_core::{HRESULT, Interface}; -use crate::{abi::AbiValue, signature::Parameter, value::WinRTValue}; +use crate::{ + abi::{AbiType, AbiValue}, + signature::{MethodReturn, Parameter}, + value::WinRTValue, +}; pub(crate) trait ArgumentList { fn get_value(&self, index: usize) -> &WinRTValue; @@ -117,7 +121,7 @@ pub fn call_fill_array_1in( }) } -use crate::metadata_table::{TypeHandle, TypeKind}; +use crate::metadata_table::TypeHandle; /// Stable heap storage for array in-param data. /// Owns the serialized byte buffer so it stays alive for the FFI call. @@ -180,12 +184,40 @@ impl Drop for FillArraySlot { } } -pub fn call_winrt_method_dynamic( +fn input_abi_value(value: &WinRTValue) -> windows_core::Result { + let value = match value { + WinRTValue::Bool(value) => AbiValue::Bool(u8::from(*value)), + WinRTValue::I8(value) => AbiValue::I8(*value), + WinRTValue::U8(value) => AbiValue::U8(*value), + WinRTValue::I16(value) => AbiValue::I16(*value), + WinRTValue::U16(value) => AbiValue::U16(*value), + WinRTValue::I32(value) => AbiValue::I32(*value), + WinRTValue::U32(value) => AbiValue::U32(*value), + WinRTValue::I64(value) => AbiValue::I64(*value), + WinRTValue::U64(value) => AbiValue::U64(*value), + WinRTValue::F32(value) => AbiValue::F32(*value), + WinRTValue::F64(value) => AbiValue::F64(*value), + WinRTValue::HResult(value) => AbiValue::I32(value.0), + WinRTValue::Enum { value, .. } => AbiValue::I32(*value), + WinRTValue::RawPtr(value) => AbiValue::Pointer(*value), + WinRTValue::Null => AbiValue::Pointer(std::ptr::null_mut()), + _ => { + return Err(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + "unsupported in/out argument value", + )); + } + }; + Ok(value) +} + +pub fn call_method_dynamic( vtable_index: usize, obj: *mut c_void, parameters: &[Parameter], args: &A, out_count: usize, + return_kind: &MethodReturn, cif: &libffi::middle::Cif, ) -> windows_core::Result> { use crate::metadata_table::ValueTypeData; @@ -196,6 +228,7 @@ pub fn call_winrt_method_dynamic( let mut out_values: Vec = Vec::with_capacity(out_count); let mut out_ptrs: Vec<*const std::ffi::c_void> = Vec::with_capacity(out_count); let mut struct_out_values: Vec> = Vec::with_capacity(out_count); + let mut guid_out_values: Vec>> = Vec::with_capacity(out_count); // Array storage: Box'd for pointer stability (addresses don't change after creation) let mut array_out_slots: Vec> = Vec::new(); @@ -242,6 +275,7 @@ pub fn call_winrt_method_dynamic( out_values.push(AbiValue::Pointer(std::ptr::null_mut())); out_ptrs.push(std::ptr::null()); struct_out_values.push(None); + guid_out_values.push(None); array_out_map.push(None); } else if p.typ.is_array() { let slot = Box::new(ArrayOutSlot { @@ -258,18 +292,46 @@ pub fn call_winrt_method_dynamic( out_values.push(AbiValue::Pointer(std::ptr::null_mut())); out_ptrs.push(std::ptr::null()); struct_out_values.push(None); + guid_out_values.push(None); + fill_array_map.push(None); + } else if p.typ.is_guid() { + let value = Box::new(windows_core::GUID::zeroed()); + out_ptrs.push((&*value as *const windows_core::GUID).cast()); + out_values.push(AbiValue::Pointer(std::ptr::null_mut())); + struct_out_values.push(None); + guid_out_values.push(Some(value)); + array_out_map.push(None); fill_array_map.push(None); - } else if matches!(p.typ.kind(), TypeKind::Struct(_)) { - let val = p.typ.default_value(); + } else if p.typ.is_struct() { + let val = if p.is_in_out() { + args.get_value(p.input_index.expect("in/out input index")) + .as_struct() + .ok_or_else(|| { + windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + "expected struct value for in/out parameter", + ) + })? + .clone() + } else { + p.typ.default_struct_value() + }; out_ptrs.push(val.as_ptr() as *const std::ffi::c_void); out_values.push(AbiValue::Pointer(std::ptr::null_mut())); struct_out_values.push(Some(val)); + guid_out_values.push(None); array_out_map.push(None); fill_array_map.push(None); } else { - out_values.push(p.typ.abi_type().default_value()); + let value = if p.is_in_out() { + input_abi_value(args.get_value(p.input_index.expect("in/out input index")))? + } else { + p.typ.abi_type().default_value() + }; + out_values.push(value); out_ptrs.push(out_values.last().unwrap().as_out_ptr()); struct_out_values.push(None); + guid_out_values.push(None); array_out_map.push(None); fill_array_map.push(None); } @@ -278,7 +340,7 @@ pub fn call_winrt_method_dynamic( // Phase 1b: Pre-compute all array in-param data (must happen before Phase 2) for p in parameters { - if !p.is_out() && p.typ.is_array() { + if p.is_input() && !p.is_out() && p.typ.is_array() { let array_data = args .get_value(p.value_index) .as_array() @@ -323,8 +385,41 @@ pub fn call_winrt_method_dynamic( } // Phase 3: Call - let hr: windows_core::HRESULT = unsafe { cif.call(CodePtr(fptr), &ffi_args) }; - hr.ok()?; + let return_value = unsafe { + match return_kind { + MethodReturn::HResult => { + let hr: windows_core::HRESULT = cif.call(CodePtr(fptr), &ffi_args); + hr.ok()?; + None + } + MethodReturn::Void => { + cif.call::<()>(CodePtr(fptr), &ffi_args); + None + } + MethodReturn::Value(typ) => { + let value = match typ.abi_type() { + AbiType::Bool => AbiValue::Bool(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::I8 => AbiValue::I8(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::U8 => AbiValue::U8(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::I16 => AbiValue::I16(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::U16 => AbiValue::U16(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::I32 => AbiValue::I32(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::U32 => AbiValue::U32(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::I64 => AbiValue::I64(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::U64 => AbiValue::U64(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::F32 => AbiValue::F32(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::F64 => AbiValue::F64(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::Ptr => AbiValue::Pointer(cif.call(CodePtr(fptr), &ffi_args)), + }; + Some(typ.from_out_value(&value).map_err(|error| { + windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &error.message(), + ) + })?) + } + } + }; // Counted FillArray methods (for example GetMany) carry the actual count // as their UInt32 retval. Other FillArray methods write the full capacity. @@ -335,7 +430,7 @@ pub fn call_winrt_method_dynamic( .iter() .rev() .find(|param| param.is_out() && !param.is_fill_array()) - .filter(|param| matches!(param.typ.kind(), TypeKind::U32)) + .filter(|param| param.typ.is_u32()) .and_then(|param| match out_values[param.value_index] { AbiValue::U32(value) => Some(value), _ => None, @@ -343,7 +438,11 @@ pub fn call_winrt_method_dynamic( }; // Phase 4: Extract results - let mut result_values: Vec = Vec::with_capacity(out_count); + let mut result_values: Vec = + Vec::with_capacity(out_count + usize::from(return_value.is_some())); + if let Some(value) = return_value { + result_values.push(value); + } for p in parameters { if p.is_out() { if let Some(slot_idx) = fill_array_map[p.value_index] { @@ -383,6 +482,8 @@ pub fn call_winrt_method_dynamic( ) }; result_values.push(WinRTValue::Array(array_value)); + } else if let Some(guid) = guid_out_values[p.value_index].take() { + result_values.push(WinRTValue::Guid(*guid)); } else if let Some(struct_val) = struct_out_values[p.value_index].take() { result_values.push(WinRTValue::Struct(struct_val)); } else { diff --git a/crates/dynwinrt/src/classic_com.rs b/crates/dynwinrt/src/classic_com.rs deleted file mode 100644 index 69bba0f2..00000000 --- a/crates/dynwinrt/src/classic_com.rs +++ /dev/null @@ -1,338 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use core::ffi::c_void; -use std::cell::RefCell; - -use windows::Win32::System::Com::{ - CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED, CoCreateInstance, CoInitializeEx, CoUninitialize, -}; -use windows_core::{GUID, IUnknown, Interface}; - -use crate::{MethodSignature, WinRTValue, result}; - -const RPC_E_CHANGED_MODE: windows_core::HRESULT = windows_core::HRESULT(0x80010106u32 as i32); - -struct ComApartment; - -impl Drop for ComApartment { - fn drop(&mut self) { - unsafe { CoUninitialize() }; - } -} - -enum ComInitialization { - Unknown, - Owned(ComApartment), - ExistingApartment, -} - -thread_local! { - static COM_INITIALIZATION: RefCell = - const { RefCell::new(ComInitialization::Unknown) }; -} - -pub fn ensure_com_initialized() -> result::Result<()> { - COM_INITIALIZATION.with(|state| { - if !matches!(*state.borrow(), ComInitialization::Unknown) { - return Ok(()); - } - - let hr = unsafe { CoInitializeEx(None, COINIT_MULTITHREADED) }; - if hr.is_ok() { - *state.borrow_mut() = ComInitialization::Owned(ComApartment); - Ok(()) - } else if hr == RPC_E_CHANGED_MODE { - *state.borrow_mut() = ComInitialization::ExistingApartment; - Ok(()) - } else { - Err(result::Error::WindowsError( - windows_core::Error::from_hresult(hr), - )) - } - }) -} - -pub fn co_create_instance(clsid: GUID, iid: GUID) -> result::Result { - ensure_com_initialized()?; - - let unknown: IUnknown = unsafe { CoCreateInstance(&clsid, None, CLSCTX_INPROC_SERVER) } - .map_err(result::Error::WindowsError)?; - let mut result = std::ptr::null_mut(); - unsafe { unknown.query(&iid, &mut result) } - .ok() - .map_err(result::Error::WindowsError)?; - Ok(WinRTValue::Object(unsafe { IUnknown::from_raw(result) })) -} - -/// Adopt an AddRef-owned COM interface pointer into a managed Object value. -/// -/// The pointer must represent a caller-owned COM reference (+1). This function -/// takes ownership with `IUnknown::from_raw` and must not be used for borrowed -/// pointers. -pub unsafe fn adopt_com_pointer(ptr: *mut c_void) -> WinRTValue { - if ptr.is_null() { - WinRTValue::Null - } else { - WinRTValue::Object(unsafe { IUnknown::from_raw(ptr) }) - } -} - -pub fn call_method( - vtable_index: usize, - obj: *mut c_void, - signature: MethodSignature, - args: &[WinRTValue], -) -> result::Result> { - signature - .build(vtable_index) - .call_dynamic(obj, args) - .map_err(result::Error::WindowsError) -} - -#[cfg(test)] -fn call_method_1_ptr( - vtable_index: usize, - obj: *mut c_void, - ptr: *const c_void, -) -> result::Result<()> { - crate::call::call_winrt_method_1(vtable_index, obj, ptr) - .ok() - .map_err(result::Error::WindowsError) -} - -#[cfg(test)] -fn call_method_2_ptr_i32( - vtable_index: usize, - obj: *mut c_void, - ptr: *mut c_void, - value: i32, -) -> result::Result<()> { - crate::call::call_winrt_method_2(vtable_index, obj, ptr, value) - .ok() - .map_err(result::Error::WindowsError) -} - -#[cfg(test)] -fn wide_null(text: &str) -> Vec { - text.encode_utf16().chain(std::iter::once(0)).collect() -} - -#[cfg(test)] -fn wide_buffer(characters: usize) -> Vec { - vec![0; characters] -} - -#[cfg(test)] -fn wide_to_string(buffer: &[u16]) -> String { - let end = buffer - .iter() - .position(|ch| *ch == 0) - .unwrap_or(buffer.len()); - String::from_utf16_lossy(&buffer[..end]) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - InterfaceSignature, MetadataTable, com_helpers::E_NOINTERFACE, ro_get_activation_factory_2, - roapi::query_interface, - }; - use windows::{ - ApplicationModel::DataTransfer::DataTransferManager, - Win32::{ - UI::Shell::IDataTransferManagerInterop, - UI::WindowsAndMessaging::{ - CreateWindowExW, DestroyWindow, WINDOW_EX_STYLE, WS_OVERLAPPED, - }, - }, - }; - use windows_core::{HSTRING, Interface, w}; - - const CLSID_SHELL_LINK: GUID = GUID::from_u128(0x00021401_0000_0000_c000_000000000046); - const IID_ISHELL_LINK_W: GUID = GUID::from_u128(0x000214f9_0000_0000_c000_000000000046); - const REGDB_E_CLASSNOTREG: windows_core::HRESULT = windows_core::HRESULT(0x80040154u32 as i32); - - fn shell_link() -> result::Result { - co_create_instance(CLSID_SHELL_LINK, IID_ISHELL_LINK_W) - } - - fn shell_link_signature(table: &std::sync::Arc) -> InterfaceSignature { - let mut iface = - InterfaceSignature::define_from_iunknown("IShellLinkW", IID_ISHELL_LINK_W, table); - iface - .add_method(MethodSignature::new(table)) // 3 GetPath - .add_method(MethodSignature::new(table)) // 4 GetIDList - .add_method(MethodSignature::new(table)) // 5 SetIDList - .add_method(MethodSignature::new(table)) // 6 GetDescription - .add_method(MethodSignature::new(table)) // 7 SetDescription - .add_method(MethodSignature::new(table)) // 8 GetWorkingDirectory - .add_method(MethodSignature::new(table)) // 9 SetWorkingDirectory - .add_method(MethodSignature::new(table)) // 10 GetArguments - .add_method(MethodSignature::new(table)) // 11 SetArguments - .add_method(MethodSignature::new(table).add_out(table.u16_type())) // 12 GetHotkey - .add_method(MethodSignature::new(table).add_in(table.u16_type())) // 13 SetHotkey - .add_method(MethodSignature::new(table).add_out(table.i32_type())) // 14 GetShowCmd - .add_method(MethodSignature::new(table).add_in(table.i32_type())); // 15 SetShowCmd - iface - } - - #[test] - fn shell_link_set_get_show_cmd_round_trips_via_classic_com_vtable() -> result::Result<()> { - let shell_link = shell_link()?.as_object().unwrap(); - let table = MetadataTable::new(); - let iface = shell_link_signature(&table); - - iface.methods[15].call_dynamic(shell_link.as_raw(), &[WinRTValue::I32(3)])?; - let result = iface.methods[14].call_dynamic(shell_link.as_raw(), &[])?; - - assert_eq!(result[0].as_i32().unwrap(), 3); - Ok(()) - } - - #[test] - fn shell_link_set_get_hotkey_round_trips_u16() -> result::Result<()> { - let shell_link = shell_link()?.as_object().unwrap(); - let table = MetadataTable::new(); - let iface = shell_link_signature(&table); - - iface.methods[13].call_dynamic(shell_link.as_raw(), &[WinRTValue::U16(0x0141)])?; - let result = iface.methods[12].call_dynamic(shell_link.as_raw(), &[])?; - - assert_eq!(result[0].as_i32().unwrap() as u16, 0x0141); - Ok(()) - } - - #[test] - fn shell_link_set_get_description_round_trips_wide_string() -> result::Result<()> { - let shell_link = shell_link()?.as_object().unwrap(); - let expected = "dynwinrt classic COM"; - let wide = wide_null(expected); - - call_method_1_ptr(7, shell_link.as_raw(), wide.as_ptr() as *const c_void)?; - - let mut buffer = wide_buffer(128); - call_method_2_ptr_i32( - 6, - shell_link.as_raw(), - buffer.as_mut_ptr() as *mut c_void, - buffer.len() as i32, - )?; - - assert_eq!(wide_to_string(&buffer), expected); - Ok(()) - } - - #[test] - fn adopt_com_pointer_accepts_addref_owned_pointer() -> result::Result<()> { - let shell_link = shell_link()?.as_object().unwrap(); - let shell_link_raw = shell_link.as_raw(); - let borrowed = unsafe { IUnknown::from_raw_borrowed(&shell_link_raw) }.unwrap(); - let addref_owned = borrowed.clone(); - let raw = addref_owned.as_raw(); - std::mem::forget(addref_owned); - - let adopted = unsafe { adopt_com_pointer(raw) }; - let adopted = adopted.as_object().expect("adopted value must be Object"); - let table = MetadataTable::new(); - let iface = shell_link_signature(&table); - - iface.methods[15].call_dynamic(adopted.as_raw(), &[WinRTValue::I32(7)])?; - let result = iface.methods[14].call_dynamic(adopted.as_raw(), &[])?; - - assert_eq!(result[0].as_i32().unwrap(), 7); - Ok(()) - } - - #[test] - fn co_create_instance_with_bogus_clsid_returns_error() -> result::Result<()> { - let bogus = GUID::from_u128(0xaaaaaaaa_bbbb_cccc_dddd_eeeeeeeeeeee); - - let err = co_create_instance(bogus, IID_ISHELL_LINK_W).unwrap_err(); - match err { - result::Error::WindowsError(err) => assert_eq!(err.code(), REGDB_E_CLASSNOTREG), - err => panic!("expected REGDB_E_CLASSNOTREG, got {err:?}"), - } - Ok(()) - } - - #[test] - fn query_interface_with_unsupported_iid_returns_error() -> result::Result<()> { - let shell_link = shell_link()?; - let bogus = GUID::from_u128(0xbbbbbbbb_cccc_dddd_eeee_ffffffffffff); - - let err = shell_link.cast(&bogus).unwrap_err(); - match err { - result::Error::WindowsError(err) => assert_eq!(err.code(), E_NOINTERFACE), - err => panic!("expected E_NOINTERFACE, got {err:?}"), - } - Ok(()) - } - - #[test] - fn data_transfer_manager_interop_get_for_window_returns_winrt_object_via_dynamic_iunknown_vtable() - -> result::Result<()> { - ensure_com_initialized()?; - - let hwnd = unsafe { - CreateWindowExW( - WINDOW_EX_STYLE(0), - w!("STATIC"), - w!("dynwinrt data transfer interop test"), - WS_OVERLAPPED, - 0, - 0, - 1, - 1, - None, - None, - None, - None, - ) - } - .map_err(result::Error::WindowsError)?; - struct WindowGuard(windows::Win32::Foundation::HWND); - impl Drop for WindowGuard { - fn drop(&mut self) { - let _ = unsafe { DestroyWindow(self.0) }; - } - } - let _window = WindowGuard(hwnd); - - let factory = ro_get_activation_factory_2(&HSTRING::from( - "Windows.ApplicationModel.DataTransfer.DataTransferManager", - ))?; - let interop = query_interface(factory, &IDataTransferManagerInterop::IID) - .map_err(result::Error::WindowsError)? - .as_object() - .unwrap(); - - let table = MetadataTable::new(); - let mut iface = InterfaceSignature::define_from_iunknown( - "IDataTransferManagerInterop", - IDataTransferManagerInterop::IID, - &table, - ); - iface.add_method( - MethodSignature::new(&table) - .add_in(table.object()) - .add_in(table.object()) - .add_out(table.object()), - ); - - let target_iid = DataTransferManager::IID; - let result = iface.methods[3].call_dynamic( - interop.as_raw(), - &[ - WinRTValue::RawPtr(hwnd.0 as *mut c_void), - WinRTValue::RawPtr(&target_iid as *const GUID as *mut c_void), - ], - )?; - - let manager = result[0].as_object().expect("GetForWindow returned null"); - assert!(!manager.as_raw().is_null()); - let _typed: DataTransferManager = manager.cast().map_err(result::Error::WindowsError)?; - Ok(()) - } -} diff --git a/crates/dynwinrt/src/com.rs b/crates/dynwinrt/src/com.rs new file mode 100644 index 00000000..6467a7ca --- /dev/null +++ b/crates/dynwinrt/src/com.rs @@ -0,0 +1,639 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use core::ffi::c_void; +use std::cell::RefCell; + +use windows::Win32::System::Com::{ + CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, COINIT_MULTITHREADED, CoCreateInstance, + CoInitializeEx, CoUninitialize, +}; +use windows_core::{GUID, IUnknown, Interface as WindowsInterface}; + +use crate::{ + MetadataTable, MethodHandle, TypeHandle, WinRTValue, result, + signature::{AbiMethodSignature, ParameterType}, +}; + +const RPC_E_CHANGED_MODE: windows_core::HRESULT = windows_core::HRESULT(0x80010106u32 as i32); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InterfaceBase { + IUnknown, + IInspectable, +} + +impl InterfaceBase { + pub const fn first_method_slot(self) -> usize { + match self { + Self::IUnknown => 3, + Self::IInspectable => 6, + } + } +} + +#[derive(Debug, Clone)] +pub struct Type(ParameterType); + +impl Type { + pub fn winrt(typ: TypeHandle) -> Self { + Self(ParameterType::winrt(typ)) + } + + pub fn pointer() -> Self { + Self(ParameterType::pointer()) + } +} + +#[derive(Debug, Clone)] +pub struct MethodSignature(AbiMethodSignature); + +impl MethodSignature { + pub fn new(table: &std::sync::Arc) -> Self { + Self(AbiMethodSignature::new(table)) + } + + pub fn add_in(self, typ: Type) -> Self { + Self(self.0.add_in_type(typ.0)) + } + + pub fn add_out(self, typ: Type) -> Self { + Self(self.0.add_out_type(typ.0)) + } + + pub fn add_in_out(self, typ: Type) -> Self { + Self(self.0.add_in_out_type(typ.0)) + } + + pub fn add_out_fill(self, typ: Type) -> Self { + Self(self.0.add_out_fill_type(typ.0)) + } + + pub fn returns(self, typ: Type) -> Self { + Self(self.0.returns_type(typ.0)) + } + + pub fn returns_void(self) -> Self { + Self(self.0.returns_void()) + } +} + +#[derive(Debug, Clone)] +pub struct Interface(TypeHandle); + +impl Interface { + pub fn add_method(self, name: &str, signature: MethodSignature) -> Self { + self.0 + .clone() + .add_method(name, crate::MethodSignature::from_abi(signature.0)); + self + } + + pub fn method(&self, vtable_index: usize) -> Option { + self.0.method(vtable_index) + } +} + +pub fn register_interface( + table: &std::sync::Arc, + name: &str, + iid: GUID, + base: InterfaceBase, +) -> Interface { + Interface(table.register_com_interface(name, iid, base.first_method_slot())) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ApartmentType { + SingleThreaded, + MultiThreaded, +} + +impl ApartmentType { + fn as_flag(self) -> windows::Win32::System::Com::COINIT { + match self { + Self::SingleThreaded => COINIT_APARTMENTTHREADED, + Self::MultiThreaded => COINIT_MULTITHREADED, + } + } +} + +struct ComApartment { + apartment_type: ApartmentType, +} + +impl Drop for ComApartment { + fn drop(&mut self) { + unsafe { CoUninitialize() }; + } +} + +enum ComInitialization { + Uninitialized, + Owned(ComApartment), +} + +thread_local! { + static COM_INITIALIZATION: RefCell = + const { RefCell::new(ComInitialization::Uninitialized) }; +} + +pub fn initialize_apartment(apartment_type: ApartmentType) -> result::Result<()> { + COM_INITIALIZATION.with(|state| { + if let ComInitialization::Owned(existing) = &*state.borrow() { + return if existing.apartment_type == apartment_type { + Ok(()) + } else { + Err(result::Error::WindowsError( + windows_core::Error::from_hresult(RPC_E_CHANGED_MODE), + )) + }; + } + + let hr = unsafe { CoInitializeEx(None, apartment_type.as_flag()) }; + if hr.is_ok() { + *state.borrow_mut() = ComInitialization::Owned(ComApartment { apartment_type }); + Ok(()) + } else { + Err(result::Error::WindowsError( + windows_core::Error::from_hresult(hr), + )) + } + }) +} + +pub fn co_create_instance(clsid: GUID, iid: GUID) -> result::Result { + let unknown: IUnknown = unsafe { CoCreateInstance(&clsid, None, CLSCTX_INPROC_SERVER) } + .map_err(result::Error::WindowsError)?; + let mut result = std::ptr::null_mut(); + unsafe { unknown.query(&iid, &mut result) } + .ok() + .map_err(result::Error::WindowsError)?; + Ok(WinRTValue::Object(unsafe { IUnknown::from_raw(result) })) +} + +/// Adopt an AddRef-owned COM interface pointer into a managed Object value. +/// +/// The pointer must represent a caller-owned COM reference (+1). This function +/// takes ownership with `IUnknown::from_raw` and must not be used for borrowed +/// pointers. +pub unsafe fn adopt_com_pointer(ptr: *mut c_void) -> WinRTValue { + if ptr.is_null() { + WinRTValue::Null + } else { + WinRTValue::Object(unsafe { IUnknown::from_raw(ptr) }) + } +} + +pub fn call_method( + vtable_index: usize, + obj: *mut c_void, + signature: MethodSignature, + args: &[WinRTValue], +) -> result::Result> { + signature + .0 + .build(vtable_index) + .call_dynamic(obj, args) + .map_err(result::Error::WindowsError) +} + +#[cfg(test)] +fn call_method_1_ptr( + vtable_index: usize, + obj: *mut c_void, + ptr: *const c_void, +) -> result::Result<()> { + crate::call::call_winrt_method_1(vtable_index, obj, ptr) + .ok() + .map_err(result::Error::WindowsError) +} + +#[cfg(test)] +fn call_method_2_ptr_i32( + vtable_index: usize, + obj: *mut c_void, + ptr: *mut c_void, + value: i32, +) -> result::Result<()> { + crate::call::call_winrt_method_2(vtable_index, obj, ptr, value) + .ok() + .map_err(result::Error::WindowsError) +} + +#[cfg(test)] +fn wide_null(text: &str) -> Vec { + text.encode_utf16().chain(std::iter::once(0)).collect() +} + +#[cfg(test)] +fn wide_buffer(characters: usize) -> Vec { + vec![0; characters] +} + +#[cfg(test)] +fn wide_to_string(buffer: &[u16]) -> String { + let end = buffer + .iter() + .position(|ch| *ch == 0) + .unwrap_or(buffer.len()); + String::from_utf16_lossy(&buffer[..end]) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + InterfaceSignature, MetadataTable, com_helpers::E_NOINTERFACE, ro_get_activation_factory_2, + roapi::query_interface, + }; + use std::sync::atomic::{AtomicU32, Ordering}; + use windows::{ + ApplicationModel::DataTransfer::DataTransferManager, + Win32::{ + UI::Shell::IDataTransferManagerInterop, + UI::WindowsAndMessaging::{ + CreateWindowExW, DestroyWindow, WINDOW_EX_STYLE, WS_OVERLAPPED, + }, + }, + }; + use windows_core::{HSTRING, Interface, w}; + + #[repr(C)] + struct FakeComObject { + vtable: *const *mut c_void, + } + + unsafe extern "system" fn return_u32(_this: *mut c_void) -> u32 { + u32::MAX + } + + static VOID_CALLS: AtomicU32 = AtomicU32::new(0); + + unsafe extern "system" fn return_void(_this: *mut c_void) { + VOID_CALLS.fetch_add(1, Ordering::Relaxed); + } + + unsafe extern "system" fn increment_i32( + _this: *mut c_void, + value: *mut i32, + ) -> windows_core::HRESULT { + unsafe { *value += 1 }; + windows_core::HRESULT(0) + } + + unsafe extern "system" fn write_native_pointer( + _this: *mut c_void, + value: *mut *mut c_void, + ) -> windows_core::HRESULT { + unsafe { *value = 0x1234usize as *mut c_void }; + windows_core::HRESULT(0) + } + + unsafe extern "system" fn write_guid_and_i32( + _this: *mut c_void, + guid: *mut GUID, + value: *mut i32, + ) -> windows_core::HRESULT { + unsafe { + *guid = GUID::from_u128(0x11111111_2222_3333_4444_555555555555); + *value = 42; + } + windows_core::HRESULT(0) + } + + #[test] + fn direct_native_return_is_not_interpreted_as_hresult() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table).returns(Type::winrt(table.u32_type())); + let vtable = [return_u32 as *mut c_void]; + let mut object = FakeComObject { + vtable: vtable.as_ptr(), + }; + + let values = call_method( + 0, + (&mut object as *mut FakeComObject).cast(), + signature, + &[], + ) + .expect("u32::MAX is a value, not a failed HRESULT"); + + assert!(matches!(values.as_slice(), [WinRTValue::U32(u32::MAX)])); + } + + #[test] + fn native_void_return_does_not_read_hresult_register() { + VOID_CALLS.store(0, Ordering::Relaxed); + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table).returns_void(); + let vtable = [return_void as *mut c_void]; + let mut object = FakeComObject { + vtable: vtable.as_ptr(), + }; + + let values = call_method( + 0, + (&mut object as *mut FakeComObject).cast(), + signature, + &[], + ) + .unwrap(); + + assert!(values.is_empty()); + assert_eq!(VOID_CALLS.load(Ordering::Relaxed), 1); + } + + #[test] + fn in_out_parameter_preserves_input_and_returns_updated_value() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table).add_in_out(Type::winrt(table.i32_type())); + let vtable = [increment_i32 as *mut c_void]; + let mut object = FakeComObject { + vtable: vtable.as_ptr(), + }; + + let values = call_method( + 0, + (&mut object as *mut FakeComObject).cast(), + signature, + &[WinRTValue::I32(41)], + ) + .unwrap(); + + assert!(matches!(values.as_slice(), [WinRTValue::I32(42)])); + } + + #[test] + fn native_pointer_out_is_not_adopted_as_com_object() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table).add_out(Type::pointer()); + let vtable = [write_native_pointer as *mut c_void]; + let mut object = FakeComObject { + vtable: vtable.as_ptr(), + }; + + let values = call_method( + 0, + (&mut object as *mut FakeComObject).cast(), + signature, + &[], + ) + .unwrap(); + + assert!(matches!( + values.as_slice(), + [WinRTValue::RawPtr(ptr)] if *ptr == 0x1234usize as *mut c_void + )); + } + + #[test] + fn multi_output_guid_uses_full_sized_storage() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table) + .add_out(Type::winrt(table.guid_type())) + .add_out(Type::winrt(table.i32_type())); + let vtable = [write_guid_and_i32 as *mut c_void]; + let mut object = FakeComObject { + vtable: vtable.as_ptr(), + }; + + let values = call_method( + 0, + (&mut object as *mut FakeComObject).cast(), + signature, + &[], + ) + .unwrap(); + + assert!(matches!( + values.as_slice(), + [WinRTValue::Guid(guid), WinRTValue::I32(42)] + if *guid == GUID::from_u128(0x11111111_2222_3333_4444_555555555555) + )); + } + + const CLSID_SHELL_LINK: GUID = GUID::from_u128(0x00021401_0000_0000_c000_000000000046); + const IID_ISHELL_LINK_W: GUID = GUID::from_u128(0x000214f9_0000_0000_c000_000000000046); + const REGDB_E_CLASSNOTREG: windows_core::HRESULT = windows_core::HRESULT(0x80040154u32 as i32); + + fn shell_link() -> result::Result { + initialize_apartment(ApartmentType::MultiThreaded)?; + co_create_instance(CLSID_SHELL_LINK, IID_ISHELL_LINK_W) + } + + #[test] + #[should_panic(expected = "already registered with a different type or IID")] + fn interface_names_cannot_alias_different_iids() { + let table = MetadataTable::new(); + register_interface( + &table, + "Windows.Win32.Example.IThing", + GUID::from_u128(1), + InterfaceBase::IUnknown, + ); + register_interface( + &table, + "Windows.Win32.Example.IThing", + GUID::from_u128(2), + InterfaceBase::IUnknown, + ); + } + + fn shell_link_signature(table: &std::sync::Arc) -> InterfaceSignature { + let mut iface = + InterfaceSignature::define_from_iunknown("IShellLinkW", IID_ISHELL_LINK_W, table); + iface + .add_method(crate::MethodSignature::new(table)) // 3 GetPath + .add_method(crate::MethodSignature::new(table)) // 4 GetIDList + .add_method(crate::MethodSignature::new(table)) // 5 SetIDList + .add_method(crate::MethodSignature::new(table)) // 6 GetDescription + .add_method(crate::MethodSignature::new(table)) // 7 SetDescription + .add_method(crate::MethodSignature::new(table)) // 8 GetWorkingDirectory + .add_method(crate::MethodSignature::new(table)) // 9 SetWorkingDirectory + .add_method(crate::MethodSignature::new(table)) // 10 GetArguments + .add_method(crate::MethodSignature::new(table)) // 11 SetArguments + .add_method(crate::MethodSignature::new(table).add_out(table.u16_type())) // 12 GetHotkey + .add_method(crate::MethodSignature::new(table).add_in(table.u16_type())) // 13 SetHotkey + .add_method(crate::MethodSignature::new(table).add_out(table.i32_type())) // 14 GetShowCmd + .add_method(crate::MethodSignature::new(table).add_in(table.i32_type())); // 15 SetShowCmd + iface + } + + #[test] + fn shell_link_set_get_show_cmd_round_trips_via_classic_com_vtable() -> result::Result<()> { + let shell_link = shell_link()?.as_object().unwrap(); + let table = MetadataTable::new(); + let iface = shell_link_signature(&table); + + iface.methods[15].call_dynamic(shell_link.as_raw(), &[WinRTValue::I32(3)])?; + let result = iface.methods[14].call_dynamic(shell_link.as_raw(), &[])?; + + assert_eq!(result[0].as_i32().unwrap(), 3); + Ok(()) + } + + #[test] + fn shell_link_set_get_hotkey_round_trips_u16() -> result::Result<()> { + let shell_link = shell_link()?.as_object().unwrap(); + let table = MetadataTable::new(); + let iface = shell_link_signature(&table); + + iface.methods[13].call_dynamic(shell_link.as_raw(), &[WinRTValue::U16(0x0141)])?; + let result = iface.methods[12].call_dynamic(shell_link.as_raw(), &[])?; + + assert_eq!(result[0].as_i32().unwrap() as u16, 0x0141); + Ok(()) + } + + #[test] + fn shell_link_set_get_description_round_trips_wide_string() -> result::Result<()> { + let shell_link = shell_link()?.as_object().unwrap(); + let expected = "dynwinrt classic COM"; + let wide = wide_null(expected); + + call_method_1_ptr(7, shell_link.as_raw(), wide.as_ptr() as *const c_void)?; + + let mut buffer = wide_buffer(128); + call_method_2_ptr_i32( + 6, + shell_link.as_raw(), + buffer.as_mut_ptr() as *mut c_void, + buffer.len() as i32, + )?; + + assert_eq!(wide_to_string(&buffer), expected); + Ok(()) + } + + #[test] + fn adopt_com_pointer_accepts_addref_owned_pointer() -> result::Result<()> { + let shell_link = shell_link()?.as_object().unwrap(); + let shell_link_raw = shell_link.as_raw(); + let borrowed = unsafe { IUnknown::from_raw_borrowed(&shell_link_raw) }.unwrap(); + let addref_owned = borrowed.clone(); + let raw = addref_owned.as_raw(); + std::mem::forget(addref_owned); + + let adopted = unsafe { adopt_com_pointer(raw) }; + let adopted = adopted.as_object().expect("adopted value must be Object"); + let table = MetadataTable::new(); + let iface = shell_link_signature(&table); + + iface.methods[15].call_dynamic(adopted.as_raw(), &[WinRTValue::I32(7)])?; + let result = iface.methods[14].call_dynamic(adopted.as_raw(), &[])?; + + assert_eq!(result[0].as_i32().unwrap(), 7); + Ok(()) + } + + #[test] + fn co_create_instance_with_bogus_clsid_returns_error() -> result::Result<()> { + initialize_apartment(ApartmentType::MultiThreaded)?; + let bogus = GUID::from_u128(0xaaaaaaaa_bbbb_cccc_dddd_eeeeeeeeeeee); + + let err = co_create_instance(bogus, IID_ISHELL_LINK_W).unwrap_err(); + match err { + result::Error::WindowsError(err) => assert_eq!(err.code(), REGDB_E_CLASSNOTREG), + err => panic!("expected REGDB_E_CLASSNOTREG, got {err:?}"), + } + Ok(()) + } + + #[test] + fn co_create_instance_does_not_choose_an_apartment_implicitly() { + let remains_uninitialized = std::thread::spawn(|| { + let _ = co_create_instance( + GUID::from_u128(0xaaaaaaaa_bbbb_cccc_dddd_eeeeeeeeeeee), + IID_ISHELL_LINK_W, + ); + COM_INITIALIZATION + .with(|state| matches!(*state.borrow(), ComInitialization::Uninitialized)) + }) + .join() + .unwrap(); + + assert!(remains_uninitialized); + } + + #[test] + fn query_interface_with_unsupported_iid_returns_error() -> result::Result<()> { + let shell_link = shell_link()?; + let bogus = GUID::from_u128(0xbbbbbbbb_cccc_dddd_eeee_ffffffffffff); + + let err = shell_link.cast(&bogus).unwrap_err(); + match err { + result::Error::WindowsError(err) => assert_eq!(err.code(), E_NOINTERFACE), + err => panic!("expected E_NOINTERFACE, got {err:?}"), + } + Ok(()) + } + + #[test] + fn data_transfer_manager_interop_get_for_window_returns_winrt_object_via_dynamic_iunknown_vtable() + -> result::Result<()> { + initialize_apartment(ApartmentType::MultiThreaded)?; + + let hwnd = unsafe { + CreateWindowExW( + WINDOW_EX_STYLE(0), + w!("STATIC"), + w!("dynwinrt data transfer interop test"), + WS_OVERLAPPED, + 0, + 0, + 1, + 1, + None, + None, + None, + None, + ) + } + .map_err(result::Error::WindowsError)?; + struct WindowGuard(windows::Win32::Foundation::HWND); + impl Drop for WindowGuard { + fn drop(&mut self) { + let _ = unsafe { DestroyWindow(self.0) }; + } + } + let _window = WindowGuard(hwnd); + + let factory = ro_get_activation_factory_2(&HSTRING::from( + "Windows.ApplicationModel.DataTransfer.DataTransferManager", + ))?; + let interop = query_interface(factory, &IDataTransferManagerInterop::IID) + .map_err(result::Error::WindowsError)? + .as_object() + .unwrap(); + + let table = MetadataTable::new(); + let iface = register_interface( + &table, + "IDataTransferManagerInterop", + IDataTransferManagerInterop::IID, + InterfaceBase::IUnknown, + ) + .add_method( + "GetForWindow", + MethodSignature::new(&table) + .add_in(Type::pointer()) + .add_in(Type::pointer()) + .add_out(Type::winrt(table.object())), + ); + + let target_iid = DataTransferManager::IID; + let result = iface.method(3).unwrap().invoke( + interop.as_raw(), + &[ + WinRTValue::RawPtr(hwnd.0 as *mut c_void), + WinRTValue::RawPtr(&target_iid as *const GUID as *mut c_void), + ], + )?; + + let manager = result[0].as_object().expect("GetForWindow returned null"); + assert!(!manager.as_raw().is_null()); + let _typed: DataTransferManager = manager.cast().map_err(result::Error::WindowsError)?; + Ok(()) + } +} diff --git a/crates/dynwinrt/src/lib.rs b/crates/dynwinrt/src/lib.rs index c62984bd..9b6ca384 100644 --- a/crates/dynwinrt/src/lib.rs +++ b/crates/dynwinrt/src/lib.rs @@ -5,7 +5,7 @@ use windows::core::*; mod abi; mod call; -pub mod classic_com; +pub mod com; mod interfaces; mod result; mod roapi; diff --git a/crates/dynwinrt/src/metadata_table/arena.rs b/crates/dynwinrt/src/metadata_table/arena.rs index ed5c5362..0ca41c9d 100644 --- a/crates/dynwinrt/src/metadata_table/arena.rs +++ b/crates/dynwinrt/src/metadata_table/arena.rs @@ -40,8 +40,6 @@ pub(super) struct EnumData { pub(super) struct InterfaceMethodTable { pub(super) method_names: Vec, pub(super) method_indices: Vec, - /// First user-method vtable slot for this interface. - /// 6 for IInspectable-based (WinRT) interfaces, 3 for IUnknown-based (classic COM). pub(super) base_slot: usize, } @@ -116,30 +114,19 @@ impl MetadataTable { // ----------------------------------------------------------------------- /// Create an interface method table. Called only when dedup already checked by caller. - pub(super) fn create_interface_method_table(&self, iid: GUID) { - self.create_interface_method_table_with_base(iid, 6); - } - - /// Create an interface method table with a specific base vtable slot. - /// 6 = IInspectable-based (WinRT), 3 = IUnknown-based (classic COM). - /// - /// If a method table for this IID already exists, its `base_slot` MUST - /// match `base_slot`; otherwise subsequent method registrations for the + /// If a method table for this IID already exists, its base MUST match; + /// otherwise subsequent method registrations for the /// IID would compute wrong vtable indices for one of the callers. - /// Failing loudly is safer than silently keeping the first-registered - /// base slot (as `or_insert_with` would). - pub(super) fn create_interface_method_table_with_base(&self, iid: GUID, base_slot: usize) { + pub(super) fn create_interface_method_table(&self, iid: GUID, base_slot: usize) { + assert!(matches!(base_slot, 3 | 6)); let mut tables = self.interface_methods.write().unwrap(); match tables.entry(iid) { std::collections::hash_map::Entry::Occupied(existing) => { let existing_base = existing.get().base_slot; assert_eq!( existing_base, base_slot, - "interface IID {:?} registered twice with conflicting base slots \ - (existing={}, new={}). This would silently produce wrong vtable \ - indices; each IID must be registered with a single base_slot \ - (3 for IUnknown-based classic COM, 6 for IInspectable/WinRT).", - iid, existing_base, base_slot, + "interface IID {iid:?} registered twice with conflicting bases \ + (existing={existing_base}, new={base_slot})" ); } std::collections::hash_map::Entry::Vacant(v) => { diff --git a/crates/dynwinrt/src/metadata_table/mod.rs b/crates/dynwinrt/src/metadata_table/mod.rs index 596c085b..6e1175dc 100644 --- a/crates/dynwinrt/src/metadata_table/mod.rs +++ b/crates/dynwinrt/src/metadata_table/mod.rs @@ -232,34 +232,31 @@ impl MetadataTable { /// Register a named interface. Creates an IID → method table. /// Returns a TypeHandle for chaining `.add_method()`. pub fn register_interface(self: &Arc, name: &str, iid: GUID) -> TypeHandle { - // See `register_interface_iunknown` for the rationale: always route - // through the assertive method-table creator so a stale registration - // with a mismatched base_slot fails loudly instead of silently - // returning the wrong vtable. - self.create_interface_method_table(iid); if let Some(kind) = self.get_named_type(name) { return self.make(kind); } + self.create_interface_method_table(iid, 6); let kind = TypeKind::Interface(iid); self.insert_named_type(name, kind); self.make(kind) } - /// Register a named IUnknown-based (classic COM) interface. User methods - /// start at vtable slot 3 (QI/AddRef/Release occupy 0/1/2), rather than the - /// WinRT default of 6 (IInspectable adds three more slots at 3/4/5). - pub fn register_interface_iunknown(self: &Arc, name: &str, iid: GUID) -> TypeHandle { - // Even if the name already resolves to a TypeKind, still route through - // `create_interface_method_table_with_base(iid, 3)`. That call is - // idempotent when the IID's method table already exists with the same - // base_slot, and panics loudly (see arena.rs:131) if a prior - // `register_interface` created it with base_slot=6. This closes the - // window where callers would otherwise silently reuse a WinRT-shaped - // vtable for classic-COM dispatch and get wrong absolute slots. - self.create_interface_method_table_with_base(iid, 3); + pub(crate) fn register_com_interface( + self: &Arc, + name: &str, + iid: GUID, + base_slot: usize, + ) -> TypeHandle { if let Some(kind) = self.get_named_type(name) { + assert_eq!( + kind, + TypeKind::Interface(iid), + "type name {name:?} is already registered with a different type or IID" + ); + self.create_interface_method_table(iid, base_slot); return self.make(kind); } + self.create_interface_method_table(iid, base_slot); let kind = TypeKind::Interface(iid); self.insert_named_type(name, kind); self.make(kind) diff --git a/crates/dynwinrt/src/metadata_table/type_handle.rs b/crates/dynwinrt/src/metadata_table/type_handle.rs index e8eaba83..e4bb6238 100644 --- a/crates/dynwinrt/src/metadata_table/type_handle.rs +++ b/crates/dynwinrt/src/metadata_table/type_handle.rs @@ -322,7 +322,13 @@ impl TypeHandle { TypeKind::Object | TypeKind::Interface(_) | TypeKind::Delegate(_) - | TypeKind::RuntimeClass(_) => Ok(WinRTValue::Object(IUnknown::from_raw(ptr))), + | TypeKind::RuntimeClass(_) => { + if ptr.is_null() { + Ok(WinRTValue::Null) + } else { + Ok(WinRTValue::Object(IUnknown::from_raw(ptr))) + } + } TypeKind::HString => Ok(WinRTValue::HString(std::mem::transmute(ptr))), @@ -331,6 +337,9 @@ impl TypeHandle { ))), TypeKind::Parameterized(idx) => { + if ptr.is_null() { + return Ok(WinRTValue::Null); + } let (generic_def, args) = self.table.get_parameterized(idx); if is_async_piid(generic_def) { let raw = IUnknown::from_raw(ptr); @@ -345,6 +354,9 @@ impl TypeHandle { | TypeKind::IAsyncActionWithProgress(_) | TypeKind::IAsyncOperation(_) | TypeKind::IAsyncOperationWithProgress(_) => { + if ptr.is_null() { + return Ok(WinRTValue::Null); + } let raw = IUnknown::from_raw(ptr); let info: windows_future::IAsyncInfo = raw .cast() @@ -388,7 +400,13 @@ impl TypeHandle { | TypeKind::Delegate(_) | TypeKind::RuntimeClass(_), AbiValue::Pointer(p), - ) => Ok(WinRTValue::Object(unsafe { IUnknown::from_raw(*p) })), + ) => { + if p.is_null() { + Ok(WinRTValue::Null) + } else { + Ok(WinRTValue::Object(unsafe { IUnknown::from_raw(*p) })) + } + } (TypeKind::HString, AbiValue::Pointer(p)) => { Ok(WinRTValue::HString(unsafe { core::mem::transmute(*p) })) @@ -399,6 +417,9 @@ impl TypeHandle { } (TypeKind::Parameterized(idx), AbiValue::Pointer(p)) => { + if p.is_null() { + return Ok(WinRTValue::Null); + } let (generic_def, args) = self.table.get_parameterized(idx); if is_async_piid(generic_def) { let raw = unsafe { IUnknown::from_raw(*p) }; diff --git a/crates/dynwinrt/src/signature.rs b/crates/dynwinrt/src/signature.rs index 5f91026d..69e637cb 100644 --- a/crates/dynwinrt/src/signature.rs +++ b/crates/dynwinrt/src/signature.rs @@ -6,17 +6,164 @@ use std::sync::Arc; use windows::core::{GUID, HSTRING, IInspectable, Interface}; use crate::{ + abi::{AbiType, AbiValue}, call, call::ArgumentList, metadata_table::{MetadataTable, TypeHandle, TypeKind}, value::WinRTValue, }; +#[derive(Debug, Clone)] +pub(crate) enum ParameterType { + WinRT(TypeHandle), + Pointer, +} + +impl ParameterType { + pub(crate) fn winrt(typ: TypeHandle) -> Self { + Self::WinRT(typ) + } + + pub(crate) fn pointer() -> Self { + Self::Pointer + } + + pub(crate) fn as_winrt(&self) -> Option<&TypeHandle> { + match self { + Self::WinRT(typ) => Some(typ), + Self::Pointer => None, + } + } + + pub(crate) fn is_array(&self) -> bool { + self.as_winrt().is_some_and(TypeHandle::is_array) + } + + pub(crate) fn is_struct(&self) -> bool { + matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::Struct(_))) + } + + pub(crate) fn is_hstring(&self) -> bool { + matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::HString)) + } + + pub(crate) fn is_u32(&self) -> bool { + matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::U32)) + } + + pub(crate) fn is_guid(&self) -> bool { + matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::Guid)) + } + + pub(crate) fn supports_in_out(&self) -> bool { + matches!(self, Self::Pointer) + || matches!( + self, + Self::WinRT(typ) + if matches!( + typ.kind(), + TypeKind::Bool + | TypeKind::I8 + | TypeKind::U8 + | TypeKind::I16 + | TypeKind::U16 + | TypeKind::Char16 + | TypeKind::I32 + | TypeKind::U32 + | TypeKind::I64 + | TypeKind::U64 + | TypeKind::F32 + | TypeKind::F64 + | TypeKind::HResult + | TypeKind::Enum(_) + | TypeKind::Struct(_) + ) + ) + } + + pub(crate) fn supports_direct_return(&self) -> bool { + matches!(self, Self::Pointer) + || matches!( + self, + Self::WinRT(typ) + if matches!( + typ.kind(), + TypeKind::Bool + | TypeKind::I8 + | TypeKind::U8 + | TypeKind::I16 + | TypeKind::U16 + | TypeKind::Char16 + | TypeKind::I32 + | TypeKind::U32 + | TypeKind::I64 + | TypeKind::U64 + | TypeKind::F32 + | TypeKind::F64 + | TypeKind::HResult + | TypeKind::Enum(_) + ) + ) + } + + pub(crate) fn abi_type(&self) -> AbiType { + match self { + Self::WinRT(typ) => typ.abi_type(), + Self::Pointer => AbiType::Ptr, + } + } + + pub(crate) fn libffi_type(&self) -> libffi::middle::Type { + match self { + Self::WinRT(typ) => typ.libffi_type(), + Self::Pointer => libffi::middle::Type::pointer(), + } + } + + pub(crate) fn array_element_type(&self) -> TypeHandle { + self.as_winrt() + .expect("native pointer is not an array") + .array_element_type() + } + + pub(crate) fn default_struct_value(&self) -> crate::metadata_table::ValueTypeData { + self.as_winrt() + .expect("native pointer is not a struct") + .default_value() + } + + pub(crate) fn default_value(&self) -> WinRTValue { + match self { + Self::WinRT(typ) => typ.default_winrt_value(), + Self::Pointer => WinRTValue::RawPtr(std::ptr::null_mut()), + } + } + + pub(crate) fn from_out(&self, ptr: *mut std::ffi::c_void) -> crate::result::Result { + match self { + Self::WinRT(typ) => typ.from_out(ptr), + Self::Pointer => Ok(WinRTValue::RawPtr(ptr)), + } + } + + pub(crate) fn from_out_value(&self, value: &AbiValue) -> crate::result::Result { + match (self, value) { + (Self::WinRT(typ), value) => typ.from_out_value(value), + (Self::Pointer, AbiValue::Pointer(ptr)) => Ok(WinRTValue::RawPtr(*ptr)), + (Self::Pointer, value) => Err(crate::result::Error::InvalidTypeAbiToWinRT( + TypeKind::Object, + value.abi_type(), + )), + } + } +} + /// How a parameter is passed at the ABI level. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ParamKind { In, Out, + InOut, /// FillArray: caller allocates buffer, callee fills it. /// ABI expands to 2 params: (u32 capacity, T* items). OutFillArray, @@ -24,7 +171,7 @@ pub enum ParamKind { #[derive(Debug, Clone)] pub struct Parameter { - pub typ: TypeHandle, + pub(crate) typ: ParameterType, /// Index in the method result vector for out and FillArray parameters. pub value_index: usize, /// Index in the caller-provided argument slice. FillArray parameters have @@ -34,8 +181,19 @@ pub struct Parameter { } impl Parameter { + pub fn is_input(&self) -> bool { + matches!(self.kind, ParamKind::In | ParamKind::InOut) + } + pub fn is_out(&self) -> bool { - matches!(self.kind, ParamKind::Out | ParamKind::OutFillArray) + matches!( + self.kind, + ParamKind::Out | ParamKind::InOut | ParamKind::OutFillArray + ) + } + + pub fn is_in_out(&self) -> bool { + self.kind == ParamKind::InOut } pub fn is_fill_array(&self) -> bool { @@ -44,34 +202,47 @@ impl Parameter { } #[derive(Debug, Clone)] -pub struct MethodSignature { +pub(crate) struct AbiMethodSignature { out_count: usize, input_count: usize, parameters: Vec, - return_type: TypeHandle, + return_kind: MethodReturn, #[allow(dead_code)] is_opaque: bool, #[allow(dead_code)] table: Arc, } -impl MethodSignature { - pub fn new(table: &Arc) -> Self { - MethodSignature { +#[derive(Debug, Clone)] +pub(crate) enum MethodReturn { + HResult, + Void, + Value(ParameterType), +} + +impl MethodReturn { + fn libffi_type(&self) -> libffi::middle::Type { + match self { + Self::HResult => libffi::middle::Type::i32(), + Self::Void => libffi::middle::Type::void(), + Self::Value(typ) => typ.libffi_type(), + } + } +} + +impl AbiMethodSignature { + pub(crate) fn new(table: &Arc) -> Self { + AbiMethodSignature { out_count: 0, input_count: 0, parameters: Vec::new(), - return_type: table.hresult(), + return_kind: MethodReturn::HResult, is_opaque: false, table: Arc::clone(table), } } - pub fn new_with_registry(table: &Arc) -> Self { - Self::new(table) - } - - pub fn add_in(mut self, typ: TypeHandle) -> Self { + pub(crate) fn add_in_type(mut self, typ: ParameterType) -> Self { let input_index = self.input_count; self.input_count += 1; self.parameters.push(Parameter { @@ -83,7 +254,7 @@ impl MethodSignature { self } - pub fn add_out(mut self, typ: TypeHandle) -> Self { + pub(crate) fn add_out_type(mut self, typ: ParameterType) -> Self { self.parameters.push(Parameter { kind: ParamKind::Out, typ, @@ -94,9 +265,24 @@ impl MethodSignature { self } - /// Add a FillArray out parameter: caller allocates buffer, callee fills it. - /// ABI expands to (u32 capacity, T* items). - pub fn add_out_fill(mut self, typ: TypeHandle) -> Self { + pub(crate) fn add_in_out_type(mut self, typ: ParameterType) -> Self { + assert!( + typ.supports_in_out(), + "in/out currently supports native scalars, pointers, enums, and structs" + ); + let input_index = self.input_count; + self.input_count += 1; + self.parameters.push(Parameter { + kind: ParamKind::InOut, + typ, + value_index: self.out_count, + input_index: Some(input_index), + }); + self.out_count += 1; + self + } + + pub(crate) fn add_out_fill_type(mut self, typ: ParameterType) -> Self { let input_index = self.input_count; self.input_count += 1; self.parameters.push(Parameter { @@ -109,7 +295,21 @@ impl MethodSignature { self } - pub fn build(self, index: usize) -> Method { + pub(crate) fn returns_type(mut self, typ: ParameterType) -> Self { + assert!( + typ.supports_direct_return(), + "direct native returns currently support scalars, enums, and pointers" + ); + self.return_kind = MethodReturn::Value(typ); + self + } + + pub(crate) fn returns_void(mut self) -> Self { + self.return_kind = MethodReturn::Void; + self + } + + pub(crate) fn build(self, index: usize) -> Method { use libffi::middle::Type; let mut types: Vec = Vec::with_capacity(self.parameters.len() + 1); types.push(Type::pointer()); // com object's this pointer @@ -134,22 +334,23 @@ impl MethodSignature { types.push(param.typ.libffi_type()); } } - let in_count = self.parameters.len() - self.out_count; - let has_complex_param = self.parameters.iter().any(|p| { - p.typ.is_array() || p.is_fill_array() || matches!(p.typ.kind(), TypeKind::Struct(_)) - }); + let in_count = self.parameters.iter().filter(|p| p.is_input()).count(); + let has_complex_param = self + .parameters + .iter() + .any(|p| p.typ.is_array() || p.is_fill_array() || p.is_in_out() || p.typ.is_struct()); // Check if the single in-param (if any) is a simple non-HString, non-Struct type let simple_in = !has_complex_param && in_count == 1 && { - let in_param = self.parameters.iter().find(|p| !p.is_out()).unwrap(); - !matches!(in_param.typ.kind(), TypeKind::HString) + let in_param = self.parameters.iter().find(|p| p.is_input()).unwrap(); + !in_param.typ.is_hstring() }; // Classify array parameters let array_in_count = self .parameters .iter() - .filter(|p| !p.is_out() && p.typ.is_array()) + .filter(|p| p.is_input() && p.typ.is_array()) .count(); let fill_out_count = self.parameters.iter().filter(|p| p.is_fill_array()).count(); let array_out_count = self @@ -160,16 +361,22 @@ impl MethodSignature { let scalar_in_count = in_count - array_in_count; let scalar_out_count = self.out_count - fill_out_count - array_out_count; - let strategy = if !has_complex_param && in_count == 0 && self.out_count == 1 { + let returns_hresult = matches!(self.return_kind, MethodReturn::HResult); + let strategy = if returns_hresult + && !has_complex_param + && in_count == 0 + && self.out_count == 1 + { CallStrategy::Direct0In1Out - } else if !has_complex_param && in_count == 0 && self.out_count == 0 { + } else if returns_hresult && !has_complex_param && in_count == 0 && self.out_count == 0 { CallStrategy::Direct0In0Out - } else if simple_in && self.out_count == 0 { + } else if returns_hresult && simple_in && self.out_count == 0 { CallStrategy::Direct1In0Out - } else if simple_in && self.out_count == 1 { + } else if returns_hresult && simple_in && self.out_count == 1 { CallStrategy::Direct1In1Out // ReceiveArray only: fn(this, *mut u32, *mut *mut c_void) -> HRESULT - } else if scalar_in_count == 0 + } else if returns_hresult + && scalar_in_count == 0 && array_in_count == 0 && array_out_count == 1 && fill_out_count == 0 @@ -177,7 +384,8 @@ impl MethodSignature { { CallStrategy::DirectReceiveArray // PassArray + 1 out: fn(this, u32, *const u8, out) -> HRESULT - } else if scalar_in_count == 0 + } else if returns_hresult + && scalar_in_count == 0 && array_in_count == 1 && array_out_count == 0 && fill_out_count == 0 @@ -185,7 +393,8 @@ impl MethodSignature { { CallStrategy::DirectPassArray1Out // FillArray only: fn(this, u32, *mut u8, *mut u32) -> HRESULT - } else if scalar_in_count == 0 + } else if returns_hresult + && scalar_in_count == 0 && array_in_count == 0 && fill_out_count == 1 && array_out_count == 0 @@ -193,7 +402,8 @@ impl MethodSignature { { CallStrategy::DirectFillArray // 1 scalar in + FillArray: fn(this, val, u32, *mut u8, *mut u32) -> HRESULT - } else if scalar_in_count == 1 + } else if returns_hresult + && scalar_in_count == 1 && array_in_count == 0 && fill_out_count == 1 && array_out_count == 0 @@ -202,21 +412,15 @@ impl MethodSignature { let in_param = self .parameters .iter() - .find(|p| !p.is_out() && !p.typ.is_array()) + .find(|p| p.is_input() && !p.typ.is_array()) .unwrap(); - if !matches!(in_param.typ.kind(), TypeKind::HString | TypeKind::Struct(_)) { + if !in_param.typ.is_hstring() && !in_param.typ.is_struct() { CallStrategy::Direct1InFillArray } else { - CallStrategy::Libffi(Cif::new( - types.into_iter(), - self.return_type.abi_type().libffi_type(), - )) + CallStrategy::Libffi(Cif::new(types.into_iter(), self.return_kind.libffi_type())) } } else { - CallStrategy::Libffi(Cif::new( - types.into_iter(), - self.return_type.abi_type().libffi_type(), - )) + CallStrategy::Libffi(Cif::new(types.into_iter(), self.return_kind.libffi_type())) }; Method { @@ -224,17 +428,52 @@ impl MethodSignature { index, parameters: self.parameters, out_count: self.out_count, + return_kind: self.return_kind, }, strategy, } } } +#[derive(Debug, Clone)] +pub struct MethodSignature(AbiMethodSignature); + +impl MethodSignature { + pub(crate) fn from_abi(signature: AbiMethodSignature) -> Self { + Self(signature) + } + + pub fn new(table: &Arc) -> Self { + Self(AbiMethodSignature::new(table)) + } + + pub fn new_with_registry(table: &Arc) -> Self { + Self::new(table) + } + + pub fn add_in(self, typ: TypeHandle) -> Self { + Self(self.0.add_in_type(ParameterType::winrt(typ))) + } + + pub fn add_out(self, typ: TypeHandle) -> Self { + Self(self.0.add_out_type(ParameterType::winrt(typ))) + } + + pub fn add_out_fill(self, typ: TypeHandle) -> Self { + Self(self.0.add_out_fill_type(ParameterType::winrt(typ))) + } + + pub fn build(self, index: usize) -> Method { + self.0.build(index) + } +} + #[derive(Debug)] pub struct MethodInfo { pub index: usize, pub parameters: Vec, pub out_count: usize, + pub(crate) return_kind: MethodReturn, } /// How a Method should be invoked — decided once at build time. @@ -294,29 +533,15 @@ fn coerce_input_object( if value.is_null_object() { return Ok(None); } - // A `WinRTValue::RawPtr` is a raw ABI pointer supplied by the caller - // (e.g. `DynWinRtValue.pointer(hwnd)`). It is legitimate ONLY when the - // parameter is untyped `TypeKind::Object` (the codegen's `pointer()` - // alias, used for HWND / PWSTR / void* / function-pointer slots). - // - // For a TYPED interface / delegate / runtime class / async parameter - // the runtime would otherwise blindly forward the caller's pointer bits - // into the vtable dispatch, without QI'ing to the required IID. If - // the pointer wasn't actually a live COM object with the expected - // vtable layout the dispatch would read a bogus vtable → crash / UB. - // Reject up-front with E_INVALIDARG so callers pass a real `Object` - // (or an explicitly `.cast()`-ed one) instead. + // Raw pointers never satisfy a WinRT object parameter. Otherwise arbitrary + // pointer bits could reach a typed COM slot without QueryInterface validation. if matches!(value, WinRTValue::RawPtr(_)) { - if matches!(expected.kind(), TypeKind::Object) { - return Ok(None); - } return Err(windows_core::Error::new( windows_core::HRESULT(0x80070057u32 as i32), &format!( "Refusing to pass a raw pointer as a typed COM parameter ({}). \ - Only untyped Object / void* / handle parameters accept \ - DynWinRtValue.pointer(...); for a concrete interface pass a \ - real object (or one obtained via `.cast(IID)`).", + Use a Pointer signature for native pointers and handles; for \ + COM parameters pass a real object (or one obtained via `.cast(IID)`).", expected.signature_string(), ), )); @@ -485,15 +710,20 @@ impl Method { args: &[WinRTValue], ) -> windows_core::Result> { let mut args = InvocationArgs::new(args); - for parameter in self.info.parameters.iter().filter(|p| !p.is_out()) { - let value = args.get_value(parameter.value_index); - let coerced = if parameter.typ.is_array() { - coerce_input_array(¶meter.typ, value)? + for parameter in self.info.parameters.iter().filter(|p| p.is_input()) { + let input_index = parameter.input_index.expect("input parameter index"); + let value = args.get_value(input_index); + let coerced = if let Some(typ) = parameter.typ.as_winrt() { + if typ.is_array() { + coerce_input_array(typ, value)? + } else { + coerce_input_object(typ, value)? + } } else { - coerce_input_object(¶meter.typ, value)? + None }; if let Some(value) = coerced { - args.replace(parameter.value_index, value); + args.replace(input_index, value); } } @@ -507,7 +737,7 @@ impl Method { CallStrategy::Direct0In1Out => { // 0 in + 1 out: fn(this, out) -> HRESULT let param = &self.info.parameters[0]; - let mut out = param.typ.default_winrt_value(); + let mut out = param.typ.default_value(); let hr = call::call_winrt_method_1(self.info.index, obj, out.out_ptr()); hr.ok()?; // COM pointer types use RawPtr(null) as buffer to avoid IUnknown::from_raw(null) UB. @@ -529,7 +759,7 @@ impl Method { CallStrategy::Direct1In1Out => { // 1 in + 1 out: fn(this, val, out) -> HRESULT let out_param = self.info.parameters.iter().find(|p| p.is_out()).unwrap(); - let mut out = out_param.typ.default_winrt_value(); + let mut out = out_param.typ.default_value(); let hr = call::call_1in_1out(self.info.index, obj, args.get_value(0), out.out_ptr()); hr.ok()?; @@ -584,11 +814,11 @@ impl Method { } CallStrategy::DirectPassArray1Out => { // fn(this, u32, *const u8, out) -> HRESULT - let in_param = self.info.parameters.iter().find(|p| !p.is_out()).unwrap(); + let in_param = self.info.parameters.iter().find(|p| p.is_input()).unwrap(); let out_param = self.info.parameters.iter().find(|p| p.is_out()).unwrap(); let array_data = args.get_value(in_param.value_index).as_array().unwrap(); let buffer = array_data.serialize_for_abi(); - let mut out = out_param.typ.default_winrt_value(); + let mut out = out_param.typ.default_value(); let fptr = call::get_vtable_function_ptr(obj, self.info.index); let hr: windows_core::HRESULT = unsafe { let method: unsafe extern "system" fn( @@ -662,7 +892,7 @@ impl Method { } CallStrategy::Direct1InFillArray => { // fn(this, val, u32, *mut u8) -> HRESULT - let in_param = self.info.parameters.iter().find(|p| !p.is_out()).unwrap(); + let in_param = self.info.parameters.iter().find(|p| p.is_input()).unwrap(); let fill_param = self .info .parameters @@ -704,12 +934,13 @@ impl Method { ); Ok(vec![WinRTValue::Array(array)]) } - CallStrategy::Libffi(cif) => call::call_winrt_method_dynamic( + CallStrategy::Libffi(cif) => call::call_method_dynamic( self.info.index, obj, &self.info.parameters, &args, self.info.out_count, + &self.info.return_kind, cif, ), } @@ -825,36 +1056,16 @@ mod tests { Ok(()) } - /// Regression: `coerce_input_object` must accept `WinRTValue::RawPtr` - /// ONLY when the expected parameter type is the untyped - /// `TypeKind::Object` (the codegen's `pointer()` alias used for HWND / - /// void* / handle slots). Passing a raw pointer where a *typed* - /// interface / delegate / runtime class / async parameter is expected - /// must be rejected up-front with `E_INVALIDARG`, so we don't - /// blindly forward pointer bits into a vtable dispatch that would - /// then read a bogus vtable and crash / UB. #[test] - fn raw_pointer_only_accepted_for_untyped_object_params() { + fn raw_pointer_is_rejected_for_winrt_object_params() { let table = MetadataTable::new(); let bogus = WinRTValue::RawPtr(0xDEADBEEF as *mut std::ffi::c_void); - // Legitimate case: `TypeKind::Object` (a.k.a. codegen's `pointer()` / - // HWND / void*) accepts RawPtr — bypass coercion so the raw ABI - // pointer is forwarded to the callee unchanged. let object_ty = table.object(); - assert!( - matches!(object_ty.kind(), TypeKind::Object), - "sanity: table.object() must be TypeKind::Object" - ); - assert!( - coerce_input_object(&object_ty, &bogus) - .expect("RawPtr into TypeKind::Object must be allowed") - .is_none(), - "RawPtr into TypeKind::Object should bypass coercion (Ok(None))", - ); + let object_err = coerce_input_object(&object_ty, &bogus) + .expect_err("RawPtr into Object must be rejected"); + assert_eq!(object_err.code().0, 0x80070057u32 as i32); - // Unsafe case: RawPtr into a typed `TypeKind::Interface(IID)` - // must FAIL with E_INVALIDARG, not silently succeed. let iface_ty = table.interface(IStringable::IID); let err = coerce_input_object(&iface_ty, &bogus) .expect_err("RawPtr into a typed interface must be rejected"); @@ -871,8 +1082,6 @@ mod tests { msg ); - // Null objects remain allowed for both untyped and typed slots - // (a null pointer is a valid COM null-object). let null_object = WinRTValue::Null; assert!( coerce_input_object(&object_ty, &null_object) diff --git a/tools/dynwinrt-codegen/src/codegen/com/mod.rs b/tools/dynwinrt-codegen/src/codegen/com/mod.rs new file mode 100644 index 00000000..920ee7c2 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/mod.rs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Classic-COM metadata projection and JavaScript generation. + +mod naming; +mod projection; +mod render; +mod type_mapping; + +pub use render::{ComGeneratedOutput, generate_com_interface_files}; diff --git a/tools/dynwinrt-codegen/src/codegen/com/naming.rs b/tools/dynwinrt-codegen/src/codegen/com/naming.rs new file mode 100644 index 00000000..fa38ad79 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/naming.rs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub(super) fn camel_case(name: &str) -> String { + if name.is_empty() { + return String::new(); + } + let chars: Vec = name.chars().collect(); + let mut run = 0usize; + while run < chars.len() && chars[run].is_ascii_uppercase() { + run += 1; + } + let mut result = String::with_capacity(name.len()); + if run == 0 { + return name.to_string(); + } + if run == chars.len() { + for c in &chars { + result.push(c.to_ascii_lowercase()); + } + return result; + } + if run == 1 { + result.push(chars[0].to_ascii_lowercase()); + for c in &chars[1..] { + result.push(*c); + } + return result; + } + for c in &chars[..run - 1] { + result.push(c.to_ascii_lowercase()); + } + for c in &chars[run - 1..] { + result.push(*c); + } + result +} + +pub(super) fn js_param_name(raw: &str, index: usize) -> String { + let base = if raw.is_empty() { + format!("arg{}", index) + } else { + raw.to_string() + }; + let stripped = strip_hungarian(&base); + let mut out = String::with_capacity(stripped.len()); + let mut chars = stripped.chars(); + if let Some(first) = chars.next() { + out.push(first.to_ascii_lowercase()); + } + for c in chars { + out.push(c); + } + match out.as_str() { + "class" | "return" | "function" | "default" | "this" | "new" | "delete" | "let" + | "const" | "var" | "if" | "else" | "for" | "while" | "do" | "switch" | "case" + | "break" | "continue" | "true" | "false" | "null" | "undefined" | "in" | "of" + | "typeof" | "instanceof" | "throw" | "try" | "catch" | "finally" | "yield" | "async" + | "await" | "with" | "void" | "public" | "private" | "protected" | "package" | "static" + | "import" | "export" | "extends" | "super" | "arguments" => { + format!("{}_", out) + } + _ => out, + } +} + +pub(super) fn strip_hungarian(s: &str) -> &str { + let prefixes = [ + "lpwsz", "pwsz", "lpsz", "psz", "lpsz", "pwstr", "pcwstr", "hwnd", "dw", "sz", "cb", "cx", + "cy", "cw", "ch", "cn", "cc", "lp", "np", "ph", "pd", "pf", "pv", "ppv", "pp", "wsz", + ]; + for p in prefixes { + if let Some(rest) = s.strip_prefix(p) { + if rest + .chars() + .next() + .map(|c| c.is_ascii_uppercase()) + .unwrap_or(false) + { + return rest; + } + } + } + s +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/projection.rs b/tools/dynwinrt-codegen/src/codegen/com/projection.rs new file mode 100644 index 00000000..bad5e2e1 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/projection.rs @@ -0,0 +1,224 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::com_metadata::{ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; +use crate::types::TypeMeta; + +use super::naming::camel_case; +use super::type_mapping::is_hresult; + +#[derive(Debug, Clone)] +pub(super) struct InteropMethod { + pub(super) camel: String, + pub(super) vtable_index: usize, + pub(super) natural_params: Option>, + pub(super) plain: Option, + pub(super) _doc: Option, +} + +#[derive(Debug, Clone)] +pub(super) struct InteropInfo { + pub(super) methods: Vec, + pub(super) class_name: String, + pub(super) class_namespace: String, + pub(super) target_iid: String, +} + +pub(super) fn method_is_interop_shape(m: &MethodMeta) -> Option> { + match &m.return_type { + Some(t) if is_hresult(t) => {} + _ => return None, + } + if m.params.len() < 2 { + return None; + } + let last_idx = m.params.len() - 1; + let out_param = &m.params[last_idx]; + if out_param.direction != ParamDirection::Out || !matches!(out_param.typ, TypeMeta::Object) { + return None; + } + if m.params[..last_idx] + .iter() + .any(|param| param.direction != ParamDirection::In) + { + return None; + } + let riid = &m.params[last_idx - 1]; + let is_riid = match &riid.typ { + TypeMeta::Guid => true, + TypeMeta::Object => { + let name = riid.name.to_ascii_lowercase(); + name == "riid" || name == "iid" + } + _ => false, + }; + is_riid.then(|| m.params[..last_idx - 1].to_vec()) +} + +pub(super) fn detect_interop( + meta: &ComInterfaceMeta, + winmd_paths: &str, +) -> Result, String> { + let iface = &meta.interface; + if !iface.name.ends_with("Interop") || iface.methods.is_empty() { + return Ok(None); + } + + let mut has_interop_method = false; + let methods = iface + .methods + .iter() + .map(|method| match method_is_interop_shape(method) { + Some(natural_params) if method.name == "GetForWindow" => { + has_interop_method = true; + InteropMethod { + camel: camel_case(&method.name), + vtable_index: method.vtable_index, + natural_params: Some(natural_params), + plain: None, + _doc: method.doc.clone(), + } + } + _ => InteropMethod { + camel: camel_case(&method.name), + vtable_index: method.vtable_index, + natural_params: None, + plain: Some(method.clone()), + _doc: method.doc.clone(), + }, + }) + .collect(); + if !has_interop_method { + return Ok(None); + } + + let stripped_i = iface.name.strip_prefix('I').unwrap_or(&iface.name); + let class_name = stripped_i + .strip_suffix("Interop") + .unwrap_or(stripped_i) + .to_string(); + let (class_namespace, target_iid) = match resolve_projected_default_iid( + winmd_paths, + &class_name, + ) { + Some((namespace, _interface_name, iid)) => (namespace, iid), + None => { + return Err(format!( + "Classic-COM interop generator: cannot resolve default IID for the projected \ + WinRT runtime class `{class_name}` (derived from `{}`). \ + Neither the winmds passed to the generator ({winmd_paths:?}) nor the newest installed \ + `C:\\Program Files (x86)\\Windows Kits\\10\\UnionMetadata\\\\Windows.winmd` \ + contains a WinRT runtime class of that name with a resolvable default interface. \ + Pass the correct Windows.winmd via --ref or install a recent Windows SDK.", + iface.name + )); + } + }; + + Ok(Some(InteropInfo { + methods, + class_name, + class_namespace, + target_iid, + })) +} + +fn resolve_projected_default_iid( + winmd_paths: &str, + simple_class_name: &str, +) -> Option<(String, String, String)> { + if !winmd_paths.is_empty() { + if let Some(result) = + crate::com_metadata::find_runtime_class_default_iid(winmd_paths, simple_class_name) + { + return Some(result); + } + } + let sdk_winmd = crate::com_metadata::discover_newest_windows_winmd()?; + if winmd_paths + .split(';') + .any(|path| path.eq_ignore_ascii_case(&sdk_winmd)) + { + return None; + } + crate::com_metadata::find_runtime_class_default_iid(&sdk_winmd, simple_class_name) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn interop_shape_rejects_non_refiid_trailing_object() { + let method = MethodMeta { + params: vec![ + ParamMeta { + name: "value".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "result".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HRESULT".into(), + fields: Vec::new(), + }), + ..Default::default() + }; + + assert!(method_is_interop_shape(&method).is_none()); + } + + #[test] + fn non_get_for_window_interop_keeps_caller_iid() { + let method = MethodMeta { + name: "CreateSessionForWindow".into(), + params: vec![ + ParamMeta { + name: "window".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "result".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HRESULT".into(), + fields: Vec::new(), + }), + ..Default::default() + }; + let meta = ComInterfaceMeta { + interface: crate::com_metadata::InterfaceMeta { + name: "IUserActivityInterop".into(), + namespace: "Windows.Win32.System.WinRT".into(), + iid: "00000000-0000-0000-0000-000000000000".into(), + methods: vec![method], + ..Default::default() + }, + base_offset: 3, + is_iunknown_rooted: true, + base_chain: vec!["IUnknown".into()], + coclass_clsid: None, + coclass_name: None, + own_methods_start: 3, + referenced_enums: Vec::new(), + }; + + assert!(detect_interop(&meta, "").unwrap().is_none()); + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/com.rs b/tools/dynwinrt-codegen/src/codegen/com/render.rs similarity index 52% rename from tools/dynwinrt-codegen/src/codegen/com.rs rename to tools/dynwinrt-codegen/src/codegen/com/render.rs index d93c4bdc..9416f84c 100644 --- a/tools/dynwinrt-codegen/src/codegen/com.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/render.rs @@ -11,7 +11,7 @@ //! projection) so mixing them into the existing IR would obscure both paths. //! //! What we emit today (phase 1): -//! - `.js`: registration via `DynWinRtType.registerInterfaceUnknown` +//! - `.js`: registration via `DynCom.registerIUnknownInterface` //! + a natural class with camelCase methods and static `create()` / //! `_fromNative()`. //! - `.d.ts`: PascalCase class, camelCase methods, opaque @@ -19,11 +19,24 @@ //! projected to `void` (throwing on failure via the runtime). //! - Per-enum sibling files for each enum referenced by any method parameter. -use std::collections::BTreeSet; - -use crate::meta::{ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; +use crate::com_metadata::{ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; use crate::types::TypeMeta; +#[cfg(test)] +use super::naming::strip_hungarian; +use super::naming::{camel_case, js_param_name}; +use super::projection::method_is_interop_shape; +use super::projection::{InteropInfo, InteropMethod, detect_interop}; +#[cfg(test)] +use super::type_mapping::handle_type_name; +use super::type_mapping::{ + MethodResult, StringEncoding, collect_handle_aliases, dts_params_for_method, dts_return_type, + enum_import_names, has_string_buffer_method, is_cotaskmem_owned, is_hresult, + is_optional_find_data_out_after_string_count, method_results, string_buffer_pattern, + ts_type_expr_dts, ts_type_expr_js, unwrap_return_js, uses_winrt_bridge_value, validate_com_abi, + wrap_arg_js, +}; + /// A rendered classic-COM output: primary `.js` + `.d.ts` for the interface, /// plus zero or more sibling files (one `.js` + `.d.ts` per referenced enum). #[derive(Debug, Clone, PartialEq, Eq)] @@ -53,12 +66,12 @@ pub fn generate_com_interface_files( meta: &ComInterfaceMeta, winmd_paths: &str, ) -> Result { + validate_com_abi(meta)?; + validate_untyped_outputs(meta)?; + // Detect whether this is a `*Interop` interface whose every method has the - // `(HWND, [HSTRING…,] REFIID, out void**)` GetForWindow shape. When so, we - // emit natural signatures that hide the REFIID + void** — the caller only - // supplies the natural in-params, and the wrapper returns the projected - // WinRT object. We also emit a companion runtime-class file that provides - // the ergonomic `.getForWindow(hwnd)` static surface. + // `(HWND, [HSTRING…,] REFIID, out void**)` GetForWindow shape. Natural + // signatures hide the REFIID + void** and return an explicit bridge value. let interop = detect_interop(meta, winmd_paths)?; let js = render_js(meta, interop.as_ref()); @@ -74,17 +87,6 @@ pub fn generate_com_interface_files( } } - // Companion projected-class files: only when the interop resolved to a - // real WinRT runtime class. This emits a natural `.js`/.d.ts - // with a static `getForWindow(hwnd)` and a `.runtimeClassName` getter, - // giving the E2E a MEANINGFUL surface to exercise on the returned object. - if let Some(ref info) = interop { - if let Some((cjs, cdts)) = render_projected_class_files(meta, info) { - extra_files.push((format!("{}.js", info.class_name), cjs)); - extra_files.push((format!("{}.d.ts", info.class_name), cdts)); - } - } - extra_files.sort_by(|a, b| a.0.cmp(&b.0)); Ok(ComGeneratedOutput { @@ -94,241 +96,6 @@ pub fn generate_com_interface_files( }) } -// --------------------------------------------------------------------------- -// Interop detection -// --------------------------------------------------------------------------- - -/// Metadata for a single method within a `*Interop` interface. Each method -/// is EITHER interop-shaped (`riid + void**` trailing pair to hide) OR plain -/// (no special handling — HWND setter etc.). -#[derive(Debug, Clone)] -struct InteropMethod { - /// Original method name (PascalCase, e.g. "GetForWindow"). - name: String, - /// camelCase method name for JS/TS emission. - camel: String, - /// Absolute vtable slot. - vtable_index: usize, - /// `Some(natural_params)` when the method has the interop shape (last two - /// ABI params are `(REFIID, out void**)`), i.e. the surface should hide - /// them. `None` means "plain" — emit like a normal classic-COM method. - natural_params: Option>, - /// For plain methods, the underlying `MethodMeta` so we can reuse the - /// existing emission path. - plain: Option, - /// Underlying method's docstring, if any. - _doc: Option, -} - -/// Interop-level metadata for the whole interface. -#[derive(Debug, Clone)] -struct InteropInfo { - /// Every method — some tagged interop-shape, some plain. - methods: Vec, - /// The projected WinRT runtime-class name (derived from the interop - /// interface: `ISystemMediaTransportControlsInterop` → - /// `SystemMediaTransportControls`). - class_name: String, - /// Full namespace of the projected runtime class in the WinRT metadata - /// (e.g. `"Windows.Media"`). Empty when auto-resolution failed. - class_namespace: String, - /// Default interface IID of the projected runtime class, used as the - /// REFIID in the interop call. Empty when auto-resolution failed. - target_iid: String, -} - -/// Recognise an interop method: last two ABI parameters are -/// `(In: REFIID /* Guid* */, Out: Object /* void** */)`, HRESULT return. -/// -/// The trailing in-param is treated as a hidden REFIID **only when we're -/// confident it's actually one** — either its metadata type projects to -/// `TypeMeta::Guid` (System.Guid) OR its parameter name (case-insensitive) -/// is exactly `riid` / `iid`. A method whose last in-param is a real -/// application-level Object (a live COM interface pointer) MUST NOT be -/// interpreted as interop-shaped, since dropping that argument would silently -/// break the wrapper. See Fix 3 in the accompanying code-review notes. -fn method_is_interop_shape(m: &MethodMeta) -> Option> { - // Must return HRESULT - match &m.return_type { - Some(t) if is_hresult(t) => {} - _ => return None, - } - // Enforce the exact structural shape in the ORIGINAL parameter order: - // [in]... [in REFIID] [out void**] - // i.e. every param except the last is [in], the last is the sole [out], - // and the second-to-last [in] is the REFIID. Filtering into direction - // buckets would have lost this ordering and could misclassify methods - // where the [out] param appears mid-signature or where the REFIID is - // not at the tail of the in-list. - if m.params.len() < 2 { - return None; - } - let last_idx = m.params.len() - 1; - let out_param = &m.params[last_idx]; - if out_param.direction != ParamDirection::Out { - return None; - } - if !matches!(out_param.typ, TypeMeta::Object) { - return None; - } - // All preceding params must be [in]. - for p in &m.params[..last_idx] { - if p.direction != ParamDirection::In { - return None; - } - } - // The last of those [in] params is the REFIID. - let riid = &m.params[last_idx - 1]; - let is_riid = match &riid.typ { - TypeMeta::Guid => true, - TypeMeta::Object => { - let n = riid.name.to_ascii_lowercase(); - n == "riid" || n == "iid" - } - _ => false, - }; - if !is_riid { - return None; - } - // Natural params: every [in] EXCEPT the trailing REFIID, preserving - // original order. - let natural: Vec = m.params[..last_idx - 1].iter().cloned().collect(); - Some(natural) -} - -/// Best-effort detection: an interface qualifies as an "interop" iff -/// (a) its name ends with `"Interop"`, and -/// (b) at least ONE method matches the interop shape. -/// -/// Any interop-shape methods get natural signatures (hide riid + void**); -/// the rest fall back to the normal classic-COM emission. -/// -/// Returns: -/// - `Ok(None)` — not an interop interface. -/// - `Ok(Some(info))` — an interop interface with a resolved target IID. -/// - `Err(msg)` — an interop interface was detected but the projected WinRT -/// runtime class's default IID could not be resolved from either the -/// passed winmds or the newest installed Windows SDK. This is a hard -/// failure by design: silently emitting a NULL riid would produce a -/// generated wrapper that fails only at runtime, on a machine the -/// developer may not have. -fn detect_interop( - meta: &ComInterfaceMeta, - winmd_paths: &str, -) -> Result, String> { - let iface = &meta.interface; - if !iface.name.ends_with("Interop") { - return Ok(None); - } - if iface.methods.is_empty() { - return Ok(None); - } - let mut methods = Vec::with_capacity(iface.methods.len()); - let mut has_interop_method = false; - for m in &iface.methods { - match method_is_interop_shape(m) { - Some(natural) => { - has_interop_method = true; - methods.push(InteropMethod { - name: m.name.clone(), - camel: camel_case(&m.name), - vtable_index: m.vtable_index, - natural_params: Some(natural), - plain: None, - _doc: m.doc.clone(), - }); - } - None => { - methods.push(InteropMethod { - name: m.name.clone(), - camel: camel_case(&m.name), - vtable_index: m.vtable_index, - natural_params: None, - plain: Some(m.clone()), - _doc: m.doc.clone(), - }); - } - } - } - if !has_interop_method { - return Ok(None); - } - - // Derive the WinRT runtime-class simple name from the interop name: - // strip leading `I` and trailing `Interop`. - let stripped_i = iface.name.strip_prefix('I').unwrap_or(&iface.name); - let class_name = stripped_i - .strip_suffix("Interop") - .unwrap_or(stripped_i) - .to_string(); - - // Auto-resolve the projected class's default interface IID. Try the winmds - // the generator was actually given FIRST (portable — respects an integrator - // who pinned a specific SDK via --ref); if that fails, discover the newest - // installed Windows SDK winmd. If BOTH fail, we cannot generate a working - // interop wrapper — fail loudly rather than emit a NULL riid. - let (class_namespace, target_iid) = match resolve_projected_default_iid( - winmd_paths, - &class_name, - ) { - Some((ns, _iface_name, iid)) => (ns, iid), - None => { - return Err(format!( - "Classic-COM interop generator: cannot resolve default IID for the projected \ - WinRT runtime class `{cls}` (derived from `{iface}`). \ - Neither the winmds passed to the generator ({paths:?}) nor the newest installed \ - `C:\\Program Files (x86)\\Windows Kits\\10\\UnionMetadata\\\\Windows.winmd` \ - contains a WinRT runtime class of that name with a resolvable default interface. \ - Pass the correct Windows.winmd via --ref or install a recent Windows SDK.", - cls = class_name, - iface = iface.name, - paths = winmd_paths, - )); - } - }; - - Ok(Some(InteropInfo { - methods, - class_name, - class_namespace, - target_iid, - })) -} - -/// Auto-resolve the target class + IID for interop projection. -/// -/// Consults, in order: -/// 1. The winmd paths currently loaded by the generator (`winmd_paths`). -/// 2. The NEWEST installed `Windows Kits\10\UnionMetadata\\Windows.winmd` -/// (dynamically discovered — NOT pinned to a specific SDK version). -/// -/// Returns `None` when the class cannot be found in either source. -fn resolve_projected_default_iid( - winmd_paths: &str, - simple_class_name: &str, -) -> Option<(String, String, String)> { - // First: try the winmds the generator was given. When integrators pass - // pinned Windows metadata via --ref/--ref-list this preserves reproducibility. - if !winmd_paths.is_empty() { - if let Some(result) = - crate::meta::find_runtime_class_default_iid(winmd_paths, simple_class_name) - { - return Some(result); - } - } - // Fallback: newest installed SDK. This makes the generator portable across - // machines that have any recent SDK installed, not just `10.0.26100.0`. - let sdk_winmd = crate::meta::discover_newest_windows_winmd()?; - // Avoid re-loading if the SDK path was already among the passed winmds. - if winmd_paths - .split(';') - .any(|p| p.eq_ignore_ascii_case(&sdk_winmd)) - { - return None; - } - crate::meta::find_runtime_class_default_iid(&sdk_winmd, simple_class_name) -} - // --------------------------------------------------------------------------- // .js rendering // --------------------------------------------------------------------------- @@ -342,27 +109,18 @@ fn render_js(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { out.push_str("// Generated by dynwinrt-codegen — do not edit\n"); // Imports (runtime + any referenced enums) + let runtime_imports = if interop.is_some() { + "DynCom, DynComMethodSig, DynWinRtValue, WinGuid" + } else { + "DynCom, DynComMethodSig, WinGuid" + }; out.push_str(&format!( - "import {{ DynWinRtType, DynWinRtMethodSig, DynWinRtValue, WinGuid }} from '{}';\n", + "import {{ {runtime_imports} }} from '{}';\n", crate::codegen::project::get_import_name() )); for en in enum_import_names(meta) { out.push_str(&format!("import {{ {} }} from './{}.js';\n", en, en)); } - for iface in returned_interface_import_names(meta) { - if iface != *name { - out.push_str(&format!("import {{ {iface} }} from './{iface}.js';\n")); - } - } - // Interop: import the projected class so we can wrap the returned object. - if let Some(info) = interop { - if !info.target_iid.is_empty() { - out.push_str(&format!( - "import {{ {cls} }} from './{cls}.js';\n", - cls = info.class_name - )); - } - } out.push('\n'); if has_string_buffer_method(meta) { @@ -397,20 +155,18 @@ fn render_js(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { } out.push('\n'); - // Interface registration (lazy). Base-aware: IUnknown-rooted uses - // registerInterfaceUnknown (first user slot = 3); IInspectable-rooted - // uses registerInterface (first user slot = 6). + // Interface registration is base-aware. let register_fn = if meta.is_iunknown_rooted { - "registerInterfaceUnknown" + "registerIUnknownInterface" } else { - "registerInterface" + "registerIInspectableInterface" }; let registration_name = format!("{}.{}", iface.namespace, name); let cache_var = format!("_{name}Cache", name = name); let iface_var = format!("_{name}", name = name); out.push_str(&format!("let {cache_var};\n", cache_var = cache_var)); out.push_str(&format!( - "const {iface_var} = new Proxy({{}}, {{\n get(_target, prop) {{\n {cache_var} ??= DynWinRtType.{register_fn}('{registration_name}', IID_{name})\n", + "const {iface_var} = new Proxy({{}}, {{\n get(_target, prop) {{\n {cache_var} ??= DynCom.{register_fn}('{registration_name}', IID_{name})\n", iface_var = iface_var, cache_var = cache_var, name = name, @@ -452,7 +208,7 @@ fn render_js(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { cc = meta.coclass_name.as_deref().unwrap_or("Coclass") )); out.push_str(&format!( - " static create() {{\n const _obj = DynWinRtValue.coCreateInstance('{clsid}', IID_{name});\n return new {name}(_obj);\n }}\n", + " static create() {{\n const _obj = DynCom.coCreateInstance('{clsid}', IID_{name});\n return new {name}(_obj);\n }}\n", clsid = clsid, name = name, )); @@ -481,26 +237,103 @@ fn render_js(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { } } else { for m in &iface.methods { - emit_method_js(&mut out, m, &iface_var); + if let Some(natural_params) = method_is_interop_shape(m) { + emit_dynamic_iid_method_js(&mut out, m, &natural_params, &iface_var); + } else { + emit_method_js(&mut out, m, &iface_var); + } } } out.push_str("}\n"); out } +fn unwrap_method_result_js( + method: &MethodMeta, + result: MethodResult<'_>, + expression: &str, +) -> String { + if is_cotaskmem_owned(method, result) + && !matches!( + result.typ, + TypeMeta::Struct { namespace, name, .. } + if namespace == "Windows.Win32.Foundation" + && (name == "PWSTR" || name == "PSTR") + ) + { + format!("DynCom.adoptCoTaskMemPointer({expression})") + } else { + unwrap_return_js(result.typ, expression) + } +} + +fn validate_untyped_outputs(meta: &ComInterfaceMeta) -> Result<(), String> { + for method in &meta.interface.methods { + for (param_index, param) in method.params.iter().enumerate() { + let is_untyped = + param.direction == ParamDirection::Out && param.typ == TypeMeta::Object; + let is_owned = method.owned_outputs.iter().any(|owned| { + owned.param_index == param_index && owned.free_with.contains("CoTaskMemFree") + }); + if is_untyped && !is_owned && method_is_interop_shape(method).is_none() { + return Err(format!( + "{}.{}: untyped pointer output has no ownership projection", + meta.interface.name, method.name + )); + } + } + } + Ok(()) +} + +fn emit_dynamic_iid_method_js( + out: &mut String, + method: &MethodMeta, + natural_params: &[ParamMeta], + interface_var: &str, +) { + let mut surface_params = natural_params + .iter() + .enumerate() + .map(|(index, param)| js_param_name(¶m.name, index)) + .collect::>(); + surface_params.push("iid".into()); + let mut args = natural_params + .iter() + .enumerate() + .map(|(index, param)| wrap_arg_js(¶m.typ, &js_param_name(¶m.name, index))) + .collect::>(); + args.push("DynCom.iidPointer(_iid)".into()); + out.push_str(&format!( + " {name}({params}) {{\n", + name = camel_case(&method.name), + params = surface_params.join(", ") + )); + out.push_str(" const _iid = WinGuid.parse(iid);\n"); + out.push_str(&format!( + " const _raw = {interface_var}.method({slot}).invoke(this._obj, [{args}]);\n", + slot = method.vtable_index, + args = args.join(", ") + )); + out.push_str(" return DynCom.adoptComPointer(_raw, _iid);\n"); + out.push_str(" }\n"); +} + fn build_method_sig_js(m: &MethodMeta) -> String { let mut parts = Vec::new(); let string_buffer = string_buffer_pattern(m); for (idx, p) in m.params.iter().enumerate() { if p.direction == ParamDirection::In { parts.push(format!(".addIn({})", ts_type_expr_js(&p.typ))); + } else if p.direction == ParamDirection::InOut { + parts.push(format!(".addInOut({})", ts_type_expr_js(&p.typ))); } else if matches!(p.direction, ParamDirection::OutStringBuffer { .. }) { - parts.push(".addIn(DynWinRtType.pointer())".to_string()); + parts.push(".addIn(DynCom.pointerType())".to_string()); } else if p.direction == ParamDirection::Out { if string_buffer.is_some_and(|(_, count_idx, _)| { idx > count_idx && is_optional_find_data_out_after_string_count(p) }) { - parts.push(".addIn(DynWinRtType.pointer())".to_string()); + parts.push(".addIn(DynCom.pointerType())".to_string()); } else { parts.push(format!(".addOut({})", ts_type_expr_js(&p.typ))); } @@ -508,19 +341,17 @@ fn build_method_sig_js(m: &MethodMeta) -> String { parts.push(format!(".addOutFill({})", ts_type_expr_js(&p.typ))); } } - // Return type of a classic-COM HRESULT method is NOT part of the sig — - // the runtime swallows HRESULT and throws on failure. Only non-HRESULT - // returns are recorded (rare; e.g. IClassFactory::CreateInstance uses - // HRESULT, so most Win32 methods land here). - if let Some(ref rt) = m.return_type { - if !is_hresult(rt) { - parts.push(format!(".addOut({})", ts_type_expr_js(rt))); + match &m.return_type { + None => parts.push(".returnsVoid()".to_string()), + Some(rt) if !is_hresult(rt) => { + parts.push(format!(".returns({})", ts_type_expr_js(rt))); } + _ => {} } if parts.is_empty() { - "new DynWinRtMethodSig()".to_string() + "new DynComMethodSig()".to_string() } else { - format!("new DynWinRtMethodSig(){}", parts.join("")) + format!("new DynComMethodSig(){}", parts.join("")) } } @@ -530,13 +361,9 @@ fn emit_method_js(out: &mut String, m: &MethodMeta, iface_var: &str) { .params .iter() .enumerate() - .filter(|(_, p)| p.direction == ParamDirection::In) - .collect(); - let out_params: Vec<&ParamMeta> = m - .params - .iter() - .filter(|p| p.direction == ParamDirection::Out) + .filter(|(_, p)| p.direction.is_input()) .collect(); + let results = method_results(m); let has_outfill = m .params .iter() @@ -589,15 +416,15 @@ fn emit_method_js(out: &mut String, m: &MethodMeta, iface_var: &str) { .enumerate() .filter_map(|(idx, p)| { if idx == buffer_idx { - Some("DynWinRtValue.pointer(_buffer)".to_string()) - } else if p.direction == ParamDirection::In { + Some("DynCom.pointer(_buffer)".to_string()) + } else if p.direction.is_input() { let surface_idx = in_params .iter() .position(|(param_idx, _)| *param_idx == idx) .expect("input param must have a surface index"); Some(wrap_arg_js(&p.typ, &js_param_name(&p.name, surface_idx))) } else if idx > count_idx && is_optional_find_data_out_after_string_count(p) { - Some("DynWinRtValue.pointer(0n)".to_string()) + Some("DynCom.pointer(0n)".to_string()) } else { None } @@ -609,13 +436,44 @@ fn emit_method_js(out: &mut String, m: &MethodMeta, iface_var: &str) { out.push_str(&format!( " const _buffer = Buffer.alloc({count_name} * 2);\n" )); - out.push_str(&format!( - " {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", - iface_var = iface_var, - slot = m.vtable_index, - args = args.join(", ") - )); - out.push_str(" return _decodeWideString(_buffer);\n"); + match results.len() { + 0 => out.push_str(&format!( + " {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", + iface_var = iface_var, + slot = m.vtable_index, + args = args.join(", ") + )), + 1 => out.push_str(&format!( + " const _out = {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", + iface_var = iface_var, + slot = m.vtable_index, + args = args.join(", ") + )), + _ => out.push_str(&format!( + " const _out = {iface_var}.method({slot}).invokeAll(this._obj, [{args}]);\n", + iface_var = iface_var, + slot = m.vtable_index, + args = args.join(", ") + )), + } + out.push_str(" const _text = _decodeWideString(_buffer);\n"); + match results.len() { + 0 => out.push_str(" return _text;\n"), + 1 => out.push_str(&format!( + " return [_text, {}];\n", + unwrap_method_result_js(m, results[0], "_out") + )), + _ => { + let values = results + .iter() + .enumerate() + .map(|(index, result)| { + unwrap_method_result_js(m, *result, &format!("_out[{index}]")) + }) + .collect::>(); + out.push_str(&format!(" return [_text, {}];\n", values.join(", "))); + } + } out.push_str(" }\n"); return; } @@ -627,7 +485,7 @@ fn emit_method_js(out: &mut String, m: &MethodMeta, iface_var: &str) { if has_outfill { out.push_str(" // TODO: caller-allocated [out, sizeis] buffers are not yet projected as returns.\n"); } - match out_params.len() { + match results.len() { 0 => { out.push_str(&format!( " {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", @@ -643,12 +501,9 @@ fn emit_method_js(out: &mut String, m: &MethodMeta, iface_var: &str) { slot = m.vtable_index, args = args_exprs.join(", ") )); - if matches!(out_params[0].typ, TypeMeta::Object) { - out.push_str(" // TODO: raw COM interface pointer adoption requires preserved pointee metadata.\n"); - } out.push_str(&format!( " return {};\n", - unwrap_return_js(&out_params[0].typ, "_out") + unwrap_method_result_js(m, results[0], "_out") )); } _ => { @@ -658,10 +513,10 @@ fn emit_method_js(out: &mut String, m: &MethodMeta, iface_var: &str) { slot = m.vtable_index, args = args_exprs.join(", ") )); - let items: Vec = out_params + let items: Vec = results .iter() .enumerate() - .map(|(i, p)| unwrap_return_js(&p.typ, &format!("_r[{i}]"))) + .map(|(i, result)| unwrap_method_result_js(m, *result, &format!("_r[{i}]"))) .collect(); out.push_str(&format!(" return [{}];\n", items.join(", "))); } @@ -669,93 +524,6 @@ fn emit_method_js(out: &mut String, m: &MethodMeta, iface_var: &str) { out.push_str(" }\n"); } -/// Unwrap the `DynWinRtValue` result of a method invocation into a natural JS -/// value, according to the `[out]` param's declared type. Mirrors the WinRT -/// codegen's `convert_return` for the primitive/GUID/enum/handle cases; -/// Object/Interface/RuntimeClass currently return the raw `DynWinRtValue` -/// (caller can `.cast(IID)` to bridge to another wrapper). -fn unwrap_return_js(t: &TypeMeta, expr: &str) -> String { - if is_win32_bool(t) { - // Win32 BOOL marshals as i32 at the ABI; project as JS boolean. - return format!("({expr}.toNumber() !== 0)"); - } - if handle_type_name(t).is_some() { - // Opaque Win32 handle (HWND, PWSTR, etc.) → raw pointer as bigint. - // Use `asPointerBigint` (not `toI64`): the runtime may return the - // handle as a `WinRTValue::Object`/`RawPtr`/`Null` when the handle's - // inner `Value` field is a `void*`-shaped type, and `toI64` panics - // on those variants (it falls back to `toNumber`, which explicitly - // panics for non-numeric WinRTValues). `asPointerBigint` cleanly - // handles Object/RawPtr/Null and preserves all 64 pointer bits. - return format!("{expr}.asPointerBigint()"); - } - match t { - TypeMeta::Bool => format!("{expr}.toBool()"), - TypeMeta::I8 - | TypeMeta::U8 - | TypeMeta::I16 - | TypeMeta::U16 - | TypeMeta::I32 - | TypeMeta::U32 - | TypeMeta::Char16 => format!("{expr}.toNumber()"), - TypeMeta::I64 | TypeMeta::U64 => format!("{expr}.toI64()"), - TypeMeta::F32 | TypeMeta::F64 => format!("{expr}.toF64()"), - TypeMeta::Guid => format!("{expr}.toGuid().toString()"), - TypeMeta::Enum { underlying, .. } => unwrap_return_js(underlying, expr), - TypeMeta::String => format!("{expr}.toString()"), - TypeMeta::Interface { name, iid, .. } if !iid.is_empty() => { - format!("{name}._fromNative({expr})") - } - // Object / Interface / RuntimeClass / Struct pointer / etc. - // Return the raw DynWinRtValue and let the caller decide (e.g. cast). - _ => expr.to_string(), - } -} - -/// `.d.ts` return-type text for a classic-COM plain method whose HRESULT is -/// swallowed. Projects `[out]` params as the natural return type: 0 outs → -/// `void`, 1 out → that type, N outs → a tuple. -fn dts_return_type_for_outs(m: &MethodMeta) -> String { - if string_buffer_pattern(m).is_some() { - return "string".to_string(); - } - let out_params: Vec<&ParamMeta> = m - .params - .iter() - .filter(|p| p.direction == ParamDirection::Out) - .collect(); - match out_params.len() { - 0 => "void".to_string(), - 1 => ts_type_expr_dts(&out_params[0].typ), - _ => { - let items: Vec = out_params - .iter() - .map(|p| ts_type_expr_dts(&p.typ)) - .collect(); - format!("[{}]", items.join(", ")) - } - } -} - -fn dts_params_for_method(m: &MethodMeta) -> Vec { - let string_buffer = string_buffer_pattern(m); - m.params - .iter() - .enumerate() - .filter(|(_, p)| p.direction == ParamDirection::In) - .enumerate() - .map(|(surface_i, (idx, p))| { - let mut name = js_param_name(&p.name, surface_i); - if let Some((_, count_idx, _)) = string_buffer { - if idx >= count_idx { - name.push('?'); - } - } - format!("{}: {}", name, ts_type_expr_dts(&p.typ)) - }) - .collect() -} - /// Emit an interop method: either natural (hide trailing REFIID + void**) or /// plain (fall back to the normal classic-COM emission). fn emit_interop_method_js( @@ -765,9 +533,12 @@ fn emit_interop_method_js( info: &InteropInfo, ) { let Some(natural_params) = &im.natural_params else { - // Plain method — reuse the existing pass-through emission. if let Some(m) = &im.plain { - emit_method_js(out, m, iface_var); + if let Some(natural) = method_is_interop_shape(m) { + emit_dynamic_iid_method_js(out, m, &natural, iface_var); + } else { + emit_method_js(out, m, iface_var); + } } return; }; @@ -787,9 +558,9 @@ fn emit_interop_method_js( // pass the cached pointer; otherwise the method is unusable (still emitted // for completeness so `.d.ts` doesn't lie about the surface). let riid_arg = if !info.target_iid.is_empty() { - format!("DynWinRtValue.iidPointer(IID_{}_default)", info.class_name) + format!("DynCom.iidPointer(IID_{}_default)", info.class_name) } else { - "DynWinRtValue.pointer(0n)".to_string() + "DynCom.pointer(0n)".to_string() }; arg_exprs.push(riid_arg); @@ -800,15 +571,16 @@ fn emit_interop_method_js( )); if !info.target_iid.is_empty() { out.push_str(&format!( - " const _out = {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", + " const _raw = {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", iface_var = iface_var, slot = im.vtable_index, args = arg_exprs.join(", "), )); out.push_str(&format!( - " return {cls}._fromNative(_out);\n", + " const _out = DynCom.adoptComPointer(_raw, IID_{cls}_default);\n", cls = info.class_name, )); + out.push_str(" return _out;\n"); } else { // Fallback: no projection available. Return the raw object. out.push_str(&format!( @@ -836,19 +608,15 @@ fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String for en in enum_import_names(meta) { out.push_str(&format!("import {{ {} }} from './{}.js';\n", en, en)); } - for iface in returned_interface_import_names(meta) { - if iface != *name { - out.push_str(&format!("import {{ {iface} }} from './{iface}.js';\n")); - } - } - // Interop: import the projected class declaration so return types resolve. - if let Some(info) = interop { - if !info.target_iid.is_empty() { - out.push_str(&format!( - "import {{ {cls} }} from './{cls}.js';\n", - cls = info.class_name - )); - } + if interop.is_some() + || uses_winrt_bridge_value(meta) + || has_dynamic_iid_method(meta) + || has_owned_pointer_output(meta) + { + out.push_str(&format!( + "import type {{ DynWinRtValue }} from '{}';\n", + crate::codegen::project::get_import_name() + )); } out.push('\n'); @@ -904,11 +672,7 @@ fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String ) }) .collect(); - let ret = if !info.target_iid.is_empty() { - info.class_name.clone() - } else { - "unknown".to_string() - }; + let ret = "DynWinRtValue"; out.push_str(&format!( " {camel}({params}): {ret};\n", camel = im.camel, @@ -918,13 +682,22 @@ fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String } (None, Some(m)) => { let camel = camel_case(&m.name); - let ts_params = dts_params_for_method(m); - let ret = match &m.return_type { - None => "void".to_string(), - // HRESULT is swallowed by the runtime (throw on failure). - // Project `[out]` params as the natural return instead. - Some(t) if is_hresult(t) => dts_return_type_for_outs(m), - Some(t) => ts_type_expr_dts(t), + let (ts_params, ret) = if let Some(natural) = method_is_interop_shape(m) { + let mut params = natural + .iter() + .enumerate() + .map(|(index, param)| { + format!( + "{}: {}", + js_param_name(¶m.name, index), + ts_type_expr_dts(¶m.typ) + ) + }) + .collect::>(); + params.push("iid: string".into()); + (params, "DynWinRtValue".to_string()) + } else { + (dts_params_for_method(m), dts_return_type(m)) }; out.push_str(&format!( " {camel}({params}): {ret};\n", @@ -939,13 +712,22 @@ fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String } else { for m in &iface.methods { let camel = camel_case(&m.name); - let ts_params = dts_params_for_method(m); - let ret = match &m.return_type { - None => "void".to_string(), - // HRESULT is swallowed by the runtime (throw on failure). - // Project `[out]` params as the natural return instead. - Some(t) if is_hresult(t) => dts_return_type_for_outs(m), - Some(t) => ts_type_expr_dts(t), + let (ts_params, ret) = if let Some(natural) = method_is_interop_shape(m) { + let mut params = natural + .iter() + .enumerate() + .map(|(index, param)| { + format!( + "{}: {}", + js_param_name(¶m.name, index), + ts_type_expr_dts(¶m.typ) + ) + }) + .collect::>(); + params.push("iid: string".into()); + (params, "DynWinRtValue".to_string()) + } else { + (dts_params_for_method(m), dts_return_type(m)) }; out.push_str(&format!( " {camel}({params}): {ret};\n", @@ -959,580 +741,61 @@ fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String out } -/// Emit the companion `.js` + `.d.ts` for the projected WinRT -/// runtime class. Provides: -/// - a `static getForWindow(hwnd)` that opens the interop and calls it, -/// returning a natural `` wrapper; -/// - an internal constructor that stores the live COM object; -/// - a `runtimeClassName` getter (via IInspectable::GetRuntimeClassName) — -/// the E2E's proof that the returned object is a live WinRT instance. -fn render_projected_class_files( - meta: &ComInterfaceMeta, - info: &InteropInfo, -) -> Option<(String, String)> { - if info.target_iid.is_empty() || info.class_namespace.is_empty() { - return None; - } - // The interop wrapper file is named after the interface (e.g. - // `IDataTransferManagerInterop.js`). We import from it. - let interop_module = &meta.interface.name; - let full_class_name = format!("{}.{}", info.class_namespace, info.class_name); - - // Pick the primary interop method to expose as the `static getForWindow`. - // Prefer one whose PascalCase name equals "GetForWindow"; otherwise take - // the first interop-shape method. - let primary = info - .methods - .iter() - .find(|im| im.name == "GetForWindow" && im.natural_params.is_some()) - .or_else(|| info.methods.iter().find(|im| im.natural_params.is_some()))?; - let primary_natural = primary.natural_params.as_ref()?; - - // The IInspectable IID is a fixed WinRT constant. - const IID_IINSPECTABLE: &str = "af86e2e0-b12d-4c6a-9c5a-d7aa65101e90"; - - // -- .js -- - let mut js = String::new(); - js.push_str("// Generated by dynwinrt-codegen — do not edit\n"); - js.push_str(&format!( - "import {{ DynWinRtType, DynWinRtMethodSig, WinGuid }} from '{}';\n", - crate::codegen::project::get_import_name() - )); - js.push_str(&format!( - "import {{ {interop} }} from './{interop}.js';\n", - interop = interop_module, - )); - js.push('\n'); - - js.push_str(&format!( - "const IID_IInspectable = WinGuid.parse('{iid}');\n\n", - iid = IID_IINSPECTABLE - )); - // IInspectable registration (lazy) — used to reach GetRuntimeClassName. - // IInspectable is the base itself; its methods live at absolute vtable - // slots 3, 4, 5 (right after IUnknown). Register with the +3 base so that - // `.method(4)` resolves to `GetRuntimeClassName` at the real absolute slot. - js.push_str("let _IInspectableCache;\n"); - js.push_str("const _IInspectable = new Proxy({}, {\n get(_target, prop) {\n"); - js.push_str(" _IInspectableCache ??= DynWinRtType.registerInterfaceUnknown('IInspectable_projected', IID_IInspectable)\n"); - js.push_str(" .addMethod('GetIids', new DynWinRtMethodSig().addOut(DynWinRtType.pointer()).addOut(DynWinRtType.pointer()))\n"); - js.push_str(" .addMethod('GetRuntimeClassName', new DynWinRtMethodSig().addOut(DynWinRtType.hstring()))\n"); - js.push_str(" .addMethod('GetTrustLevel', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type()));\n"); - js.push_str(" const value = _IInspectableCache[prop];\n"); - js.push_str( - " return typeof value === 'function' ? value.bind(_IInspectableCache) : value;\n", - ); - js.push_str(" },\n});\n\n"); - - js.push_str(&format!("export class {cls} {{\n", cls = info.class_name)); - js.push_str(" _obj;\n"); - js.push_str(" constructor(obj) { this._obj = obj; }\n"); - js.push_str(&format!( - " static _fromNative(obj) {{ return new {cls}(obj); }}\n", - cls = info.class_name, - )); - - // Static getForWindow(hwnd) — the high-level natural surface. - let param_list: Vec = primary_natural - .iter() - .enumerate() - .map(|(i, p)| js_param_name(&p.name, i)) - .collect(); - js.push_str(&format!( - " /** Get a `{cls}` for the given HWND via the {interop} interop. */\n", - cls = info.class_name, - interop = interop_module, - )); - js.push_str(&format!( - " static {camel}({params}) {{\n", - camel = primary.camel, - params = param_list.join(", "), - )); - js.push_str(&format!( - " const interop = {interop}.create();\n", - interop = interop_module, - )); - // Call interop.(...naturalArgs) — this returns a - // `` already wrapped via `_fromNative`. - js.push_str(&format!( - " return interop.{camel}({params});\n", - camel = primary.camel, - params = param_list.join(", "), - )); - js.push_str(" }\n"); - - // runtimeClassName getter — IInspectable slot 4 (absolute vtable index). - js.push_str(" /** IInspectable::GetRuntimeClassName — the projected class name. */\n"); - js.push_str(" get runtimeClassName() {\n"); - js.push_str(" return _IInspectable.method(4).getString(this._obj);\n"); - js.push_str(" }\n"); - - js.push_str("}\n"); - - // -- .d.ts -- - let mut dts = String::new(); - dts.push_str("// Generated by dynwinrt-codegen — do not edit\n\n"); - // Handle typedef for HWND (needed for the static getForWindow signature). - let handle_aliases = collect_handle_aliases(meta); - for h in &handle_aliases { - dts.push_str(&format!( - "/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */\nexport type {h} = bigint | Buffer;\n", - h = h - )); - } - if !handle_aliases.is_empty() { - dts.push('\n'); - } - dts.push_str(&format!( - "export declare class {cls} {{\n", - cls = info.class_name - )); - dts.push_str(&format!( - " /** Wrap an existing native COM pointer (for QueryInterface bridging). */\n static _fromNative(obj: unknown): {cls};\n", - cls = info.class_name, - )); - let ts_params: Vec = primary_natural - .iter() - .enumerate() - .map(|(i, p)| { - format!( - "{}: {}", - js_param_name(&p.name, i), - ts_type_expr_dts(&p.typ) - ) - }) - .collect(); - dts.push_str(&format!( - " /** Get a `{cls}` for the given HWND (projected from `{full_class_name}`). */\n", - cls = info.class_name, - full_class_name = full_class_name, - )); - dts.push_str(&format!( - " static {camel}({params}): {cls};\n", - camel = primary.camel, - params = ts_params.join(", "), - cls = info.class_name, - )); - dts.push_str(" /** IInspectable::GetRuntimeClassName — the projected class name. */\n"); - dts.push_str(" get runtimeClassName(): string;\n"); - dts.push_str("}\n"); - - Some((js, dts)) -} - -fn collect_handle_aliases(meta: &ComInterfaceMeta) -> Vec { - let mut set = BTreeSet::new(); - for m in &meta.interface.methods { - for p in &m.params { - if let Some(h) = handle_type_name(&p.typ) { - set.insert(h); - } - } - } - set.into_iter().collect() -} - -// --------------------------------------------------------------------------- -// Enum sibling files -// --------------------------------------------------------------------------- - -fn render_enum_files(en: &TypeMeta) -> (String, String) { - let (name, members) = match en { - TypeMeta::Enum { name, members, .. } => (name.as_str(), members), - _ => unreachable!(), - }; - - // .js: a frozen object. - let mut js = String::new(); - js.push_str("// Generated by dynwinrt-codegen — do not edit\n"); - js.push_str(&format!( - "export const {name} = Object.freeze({{\n", - name = name - )); - for m in members { - js.push_str(&format!(" {}: {},\n", m.name, m.value)); - } - js.push_str("});\n"); - - // .d.ts: emit a const object + companion type — matches the JS `Object.freeze({...})` - // runtime shape and mirrors the WinRT enum generator (see - // `codegen::javascript::render::declarations::render_enum_dts`). Using `const enum` - // breaks under TS `isolatedModules`, so we intentionally avoid it. - let mut dts = String::new(); - dts.push_str("// Generated by dynwinrt-codegen — do not edit\n"); - dts.push_str(&format!( - "export type {name} = (typeof {name})[keyof typeof {name}];\n", - name = name - )); - dts.push_str(&format!("export declare const {name}: {{\n", name = name)); - for m in members { - dts.push_str(&format!(" readonly {}: {};\n", m.name, m.value)); - } - dts.push_str("};\n"); - - (js, dts) -} - -fn enum_import_names(meta: &ComInterfaceMeta) -> Vec { - meta.referenced_enums - .iter() - .filter_map(|e| match e { - TypeMeta::Enum { name, .. } => Some(name.clone()), - _ => None, - }) - .collect() -} - -fn returned_interface_import_names(meta: &ComInterfaceMeta) -> Vec { - let mut set = BTreeSet::new(); - for m in &meta.interface.methods { - for p in &m.params { - if p.direction == ParamDirection::Out { - if let TypeMeta::Interface { name, iid, .. } = &p.typ { - if !iid.is_empty() { - set.insert(name.clone()); - } - } - } - } - } - set.into_iter().collect() -} - -// --------------------------------------------------------------------------- -// Type mapping helpers -// --------------------------------------------------------------------------- - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum StringEncoding { - Wide, - Ansi, -} - -fn has_string_buffer_method(meta: &ComInterfaceMeta) -> bool { +fn has_dynamic_iid_method(meta: &ComInterfaceMeta) -> bool { meta.interface .methods .iter() - .any(|m| string_buffer_pattern(m).is_some()) -} - -fn string_buffer_pattern(m: &MethodMeta) -> Option<(usize, usize, StringEncoding)> { - for (idx, p) in m.params.iter().enumerate() { - let ParamDirection::OutStringBuffer { count_param_index } = p.direction else { - continue; - }; - let encoding = string_buffer_encoding(&p.typ)?; - if m.params - .get(count_param_index) - .is_some_and(|count| count.direction == ParamDirection::In) - { - return Some((idx, count_param_index, encoding)); - } - } - None -} - -fn string_buffer_encoding(t: &TypeMeta) -> Option { - match t { - TypeMeta::Struct { - namespace, name, .. - } if namespace == "Windows.Win32.Foundation" && name == "PWSTR" => { - Some(StringEncoding::Wide) - } - TypeMeta::Struct { - namespace, name, .. - } if namespace == "Windows.Win32.Foundation" && name == "PSTR" => { - Some(StringEncoding::Ansi) - } - _ => None, - } -} - -fn is_optional_find_data_out_after_string_count(p: &ParamMeta) -> bool { - if p.direction != ParamDirection::Out { - return false; - } - let n = p.name.to_ascii_lowercase(); - if n == "pfd" || n.contains("finddata") || n.contains("find_data") { - return true; - } - matches!( - &p.typ, - TypeMeta::Struct { name, .. } if name == "WIN32_FIND_DATAW" || name == "WIN32_FIND_DATAA" - ) -} - -/// TS type expression for the `.d.ts` surface. -fn ts_type_expr_dts(t: &TypeMeta) -> String { - // Win32 BOOL is a struct with a single `Value: I32` field — the same shape - // as an opaque handle. Special-case it to the natural boolean surface so - // callers can just pass `true`/`false` rather than a bigint. - if is_win32_bool(t) { - return "boolean".into(); - } - if is_hresult(t) { - return "number".into(); - } - if let Some(h) = handle_type_name(t) { - return h; - } - match t { - TypeMeta::Bool => "boolean".into(), - TypeMeta::I8 - | TypeMeta::U8 - | TypeMeta::I16 - | TypeMeta::U16 - | TypeMeta::I32 - | TypeMeta::U32 - | TypeMeta::F32 - | TypeMeta::F64 - | TypeMeta::Char16 => "number".into(), - TypeMeta::I64 | TypeMeta::U64 => "bigint".into(), - TypeMeta::String => "string".into(), - TypeMeta::Guid => "string".into(), - TypeMeta::Interface { name, iid, .. } if !iid.is_empty() => name.clone(), - TypeMeta::Enum { name, .. } => name.clone(), - TypeMeta::Struct { name, .. } => name.clone(), - // Pointer-to-struct or unknown — opaque bigint|Buffer at the surface. - _ => "bigint | Buffer".into(), - } + .any(|method| method_is_interop_shape(method).is_some()) } - -/// Runtime type expression for `DynWinRtMethodSig` calls in `.js`. -fn ts_type_expr_js(t: &TypeMeta) -> String { - // Win32 BOOL marshals as a 32-bit int at the ABI (Win32 BOOL is `int`), - // NOT an opaque pointer. Mirrors how enums map to their underlying i32. - if is_win32_bool(t) { - return "DynWinRtType.i32Type()".into(); - } - if is_hresult(t) { - return "DynWinRtType.i32Type()".into(); - } - if handle_type_name(t).is_some() { - return "DynWinRtType.pointer()".into(); - } - match t { - TypeMeta::Bool => "DynWinRtType.boolType()".into(), - TypeMeta::I8 => "DynWinRtType.i8Type()".into(), - TypeMeta::U8 => "DynWinRtType.u8Type()".into(), - TypeMeta::I16 => "DynWinRtType.i16Type()".into(), - TypeMeta::U16 => "DynWinRtType.u16Type()".into(), - TypeMeta::I32 => "DynWinRtType.i32Type()".into(), - TypeMeta::U32 => "DynWinRtType.u32Type()".into(), - TypeMeta::I64 => "DynWinRtType.i64Type()".into(), - TypeMeta::U64 => "DynWinRtType.u64Type()".into(), - TypeMeta::F32 => "DynWinRtType.f32Type()".into(), - TypeMeta::F64 => "DynWinRtType.f64Type()".into(), - TypeMeta::Char16 => "DynWinRtType.char16()".into(), - TypeMeta::String => "DynWinRtType.pointer()".into(), // PCWSTR/PWSTR → opaque - TypeMeta::Guid => "DynWinRtType.guidType()".into(), - TypeMeta::Enum { underlying, .. } => ts_type_expr_js(underlying), - _ => "DynWinRtType.pointer()".into(), - } -} - -fn wrap_arg_js(t: &TypeMeta, var: &str) -> String { - // Win32 BOOL: accept `boolean`/`number`/`bigint` on the surface and - // narrow to an i32 (0/1) at the ABI. Truthy → 1, falsy → 0. Non-nullish - // numerics are preserved so callers passing `1`/`0` still work. - if is_win32_bool(t) { - return format!("DynWinRtValue.i32({var} ? 1 : 0)", var = var); - } - if is_hresult(t) { - return format!("DynWinRtValue.i32({var})", var = var); - } - if handle_type_name(t).is_some() { - return format!("DynWinRtValue.pointer({var})", var = var); - } - match t { - TypeMeta::Bool => format!("DynWinRtValue.boolValue({var})", var = var), - TypeMeta::I8 => format!("DynWinRtValue.i8Value({var})", var = var), - TypeMeta::U8 => format!("DynWinRtValue.u8Value({var})", var = var), - TypeMeta::I16 => format!("DynWinRtValue.i16({var})", var = var), - TypeMeta::U16 => format!("DynWinRtValue.u16({var})", var = var), - TypeMeta::I32 => format!("DynWinRtValue.i32({var})", var = var), - TypeMeta::U32 => format!("DynWinRtValue.u32({var})", var = var), - TypeMeta::I64 => format!("DynWinRtValue.i64(BigInt({var}))", var = var), - TypeMeta::U64 => format!("DynWinRtValue.u64(BigInt({var}))", var = var), - TypeMeta::F32 => format!("DynWinRtValue.f32({var})", var = var), - TypeMeta::F64 => format!("DynWinRtValue.f64({var})", var = var), - TypeMeta::Char16 => format!("DynWinRtValue.char16({var})", var = var), - TypeMeta::String => format!("DynWinRtValue.pointer({var})", var = var), - TypeMeta::Guid => format!("DynWinRtValue.guid(WinGuid.parse({var}))", var = var), - TypeMeta::Enum { underlying, .. } => wrap_arg_js(underlying, var), - _ => format!("DynWinRtValue.pointer({var})", var = var), - } -} - -/// Returns `Some("HWND")` etc. when the given type is a Win32 opaque handle -/// (a struct in `Windows.Win32.Foundation` or similar handle-namespace with a -/// single pointer-shaped `Value` field). Also returns handle names for -/// PWSTR/PCWSTR/HRESULT-family types encountered as parameters (except -/// HRESULT itself which is treated as `void`). -fn handle_type_name(t: &TypeMeta) -> Option { - // BOOL is NOT a handle even though it shape-matches (`{ Value: I32 }`). - // The natural surface is `boolean` (see `is_win32_bool`). - if is_win32_bool(t) { - return None; - } - match t { - TypeMeta::Struct { - namespace, - name, - fields, - } => { - if !is_win32_handle_namespace(namespace) { - return None; - } - if is_hresult_by_name(namespace, name) { - return None; // HRESULT is not a "handle" — never surface it as one - } - // Handle heuristic: exactly one field named `Value`, of pointer/int type. - if fields.len() == 1 - && fields[0].name == "Value" - && matches!( - fields[0].typ, - TypeMeta::Object - | TypeMeta::U64 - | TypeMeta::I64 - | TypeMeta::U32 - | TypeMeta::I32 - ) - { - return Some(name.clone()); - } - None - } - _ => None, - } -} - -fn is_win32_handle_namespace(ns: &str) -> bool { - ns.starts_with("Windows.Win32.") -} - -fn is_hresult(t: &TypeMeta) -> bool { - matches!( - t, - TypeMeta::Struct { namespace, name, .. } - if is_hresult_by_name(namespace, name) - ) -} - -fn is_hresult_by_name(ns: &str, name: &str) -> bool { - ns == "Windows.Win32.Foundation" && name == "HRESULT" -} - -/// Recognise the Win32 `BOOL` struct (`Windows.Win32.Foundation.BOOL`) — a -/// `{ Value: I32 }` struct whose natural surface is a JS `boolean` but whose -/// ABI is a 32-bit int. Kept as a distinct helper so the surface remains -/// obvious and greppable. -fn is_win32_bool(t: &TypeMeta) -> bool { - matches!( - t, - TypeMeta::Struct { namespace, name, .. } - if namespace == "Windows.Win32.Foundation" && name == "BOOL" - ) -} - -// --------------------------------------------------------------------------- -// Naming helpers -// --------------------------------------------------------------------------- - -fn camel_case(name: &str) -> String { - if name.is_empty() { - return String::new(); - } - let chars: Vec = name.chars().collect(); - // Count the leading uppercase run. - let mut run = 0usize; - while run < chars.len() && chars[run].is_ascii_uppercase() { - run += 1; - } - let mut result = String::with_capacity(name.len()); - if run == 0 { - // Already starts lowercase — return unchanged. - return name.to_string(); - } - if run == chars.len() { - // Fully uppercase (e.g. "URL") — lowercase everything. - for c in &chars { - result.push(c.to_ascii_lowercase()); - } - return result; - } - if run == 1 { - // Simple case: lowercase first char, keep the rest. - result.push(chars[0].to_ascii_lowercase()); - for c in &chars[1..] { - result.push(*c); - } - return result; - } - // Multi-char uppercase run followed by lowercase: last uppercase char is - // the start of the next word. E.g. "IOHandle" -> "ioHandle". - for c in &chars[..run - 1] { - result.push(c.to_ascii_lowercase()); - } - for c in &chars[run - 1..] { - result.push(*c); - } - result + +fn has_owned_pointer_output(meta: &ComInterfaceMeta) -> bool { + meta.interface.methods.iter().any(|method| { + method + .owned_outputs + .iter() + .any(|owned| owned.free_with.contains("CoTaskMemFree")) + }) } -fn js_param_name(raw: &str, index: usize) -> String { - let base = if raw.is_empty() { - format!("arg{}", index) - } else { - raw.to_string() +// --------------------------------------------------------------------------- +// Enum sibling files +// --------------------------------------------------------------------------- + +fn render_enum_files(en: &TypeMeta) -> (String, String) { + let (name, members) = match en { + TypeMeta::Enum { name, members, .. } => (name.as_str(), members), + _ => unreachable!(), }; - // Camelize (strip common Hungarian prefixes lightly for prettier surface): - // dwFoo -> foo, pFoo -> foo, lpszFoo -> foo, cbFoo -> foo, iFoo -> foo, hFoo -> foo, hwndFoo -> foo. - let stripped = strip_hungarian(&base); - let mut out = String::with_capacity(stripped.len()); - let mut chars = stripped.chars(); - if let Some(first) = chars.next() { - out.push(first.to_ascii_lowercase()); - } - for c in chars { - out.push(c); - } - // Guard against JS reserved words. - match out.as_str() { - "class" | "return" | "function" | "default" | "this" | "new" | "delete" | "let" - | "const" | "var" | "if" | "else" | "for" | "while" | "do" | "switch" | "case" - | "break" | "continue" | "true" | "false" | "null" | "undefined" | "in" | "of" - | "typeof" | "instanceof" | "throw" | "try" | "catch" | "finally" | "yield" | "async" - | "await" | "with" | "void" | "public" | "private" | "protected" | "package" | "static" - | "import" | "export" | "extends" | "super" | "arguments" => { - format!("{}_", out) - } - _ => out, + + // .js: a frozen object. + let mut js = String::new(); + js.push_str("// Generated by dynwinrt-codegen — do not edit\n"); + js.push_str(&format!( + "export const {name} = Object.freeze({{\n", + name = name + )); + for m in members { + js.push_str(&format!(" {}: {},\n", m.name, m.value)); } -} + js.push_str("});\n"); -fn strip_hungarian(s: &str) -> &str { - // Only strip common **multi-character** Hungarian prefixes, and only when - // followed by an uppercase letter (word boundary). Single-letter prefixes - // like `h`, `p`, `i` cause too many false positives on real method-param - // names (e.g. `hwnd` starts with `h` but isn't Hungarian; `pButton` is). - let prefixes = [ - "lpwsz", "pwsz", "lpsz", "psz", "lpsz", "pwstr", "pcwstr", "hwnd", "dw", "sz", "cb", "cx", - "cy", "cw", "ch", "cn", "cc", "lp", "np", "ph", "pd", "pf", "pv", "ppv", "pp", "wsz", - ]; - for p in prefixes { - if let Some(rest) = s.strip_prefix(p) { - if rest - .chars() - .next() - .map(|c| c.is_ascii_uppercase()) - .unwrap_or(false) - { - return rest; - } - } + // .d.ts: emit a const object + companion type — matches the JS `Object.freeze({...})` + // runtime shape and mirrors the WinRT enum generator (see + // `codegen::javascript::render::declarations::render_enum_dts`). Using `const enum` + // breaks under TS `isolatedModules`, so we intentionally avoid it. + let mut dts = String::new(); + dts.push_str("// Generated by dynwinrt-codegen — do not edit\n"); + dts.push_str(&format!( + "export type {name} = (typeof {name})[keyof typeof {name}];\n", + name = name + )); + dts.push_str(&format!("export declare const {name}: {{\n", name = name)); + for m in members { + dts.push_str(&format!(" readonly {}: {};\n", m.name, m.value)); } - s + dts.push_str("};\n"); + + (js, dts) } // --------------------------------------------------------------------------- @@ -1644,11 +907,11 @@ mod tests { // .d.ts surface: boolean (not `BOOL` or `bigint | Buffer`) assert_eq!(ts_type_expr_dts(&b), "boolean"); // .js registration: i32 type (not pointer) - assert_eq!(ts_type_expr_js(&b), "DynWinRtType.i32Type()"); + assert_eq!(ts_type_expr_js(&b), "DynCom.i32Type()"); // .js argument marshalling: truthy→1, falsy→0 as an i32 (not pointer) assert_eq!( wrap_arg_js(&b, "fFullscreen"), - "DynWinRtValue.i32(fFullscreen ? 1 : 0)" + "DynCom.i32(fFullscreen ? 1 : 0)" ); } @@ -1656,8 +919,8 @@ mod tests { fn hresult_input_projects_as_number_and_i32_value() { let hr = make_hresult(); assert_eq!(ts_type_expr_dts(&hr), "number"); - assert_eq!(ts_type_expr_js(&hr), "DynWinRtType.i32Type()"); - assert_eq!(wrap_arg_js(&hr, "hr"), "DynWinRtValue.i32(hr)"); + assert_eq!(ts_type_expr_js(&hr), "DynCom.i32Type()"); + assert_eq!(wrap_arg_js(&hr, "hr"), "DynCom.i32(hr)"); let m = MethodMeta { name: "Close".into(), @@ -1674,19 +937,17 @@ mod tests { let js = render_js(&com, None); let dts = render_dts(&com, None); assert!( - js.contains( - ".addMethod('Close', new DynWinRtMethodSig().addIn(DynWinRtType.i32Type()))" - ), + js.contains(".addMethod('Close', new DynComMethodSig().addIn(DynCom.i32Type()))"), ".js must register HRESULT in-param as i32:\n{}", js ); assert!( - js.contains("DynWinRtValue.i32(hr)"), + js.contains("DynCom.i32(hr)"), ".js must pass HRESULT by value as i32:\n{}", js ); assert!( - !js.contains("DynWinRtValue.pointer(hr)"), + !js.contains("DynCom.pointer(hr)"), ".js must not pass HRESULT as a pointer:\n{}", js ); @@ -1862,7 +1123,7 @@ mod tests { /// a NULL riid. #[test] fn interop_generation_fails_when_target_iid_unresolvable() { - use crate::meta::{ComInterfaceMeta, InterfaceMeta}; + use crate::com_metadata::{ComInterfaceMeta, InterfaceMeta}; let iface = InterfaceMeta { name: "IThisRuntimeClassDoesNotExist_DynWinrtInterop".into(), @@ -1931,7 +1192,7 @@ mod tests { fn non_interop_iunknown_interface_still_generates_without_winmd_lookup() { // A vanilla IUnknown-rooted interface with no coclass and no // interop shape must succeed even when we pass empty winmd paths. - use crate::meta::{ComInterfaceMeta, InterfaceMeta}; + use crate::com_metadata::{ComInterfaceMeta, InterfaceMeta}; let iface = InterfaceMeta { name: "IMyPlainClassicCom".into(), namespace: "Windows.Win32.System.Com".into(), @@ -1960,14 +1221,14 @@ mod tests { }; let out = generate_com_interface_files(&com, "") .expect("plain classic-COM codegen must succeed with no winmds"); - assert!(out.js.contains("registerInterfaceUnknown")); + assert!(out.js.contains("DynCom.registerIUnknownInterface")); assert!(out.js.contains("method(3)")); } // ---- Fix 4 (classic-COM plain `[out]` param → return-value projection) ---- - fn plain_iface_with_method(m: MethodMeta) -> crate::meta::ComInterfaceMeta { - use crate::meta::{ComInterfaceMeta, InterfaceMeta}; + fn plain_iface_with_method(m: MethodMeta) -> crate::com_metadata::ComInterfaceMeta { + use crate::com_metadata::{ComInterfaceMeta, InterfaceMeta}; let iface = InterfaceMeta { name: "IHasOut".into(), namespace: "Windows.Win32.System.Com".into(), @@ -1990,6 +1251,93 @@ mod tests { } } + #[test] + fn unsupported_struct_in_out_fails_closed() { + let method = MethodMeta { + name: "Read".into(), + params: vec![ParamMeta { + name: "value".into(), + typ: TypeMeta::Struct { + namespace: "Windows.Win32.System.Com".into(), + name: "VARIANT".into(), + fields: vec![ + crate::types::FieldMeta { + name: "vt".into(), + typ: TypeMeta::U16, + }, + crate::types::FieldMeta { + name: "data".into(), + typ: TypeMeta::U64, + }, + ], + }, + direction: ParamDirection::InOut, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("unsupported struct in/out must not emit a wrong T** ABI"); + assert!(error.contains("requires native layout projection")); + } + + #[test] + fn unsupported_by_value_struct_fails_closed() { + let method = MethodMeta { + name: "DragEnter".into(), + params: vec![ParamMeta { + name: "point".into(), + typ: TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "POINTL".into(), + fields: vec![ + crate::types::FieldMeta { + name: "x".into(), + typ: TypeMeta::I32, + }, + crate::types::FieldMeta { + name: "y".into(), + typ: TypeMeta::I32, + }, + ], + }, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("struct layout must fail closed"); + assert!(error.contains("requires native layout projection")); + } + + #[test] + fn unsupported_struct_direct_return_fails_closed() { + let method = MethodMeta { + name: "GetPoint".into(), + return_type: Some(TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "POINT".into(), + fields: vec![ + crate::types::FieldMeta { + name: "x".into(), + typ: TypeMeta::I32, + }, + crate::types::FieldMeta { + name: "y".into(), + typ: TypeMeta::I32, + }, + ], + }), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("unsupported struct return must not panic at invocation time"); + assert!(error.contains("unsupported direct native return")); + } + #[test] fn plain_method_single_out_scalar_projects_as_return() { // Model: `HRESULT GetShowCmd([out] int* pcmd)` — the classic single-out @@ -2008,16 +1356,6 @@ mod tests { let com = plain_iface_with_method(m); let js = render_js(&com, None); let dts = render_dts(&com, None); - assert!( - js.contains(".addMethod('GetShowCmd', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type()))"), - ".js must register I32 out-param as i32, not pointer:\n{}", - js - ); - assert!( - !js.contains(".addMethod('GetShowCmd', new DynWinRtMethodSig().addOut(DynWinRtType.pointer()))"), - ".js must not register scalar out-param as pointer:\n{}", - js - ); // .js: must capture `_out` and return it as a JS number. assert!( js.contains("const _out = _IHasOut.method(8).invoke(this._obj, [])"), @@ -2025,8 +1363,8 @@ mod tests { js ); assert!( - js.contains("return _out.toNumber();"), - ".js must unwrap the I32 out as _out.toNumber():\n{}", + js.contains("return DynCom.toNumber(_out);"), + ".js must unwrap the I32 out:\n{}", js ); // .d.ts: return type must be `number`, not `void`. @@ -2037,45 +1375,6 @@ mod tests { ); } - #[test] - fn plain_method_single_out_u16_projects_as_return() { - // Model: `HRESULT GetHotkey([out] WORD* pwHotkey)`. - let m = MethodMeta { - name: "GetHotkey".into(), - vtable_index: 9, - params: vec![ParamMeta { - name: "pwHotkey".into(), - typ: TypeMeta::U16, - direction: ParamDirection::Out, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - let com = plain_iface_with_method(m); - let js = render_js(&com, None); - let dts = render_dts(&com, None); - assert!( - js.contains(".addMethod('GetHotkey', new DynWinRtMethodSig().addOut(DynWinRtType.u16Type()))"), - ".js must register U16 out-param as u16, not pointer:\n{}", - js - ); - assert!( - js.contains("return _out.toNumber();"), - ".js must unwrap the U16 out as _out.toNumber():\n{}", - js - ); - assert!( - !js.contains("raw COM interface pointer adoption"), - ".js must not emit COM-pointer adoption TODO for scalar outs:\n{}", - js - ); - assert!( - dts.contains("getHotkey(): number;"), - ".d.ts must project single-out U16 as `number`:\n{}", - dts - ); - } - #[test] fn plain_method_single_out_guid_projects_as_string() { // Model: `HRESULT GetClassID([out] GUID* pClassID)` (IPersist shape). @@ -2099,8 +1398,8 @@ mod tests { js ); assert!( - js.contains("return _out.toGuid().toString();"), - ".js must unwrap GUID out via .toGuid().toString():\n{}", + js.contains("return DynCom.toGuidString(_out);"), + ".js must unwrap GUID out:\n{}", js ); assert!( @@ -2138,8 +1437,8 @@ mod tests { let js = render_js(&com, None); let dts = render_dts(&com, None); assert!( - js.contains("return _out.toNumber();"), - ".js must unwrap enum out via underlying scalar (.toNumber()):\n{}", + js.contains("return DynCom.toNumber(_out);"), + ".js must unwrap enum out via its underlying scalar:\n{}", js ); assert!( @@ -2180,7 +1479,7 @@ mod tests { js ); assert!( - js.contains("return [_r[0].toNumber(), _r[1].toBool()];"), + js.contains("return [DynCom.toU32(_r[0]), DynCom.toBool(_r[1])];"), ".js multi-out must return a tuple with each out unwrapped:\n{}", js ); @@ -2231,10 +1530,147 @@ mod tests { } #[test] - fn plain_method_outfill_stays_void_with_todo() { - // Caller-allocated `[out, sizeis]` buffers are NOT yet projected — - // emit a `TODO` comment and keep the surface as `void` so we don't - // half-break anything. + fn direct_native_return_uses_return_abi_instead_of_synthetic_out_param() { + let method = MethodMeta { + name: "RetryRejectedCall".into(), + vtable_index: 5, + return_type: Some(TypeMeta::U32), + ..Default::default() + }; + let signature = build_method_sig_js(&method); + assert_eq!(signature, "new DynComMethodSig().returns(DynCom.u32Type())"); + + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!(js.contains("const _out = _IHasOut.method(5).invoke(this._obj, [])")); + assert!(js.contains("return DynCom.toU32(_out);")); + assert!(dts.contains("retryRejectedCall(): number;")); + } + + #[test] + fn native_void_return_is_declared_explicitly() { + let method = MethodMeta { + name: "OnClose".into(), + vtable_index: 8, + return_type: None, + ..Default::default() + }; + assert_eq!( + build_method_sig_js(&method), + "new DynComMethodSig().returnsVoid()" + ); + + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + assert!(js.contains("_IHasOut.method(8).invoke(this._obj, [])")); + assert!(!js.contains("const _out =")); + } + + #[test] + fn direct_64_bit_returns_use_bigint_accessors() { + let i64_method = MethodMeta { + name: "GetSigned".into(), + return_type: Some(TypeMeta::I64), + ..Default::default() + }; + let u64_method = MethodMeta { + name: "GetUnsigned".into(), + return_type: Some(TypeMeta::U64), + ..Default::default() + }; + + let i64_js = render_js(&plain_iface_with_method(i64_method), None); + let u64_js = render_js(&plain_iface_with_method(u64_method), None); + assert!(i64_js.contains("return DynCom.toI64Bigint(_out);")); + assert!(u64_js.contains("return DynCom.toU64Bigint(_out);")); + } + + #[test] + fn return_only_handle_declares_its_alias() { + let hwnd = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + let method = MethodMeta { + name: "GetWindow".into(), + return_type: Some(hwnd), + ..Default::default() + }; + let com = plain_iface_with_method(method); + let dts = render_dts(&com, None); + assert!(dts.contains("export type HWND = bigint | Buffer;")); + assert!(dts.contains("getWindow(): HWND;")); + } + + #[test] + fn return_only_enum_emits_import_and_sibling_files() { + let kind = TypeMeta::Enum { + namespace: "Windows.Win32.Example".into(), + name: "THING_KIND".into(), + underlying: Box::new(TypeMeta::I32), + members: Vec::new(), + is_flags: false, + doc: None, + deprecated: None, + }; + let method = MethodMeta { + name: "GetKind".into(), + return_type: Some(kind.clone()), + ..Default::default() + }; + let mut com = plain_iface_with_method(method); + com.referenced_enums.push(kind); + + let output = generate_com_interface_files(&com, "").unwrap(); + assert!( + output + .dts + .contains("import { THING_KIND } from './THING_KIND.js';") + ); + assert!( + output + .extra_files + .iter() + .any(|(name, _)| name == "THING_KIND.d.ts") + ); + } + + #[test] + fn in_out_parameter_is_both_argument_and_result() { + let method = MethodMeta { + name: "Adjust".into(), + vtable_index: 4, + params: vec![ParamMeta { + name: "value".into(), + typ: TypeMeta::I32, + direction: ParamDirection::InOut, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + assert_eq!( + build_method_sig_js(&method), + "new DynComMethodSig().addInOut(DynCom.i32Type())" + ); + + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!(js.contains("adjust(value)")); + assert!( + js.contains("const _out = _IHasOut.method(4).invoke(this._obj, [DynCom.i32(value)])") + ); + assert!(js.contains("return DynCom.toNumber(_out);")); + assert!(dts.contains("adjust(value: number): number;")); + } + + #[test] + fn unsupported_outfill_fails_closed() { let m = MethodMeta { name: "GetPath".into(), vtable_index: 2, @@ -2254,23 +1690,9 @@ mod tests { ..Default::default() }; let com = plain_iface_with_method(m); - let js = render_js(&com, None); - let dts = render_dts(&com, None); - assert!( - js.contains("TODO: caller-allocated [out, sizeis] buffers"), - ".js OutFill must include a TODO comment:\n{}", - js - ); - assert!( - !js.contains("return _out") && !js.contains("return _r") && !js.contains("return ["), - ".js OutFill must not return anything (avoid half-broken projection):\n{}", - js - ); - assert!( - dts.contains("getPath(cch: number): void;"), - ".d.ts OutFill must stay `void`:\n{}", - dts - ); + let error = generate_com_interface_files(&com, "") + .expect_err("unsupported caller-allocated arrays must fail closed"); + assert!(error.contains("caller-allocated array outputs are not supported")); } fn pwstr_struct() -> TypeMeta { @@ -2315,7 +1737,7 @@ mod tests { js ); assert!( - js.contains(".addMethod('GetDescription', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type()))"), + js.contains(".addMethod('GetDescription', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type()))"), ".js must register string buffer as an input pointer:\n{}", js ); @@ -2325,7 +1747,8 @@ mod tests { js ); assert!( - js.contains("return _decodeWideString(_buffer);"), + js.contains("const _text = _decodeWideString(_buffer);") + && js.contains("return _text;"), ".js must return the decoded wide string:\n{}", js ); @@ -2337,7 +1760,64 @@ mod tests { } #[test] - fn interface_out_param_projects_as_typed_wrapper() { + fn callee_allocated_pwstr_is_decoded_and_freed() { + let method = MethodMeta { + name: "GetDisplayName".into(), + vtable_index: 5, + params: vec![ParamMeta { + name: "name".into(), + typ: pwstr_struct(), + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + + assert!(js.contains("return DynCom.takeCoTaskMemWideString(_out);")); + assert!(dts.contains("getDisplayName(): string;")); + } + + #[test] + fn string_buffer_preserves_additional_outputs() { + let method = MethodMeta { + name: "GetIconLocation".into(), + vtable_index: 16, + params: vec![ + ParamMeta { + name: "path".into(), + typ: pwstr_struct(), + direction: ParamDirection::OutStringBuffer { + count_param_index: 1, + }, + }, + ParamMeta { + name: "cch".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ParamMeta { + name: "icon".into(), + typ: TypeMeta::I32, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + + assert!(js.contains("const _out = _IHasOut.method(16).invoke")); + assert!(js.contains("return [_text, DynCom.toNumber(_out)];")); + assert!(dts.contains("getIconLocation(cch?: number): [string, number];")); + } + + #[test] + fn interface_out_param_projects_as_explicit_bridge_value() { let m = MethodMeta { name: "GetThing".into(), vtable_index: 7, @@ -2357,24 +1837,64 @@ mod tests { let js = render_js(&com, None); let dts = render_dts(&com, None); assert!( - js.contains("import { IThing } from './IThing.js';"), - ".js must import the returned interface wrapper:\n{}", + !js.contains("from './IThing.js'"), + ".js must not depend on an ungenerated wrapper:\n{}", js ); + assert!(js.contains( + ".addOut(DynCom.interfaceType(WinGuid.parse('11111111-2222-3333-4444-555555555555')))" + )); assert!( - js.contains("return IThing._fromNative(_out);"), - ".js must wrap the returned COM object in the typed interface wrapper:\n{}", + js.contains("return _out;"), + ".js must return the managed bridge value:\n{}", js ); assert!( - dts.contains("import { IThing } from './IThing.js';"), - ".d.ts must import the returned interface type:\n{}", + dts.contains("import type { DynWinRtValue }"), + ".d.ts must import the bridge type:\n{}", dts ); assert!( - dts.contains("getThing(): IThing;"), - ".d.ts must return the typed interface wrapper:\n{}", + dts.contains("getThing(): DynWinRtValue;"), + ".d.ts must return the explicit bridge value:\n{}", dts ); } + + #[test] + fn caller_supplied_riid_output_is_adopted() { + let method = MethodMeta { + name: "BindToHandler".into(), + vtable_index: 4, + params: vec![ + ParamMeta { + name: "pbc".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "ppv".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(method); + let output = generate_com_interface_files(&com, "").unwrap(); + + assert!(output.js.contains("bindToHandler(pbc, iid)")); + assert!(output.js.contains("DynCom.adoptComPointer(_raw, _iid)")); + assert!( + output + .dts + .contains("bindToHandler(pbc: bigint | Buffer, iid: string): DynWinRtValue;") + ); + } } diff --git a/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs b/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs new file mode 100644 index 00000000..6ebe8c55 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs @@ -0,0 +1,457 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::BTreeSet; + +use crate::com_metadata::{ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; +use crate::types::TypeMeta; + +use super::naming::js_param_name; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum StringEncoding { + Wide, + Ansi, +} + +pub(super) fn validate_com_abi(meta: &ComInterfaceMeta) -> Result<(), String> { + for method in &meta.interface.methods { + for param in &method.params { + if matches!(param.typ, TypeMeta::Struct { .. }) + && !is_win32_bool(¶m.typ) + && !is_hresult(¶m.typ) + && handle_type_name(¶m.typ).is_none() + { + return Err(format!( + "{}.{}: struct parameter `{}` requires native layout projection", + meta.interface.name, method.name, param.name + )); + } + if param.direction == ParamDirection::OutFill { + return Err(format!( + "{}.{}: caller-allocated array outputs are not supported", + meta.interface.name, method.name + )); + } + if param.direction == ParamDirection::InOut && !supports_in_out(¶m.typ) { + return Err(format!( + "{}.{}: unsupported [in, out] parameter `{}` of type {:?}", + meta.interface.name, method.name, param.name, param.typ + )); + } + } + if let Some(return_type) = method + .return_type + .as_ref() + .filter(|return_type| !is_hresult(return_type)) + { + if !supports_direct_return(return_type) { + return Err(format!( + "{}.{}: unsupported direct native return type {:?}", + meta.interface.name, method.name, return_type + )); + } + } + } + Ok(()) +} + +fn supports_in_out(t: &TypeMeta) -> bool { + is_win32_bool(t) + || is_hresult(t) + || handle_type_name(t).is_some() + || matches!( + t, + TypeMeta::Bool + | TypeMeta::I8 + | TypeMeta::U8 + | TypeMeta::I16 + | TypeMeta::U16 + | TypeMeta::I32 + | TypeMeta::U32 + | TypeMeta::I64 + | TypeMeta::U64 + | TypeMeta::F32 + | TypeMeta::F64 + | TypeMeta::Char16 + | TypeMeta::Enum { .. } + ) +} + +fn supports_direct_return(t: &TypeMeta) -> bool { + supports_in_out(t) +} + +pub(super) fn unwrap_return_js(t: &TypeMeta, expr: &str) -> String { + match string_buffer_encoding(t) { + Some(StringEncoding::Wide) => { + return format!("DynCom.takeCoTaskMemWideString({expr})"); + } + Some(StringEncoding::Ansi) => { + return format!("DynCom.takeCoTaskMemAnsiString({expr})"); + } + None => {} + } + if is_win32_bool(t) { + return format!("(DynCom.toNumber({expr}) !== 0)"); + } + if handle_type_name(t).is_some() { + return format!("DynCom.asPointerBigint({expr})"); + } + match t { + TypeMeta::Bool => format!("DynCom.toBool({expr})"), + TypeMeta::I8 + | TypeMeta::U8 + | TypeMeta::I16 + | TypeMeta::U16 + | TypeMeta::I32 + | TypeMeta::Char16 => format!("DynCom.toNumber({expr})"), + TypeMeta::U32 => format!("DynCom.toU32({expr})"), + TypeMeta::I64 => format!("DynCom.toI64Bigint({expr})"), + TypeMeta::U64 => format!("DynCom.toU64Bigint({expr})"), + TypeMeta::F32 | TypeMeta::F64 => format!("DynCom.toF64({expr})"), + TypeMeta::Guid => format!("DynCom.toGuidString({expr})"), + TypeMeta::Enum { underlying, .. } => unwrap_return_js(underlying, expr), + TypeMeta::String => format!("{expr}.toString()"), + TypeMeta::Interface { iid, .. } if !iid.is_empty() => expr.to_string(), + _ => expr.to_string(), + } +} + +#[derive(Clone, Copy)] +pub(super) struct MethodResult<'a> { + pub(super) typ: &'a TypeMeta, + pub(super) param_index: Option, +} + +pub(super) fn method_results(m: &MethodMeta) -> Vec> { + let mut result = Vec::new(); + if let Some(typ) = m.return_type.as_ref().filter(|typ| !is_hresult(typ)) { + result.push(MethodResult { + typ, + param_index: None, + }); + } + result.extend( + m.params + .iter() + .enumerate() + .filter(|(_, param)| { + matches!(param.direction, ParamDirection::Out | ParamDirection::InOut) + }) + .map(|(param_index, param)| MethodResult { + typ: ¶m.typ, + param_index: Some(param_index), + }), + ); + result +} + +pub(super) fn dts_return_type(m: &MethodMeta) -> String { + if string_buffer_pattern(m).is_some() { + let outputs = method_results(m); + if outputs.is_empty() { + return "string".to_string(); + } + return format!( + "[string, {}]", + outputs + .iter() + .map(|result| ts_result_type(m, *result)) + .collect::>() + .join(", ") + ); + } + let result_types = method_results(m); + match result_types.len() { + 0 => "void".to_string(), + 1 => ts_result_type(m, result_types[0]), + _ => format!( + "[{}]", + result_types + .iter() + .map(|result| ts_result_type(m, *result)) + .collect::>() + .join(", ") + ), + } +} + +fn ts_result_type(method: &MethodMeta, result: MethodResult<'_>) -> String { + if string_buffer_encoding(result.typ).is_some() { + "string".into() + } else if is_cotaskmem_owned(method, result) { + "DynWinRtValue".into() + } else { + ts_type_expr_dts(result.typ) + } +} + +pub(super) fn is_cotaskmem_owned(method: &MethodMeta, result: MethodResult<'_>) -> bool { + let Some(param_index) = result.param_index else { + return false; + }; + method + .owned_outputs + .iter() + .any(|owned| owned.param_index == param_index && owned.free_with.contains("CoTaskMemFree")) +} + +pub(super) fn dts_params_for_method(m: &MethodMeta) -> Vec { + let string_buffer = string_buffer_pattern(m); + m.params + .iter() + .enumerate() + .filter(|(_, param)| param.direction.is_input()) + .enumerate() + .map(|(surface_index, (param_index, param))| { + let mut name = js_param_name(¶m.name, surface_index); + if let Some((_, count_index, _)) = string_buffer { + if param_index >= count_index { + name.push('?'); + } + } + format!("{}: {}", name, ts_type_expr_dts(¶m.typ)) + }) + .collect() +} + +pub(super) fn collect_handle_aliases(meta: &ComInterfaceMeta) -> Vec { + let mut aliases = BTreeSet::new(); + for method in &meta.interface.methods { + for param in &method.params { + if let Some(alias) = handle_type_name(¶m.typ) { + aliases.insert(alias); + } + } + if let Some(alias) = method.return_type.as_ref().and_then(handle_type_name) { + aliases.insert(alias); + } + } + aliases.into_iter().collect() +} + +pub(super) fn enum_import_names(meta: &ComInterfaceMeta) -> Vec { + meta.referenced_enums + .iter() + .filter_map(|typ| match typ { + TypeMeta::Enum { name, .. } => Some(name.clone()), + _ => None, + }) + .collect() +} + +pub(super) fn uses_winrt_bridge_value(meta: &ComInterfaceMeta) -> bool { + for method in &meta.interface.methods { + for typ in method + .params + .iter() + .map(|param| ¶m.typ) + .chain(method.return_type.iter()) + { + if let TypeMeta::Interface { iid, .. } = typ { + if !iid.is_empty() { + return true; + } + } + } + } + false +} + +pub(super) fn has_string_buffer_method(meta: &ComInterfaceMeta) -> bool { + meta.interface + .methods + .iter() + .any(|method| string_buffer_pattern(method).is_some()) +} + +pub(super) fn string_buffer_pattern(method: &MethodMeta) -> Option<(usize, usize, StringEncoding)> { + for (index, param) in method.params.iter().enumerate() { + let ParamDirection::OutStringBuffer { count_param_index } = param.direction else { + continue; + }; + let encoding = string_buffer_encoding(¶m.typ)?; + if method + .params + .get(count_param_index) + .is_some_and(|count| count.direction == ParamDirection::In) + { + return Some((index, count_param_index, encoding)); + } + } + None +} + +fn string_buffer_encoding(t: &TypeMeta) -> Option { + match t { + TypeMeta::Struct { + namespace, name, .. + } if namespace == "Windows.Win32.Foundation" && name == "PWSTR" => { + Some(StringEncoding::Wide) + } + TypeMeta::Struct { + namespace, name, .. + } if namespace == "Windows.Win32.Foundation" && name == "PSTR" => { + Some(StringEncoding::Ansi) + } + _ => None, + } +} + +pub(super) fn is_optional_find_data_out_after_string_count(param: &ParamMeta) -> bool { + if param.direction != ParamDirection::Out { + return false; + } + let name = param.name.to_ascii_lowercase(); + if name == "pfd" || name.contains("finddata") || name.contains("find_data") { + return true; + } + matches!( + ¶m.typ, + TypeMeta::Struct { name, .. } if name == "WIN32_FIND_DATAW" || name == "WIN32_FIND_DATAA" + ) +} + +pub(super) fn ts_type_expr_dts(t: &TypeMeta) -> String { + if is_win32_bool(t) { + return "boolean".into(); + } + if is_hresult(t) { + return "number".into(); + } + if let Some(handle) = handle_type_name(t) { + return handle; + } + match t { + TypeMeta::Bool => "boolean".into(), + TypeMeta::I8 + | TypeMeta::U8 + | TypeMeta::I16 + | TypeMeta::U16 + | TypeMeta::I32 + | TypeMeta::U32 + | TypeMeta::F32 + | TypeMeta::F64 + | TypeMeta::Char16 => "number".into(), + TypeMeta::I64 | TypeMeta::U64 => "bigint".into(), + TypeMeta::String => "string".into(), + TypeMeta::Guid => "string".into(), + TypeMeta::Interface { iid, .. } if !iid.is_empty() => "DynWinRtValue".into(), + TypeMeta::Enum { name, .. } | TypeMeta::Struct { name, .. } => name.clone(), + _ => "bigint | Buffer".into(), + } +} + +pub(super) fn ts_type_expr_js(t: &TypeMeta) -> String { + if is_win32_bool(t) || is_hresult(t) { + return "DynCom.i32Type()".into(); + } + if handle_type_name(t).is_some() { + return "DynCom.pointerType()".into(); + } + if let TypeMeta::Interface { iid, .. } = t { + if !iid.is_empty() { + return format!("DynCom.interfaceType(WinGuid.parse('{iid}'))"); + } + } + match t { + TypeMeta::Bool => "DynCom.boolType()".into(), + TypeMeta::I8 => "DynCom.i8Type()".into(), + TypeMeta::U8 => "DynCom.u8Type()".into(), + TypeMeta::I16 => "DynCom.i16Type()".into(), + TypeMeta::U16 => "DynCom.u16Type()".into(), + TypeMeta::I32 => "DynCom.i32Type()".into(), + TypeMeta::U32 => "DynCom.u32Type()".into(), + TypeMeta::I64 => "DynCom.i64Type()".into(), + TypeMeta::U64 => "DynCom.u64Type()".into(), + TypeMeta::F32 => "DynCom.f32Type()".into(), + TypeMeta::F64 => "DynCom.f64Type()".into(), + TypeMeta::Char16 => "DynCom.char16Type()".into(), + TypeMeta::Guid => "DynCom.guidType()".into(), + TypeMeta::Enum { underlying, .. } => ts_type_expr_js(underlying), + _ => "DynCom.pointerType()".into(), + } +} + +pub(super) fn wrap_arg_js(t: &TypeMeta, var: &str) -> String { + if is_win32_bool(t) { + return format!("DynCom.i32({var} ? 1 : 0)"); + } + if is_hresult(t) { + return format!("DynCom.i32({var})"); + } + if handle_type_name(t).is_some() { + return format!("DynCom.pointer({var})"); + } + if let TypeMeta::Interface { iid, .. } = t { + if !iid.is_empty() { + return var.to_string(); + } + } + match t { + TypeMeta::Bool => format!("DynCom.boolValue({var})"), + TypeMeta::I8 => format!("DynCom.i8Value({var})"), + TypeMeta::U8 => format!("DynCom.u8Value({var})"), + TypeMeta::I16 => format!("DynCom.i16({var})"), + TypeMeta::U16 => format!("DynCom.u16({var})"), + TypeMeta::I32 => format!("DynCom.i32({var})"), + TypeMeta::U32 => format!("DynCom.u32({var})"), + TypeMeta::I64 => format!("DynCom.i64(BigInt({var}))"), + TypeMeta::U64 => format!("DynCom.u64(BigInt({var}))"), + TypeMeta::F32 => format!("DynCom.f32({var})"), + TypeMeta::F64 => format!("DynCom.f64({var})"), + TypeMeta::Char16 => format!("DynCom.char16({var})"), + TypeMeta::Guid => format!("DynCom.guid(WinGuid.parse({var}))"), + TypeMeta::Enum { underlying, .. } => wrap_arg_js(underlying, var), + _ => format!("DynCom.pointer({var})"), + } +} + +pub(super) fn handle_type_name(t: &TypeMeta) -> Option { + if is_win32_bool(t) { + return None; + } + match t { + TypeMeta::Struct { + namespace, + name, + fields, + } if is_win32_handle_namespace(namespace) + && !is_hresult_by_name(namespace, name) + && fields.len() == 1 + && fields[0].name == "Value" + && matches!( + fields[0].typ, + TypeMeta::Object | TypeMeta::U64 | TypeMeta::I64 | TypeMeta::U32 | TypeMeta::I32 + ) => + { + Some(name.clone()) + } + _ => None, + } +} + +fn is_win32_handle_namespace(namespace: &str) -> bool { + namespace.starts_with("Windows.Win32.") +} + +pub(super) fn is_hresult(t: &TypeMeta) -> bool { + matches!( + t, + TypeMeta::Struct { namespace, name, .. } + if is_hresult_by_name(namespace, name) + ) +} + +fn is_hresult_by_name(namespace: &str, name: &str) -> bool { + namespace == "Windows.Win32.Foundation" && name == "HRESULT" +} + +pub(super) fn is_win32_bool(t: &TypeMeta) -> bool { + matches!( + t, + TypeMeta::Struct { namespace, name, .. } + if namespace == "Windows.Win32.Foundation" && name == "BOOL" + ) +} diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/project/methods.rs b/tools/dynwinrt-codegen/src/codegen/javascript/project/methods.rs index ed494d16..c1a741b5 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/project/methods.rs +++ b/tools/dynwinrt-codegen/src/codegen/javascript/project/methods.rs @@ -10,9 +10,7 @@ fn projected_method_outputs(method: &MethodMeta) -> Vec<(usize, &TypeMeta)> { let mut outputs = Vec::new(); for param in &method.params { match param.direction { - ParamDirection::Out - | ParamDirection::OutFill - | ParamDirection::OutStringBuffer { .. } => { + ParamDirection::Out | ParamDirection::OutFill => { outputs.push((result_index, ¶m.typ)); result_index += 1; } diff --git a/tools/dynwinrt-codegen/src/codegen/python/type_helpers.rs b/tools/dynwinrt-codegen/src/codegen/python/type_helpers.rs index 59ced174..ffedcd14 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/type_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/python/type_helpers.rs @@ -207,8 +207,7 @@ pub(super) fn py_method_outputs(method: &MethodMeta) -> Vec<(usize, &TypeMeta)> for param in &method.params { match param.direction { - crate::meta::ParamDirection::Out - | crate::meta::ParamDirection::OutStringBuffer { .. } => { + crate::meta::ParamDirection::Out => { outputs.push((result_index, ¶m.typ)); result_index += 1; } diff --git a/tools/dynwinrt-codegen/src/codegen/shared/imports.rs b/tools/dynwinrt-codegen/src/codegen/shared/imports.rs index 9a821a7c..cf8e4047 100644 --- a/tools/dynwinrt-codegen/src/codegen/shared/imports.rs +++ b/tools/dynwinrt-codegen/src/codegen/shared/imports.rs @@ -218,9 +218,7 @@ pub(crate) fn method_abi_output_count(method: &MethodMeta) -> usize { .filter(|param| { matches!( param.direction, - ParamDirection::Out - | ParamDirection::OutFill - | ParamDirection::OutStringBuffer { .. } + ParamDirection::Out | ParamDirection::OutFill ) }) .count() @@ -231,7 +229,7 @@ pub(crate) fn fill_array_output_index(method: &MethodMeta) -> Option { let mut result_index = 0; for param in &method.params { match param.direction { - ParamDirection::Out | ParamDirection::OutStringBuffer { .. } => result_index += 1, + ParamDirection::Out => result_index += 1, ParamDirection::OutFill => return Some(result_index), ParamDirection::In => {} } diff --git a/tools/dynwinrt-codegen/src/com_metadata.rs b/tools/dynwinrt-codegen/src/com_metadata.rs new file mode 100644 index 00000000..c7c84b3d --- /dev/null +++ b/tools/dynwinrt-codegen/src/com_metadata.rs @@ -0,0 +1,576 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::HashSet; + +use windows_metadata::{HasAttributes, reader}; + +use crate::types::TypeMeta; + +#[derive(Debug, Clone, PartialEq)] +pub enum ParamDirection { + In, + Out, + InOut, + OutFill, + OutStringBuffer { count_param_index: usize }, +} + +impl ParamDirection { + pub fn is_input(&self) -> bool { + matches!(self, Self::In | Self::InOut) + } + + pub fn is_output(&self) -> bool { + matches!( + self, + Self::Out | Self::InOut | Self::OutFill | Self::OutStringBuffer { .. } + ) + } +} + +#[derive(Debug, Clone)] +pub struct ParamMeta { + pub name: String, + pub typ: TypeMeta, + pub direction: ParamDirection, +} + +#[derive(Debug, Clone, Default)] +pub struct MethodMeta { + pub name: String, + pub vtable_index: usize, + pub params: Vec, + pub return_type: Option, + pub doc: Option, + pub owned_outputs: Vec, +} + +#[derive(Debug, Clone)] +pub struct OwnedOutput { + pub param_index: usize, + pub free_with: String, +} + +#[derive(Debug, Clone, Default)] +pub struct InterfaceMeta { + pub name: String, + pub namespace: String, + pub iid: String, + pub methods: Vec, + pub generic_piid: Option, + pub generic_args: Vec, + pub doc: Option, + pub deprecated: Option, +} + +#[derive(Debug, Clone)] +pub struct ComInterfaceMeta { + pub interface: InterfaceMeta, + pub base_offset: usize, + pub is_iunknown_rooted: bool, + pub base_chain: Vec, + pub coclass_clsid: Option, + pub coclass_name: Option, + pub own_methods_start: usize, + pub referenced_enums: Vec, +} + +pub fn parse_com_interface( + winmd_paths: &str, + namespace: &str, + name: &str, +) -> Option { + let index = crate::meta::load_index(winmd_paths)?; + parse_com_interface_from_index(&index, namespace, name) +} + +fn parse_com_interface_from_index( + index: &reader::Index, + namespace: &str, + name: &str, +) -> Option { + let def = index.get(namespace, name).next()?; + if !def + .flags() + .contains(windows_metadata::TypeAttributes::Interface) + { + return None; + } + + let mut base_chain = Vec::new(); + let mut current = (namespace.to_string(), name.to_string()); + let mut root = None; + for _ in 0..32 { + let current_def = index.get(¤t.0, ¤t.1).next()?; + let base = match current_def.interface_impls().next()?.interface(&[]) { + windows_metadata::Type::Name(name) => (name.namespace, name.name), + _ => return None, + }; + match base.1.as_str() { + "IUnknown" => { + root = Some((true, 3)); + base_chain.push(( + "Windows.Win32.System.Com".to_string(), + "IUnknown".to_string(), + 0, + )); + break; + } + "IInspectable" => { + root = Some((false, 6)); + base_chain.push(( + "Windows.Foundation".to_string(), + "IInspectable".to_string(), + 0, + )); + break; + } + _ => { + let base_def = index.get(&base.0, &base.1).next()?; + let count = base_def.methods().count(); + base_chain.push((base.0.clone(), base.1.clone(), count)); + current = base; + } + } + } + let (is_iunknown_rooted, root_offset) = root?; + let own_methods_start = root_offset + + base_chain + .iter() + .filter(|(_, name, _)| name != "IUnknown" && name != "IInspectable") + .map(|(_, _, count)| count) + .sum::(); + + let mut methods = Vec::new(); + let mut slot = root_offset; + for (base_namespace, base_name, _) in base_chain + .iter() + .rev() + .filter(|(_, name, _)| name != "IUnknown" && name != "IInspectable") + { + let base_def = index.get(base_namespace, base_name).next()?; + let mut base_methods = parse_methods(index, &base_def, slot); + slot += base_methods.len(); + methods.append(&mut base_methods); + } + if slot != own_methods_start { + return None; + } + methods.extend(parse_methods(index, &def, slot)); + + let iid = crate::meta::extract_iid(&def); + let interface = InterfaceMeta { + name: name.to_string(), + namespace: namespace.to_string(), + iid, + methods, + generic_piid: None, + generic_args: Vec::new(), + doc: None, + deprecated: None, + }; + let (coclass_name, coclass_clsid) = find_coclass(index, namespace, name); + let referenced_enums = collect_referenced_enums(&interface); + + Some(ComInterfaceMeta { + interface, + base_offset: root_offset, + is_iunknown_rooted, + base_chain: base_chain.into_iter().map(|(_, name, _)| name).collect(), + coclass_clsid, + coclass_name, + own_methods_start, + referenced_enums, + }) +} + +fn parse_methods( + index: &reader::Index, + def: &reader::TypeDef, + base_offset: usize, +) -> Vec { + def.methods() + .enumerate() + .map(|(index_in_interface, method)| { + let signature = method.signature(&[]); + let raw_name = method.name().to_string(); + let name = method + .find_attribute("OverloadAttribute") + .and_then(|attribute| { + attribute + .value() + .into_iter() + .next() + .and_then(|(_, value)| match value { + windows_metadata::Value::Utf8(value) => Some(value), + _ => None, + }) + }) + .unwrap_or(raw_name); + let mut params = Vec::new(); + let mut owned_outputs = Vec::new(); + for (param_index, (param, typ)) in method + .params() + .filter(|param| param.sequence() > 0) + .zip(signature.types.iter()) + .enumerate() + { + let direction = classify_direction( + param.flags(), + matches!(typ, windows_metadata::Type::Array(_)), + ); + let free_with = + param + .find_attribute("FreeWithAttribute") + .and_then(|attribute| { + attribute.value().into_iter().next().and_then( + |(_, value)| match value { + windows_metadata::Value::Utf8(value) => Some(value), + _ => None, + }, + ) + }) + .or_else(|| known_free_with(typ, &direction)); + if let Some(free_with) = free_with { + owned_outputs.push(OwnedOutput { + param_index, + free_with, + }); + } + params.push(ParamMeta { + name: param.name().to_string(), + typ: map_parameter_type(typ, &direction, index), + direction, + }); + } + mark_caller_owned_string_buffers(&mut params); + let return_type = (signature.return_type != windows_metadata::Type::Void) + .then(|| map_return_type(&signature.return_type, index)); + MethodMeta { + name, + vtable_index: base_offset + index_in_interface, + params, + return_type, + doc: None, + owned_outputs, + } + }) + .collect() +} + +fn known_free_with(typ: &windows_metadata::Type, direction: &ParamDirection) -> Option { + // Windows.Win32.winmd omits FreeWith on IShellLink::GetIDList. + let (windows_metadata::Type::PtrMut(inner, depth) + | windows_metadata::Type::PtrConst(inner, depth)) = typ + else { + return None; + }; + if !matches!(direction, ParamDirection::Out | ParamDirection::InOut) || *depth < 2 { + return None; + } + match inner.as_ref() { + windows_metadata::Type::Name(name) + if name.namespace == "Windows.Win32.UI.Shell.Common" && name.name == "ITEMIDLIST" => + { + Some("CoTaskMemFree".into()) + } + _ => None, + } +} + +fn map_parameter_type( + typ: &windows_metadata::Type, + direction: &ParamDirection, + index: &reader::Index, +) -> TypeMeta { + use windows_metadata::Type; + + match typ { + Type::PtrMut(inner, depth) | Type::PtrConst(inner, depth) => { + if matches!(direction, ParamDirection::Out | ParamDirection::InOut) && *depth == 1 { + crate::meta::map_winmd_type_with_generics(inner, index, &[]) + } else { + TypeMeta::Object + } + } + Type::ConstRef(inner) + if matches!(direction, ParamDirection::Out | ParamDirection::InOut) => + { + crate::meta::map_winmd_type_with_generics(inner, index, &[]) + } + Type::ConstRef(_) | Type::ISize | Type::USize => match typ { + Type::ISize => TypeMeta::I64, + Type::USize => TypeMeta::U64, + _ => TypeMeta::Object, + }, + _ => crate::meta::map_winmd_type_with_generics(typ, index, &[]), + } +} + +fn map_return_type(typ: &windows_metadata::Type, index: &reader::Index) -> TypeMeta { + use windows_metadata::Type; + + match typ { + Type::PtrMut(_, _) | Type::PtrConst(_, _) | Type::ConstRef(_) => TypeMeta::Object, + Type::ISize => TypeMeta::I64, + Type::USize => TypeMeta::U64, + _ => crate::meta::map_winmd_type_with_generics(typ, index, &[]), + } +} + +fn classify_direction(flags: windows_metadata::ParamAttributes, is_array: bool) -> ParamDirection { + let is_in = flags.contains(windows_metadata::ParamAttributes::In); + let is_out = flags.contains(windows_metadata::ParamAttributes::Out); + match (is_in, is_out, is_array) { + (true, true, _) => ParamDirection::InOut, + (_, true, true) => ParamDirection::OutFill, + (_, true, false) => ParamDirection::Out, + _ => ParamDirection::In, + } +} + +fn find_coclass( + index: &reader::Index, + namespace: &str, + interface_name: &str, +) -> (Option, Option) { + let Some(stripped) = interface_name.strip_prefix('I') else { + return (None, None); + }; + let mut candidates = vec![stripped.to_string()]; + let without_version = stripped + .trim_end_matches(|character: char| character.is_ascii_digit()) + .to_string(); + if without_version != stripped { + candidates.push(without_version); + } + for candidate in candidates { + let Some(def) = index.get(namespace, &candidate).next() else { + continue; + }; + let is_coclass = matches!( + def.extends() + .map(|base| (base.namespace().to_string(), base.name().to_string())), + Some((namespace, name)) if namespace == "System" && name == "ValueType" + ); + if is_coclass { + let clsid = crate::meta::extract_iid(&def); + if !clsid.is_empty() { + return (Some(candidate), Some(clsid)); + } + } + } + (None, None) +} + +fn collect_referenced_enums(interface: &InterfaceMeta) -> Vec { + let mut names = HashSet::new(); + let mut result = Vec::new(); + for method in &interface.methods { + for typ in method + .params + .iter() + .map(|param| ¶m.typ) + .chain(method.return_type.iter()) + { + if let TypeMeta::Enum { name, .. } = typ { + if names.insert(name.clone()) { + result.push(typ.clone()); + } + } + } + } + result +} + +fn mark_caller_owned_string_buffers(params: &mut [ParamMeta]) { + for index in 0..params.len().saturating_sub(1) { + if params[index].direction == ParamDirection::Out + && is_string_buffer(¶ms[index].typ) + && params[index + 1].direction == ParamDirection::In + && is_string_buffer_count(¶ms[index].typ, ¶ms[index + 1]) + { + params[index].direction = ParamDirection::OutStringBuffer { + count_param_index: index + 1, + }; + } + } + let count_index = params.iter().find_map(|param| match param.direction { + ParamDirection::OutStringBuffer { count_param_index } => Some(count_param_index), + _ => None, + }); + if let Some(count_index) = count_index { + for param in params.iter_mut().skip(count_index + 1) { + let name = param.name.to_ascii_lowercase(); + let is_find_data = name == "pfd" + || name.contains("finddata") + || matches!( + ¶m.typ, + TypeMeta::Struct { name, .. } + if name == "WIN32_FIND_DATAW" || name == "WIN32_FIND_DATAA" + ); + if is_find_data + && matches!(param.direction, ParamDirection::Out | ParamDirection::InOut) + { + param.direction = ParamDirection::In; + param.typ = TypeMeta::Object; + } + } + } +} + +fn is_string_buffer(typ: &TypeMeta) -> bool { + matches!( + typ, + TypeMeta::Struct { namespace, name, .. } + if namespace == "Windows.Win32.Foundation" && (name == "PWSTR" || name == "PSTR") + ) +} + +fn is_string_buffer_count(buffer_type: &TypeMeta, param: &ParamMeta) -> bool { + let name = param.name.to_ascii_lowercase(); + let is_wide = matches!( + buffer_type, + TypeMeta::Struct { namespace, name, .. } + if namespace == "Windows.Win32.Foundation" && name == "PWSTR" + ); + matches!(param.typ, TypeMeta::I32 | TypeMeta::U32) + && (name.starts_with("cch") + || (!is_wide && name.starts_with("cb")) + || matches!(name.as_str(), "len" | "length" | "size" | "max" | "count") + || name.starts_with("max") + || name.starts_with("size")) +} + +pub fn find_runtime_class_default_iid( + winmd_paths: &str, + simple_name: &str, +) -> Option<(String, String, String)> { + let index = crate::meta::load_index(winmd_paths)?; + let mut found = None; + let mut collision = false; + for def in index.all() { + if def.name() != simple_name + || !def + .flags() + .contains(windows_metadata::TypeAttributes::WindowsRuntime) + || def + .flags() + .contains(windows_metadata::TypeAttributes::Interface) + { + continue; + } + for implementation in def.interface_impls() { + if !implementation.has_attribute("DefaultAttribute") { + continue; + } + let windows_metadata::Type::Name(name) = implementation.interface(&[]) else { + continue; + }; + if !name.generics.is_empty() { + continue; + } + let interface = index.get(&name.namespace, &name.name).next()?; + let iid = crate::meta::extract_iid(&interface); + if iid.is_empty() { + continue; + } + let candidate = (def.namespace().to_string(), name.name, iid); + match &found { + None => found = Some(candidate), + Some(existing) if existing == &candidate => {} + Some(_) => collision = true, + } + break; + } + } + (!collision).then_some(found).flatten() +} + +pub fn discover_newest_windows_winmd() -> Option { + let base = std::path::Path::new(r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata"); + let mut versions = std::fs::read_dir(base) + .ok()? + .filter_map(Result::ok) + .filter(|entry| entry.path().is_dir()) + .map(|entry| entry.file_name().to_string_lossy().to_string()) + .filter(|name| name.starts_with("10.")) + .collect::>(); + versions.sort_by_key(|version| { + version + .split('.') + .filter_map(|part| part.parse::().ok()) + .collect::>() + }); + versions.into_iter().rev().find_map(|version| { + let path = base.join(version).join("Windows.winmd"); + path.exists().then(|| path.to_string_lossy().to_string()) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn in_out_is_com_only() { + use windows_metadata::ParamAttributes; + + assert_eq!( + classify_direction(ParamAttributes::In | ParamAttributes::Out, false), + ParamDirection::InOut + ); + } + + #[test] + fn find_data_after_string_buffer_is_caller_owned_pointer() { + let mut params = vec![ + ParamMeta { + name: "pszFile".into(), + typ: TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "PWSTR".into(), + fields: Vec::new(), + }, + direction: ParamDirection::Out, + }, + ParamMeta { + name: "cch".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ParamMeta { + name: "pfd".into(), + typ: TypeMeta::Object, + direction: ParamDirection::InOut, + }, + ]; + + mark_caller_owned_string_buffers(&mut params); + + assert_eq!( + params[0].direction, + ParamDirection::OutStringBuffer { + count_param_index: 1 + } + ); + assert_eq!(params[2].direction, ParamDirection::In); + assert!(matches!(params[2].typ, TypeMeta::Object)); + } + + #[test] + fn item_id_list_double_pointer_uses_cotaskmem_ownership() { + let typ = windows_metadata::Type::PtrMut( + Box::new(windows_metadata::Type::named( + "Windows.Win32.UI.Shell.Common", + "ITEMIDLIST", + )), + 2, + ); + assert_eq!( + known_free_with(&typ, &ParamDirection::Out).as_deref(), + Some("CoTaskMemFree") + ); + } +} diff --git a/tools/dynwinrt-codegen/src/lib.rs b/tools/dynwinrt-codegen/src/lib.rs index bd1e1af3..f3e29f4a 100644 --- a/tools/dynwinrt-codegen/src/lib.rs +++ b/tools/dynwinrt-codegen/src/lib.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. pub mod codegen; +pub mod com_metadata; pub mod meta; pub mod types; pub mod xml_doc; diff --git a/tools/dynwinrt-codegen/src/main.rs b/tools/dynwinrt-codegen/src/main.rs index a062dd50..9d167759 100644 --- a/tools/dynwinrt-codegen/src/main.rs +++ b/tools/dynwinrt-codegen/src/main.rs @@ -12,6 +12,7 @@ use dynwinrt_codegen::codegen::python; use dynwinrt_codegen::codegen::render_package_json; use dynwinrt_codegen::codegen::typescript; use dynwinrt_codegen::codegen::{project, render_dts, render_js}; +use dynwinrt_codegen::com_metadata; use dynwinrt_codegen::meta; use dynwinrt_codegen::types::TypeMeta; use dynwinrt_codegen::xml_doc::DocTable; @@ -272,9 +273,9 @@ fn run() -> Result<(), String> { // First: partition into WinRT classes and classic-COM interfaces. let mut classes = Vec::new(); - let mut com_interfaces: Vec = Vec::new(); + let mut com_interfaces: Vec = Vec::new(); for cls in &class_names { - if let Some(com_iface) = meta::parse_com_interface(&winmd, ns, cls) { + if let Some(com_iface) = com_metadata::parse_com_interface(&winmd, ns, cls) { // Route through classic-COM path when: // 1) The interface is IUnknown-rooted (base +3), OR // 2) It is a `*Interop` bridge (name ends with "Interop") — even diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index 85887bd3..e0e6b45e 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -15,11 +15,6 @@ pub enum ParamDirection { Out, /// FillArray: caller allocates buffer, callee fills it. OutFill, - /// Caller-owned classic-COM string buffer, immediately sized by another - /// input parameter. - OutStringBuffer { - count_param_index: usize, - }, } /// A single method parameter. @@ -161,137 +156,6 @@ pub fn parse_class(winmd_paths: &str, namespace: &str, name: &str) -> Option Option<(String, String, String)> { - let index = load_index(winmd_paths)?; - // Collect *all* runtime classes with this simple name so we can detect - // cross-namespace collisions (e.g. two runtime classes both called - // `SomeThing` in different namespaces). Returning the first match blindly - // would silently drive interop codegen with the wrong default-interface - // IID → wrappers that call `GetForWindow(riid=…, ppv)` for a different - // interface than the caller expects. - let mut found: Option<(String, String, String)> = None; - let mut collisions: Vec<(String, String, String)> = Vec::new(); - for def in index.all() { - if def.name() != simple_name { - continue; - } - // A WinRT runtime class extends System.Object AND carries the - // WindowsRuntime flag on its type. Interfaces extend nothing; - // classes extend Object/etc. We filter to actual runtime classes. - if !def - .flags() - .contains(windows_metadata::TypeAttributes::WindowsRuntime) - { - continue; - } - // Must be a class (not interface/enum/struct). - if def - .flags() - .contains(windows_metadata::TypeAttributes::Interface) - { - continue; - } - let namespace = def.namespace().to_string(); - // Look for the default interface via DefaultAttribute. - for iface_impl in def.interface_impls() { - if !iface_impl.has_attribute("DefaultAttribute") { - continue; - } - let iface_ty = iface_impl.interface(&[]); - let windows_metadata::Type::Name(tn) = &iface_ty else { - continue; - }; - // Resolve concrete (non-generic) interface's IID from its TypeDef. - if !tn.generics.is_empty() { - // Skip generic default interfaces — interop projections don't - // hit them in practice, and the parameterized IID would need - // separate computation. - continue; - } - let Some(iface_def) = index.get(&tn.namespace, &tn.name).next() else { - // Unreadable/missing TypeDef for this DefaultAttribute impl - // — skip *this* candidate rather than aborting the whole - // lookup. Other matching runtime classes (or other - // DefaultAttribute impls on the same class) can still resolve - // successfully. - continue; - }; - let iid = extract_iid(&iface_def); - if iid.is_empty() { - continue; - } - let candidate = (namespace.clone(), tn.name.clone(), iid); - match &found { - None => found = Some(candidate), - Some(prev) if prev == &candidate => { - // Exact duplicate — same namespace + same IID means the - // same TypeDef, harmless. - } - Some(_) => collisions.push(candidate), - } - break; // stop looking at this class's other interface_impls - } - } - if !collisions.is_empty() { - let mut all = vec![found.clone().unwrap()]; - all.extend(collisions); - eprintln!( - "warning: find_runtime_class_default_iid({}): multiple runtime classes with this simple name resolve to distinct default IIDs — refusing to guess. Candidates: {:?}", - simple_name, all - ); - return None; - } - found -} - -/// Discover the NEWEST installed Windows SDK `Windows.winmd` by enumerating the -/// versioned directories under `C:\Program Files (x86)\Windows Kits\10\UnionMetadata` -/// and picking the highest version that actually contains a readable file. -/// -/// Used as a portable fallback by the classic-COM interop code generator when -/// the winmds explicitly loaded for generation don't contain the projected -/// WinRT runtime class. Returns `None` when no SDK is installed. -pub fn discover_newest_windows_winmd() -> Option { - let base = std::path::Path::new(r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata"); - if !base.exists() { - return None; - } - let mut versions: Vec = std::fs::read_dir(base) - .ok()? - .filter_map(|e| e.ok()) - .filter(|e| e.path().is_dir()) - .map(|e| e.file_name().to_string_lossy().to_string()) - .filter(|name| name.starts_with("10.")) - .collect(); - // Sort by dotted-version tuple so `10.0.26100.0` beats `10.0.19041.0`. - versions.sort_by(|a, b| { - let pa: Vec = a.split('.').filter_map(|s| s.parse().ok()).collect(); - let pb: Vec = b.split('.').filter_map(|s| s.parse().ok()).collect(); - pa.cmp(&pb) - }); - for version in versions.iter().rev() { - let winmd_path = base.join(version).join("Windows.winmd"); - if winmd_path.exists() { - return Some(winmd_path.to_string_lossy().to_string()); - } - } - None -} - /// Parse all RuntimeClasses in a given namespace. pub fn parse_namespace(winmd_paths: &str, namespace: &str) -> Vec { let index = match load_index(winmd_paths) { @@ -815,7 +679,7 @@ pub fn expand_winmd_paths(winmd_paths: &str) -> String { all_paths.join(";") } -fn load_index(winmd_paths: &str) -> Option { +pub(crate) fn load_index(winmd_paths: &str) -> Option { let paths: Vec<&str> = winmd_paths.split(';').filter(|s| !s.is_empty()).collect(); if paths.is_empty() { eprintln!("warning: no winmd paths provided"); @@ -1067,302 +931,7 @@ fn split_full_name(full_name: &str) -> Option<(&str, &str)> { fn parse_interface(index: &reader::Index, namespace: &str, name: &str) -> Option { let def = index.get(namespace, name).next()?; let iid = extract_iid(&def); - parse_interface_methods(index, &def, name, namespace, &iid, &[], 6) -} - -// ========================================================================== -// Classic-COM (option A) support -// ========================================================================== - -/// Rich metadata for a classic-COM interface discovered by walking the -/// `interface_impls()` chain. The `interface.methods` list is the *flattened* -/// method set (own + all inherited, excluding IUnknown's QI/AddRef/Release) -/// with absolute vtable indices — so the codegen renderer never has to think -/// about inheritance again. -/// -/// This is entirely separate from the WinRT `parse_class`/`parse_interface` -/// path so we do not risk regressing IInspectable-based generation. -#[derive(Debug, Clone)] -pub struct ComInterfaceMeta { - /// Flattened interface with own + inherited methods, absolute vtable indices. - pub interface: InterfaceMeta, - /// The vtable index of the first user method in the flattened list: - /// - `3` for any IUnknown-rooted interface (QI/AddRef/Release occupy 0..2). - /// - `6` for any IInspectable-rooted interface (WinRT projection layout). - pub base_offset: usize, - /// `true` iff the inheritance chain terminates at IUnknown. - /// `false` iff it terminates at IInspectable (WinRT-projected classic COM). - pub is_iunknown_rooted: bool, - /// Ordered list of base names from immediate parent up to the root - /// (e.g. `["ITaskbarList2", "ITaskbarList", "IUnknown"]`). - pub base_chain: Vec, - /// If a Win32 coclass matches this interface, the coclass GUID (=CLSID). - pub coclass_clsid: Option, - /// The name of the discovered coclass, e.g. `"TaskbarList"`. - pub coclass_name: Option, - /// The absolute vtable slot of this leaf interface's first *own* method - /// (i.e. the number of methods contributed by all bases plus the root - /// offset). Renderer helper — not core metadata. - pub own_methods_start: usize, - /// Enum types referenced by this interface's methods (directly resolved - /// during metadata parsing so codegen can emit them without a second - /// resolve_dependencies pass over the whole namespace). - pub referenced_enums: Vec, -} - -/// Parse a classic-COM interface (IUnknown-rooted) by name, walking the -/// `interface_impls()` chain to compute absolute vtable slots and flatten -/// inherited methods. -/// -/// Returns `None` if the type isn't found. Unlike `parse_interface`, this -/// function also handles interfaces that inherit from other classic-COM -/// interfaces via `interface_impls()` (the Windows.Win32 winmd doesn't -/// use `[NativeInheritance]` attributes — it uses actual InterfaceImpl rows). -pub fn parse_com_interface( - winmd_paths: &str, - namespace: &str, - name: &str, -) -> Option { - let index = load_index(winmd_paths)?; - parse_com_interface_from_index(&index, namespace, name) -} - -fn parse_com_interface_from_index( - index: &reader::Index, - namespace: &str, - name: &str, -) -> Option { - let def = index.get(namespace, name).next()?; - - // Guard: refuse to treat non-interface TypeDefs (WinRT runtime classes, - // enums, structs, delegates) as classic-COM interfaces. Without this, - // routing a name that happens to resolve to e.g. a `*Interop` runtime - // class through this path would walk its `interface_impls()` and produce - // a bogus flattened method list. Callers see `None` and can fall through - // to the correct WinRT code path in `main.rs`. - if !def - .flags() - .contains(windows_metadata::TypeAttributes::Interface) - { - return None; - } - - // Walk the interface_impls chain: for each base, collect its own method - // count, and stop at IUnknown or IInspectable. Traverse from the leaf up - // so we can compute cumulative offsets. - let mut base_chain: Vec<(String, String, usize)> = Vec::new(); // (ns, name, own_method_count) - let mut cur_ns = namespace.to_string(); - let mut cur_name = name.to_string(); - let mut is_iunknown_rooted = false; - // Explicit-termination flag: set only when the walk reaches a well-known - // COM/WinRT root (IUnknown or IInspectable). If we exit the loop without - // this being set — malformed/incomplete winmd, missing `interface_impls`, - // or a depth-limit overrun — the offset-3 vs. offset-6 decision below - // would be guesswork. In that case we return None rather than emit code - // with silently-wrong vtable slots. - let mut terminated_at_known_root = false; - - // Walk up to 32 levels deep as a safety limit (real chains are 3-4 deep). - for _ in 0..32 { - let cur_def = match index.get(&cur_ns, &cur_name).next() { - Some(d) => d, - None => break, - }; - // Find the (single) base via interface_impls. - let base_ii = cur_def.interface_impls().next(); - let base_type = base_ii.map(|ii| ii.interface(&[])); - let base = match base_type { - Some(windows_metadata::Type::Name(tn)) => (tn.namespace.clone(), tn.name.clone()), - _ => break, - }; - // Terminate at IUnknown or IInspectable. - if base.1 == "IUnknown" { - is_iunknown_rooted = true; - terminated_at_known_root = true; - base_chain.push(( - "Windows.Win32.System.Com".to_string(), - "IUnknown".to_string(), - 0, - )); - break; - } - if base.1 == "IInspectable" { - terminated_at_known_root = true; - base_chain.push(( - "Windows.Foundation".to_string(), - "IInspectable".to_string(), - 0, - )); - break; - } - // Otherwise this base is a real classic-COM interface — count its methods. - let base_def = match index.get(&base.0, &base.1).next() { - Some(d) => d, - None => break, - }; - let own_count = base_def.methods().count(); - base_chain.push((base.0.clone(), base.1.clone(), own_count)); - cur_ns = base.0; - cur_name = base.1; - } - - // Refuse to guess a root offset when the walk didn't terminate cleanly: - // an unknown-shape base chain would produce wrong absolute vtable slots - // and therefore wrong method dispatch. Callers see `None` and can log / - // surface a clearer error than a silent mis-generation. - if !terminated_at_known_root { - eprintln!( - "warning: base-chain walk for {}.{} did not terminate at IUnknown or IInspectable — refusing to guess vtable root offset", - namespace, name - ); - return None; - } - - // Compute root offset (3 for IUnknown, 6 for IInspectable) and the - // absolute vtable slot at which THIS leaf interface's own methods start. - let root_offset = if is_iunknown_rooted { 3 } else { 6 }; - let intermediate_methods: usize = base_chain - .iter() - .filter(|(_, name, _)| name != "IUnknown" && name != "IInspectable") - .map(|(_, _, c)| *c) - .sum(); - let own_methods_start = root_offset + intermediate_methods; - - // Build a flattened method list: iterate the chain top-down (from root - // toward the leaf, i.e. reverse `base_chain`), assigning consecutive - // vtable slots. Base interfaces contribute their own methods first. - // - // Vtable layout: [IUnknown 0..2] [base_N 3..] [base_{N-1} ...] ... [leaf's own]. - let mut methods: Vec = Vec::new(); - - let mut slot_cursor = root_offset; - // Reverse: iterate from the outermost base (closest to IUnknown) down - // toward the immediate parent. - let mut chain_top_down: Vec<&(String, String, usize)> = base_chain.iter().rev().collect(); - // Filter out the root (IUnknown/IInspectable, which contribute 0 own methods to the vtable - // *from the user-visible perspective* — their slots are already counted in `root_offset`). - chain_top_down.retain(|(_, n, _)| n != "IUnknown" && n != "IInspectable"); - - for (base_ns, base_name, _own_count) in chain_top_down { - match parse_interface_with_offset(index, base_ns, base_name, slot_cursor) { - Some(base_iface) => { - slot_cursor += base_iface.methods.len(); - methods.extend(base_iface.methods); - } - None => { - // Fail loud: if we can't parse a base interface's methods, - // the flattened method list would be missing entries and the - // leaf's absolute vtable indices would be wrong. In release - // the `debug_assert_eq!` below is compiled out, so we'd - // silently emit wrappers that dispatch to the wrong COM - // methods. Return None so callers surface a clear error. - eprintln!( - "warning: could not parse base classic-COM interface {}.{} — refusing to emit {}.{} with a truncated vtable", - base_ns, base_name, namespace, name - ); - return None; - } - } - } - // Assert the invariant that we lined up correctly. - debug_assert_eq!( - slot_cursor, own_methods_start, - "vtable cursor {} != computed own_methods_start {}", - slot_cursor, own_methods_start - ); - - // Now the leaf's own methods - let iid = extract_iid(&def); - let own = parse_interface_methods(index, &def, name, namespace, &iid, &[], slot_cursor)?; - methods.extend(own.methods); - - // Build a mostly-standard InterfaceMeta wrapping the flattened method list. - let interface = InterfaceMeta { - name: name.to_string(), - namespace: namespace.to_string(), - iid: iid.clone(), - methods, - generic_piid: None, - generic_args: Vec::new(), - doc: None, - deprecated: None, - }; - - // Discover coclass CLSID. Heuristic: strip leading `I` from the interface - // name, then strip trailing digits (e.g. `ITaskbarList3` → `TaskbarList3` - // → `TaskbarList`). Return the first coclass matching either variant that - // has a GuidAttribute AND `extends System.ValueType`. - let mut candidates: Vec = Vec::new(); - if let Some(stripped) = name.strip_prefix('I') { - candidates.push(stripped.to_string()); - // Also try trimming trailing digits: TaskbarList3 → TaskbarList - let trimmed: String = stripped - .trim_end_matches(|c: char| c.is_ascii_digit()) - .to_string(); - if trimmed != stripped { - candidates.push(trimmed); - } - } - let mut coclass_clsid: Option = None; - let mut coclass_name: Option = None; - for cand in &candidates { - if let Some(cc_def) = index.get(namespace, cand).next() { - let ext = cc_def.extends(); - let is_coclass_shape = matches!( - ext.map(|e| (e.namespace().to_string(), e.name().to_string())), - Some((ref ns, ref n)) if ns == "System" && n == "ValueType" - ); - if !is_coclass_shape { - continue; - } - let cc_iid = extract_iid(&cc_def); - if !cc_iid.is_empty() { - coclass_clsid = Some(cc_iid); - coclass_name = Some(cand.clone()); - break; - } - } - } - - // Collect enum types referenced in methods' parameters (direct only). - let mut referenced_enums: Vec = Vec::new(); - let mut seen_enum_names: HashSet = HashSet::new(); - for m in &interface.methods { - for p in &m.params { - if let TypeMeta::Enum { .. } = &p.typ { - if let TypeMeta::Enum { name: en, .. } = &p.typ { - if seen_enum_names.insert(en.clone()) { - referenced_enums.push(p.typ.clone()); - } - } - } - } - } - - Some(ComInterfaceMeta { - interface, - base_offset: root_offset, - is_iunknown_rooted, - base_chain: base_chain.into_iter().map(|(_, n, _)| n).collect(), - coclass_clsid, - coclass_name, - own_methods_start, - referenced_enums, - }) -} - -/// Parse an interface's OWN methods (no inheritance flattening) with a caller- -/// supplied base offset. Used by `parse_com_interface_from_index` to lay out -/// base-class methods at the correct absolute vtable slots. -fn parse_interface_with_offset( - index: &reader::Index, - namespace: &str, - name: &str, - base_offset: usize, -) -> Option { - let def = index.get(namespace, name).next()?; - let iid = extract_iid(&def); - parse_interface_methods(index, &def, name, namespace, &iid, &[], base_offset) + parse_interface_methods(index, &def, name, namespace, &iid, &[]) } fn parse_interface_type( @@ -1408,15 +977,10 @@ fn parse_parameterized_interface( ) -> Option { let trimmed_name = generic_name.split('`').next().unwrap_or(generic_name); let def = index.get(namespace, trimmed_name).next()?; - parse_interface_methods(index, &def, concrete_name, namespace, piid, generic_args, 6) + parse_interface_methods(index, &def, concrete_name, namespace, piid, generic_args) } /// Core interface parsing: extract methods from a TypeDef, optionally substituting generics. -/// -/// `base_offset` is the vtable index of the first user method: -/// - `6` for WinRT (IInspectable-rooted: QI/AddRef/Release + GetIids/GetRuntimeClassName/GetTrustLevel). -/// - `3` for classic-COM IUnknown-rooted interfaces (QI/AddRef/Release only). -/// - Or any absolute offset for a base-aware slot in a chained classic-COM interface. fn parse_interface_methods( index: &reader::Index, def: &reader::TypeDef, @@ -1424,14 +988,13 @@ fn parse_interface_methods( namespace: &str, iid: &str, generic_args: &[TypeMeta], - base_offset: usize, ) -> Option { let winmd_generics: Vec = generic_args.iter().map(type_meta_to_winmd_type).collect(); let mut methods = Vec::new(); for (i, method) in def.methods().enumerate() { - let vtable_index = base_offset + i; + let vtable_index = 6 + i; let sig = method.signature(&winmd_generics); let raw_name = method.name().to_string(); @@ -1449,14 +1012,10 @@ fn parse_interface_methods( for (j, param_def) in param_defs.iter().enumerate() { if j < sig.types.len() { clr_sig_types.push(clr_type_name(&sig.types[j])); + let typ = map_winmd_type_with_generics(&sig.types[j], index, generic_args); let is_out = param_def .flags() .contains(windows_metadata::ParamAttributes::Out); - let typ = if is_out { - map_winmd_out_param_type(&sig.types[j], index, generic_args) - } else { - map_winmd_type_with_generics(&sig.types[j], index, generic_args) - }; let direction = if is_out { if matches!(sig.types[j], windows_metadata::Type::Array(_)) { // [out] Array = FillArray (caller allocates buffer, callee fills) @@ -1474,7 +1033,6 @@ fn parse_interface_methods( }); } } - mark_caller_owned_string_buffers(&mut params); let return_type = if sig.return_type == windows_metadata::Type::Void { None @@ -1636,7 +1194,7 @@ fn type_meta_to_winmd_type(typ: &TypeMeta) -> windows_metadata::Type { } } -fn extract_iid(def: &reader::TypeDef) -> String { +pub(crate) fn extract_iid(def: &reader::TypeDef) -> String { if let Some(attr) = def.find_attribute("GuidAttribute") { let args: Vec<(String, windows_metadata::Value)> = attr.value(); if args.len() >= 11 { @@ -1732,59 +1290,11 @@ fn parse_enum_def(def: &reader::TypeDef) -> TypeMeta { } } -fn mark_caller_owned_string_buffers(params: &mut [ParamMeta]) { - if params.len() < 2 { - return; - } - for idx in 0..params.len() - 1 { - if params[idx].direction == ParamDirection::Out - && is_direct_win32_string_buffer(¶ms[idx].typ) - && params[idx + 1].direction == ParamDirection::In - && is_string_buffer_count_param(¶ms[idx].typ, ¶ms[idx + 1]) - { - params[idx].direction = ParamDirection::OutStringBuffer { - count_param_index: idx + 1, - }; - } - } -} - -fn is_direct_win32_string_buffer(t: &TypeMeta) -> bool { - matches!( - t, - TypeMeta::Struct { namespace, name, .. } - if namespace == "Windows.Win32.Foundation" && (name == "PWSTR" || name == "PSTR") - ) -} - -fn is_pwstr_type(t: &TypeMeta) -> bool { - matches!( - t, - TypeMeta::Struct { - namespace, name, .. - } if namespace == "Windows.Win32.Foundation" && name == "PWSTR" - ) -} - -fn is_string_buffer_count_param(buffer_type: &TypeMeta, p: &ParamMeta) -> bool { - let n = p.name.to_ascii_lowercase(); - matches!(p.typ, TypeMeta::I32 | TypeMeta::U32) - && (n.starts_with("cch") - || (!is_pwstr_type(buffer_type) && n.starts_with("cb")) - || n == "len" - || n == "length" - || n == "size" - || n == "max" - || n == "count" - || n.starts_with("max") - || n.starts_with("size")) -} - fn map_winmd_type(ty: &windows_metadata::Type, index: &reader::Index) -> TypeMeta { map_winmd_type_with_generics(ty, index, &[]) } -fn map_winmd_type_with_generics( +pub(crate) fn map_winmd_type_with_generics( ty: &windows_metadata::Type, index: &reader::Index, generic_args: &[TypeMeta], @@ -1827,42 +1337,6 @@ fn map_winmd_type_with_generics( } } -fn map_winmd_out_param_type( - ty: &windows_metadata::Type, - index: &reader::Index, - generic_args: &[TypeMeta], -) -> TypeMeta { - match ty { - windows_metadata::Type::PtrMut(inner, _) | windows_metadata::Type::PtrConst(inner, _) => { - let pointee = map_winmd_type_with_generics(inner, index, generic_args); - if is_scalar_out_pointee(&pointee) { - pointee - } else { - map_winmd_type_with_generics(ty, index, generic_args) - } - } - _ => map_winmd_type_with_generics(ty, index, generic_args), - } -} - -fn is_scalar_out_pointee(t: &TypeMeta) -> bool { - matches!( - t, - TypeMeta::Bool - | TypeMeta::I8 - | TypeMeta::U8 - | TypeMeta::I16 - | TypeMeta::U16 - | TypeMeta::I32 - | TypeMeta::U32 - | TypeMeta::I64 - | TypeMeta::U64 - | TypeMeta::F32 - | TypeMeta::F64 - | TypeMeta::Enum { .. } - ) -} - fn resolve_named_type( namespace: &str, name: &str, @@ -2029,131 +1503,6 @@ mod tests { assert_eq!(name, "IIterable_IVector_Int32"); } - fn pwstr_type() -> TypeMeta { - TypeMeta::Struct { - namespace: "Windows.Win32.Foundation".into(), - name: "PWSTR".into(), - fields: vec![crate::types::FieldMeta { - name: "Value".into(), - typ: TypeMeta::Object, - }], - } - } - - #[test] - fn marks_direct_pwstr_plus_adjacent_count_as_out_string_buffer() { - let mut params = vec![ - ParamMeta { - name: "pszName".into(), - typ: pwstr_type(), - direction: ParamDirection::Out, - }, - ParamMeta { - name: "cch".into(), - typ: TypeMeta::I32, - direction: ParamDirection::In, - }, - ]; - mark_caller_owned_string_buffers(&mut params); - assert_eq!( - params[0].direction, - ParamDirection::OutStringBuffer { - count_param_index: 1 - } - ); - } - - #[test] - fn does_not_mark_pwstr_plus_byte_count_as_out_string_buffer() { - let mut params = vec![ - ParamMeta { - name: "pszName".into(), - typ: pwstr_type(), - direction: ParamDirection::Out, - }, - ParamMeta { - name: "cbSize".into(), - typ: TypeMeta::U32, - direction: ParamDirection::In, - }, - ]; - mark_caller_owned_string_buffers(&mut params); - assert_eq!(params[0].direction, ParamDirection::Out); - } - - #[test] - fn does_not_mark_callee_allocated_pwstr_pointer_object() { - let mut params = vec![ParamMeta { - name: "ppszName".into(), - typ: TypeMeta::Object, - direction: ParamDirection::Out, - }]; - mark_caller_owned_string_buffers(&mut params); - assert_eq!(params[0].direction, ParamDirection::Out); - } - - #[test] - fn does_not_mark_generic_object_plus_size() { - let mut params = vec![ - ParamMeta { - name: "buffer".into(), - typ: TypeMeta::Object, - direction: ParamDirection::Out, - }, - ParamMeta { - name: "size".into(), - typ: TypeMeta::U32, - direction: ParamDirection::In, - }, - ]; - mark_caller_owned_string_buffers(&mut params); - assert_eq!(params[0].direction, ParamDirection::Out); - } - - #[test] - fn does_not_mark_non_adjacent_count() { - let mut params = vec![ - ParamMeta { - name: "pszName".into(), - typ: pwstr_type(), - direction: ParamDirection::Out, - }, - ParamMeta { - name: "flags".into(), - typ: TypeMeta::U32, - direction: ParamDirection::In, - }, - ParamMeta { - name: "cch".into(), - typ: TypeMeta::I32, - direction: ParamDirection::In, - }, - ]; - mark_caller_owned_string_buffers(&mut params); - assert_eq!(params[0].direction, ParamDirection::Out); - } - - #[test] - fn out_pointer_to_scalar_preserves_pointee_type() { - let index = reader::Index::new(vec![]); - assert_eq!( - map_winmd_out_param_type( - &windows_metadata::Type::PtrMut(Box::new(windows_metadata::Type::I32), 1), - &index, - &[], - ), - TypeMeta::I32 - ); - assert_eq!( - map_winmd_out_param_type( - &windows_metadata::Type::PtrMut(Box::new(windows_metadata::Type::U16), 1), - &index, - &[], - ), - TypeMeta::U16 - ); - } - #[test] fn class_all_interfaces_iterates_all() { let mk_iface = |n: &str| InterfaceMeta { diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.d.ts b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.d.ts deleted file mode 100644 index dd52ede7..00000000 --- a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit - -/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ -export type HWND = bigint | Buffer; - -export declare class DataTransferManager { - /** Wrap an existing native COM pointer (for QueryInterface bridging). */ - static _fromNative(obj: unknown): DataTransferManager; - /** Get a `DataTransferManager` for the given HWND (projected from `Windows.ApplicationModel.DataTransfer.DataTransferManager`). */ - static getForWindow(appWindow: HWND): DataTransferManager; - /** IInspectable::GetRuntimeClassName — the projected class name. */ - get runtimeClassName(): string; -} diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.js b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.js deleted file mode 100644 index c41630e8..00000000 --- a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/DataTransferManager.js +++ /dev/null @@ -1,32 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit -import { DynWinRtType, DynWinRtMethodSig, WinGuid } from '@microsoft/dynwinrt'; -import { IDataTransferManagerInterop } from './IDataTransferManagerInterop.js'; - -const IID_IInspectable = WinGuid.parse('af86e2e0-b12d-4c6a-9c5a-d7aa65101e90'); - -let _IInspectableCache; -const _IInspectable = new Proxy({}, { - get(_target, prop) { - _IInspectableCache ??= DynWinRtType.registerInterfaceUnknown('IInspectable_projected', IID_IInspectable) - .addMethod('GetIids', new DynWinRtMethodSig().addOut(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())) - .addMethod('GetRuntimeClassName', new DynWinRtMethodSig().addOut(DynWinRtType.hstring())) - .addMethod('GetTrustLevel', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type())); - const value = _IInspectableCache[prop]; - return typeof value === 'function' ? value.bind(_IInspectableCache) : value; - }, -}); - -export class DataTransferManager { - _obj; - constructor(obj) { this._obj = obj; } - static _fromNative(obj) { return new DataTransferManager(obj); } - /** Get a `DataTransferManager` for the given HWND via the IDataTransferManagerInterop interop. */ - static getForWindow(appWindow) { - const interop = IDataTransferManagerInterop.create(); - return interop.getForWindow(appWindow); - } - /** IInspectable::GetRuntimeClassName — the projected class name. */ - get runtimeClassName() { - return _IInspectable.method(4).getString(this._obj); - } -} diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts index 7ad9ce06..176b7198 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts @@ -1,5 +1,5 @@ // Generated by dynwinrt-codegen — do not edit -import { DataTransferManager } from './DataTransferManager.js'; +import type { DynWinRtValue } from '@microsoft/dynwinrt'; /** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ export type HWND = bigint | Buffer; @@ -11,6 +11,6 @@ export declare class IDataTransferManagerInterop { static create(): IDataTransferManagerInterop; /** Wrap an existing native COM pointer (for QueryInterface bridging). */ static _fromNative(obj: unknown): IDataTransferManagerInterop; - getForWindow(appWindow: HWND): DataTransferManager; + getForWindow(appWindow: HWND): DynWinRtValue; showShareUIForWindow(appWindow: HWND): void; } diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js index b30de7e3..947f844a 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js @@ -1,6 +1,5 @@ // Generated by dynwinrt-codegen — do not edit -import { DynWinRtType, DynWinRtMethodSig, DynWinRtValue, WinGuid } from '@microsoft/dynwinrt'; -import { DataTransferManager } from './DataTransferManager.js'; +import { DynCom, DynComMethodSig, DynWinRtValue, WinGuid } from '@microsoft/dynwinrt'; export const IID_IDataTransferManagerInterop = WinGuid.parse('3a3dcd6c-3eab-43dc-bcde-45671ce800c8'); const IID_DataTransferManager_default = WinGuid.parse('a5caee9b-8708-49d1-8d36-67d25a8da00c'); @@ -8,9 +7,9 @@ const IID_DataTransferManager_default = WinGuid.parse('a5caee9b-8708-49d1-8d36-6 let _IDataTransferManagerInteropCache; const _IDataTransferManagerInterop = new Proxy({}, { get(_target, prop) { - _IDataTransferManagerInteropCache ??= DynWinRtType.registerInterfaceUnknown('Windows.Win32.UI.Shell.IDataTransferManagerInterop', IID_IDataTransferManagerInterop) - .addMethod('GetForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addOut(DynWinRtType.pointer())) - .addMethod('ShowShareUIForWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())); + _IDataTransferManagerInteropCache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.IDataTransferManagerInterop', IID_IDataTransferManagerInterop) + .addMethod('GetForWindow', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addOut(DynCom.pointerType())) + .addMethod('ShowShareUIForWindow', new DynComMethodSig().addIn(DynCom.pointerType())); const value = _IDataTransferManagerInteropCache[prop]; return typeof value === 'function' ? value.bind(_IDataTransferManagerInteropCache) : value; }, @@ -27,10 +26,11 @@ export class IDataTransferManagerInterop { return new IDataTransferManagerInterop(_obj); } getForWindow(appWindow) { - const _out = _IDataTransferManagerInterop.method(3).invoke(this._obj, [DynWinRtValue.pointer(appWindow), DynWinRtValue.iidPointer(IID_DataTransferManager_default)]); - return DataTransferManager._fromNative(_out); + const _raw = _IDataTransferManagerInterop.method(3).invoke(this._obj, [DynCom.pointer(appWindow), DynCom.iidPointer(IID_DataTransferManager_default)]); + const _out = DynCom.adoptComPointer(_raw, IID_DataTransferManager_default); + return _out; } showShareUIForWindow(appWindow) { - _IDataTransferManagerInterop.method(4).invoke(this._obj, [DynWinRtValue.pointer(appWindow)]); + _IDataTransferManagerInterop.method(4).invoke(this._obj, [DynCom.pointer(appWindow)]); } } diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js index f7ec570a..73d716b1 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js @@ -1,5 +1,5 @@ // Generated by dynwinrt-codegen — do not edit -import { DynWinRtType, DynWinRtMethodSig, DynWinRtValue, WinGuid } from '@microsoft/dynwinrt'; +import { DynCom, DynComMethodSig, WinGuid } from '@microsoft/dynwinrt'; import { TBPFLAG } from './TBPFLAG.js'; export const IID_ITaskbarList3 = WinGuid.parse('ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf'); @@ -7,25 +7,25 @@ export const IID_ITaskbarList3 = WinGuid.parse('ea1afb91-9e28-4b86-90e9-9e9f8a5e let _ITaskbarList3Cache; const _ITaskbarList3 = new Proxy({}, { get(_target, prop) { - _ITaskbarList3Cache ??= DynWinRtType.registerInterfaceUnknown('Windows.Win32.UI.Shell.ITaskbarList3', IID_ITaskbarList3) - .addMethod('HrInit', new DynWinRtMethodSig()) - .addMethod('AddTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('DeleteTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('ActivateTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('SetActiveAlt', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('MarkFullscreenWindow', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) - .addMethod('SetProgressValue', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u64Type()).addIn(DynWinRtType.u64Type())) - .addMethod('SetProgressState', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type())) - .addMethod('RegisterTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) - .addMethod('UnregisterTab', new DynWinRtMethodSig().addIn(DynWinRtType.pointer())) - .addMethod('SetTabOrder', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) - .addMethod('SetTabActive', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type())) - .addMethod('ThumbBarAddButtons', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type()).addIn(DynWinRtType.pointer())) - .addMethod('ThumbBarUpdateButtons', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.u32Type()).addIn(DynWinRtType.pointer())) - .addMethod('ThumbBarSetImageList', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) - .addMethod('SetOverlayIcon', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) - .addMethod('SetThumbnailTooltip', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())) - .addMethod('SetThumbnailClip', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.pointer())); + _ITaskbarList3Cache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.ITaskbarList3', IID_ITaskbarList3) + .addMethod('HrInit', new DynComMethodSig()) + .addMethod('AddTab', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('DeleteTab', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('ActivateTab', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('SetActiveAlt', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('MarkFullscreenWindow', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) + .addMethod('SetProgressValue', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u64Type()).addIn(DynCom.u64Type())) + .addMethod('SetProgressState', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) + .addMethod('RegisterTab', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('UnregisterTab', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('SetTabOrder', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('SetTabActive', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addIn(DynCom.u32Type())) + .addMethod('ThumbBarAddButtons', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u32Type()).addIn(DynCom.pointerType())) + .addMethod('ThumbBarUpdateButtons', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u32Type()).addIn(DynCom.pointerType())) + .addMethod('ThumbBarSetImageList', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('SetOverlayIcon', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('SetThumbnailTooltip', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('SetThumbnailClip', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())); const value = _ITaskbarList3Cache[prop]; return typeof value === 'function' ? value.bind(_ITaskbarList3Cache) : value; }, @@ -37,61 +37,61 @@ export class ITaskbarList3 { static _fromNative(obj) { return new ITaskbarList3(obj); } /** Create a new `ITaskbarList3` via `CoCreateInstance` on `CLSID_TaskbarList`. */ static create() { - const _obj = DynWinRtValue.coCreateInstance('56fdf344-fd6d-11d0-958a-006097c9a090', IID_ITaskbarList3); + const _obj = DynCom.coCreateInstance('56fdf344-fd6d-11d0-958a-006097c9a090', IID_ITaskbarList3); return new ITaskbarList3(_obj); } hrInit() { _ITaskbarList3.method(3).invoke(this._obj, []); } addTab(hwnd) { - _ITaskbarList3.method(4).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + _ITaskbarList3.method(4).invoke(this._obj, [DynCom.pointer(hwnd)]); } deleteTab(hwnd) { - _ITaskbarList3.method(5).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + _ITaskbarList3.method(5).invoke(this._obj, [DynCom.pointer(hwnd)]); } activateTab(hwnd) { - _ITaskbarList3.method(6).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + _ITaskbarList3.method(6).invoke(this._obj, [DynCom.pointer(hwnd)]); } setActiveAlt(hwnd) { - _ITaskbarList3.method(7).invoke(this._obj, [DynWinRtValue.pointer(hwnd)]); + _ITaskbarList3.method(7).invoke(this._obj, [DynCom.pointer(hwnd)]); } markFullscreenWindow(hwnd, fFullscreen) { - _ITaskbarList3.method(8).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.i32(fFullscreen ? 1 : 0)]); + _ITaskbarList3.method(8).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.i32(fFullscreen ? 1 : 0)]); } setProgressValue(hwnd, ullCompleted, ullTotal) { - _ITaskbarList3.method(9).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.u64(BigInt(ullCompleted)), DynWinRtValue.u64(BigInt(ullTotal))]); + _ITaskbarList3.method(9).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.u64(BigInt(ullCompleted)), DynCom.u64(BigInt(ullTotal))]); } setProgressState(hwnd, tbpFlags) { - _ITaskbarList3.method(10).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.i32(tbpFlags)]); + _ITaskbarList3.method(10).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.i32(tbpFlags)]); } registerTab(tab, mDI) { - _ITaskbarList3.method(11).invoke(this._obj, [DynWinRtValue.pointer(tab), DynWinRtValue.pointer(mDI)]); + _ITaskbarList3.method(11).invoke(this._obj, [DynCom.pointer(tab), DynCom.pointer(mDI)]); } unregisterTab(tab) { - _ITaskbarList3.method(12).invoke(this._obj, [DynWinRtValue.pointer(tab)]); + _ITaskbarList3.method(12).invoke(this._obj, [DynCom.pointer(tab)]); } setTabOrder(tab, insertBefore) { - _ITaskbarList3.method(13).invoke(this._obj, [DynWinRtValue.pointer(tab), DynWinRtValue.pointer(insertBefore)]); + _ITaskbarList3.method(13).invoke(this._obj, [DynCom.pointer(tab), DynCom.pointer(insertBefore)]); } setTabActive(tab, mDI, reserved) { - _ITaskbarList3.method(14).invoke(this._obj, [DynWinRtValue.pointer(tab), DynWinRtValue.pointer(mDI), DynWinRtValue.u32(reserved)]); + _ITaskbarList3.method(14).invoke(this._obj, [DynCom.pointer(tab), DynCom.pointer(mDI), DynCom.u32(reserved)]); } thumbBarAddButtons(hwnd, cButtons, pButton) { - _ITaskbarList3.method(15).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.u32(cButtons), DynWinRtValue.pointer(pButton)]); + _ITaskbarList3.method(15).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.u32(cButtons), DynCom.pointer(pButton)]); } thumbBarUpdateButtons(hwnd, cButtons, pButton) { - _ITaskbarList3.method(16).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.u32(cButtons), DynWinRtValue.pointer(pButton)]); + _ITaskbarList3.method(16).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.u32(cButtons), DynCom.pointer(pButton)]); } thumbBarSetImageList(hwnd, himl) { - _ITaskbarList3.method(17).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(himl)]); + _ITaskbarList3.method(17).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(himl)]); } setOverlayIcon(hwnd, hIcon, description) { - _ITaskbarList3.method(18).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(hIcon), DynWinRtValue.pointer(description)]); + _ITaskbarList3.method(18).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(hIcon), DynCom.pointer(description)]); } setThumbnailTooltip(hwnd, tip) { - _ITaskbarList3.method(19).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(tip)]); + _ITaskbarList3.method(19).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(tip)]); } setThumbnailClip(hwnd, prcClip) { - _ITaskbarList3.method(20).invoke(this._obj, [DynWinRtValue.pointer(hwnd), DynWinRtValue.pointer(prcClip)]); + _ITaskbarList3.method(20).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(prcClip)]); } } diff --git a/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs b/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs index fbf41ad8..6cb8841d 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs @@ -21,7 +21,7 @@ use std::fs; use std::path::{Path, PathBuf}; use dynwinrt_codegen::codegen::com; -use dynwinrt_codegen::meta; +use dynwinrt_codegen::com_metadata; const WIN32_WINMD: &str = r"C:\s\win32metadata\Windows.Win32.winmd"; @@ -33,7 +33,7 @@ fn win32_available() -> bool { /// can auto-resolve the projected class IID. Uses the SAME discovery logic the /// codegen itself uses — no pinned version. fn newest_windows_winmd_available() -> bool { - meta::discover_newest_windows_winmd().is_some() + com_metadata::discover_newest_windows_winmd().is_some() } /// 1. IDataTransferManagerInterop parses cleanly, is IUnknown-rooted (+3), @@ -44,7 +44,7 @@ fn parse_data_transfer_manager_interop() { eprintln!("Skipping: Win32 winmd not available"); return; } - let com = meta::parse_com_interface( + let com = com_metadata::parse_com_interface( WIN32_WINMD, "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", @@ -71,7 +71,7 @@ fn parse_smtc_interop() { eprintln!("Skipping: Win32 winmd not available"); return; } - let com = meta::parse_com_interface( + let com = com_metadata::parse_com_interface( WIN32_WINMD, "Windows.Win32.System.WinRT", "ISystemMediaTransportControlsInterop", @@ -99,7 +99,7 @@ fn interop_dts_hides_riid_and_out_ptr_for_datatransfermanager() { eprintln!("Skipping: winmd(s) not available"); return; } - let com = meta::parse_com_interface( + let com = com_metadata::parse_com_interface( WIN32_WINMD, "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", @@ -153,7 +153,7 @@ fn interop_js_synthesizes_target_iid_for_datatransfermanager() { eprintln!("Skipping: winmd(s) not available"); return; } - let com = meta::parse_com_interface( + let com = com_metadata::parse_com_interface( WIN32_WINMD, "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", @@ -184,6 +184,11 @@ fn interop_js_synthesizes_target_iid_for_datatransfermanager() { ".js must invoke slot 3 for GetForWindow:\n{}", js ); + assert!( + js.contains("DynCom.adoptComPointer(_raw, IID_DataTransferManager_default)"), + ".js must adopt the AddRef-owned void** result:\n{}", + js + ); // Activation: uses activationFactory (WinRT) for the projected class // + QI to the interop IID — NOT CoCreateInstance (which is for classic COM CLSIDs). @@ -207,7 +212,7 @@ fn smtc_interop_js_uses_inspectable_base_slot_6() { eprintln!("Skipping: winmd(s) not available"); return; } - let com = meta::parse_com_interface( + let com = com_metadata::parse_com_interface( WIN32_WINMD, "Windows.Win32.System.WinRT", "ISystemMediaTransportControlsInterop", @@ -218,9 +223,10 @@ fn smtc_interop_js_uses_inspectable_base_slot_6() { let js = out.js.as_str(); // IInspectable-rooted → register with the WinRT base (registerInterface), - // not registerInterfaceUnknown. + // not the IUnknown-rooted registration path. assert!( - js.contains("registerInterface(") && !js.contains("registerInterfaceUnknown("), + js.contains("DynCom.registerIInspectableInterface(") + && !js.contains("DynCom.registerIUnknownInterface("), ".js for an IInspectable-rooted interop must use registerInterface \ (base_slot=6), got:\n{}", js @@ -240,17 +246,15 @@ fn smtc_interop_js_uses_inspectable_base_slot_6() { ); } -/// 6. The interop wrapper's return object exposes `runtimeClassName` — a -/// natural, meaningful property that reads via IInspectable::GetRuntimeClassName. -/// This is what the E2E asserts to prove the returned object is a live WinRT -/// object (not just a non-null pointer). +/// 6. The COM projection returns the bridge value without synthesizing a +/// partial WinRT runtime-class projection. #[test] -fn interop_return_type_exposes_runtime_class_name() { +fn interop_return_is_explicit_winrt_bridge_value() { if !win32_available() || !newest_windows_winmd_available() { eprintln!("Skipping: winmd(s) not available"); return; } - let com = meta::parse_com_interface( + let com = com_metadata::parse_com_interface( WIN32_WINMD, "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", @@ -259,40 +263,17 @@ fn interop_return_type_exposes_runtime_class_name() { let out = com::generate_com_interface_files(&com, WIN32_WINMD) .expect("interop codegen must succeed when winmds are present"); - // The projected class `DataTransferManager` is emitted as a separate - // sibling file (own .js + .d.ts), NOT inside the interop wrapper's .d.ts. - let projected_dts = out - .extra_files - .iter() - .find(|(name, _)| name == "DataTransferManager.d.ts") - .map(|(_, content)| content.as_str()) - .expect( - "DataTransferManager.d.ts must be emitted as a projected companion \ - (via Windows.winmd default-interface lookup)", - ); - - assert!( - projected_dts.contains("runtimeClassName"), - "DataTransferManager.d.ts must declare a `runtimeClassName` getter:\n{}", - projected_dts - ); - assert!( - projected_dts.contains("class DataTransferManager"), - "DataTransferManager.d.ts must declare `class DataTransferManager`:\n{}", - projected_dts - ); assert!( - projected_dts.contains("static getForWindow"), - "DataTransferManager.d.ts must expose `static getForWindow(hwnd)`:\n{}", - projected_dts + out.dts + .contains("getForWindow(appWindow: HWND): DynWinRtValue;"), + "interop .d.ts must expose the WinRT bridge value:\n{}", + out.dts ); - - // Also confirm the interop's own .d.ts references DataTransferManager as - // the natural return type (verified via import). assert!( - out.dts.contains("DataTransferManager"), - "interop .d.ts must reference the projected return type:\n{}", - out.dts + !out.extra_files + .iter() + .any(|(name, _)| name.starts_with("DataTransferManager.")), + "COM codegen must not synthesize a WinRT class projection" ); } @@ -304,7 +285,7 @@ fn interop_generation_is_deterministic() { return; } let mk = || { - let com = meta::parse_com_interface( + let com = com_metadata::parse_com_interface( WIN32_WINMD, "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", @@ -328,7 +309,7 @@ fn snapshot_datatransfermanager_interop() { eprintln!("Skipping: winmd(s) not available"); return; } - let com = meta::parse_com_interface( + let com = com_metadata::parse_com_interface( WIN32_WINMD, "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", @@ -411,8 +392,8 @@ fn fix1_interop_iid_resolution_is_portable_and_asserted() { // 1. IDataTransferManager: default interface IID must resolve to the // well-known value regardless of which SDK version is installed. - let (ns_dtm, _iface_dtm, iid_dtm) = meta::find_runtime_class_default_iid( - &meta::discover_newest_windows_winmd().unwrap(), + let (ns_dtm, _iface_dtm, iid_dtm) = com_metadata::find_runtime_class_default_iid( + &com_metadata::discover_newest_windows_winmd().unwrap(), "DataTransferManager", ) .expect("DataTransferManager must resolve via discovered SDK winmd"); @@ -420,8 +401,8 @@ fn fix1_interop_iid_resolution_is_portable_and_asserted() { assert_eq!(iid_dtm, "a5caee9b-8708-49d1-8d36-67d25a8da00c"); // 2. SystemMediaTransportControls: same portability contract. - let (ns_smtc, _iface_smtc, iid_smtc) = meta::find_runtime_class_default_iid( - &meta::discover_newest_windows_winmd().unwrap(), + let (ns_smtc, _iface_smtc, iid_smtc) = com_metadata::find_runtime_class_default_iid( + &com_metadata::discover_newest_windows_winmd().unwrap(), "SystemMediaTransportControls", ) .expect("SystemMediaTransportControls must resolve via discovered SDK winmd"); @@ -431,7 +412,7 @@ fn fix1_interop_iid_resolution_is_portable_and_asserted() { // 3. End-to-end: the classic-COM interop wrapper embeds the correct IID. // Test intentionally passes ONLY the Win32 winmd (no Windows.winmd in // winmd_paths) to exercise the newest-SDK fallback path. - let com = meta::parse_com_interface( + let com = com_metadata::parse_com_interface( WIN32_WINMD, "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", @@ -448,7 +429,7 @@ fn fix1_interop_iid_resolution_is_portable_and_asserted() { // Must NEVER emit the silent NULL riid sentinel that the pre-fix code // could produce when resolution failed. assert!( - !out.js.contains("DynWinRtValue.pointer(0n)"), + !out.js.contains("DynCom.pointer(0n)"), "generator must not emit a NULL riid — indicates silent failure:\n{}", out.js ); @@ -463,11 +444,11 @@ fn fix1_interop_iid_prefers_passed_winmds_over_sdk() { eprintln!("Skipping: winmd(s) not available"); return; } - let sdk = meta::discover_newest_windows_winmd().unwrap(); + let sdk = com_metadata::discover_newest_windows_winmd().unwrap(); // Pass Windows.winmd as part of winmd_paths — the generator should find // the runtime class immediately without hitting the fallback path. let combined = format!("{};{}", WIN32_WINMD, sdk); - let com = meta::parse_com_interface( + let com = com_metadata::parse_com_interface( WIN32_WINMD, "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs index d69bb504..bf1a64fc 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -17,6 +17,7 @@ use std::path::{Path, PathBuf}; use dynwinrt_codegen::codegen::com; use dynwinrt_codegen::codegen::project::{get_import_name, set_import_name}; +use dynwinrt_codegen::com_metadata; use dynwinrt_codegen::meta; use dynwinrt_codegen::types::TypeMeta; @@ -31,7 +32,7 @@ fn win32_available() -> bool { /// installed on this machine (the test that calls this should skip in that /// case, consistent with other tests in this module). fn discovered_windows_winmd() -> Option { - meta::discover_newest_windows_winmd() + com_metadata::discover_newest_windows_winmd() } // ------------------------------------------------------------------------- @@ -46,7 +47,7 @@ fn parse_itaskbarlist3_iid() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") .expect("ITaskbarList3 must exist in Win32 metadata"); assert_eq!(com_iface.interface.name, "ITaskbarList3"); assert_eq!(com_iface.interface.namespace, "Windows.Win32.UI.Shell"); @@ -66,7 +67,7 @@ fn parse_itaskbarlist3_vtable_slots() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") .expect("ITaskbarList3 must exist"); let by_name = |n: &str| -> usize { @@ -117,7 +118,8 @@ fn itaskbarlist3_is_iunknown_rooted() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3").unwrap(); + com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .unwrap(); assert_eq!(com_iface.base_offset, 3); assert!(com_iface.is_iunknown_rooted); // Base chain should include ITaskbarList2, ITaskbarList (and stop at IUnknown) @@ -138,7 +140,8 @@ fn itaskbarlist3_clsid_resolution() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3").unwrap(); + com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .unwrap(); assert_eq!( com_iface.coclass_clsid.as_deref(), Some("56fdf344-fd6d-11d0-958a-006097c9a090") @@ -154,7 +157,8 @@ fn param_type_mapping() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3").unwrap(); + com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .unwrap(); // Generate wrapper as a text bundle we can inspect for the mapping decisions let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) @@ -206,70 +210,48 @@ fn param_type_mapping() { } #[test] -fn shelllink_scalar_out_pointers_preserve_pointees_and_codegen_as_scalars() { +fn shelllink_scalar_out_pointers_preserve_pointee_types() { if !win32_available() { eprintln!("Skipping: Win32 winmd not available"); return; } - let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "IShellLinkW").unwrap(); + let interface = + com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "IShellLinkW") + .unwrap(); - let get_show_cmd = com_iface + let get_show_cmd = interface .interface .methods .iter() - .find(|m| m.name == "GetShowCmd") - .expect("GetShowCmd"); + .find(|method| method.name == "GetShowCmd") + .unwrap(); assert!(matches!( &get_show_cmd.params[0].typ, - TypeMeta::Enum { name, underlying, .. } - if name == "SHOW_WINDOW_CMD" && matches!(**underlying, TypeMeta::I32) + TypeMeta::Enum { underlying, .. } if matches!(**underlying, TypeMeta::I32) )); - - let get_hotkey = com_iface + let get_hotkey = interface .interface .methods .iter() - .find(|m| m.name == "GetHotkey") - .expect("GetHotkey"); - assert_eq!(get_hotkey.params[0].typ, TypeMeta::U16); - - let get_icon_location = com_iface + .find(|method| method.name == "GetHotkey") + .unwrap(); + assert!(matches!(get_hotkey.params[0].typ, TypeMeta::U16)); + let get_icon_location = interface .interface .methods .iter() - .find(|m| m.name == "GetIconLocation") - .expect("GetIconLocation"); - assert_eq!(get_icon_location.params[2].typ, TypeMeta::I32); + .find(|method| method.name == "GetIconLocation") + .unwrap(); + assert!(matches!(get_icon_location.params[2].typ, TypeMeta::I32)); - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) - .expect("codegen must succeed for IShellLinkW"); - assert!( - out.js - .contains(".addMethod('GetHotkey', new DynWinRtMethodSig().addOut(DynWinRtType.u16Type()))"), - "GetHotkey must register WORD* out as u16:\n{}", - out.js - ); - assert!( - out.js - .contains(".addMethod('GetShowCmd', new DynWinRtMethodSig().addOut(DynWinRtType.i32Type()))"), - "GetShowCmd must register SHOW_WINDOW_CMD* out as i32:\n{}", - out.js - ); - assert!( - out.js - .contains(".addMethod('GetIconLocation', new DynWinRtMethodSig().addIn(DynWinRtType.pointer()).addIn(DynWinRtType.i32Type()).addOut(DynWinRtType.i32Type()))"), - "GetIconLocation's trailing int* out must register as i32:\n{}", - out.js - ); - assert!(out.js.contains("getHotkey() {\n const _out")); - assert!(out.js.contains("getShowCmd() {\n const _out")); - assert!(out.js.contains("return _out.toNumber();")); - assert!( - !out.js.contains("getHotkey() {\n const _out = _IShellLinkW.method(12).invoke(this._obj, []);\n // TODO: raw COM interface pointer adoption"), - "GetHotkey must not get the COM-pointer TODO:\n{}", - out.js - ); + let output = com::generate_com_interface_files(&interface, WIN32_WINMD).unwrap(); + assert!(output.js.contains( + ".addMethod('GetHotkey', new DynComMethodSig().addOut(DynCom.u16Type()))" + )); + assert!(output.js.contains( + ".addMethod('GetShowCmd', new DynComMethodSig().addOut(DynCom.i32Type()))" + )); + assert!(output.dts.contains("getIconLocation(cch?: number): [string, number];")); } /// 6. Partial generation: generating a single class-name yields ONLY that @@ -282,7 +264,8 @@ fn partial_generation_only_emits_target_interface() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3").unwrap(); + com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .unwrap(); let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) .expect("codegen must succeed for classic-COM interface"); @@ -321,7 +304,8 @@ fn dts_surface_is_natural_and_clean() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3").unwrap(); + com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .unwrap(); let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) .expect("codegen must succeed for classic-COM interface"); let dts = out.dts.as_str(); @@ -395,7 +379,8 @@ fn js_body_uses_cocreateinstance_and_correct_slots() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3").unwrap(); + com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + .unwrap(); let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) .expect("codegen must succeed for classic-COM interface"); let js = out.js.as_str(); @@ -419,10 +404,10 @@ fn js_body_uses_cocreateinstance_and_correct_slots() { js ); - // registerInterfaceUnknown (not registerInterface) since IUnknown-based + // Classic COM registration is kept out of the WinRT type namespace. assert!( - js.contains("registerInterfaceUnknown"), - ".js must use registerInterfaceUnknown for classic COM:\n{}", + js.contains("DynCom.registerIUnknownInterface"), + ".js must use DynCom registration for classic COM:\n{}", js ); assert!(js.contains("Windows.Win32.UI.Shell.ITaskbarList3")); @@ -484,8 +469,11 @@ fn interface_not_found_is_clean_none() { eprintln!("Skipping: Win32 winmd not available"); return; } - let missing = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "IDoesNotExist_XYZ"); + let missing = com_metadata::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "IDoesNotExist_XYZ", + ); assert!(missing.is_none()); } @@ -499,17 +487,26 @@ fn qi_only_interface_has_no_create() { } // IPersist is IUnknown-rooted (has 1 own method: GetClassID) and has NO // "Persist" coclass anywhere in the metadata — verified via probe. - let com_iface = meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.System.Com", "IPersist") - .expect("IPersist must exist in Win32 metadata"); + let com_iface = + com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.System.Com", "IPersist") + .expect("IPersist must exist in Win32 metadata"); assert!( com_iface.coclass_clsid.is_none(), "IPersist has no associated coclass CLSID" ); + let get_class_id = com_iface + .interface + .methods + .iter() + .find(|method| method.name == "GetClassID") + .unwrap(); + assert!(matches!(get_class_id.params[0].typ, TypeMeta::Guid)); let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) .expect("codegen must succeed for classic-COM interface"); let js = out.js.as_str(); let dts = out.dts.as_str(); + assert!(js.contains(".addOut(DynCom.guidType())")); // No `create()` in either surface assert!( @@ -546,14 +543,22 @@ fn generation_is_deterministic() { return; } let a = { - let com = meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") - .unwrap(); + let com = com_metadata::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); com::generate_com_interface_files(&com, WIN32_WINMD) .expect("codegen must succeed for classic-COM interface") }; let b = { - let com = meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") - .unwrap(); + let com = com_metadata::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); com::generate_com_interface_files(&com, WIN32_WINMD) .expect("codegen must succeed for classic-COM interface") }; @@ -581,7 +586,7 @@ fn snapshot_itaskbarlist3() { return; } let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") .expect("ITaskbarList3 must exist"); let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) .expect("codegen must succeed for classic-COM interface"); @@ -663,9 +668,12 @@ fn import_name_flag_is_honored_by_com_path() { set_import_name("../dist/index.js"); let result = std::panic::catch_unwind(|| { - let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") - .expect("ITaskbarList3 must exist"); + let com_iface = com_metadata::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .expect("ITaskbarList3 must exist"); com::generate_com_interface_files(&com_iface, WIN32_WINMD) .expect("codegen must succeed for classic-COM interface") }); @@ -690,9 +698,12 @@ fn import_name_flag_is_honored_by_com_path() { // Sanity: after restoring the default, subsequent generation reverts. let default_out = { - let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") - .expect("ITaskbarList3 must exist"); + let com_iface = com_metadata::parse_com_interface( + WIN32_WINMD, + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .expect("ITaskbarList3 must exist"); com::generate_com_interface_files(&com_iface, WIN32_WINMD) .expect("codegen must succeed for classic-COM interface") }; @@ -703,10 +714,7 @@ fn import_name_flag_is_honored_by_com_path() { ); } -/// Same test for the *interop wrapper* generation path (the second hardcoded -/// site in `com.rs`) — regenerating `IDataTransferManagerInterop` with a -/// custom import name should thread through to the emitted -/// `DataTransferManager.js` companion. +/// Same test for the interop bridge generation path. #[test] fn import_name_flag_is_honored_by_interop_wrapper() { if !win32_available() { @@ -724,7 +732,7 @@ fn import_name_flag_is_honored_by_interop_wrapper() { set_import_name("../dist/index.js"); let result = std::panic::catch_unwind(|| { - let com_iface = meta::parse_com_interface( + let com_iface = com_metadata::parse_com_interface( WIN32_WINMD, "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", @@ -749,22 +757,16 @@ fn import_name_flag_is_honored_by_interop_wrapper() { out.js ); - // And the projected companion class file must honor it too. - let companion = out - .extra_files - .iter() - .find(|(name, _)| name == "DataTransferManager.js") - .map(|(_, content)| content.as_str()) - .expect("DataTransferManager.js companion must be emitted"); assert!( - companion.contains("from '../dist/index.js'"), - "projected companion .js must honor --import-name:\n{}", - companion + out.dts.contains("from '../dist/index.js'"), + "interop .d.ts must honor --import-name:\n{}", + out.dts ); assert!( - !companion.contains("'@microsoft/dynwinrt'"), - "projected companion .js must NOT hardcode '@microsoft/dynwinrt':\n{}", - companion + !out.extra_files + .iter() + .any(|(name, _)| name.starts_with("DataTransferManager.")), + "COM codegen must not emit a projected WinRT companion" ); } @@ -775,13 +777,14 @@ fn shellitem_getdisplayname_is_not_classified_as_caller_owned_string_buffer() { return; } - let com_iface = meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "IShellItem") - .expect("IShellItem must exist"); + let com_iface = + com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "IShellItem") + .expect("IShellItem must exist"); let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) .expect("codegen must succeed for IShellItem"); assert!( - out.js.contains(".addMethod('GetDisplayName', new DynWinRtMethodSig().addIn(DynWinRtType.i32Type()).addOut(DynWinRtType.pointer()))"), + out.js.contains(".addMethod('GetDisplayName', new DynWinRtMethodSig().addIn(DynWinRtType.i32Type()).addOut(DynCom.pointerType()))"), "PWSTR* callee-allocated output must remain addOut(pointer), not caller-owned buffer:\n{}", out.js ); @@ -805,7 +808,7 @@ fn u16_input_param_uses_existing_u16_value_ctor_not_u16value() { // guard: the arg-wrapper must emit the ctor that actually exists, or the // generated call throws at runtime. let com_iface = - meta::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "IShellLinkW") + com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "IShellLinkW") .expect("IShellLinkW must exist"); let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) .expect("codegen must succeed for IShellLinkW"); From 60d176abd0a1113b187600f0fbdffe8466e5115e Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Fri, 24 Jul 2026 05:41:51 +0800 Subject: [PATCH 15/28] fix(test): assert interop return is DynWinRtValue bridge, not DataTransferManager substring The interop return-type assertion matched the bare substring "DataTransferManager", which is always present as part of the interop class name `IDataTransferManagerInterop`. That made it a false positive: it passed regardless of the real return type and its message ("must project the return type as DataTransferManager") contradicted the actual design, which returns the explicit `DynWinRtValue` bridge (no synthesized WinRT runtime-class projection). Assert the real contract instead: `getForWindow(appWindow: HWND): DynWinRtValue;`. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/win32_com_interop_test.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs b/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs index 6cb8841d..a37fb81f 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs @@ -129,12 +129,15 @@ fn interop_dts_hides_riid_and_out_ptr_for_datatransfermanager() { dts ); - // Return type — must be a NATURAL WinRT type name, not `bigint | Buffer` - // and not the raw `unknown` fallback. - // For IDataTransferManagerInterop → DataTransferManager. + // Return type — must be the explicit WinRT bridge value (`DynWinRtValue`), + // NOT the raw `bigint | Buffer` ABI leak and NOT a synthesized WinRT + // runtime-class projection. The runtime-class name only ever appears as + // part of the interop class name `IDataTransferManagerInterop`, never as + // the `getForWindow` return type (asserting a bare `DataTransferManager` + // substring would be a false positive that matches the class name). assert!( - dts.contains("DataTransferManager"), - ".d.ts must project the return type as DataTransferManager:\n{}", + dts.contains("getForWindow(appWindow: HWND): DynWinRtValue;"), + ".d.ts getForWindow must return the DynWinRtValue bridge:\n{}", dts ); assert!( From 10c95a4638f93c7070682cfb1466d39761d09013 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Fri, 24 Jul 2026 11:22:39 +0800 Subject: [PATCH 16/28] fix(com): make take_raw_pointer error message ownership-neutral The fallback error hardcoded "CoTaskMem-allocated", but take_raw_pointer serves multiple pointer kinds (description = "COM interface", "wide-string", "ANSI-string", "CoTaskMem allocation"). The claim was inaccurate for 3 of 4 callers. The `description` parameter already conveys the kind, so drop the misleading qualifier: "Expected a {description} raw pointer". Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bindings/js/src/com.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bindings/js/src/com.rs b/bindings/js/src/com.rs index 56c6d6a4..62b03d5e 100644 --- a/bindings/js/src/com.rs +++ b/bindings/js/src/com.rs @@ -225,7 +225,7 @@ fn take_raw_pointer( other => { value.0 = other; Err(napi::Error::from_reason(format!( - "Expected a CoTaskMem-allocated {description} pointer" + "Expected a {description} raw pointer" ))) } } From f5ac4c030293757a41f9b832174d76b2625cec5f Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Fri, 24 Jul 2026 11:38:09 +0800 Subject: [PATCH 17/28] test(win32): env-overridable winmd path + fix stale post-refactor COM assertions 1. Address review feedback: WIN32_WINMD was hardcoded to a developer-local path (C:\s\win32metadata\Windows.Win32.winmd), so the whole classic-COM test suite silently self-skipped on CI and other machines. Replace the const with a win32_winmd() helper that honors the DYNWINRT_WIN32_WINMD environment variable and falls back to the local path. Applied to win32_com_test.rs and win32_com_interop_test.rs. 2. Because those tests never ran in CI, two assertions went stale after the "isolate classic COM from WinRT" refactor and were failing locally: - shellitem_getdisplayname_...: expected DynWinRtMethodSig/DynWinRtType.i32Type() - u16_input_param_...: expected DynWinRtValue.u16(wHotkey) Classic-COM codegen now emits DynComMethodSig / DynCom.i32Type() / DynCom.u16(...). Update the expectations (intent unchanged: callee-allocated addOut(pointer), and u16 wrapped via the existing ctor). Full dynwinrt-codegen suite now green. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../tests/win32_com_interop_test.rs | 50 ++++++----- .../dynwinrt-codegen/tests/win32_com_test.rs | 90 ++++++++++--------- 2 files changed, 76 insertions(+), 64 deletions(-) diff --git a/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs b/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs index a37fb81f..b3947e41 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs @@ -15,7 +15,7 @@ //! Windows.winmd is auto-discovered from the newest installed Windows SDK by //! the classic-COM interop codegen (see `com::resolve_projected_default_iid`), //! so these tests do not require a specific SDK version — they only need any -//! recent SDK to be installed AND the Windows.Win32 metadata at `WIN32_WINMD`. +//! recent SDK to be installed AND the Windows.Win32 metadata (path from `win32_winmd`, override via `DYNWINRT_WIN32_WINMD`). use std::fs; use std::path::{Path, PathBuf}; @@ -23,10 +23,16 @@ use std::path::{Path, PathBuf}; use dynwinrt_codegen::codegen::com; use dynwinrt_codegen::com_metadata; -const WIN32_WINMD: &str = r"C:\s\win32metadata\Windows.Win32.winmd"; +/// Path to `Windows.Win32.winmd`. Overridable via the `DYNWINRT_WIN32_WINMD` +/// environment variable so this suite can run on CI and other machines without +/// editing the source; falls back to the common local checkout path. +fn win32_winmd() -> String { + std::env::var("DYNWINRT_WIN32_WINMD") + .unwrap_or_else(|_| r"C:\s\win32metadata\Windows.Win32.winmd".to_string()) +} fn win32_available() -> bool { - Path::new(WIN32_WINMD).exists() + Path::new(&win32_winmd()).exists() } /// Ensure any recent installed Windows SDK is present so the interop generator @@ -45,7 +51,7 @@ fn parse_data_transfer_manager_interop() { return; } let com = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", ) @@ -72,7 +78,7 @@ fn parse_smtc_interop() { return; } let com = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.System.WinRT", "ISystemMediaTransportControlsInterop", ) @@ -100,12 +106,12 @@ fn interop_dts_hides_riid_and_out_ptr_for_datatransfermanager() { return; } let com = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", ) .unwrap(); - let out = com::generate_com_interface_files(&com, WIN32_WINMD) + let out = com::generate_com_interface_files(&com, &win32_winmd()) .expect("interop codegen must succeed when winmds are present"); let dts = out.dts.as_str(); @@ -157,12 +163,12 @@ fn interop_js_synthesizes_target_iid_for_datatransfermanager() { return; } let com = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", ) .unwrap(); - let out = com::generate_com_interface_files(&com, WIN32_WINMD) + let out = com::generate_com_interface_files(&com, &win32_winmd()) .expect("interop codegen must succeed when winmds are present"); let js = out.js.as_str(); @@ -216,12 +222,12 @@ fn smtc_interop_js_uses_inspectable_base_slot_6() { return; } let com = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.System.WinRT", "ISystemMediaTransportControlsInterop", ) .unwrap(); - let out = com::generate_com_interface_files(&com, WIN32_WINMD) + let out = com::generate_com_interface_files(&com, &win32_winmd()) .expect("interop codegen must succeed when winmds are present"); let js = out.js.as_str(); @@ -258,12 +264,12 @@ fn interop_return_is_explicit_winrt_bridge_value() { return; } let com = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", ) .unwrap(); - let out = com::generate_com_interface_files(&com, WIN32_WINMD) + let out = com::generate_com_interface_files(&com, &win32_winmd()) .expect("interop codegen must succeed when winmds are present"); assert!( @@ -289,12 +295,12 @@ fn interop_generation_is_deterministic() { } let mk = || { let com = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", ) .unwrap(); - com::generate_com_interface_files(&com, WIN32_WINMD) + com::generate_com_interface_files(&com, &win32_winmd()) .expect("interop codegen must succeed when winmds are present") }; let a = mk(); @@ -313,12 +319,12 @@ fn snapshot_datatransfermanager_interop() { return; } let com = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", ) .unwrap(); - let out = com::generate_com_interface_files(&com, WIN32_WINMD) + let out = com::generate_com_interface_files(&com, &win32_winmd()) .expect("interop codegen must succeed when winmds are present"); let snapshot_dir: PathBuf = @@ -382,7 +388,7 @@ fn fix1_interop_iid_resolution_is_portable_and_asserted() { if !win32_available() { eprintln!( "Skipping fix1_interop_iid_resolution_is_portable_and_asserted: Win32 winmd not available at {}", - WIN32_WINMD + &win32_winmd() ); return; } @@ -416,12 +422,12 @@ fn fix1_interop_iid_resolution_is_portable_and_asserted() { // Test intentionally passes ONLY the Win32 winmd (no Windows.winmd in // winmd_paths) to exercise the newest-SDK fallback path. let com = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", ) .expect("IDataTransferManagerInterop must exist"); - let out = com::generate_com_interface_files(&com, WIN32_WINMD) + let out = com::generate_com_interface_files(&com, &win32_winmd()) .expect("interop codegen must resolve IID via newest-SDK fallback"); assert!( out.js.contains(&iid_dtm), @@ -450,9 +456,9 @@ fn fix1_interop_iid_prefers_passed_winmds_over_sdk() { let sdk = com_metadata::discover_newest_windows_winmd().unwrap(); // Pass Windows.winmd as part of winmd_paths — the generator should find // the runtime class immediately without hitting the fallback path. - let combined = format!("{};{}", WIN32_WINMD, sdk); + let combined = format!("{};{}", &win32_winmd(), sdk); let com = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", ) diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs index bf1a64fc..130a3971 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -21,10 +21,16 @@ use dynwinrt_codegen::com_metadata; use dynwinrt_codegen::meta; use dynwinrt_codegen::types::TypeMeta; -const WIN32_WINMD: &str = r"C:\s\win32metadata\Windows.Win32.winmd"; +/// Path to `Windows.Win32.winmd`. Overridable via the `DYNWINRT_WIN32_WINMD` +/// environment variable so this suite can run on CI and other machines without +/// editing the source; falls back to the common local checkout path. +fn win32_winmd() -> String { + std::env::var("DYNWINRT_WIN32_WINMD") + .unwrap_or_else(|_| r"C:\s\win32metadata\Windows.Win32.winmd".to_string()) +} fn win32_available() -> bool { - Path::new(WIN32_WINMD).exists() + Path::new(&win32_winmd()).exists() } /// Resolve a `Windows.winmd` from the newest installed Windows SDK, matching @@ -43,11 +49,11 @@ fn discovered_windows_winmd() -> Option { #[test] fn parse_itaskbarlist3_iid() { if !win32_available() { - eprintln!("Skipping: Win32 winmd not available at {}", WIN32_WINMD); + eprintln!("Skipping: Win32 winmd not available at {}", &win32_winmd()); return; } let com_iface = - com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") .expect("ITaskbarList3 must exist in Win32 metadata"); assert_eq!(com_iface.interface.name, "ITaskbarList3"); assert_eq!(com_iface.interface.namespace, "Windows.Win32.UI.Shell"); @@ -67,7 +73,7 @@ fn parse_itaskbarlist3_vtable_slots() { return; } let com_iface = - com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") .expect("ITaskbarList3 must exist"); let by_name = |n: &str| -> usize { @@ -118,7 +124,7 @@ fn itaskbarlist3_is_iunknown_rooted() { return; } let com_iface = - com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") .unwrap(); assert_eq!(com_iface.base_offset, 3); assert!(com_iface.is_iunknown_rooted); @@ -140,7 +146,7 @@ fn itaskbarlist3_clsid_resolution() { return; } let com_iface = - com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") .unwrap(); assert_eq!( com_iface.coclass_clsid.as_deref(), @@ -157,11 +163,11 @@ fn param_type_mapping() { return; } let com_iface = - com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") .unwrap(); // Generate wrapper as a text bundle we can inspect for the mapping decisions - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for classic-COM interface"); let dts = out.dts.as_str(); @@ -216,7 +222,7 @@ fn shelllink_scalar_out_pointers_preserve_pointee_types() { return; } let interface = - com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "IShellLinkW") + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "IShellLinkW") .unwrap(); let get_show_cmd = interface @@ -244,7 +250,7 @@ fn shelllink_scalar_out_pointers_preserve_pointee_types() { .unwrap(); assert!(matches!(get_icon_location.params[2].typ, TypeMeta::I32)); - let output = com::generate_com_interface_files(&interface, WIN32_WINMD).unwrap(); + let output = com::generate_com_interface_files(&interface, &win32_winmd()).unwrap(); assert!(output.js.contains( ".addMethod('GetHotkey', new DynComMethodSig().addOut(DynCom.u16Type()))" )); @@ -264,9 +270,9 @@ fn partial_generation_only_emits_target_interface() { return; } let com_iface = - com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") .unwrap(); - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for classic-COM interface"); // Expected files: ITaskbarList3.js, ITaskbarList3.d.ts, TBPFLAG.js, TBPFLAG.d.ts @@ -304,9 +310,9 @@ fn dts_surface_is_natural_and_clean() { return; } let com_iface = - com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") .unwrap(); - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for classic-COM interface"); let dts = out.dts.as_str(); @@ -379,9 +385,9 @@ fn js_body_uses_cocreateinstance_and_correct_slots() { return; } let com_iface = - com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") .unwrap(); - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for classic-COM interface"); let js = out.js.as_str(); @@ -470,7 +476,7 @@ fn interface_not_found_is_clean_none() { return; } let missing = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.UI.Shell", "IDoesNotExist_XYZ", ); @@ -488,7 +494,7 @@ fn qi_only_interface_has_no_create() { // IPersist is IUnknown-rooted (has 1 own method: GetClassID) and has NO // "Persist" coclass anywhere in the metadata — verified via probe. let com_iface = - com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.System.Com", "IPersist") + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.System.Com", "IPersist") .expect("IPersist must exist in Win32 metadata"); assert!( com_iface.coclass_clsid.is_none(), @@ -502,7 +508,7 @@ fn qi_only_interface_has_no_create() { .unwrap(); assert!(matches!(get_class_id.params[0].typ, TypeMeta::Guid)); - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for classic-COM interface"); let js = out.js.as_str(); let dts = out.dts.as_str(); @@ -544,22 +550,22 @@ fn generation_is_deterministic() { } let a = { let com = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3", ) .unwrap(); - com::generate_com_interface_files(&com, WIN32_WINMD) + com::generate_com_interface_files(&com, &win32_winmd()) .expect("codegen must succeed for classic-COM interface") }; let b = { let com = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3", ) .unwrap(); - com::generate_com_interface_files(&com, WIN32_WINMD) + com::generate_com_interface_files(&com, &win32_winmd()) .expect("codegen must succeed for classic-COM interface") }; assert_eq!(a.js, b.js); @@ -586,9 +592,9 @@ fn snapshot_itaskbarlist3() { return; } let com_iface = - com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "ITaskbarList3") + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") .expect("ITaskbarList3 must exist"); - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for classic-COM interface"); let snapshot_dir: PathBuf = @@ -669,12 +675,12 @@ fn import_name_flag_is_honored_by_com_path() { let result = std::panic::catch_unwind(|| { let com_iface = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3", ) .expect("ITaskbarList3 must exist"); - com::generate_com_interface_files(&com_iface, WIN32_WINMD) + com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for classic-COM interface") }); @@ -699,12 +705,12 @@ fn import_name_flag_is_honored_by_com_path() { // Sanity: after restoring the default, subsequent generation reverts. let default_out = { let com_iface = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3", ) .expect("ITaskbarList3 must exist"); - com::generate_com_interface_files(&com_iface, WIN32_WINMD) + com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for classic-COM interface") }; assert!( @@ -733,12 +739,12 @@ fn import_name_flag_is_honored_by_interop_wrapper() { let result = std::panic::catch_unwind(|| { let com_iface = com_metadata::parse_com_interface( - WIN32_WINMD, + &win32_winmd(), "Windows.Win32.UI.Shell", "IDataTransferManagerInterop", ) .expect("IDataTransferManagerInterop must exist"); - com::generate_com_interface_files(&com_iface, WIN32_WINMD) + com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for classic-COM interop interface") }); @@ -778,13 +784,13 @@ fn shellitem_getdisplayname_is_not_classified_as_caller_owned_string_buffer() { } let com_iface = - com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "IShellItem") + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "IShellItem") .expect("IShellItem must exist"); - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for IShellItem"); assert!( - out.js.contains(".addMethod('GetDisplayName', new DynWinRtMethodSig().addIn(DynWinRtType.i32Type()).addOut(DynCom.pointerType()))"), + out.js.contains(".addMethod('GetDisplayName', new DynComMethodSig().addIn(DynCom.i32Type()).addOut(DynCom.pointerType()))"), "PWSTR* callee-allocated output must remain addOut(pointer), not caller-owned buffer:\n{}", out.js ); @@ -803,24 +809,24 @@ fn u16_input_param_uses_existing_u16_value_ctor_not_u16value() { return; } - // IShellLinkW.SetHotkey takes a [in] u16 (WORD). The napi value ctor is - // DynWinRtValue.u16(...) — there is no `u16Value`/`i16Value`. Regression + // IShellLinkW.SetHotkey takes a [in] u16 (WORD). The classic-COM value + // ctor is DynCom.u16(...) — there is no `u16Value`/`i16Value`. Regression // guard: the arg-wrapper must emit the ctor that actually exists, or the // generated call throws at runtime. let com_iface = - com_metadata::parse_com_interface(WIN32_WINMD, "Windows.Win32.UI.Shell", "IShellLinkW") + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "IShellLinkW") .expect("IShellLinkW must exist"); - let out = com::generate_com_interface_files(&com_iface, WIN32_WINMD) + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for IShellLinkW"); assert!( - out.js.contains("DynWinRtValue.u16(wHotkey)"), - "u16 input param must wrap via the existing DynWinRtValue.u16(...):\n{}", + out.js.contains("DynCom.u16(wHotkey)"), + "u16 input param must wrap via the existing DynCom.u16(...):\n{}", out.js ); assert!( !out.js.contains("u16Value(") && !out.js.contains("i16Value("), - "codegen must not emit non-existent DynWinRtValue.u16Value/i16Value:\n{}", + "codegen must not emit non-existent u16Value/i16Value ctor:\n{}", out.js ); } From 50193b402bbed9779b0caada2cb9c22a6c79bb11 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Fri, 24 Jul 2026 14:30:39 +0800 Subject: [PATCH 18/28] =?UTF-8?q?fix(com):=20memory-safety=20hardening=20?= =?UTF-8?q?=E2=80=94=20reject=20object=20in=20pointer(),=20owner-back=20ii?= =?UTF-8?q?d=5Fpointer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two classic-COM memory fixes surfaced by the pointer-lifecycle audit: #2 (Medium, double-release hazard): DynCom.pointer() accepted an existing DynWinRtValue and, for Object values, returned a borrowed raw COM pointer owner-backed by a clone. That raw pointer is indistinguishable from an owned +1 pointer to adoptComPointer(), enabling a double-release. Align tier1 with tier2: reject all DynWinRtValue inputs; callers pass raw pointer bits, Buffer/Uint8Array, or null. Generated COM code only ever passes HWND/buffer/ PIDL values to pointer(), never objects, so nothing breaks. Adds e2e/pointer-reject-object.mjs regression. #4 (Low, unbounded leak): iid_pointer boxed one GUID per distinct GUID into a static HashMap and never freed it. Replace with an owner-backed NativePointerOwner::Guid(Box) that frees on drop/GC. The REFIID is only read during the synchronous COM call and the JS temporary outlives it. Classic-COM gauntlet (taskbarlist, dtm, smtc, shelllink, hwnd) all pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bindings/js/e2e/pointer-reject-object.mjs | 21 +++++++ bindings/js/src/com.rs | 68 +++++++++++------------ 2 files changed, 54 insertions(+), 35 deletions(-) create mode 100644 bindings/js/e2e/pointer-reject-object.mjs diff --git a/bindings/js/e2e/pointer-reject-object.mjs b/bindings/js/e2e/pointer-reject-object.mjs new file mode 100644 index 00000000..70a7c5d2 --- /dev/null +++ b/bindings/js/e2e/pointer-reject-object.mjs @@ -0,0 +1,21 @@ +// Regression for memory-safety fix #2: DynCom.pointer() must REJECT +// DynWinRtValue inputs. Borrowing an owned COM object's raw pointer here would +// make it indistinguishable from an owned raw pointer to adoptComPointer(), +// which can double-release the original wrapper's COM object. +import { DynCom, WinGuid } from '../dist/index.js'; + +// iidPointer() returns a DynWinRtValue — a representative value input. +const someValue = DynCom.iidPointer(WinGuid.parse('a5caee9b-8708-49d1-8d36-67d25a8da00c')); + +let rejected = false; +try { + DynCom.pointer(someValue); +} catch (e) { + rejected = String(e).includes('not accepted'); +} + +if (!rejected) { + console.log('FAIL: DynCom.pointer() accepted a DynWinRtValue input (double-release hazard)'); + process.exit(1); +} +console.log('PASS: DynCom.pointer() rejects DynWinRtValue inputs'); diff --git a/bindings/js/src/com.rs b/bindings/js/src/com.rs index 62b03d5e..03b38b5e 100644 --- a/bindings/js/src/com.rs +++ b/bindings/js/src/com.rs @@ -4,7 +4,7 @@ use napi::bindgen_prelude::{BigInt, FromNapiValue, Unknown}; use napi::JsValue; use napi_derive::napi; -use windows::core::{IUnknown, Interface as _}; +use windows::core::{GUID, IUnknown, Interface as _}; use super::{DynWinRTValue, WinGUID, TABLE}; @@ -14,15 +14,25 @@ pub(super) enum NativePointerOwner { Uint8Array(napi::bindgen_prelude::Uint8Array), ComObject(IUnknown), CoTaskMem(*mut std::ffi::c_void), + Guid(*mut GUID), } impl Drop for NativePointerOwner { fn drop(&mut self) { - if let Self::CoTaskMem(ptr) = self { - if !ptr.is_null() { - unsafe { windows::Win32::System::Com::CoTaskMemFree(Some(*ptr)) }; - *ptr = std::ptr::null_mut(); + match self { + Self::CoTaskMem(ptr) => { + if !ptr.is_null() { + unsafe { windows::Win32::System::Com::CoTaskMemFree(Some(*ptr)) }; + *ptr = std::ptr::null_mut(); + } } + Self::Guid(ptr) => { + if !ptr.is_null() { + drop(unsafe { Box::from_raw(*ptr) }); + *ptr = std::ptr::null_mut(); + } + } + _ => {} } } } @@ -126,25 +136,17 @@ fn pointer(value: Unknown) -> napi::Result { NativePointerOwner::Uint8Array(array), )); } - if let Ok(existing) = unsafe { <&DynWinRTValue>::from_napi_value(env, raw) } { - return match &existing.0 { - dynwinrt::WinRTValue::Object(object) => Ok(DynWinRTValue::with_pointer_owner( - dynwinrt::WinRTValue::RawPtr(object.as_raw()), - NativePointerOwner::ComObject(object.clone()), - )), - dynwinrt::WinRTValue::RawPtr(ptr) if existing.1.is_none() => { - Ok(DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr(*ptr))) - } - dynwinrt::WinRTValue::Null => Ok(DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr( - std::ptr::null_mut(), - ))), - _ => Err(napi::Error::from_reason( - "pointer(): expected an object or unowned pointer value", - )), - }; + // Reject existing DynWinRtValue inputs. Borrowing an Object's raw COM pointer + // here would make it indistinguishable from an owned raw pointer to + // adoptComPointer(), which can double-release the original wrapper's COM + // object. Callers that already have raw pointer bits should pass those bits. + if unsafe { <&DynWinRTValue>::from_napi_value(env, raw) }.is_ok() { + return Err(napi::Error::from_reason( + "pointer(): DynWinRtValue inputs are not accepted; pass raw pointer bits, Buffer/Uint8Array, or null instead", + )); } Err(napi::Error::from_reason( - "pointer(): expected bigint, number, Buffer, Uint8Array, object, null, or undefined", + "pointer(): expected bigint, number, Buffer, Uint8Array, null, or undefined", )) } @@ -232,19 +234,15 @@ fn take_raw_pointer( } fn iid_pointer(value: &WinGUID) -> DynWinRTValue { - use std::collections::HashMap; - use std::sync::{Mutex, OnceLock}; - - static CACHE: OnceLock>> = OnceLock::new(); - let guid = value.0; - let key = u128::from_le_bytes(unsafe { std::mem::transmute(guid) }); - let address = *CACHE - .get_or_init(|| Mutex::new(HashMap::new())) - .lock() - .unwrap() - .entry(key) - .or_insert_with(|| Box::into_raw(Box::new(guid)) as usize); - DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr(address as *mut _)) + // Owner-backed: the boxed GUID is freed when the returned DynWinRtValue is + // dropped / GC'd, instead of being leaked into a process-lifetime cache. The + // REFIID is only read during the synchronous COM call the value is passed to, + // and the JS temporary holding it outlives that call, so this is safe. + let ptr = Box::into_raw(Box::new(value.0)); + DynWinRTValue::with_pointer_owner( + dynwinrt::WinRTValue::RawPtr(ptr as *mut std::ffi::c_void), + NativePointerOwner::Guid(ptr), + ) } #[napi] From 799c428a15f7e385e23795cb634bc18d25a702ed Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Fri, 24 Jul 2026 16:54:43 +0800 Subject: [PATCH 19/28] Fix COM handle typedef marshalling Classic COM handle-value newtypes like HWND were projected as bigint | Buffer, but DynCom.pointer(Buffer) passes the Buffer's address instead of the handle bits it contains. Emit handle values as bigint | number while keeping NUL-terminated string pointer aliases such as PWSTR as bigint | Buffer. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../js/e2e/IDataTransferManagerInterop.d.ts | 4 +- bindings/js/e2e/IShellLinkW.d.ts | 6 +- .../ISystemMediaTransportControlsInterop.d.ts | 4 +- bindings/js/e2e/electron-hwnd-buffer.mjs | 52 ++++++++++++ .../src/codegen/com/render.rs | 84 ++++++++++++++++--- .../src/codegen/com/type_mapping.rs | 64 ++++++++++++-- .../IDataTransferManagerInterop.d.ts | 4 +- .../itaskbarlist3/ITaskbarList3.d.ts | 14 ++-- .../dynwinrt-codegen/tests/win32_com_test.rs | 6 +- 9 files changed, 198 insertions(+), 40 deletions(-) create mode 100644 bindings/js/e2e/electron-hwnd-buffer.mjs diff --git a/bindings/js/e2e/IDataTransferManagerInterop.d.ts b/bindings/js/e2e/IDataTransferManagerInterop.d.ts index 76f1798e..25ba8c1a 100644 --- a/bindings/js/e2e/IDataTransferManagerInterop.d.ts +++ b/bindings/js/e2e/IDataTransferManagerInterop.d.ts @@ -1,8 +1,8 @@ // Generated by dynwinrt-codegen — do not edit import type { DynWinRtValue } from '../dist/index.js'; -/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ -export type HWND = bigint | Buffer; +/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ +export type HWND = bigint | number; export declare const IID_IDataTransferManagerInterop: unknown; diff --git a/bindings/js/e2e/IShellLinkW.d.ts b/bindings/js/e2e/IShellLinkW.d.ts index 5435019c..a803ea28 100644 --- a/bindings/js/e2e/IShellLinkW.d.ts +++ b/bindings/js/e2e/IShellLinkW.d.ts @@ -2,9 +2,9 @@ import { SHOW_WINDOW_CMD } from './SHOW_WINDOW_CMD.js'; import type { DynWinRtValue } from '../dist/index.js'; -/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ -export type HWND = bigint | Buffer; -/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ +/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ +export type HWND = bigint | number; +/** Win32 NUL-terminated string pointer. Pass a `Buffer` holding the string bytes (including the NUL terminator), or pass a raw pointer as `bigint`. */ export type PWSTR = bigint | Buffer; export declare const IID_IShellLinkW: unknown; diff --git a/bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts b/bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts index ff870b13..d3f83bf9 100644 --- a/bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts +++ b/bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts @@ -1,8 +1,8 @@ // Generated by dynwinrt-codegen — do not edit import type { DynWinRtValue } from '../dist/index.js'; -/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ -export type HWND = bigint | Buffer; +/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ +export type HWND = bigint | number; export declare const IID_ISystemMediaTransportControlsInterop: unknown; diff --git a/bindings/js/e2e/electron-hwnd-buffer.mjs b/bindings/js/e2e/electron-hwnd-buffer.mjs new file mode 100644 index 00000000..a7af3767 --- /dev/null +++ b/bindings/js/e2e/electron-hwnd-buffer.mjs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// Regression for classic-COM handle-value typedefs: Electron exposes HWNDs as +// Buffers (BrowserWindow.getNativeWindowHandle()). Callers must read the handle +// bits out of that Buffer and pass the numeric handle value, not the Buffer +// itself, because DynCom.pointer(Buffer) passes the Buffer's own address. + +import { ITaskbarList3 } from './ITaskbarList3.js'; +import { TBPFLAG } from './TBPFLAG.js'; +import { acquireHwndBigInt } from './hwnd.mjs'; + +function fail(msg) { + console.error(`[e2e] FAIL: ${msg}`); + process.exit(1); +} + +console.log('[e2e] step 1: acquiring a process-owned HWND'); +const hwnd = acquireHwndBigInt(); +console.log(`[e2e] HWND → 0x${hwnd.toString(16)}`); + +console.log('[e2e] step 2: simulating Electron getNativeWindowHandle() Buffer'); +const electronHandleBuffer = Buffer.alloc(8); +electronHandleBuffer.writeBigUInt64LE(hwnd, 0); +const hwndFromBuffer = electronHandleBuffer.readBigUInt64LE(0); +if (hwndFromBuffer !== hwnd) { + fail(`round-trip through Buffer changed HWND: ${hwndFromBuffer} !== ${hwnd}`); +} + +console.log('[e2e] step 3: creating ITaskbarList3'); +let taskbar; +try { + taskbar = ITaskbarList3.create(); + taskbar.hrInit(); +} catch (e) { + fail(`ITaskbarList3 activation/HrInit threw: ${e && e.message ? e.message : e}`); +} + +console.log('[e2e] step 4: passing Buffer-read bigint HWND to real classic-COM calls'); +try { + taskbar.setProgressState(hwndFromBuffer, TBPFLAG.TBPF_NORMAL); + taskbar.markFullscreenWindow(hwndFromBuffer, false); + taskbar.setProgressState(hwndFromBuffer, TBPFLAG.TBPF_NOPROGRESS); +} catch (e) { + fail(`Electron Buffer -> bigint HWND pattern threw: ${e && e.message ? e.message : e}`); +} + +// Do not assert that passing `electronHandleBuffer` directly fails: some shell +// APIs tolerate invalid HWNDs and return S_OK/no-op. The regression this locks +// down is that the documented readBigUInt64LE(0) pattern is valid end-to-end. +console.log('PASS'); +process.exit(0); diff --git a/tools/dynwinrt-codegen/src/codegen/com/render.rs b/tools/dynwinrt-codegen/src/codegen/com/render.rs index 9416f84c..698a4f95 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/render.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/render.rs @@ -14,9 +14,10 @@ //! - `.js`: registration via `DynCom.registerIUnknownInterface` //! + a natural class with camelCase methods and static `create()` / //! `_fromNative()`. -//! - `.d.ts`: PascalCase class, camelCase methods, opaque -//! handle typedefs (HWND etc.) as `bigint | Buffer`, HRESULT returns -//! projected to `void` (throwing on failure via the runtime). +//! - `.d.ts`: PascalCase class, camelCase methods, handle-value +//! typedefs (HWND etc.) as `bigint | number`, string-pointer typedefs (PWSTR +//! etc.) as `bigint | Buffer`, HRESULT returns projected to `void` (throwing +//! on failure via the runtime). //! - Per-enum sibling files for each enum referenced by any method parameter. use crate::com_metadata::{ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; @@ -27,15 +28,15 @@ use super::naming::strip_hungarian; use super::naming::{camel_case, js_param_name}; use super::projection::method_is_interop_shape; use super::projection::{InteropInfo, InteropMethod, detect_interop}; -#[cfg(test)] -use super::type_mapping::handle_type_name; use super::type_mapping::{ - MethodResult, StringEncoding, collect_handle_aliases, dts_params_for_method, dts_return_type, - enum_import_names, has_string_buffer_method, is_cotaskmem_owned, is_hresult, + HandleAliasKind, MethodResult, StringEncoding, collect_handle_aliases, dts_params_for_method, + dts_return_type, enum_import_names, has_string_buffer_method, is_cotaskmem_owned, is_hresult, is_optional_find_data_out_after_string_count, method_results, string_buffer_pattern, ts_type_expr_dts, ts_type_expr_js, unwrap_return_js, uses_winrt_bridge_value, validate_com_abi, wrap_arg_js, }; +#[cfg(test)] +use super::type_mapping::{handle_alias_kind, handle_type_name}; /// A rendered classic-COM output: primary `.js` + `.d.ts` for the interface, /// plus zero or more sibling files (one `.js` + `.d.ts` per referenced enum). @@ -622,11 +623,15 @@ fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String // Emit typedef aliases for handles seen in method parameters. let handle_aliases = collect_handle_aliases(meta); - for h in &handle_aliases { - out.push_str(&format!( - "/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */\nexport type {h} = bigint | Buffer;\n", - h = h - )); + for (h, kind) in &handle_aliases { + match kind { + HandleAliasKind::HandleValue => out.push_str(&format!( + "/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */\nexport type {h} = bigint | number;\n" + )), + HandleAliasKind::StringPointer => out.push_str(&format!( + "/** Win32 NUL-terminated string pointer. Pass a `Buffer` holding the string bytes (including the NUL terminator), or pass a raw pointer as `bigint`. */\nexport type {h} = bigint | Buffer;\n" + )), + } } if !handle_aliases.is_empty() { out.push('\n'); @@ -836,6 +841,23 @@ mod tests { assert_eq!(handle_type_name(&hwnd).as_deref(), Some("HWND")); } + #[test] + fn handle_alias_kind_distinguishes_handle_values_from_string_pointers() { + let hwnd = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + assert_eq!(handle_alias_kind(&hwnd), Some(HandleAliasKind::HandleValue)); + assert_eq!( + handle_alias_kind(&pwstr_struct()), + Some(HandleAliasKind::StringPointer) + ); + } + #[test] fn hresult_is_not_a_handle() { let hr = TypeMeta::Struct { @@ -1603,10 +1625,46 @@ mod tests { }; let com = plain_iface_with_method(method); let dts = render_dts(&com, None); - assert!(dts.contains("export type HWND = bigint | Buffer;")); + assert!(dts.contains("export type HWND = bigint | number;")); assert!(dts.contains("getWindow(): HWND;")); } + #[test] + fn handle_value_typedef_rejects_buffer_but_string_pointer_keeps_buffer() { + let hwnd = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + let method = MethodMeta { + name: "SetOverlayIcon".into(), + params: vec![ + ParamMeta { + name: "hwnd".into(), + typ: hwnd, + direction: ParamDirection::In, + }, + ParamMeta { + name: "description".into(), + typ: pwstr_struct(), + direction: ParamDirection::In, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let dts = render_dts(&plain_iface_with_method(method), None); + + assert!(dts.contains("export type HWND = bigint | number;")); + assert!(dts.contains("Do NOT pass a `Buffer`")); + assert!(dts.contains("export type PWSTR = bigint | Buffer;")); + assert!(dts.contains("Pass a `Buffer` holding the string bytes")); + assert!(dts.contains("setOverlayIcon(hwnd: HWND, description: PWSTR): void;")); + } + #[test] fn return_only_enum_emits_import_and_sibling_files() { let kind = TypeMeta::Enum { diff --git a/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs b/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs index 6ebe8c55..543f049e 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::collections::BTreeSet; +use std::collections::BTreeMap; use crate::com_metadata::{ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; use crate::types::TypeMeta; @@ -14,6 +14,12 @@ pub(super) enum StringEncoding { Ansi, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum HandleAliasKind { + HandleValue, + StringPointer, +} + pub(super) fn validate_com_abi(meta: &ComInterfaceMeta) -> Result<(), String> { for method in &meta.interface.methods { for param in &method.params { @@ -216,16 +222,16 @@ pub(super) fn dts_params_for_method(m: &MethodMeta) -> Vec { .collect() } -pub(super) fn collect_handle_aliases(meta: &ComInterfaceMeta) -> Vec { - let mut aliases = BTreeSet::new(); +pub(super) fn collect_handle_aliases(meta: &ComInterfaceMeta) -> Vec<(String, HandleAliasKind)> { + let mut aliases = BTreeMap::new(); for method in &meta.interface.methods { for param in &method.params { - if let Some(alias) = handle_type_name(¶m.typ) { - aliases.insert(alias); + if let Some((alias, kind)) = handle_alias(¶m.typ) { + aliases.insert(alias, kind); } } - if let Some(alias) = method.return_type.as_ref().and_then(handle_type_name) { - aliases.insert(alias); + if let Some((alias, kind)) = method.return_type.as_ref().and_then(handle_alias) { + aliases.insert(alias, kind); } } aliases.into_iter().collect() @@ -409,6 +415,15 @@ pub(super) fn wrap_arg_js(t: &TypeMeta, var: &str) -> String { } pub(super) fn handle_type_name(t: &TypeMeta) -> Option { + handle_alias(t).map(|(name, _)| name) +} + +#[cfg(test)] +pub(super) fn handle_alias_kind(t: &TypeMeta) -> Option { + handle_alias(t).map(|(_, kind)| kind) +} + +fn handle_alias(t: &TypeMeta) -> Option<(String, HandleAliasKind)> { if is_win32_bool(t) { return None; } @@ -426,12 +441,45 @@ pub(super) fn handle_type_name(t: &TypeMeta) -> Option { TypeMeta::Object | TypeMeta::U64 | TypeMeta::I64 | TypeMeta::U32 | TypeMeta::I32 ) => { - Some(name.clone()) + Some((name.clone(), classify_handle_alias(namespace, name))) } _ => None, } } +fn classify_handle_alias(_namespace: &str, name: &str) -> HandleAliasKind { + if is_string_pointer_alias_name(name) { + HandleAliasKind::StringPointer + } else { + HandleAliasKind::HandleValue + } +} + +fn is_string_pointer_alias_name(name: &str) -> bool { + // Classic COM handle typedefs lose pointer-pointee detail by the time they + // reach TypeMeta (`Value: *mut u16` and `Value: *mut c_void` both become + // `Value: Object`). Keep the known Win32 NUL-terminated character-pointer + // aliases as Buffer-capable pointer parameters; all other handle-shaped + // structs are handle values and must not accept Buffer-of-bits inputs. + matches!( + name, + "PWSTR" + | "PCWSTR" + | "PSTR" + | "PCSTR" + | "LPWSTR" + | "LPCWSTR" + | "LPSTR" + | "LPCSTR" + | "PWCHAR" + | "PCWCHAR" + | "LPWCH" + | "LPCWCH" + | "LPCH" + | "LPCCH" + ) +} + fn is_win32_handle_namespace(namespace: &str) -> bool { namespace.starts_with("Windows.Win32.") } diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts index 176b7198..c86f4e33 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts @@ -1,8 +1,8 @@ // Generated by dynwinrt-codegen — do not edit import type { DynWinRtValue } from '@microsoft/dynwinrt'; -/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ -export type HWND = bigint | Buffer; +/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ +export type HWND = bigint | number; export declare const IID_IDataTransferManagerInterop: unknown; diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts index d4cf0607..5209ac63 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts @@ -1,13 +1,13 @@ // Generated by dynwinrt-codegen — do not edit import { TBPFLAG } from './TBPFLAG.js'; -/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ -export type HICON = bigint | Buffer; -/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ -export type HIMAGELIST = bigint | Buffer; -/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ -export type HWND = bigint | Buffer; -/** Opaque Win32 handle or pointer newtype (e.g. HWND, PWSTR). Accepts either a raw pointer as `bigint` or a `Buffer`. */ +/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ +export type HICON = bigint | number; +/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ +export type HIMAGELIST = bigint | number; +/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ +export type HWND = bigint | number; +/** Win32 NUL-terminated string pointer. Pass a `Buffer` holding the string bytes (including the NUL terminator), or pass a raw pointer as `bigint`. */ export type PWSTR = bigint | Buffer; export declare const IID_ITaskbarList3: unknown; diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs index 130a3971..e2da22ee 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -173,10 +173,10 @@ fn param_type_mapping() { let dts = out.dts.as_str(); let js = out.js.as_str(); - // HWND is a handle type → bigint | Buffer surface + // HWND is a handle value type → bigint | number surface (never Buffer). assert!( - dts.contains("bigint | Buffer") || dts.contains("bigint|Buffer"), - "HWND should be projected as `bigint | Buffer` in .d.ts, got:\n{}", + dts.contains("export type HWND = bigint | number;"), + "HWND should be projected as `bigint | number` in .d.ts, got:\n{}", dts ); From db7b529f78c493f875b4a4602a76a49c099b9219 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Fri, 24 Jul 2026 17:04:36 +0800 Subject: [PATCH 20/28] test(com): add regression test for iid_pointer owner-backed fix (#4) Backfills the missing test for the #4 memory fix (commit 50193b4): asserts iid_pointer returns an owner-backed DynWinRtValue (NativePointerOwner::Guid, so the boxed GUID frees on drop) holding the correct GUID bytes, and that two concurrently-live calls for the same GUID allocate distinct boxes (no shared static cache). Verified this FAILS against the pre-fix static-cache-leak impl (the owner-backed assertion fails) and passes after. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bindings/js/src/com.rs | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/bindings/js/src/com.rs b/bindings/js/src/com.rs index 03b38b5e..de4a8c0d 100644 --- a/bindings/js/src/com.rs +++ b/bindings/js/src/com.rs @@ -693,4 +693,45 @@ mod tests { assert!(matches!(value.0, dynwinrt::WinRTValue::Null)); assert!(take_raw_pointer(&mut value, "test").unwrap().is_null()); } + + #[test] + fn iid_pointer_is_owner_backed_and_holds_the_guid() { + // Regression (#4): iid_pointer must return an OWNER-BACKED value so the + // boxed GUID is freed on drop/GC — not leak one Box per distinct GUID + // into a process-lifetime static cache. The pre-fix version returned an + // unowned RawPtr (`.1 == None`) into a static cache (stable address per + // GUID), so both assertions below fail against it. + let guid = GUID::from_u128(0xa5caee9b_8708_49d1_8d36_67d25a8da00c); + + let value = iid_pointer(&WinGUID(guid)); + assert!( + value.1.is_some(), + "iid_pointer must be owner-backed (NativePointerOwner::Guid) so it frees on drop" + ); + match value.0 { + dynwinrt::WinRTValue::RawPtr(ptr) => { + assert!(!ptr.is_null()); + let read = unsafe { *(ptr as *const GUID) }; + assert_eq!(read, guid, "REFIID pointer must hold the correct GUID bytes"); + } + _ => panic!("iid_pointer must return a RawPtr"), + } + + // Two concurrently-live calls for the SAME GUID must allocate distinct + // boxes (distinct addresses) — proving there is no shared static cache. + let a = iid_pointer(&WinGUID(guid)); + let b = iid_pointer(&WinGUID(guid)); + let pa = match a.0 { + dynwinrt::WinRTValue::RawPtr(p) => p as usize, + _ => 0, + }; + let pb = match b.0 { + dynwinrt::WinRTValue::RawPtr(p) => p as usize, + _ => 0, + }; + assert_ne!( + pa, pb, + "each iid_pointer call must own its own boxed GUID, not share a static one" + ); + } } From 9ecbdaf284804eb8994d60501a198d7861067051 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH) (from Dev Box)" Date: Tue, 28 Jul 2026 18:24:48 +0800 Subject: [PATCH 21/28] feat(codegen/com): accept Electron Buffer for handle-value args (no manual unwrap) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Classic-COM handle-value params (HWND/HANDLE/HKEY…) now accept an Electron/Node Buffer/Uint8Array (e.g. BrowserWindow.getNativeWindowHandle()) directly, read as the handle VALUE, in addition to bigint|number. Removes the .readBigUInt64LE(0) tax for Electron callers. - wrap_arg_js: handle-VALUE args wrap via a generated `_handleArg(x)` helper (Buffer/Uint8Array → little-endian pointer value; bigint|number pass-through). String-pointer handles (PWSTR) keep address semantics — NOT wrapped. - render_js: emit the inline `_handleArg` helper only when an interface has a handle-value input (new `uses_handle_value_input` predicate). - .d.ts handle typedef: `bigint | number | Buffer | Uint8Array` (+ reworded doc). - Pure codegen: emits `pointer(_handleArg(x))` = pointer(bigint) — no napi change. - Tests: flipped the rejects-buffer test to accepts-buffer (proven fail-before: reverting the wrap fails "handle arg must be unwrapped"); updated param_type_mapping assertion; regenerated ITaskbarList3 + IDataTransferManagerInterop snapshots. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/codegen/com/render.rs | 58 ++++++++++++++++--- .../src/codegen/com/type_mapping.rs | 23 +++++++- .../IDataTransferManagerInterop.d.ts | 4 +- .../IDataTransferManagerInterop.js | 15 ++++- .../itaskbarlist3/ITaskbarList3.d.ts | 12 ++-- .../snapshots/itaskbarlist3/ITaskbarList3.js | 45 ++++++++------ .../dynwinrt-codegen/tests/win32_com_test.rs | 12 +++- 7 files changed, 128 insertions(+), 41 deletions(-) diff --git a/tools/dynwinrt-codegen/src/codegen/com/render.rs b/tools/dynwinrt-codegen/src/codegen/com/render.rs index 698a4f95..2b86aa33 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/render.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/render.rs @@ -32,8 +32,8 @@ use super::type_mapping::{ HandleAliasKind, MethodResult, StringEncoding, collect_handle_aliases, dts_params_for_method, dts_return_type, enum_import_names, has_string_buffer_method, is_cotaskmem_owned, is_hresult, is_optional_find_data_out_after_string_count, method_results, string_buffer_pattern, - ts_type_expr_dts, ts_type_expr_js, unwrap_return_js, uses_winrt_bridge_value, validate_com_abi, - wrap_arg_js, + ts_type_expr_dts, ts_type_expr_js, unwrap_return_js, uses_handle_value_input, + uses_winrt_bridge_value, validate_com_abi, wrap_arg_js, }; #[cfg(test)] use super::type_mapping::{handle_alias_kind, handle_type_name}; @@ -101,6 +101,23 @@ pub fn generate_com_interface_files( // .js rendering // --------------------------------------------------------------------------- +/// Inline JS helper: normalize a handle argument to a bigint/number pointer +/// value. Accepts a bigint/number (pass-through) or an Electron/Node +/// `Buffer`/`Uint8Array` of pointer bits (e.g. `getNativeWindowHandle()`), +/// reading its little-endian bytes as the handle VALUE (not the buffer address). +const HANDLE_ARG_HELPER: &str = "\ +function _handleArg(h) { + if (typeof h === 'bigint' || typeof h === 'number') return h; + if (h instanceof Uint8Array) { + const _dv = new DataView(h.buffer, h.byteOffset, h.byteLength); + if (h.byteLength >= 8) return _dv.getBigUint64(0, true); + if (h.byteLength >= 4) return BigInt(_dv.getUint32(0, true)); + throw new TypeError('handle Buffer must be at least 4 bytes'); + } + throw new TypeError(`handle must be a bigint, number, or Buffer, got ${typeof h}`); +} +"; + fn render_js(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { let iface = &meta.interface; let iid = &iface.iid; @@ -138,6 +155,11 @@ fn render_js(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { out.push_str("}\n\n"); } + if uses_handle_value_input(meta) { + out.push_str(HANDLE_ARG_HELPER); + out.push('\n'); + } + out.push_str(&format!( "export const IID_{name} = WinGuid.parse('{iid}');\n", name = name, @@ -626,7 +648,7 @@ fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String for (h, kind) in &handle_aliases { match kind { HandleAliasKind::HandleValue => out.push_str(&format!( - "/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */\nexport type {h} = bigint | number;\n" + "/** Opaque Win32 handle. Pass a raw pointer value as a `bigint` (full 64-bit) or `number` (safe integer), or an Electron/Node `Buffer`/`Uint8Array` of the handle's pointer bits — e.g. `BrowserWindow.getNativeWindowHandle()` — which is read as the handle VALUE. */\nexport type {h} = bigint | number | Buffer | Uint8Array;\n" )), HandleAliasKind::StringPointer => out.push_str(&format!( "/** Win32 NUL-terminated string pointer. Pass a `Buffer` holding the string bytes (including the NUL terminator), or pass a raw pointer as `bigint`. */\nexport type {h} = bigint | Buffer;\n" @@ -1625,12 +1647,12 @@ mod tests { }; let com = plain_iface_with_method(method); let dts = render_dts(&com, None); - assert!(dts.contains("export type HWND = bigint | number;")); + assert!(dts.contains("export type HWND = bigint | number | Buffer | Uint8Array;")); assert!(dts.contains("getWindow(): HWND;")); } #[test] - fn handle_value_typedef_rejects_buffer_but_string_pointer_keeps_buffer() { + fn handle_value_arg_accepts_buffer_and_string_pointer_keeps_buffer() { let hwnd = TypeMeta::Struct { namespace: "Windows.Win32.Foundation".into(), name: "HWND".into(), @@ -1656,13 +1678,31 @@ mod tests { return_type: Some(make_hresult()), ..Default::default() }; - let dts = render_dts(&plain_iface_with_method(method), None); - - assert!(dts.contains("export type HWND = bigint | number;")); - assert!(dts.contains("Do NOT pass a `Buffer`")); + let iface = plain_iface_with_method(method); + let dts = render_dts(&iface, None); + let js = render_js(&iface, None); + + // Handle-value typedef now ACCEPTS an Electron/Node Buffer (read as the + // handle VALUE, not the buffer address); string pointers keep Buffer + // with address semantics. + assert!(dts.contains("export type HWND = bigint | number | Buffer | Uint8Array;")); + assert!(!dts.contains("Do NOT pass a `Buffer`")); + assert!(dts.contains("read as the handle VALUE")); assert!(dts.contains("export type PWSTR = bigint | Buffer;")); assert!(dts.contains("Pass a `Buffer` holding the string bytes")); assert!(dts.contains("setOverlayIcon(hwnd: HWND, description: PWSTR): void;")); + + // The handle arg is unwrapped via `_handleArg(...)`, and the helper is + // emitted; the string-pointer arg is NOT unwrapped that way. + assert!( + js.contains("DynCom.pointer(_handleArg(hwnd))"), + "handle arg must be unwrapped via _handleArg:\n{js}" + ); + assert!( + js.contains("function _handleArg("), + "the _handleArg helper must be emitted:\n{js}" + ); + assert!(!js.contains("_handleArg(description)")); } #[test] diff --git a/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs b/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs index 543f049e..936183e7 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs @@ -237,6 +237,20 @@ pub(super) fn collect_handle_aliases(meta: &ComInterfaceMeta) -> Vec<(String, Ha aliases.into_iter().collect() } +/// True if any `[in]` parameter projects as a handle VALUE (HWND/HANDLE/HKEY…), +/// i.e. an arg wrapped via `_handleArg(...)`. String-pointer handles excluded. +pub(super) fn uses_handle_value_input(meta: &ComInterfaceMeta) -> bool { + meta.interface.methods.iter().any(|method| { + method.params.iter().any(|param| { + matches!(param.direction, ParamDirection::In) + && matches!( + handle_alias(¶m.typ), + Some((_, HandleAliasKind::HandleValue)) + ) + }) + }) +} + pub(super) fn enum_import_names(meta: &ComInterfaceMeta) -> Vec { meta.referenced_enums .iter() @@ -387,8 +401,11 @@ pub(super) fn wrap_arg_js(t: &TypeMeta, var: &str) -> String { if is_hresult(t) { return format!("DynCom.i32({var})"); } - if handle_type_name(t).is_some() { - return format!("DynCom.pointer({var})"); + if let Some((_, kind)) = handle_alias(t) { + return match kind { + HandleAliasKind::HandleValue => format!("DynCom.pointer(_handleArg({var}))"), + HandleAliasKind::StringPointer => format!("DynCom.pointer({var})"), + }; } if let TypeMeta::Interface { iid, .. } = t { if !iid.is_empty() { @@ -503,3 +520,5 @@ pub(super) fn is_win32_bool(t: &TypeMeta) -> bool { if namespace == "Windows.Win32.Foundation" && name == "BOOL" ) } + + diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts index c86f4e33..eaabb745 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts @@ -1,8 +1,8 @@ // Generated by dynwinrt-codegen — do not edit import type { DynWinRtValue } from '@microsoft/dynwinrt'; -/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ -export type HWND = bigint | number; +/** Opaque Win32 handle. Pass a raw pointer value as a `bigint` (full 64-bit) or `number` (safe integer), or an Electron/Node `Buffer`/`Uint8Array` of the handle's pointer bits — e.g. `BrowserWindow.getNativeWindowHandle()` — which is read as the handle VALUE. */ +export type HWND = bigint | number | Buffer | Uint8Array; export declare const IID_IDataTransferManagerInterop: unknown; diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js index 947f844a..93fb775b 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js @@ -1,6 +1,17 @@ // Generated by dynwinrt-codegen — do not edit import { DynCom, DynComMethodSig, DynWinRtValue, WinGuid } from '@microsoft/dynwinrt'; +function _handleArg(h) { + if (typeof h === 'bigint' || typeof h === 'number') return h; + if (h instanceof Uint8Array) { + const _dv = new DataView(h.buffer, h.byteOffset, h.byteLength); + if (h.byteLength >= 8) return _dv.getBigUint64(0, true); + if (h.byteLength >= 4) return BigInt(_dv.getUint32(0, true)); + throw new TypeError('handle Buffer must be at least 4 bytes'); + } + throw new TypeError(`handle must be a bigint, number, or Buffer, got ${typeof h}`); +} + export const IID_IDataTransferManagerInterop = WinGuid.parse('3a3dcd6c-3eab-43dc-bcde-45671ce800c8'); const IID_DataTransferManager_default = WinGuid.parse('a5caee9b-8708-49d1-8d36-67d25a8da00c'); @@ -26,11 +37,11 @@ export class IDataTransferManagerInterop { return new IDataTransferManagerInterop(_obj); } getForWindow(appWindow) { - const _raw = _IDataTransferManagerInterop.method(3).invoke(this._obj, [DynCom.pointer(appWindow), DynCom.iidPointer(IID_DataTransferManager_default)]); + const _raw = _IDataTransferManagerInterop.method(3).invoke(this._obj, [DynCom.pointer(_handleArg(appWindow)), DynCom.iidPointer(IID_DataTransferManager_default)]); const _out = DynCom.adoptComPointer(_raw, IID_DataTransferManager_default); return _out; } showShareUIForWindow(appWindow) { - _IDataTransferManagerInterop.method(4).invoke(this._obj, [DynCom.pointer(appWindow)]); + _IDataTransferManagerInterop.method(4).invoke(this._obj, [DynCom.pointer(_handleArg(appWindow))]); } } diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts index 5209ac63..ad7a9228 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts @@ -1,12 +1,12 @@ // Generated by dynwinrt-codegen — do not edit import { TBPFLAG } from './TBPFLAG.js'; -/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ -export type HICON = bigint | number; -/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ -export type HIMAGELIST = bigint | number; -/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ -export type HWND = bigint | number; +/** Opaque Win32 handle. Pass a raw pointer value as a `bigint` (full 64-bit) or `number` (safe integer), or an Electron/Node `Buffer`/`Uint8Array` of the handle's pointer bits — e.g. `BrowserWindow.getNativeWindowHandle()` — which is read as the handle VALUE. */ +export type HICON = bigint | number | Buffer | Uint8Array; +/** Opaque Win32 handle. Pass a raw pointer value as a `bigint` (full 64-bit) or `number` (safe integer), or an Electron/Node `Buffer`/`Uint8Array` of the handle's pointer bits — e.g. `BrowserWindow.getNativeWindowHandle()` — which is read as the handle VALUE. */ +export type HIMAGELIST = bigint | number | Buffer | Uint8Array; +/** Opaque Win32 handle. Pass a raw pointer value as a `bigint` (full 64-bit) or `number` (safe integer), or an Electron/Node `Buffer`/`Uint8Array` of the handle's pointer bits — e.g. `BrowserWindow.getNativeWindowHandle()` — which is read as the handle VALUE. */ +export type HWND = bigint | number | Buffer | Uint8Array; /** Win32 NUL-terminated string pointer. Pass a `Buffer` holding the string bytes (including the NUL terminator), or pass a raw pointer as `bigint`. */ export type PWSTR = bigint | Buffer; diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js index 73d716b1..8d848f80 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js @@ -2,6 +2,17 @@ import { DynCom, DynComMethodSig, WinGuid } from '@microsoft/dynwinrt'; import { TBPFLAG } from './TBPFLAG.js'; +function _handleArg(h) { + if (typeof h === 'bigint' || typeof h === 'number') return h; + if (h instanceof Uint8Array) { + const _dv = new DataView(h.buffer, h.byteOffset, h.byteLength); + if (h.byteLength >= 8) return _dv.getBigUint64(0, true); + if (h.byteLength >= 4) return BigInt(_dv.getUint32(0, true)); + throw new TypeError('handle Buffer must be at least 4 bytes'); + } + throw new TypeError(`handle must be a bigint, number, or Buffer, got ${typeof h}`); +} + export const IID_ITaskbarList3 = WinGuid.parse('ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf'); let _ITaskbarList3Cache; @@ -44,54 +55,54 @@ export class ITaskbarList3 { _ITaskbarList3.method(3).invoke(this._obj, []); } addTab(hwnd) { - _ITaskbarList3.method(4).invoke(this._obj, [DynCom.pointer(hwnd)]); + _ITaskbarList3.method(4).invoke(this._obj, [DynCom.pointer(_handleArg(hwnd))]); } deleteTab(hwnd) { - _ITaskbarList3.method(5).invoke(this._obj, [DynCom.pointer(hwnd)]); + _ITaskbarList3.method(5).invoke(this._obj, [DynCom.pointer(_handleArg(hwnd))]); } activateTab(hwnd) { - _ITaskbarList3.method(6).invoke(this._obj, [DynCom.pointer(hwnd)]); + _ITaskbarList3.method(6).invoke(this._obj, [DynCom.pointer(_handleArg(hwnd))]); } setActiveAlt(hwnd) { - _ITaskbarList3.method(7).invoke(this._obj, [DynCom.pointer(hwnd)]); + _ITaskbarList3.method(7).invoke(this._obj, [DynCom.pointer(_handleArg(hwnd))]); } markFullscreenWindow(hwnd, fFullscreen) { - _ITaskbarList3.method(8).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.i32(fFullscreen ? 1 : 0)]); + _ITaskbarList3.method(8).invoke(this._obj, [DynCom.pointer(_handleArg(hwnd)), DynCom.i32(fFullscreen ? 1 : 0)]); } setProgressValue(hwnd, ullCompleted, ullTotal) { - _ITaskbarList3.method(9).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.u64(BigInt(ullCompleted)), DynCom.u64(BigInt(ullTotal))]); + _ITaskbarList3.method(9).invoke(this._obj, [DynCom.pointer(_handleArg(hwnd)), DynCom.u64(BigInt(ullCompleted)), DynCom.u64(BigInt(ullTotal))]); } setProgressState(hwnd, tbpFlags) { - _ITaskbarList3.method(10).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.i32(tbpFlags)]); + _ITaskbarList3.method(10).invoke(this._obj, [DynCom.pointer(_handleArg(hwnd)), DynCom.i32(tbpFlags)]); } registerTab(tab, mDI) { - _ITaskbarList3.method(11).invoke(this._obj, [DynCom.pointer(tab), DynCom.pointer(mDI)]); + _ITaskbarList3.method(11).invoke(this._obj, [DynCom.pointer(_handleArg(tab)), DynCom.pointer(_handleArg(mDI))]); } unregisterTab(tab) { - _ITaskbarList3.method(12).invoke(this._obj, [DynCom.pointer(tab)]); + _ITaskbarList3.method(12).invoke(this._obj, [DynCom.pointer(_handleArg(tab))]); } setTabOrder(tab, insertBefore) { - _ITaskbarList3.method(13).invoke(this._obj, [DynCom.pointer(tab), DynCom.pointer(insertBefore)]); + _ITaskbarList3.method(13).invoke(this._obj, [DynCom.pointer(_handleArg(tab)), DynCom.pointer(_handleArg(insertBefore))]); } setTabActive(tab, mDI, reserved) { - _ITaskbarList3.method(14).invoke(this._obj, [DynCom.pointer(tab), DynCom.pointer(mDI), DynCom.u32(reserved)]); + _ITaskbarList3.method(14).invoke(this._obj, [DynCom.pointer(_handleArg(tab)), DynCom.pointer(_handleArg(mDI)), DynCom.u32(reserved)]); } thumbBarAddButtons(hwnd, cButtons, pButton) { - _ITaskbarList3.method(15).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.u32(cButtons), DynCom.pointer(pButton)]); + _ITaskbarList3.method(15).invoke(this._obj, [DynCom.pointer(_handleArg(hwnd)), DynCom.u32(cButtons), DynCom.pointer(pButton)]); } thumbBarUpdateButtons(hwnd, cButtons, pButton) { - _ITaskbarList3.method(16).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.u32(cButtons), DynCom.pointer(pButton)]); + _ITaskbarList3.method(16).invoke(this._obj, [DynCom.pointer(_handleArg(hwnd)), DynCom.u32(cButtons), DynCom.pointer(pButton)]); } thumbBarSetImageList(hwnd, himl) { - _ITaskbarList3.method(17).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(himl)]); + _ITaskbarList3.method(17).invoke(this._obj, [DynCom.pointer(_handleArg(hwnd)), DynCom.pointer(_handleArg(himl))]); } setOverlayIcon(hwnd, hIcon, description) { - _ITaskbarList3.method(18).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(hIcon), DynCom.pointer(description)]); + _ITaskbarList3.method(18).invoke(this._obj, [DynCom.pointer(_handleArg(hwnd)), DynCom.pointer(_handleArg(hIcon)), DynCom.pointer(description)]); } setThumbnailTooltip(hwnd, tip) { - _ITaskbarList3.method(19).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(tip)]); + _ITaskbarList3.method(19).invoke(this._obj, [DynCom.pointer(_handleArg(hwnd)), DynCom.pointer(tip)]); } setThumbnailClip(hwnd, prcClip) { - _ITaskbarList3.method(20).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(prcClip)]); + _ITaskbarList3.method(20).invoke(this._obj, [DynCom.pointer(_handleArg(hwnd)), DynCom.pointer(prcClip)]); } } diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs index e2da22ee..6ad6e198 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -173,12 +173,18 @@ fn param_type_mapping() { let dts = out.dts.as_str(); let js = out.js.as_str(); - // HWND is a handle value type → bigint | number surface (never Buffer). + // HWND is a handle value type → accepts bigint | number, or an Electron/Node + // Buffer (read as the handle VALUE). Its arg is unwrapped via `_handleArg`. assert!( - dts.contains("export type HWND = bigint | number;"), - "HWND should be projected as `bigint | number` in .d.ts, got:\n{}", + dts.contains("export type HWND = bigint | number | Buffer | Uint8Array;"), + "HWND should be projected as `bigint | number | Buffer | Uint8Array` in .d.ts, got:\n{}", dts ); + assert!( + js.contains("_handleArg(hwnd)") && js.contains("function _handleArg("), + "HWND arg must be unwrapped via the _handleArg helper in .js, got:\n{}", + js + ); // ULONGLONG (U64) → bigint // setProgressValue's completed/total params are U64 From 3e6d98ee154d589138bfc1db9b8f063a5d425a27 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Tue, 28 Jul 2026 22:36:06 +0800 Subject: [PATCH 22/28] Harden Classic COM generation and runtime safety Keep Classic COM isolated from WinRT while fixing pointer ownership, target-width ABI types, buffer validation, BSTR lifetime, unsigned enum projection, required parameters, and package generation. Move Classic COM E2E coverage into the unified test pipeline, require real Win32 metadata in CI, and remove checked-in generated fixtures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 41a826d4-b04a-4932-b1b8-31173fc5ab40 --- .github/agents/e2e-test.md | 7 +- .github/copilot-instructions.md | 4 +- .github/workflows/build.yml | 38 ++ .gitignore | 5 +- .pipelines/ci.yml | 22 +- bindings/js/Cargo.toml | 2 +- bindings/js/__test__/index.spec.ts | 18 + .../js/e2e/IDataTransferManagerInterop.d.ts | 16 - .../js/e2e/IDataTransferManagerInterop.js | 36 -- bindings/js/e2e/IShellLinkW.d.ts | 33 -- bindings/js/e2e/IShellLinkW.js | 125 ----- .../ISystemMediaTransportControlsInterop.d.ts | 15 - .../ISystemMediaTransportControlsInterop.js | 32 -- bindings/js/e2e/ITaskbarList3.js | 97 ---- bindings/js/e2e/SHOW_WINDOW_CMD.d.ts | 19 - bindings/js/e2e/SHOW_WINDOW_CMD.js | 18 - bindings/js/e2e/TBPFLAG.js | 8 - bindings/js/e2e/package.json | 4 - bindings/js/src/com.rs | 362 +++++++++++++-- bindings/js/src/lib.rs | 40 +- tests/e2e_test.ps1 | 113 ++++- .../js/e2e => tests/runners/com}/dtm.mjs | 6 +- .../runners/com}/electron-hwnd-buffer.mjs | 4 +- .../js/e2e => tests/runners/com}/hwnd.mjs | 2 +- .../runners/com}/pointer-reject-object.mjs | 2 +- .../runners/com}/shelllink-buffer.mjs | 8 +- .../js/e2e => tests/runners/com}/smtc.mjs | 36 +- .../e2e => tests/runners/com}/taskbarlist.mjs | 6 +- .../src/codegen/com/render.rs | 136 ++++-- .../src/codegen/com/type_mapping.rs | 90 +++- tools/dynwinrt-codegen/src/com_metadata.rs | 265 ++++++++++- tools/dynwinrt-codegen/src/main.rs | 105 ++++- .../dynwinrt-codegen/tests/win32_com_test.rs | 431 ++++++++++++++++-- 33 files changed, 1490 insertions(+), 615 deletions(-) delete mode 100644 bindings/js/e2e/IDataTransferManagerInterop.d.ts delete mode 100644 bindings/js/e2e/IDataTransferManagerInterop.js delete mode 100644 bindings/js/e2e/IShellLinkW.d.ts delete mode 100644 bindings/js/e2e/IShellLinkW.js delete mode 100644 bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts delete mode 100644 bindings/js/e2e/ISystemMediaTransportControlsInterop.js delete mode 100644 bindings/js/e2e/ITaskbarList3.js delete mode 100644 bindings/js/e2e/SHOW_WINDOW_CMD.d.ts delete mode 100644 bindings/js/e2e/SHOW_WINDOW_CMD.js delete mode 100644 bindings/js/e2e/TBPFLAG.js delete mode 100644 bindings/js/e2e/package.json rename {bindings/js/e2e => tests/runners/com}/dtm.mjs (90%) rename {bindings/js/e2e => tests/runners/com}/electron-hwnd-buffer.mjs (92%) rename {bindings/js/e2e => tests/runners/com}/hwnd.mjs (93%) rename {bindings/js/e2e => tests/runners/com}/pointer-reject-object.mjs (91%) rename {bindings/js/e2e => tests/runners/com}/shelllink-buffer.mjs (78%) rename {bindings/js/e2e => tests/runners/com}/smtc.mjs (75%) rename {bindings/js/e2e => tests/runners/com}/taskbarlist.mjs (93%) diff --git a/.github/agents/e2e-test.md b/.github/agents/e2e-test.md index 73a45eb5..c0a982d5 100644 --- a/.github/agents/e2e-test.md +++ b/.github/agents/e2e-test.md @@ -1,6 +1,6 @@ --- name: e2e-test -description: Run end-to-end tests for dynwinrt code generation and WinRT API invocation +description: Run end-to-end tests for dynwinrt code generation and WinRT/Classic COM API invocation tools: - powershell - view @@ -21,6 +21,9 @@ You run and manage the dynwinrt end-to-end test suite. # Python only .\tests\e2e_test.ps1 -SkipBuild -Lang py +# Classic COM only (requires Windows.Win32.winmd) +.\tests\e2e_test.ps1 -SkipBuild -Lang com + # Full build + test .\tests\e2e_test.ps1 ``` @@ -40,5 +43,5 @@ Avoid APIs that need WinAppSDK, network, or user interaction. ## Diagnosing failures 1. Check `tests/e2e_generated/results_py.json` or `results_ts.json` for structured failure details -2. Inspect generated code in `tests/e2e_generated/python_bindings/` or `ts/` +2. Inspect generated code in `tests/e2e_generated/python_bindings/`, `ts/`, or `com/` 3. Common issues: circular imports in codegen, naming mismatch (Python snake_case vs TS camelCase) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3242f6b8..edbe94e3 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -53,9 +53,9 @@ The E2E test framework validates the full pipeline: reading .winmd metadata → - `instantiate`: how to create an instance (`activate`, `static_factory`, or `none`) - `checks`: array of assertions (`property_equals`, `property_exists`, `method_equals`, `method_result_contains`, `static_equals`, `static_not_null`) -2. **Runners** (`tests/runners/py_runner.py`, `tests/runners/ts_runner.ts`) read the specs and execute them, outputting `results.json`. +2. **Runners** (`tests/runners/py_runner.py`, `tests/runners/ts_runner.ts`, and `tests/runners/com/*.mjs`) execute generated WinRT and Classic COM bindings. -3. **Orchestrator** (`tests/e2e_test.ps1`) handles build, code generation, and runner invocation. +3. **Orchestrator** (`tests/e2e_test.ps1`) handles build, temporary code generation, and runner invocation. Use `-Lang com` for the Classic COM suite; it requires `DYNWINRT_WIN32_WINMD` or an installed `Microsoft.Windows.SDK.Win32Metadata` package. 4. **Adding new test cases**: Add entries to `e2e_specs.json`: ```json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d844ae5..9b2fe6e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,10 +17,31 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + with: + targets: i686-pc-windows-msvc + - uses: NuGet/setup-nuget@v2 + - name: Install Win32 metadata + shell: pwsh + run: | + $root = Join-Path $env:RUNNER_TEMP 'win32metadata' + nuget install Microsoft.Windows.SDK.Win32Metadata ` + -Version 69.0.7-preview ` + -OutputDirectory $root ` + -DirectDownload ` + -NonInteractive + $winmd = Get-ChildItem $root -Filter Windows.Win32.winmd -File -Recurse | + Select-Object -First 1 + if (-not $winmd) { + throw 'Microsoft.Windows.SDK.Win32Metadata did not contain Windows.Win32.winmd' + } + "DYNWINRT_WIN32_WINMD=$($winmd.FullName)" >> $env:GITHUB_ENV + "DYNWINRT_REQUIRE_WIN32_METADATA=1" >> $env:GITHUB_ENV - name: Test core library run: cargo test -p dynwinrt - name: Test dynwinrt-codegen run: cargo test -p dynwinrt-codegen + - name: Check x86 Classic COM runtime + run: cargo check -p jswinrt_rs --target i686-pc-windows-msvc # E2E tests: winmd → generate → call real WinRT APIs e2e: @@ -35,6 +56,23 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.12' + - uses: NuGet/setup-nuget@v2 + - name: Install Win32 metadata + shell: pwsh + run: | + $root = Join-Path $env:RUNNER_TEMP 'win32metadata' + nuget install Microsoft.Windows.SDK.Win32Metadata ` + -Version 69.0.7-preview ` + -OutputDirectory $root ` + -DirectDownload ` + -NonInteractive + $winmd = Get-ChildItem $root -Filter Windows.Win32.winmd -File -Recurse | + Select-Object -First 1 + if (-not $winmd) { + throw 'Microsoft.Windows.SDK.Win32Metadata did not contain Windows.Win32.winmd' + } + "DYNWINRT_WIN32_WINMD=$($winmd.FullName)" >> $env:GITHUB_ENV + "DYNWINRT_REQUIRE_WIN32_METADATA=1" >> $env:GITHUB_ENV - name: Build dynwinrt-codegen run: cargo build -p dynwinrt-codegen --release - name: Build JS binding diff --git a/.gitignore b/.gitignore index c884ca33..04c6e8b5 100644 --- a/.gitignore +++ b/.gitignore @@ -438,6 +438,5 @@ bench-electron/out/ # Claude Code local settings **/settings.local.json -# Generated E2E projection fixtures (regenerated by codegen; not committed to keep PRs reviewable) -bindings/js/e2e/smtc-projected/ -bindings/js/e2e/generated/ +# Generated E2E projections are recreated and removed by tests/e2e_test.ps1. +tests/e2e_generated/ diff --git a/.pipelines/ci.yml b/.pipelines/ci.yml index afdc94fa..51b08a6a 100644 --- a/.pipelines/ci.yml +++ b/.pipelines/ci.yml @@ -99,6 +99,24 @@ extends: env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - task: PowerShell@2 + displayName: Install Win32 metadata + inputs: + targetType: inline + script: | + $root = Join-Path "$(Agent.TempDirectory)" "win32metadata" + nuget install Microsoft.Windows.SDK.Win32Metadata ` + -Version 69.0.7-preview ` + -OutputDirectory $root ` + -DirectDownload ` + -NonInteractive + if ($LASTEXITCODE -ne 0) { Write-Error "Win32 metadata install failed"; exit 1 } + $winmd = Get-ChildItem $root -Filter Windows.Win32.winmd -File -Recurse | + Select-Object -First 1 + if (-not $winmd) { Write-Error "Windows.Win32.winmd not found"; exit 1 } + Write-Host "##vso[task.setvariable variable=DYNWINRT_WIN32_WINMD]$($winmd.FullName)" + Write-Host "##vso[task.setvariable variable=DYNWINRT_REQUIRE_WIN32_METADATA]1" + # Core library tests - task: PowerShell@2 displayName: Test core library @@ -219,9 +237,9 @@ extends: .\bindings\py\.venv\Scripts\python.exe -m mypy.stubtest dynwinrt_py --allowlist bindings\py\stubtest_allowlist.txt --ignore-disjoint-bases if ($LASTEXITCODE -ne 0) { Write-Error "Python runtime stub validation failed"; exit 1 } - # E2E tests: winmd → generate → type-check → call real WinRT APIs + # E2E tests: winmd → generate → type-check → call real WinRT and COM APIs - task: PowerShell@2 - displayName: Run E2E tests (40 tests) + displayName: Run E2E tests inputs: targetType: inline script: | diff --git a/bindings/js/Cargo.toml b/bindings/js/Cargo.toml index d0a465f5..d48da037 100644 --- a/bindings/js/Cargo.toml +++ b/bindings/js/Cargo.toml @@ -12,7 +12,7 @@ repository = "https://github.com/microsoft/dynwinrt" crate-type = ["cdylib"] [dependencies] -napi = { version = "3", features = ["napi6"] } +napi = { version = "3", features = ["napi7"] } napi-derive = "3.0.0" dynwinrt = { path = "../../crates/dynwinrt" } windows-future = "0.3.2" diff --git a/bindings/js/__test__/index.spec.ts b/bindings/js/__test__/index.spec.ts index 96c0ee18..0d4a563e 100644 --- a/bindings/js/__test__/index.spec.ts +++ b/bindings/js/__test__/index.spec.ts @@ -8,6 +8,7 @@ import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { + DynCom, DynWinRtArray, DynWinRtMethodSig, DynWinRtType, @@ -19,6 +20,23 @@ import { roInitialize, } from '../dist/index.js' +test('DynCom rejects pointers after their TypedArray backing store is detached', (t) => { + const bytes = new Uint8Array(16) + const pointer = DynCom.pointer(bytes) + + structuredClone(bytes.buffer, { transfer: [bytes.buffer] }) + + t.is(bytes.byteLength, 0) + const error = t.throws(() => DynCom.asPointerBigint(pointer)) + t.regex(error.message, /backing ArrayBuffer is detached/) +}) + +test('DynCom does not adopt borrowed raw pointer bits as owned COM references', (t) => { + const borrowed = DynCom.pointer(0n) + const error = t.throws(() => DynCom.adoptComPointer(borrowed)) + t.regex(error.message, /only owned native outputs may be consumed/) +}) + test('getComputerName', (t) => { const name = getComputerName() t.truthy(name) diff --git a/bindings/js/e2e/IDataTransferManagerInterop.d.ts b/bindings/js/e2e/IDataTransferManagerInterop.d.ts deleted file mode 100644 index 25ba8c1a..00000000 --- a/bindings/js/e2e/IDataTransferManagerInterop.d.ts +++ /dev/null @@ -1,16 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit -import type { DynWinRtValue } from '../dist/index.js'; - -/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ -export type HWND = bigint | number; - -export declare const IID_IDataTransferManagerInterop: unknown; - -export declare class IDataTransferManagerInterop { - /** Activate the projected WinRT class and QI to the interop. */ - static create(): IDataTransferManagerInterop; - /** Wrap an existing native COM pointer (for QueryInterface bridging). */ - static _fromNative(obj: unknown): IDataTransferManagerInterop; - getForWindow(appWindow: HWND): DynWinRtValue; - showShareUIForWindow(appWindow: HWND): void; -} diff --git a/bindings/js/e2e/IDataTransferManagerInterop.js b/bindings/js/e2e/IDataTransferManagerInterop.js deleted file mode 100644 index a2edf89e..00000000 --- a/bindings/js/e2e/IDataTransferManagerInterop.js +++ /dev/null @@ -1,36 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit -import { DynCom, DynComMethodSig, DynWinRtValue, WinGuid } from '../dist/index.js'; - -export const IID_IDataTransferManagerInterop = WinGuid.parse('3a3dcd6c-3eab-43dc-bcde-45671ce800c8'); -const IID_DataTransferManager_default = WinGuid.parse('a5caee9b-8708-49d1-8d36-67d25a8da00c'); - -let _IDataTransferManagerInteropCache; -const _IDataTransferManagerInterop = new Proxy({}, { - get(_target, prop) { - _IDataTransferManagerInteropCache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.IDataTransferManagerInterop', IID_IDataTransferManagerInterop) - .addMethod('GetForWindow', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addOut(DynCom.pointerType())) - .addMethod('ShowShareUIForWindow', new DynComMethodSig().addIn(DynCom.pointerType())); - const value = _IDataTransferManagerInteropCache[prop]; - return typeof value === 'function' ? value.bind(_IDataTransferManagerInteropCache) : value; - }, -}); - -export class IDataTransferManagerInterop { - _obj; - constructor(obj) { this._obj = obj; } - static _fromNative(obj) { return new IDataTransferManagerInterop(obj); } - /** Create a new `IDataTransferManagerInterop` by activating the `Windows.ApplicationModel.DataTransfer.DataTransferManager` factory and QI'ing to the interop. */ - static create() { - const factory = DynWinRtValue.activationFactory('Windows.ApplicationModel.DataTransfer.DataTransferManager'); - const _obj = factory.cast(IID_IDataTransferManagerInterop); - return new IDataTransferManagerInterop(_obj); - } - getForWindow(appWindow) { - const _raw = _IDataTransferManagerInterop.method(3).invoke(this._obj, [DynCom.pointer(appWindow), DynCom.iidPointer(IID_DataTransferManager_default)]); - const _out = DynCom.adoptComPointer(_raw, IID_DataTransferManager_default); - return _out; - } - showShareUIForWindow(appWindow) { - _IDataTransferManagerInterop.method(4).invoke(this._obj, [DynCom.pointer(appWindow)]); - } -} diff --git a/bindings/js/e2e/IShellLinkW.d.ts b/bindings/js/e2e/IShellLinkW.d.ts deleted file mode 100644 index a803ea28..00000000 --- a/bindings/js/e2e/IShellLinkW.d.ts +++ /dev/null @@ -1,33 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit -import { SHOW_WINDOW_CMD } from './SHOW_WINDOW_CMD.js'; -import type { DynWinRtValue } from '../dist/index.js'; - -/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ -export type HWND = bigint | number; -/** Win32 NUL-terminated string pointer. Pass a `Buffer` holding the string bytes (including the NUL terminator), or pass a raw pointer as `bigint`. */ -export type PWSTR = bigint | Buffer; - -export declare const IID_IShellLinkW: unknown; - -export declare class IShellLinkW { - /** Wrap an existing native COM pointer (for QueryInterface bridging). */ - static _fromNative(obj: unknown): IShellLinkW; - getPath(cch?: number, pfd?: bigint | Buffer, fFlags?: number): string; - getIDList(): DynWinRtValue; - setIDList(pidl: bigint | Buffer): void; - getDescription(cch?: number): string; - setDescription(name: PWSTR): void; - getWorkingDirectory(cch?: number): string; - setWorkingDirectory(dir: PWSTR): void; - getArguments(cch?: number): string; - setArguments(args: PWSTR): void; - getHotkey(): number; - setHotkey(wHotkey: number): void; - getShowCmd(): SHOW_WINDOW_CMD; - setShowCmd(iShowCmd: SHOW_WINDOW_CMD): void; - getIconLocation(cch?: number): [string, number]; - setIconLocation(iconPath: PWSTR, iIcon: number): void; - setRelativePath(pathRel: PWSTR, reserved: number): void; - resolve(hwnd: HWND, fFlags: number): void; - setPath(file: PWSTR): void; -} diff --git a/bindings/js/e2e/IShellLinkW.js b/bindings/js/e2e/IShellLinkW.js deleted file mode 100644 index c2199386..00000000 --- a/bindings/js/e2e/IShellLinkW.js +++ /dev/null @@ -1,125 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit -import { DynCom, DynComMethodSig, WinGuid } from '../dist/index.js'; -import { SHOW_WINDOW_CMD } from './SHOW_WINDOW_CMD.js'; - -function _normalizeStringBufferCount(value, name) { - if (!Number.isInteger(value) || value <= 0) throw new TypeError(`${name} must be a positive integer`); - return value; -} -function _decodeWideString(buffer) { - let end = 0; - while (end + 1 < buffer.length && buffer.readUInt16LE(end) !== 0) end += 2; - return buffer.subarray(0, end).toString('utf16le'); -} - -export const IID_IShellLinkW = WinGuid.parse('000214f9-0000-0000-c000-000000000046'); - -let _IShellLinkWCache; -const _IShellLinkW = new Proxy({}, { - get(_target, prop) { - _IShellLinkWCache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.IShellLinkW', IID_IShellLinkW) - .addMethod('GetPath', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type()).addIn(DynCom.pointerType()).addIn(DynCom.u32Type())) - .addMethod('GetIDList', new DynComMethodSig().addOut(DynCom.pointerType())) - .addMethod('SetIDList', new DynComMethodSig().addIn(DynCom.pointerType())) - .addMethod('GetDescription', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) - .addMethod('SetDescription', new DynComMethodSig().addIn(DynCom.pointerType())) - .addMethod('GetWorkingDirectory', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) - .addMethod('SetWorkingDirectory', new DynComMethodSig().addIn(DynCom.pointerType())) - .addMethod('GetArguments', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) - .addMethod('SetArguments', new DynComMethodSig().addIn(DynCom.pointerType())) - .addMethod('GetHotkey', new DynComMethodSig().addOut(DynCom.u16Type())) - .addMethod('SetHotkey', new DynComMethodSig().addIn(DynCom.u16Type())) - .addMethod('GetShowCmd', new DynComMethodSig().addOut(DynCom.i32Type())) - .addMethod('SetShowCmd', new DynComMethodSig().addIn(DynCom.i32Type())) - .addMethod('GetIconLocation', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type()).addOut(DynCom.i32Type())) - .addMethod('SetIconLocation', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) - .addMethod('SetRelativePath', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u32Type())) - .addMethod('Resolve', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u32Type())) - .addMethod('SetPath', new DynComMethodSig().addIn(DynCom.pointerType())); - const value = _IShellLinkWCache[prop]; - return typeof value === 'function' ? value.bind(_IShellLinkWCache) : value; - }, -}); - -export class IShellLinkW { - _obj; - constructor(obj) { this._obj = obj; } - static _fromNative(obj) { return new IShellLinkW(obj); } - getPath(cch = 260, pfd = 0, fFlags = 0) { - cch = _normalizeStringBufferCount(cch, 'cch'); - const _buffer = Buffer.alloc(cch * 2); - _IShellLinkW.method(3).invoke(this._obj, [DynCom.pointer(_buffer), DynCom.i32(cch), DynCom.pointer(pfd), DynCom.u32(fFlags)]); - const _text = _decodeWideString(_buffer); - return _text; - } - getIDList() { - const _out = _IShellLinkW.method(4).invoke(this._obj, []); - return DynCom.adoptCoTaskMemPointer(_out); - } - setIDList(pidl) { - _IShellLinkW.method(5).invoke(this._obj, [DynCom.pointer(pidl)]); - } - getDescription(cch = 260) { - cch = _normalizeStringBufferCount(cch, 'cch'); - const _buffer = Buffer.alloc(cch * 2); - _IShellLinkW.method(6).invoke(this._obj, [DynCom.pointer(_buffer), DynCom.i32(cch)]); - const _text = _decodeWideString(_buffer); - return _text; - } - setDescription(name) { - _IShellLinkW.method(7).invoke(this._obj, [DynCom.pointer(name)]); - } - getWorkingDirectory(cch = 260) { - cch = _normalizeStringBufferCount(cch, 'cch'); - const _buffer = Buffer.alloc(cch * 2); - _IShellLinkW.method(8).invoke(this._obj, [DynCom.pointer(_buffer), DynCom.i32(cch)]); - const _text = _decodeWideString(_buffer); - return _text; - } - setWorkingDirectory(dir) { - _IShellLinkW.method(9).invoke(this._obj, [DynCom.pointer(dir)]); - } - getArguments(cch = 260) { - cch = _normalizeStringBufferCount(cch, 'cch'); - const _buffer = Buffer.alloc(cch * 2); - _IShellLinkW.method(10).invoke(this._obj, [DynCom.pointer(_buffer), DynCom.i32(cch)]); - const _text = _decodeWideString(_buffer); - return _text; - } - setArguments(args) { - _IShellLinkW.method(11).invoke(this._obj, [DynCom.pointer(args)]); - } - getHotkey() { - const _out = _IShellLinkW.method(12).invoke(this._obj, []); - return DynCom.toNumber(_out); - } - setHotkey(wHotkey) { - _IShellLinkW.method(13).invoke(this._obj, [DynCom.u16(wHotkey)]); - } - getShowCmd() { - const _out = _IShellLinkW.method(14).invoke(this._obj, []); - return DynCom.toNumber(_out); - } - setShowCmd(iShowCmd) { - _IShellLinkW.method(15).invoke(this._obj, [DynCom.i32(iShowCmd)]); - } - getIconLocation(cch = 260) { - cch = _normalizeStringBufferCount(cch, 'cch'); - const _buffer = Buffer.alloc(cch * 2); - const _out = _IShellLinkW.method(16).invoke(this._obj, [DynCom.pointer(_buffer), DynCom.i32(cch)]); - const _text = _decodeWideString(_buffer); - return [_text, DynCom.toNumber(_out)]; - } - setIconLocation(iconPath, iIcon) { - _IShellLinkW.method(17).invoke(this._obj, [DynCom.pointer(iconPath), DynCom.i32(iIcon)]); - } - setRelativePath(pathRel, reserved) { - _IShellLinkW.method(18).invoke(this._obj, [DynCom.pointer(pathRel), DynCom.u32(reserved)]); - } - resolve(hwnd, fFlags) { - _IShellLinkW.method(19).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.u32(fFlags)]); - } - setPath(file) { - _IShellLinkW.method(20).invoke(this._obj, [DynCom.pointer(file)]); - } -} diff --git a/bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts b/bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts deleted file mode 100644 index d3f83bf9..00000000 --- a/bindings/js/e2e/ISystemMediaTransportControlsInterop.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit -import type { DynWinRtValue } from '../dist/index.js'; - -/** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ -export type HWND = bigint | number; - -export declare const IID_ISystemMediaTransportControlsInterop: unknown; - -export declare class ISystemMediaTransportControlsInterop { - /** Activate the projected WinRT class and QI to the interop. */ - static create(): ISystemMediaTransportControlsInterop; - /** Wrap an existing native COM pointer (for QueryInterface bridging). */ - static _fromNative(obj: unknown): ISystemMediaTransportControlsInterop; - getForWindow(appWindow: HWND): DynWinRtValue; -} diff --git a/bindings/js/e2e/ISystemMediaTransportControlsInterop.js b/bindings/js/e2e/ISystemMediaTransportControlsInterop.js deleted file mode 100644 index 6c1ab1d4..00000000 --- a/bindings/js/e2e/ISystemMediaTransportControlsInterop.js +++ /dev/null @@ -1,32 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit -import { DynCom, DynComMethodSig, DynWinRtValue, WinGuid } from '../dist/index.js'; - -export const IID_ISystemMediaTransportControlsInterop = WinGuid.parse('ddb0472d-c911-4a1f-86d9-dc3d71a95f5a'); -const IID_SystemMediaTransportControls_default = WinGuid.parse('99fa3ff4-1742-42a6-902e-087d41f965ec'); - -let _ISystemMediaTransportControlsInteropCache; -const _ISystemMediaTransportControlsInterop = new Proxy({}, { - get(_target, prop) { - _ISystemMediaTransportControlsInteropCache ??= DynCom.registerIInspectableInterface('Windows.Win32.System.WinRT.ISystemMediaTransportControlsInterop', IID_ISystemMediaTransportControlsInterop) - .addMethod('GetForWindow', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addOut(DynCom.pointerType())); - const value = _ISystemMediaTransportControlsInteropCache[prop]; - return typeof value === 'function' ? value.bind(_ISystemMediaTransportControlsInteropCache) : value; - }, -}); - -export class ISystemMediaTransportControlsInterop { - _obj; - constructor(obj) { this._obj = obj; } - static _fromNative(obj) { return new ISystemMediaTransportControlsInterop(obj); } - /** Create a new `ISystemMediaTransportControlsInterop` by activating the `Windows.Media.SystemMediaTransportControls` factory and QI'ing to the interop. */ - static create() { - const factory = DynWinRtValue.activationFactory('Windows.Media.SystemMediaTransportControls'); - const _obj = factory.cast(IID_ISystemMediaTransportControlsInterop); - return new ISystemMediaTransportControlsInterop(_obj); - } - getForWindow(appWindow) { - const _raw = _ISystemMediaTransportControlsInterop.method(6).invoke(this._obj, [DynCom.pointer(appWindow), DynCom.iidPointer(IID_SystemMediaTransportControls_default)]); - const _out = DynCom.adoptComPointer(_raw, IID_SystemMediaTransportControls_default); - return _out; - } -} diff --git a/bindings/js/e2e/ITaskbarList3.js b/bindings/js/e2e/ITaskbarList3.js deleted file mode 100644 index f6f19f54..00000000 --- a/bindings/js/e2e/ITaskbarList3.js +++ /dev/null @@ -1,97 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit -import { DynCom, DynComMethodSig, WinGuid } from '../dist/index.js'; -import { TBPFLAG } from './TBPFLAG.js'; - -export const IID_ITaskbarList3 = WinGuid.parse('ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf'); - -let _ITaskbarList3Cache; -const _ITaskbarList3 = new Proxy({}, { - get(_target, prop) { - _ITaskbarList3Cache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.ITaskbarList3', IID_ITaskbarList3) - .addMethod('HrInit', new DynComMethodSig()) - .addMethod('AddTab', new DynComMethodSig().addIn(DynCom.pointerType())) - .addMethod('DeleteTab', new DynComMethodSig().addIn(DynCom.pointerType())) - .addMethod('ActivateTab', new DynComMethodSig().addIn(DynCom.pointerType())) - .addMethod('SetActiveAlt', new DynComMethodSig().addIn(DynCom.pointerType())) - .addMethod('MarkFullscreenWindow', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) - .addMethod('SetProgressValue', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u64Type()).addIn(DynCom.u64Type())) - .addMethod('SetProgressState', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) - .addMethod('RegisterTab', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) - .addMethod('UnregisterTab', new DynComMethodSig().addIn(DynCom.pointerType())) - .addMethod('SetTabOrder', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) - .addMethod('SetTabActive', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addIn(DynCom.u32Type())) - .addMethod('ThumbBarAddButtons', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u32Type()).addIn(DynCom.pointerType())) - .addMethod('ThumbBarUpdateButtons', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u32Type()).addIn(DynCom.pointerType())) - .addMethod('ThumbBarSetImageList', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) - .addMethod('SetOverlayIcon', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) - .addMethod('SetThumbnailTooltip', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) - .addMethod('SetThumbnailClip', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())); - const value = _ITaskbarList3Cache[prop]; - return typeof value === 'function' ? value.bind(_ITaskbarList3Cache) : value; - }, -}); - -export class ITaskbarList3 { - _obj; - constructor(obj) { this._obj = obj; } - static _fromNative(obj) { return new ITaskbarList3(obj); } - /** Create a new `ITaskbarList3` via `CoCreateInstance` on `CLSID_TaskbarList`. */ - static create() { - const _obj = DynCom.coCreateInstance('56fdf344-fd6d-11d0-958a-006097c9a090', IID_ITaskbarList3); - return new ITaskbarList3(_obj); - } - hrInit() { - _ITaskbarList3.method(3).invoke(this._obj, []); - } - addTab(hwnd) { - _ITaskbarList3.method(4).invoke(this._obj, [DynCom.pointer(hwnd)]); - } - deleteTab(hwnd) { - _ITaskbarList3.method(5).invoke(this._obj, [DynCom.pointer(hwnd)]); - } - activateTab(hwnd) { - _ITaskbarList3.method(6).invoke(this._obj, [DynCom.pointer(hwnd)]); - } - setActiveAlt(hwnd) { - _ITaskbarList3.method(7).invoke(this._obj, [DynCom.pointer(hwnd)]); - } - markFullscreenWindow(hwnd, fFullscreen) { - _ITaskbarList3.method(8).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.i32(fFullscreen ? 1 : 0)]); - } - setProgressValue(hwnd, ullCompleted, ullTotal) { - _ITaskbarList3.method(9).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.u64(BigInt(ullCompleted)), DynCom.u64(BigInt(ullTotal))]); - } - setProgressState(hwnd, tbpFlags) { - _ITaskbarList3.method(10).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.i32(tbpFlags)]); - } - registerTab(tab, mDI) { - _ITaskbarList3.method(11).invoke(this._obj, [DynCom.pointer(tab), DynCom.pointer(mDI)]); - } - unregisterTab(tab) { - _ITaskbarList3.method(12).invoke(this._obj, [DynCom.pointer(tab)]); - } - setTabOrder(tab, insertBefore) { - _ITaskbarList3.method(13).invoke(this._obj, [DynCom.pointer(tab), DynCom.pointer(insertBefore)]); - } - setTabActive(tab, mDI, reserved) { - _ITaskbarList3.method(14).invoke(this._obj, [DynCom.pointer(tab), DynCom.pointer(mDI), DynCom.u32(reserved)]); - } - thumbBarAddButtons(hwnd, cButtons, pButton) { - _ITaskbarList3.method(15).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.u32(cButtons), DynCom.pointer(pButton)]); - } - thumbBarUpdateButtons(hwnd, cButtons, pButton) { - _ITaskbarList3.method(16).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.u32(cButtons), DynCom.pointer(pButton)]); - } - thumbBarSetImageList(hwnd, himl) { - _ITaskbarList3.method(17).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(himl)]); - } - setOverlayIcon(hwnd, hIcon, description) { - _ITaskbarList3.method(18).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(hIcon), DynCom.pointer(description)]); - } - setThumbnailTooltip(hwnd, tip) { - _ITaskbarList3.method(19).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(tip)]); - } - setThumbnailClip(hwnd, prcClip) { - _ITaskbarList3.method(20).invoke(this._obj, [DynCom.pointer(hwnd), DynCom.pointer(prcClip)]); - } -} diff --git a/bindings/js/e2e/SHOW_WINDOW_CMD.d.ts b/bindings/js/e2e/SHOW_WINDOW_CMD.d.ts deleted file mode 100644 index 956c018a..00000000 --- a/bindings/js/e2e/SHOW_WINDOW_CMD.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit -export type SHOW_WINDOW_CMD = (typeof SHOW_WINDOW_CMD)[keyof typeof SHOW_WINDOW_CMD]; -export declare const SHOW_WINDOW_CMD: { - readonly SW_HIDE: 0; - readonly SW_SHOWNORMAL: 1; - readonly SW_NORMAL: 1; - readonly SW_SHOWMINIMIZED: 2; - readonly SW_SHOWMAXIMIZED: 3; - readonly SW_MAXIMIZE: 3; - readonly SW_SHOWNOACTIVATE: 4; - readonly SW_SHOW: 5; - readonly SW_MINIMIZE: 6; - readonly SW_SHOWMINNOACTIVE: 7; - readonly SW_SHOWNA: 8; - readonly SW_RESTORE: 9; - readonly SW_SHOWDEFAULT: 10; - readonly SW_FORCEMINIMIZE: 11; - readonly SW_MAX: 11; -}; diff --git a/bindings/js/e2e/SHOW_WINDOW_CMD.js b/bindings/js/e2e/SHOW_WINDOW_CMD.js deleted file mode 100644 index 87f174b7..00000000 --- a/bindings/js/e2e/SHOW_WINDOW_CMD.js +++ /dev/null @@ -1,18 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit -export const SHOW_WINDOW_CMD = Object.freeze({ - SW_HIDE: 0, - SW_SHOWNORMAL: 1, - SW_NORMAL: 1, - SW_SHOWMINIMIZED: 2, - SW_SHOWMAXIMIZED: 3, - SW_MAXIMIZE: 3, - SW_SHOWNOACTIVATE: 4, - SW_SHOW: 5, - SW_MINIMIZE: 6, - SW_SHOWMINNOACTIVE: 7, - SW_SHOWNA: 8, - SW_RESTORE: 9, - SW_SHOWDEFAULT: 10, - SW_FORCEMINIMIZE: 11, - SW_MAX: 11, -}); diff --git a/bindings/js/e2e/TBPFLAG.js b/bindings/js/e2e/TBPFLAG.js deleted file mode 100644 index 58af8cf8..00000000 --- a/bindings/js/e2e/TBPFLAG.js +++ /dev/null @@ -1,8 +0,0 @@ -// Generated by dynwinrt-codegen — do not edit -export const TBPFLAG = Object.freeze({ - TBPF_NOPROGRESS: 0, - TBPF_INDETERMINATE: 1, - TBPF_NORMAL: 2, - TBPF_ERROR: 4, - TBPF_PAUSED: 8, -}); diff --git a/bindings/js/e2e/package.json b/bindings/js/e2e/package.json deleted file mode 100644 index 96b1890a..00000000 --- a/bindings/js/e2e/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "module", - "private": true -} diff --git a/bindings/js/src/com.rs b/bindings/js/src/com.rs index de4a8c0d..e42dcc5c 100644 --- a/bindings/js/src/com.rs +++ b/bindings/js/src/com.rs @@ -1,22 +1,92 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use napi::bindgen_prelude::{BigInt, FromNapiValue, Unknown}; use napi::JsValue; +use napi::bindgen_prelude::{BigInt, FromNapiValue, ToNapiValue, Unknown}; use napi_derive::napi; -use windows::core::{GUID, IUnknown, Interface as _}; +use windows::core::{GUID, Interface as _}; -use super::{DynWinRTValue, WinGUID, TABLE}; +use super::{DynWinRTValue, TABLE, WinGUID}; #[allow(dead_code)] pub(super) enum NativePointerOwner { - Buffer(napi::bindgen_prelude::Buffer), - Uint8Array(napi::bindgen_prelude::Uint8Array), - ComObject(IUnknown), + Uint8Array { + value: std::sync::Mutex, + env: napi::sys::napi_env, + pointer: usize, + length: usize, + }, CoTaskMem(*mut std::ffi::c_void), Guid(*mut GUID), } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum PointerProvenance { + None, + Borrowed, + NativeOutput, +} + +impl NativePointerOwner { + fn validate(&self) -> napi::Result<()> { + let Self::Uint8Array { + value, + env, + pointer, + length, + } = self + else { + return Ok(()); + }; + let mut value = value + .lock() + .map_err(|_| napi::Error::from_reason("TypedArray pointer owner lock is poisoned"))?; + let raw = unsafe { + <&mut napi::bindgen_prelude::Uint8Array as ToNapiValue>::to_napi_value(*env, &mut *value) + }?; + let mut typed_array_type = 0; + let mut current_length = 0usize; + let mut current_pointer = std::ptr::null_mut(); + let mut array_buffer = std::ptr::null_mut(); + let mut byte_offset = 0usize; + napi::check_status!( + unsafe { + napi::sys::napi_get_typedarray_info( + *env, + raw, + &mut typed_array_type, + &mut current_length, + &mut current_pointer, + &mut array_buffer, + &mut byte_offset, + ) + }, + "Failed to revalidate TypedArray backing storage" + )?; + let mut detached = false; + napi::check_status!( + unsafe { napi::sys::napi_is_detached_arraybuffer(*env, array_buffer, &mut detached) }, + "Failed to inspect TypedArray backing storage" + )?; + if detached { + return Err(napi::Error::from_reason( + "Cannot use a pointer whose TypedArray backing ArrayBuffer is detached", + )); + } + let current_pointer = if current_length == 0 { + 0 + } else { + current_pointer as usize + }; + if current_length != *length || current_pointer != *pointer { + return Err(napi::Error::from_reason( + "Cannot use a pointer whose TypedArray backing storage changed", + )); + } + Ok(()) + } +} + impl Drop for NativePointerOwner { fn drop(&mut self) { match self { @@ -89,9 +159,9 @@ fn pointer(value: Unknown) -> napi::Result { value_type, sys::ValueType::napi_null | sys::ValueType::napi_undefined ) { - return Ok(DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr( - std::ptr::null_mut(), - ))); + return Ok(DynWinRTValue::with_borrowed_pointer( + dynwinrt::WinRTValue::RawPtr(std::ptr::null_mut()), + )); } if value_type == sys::ValueType::napi_bigint { let bigint = unsafe { BigInt::from_napi_value(env, raw) }?; @@ -101,9 +171,9 @@ fn pointer(value: Unknown) -> napi::Result { "pointer(): bigint must fit in an unsigned pointer", )); } - return Ok(DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr( - bits as usize as *mut std::ffi::c_void, - ))); + return Ok(DynWinRTValue::with_borrowed_pointer( + dynwinrt::WinRTValue::RawPtr(bits as usize as *mut std::ffi::c_void), + )); } if value_type == sys::ValueType::napi_number { let mut number = 0.0; @@ -118,22 +188,25 @@ fn pointer(value: Unknown) -> napi::Result { "pointer(): number must be a non-negative safe integer that fits in a pointer", )); } - return Ok(DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr( - number as usize as *mut std::ffi::c_void, - ))); - } - if let Ok(buffer) = unsafe { napi::bindgen_prelude::Buffer::from_napi_value(env, raw) } { - let ptr = buffer.as_ref().as_ptr() as *mut std::ffi::c_void; - return Ok(DynWinRTValue::with_pointer_owner( - dynwinrt::WinRTValue::RawPtr(ptr), - NativePointerOwner::Buffer(buffer), + return Ok(DynWinRTValue::with_borrowed_pointer( + dynwinrt::WinRTValue::RawPtr(number as usize as *mut std::ffi::c_void), )); } if let Ok(array) = unsafe { napi::bindgen_prelude::Uint8Array::from_napi_value(env, raw) } { - let ptr = array.as_ref().as_ptr() as *mut std::ffi::c_void; + let length = array.len(); + let pointer = if length == 0 { + 0 + } else { + array.as_ref().as_ptr() as usize + }; return Ok(DynWinRTValue::with_pointer_owner( - dynwinrt::WinRTValue::RawPtr(ptr), - NativePointerOwner::Uint8Array(array), + dynwinrt::WinRTValue::RawPtr(pointer as *mut std::ffi::c_void), + NativePointerOwner::Uint8Array { + value: std::sync::Mutex::new(array), + env, + pointer, + length, + }, )); } // Reject existing DynWinRtValue inputs. Borrowing an Object's raw COM pointer @@ -154,7 +227,7 @@ fn adopt_com_pointer( value: &mut DynWinRTValue, iid: Option<&WinGUID>, ) -> napi::Result { - let ptr = take_raw_pointer(value, "COM interface")?; + let ptr = take_native_output_pointer(value, "COM interface")?; let adopted = unsafe { dynwinrt::com::adopt_com_pointer(ptr) }; match iid { Some(iid) => adopted @@ -166,7 +239,7 @@ fn adopt_com_pointer( } fn adopt_co_task_mem_pointer(value: &mut DynWinRTValue) -> napi::Result { - let ptr = take_raw_pointer(value, "CoTaskMem allocation")?; + let ptr = take_native_output_pointer(value, "CoTaskMem allocation")?; if ptr.is_null() { return Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Null)); } @@ -177,21 +250,26 @@ fn adopt_co_task_mem_pointer(value: &mut DynWinRTValue) -> napi::Result napi::Result { + validate_pointer_owner(value)?; let bits = match &value.0 { - dynwinrt::WinRTValue::Object(object) => object.as_raw() as usize, + dynwinrt::WinRTValue::Object(_) => { + return Err(napi::Error::from_reason( + "Managed COM objects cannot be exported as raw pointer addresses", + )); + } dynwinrt::WinRTValue::RawPtr(ptr) => *ptr as usize, dynwinrt::WinRTValue::Null => 0, _ => { return Err(napi::Error::from_reason( "Value is not a pointer or COM object", - )) + )); } }; Ok(BigInt::from(bits as u64)) } fn take_co_task_mem_wide_string(value: &mut DynWinRTValue) -> napi::Result { - let ptr = take_raw_pointer(value, "wide-string")?; + let ptr = take_native_output_pointer(value, "wide-string")?; if ptr.is_null() { return Ok(String::new()); } @@ -202,7 +280,7 @@ fn take_co_task_mem_wide_string(value: &mut DynWinRTValue) -> napi::Result napi::Result { - let ptr = take_raw_pointer(value, "ANSI-string")?; + let ptr = take_native_output_pointer(value, "ANSI-string")?; if ptr.is_null() { return Ok(String::new()); } @@ -212,7 +290,23 @@ fn take_co_task_mem_ansi_string(value: &mut DynWinRTValue) -> napi::Result napi::Result { + let ptr = take_native_output_pointer(value, "BSTR")?; + if ptr.is_null() { + return Ok(String::new()); + } + let value = unsafe { windows::core::BSTR::from_raw(ptr.cast()) }; + String::try_from(&value).map_err(|error| napi::Error::from_reason(error.to_string())) +} + +fn validate_pointer_owner(value: &DynWinRTValue) -> napi::Result<()> { + if let Some(owner) = &value.1 { + owner.validate()?; + } + Ok(()) +} + +fn take_native_output_pointer( value: &mut DynWinRTValue, description: &str, ) -> napi::Result<*mut std::ffi::c_void> { @@ -221,9 +315,20 @@ fn take_raw_pointer( "Cannot consume an owner-backed {description} pointer" ))); } + if value.2 != PointerProvenance::NativeOutput { + return Err(napi::Error::from_reason(format!( + "Cannot adopt a borrowed {description} pointer; only owned native outputs may be consumed" + ))); + } match std::mem::replace(&mut value.0, dynwinrt::WinRTValue::Null) { - dynwinrt::WinRTValue::RawPtr(ptr) => Ok(ptr), - dynwinrt::WinRTValue::Null => Ok(std::ptr::null_mut()), + dynwinrt::WinRTValue::RawPtr(ptr) => { + value.2 = PointerProvenance::None; + Ok(ptr) + } + dynwinrt::WinRTValue::Null => { + value.2 = PointerProvenance::None; + Ok(std::ptr::null_mut()) + } other => { value.0 = other; Err(napi::Error::from_reason(format!( @@ -341,12 +446,15 @@ impl DynComMethodHandle { .as_object() .ok_or_else(|| napi::Error::from_reason("invoke() requires a COM object"))? .as_raw(); + for arg in &args { + validate_pointer_owner(arg)?; + } let args = args.iter().map(|arg| arg.0.clone()).collect::>(); let results = self .0 .invoke(raw, &args) .map_err(|error| napi::Error::from_reason(error.message()))?; - Ok(DynWinRTValue::new( + Ok(DynWinRTValue::from_com_result( results .into_iter() .next() @@ -365,11 +473,19 @@ impl DynComMethodHandle { .as_object() .ok_or_else(|| napi::Error::from_reason("invokeAll() requires a COM object"))? .as_raw(); + for arg in &args { + validate_pointer_owner(arg)?; + } let args = args.iter().map(|arg| arg.0.clone()).collect::>(); self .0 .invoke(raw, &args) - .map(|results| results.into_iter().map(DynWinRTValue::new).collect()) + .map(|results| { + results + .into_iter() + .map(DynWinRTValue::from_com_result) + .collect() + }) .map_err(|error| napi::Error::from_reason(error.message())) } } @@ -454,6 +570,30 @@ impl DynCom { DynComType(dynwinrt::com::Type::winrt(TABLE.u64_type())) } + #[napi] + pub fn isize_type() -> DynComType { + #[cfg(target_pointer_width = "64")] + { + DynComType(dynwinrt::com::Type::winrt(TABLE.i64_type())) + } + #[cfg(target_pointer_width = "32")] + { + DynComType(dynwinrt::com::Type::winrt(TABLE.i32_type())) + } + } + + #[napi] + pub fn usize_type() -> DynComType { + #[cfg(target_pointer_width = "64")] + { + DynComType(dynwinrt::com::Type::winrt(TABLE.u64_type())) + } + #[cfg(target_pointer_width = "32")] + { + DynComType(dynwinrt::com::Type::winrt(TABLE.u32_type())) + } + } + #[napi] pub fn f32_type() -> DynComType { DynComType(dynwinrt::com::Type::winrt(TABLE.f32_type())) @@ -546,6 +686,50 @@ impl DynCom { Ok(DynWinRTValue::new(dynwinrt::WinRTValue::U64(value))) } + #[napi] + pub fn isize(value: BigInt) -> napi::Result { + let (value, lossless) = value.get_i64(); + if !lossless { + return Err(napi::Error::from_reason( + "DynCom.isize(): value must fit in a pointer-sized signed integer", + )); + } + #[cfg(target_pointer_width = "64")] + { + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::I64(value))) + } + #[cfg(target_pointer_width = "32")] + { + let value = i32::try_from(value).map_err(|_| { + napi::Error::from_reason("DynCom.isize(): value must fit in a pointer-sized signed integer") + })?; + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::I32(value))) + } + } + + #[napi] + pub fn usize(value: BigInt) -> napi::Result { + let (negative, value, lossless) = value.get_u64(); + if negative || !lossless { + return Err(napi::Error::from_reason( + "DynCom.usize(): value must fit in a pointer-sized unsigned integer", + )); + } + #[cfg(target_pointer_width = "64")] + { + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::U64(value))) + } + #[cfg(target_pointer_width = "32")] + { + let value = u32::try_from(value).map_err(|_| { + napi::Error::from_reason( + "DynCom.usize(): value must fit in a pointer-sized unsigned integer", + ) + })?; + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::U32(value))) + } + } + #[napi] pub fn f32(value: f64) -> DynWinRTValue { DynWinRTValue::f32(value) @@ -573,9 +757,7 @@ impl DynCom { #[napi] pub fn pointer( - #[napi( - ts_arg_type = "bigint | number | Buffer | Uint8Array | DynWinRtValue | null | undefined" - )] + #[napi(ts_arg_type = "bigint | number | Buffer | Uint8Array | null | undefined")] value: Unknown, ) -> napi::Result { self::pointer(value) @@ -634,6 +816,11 @@ impl DynCom { self::take_co_task_mem_ansi_string(value) } + #[napi] + pub fn take_bstr(value: &mut DynWinRTValue) -> napi::Result { + self::take_bstr(value) + } + #[napi] pub fn to_u32(value: &DynWinRTValue) -> napi::Result { match &value.0 { @@ -658,6 +845,36 @@ impl DynCom { } } + #[napi] + pub fn to_isize_bigint(value: &DynWinRTValue) -> napi::Result { + #[cfg(target_pointer_width = "64")] + let result = match &value.0 { + dynwinrt::WinRTValue::I64(value) => Some(BigInt::from(*value)), + _ => None, + }; + #[cfg(target_pointer_width = "32")] + let result = match &value.0 { + dynwinrt::WinRTValue::I32(value) => Some(BigInt::from(i64::from(*value))), + _ => None, + }; + result.ok_or_else(|| napi::Error::from_reason("Value is not a pointer-sized signed integer")) + } + + #[napi] + pub fn to_usize_bigint(value: &DynWinRTValue) -> napi::Result { + #[cfg(target_pointer_width = "64")] + let result = match &value.0 { + dynwinrt::WinRTValue::U64(value) => Some(BigInt::from(*value)), + _ => None, + }; + #[cfg(target_pointer_width = "32")] + let result = match &value.0 { + dynwinrt::WinRTValue::U32(value) => Some(BigInt::from(u64::from(*value))), + _ => None, + }; + result.ok_or_else(|| napi::Error::from_reason("Value is not a pointer-sized unsigned integer")) + } + #[napi] pub fn create_test_hwnd() -> napi::Result { self::create_test_hwnd() @@ -678,20 +895,74 @@ mod tests { unsafe { std::ptr::copy_nonoverlapping(wide.as_ptr(), ptr.cast::(), wide.len()); } - let mut value = DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr(ptr)); + let mut value = DynWinRTValue::from_com_result(dynwinrt::WinRTValue::RawPtr(ptr)); assert_eq!(take_co_task_mem_wide_string(&mut value).unwrap(), text); assert!(matches!(value.0, dynwinrt::WinRTValue::Null)); } #[test] - fn consuming_raw_pointer_clears_source_value() { + fn consuming_native_output_pointer_clears_source_value() { let ptr = 0x1234usize as *mut std::ffi::c_void; - let mut value = DynWinRTValue::new(dynwinrt::WinRTValue::RawPtr(ptr)); + let mut value = DynWinRTValue::from_com_result(dynwinrt::WinRTValue::RawPtr(ptr)); - assert_eq!(take_raw_pointer(&mut value, "test").unwrap(), ptr); + assert_eq!(take_native_output_pointer(&mut value, "test").unwrap(), ptr); assert!(matches!(value.0, dynwinrt::WinRTValue::Null)); - assert!(take_raw_pointer(&mut value, "test").unwrap().is_null()); + assert!(take_native_output_pointer(&mut value, "test").is_err()); + } + + #[test] + fn borrowed_pointer_cannot_be_adopted() { + let ptr = 0x1234usize as *mut std::ffi::c_void; + let mut value = DynWinRTValue::with_borrowed_pointer(dynwinrt::WinRTValue::RawPtr(ptr)); + + let error = take_native_output_pointer(&mut value, "COM interface").unwrap_err(); + assert!( + error + .reason + .contains("Cannot adopt a borrowed COM interface") + ); + assert!(matches!(value.0, dynwinrt::WinRTValue::RawPtr(raw) if raw == ptr)); + } + + #[test] + fn takes_and_frees_bstr() { + let raw = windows::core::BSTR::from("dynwinrt").into_raw(); + let mut value = + DynWinRTValue::from_com_result(dynwinrt::WinRTValue::RawPtr(raw as *mut std::ffi::c_void)); + + assert_eq!(take_bstr(&mut value).unwrap(), "dynwinrt"); + assert!(matches!(value.0, dynwinrt::WinRTValue::Null)); + } + + #[test] + fn managed_com_object_address_is_not_exported() { + dynwinrt::com::initialize_apartment(dynwinrt::com::ApartmentType::MultiThreaded).unwrap(); + let iid = WinGUID(GUID::from_u128(0x000214f9_0000_0000_c000_000000000046)); + let value = co_create_instance("00021401-0000-0000-c000-000000000046".into(), &iid).unwrap(); + + let error = as_pointer_bigint(&value).unwrap_err(); + assert!( + error + .reason + .contains("Managed COM objects cannot be exported") + ); + } + + #[test] + fn pointer_sized_values_use_the_current_target_width() { + let signed = DynCom::isize(BigInt::from(-1i64)).unwrap(); + let unsigned = DynCom::usize(BigInt::from(1u64)).unwrap(); + #[cfg(target_pointer_width = "64")] + { + assert!(matches!(signed.0, dynwinrt::WinRTValue::I64(-1))); + assert!(matches!(unsigned.0, dynwinrt::WinRTValue::U64(1))); + } + #[cfg(target_pointer_width = "32")] + { + assert!(matches!(signed.0, dynwinrt::WinRTValue::I32(-1))); + assert!(matches!(unsigned.0, dynwinrt::WinRTValue::U32(1))); + } } #[test] @@ -712,7 +983,10 @@ mod tests { dynwinrt::WinRTValue::RawPtr(ptr) => { assert!(!ptr.is_null()); let read = unsafe { *(ptr as *const GUID) }; - assert_eq!(read, guid, "REFIID pointer must hold the correct GUID bytes"); + assert_eq!( + read, guid, + "REFIID pointer must hold the correct GUID bytes" + ); } _ => panic!("iid_pointer must return a RawPtr"), } diff --git a/bindings/js/src/lib.rs b/bindings/js/src/lib.rs index 64c22d68..bbdb7686 100644 --- a/bindings/js/src/lib.rs +++ b/bindings/js/src/lib.rs @@ -11,11 +11,11 @@ use std::{ }; use dynwinrt; +use napi::Env; use napi::bindgen_prelude::{BigInt, PromiseRaw}; use napi::threadsafe_function::ThreadsafeFunctionCallMode; -use napi::Env; use napi_derive::napi; -use windows::core::{IUnknown, Interface, HSTRING}; +use windows::core::{HSTRING, IUnknown, Interface}; mod com; pub use com::{DynCom, DynComInterface, DynComMethodHandle, DynComMethodSig, DynComType}; @@ -105,7 +105,7 @@ pub fn get_winappsdk_resource_pri_path() -> napi::Result { #[napi] pub fn ro_initialize(apartment_type: Option) { use windows::Win32::System::WinRT::{ - RoInitialize, RO_INIT_MULTITHREADED, RO_INIT_SINGLETHREADED, + RO_INIT_MULTITHREADED, RO_INIT_SINGLETHREADED, RoInitialize, }; let init_type = match apartment_type.unwrap_or(1) { 0 => RO_INIT_SINGLETHREADED, @@ -417,7 +417,7 @@ impl DynWinRTMethodHandle { _ => { return Err(napi::Error::from_reason( "invoke() requires an Object value", - )) + )); } }; let wrt_args: Vec = args.iter().map(|a| a.0.clone()).collect(); @@ -464,7 +464,7 @@ impl DynWinRTMethodHandle { _ => { return Err(napi::Error::from_reason( "invoke_all() requires an Object value", - )) + )); } }; let wrt_args: Vec = args.iter().map(|a| a.0.clone()).collect(); @@ -575,17 +575,34 @@ impl DynWinRTMethodHandle { // ====================================================================== #[napi] -pub struct DynWinRTValue(dynwinrt::WinRTValue, Option); +pub struct DynWinRTValue( + dynwinrt::WinRTValue, + Option, + com::PointerProvenance, +); unsafe impl Send for DynWinRTValue {} unsafe impl Sync for DynWinRTValue {} impl DynWinRTValue { fn new(value: dynwinrt::WinRTValue) -> Self { - Self(value, None) + Self(value, None, com::PointerProvenance::None) } fn with_pointer_owner(value: dynwinrt::WinRTValue, owner: com::NativePointerOwner) -> Self { - Self(value, Some(owner)) + Self(value, Some(owner), com::PointerProvenance::Borrowed) + } + + fn with_borrowed_pointer(value: dynwinrt::WinRTValue) -> Self { + Self(value, None, com::PointerProvenance::Borrowed) + } + + fn from_com_result(value: dynwinrt::WinRTValue) -> Self { + let provenance = if matches!(value, dynwinrt::WinRTValue::RawPtr(_)) { + com::PointerProvenance::NativeOutput + } else { + com::PointerProvenance::None + }; + Self(value, None, provenance) } } @@ -608,6 +625,7 @@ impl DynWinRTValue { pub fn release(&mut self) { self.0 = dynwinrt::WinRTValue::Null; self.1 = None; + self.2 = com::PointerProvenance::None; } #[napi] @@ -1473,8 +1491,8 @@ pub fn has_package_identity() -> bool { pub fn get_computer_name() -> napi::Result { #[cfg(target_os = "windows")] { - use windows::core::PWSTR; use windows::Win32::System::WindowsProgramming::GetComputerNameW; + use windows::core::PWSTR; let mut buffer = [0u16; 256]; let mut size = buffer.len() as u32; @@ -1677,8 +1695,8 @@ impl DynWinRtDelegate { #[napi(ts_arg_type = "(...args: DynWinRTValue[]) => void")] callback: napi::bindgen_prelude::Function<'static, Vec, ()>, ) -> napi::Result { - use napi::bindgen_prelude::ToNapiValue; use napi::JsValue; + use napi::bindgen_prelude::ToNapiValue; use windows::Win32::System::Threading::GetCurrentThreadId; // Track the thread we were registered on. WinRT delegate callbacks that @@ -1897,8 +1915,8 @@ impl DynWinRtElementFactory { #[napi(ts_arg_type = "(args: DynWinRtValue) => void")] recycle_element: ElementFactoryRecycleFunction, ) -> napi::Result { - use napi::bindgen_prelude::{FromNapiValue, ToNapiValue}; use napi::JsValue; + use napi::bindgen_prelude::{FromNapiValue, ToNapiValue}; use windows::Win32::System::Threading::GetCurrentThreadId; const E_FAIL: windows::core::HRESULT = windows::core::HRESULT(0x80004005u32 as i32); diff --git a/tests/e2e_test.ps1 b/tests/e2e_test.ps1 index 0ebf947a..55a620ee 100644 --- a/tests/e2e_test.ps1 +++ b/tests/e2e_test.ps1 @@ -3,25 +3,32 @@ # Licensed under the MIT License. # # E2E test orchestrator: build, generate, run language-specific runners, collect results. -# All test logic lives in runners/py_runner.py and runners/ts_runner.ts. +# Test logic lives in runners/py_runner.py, runners/ts_runner.ts, and runners/com/*.mjs. # # Usage: # .\tests\e2e_test.ps1 # Full (build + generate + test) # .\tests\e2e_test.ps1 -SkipBuild # Skip build step # .\tests\e2e_test.ps1 -Lang py # Python only # .\tests\e2e_test.ps1 -Lang ts # TypeScript only +# .\tests\e2e_test.ps1 -Lang com # Classic COM only param( [switch]$SkipBuild, - [string[]]$Lang = @("py", "ts") + [ValidateSet("py", "ts", "com")] + [string[]]$Lang = @("py", "ts", "com") ) $ErrorActionPreference = "Stop" +$langWasExplicit = $PSBoundParameters.ContainsKey("Lang") $root = Split-Path $PSScriptRoot -Parent $specsFile = Join-Path $PSScriptRoot "e2e_specs.json" $e2eDir = Join-Path $root "tests\e2e_generated" $runnersDir = Join-Path $root "tests\runners" $pyBindingsDir = Join-Path $e2eDir "python_bindings" +$comBindingsDir = Join-Path $e2eDir "com" +$comShellDir = Join-Path $comBindingsDir "shell" +$comInteropDir = Join-Path $comBindingsDir "interop" +$comSmtcDir = Join-Path $comBindingsDir "smtc" $env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH" @@ -37,10 +44,45 @@ if ("py" -in $Lang -and -not $hasPython) { Write-Host " SKIP Python (not installed)" -ForegroundColor DarkYellow $Lang = $Lang | Where-Object { $_ -ne "py" } } -if ("ts" -in $Lang -and -not $hasNode) { - Write-Host " SKIP TypeScript (Node.js not installed)" -ForegroundColor DarkYellow - $Lang = $Lang | Where-Object { $_ -ne "ts" } +if (("ts" -in $Lang -or "com" -in $Lang) -and -not $hasNode) { + Write-Host " SKIP JavaScript E2E (Node.js not installed)" -ForegroundColor DarkYellow + $Lang = @($Lang | Where-Object { $_ -notin @("ts", "com") }) } + +function Find-Win32Winmd { + if ($env:DYNWINRT_WIN32_WINMD -and (Test-Path $env:DYNWINRT_WIN32_WINMD)) { + return (Resolve-Path $env:DYNWINRT_WIN32_WINMD).Path + } + + $packageRoot = Join-Path $env:USERPROFILE ".nuget\packages\microsoft.windows.sdk.win32metadata" + if (Test-Path $packageRoot) { + $candidate = Get-ChildItem $packageRoot -Filter Windows.Win32.winmd -File -Recurse | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 + if ($candidate) { return $candidate.FullName } + } + + $legacyPath = "C:\s\win32metadata\Windows.Win32.winmd" + if (Test-Path $legacyPath) { return $legacyPath } + return $null +} + +$win32Winmd = $null +if ("com" -in $Lang) { + $win32Winmd = Find-Win32Winmd + if (-not $win32Winmd) { + if ($langWasExplicit -or $env:DYNWINRT_REQUIRE_WIN32_METADATA -eq "1") { + Write-Error "Classic COM E2E requires Windows.Win32.winmd. Set DYNWINRT_WIN32_WINMD or install Microsoft.Windows.SDK.Win32Metadata." + exit 1 + } + Write-Host " SKIP Classic COM (Windows.Win32.winmd not found)" -ForegroundColor DarkYellow + $Lang = @($Lang | Where-Object { $_ -ne "com" }) + } else { + $env:DYNWINRT_WIN32_WINMD = $win32Winmd + Write-Host " Win32 metadata: $win32Winmd" + } +} + if ($Lang.Count -eq 0) { Write-Error "No languages available"; exit 1 } # -------------------------------------------------------------------------- @@ -71,7 +113,7 @@ if (-not $SkipBuild) { Pop-Location } - if ("ts" -in $Lang) { + if ("ts" -in $Lang -or "com" -in $Lang) { Push-Location (Join-Path $root "bindings\js") npm install --quiet 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { Write-Error "npm install failed"; exit 1 } @@ -123,12 +165,40 @@ function Generate($lang, $outDir) { } } -foreach ($l in $Lang) { +foreach ($l in @($Lang | Where-Object { $_ -in @("py", "ts") })) { Write-Host "`n--- Generate ($l) ---" -ForegroundColor Yellow $outDir = if ($l -eq "py") { $pyBindingsDir } else { Join-Path $e2eDir $l } Generate $l $outDir } +if ("com" -in $Lang) { + Write-Host "`n--- Generate (Classic COM) ---" -ForegroundColor Yellow + $runtimeImport = "../../../../bindings/js/dist/index.js" + + & cargo run -p dynwinrt-codegen --release --quiet -- generate ` + --winmd $win32Winmd ` + --namespace Windows.Win32.UI.Shell ` + --class-name "ITaskbarList3,IDataTransferManagerInterop,IShellLinkW" ` + --output $comShellDir ` + --import-name $runtimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM Shell generation failed"; exit 1 } + + & cargo run -p dynwinrt-codegen --release --quiet -- generate ` + --winmd $win32Winmd ` + --namespace Windows.Win32.System.WinRT ` + --class-name ISystemMediaTransportControlsInterop ` + --output $comInteropDir ` + --import-name $runtimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM interop generation failed"; exit 1 } + + & cargo run -p dynwinrt-codegen --release --quiet -- generate ` + --namespace Windows.Media ` + --class-name SystemMediaTransportControls ` + --output $comSmtcDir ` + --import-name $runtimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "SMTC WinRT generation failed"; exit 1 } +} + # -------------------------------------------------------------------------- # Run language-specific runners # -------------------------------------------------------------------------- @@ -174,6 +244,35 @@ if ("ts" -in $Lang) { if (Test-Path $tsResult) { $allResults += (Get-Content $tsResult -Raw | ConvertFrom-Json) } } +if ("com" -in $Lang) { + Write-Host "`n--- Classic COM E2E ---" -ForegroundColor Yellow + $comRunners = @( + "pointer-reject-object.mjs", + "taskbarlist.mjs", + "electron-hwnd-buffer.mjs", + "shelllink-buffer.mjs", + "dtm.mjs", + "smtc.mjs" + ) + $comPassed = 0 + $comFailed = 0 + foreach ($runner in $comRunners) { + Write-Host " $runner" + & node (Join-Path $runnersDir "com\$runner") + if ($LASTEXITCODE -eq 0) { + $comPassed++ + } else { + $comFailed++ + } + } + if ($comFailed -eq 0) { $totalPass++ } else { $totalFail++ } + $allResults += [pscustomobject]@{ + language = "com" + passed = $comPassed + total = $comRunners.Count + } +} + # -------------------------------------------------------------------------- # Summary # -------------------------------------------------------------------------- diff --git a/bindings/js/e2e/dtm.mjs b/tests/runners/com/dtm.mjs similarity index 90% rename from bindings/js/e2e/dtm.mjs rename to tests/runners/com/dtm.mjs index d4e1f7bb..3c216e05 100644 --- a/bindings/js/e2e/dtm.mjs +++ b/tests/runners/com/dtm.mjs @@ -4,10 +4,10 @@ // E2E: real Node.js proof that IDataTransferManagerInterop returns a live // WinRT object through the HWND interop pattern: // IDataTransferManagerInterop::GetForWindow(HWND, REFIID, void**) -// Run: node bindings/js/e2e/dtm.mjs +// Run: .\tests\e2e_test.ps1 -SkipBuild -Lang com -import { DynCom, DynComMethodSig, WinGuid } from '../dist/index.js'; -import { IDataTransferManagerInterop } from './IDataTransferManagerInterop.js'; +import { DynCom, DynComMethodSig, WinGuid } from '../../../bindings/js/dist/index.js'; +import { IDataTransferManagerInterop } from '../../e2e_generated/com/shell/IDataTransferManagerInterop.js'; import { acquireHwndBigInt } from './hwnd.mjs'; function fail(msg) { diff --git a/bindings/js/e2e/electron-hwnd-buffer.mjs b/tests/runners/com/electron-hwnd-buffer.mjs similarity index 92% rename from bindings/js/e2e/electron-hwnd-buffer.mjs rename to tests/runners/com/electron-hwnd-buffer.mjs index a7af3767..d7f78abb 100644 --- a/bindings/js/e2e/electron-hwnd-buffer.mjs +++ b/tests/runners/com/electron-hwnd-buffer.mjs @@ -6,8 +6,8 @@ // bits out of that Buffer and pass the numeric handle value, not the Buffer // itself, because DynCom.pointer(Buffer) passes the Buffer's own address. -import { ITaskbarList3 } from './ITaskbarList3.js'; -import { TBPFLAG } from './TBPFLAG.js'; +import { ITaskbarList3 } from '../../e2e_generated/com/shell/ITaskbarList3.js'; +import { TBPFLAG } from '../../e2e_generated/com/shell/TBPFLAG.js'; import { acquireHwndBigInt } from './hwnd.mjs'; function fail(msg) { diff --git a/bindings/js/e2e/hwnd.mjs b/tests/runners/com/hwnd.mjs similarity index 93% rename from bindings/js/e2e/hwnd.mjs rename to tests/runners/com/hwnd.mjs index 52ffd3f1..37ae9512 100644 --- a/bindings/js/e2e/hwnd.mjs +++ b/tests/runners/com/hwnd.mjs @@ -11,7 +11,7 @@ // napi `createTestHwnd()` export, which creates a hidden `WS_POPUP` // window in the Node process using the pre-registered `STATIC` class. -import { DynCom, roInitialize } from '../dist/index.js'; +import { DynCom, roInitialize } from '../../../bindings/js/dist/index.js'; roInitialize(1); diff --git a/bindings/js/e2e/pointer-reject-object.mjs b/tests/runners/com/pointer-reject-object.mjs similarity index 91% rename from bindings/js/e2e/pointer-reject-object.mjs rename to tests/runners/com/pointer-reject-object.mjs index 70a7c5d2..eb792794 100644 --- a/bindings/js/e2e/pointer-reject-object.mjs +++ b/tests/runners/com/pointer-reject-object.mjs @@ -2,7 +2,7 @@ // DynWinRtValue inputs. Borrowing an owned COM object's raw pointer here would // make it indistinguishable from an owned raw pointer to adoptComPointer(), // which can double-release the original wrapper's COM object. -import { DynCom, WinGuid } from '../dist/index.js'; +import { DynCom, WinGuid } from '../../../bindings/js/dist/index.js'; // iidPointer() returns a DynWinRtValue — a representative value input. const someValue = DynCom.iidPointer(WinGuid.parse('a5caee9b-8708-49d1-8d36-67d25a8da00c')); diff --git a/bindings/js/e2e/shelllink-buffer.mjs b/tests/runners/com/shelllink-buffer.mjs similarity index 78% rename from bindings/js/e2e/shelllink-buffer.mjs rename to tests/runners/com/shelllink-buffer.mjs index c6f5595f..2d2b1c65 100644 --- a/bindings/js/e2e/shelllink-buffer.mjs +++ b/tests/runners/com/shelllink-buffer.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; -import { DynCom } from '../dist/index.js'; -import { IShellLinkW, IID_IShellLinkW } from './IShellLinkW.js'; -import { SHOW_WINDOW_CMD } from './SHOW_WINDOW_CMD.js'; +import { DynCom } from '../../../bindings/js/dist/index.js'; +import { IShellLinkW, IID_IShellLinkW } from '../../e2e_generated/com/shell/IShellLinkW.js'; +import { SHOW_WINDOW_CMD } from '../../e2e_generated/com/shell/SHOW_WINDOW_CMD.js'; const CLSID_SHELL_LINK = '00021401-0000-0000-c000-000000000046'; DynCom.initialize(1); @@ -16,7 +16,7 @@ const link = IShellLinkW._fromNative( const expectedPath = 'C:\\Windows\\explorer.exe'; link.setPath(wide(expectedPath)); -assert.equal(link.getPath(260, 0).toLowerCase(), expectedPath.toLowerCase()); +assert.equal(link.getPath(260, 0n, 0).toLowerCase(), expectedPath.toLowerCase()); const pidl = link.getIDList(); assert.equal(pidl.isNull(), false); pidl.release(); diff --git a/bindings/js/e2e/smtc.mjs b/tests/runners/com/smtc.mjs similarity index 75% rename from bindings/js/e2e/smtc.mjs rename to tests/runners/com/smtc.mjs index f4606be1..148aa896 100644 --- a/bindings/js/e2e/smtc.mjs +++ b/tests/runners/com/smtc.mjs @@ -13,42 +13,18 @@ // real SMTC member (isPlayEnabled) to prove the returned object is a live, // usable SystemMediaTransportControls, not just a valid IInspectable pointer. // -// Run: node bindings/js/e2e/smtc.mjs +// Run: .\tests\e2e_test.ps1 -SkipBuild -Lang com -import { existsSync } from 'node:fs'; -import { fileURLToPath } from 'node:url'; -import { dirname, resolve } from 'node:path'; -import { DynCom, DynComMethodSig, WinGuid } from '../dist/index.js'; +import { DynCom, DynComMethodSig, WinGuid } from '../../../bindings/js/dist/index.js'; // Classic-COM interop wrapper: gets the SMTC pointer from an HWND. -import { ISystemMediaTransportControlsInterop } from './ISystemMediaTransportControlsInterop.js'; +import { ISystemMediaTransportControlsInterop } from '../../e2e_generated/com/interop/ISystemMediaTransportControlsInterop.js'; import { acquireHwndBigInt } from './hwnd.mjs'; -// The SMTC full-WinRT projection under ./smtc-projected/ is a bulky generated -// fixture and is intentionally gitignored. On a clean checkout it must be -// regenerated before this test can run — otherwise a static import below would -// fail with an opaque module-not-found error. Fail early with a helpful -// message that spells out the exact regeneration command. -const __dirname_smtc = dirname(fileURLToPath(import.meta.url)); -const SMTC_FIXTURE = resolve( - __dirname_smtc, - 'smtc-projected/SystemMediaTransportControls.js' -); -if (!existsSync(SMTC_FIXTURE)) { - console.error(`[e2e] FAIL: SMTC projection fixture not found: ${SMTC_FIXTURE}`); - console.error(`[e2e] This fixture is gitignored — regenerate it with:`); - console.error(` cargo run -p dynwinrt-codegen -- generate \\`); - console.error(` --namespace Windows.Media \\`); - console.error(` --class-name SystemMediaTransportControls \\`); - console.error(` --output bindings/js/e2e/smtc-projected \\`); - console.error(` --import-name ../../dist/index.js`); - process.exit(1); -} -// Full WinRT natural projection: exercises real SMTC members via the same -// underlying COM pointer. Both wrappers are generated by dynwinrt-codegen. +// Full WinRT natural projection generated by the unified E2E orchestrator. const { SystemMediaTransportControls: SmtcProjected } = - await import('./smtc-projected/SystemMediaTransportControls.js'); + await import('../../e2e_generated/com/smtc/SystemMediaTransportControls.js'); const { MediaPlaybackStatus } = - await import('./smtc-projected/MediaPlaybackStatus.js'); + await import('../../e2e_generated/com/smtc/MediaPlaybackStatus.js'); function fail(msg) { console.error(`[e2e] FAIL: ${msg}`); diff --git a/bindings/js/e2e/taskbarlist.mjs b/tests/runners/com/taskbarlist.mjs similarity index 93% rename from bindings/js/e2e/taskbarlist.mjs rename to tests/runners/com/taskbarlist.mjs index 9fb330a9..d5459b53 100644 --- a/bindings/js/e2e/taskbarlist.mjs +++ b/tests/runners/com/taskbarlist.mjs @@ -4,10 +4,10 @@ // Phase 2 E2E: real Node.js proof that the generated natural ITaskbarList3 // wrapper drives live Windows classic COM (ITaskbarList3) via CoCreateInstance. // -// Run: node bindings/js/e2e/taskbarlist.mjs +// Run: .\tests\e2e_test.ps1 -SkipBuild -Lang com -import { ITaskbarList3 } from './ITaskbarList3.js'; -import { TBPFLAG } from './TBPFLAG.js'; +import { ITaskbarList3 } from '../../e2e_generated/com/shell/ITaskbarList3.js'; +import { TBPFLAG } from '../../e2e_generated/com/shell/TBPFLAG.js'; import { acquireHwndBigInt } from './hwnd.mjs'; function fail(msg) { diff --git a/tools/dynwinrt-codegen/src/codegen/com/render.rs b/tools/dynwinrt-codegen/src/codegen/com/render.rs index 698a4f95..d24a6dcb 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/render.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/render.rs @@ -20,7 +20,9 @@ //! on failure via the runtime). //! - Per-enum sibling files for each enum referenced by any method parameter. -use crate::com_metadata::{ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; +use crate::com_metadata::{ + ComEnumMeta, ComEnumValue, ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta, +}; use crate::types::TypeMeta; #[cfg(test)] @@ -30,10 +32,10 @@ use super::projection::method_is_interop_shape; use super::projection::{InteropInfo, InteropMethod, detect_interop}; use super::type_mapping::{ HandleAliasKind, MethodResult, StringEncoding, collect_handle_aliases, dts_params_for_method, - dts_return_type, enum_import_names, has_string_buffer_method, is_cotaskmem_owned, is_hresult, - is_optional_find_data_out_after_string_count, method_results, string_buffer_pattern, - ts_type_expr_dts, ts_type_expr_js, unwrap_return_js, uses_winrt_bridge_value, validate_com_abi, - wrap_arg_js, + dts_return_type, enum_import_names, has_string_buffer_method, is_bstr, is_cotaskmem_owned, + is_hresult, is_optional_find_data_out_after_string_count, is_sys_free_string_owned, + method_results, string_buffer_param_is_optional, string_buffer_pattern, ts_type_expr_dts, + ts_type_expr_js, unwrap_return_js, uses_winrt_bridge_value, validate_com_abi, wrap_arg_js, }; #[cfg(test)] use super::type_mapping::{handle_alias_kind, handle_type_name}; @@ -81,11 +83,9 @@ pub fn generate_com_interface_files( // Per-enum sibling files (referenced by parameter types). let mut extra_files: Vec<(String, String)> = Vec::new(); for en in &meta.referenced_enums { - if let TypeMeta::Enum { name, .. } = en { - let (enum_js, enum_dts) = render_enum_files(en); - extra_files.push((format!("{}.js", name), enum_js)); - extra_files.push((format!("{}.d.ts", name), enum_dts)); - } + let (enum_js, enum_dts) = render_enum_files(en); + extra_files.push((format!("{}.js", en.name), enum_js)); + extra_files.push((format!("{}.d.ts", en.name), enum_dts)); } extra_files.sort_by(|a, b| a.0.cmp(&b.0)); @@ -254,7 +254,9 @@ fn unwrap_method_result_js( result: MethodResult<'_>, expression: &str, ) -> String { - if is_cotaskmem_owned(method, result) + if is_sys_free_string_owned(method, result) { + format!("DynCom.takeBstr({expression})") + } else if is_cotaskmem_owned(method, result) && !matches!( result.typ, TypeMeta::Struct { namespace, name, .. } @@ -274,7 +276,9 @@ fn validate_untyped_outputs(meta: &ComInterfaceMeta) -> Result<(), String> { let is_untyped = param.direction == ParamDirection::Out && param.typ == TypeMeta::Object; let is_owned = method.owned_outputs.iter().any(|owned| { - owned.param_index == param_index && owned.free_with.contains("CoTaskMemFree") + owned.param_index == param_index + && (owned.free_with.contains("CoTaskMemFree") + || (owned.free_with.contains("SysFreeString") && is_bstr(¶m.typ))) }); if is_untyped && !is_owned && method_is_interop_shape(method).is_none() { return Err(format!( @@ -376,10 +380,10 @@ fn emit_method_js(out: &mut String, m: &MethodMeta, iface_var: &str) { .map(|(surface_i, (idx, p))| { let name = js_param_name(&p.name, surface_i); if let Some((_, count_idx, _)) = string_buffer_pattern(m) { - if *idx == count_idx { + if *idx == count_idx && string_buffer_param_is_optional(m, *idx) { return format!("{name} = 260"); } - if *idx > count_idx { + if *idx > count_idx && string_buffer_param_is_optional(m, *idx) { return format!("{name} = 0"); } } @@ -755,10 +759,9 @@ fn has_dynamic_iid_method(meta: &ComInterfaceMeta) -> bool { fn has_owned_pointer_output(meta: &ComInterfaceMeta) -> bool { meta.interface.methods.iter().any(|method| { - method - .owned_outputs - .iter() - .any(|owned| owned.free_with.contains("CoTaskMemFree")) + method.owned_outputs.iter().any(|owned| { + owned.free_with.contains("CoTaskMemFree") || owned.free_with.contains("SysFreeString") + }) }) } @@ -766,12 +769,8 @@ fn has_owned_pointer_output(meta: &ComInterfaceMeta) -> bool { // Enum sibling files // --------------------------------------------------------------------------- -fn render_enum_files(en: &TypeMeta) -> (String, String) { - let (name, members) = match en { - TypeMeta::Enum { name, members, .. } => (name.as_str(), members), - _ => unreachable!(), - }; - +fn render_enum_files(en: &ComEnumMeta) -> (String, String) { + let name = en.name.as_str(); // .js: a frozen object. let mut js = String::new(); js.push_str("// Generated by dynwinrt-codegen — do not edit\n"); @@ -779,8 +778,12 @@ fn render_enum_files(en: &TypeMeta) -> (String, String) { "export const {name} = Object.freeze({{\n", name = name )); - for m in members { - js.push_str(&format!(" {}: {},\n", m.name, m.value)); + for member in &en.members { + js.push_str(&format!( + " {}: {},\n", + member.name, + render_enum_value(&member.value, &en.underlying) + )); } js.push_str("});\n"); @@ -795,14 +798,30 @@ fn render_enum_files(en: &TypeMeta) -> (String, String) { name = name )); dts.push_str(&format!("export declare const {name}: {{\n", name = name)); - for m in members { - dts.push_str(&format!(" readonly {}: {};\n", m.name, m.value)); + for member in &en.members { + dts.push_str(&format!( + " readonly {}: {};\n", + member.name, + render_enum_value(&member.value, &en.underlying) + )); } dts.push_str("};\n"); (js, dts) } +fn render_enum_value(value: &ComEnumValue, underlying: &TypeMeta) -> String { + let suffix = if matches!(underlying, TypeMeta::I64 | TypeMeta::U64) { + "n" + } else { + "" + }; + match value { + ComEnumValue::Signed(value) => format!("{value}{suffix}"), + ComEnumValue::Unsigned(value) => format!("{value}{suffix}"), + } +} + // --------------------------------------------------------------------------- // Unit tests (fast, no winmd — pure logic) // --------------------------------------------------------------------------- @@ -1682,7 +1701,13 @@ mod tests { ..Default::default() }; let mut com = plain_iface_with_method(method); - com.referenced_enums.push(kind); + com.referenced_enums.push(ComEnumMeta { + namespace: "Windows.Win32.Example".into(), + name: "THING_KIND".into(), + underlying: TypeMeta::I32, + members: Vec::new(), + is_flags: false, + }); let output = generate_com_interface_files(&com, "").unwrap(); assert!( @@ -1698,6 +1723,37 @@ mod tests { ); } + #[test] + fn unsigned_enum_literals_preserve_u32_and_u64_values() { + let u32_enum = ComEnumMeta { + namespace: "Windows.Win32.Example".into(), + name: "U32_FLAGS".into(), + underlying: TypeMeta::U32, + members: vec![crate::com_metadata::ComEnumMember { + name: "HIGH_BIT".into(), + value: ComEnumValue::Unsigned(2_147_483_648), + }], + is_flags: true, + }; + let u64_enum = ComEnumMeta { + namespace: "Windows.Win32.Example".into(), + name: "U64_FLAGS".into(), + underlying: TypeMeta::U64, + members: vec![crate::com_metadata::ComEnumMember { + name: "HIGH_BIT".into(), + value: ComEnumValue::Unsigned(9_223_372_036_854_775_808), + }], + is_flags: true, + }; + + let (u32_js, u32_dts) = render_enum_files(&u32_enum); + assert!(u32_js.contains("HIGH_BIT: 2147483648")); + assert!(u32_dts.contains("readonly HIGH_BIT: 2147483648;")); + let (u64_js, u64_dts) = render_enum_files(&u64_enum); + assert!(u64_js.contains("HIGH_BIT: 9223372036854775808n")); + assert!(u64_dts.contains("readonly HIGH_BIT: 9223372036854775808n;")); + } + #[test] fn in_out_parameter_is_both_argument_and_result() { let method = MethodMeta { @@ -1838,6 +1894,28 @@ mod tests { assert!(dts.contains("getDisplayName(): string;")); } + #[test] + fn untyped_sysfree_output_fails_closed() { + let method = MethodMeta { + name: "GetAllFileTypes".into(), + vtable_index: 4, + params: vec![ParamMeta { + name: "types".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 0, + free_with: "SysFreeString".into(), + }], + ..Default::default() + }; + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("BSTR**-style untyped outputs must fail closed"); + assert!(error.contains("untyped pointer output has no ownership projection")); + } + #[test] fn string_buffer_preserves_additional_outputs() { let method = MethodMeta { diff --git a/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs b/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs index 543f049e..9cddc04d 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs @@ -3,7 +3,9 @@ use std::collections::BTreeMap; -use crate::com_metadata::{ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; +use crate::com_metadata::{ + ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta, is_native_isize, is_native_usize, +}; use crate::types::TypeMeta; use super::naming::js_param_name; @@ -23,9 +25,20 @@ pub(super) enum HandleAliasKind { pub(super) fn validate_com_abi(meta: &ComInterfaceMeta) -> Result<(), String> { for method in &meta.interface.methods { for param in &method.params { + if let ParamDirection::UnsupportedNativeArray { count_param_index } = param.direction { + let count = count_param_index + .map(|index| format!("parameter index {index}")) + .unwrap_or_else(|| "metadata-defined size".into()); + return Err(format!( + "{}.{}: caller-sized native buffers are not supported (`{}` uses {count})", + meta.interface.name, method.name, param.name + )); + } if matches!(param.typ, TypeMeta::Struct { .. }) && !is_win32_bool(¶m.typ) && !is_hresult(¶m.typ) + && !is_native_isize(¶m.typ) + && !is_native_usize(¶m.typ) && handle_type_name(¶m.typ).is_none() { return Err(format!( @@ -63,7 +76,9 @@ pub(super) fn validate_com_abi(meta: &ComInterfaceMeta) -> Result<(), String> { } fn supports_in_out(t: &TypeMeta) -> bool { - is_win32_bool(t) + is_native_isize(t) + || is_native_usize(t) + || is_win32_bool(t) || is_hresult(t) || handle_type_name(t).is_some() || matches!( @@ -89,6 +104,12 @@ fn supports_direct_return(t: &TypeMeta) -> bool { } pub(super) fn unwrap_return_js(t: &TypeMeta, expr: &str) -> String { + if is_native_isize(t) { + return format!("DynCom.toIsizeBigint({expr})"); + } + if is_native_usize(t) { + return format!("DynCom.toUsizeBigint({expr})"); + } match string_buffer_encoding(t) { Some(StringEncoding::Wide) => { return format!("DynCom.takeCoTaskMemWideString({expr})"); @@ -184,7 +205,7 @@ pub(super) fn dts_return_type(m: &MethodMeta) -> String { } fn ts_result_type(method: &MethodMeta, result: MethodResult<'_>) -> String { - if string_buffer_encoding(result.typ).is_some() { + if is_sys_free_string_owned(method, result) || string_buffer_encoding(result.typ).is_some() { "string".into() } else if is_cotaskmem_owned(method, result) { "DynWinRtValue".into() @@ -203,6 +224,16 @@ pub(super) fn is_cotaskmem_owned(method: &MethodMeta, result: MethodResult<'_>) .any(|owned| owned.param_index == param_index && owned.free_with.contains("CoTaskMemFree")) } +pub(super) fn is_sys_free_string_owned(method: &MethodMeta, result: MethodResult<'_>) -> bool { + let Some(param_index) = result.param_index else { + return false; + }; + is_bstr(result.typ) + && method.owned_outputs.iter().any(|owned| { + owned.param_index == param_index && owned.free_with.contains("SysFreeString") + }) +} + pub(super) fn dts_params_for_method(m: &MethodMeta) -> Vec { let string_buffer = string_buffer_pattern(m); m.params @@ -213,7 +244,7 @@ pub(super) fn dts_params_for_method(m: &MethodMeta) -> Vec { .map(|(surface_index, (param_index, param))| { let mut name = js_param_name(¶m.name, surface_index); if let Some((_, count_index, _)) = string_buffer { - if param_index >= count_index { + if param_index >= count_index && string_buffer_param_is_optional(m, param_index) { name.push('?'); } } @@ -240,10 +271,7 @@ pub(super) fn collect_handle_aliases(meta: &ComInterfaceMeta) -> Vec<(String, Ha pub(super) fn enum_import_names(meta: &ComInterfaceMeta) -> Vec { meta.referenced_enums .iter() - .filter_map(|typ| match typ { - TypeMeta::Enum { name, .. } => Some(name.clone()), - _ => None, - }) + .map(|enum_meta| enum_meta.name.clone()) .collect() } @@ -289,6 +317,27 @@ pub(super) fn string_buffer_pattern(method: &MethodMeta) -> Option<(usize, usize None } +pub(super) fn string_buffer_param_is_optional(method: &MethodMeta, param_index: usize) -> bool { + let Some((_, count_index, _)) = string_buffer_pattern(method) else { + return false; + }; + let Some(param) = method.params.get(param_index) else { + return false; + }; + let is_optional_shape = param_index == count_index + || (param_index > count_index && is_optional_find_data_out_after_string_count(param)); + if !is_optional_shape { + return false; + } + method + .params + .iter() + .enumerate() + .skip(param_index + 1) + .filter(|(_, param)| param.direction.is_input()) + .all(|(_, param)| is_optional_find_data_out_after_string_count(param)) +} + fn string_buffer_encoding(t: &TypeMeta) -> Option { match t { TypeMeta::Struct { @@ -306,7 +355,7 @@ fn string_buffer_encoding(t: &TypeMeta) -> Option { } pub(super) fn is_optional_find_data_out_after_string_count(param: &ParamMeta) -> bool { - if param.direction != ParamDirection::Out { + if !matches!(param.direction, ParamDirection::In | ParamDirection::Out) { return false; } let name = param.name.to_ascii_lowercase(); @@ -320,6 +369,9 @@ pub(super) fn is_optional_find_data_out_after_string_count(param: &ParamMeta) -> } pub(super) fn ts_type_expr_dts(t: &TypeMeta) -> String { + if is_native_isize(t) || is_native_usize(t) { + return "bigint".into(); + } if is_win32_bool(t) { return "boolean".into(); } @@ -350,6 +402,12 @@ pub(super) fn ts_type_expr_dts(t: &TypeMeta) -> String { } pub(super) fn ts_type_expr_js(t: &TypeMeta) -> String { + if is_native_isize(t) { + return "DynCom.isizeType()".into(); + } + if is_native_usize(t) { + return "DynCom.usizeType()".into(); + } if is_win32_bool(t) || is_hresult(t) { return "DynCom.i32Type()".into(); } @@ -381,6 +439,12 @@ pub(super) fn ts_type_expr_js(t: &TypeMeta) -> String { } pub(super) fn wrap_arg_js(t: &TypeMeta, var: &str) -> String { + if is_native_isize(t) { + return format!("DynCom.isize(BigInt({var}))"); + } + if is_native_usize(t) { + return format!("DynCom.usize(BigInt({var}))"); + } if is_win32_bool(t) { return format!("DynCom.i32({var} ? 1 : 0)"); } @@ -503,3 +567,11 @@ pub(super) fn is_win32_bool(t: &TypeMeta) -> bool { if namespace == "Windows.Win32.Foundation" && name == "BOOL" ) } + +pub(super) fn is_bstr(t: &TypeMeta) -> bool { + matches!( + t, + TypeMeta::Struct { namespace, name, .. } + if namespace == "Windows.Win32.Foundation" && name == "BSTR" + ) +} diff --git a/tools/dynwinrt-codegen/src/com_metadata.rs b/tools/dynwinrt-codegen/src/com_metadata.rs index c7c84b3d..c0304638 100644 --- a/tools/dynwinrt-codegen/src/com_metadata.rs +++ b/tools/dynwinrt-codegen/src/com_metadata.rs @@ -14,17 +14,25 @@ pub enum ParamDirection { InOut, OutFill, OutStringBuffer { count_param_index: usize }, + UnsupportedNativeArray { count_param_index: Option }, } impl ParamDirection { pub fn is_input(&self) -> bool { - matches!(self, Self::In | Self::InOut) + matches!( + self, + Self::In | Self::InOut | Self::UnsupportedNativeArray { .. } + ) } pub fn is_output(&self) -> bool { matches!( self, - Self::Out | Self::InOut | Self::OutFill | Self::OutStringBuffer { .. } + Self::Out + | Self::InOut + | Self::OutFill + | Self::OutStringBuffer { .. } + | Self::UnsupportedNativeArray { .. } ) } } @@ -73,7 +81,28 @@ pub struct ComInterfaceMeta { pub coclass_clsid: Option, pub coclass_name: Option, pub own_methods_start: usize, - pub referenced_enums: Vec, + pub referenced_enums: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ComEnumValue { + Signed(i64), + Unsigned(u64), +} + +#[derive(Debug, Clone)] +pub struct ComEnumMember { + pub name: String, + pub value: ComEnumValue, +} + +#[derive(Debug, Clone)] +pub struct ComEnumMeta { + pub namespace: String, + pub name: String, + pub underlying: TypeMeta, + pub members: Vec, + pub is_flags: bool, } pub fn parse_com_interface( @@ -85,6 +114,34 @@ pub fn parse_com_interface( parse_com_interface_from_index(&index, namespace, name) } +pub fn parse_com_enum(winmd_paths: &str, namespace: &str, name: &str) -> Option { + let index = crate::meta::load_index(winmd_paths)?; + let def = index.get(namespace, name).next()?; + parse_com_enum_def(&def) +} + +pub fn first_classic_com_interface_in_namespace( + winmd_paths: &str, + namespace: &str, +) -> Option { + let index = crate::meta::load_index(winmd_paths)?; + let names = index + .all() + .filter(|def| { + def.namespace() == namespace + && def + .flags() + .contains(windows_metadata::TypeAttributes::Interface) + }) + .map(|def| def.name().to_string()) + .collect::>(); + names.into_iter().find(|name| { + parse_com_interface_from_index(&index, namespace, name).is_some_and(|interface| { + interface.is_iunknown_rooted || interface.interface.name.ends_with("Interop") + }) + }) +} + fn parse_com_interface_from_index( index: &reader::Index, namespace: &str, @@ -171,7 +228,7 @@ fn parse_com_interface_from_index( deprecated: None, }; let (coclass_name, coclass_clsid) = find_coclass(index, namespace, name); - let referenced_enums = collect_referenced_enums(&interface); + let referenced_enums = collect_referenced_enums(index, &interface); Some(ComInterfaceMeta { interface, @@ -216,10 +273,16 @@ fn parse_methods( .zip(signature.types.iter()) .enumerate() { - let direction = classify_direction( + let mut direction = classify_direction( param.flags(), matches!(typ, windows_metadata::Type::Array(_)), ); + let mapped_type = map_parameter_type(typ, &direction, index); + if let Some(count_param_index) = native_array_count_param(¶m) { + if direction.is_output() && !is_string_buffer(&mapped_type) { + direction = ParamDirection::UnsupportedNativeArray { count_param_index }; + } + } let free_with = param .find_attribute("FreeWithAttribute") @@ -240,7 +303,7 @@ fn parse_methods( } params.push(ParamMeta { name: param.name().to_string(), - typ: map_parameter_type(typ, &direction, index), + typ: mapped_type, direction, }); } @@ -260,13 +323,25 @@ fn parse_methods( } fn known_free_with(typ: &windows_metadata::Type, direction: &ParamDirection) -> Option { - // Windows.Win32.winmd omits FreeWith on IShellLink::GetIDList. let (windows_metadata::Type::PtrMut(inner, depth) | windows_metadata::Type::PtrConst(inner, depth)) = typ else { return None; }; - if !matches!(direction, ParamDirection::Out | ParamDirection::InOut) || *depth < 2 { + if !matches!(direction, ParamDirection::Out | ParamDirection::InOut) { + return None; + } + if *depth == 1 + && matches!( + inner.as_ref(), + windows_metadata::Type::Name(name) + if name.namespace == "Windows.Win32.Foundation" && name.name == "BSTR" + ) + { + return Some("SysFreeString".into()); + } + // Windows.Win32.winmd omits FreeWith on IShellLink::GetIDList. + if *depth < 2 { return None; } match inner.as_ref() { @@ -289,7 +364,7 @@ fn map_parameter_type( match typ { Type::PtrMut(inner, depth) | Type::PtrConst(inner, depth) => { if matches!(direction, ParamDirection::Out | ParamDirection::InOut) && *depth == 1 { - crate::meta::map_winmd_type_with_generics(inner, index, &[]) + map_com_type(inner, index) } else { TypeMeta::Object } @@ -297,14 +372,10 @@ fn map_parameter_type( Type::ConstRef(inner) if matches!(direction, ParamDirection::Out | ParamDirection::InOut) => { - crate::meta::map_winmd_type_with_generics(inner, index, &[]) + map_com_type(inner, index) } - Type::ConstRef(_) | Type::ISize | Type::USize => match typ { - Type::ISize => TypeMeta::I64, - Type::USize => TypeMeta::U64, - _ => TypeMeta::Object, - }, - _ => crate::meta::map_winmd_type_with_generics(typ, index, &[]), + Type::ConstRef(_) => TypeMeta::Object, + _ => map_com_type(typ, index), } } @@ -313,12 +384,141 @@ fn map_return_type(typ: &windows_metadata::Type, index: &reader::Index) -> TypeM match typ { Type::PtrMut(_, _) | Type::PtrConst(_, _) | Type::ConstRef(_) => TypeMeta::Object, - Type::ISize => TypeMeta::I64, - Type::USize => TypeMeta::U64, + _ => map_com_type(typ, index), + } +} + +fn map_com_type(typ: &windows_metadata::Type, index: &reader::Index) -> TypeMeta { + match typ { + windows_metadata::Type::ISize => native_isize_type(), + windows_metadata::Type::USize => native_usize_type(), + windows_metadata::Type::Name(name) => index + .get(&name.namespace, &name.name) + .next() + .and_then(|def| parse_com_enum_def(&def)) + .map(|enum_meta| enum_meta.as_type_meta()) + .unwrap_or_else(|| crate::meta::map_winmd_type_with_generics(typ, index, &[])), _ => crate::meta::map_winmd_type_with_generics(typ, index, &[]), } } +impl ComEnumMeta { + fn as_type_meta(&self) -> TypeMeta { + TypeMeta::Enum { + namespace: self.namespace.clone(), + name: self.name.clone(), + underlying: Box::new(self.underlying.clone()), + members: Vec::new(), + is_flags: self.is_flags, + doc: None, + deprecated: None, + } + } +} + +fn parse_com_enum_def(def: &reader::TypeDef) -> Option { + let mut fields = def.fields(); + let underlying = fields + .find(|field| field.name() == "value__") + .and_then(|field| map_com_enum_underlying(&field.ty()))?; + let members = def + .fields() + .filter(|field| field.name() != "value__") + .filter_map(|field| { + let value = match field.constant()?.value() { + windows_metadata::Value::I8(value) => ComEnumValue::Signed(i64::from(value)), + windows_metadata::Value::U8(value) => ComEnumValue::Unsigned(u64::from(value)), + windows_metadata::Value::I16(value) => ComEnumValue::Signed(i64::from(value)), + windows_metadata::Value::U16(value) => ComEnumValue::Unsigned(u64::from(value)), + windows_metadata::Value::I32(value) => ComEnumValue::Signed(i64::from(value)), + windows_metadata::Value::U32(value) => ComEnumValue::Unsigned(u64::from(value)), + windows_metadata::Value::I64(value) => ComEnumValue::Signed(value), + windows_metadata::Value::U64(value) => ComEnumValue::Unsigned(value), + _ => return None, + }; + Some(ComEnumMember { + name: field.name().to_string(), + value, + }) + }) + .collect(); + Some(ComEnumMeta { + namespace: def.namespace().to_string(), + name: def.name().to_string(), + underlying, + members, + is_flags: def.has_attribute("FlagsAttribute"), + }) +} + +fn map_com_enum_underlying(typ: &windows_metadata::Type) -> Option { + match typ { + windows_metadata::Type::I8 => Some(TypeMeta::I8), + windows_metadata::Type::U8 => Some(TypeMeta::U8), + windows_metadata::Type::I16 => Some(TypeMeta::I16), + windows_metadata::Type::U16 => Some(TypeMeta::U16), + windows_metadata::Type::I32 => Some(TypeMeta::I32), + windows_metadata::Type::U32 => Some(TypeMeta::U32), + windows_metadata::Type::I64 => Some(TypeMeta::I64), + windows_metadata::Type::U64 => Some(TypeMeta::U64), + _ => None, + } +} + +pub fn native_isize_type() -> TypeMeta { + TypeMeta::Struct { + namespace: "System".into(), + name: "IntPtr".into(), + fields: Vec::new(), + } +} + +pub fn native_usize_type() -> TypeMeta { + TypeMeta::Struct { + namespace: "System".into(), + name: "UIntPtr".into(), + fields: Vec::new(), + } +} + +pub fn is_native_isize(typ: &TypeMeta) -> bool { + matches!( + typ, + TypeMeta::Struct { + namespace, + name, + .. + } if namespace == "System" && name == "IntPtr" + ) +} + +pub fn is_native_usize(typ: &TypeMeta) -> bool { + matches!( + typ, + TypeMeta::Struct { + namespace, + name, + .. + } if namespace == "System" && name == "UIntPtr" + ) +} + +fn native_array_count_param(param: &reader::MethodParam) -> Option> { + let attribute = param.find_attribute("NativeArrayInfoAttribute")?; + let count = attribute + .value() + .into_iter() + .find(|(name, _)| name == "CountParamIndex") + .and_then(|(_, value)| match value { + windows_metadata::Value::I16(value) if value >= 0 => Some(value as usize), + windows_metadata::Value::U16(value) => Some(value as usize), + windows_metadata::Value::I32(value) if value >= 0 => Some(value as usize), + windows_metadata::Value::U32(value) => usize::try_from(value).ok(), + _ => None, + }); + Some(count) +} + fn classify_direction(flags: windows_metadata::ParamAttributes, is_array: bool) -> ParamDirection { let is_in = flags.contains(windows_metadata::ParamAttributes::In); let is_out = flags.contains(windows_metadata::ParamAttributes::Out); @@ -364,7 +564,7 @@ fn find_coclass( (None, None) } -fn collect_referenced_enums(interface: &InterfaceMeta) -> Vec { +fn collect_referenced_enums(index: &reader::Index, interface: &InterfaceMeta) -> Vec { let mut names = HashSet::new(); let mut result = Vec::new(); for method in &interface.methods { @@ -374,9 +574,18 @@ fn collect_referenced_enums(interface: &InterfaceMeta) -> Vec { .map(|param| ¶m.typ) .chain(method.return_type.iter()) { - if let TypeMeta::Enum { name, .. } = typ { - if names.insert(name.clone()) { - result.push(typ.clone()); + if let TypeMeta::Enum { + namespace, name, .. + } = typ + { + let full_name = format!("{namespace}.{name}"); + if names.insert(full_name) + && let Some(enum_meta) = index + .get(namespace, name) + .next() + .and_then(|def| parse_com_enum_def(&def)) + { + result.push(enum_meta); } } } @@ -573,4 +782,16 @@ mod tests { Some("CoTaskMemFree") ); } + + #[test] + fn bstr_array_does_not_claim_scalar_sysfree_ownership() { + let typ = windows_metadata::Type::PtrMut( + Box::new(windows_metadata::Type::named( + "Windows.Win32.Foundation", + "BSTR", + )), + 2, + ); + assert_eq!(known_free_with(&typ, &ParamDirection::Out), None); + } } diff --git a/tools/dynwinrt-codegen/src/main.rs b/tools/dynwinrt-codegen/src/main.rs index aa035175..80e7f8c8 100644 --- a/tools/dynwinrt-codegen/src/main.rs +++ b/tools/dynwinrt-codegen/src/main.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fs; use std::path::Path; @@ -340,7 +340,16 @@ fn run() -> Result<(), String> { )); } - // Emit classic-COM interfaces (standalone; not wired into WinRT index/barrel). + if !com_interfaces.is_empty() && !classes.is_empty() { + return Err( + "Classic-COM and WinRT class generation cannot share one output package yet. \ + Run separate `generate` commands with separate output directories." + .into(), + ); + } + + // Emit classic-COM interfaces. Mixed WinRT/COM packages were + // rejected above; COM-only output is finalized below. if !com_interfaces.is_empty() { for com_iface in &com_interfaces { let out = @@ -374,6 +383,9 @@ fn run() -> Result<(), String> { // If we only had classic-COM interfaces requested, return early — // no WinRT index/barrel work to do. if classes.is_empty() { + if !dry_run { + write_com_js_barrel_and_manifest(output_dir)?; + } return Ok(()); } } @@ -517,6 +529,16 @@ fn run() -> Result<(), String> { let mut total_enums = 0usize; for ns in &namespaces { + if let Some(interface) = + com_metadata::first_classic_com_interface_in_namespace(&winmd, ns) + { + return Err(format!( + "classic-COM namespace projection is not supported because `{ns}` \ + contains `{interface}`. Use `--class-name {interface}` (or a \ + comma-separated class list) so each interface is validated by the \ + Classic-COM ABI pipeline." + )); + } let mut classes = meta::parse_namespace(&winmd, ns); let mut interfaces = meta::parse_interfaces(&winmd, ns); let mut enums = meta::parse_enums(&winmd, ns); @@ -999,6 +1021,7 @@ fn write_js_barrel_and_manifest(output_dir: &Path, index_content: &str) -> Resul if stale.exists() { let _ = fs::remove_file(&stale); } + // Remove the previous opt-in getter barrel name if it exists from older // generated output. `index.js` is now the getter barrel and // `index.proxy.js` is the explicit compatibility path. @@ -1048,6 +1071,84 @@ fn write_js_barrel_and_manifest(output_dir: &Path, index_content: &str) -> Resul Ok(()) } +fn write_com_js_barrel_and_manifest(output_dir: &Path) -> Result<(), String> { + let mut modules: BTreeMap> = BTreeMap::new(); + let entries = fs::read_dir(output_dir).map_err(|error| { + format!( + "Failed to read COM output directory {}: {error}", + output_dir.display() + ) + })?; + for entry in entries.flatten() { + let path = entry.path(); + let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else { + continue; + }; + let Some(module) = file_name.strip_suffix(".js") else { + continue; + }; + if module == "index" { + continue; + } + let content = fs::read_to_string(&path) + .map_err(|error| format!("Failed to read {}: {error}", path.display()))?; + let exports = collect_com_esm_exports(&content); + if !exports.is_empty() { + modules.insert(module.to_string(), exports); + } + } + + let mut index = String::from("// Generated by dynwinrt-codegen - do not edit\n"); + for (module, exports) in &modules { + index.push_str(&format!( + "export {{ {} }} from './{module}.js';\n", + exports.iter().cloned().collect::>().join(", ") + )); + } + fs::write(output_dir.join("index.js"), &index) + .map_err(|error| format!("Failed to write COM index.js: {error}"))?; + fs::write(output_dir.join("index.d.ts"), &index) + .map_err(|error| format!("Failed to write COM index.d.ts: {error}"))?; + + let mut package = String::from( + "{\n \"name\": \"@winapp/bindings\",\n \"type\": \"module\",\n \ + \"sideEffects\": false,\n \"main\": \"./index.js\",\n \ + \"types\": \"./index.d.ts\",\n \"exports\": {\n \".\": {\n \ + \"types\": \"./index.d.ts\",\n \"import\": \"./index.js\",\n \ + \"default\": \"./index.js\"\n }", + ); + for module in modules.keys() { + package.push_str(&format!( + ",\n \"./{module}\": {{\n \"types\": \"./{module}.d.ts\",\n \ + \"import\": \"./{module}.js\",\n \"default\": \"./{module}.js\"\n }}" + )); + } + package.push_str("\n }\n}\n"); + fs::write(output_dir.join("package.json"), package) + .map_err(|error| format!("Failed to write COM package.json: {error}"))?; + Ok(()) +} + +fn collect_com_esm_exports(content: &str) -> BTreeSet { + const PREFIXES: &[&str] = &["export const ", "export class ", "export function "]; + content + .lines() + .filter_map(|line| { + let line = line.trim_start(); + let rest = PREFIXES + .iter() + .find_map(|prefix| line.strip_prefix(prefix))?; + let name = rest + .chars() + .take_while(|character| { + character.is_ascii_alphanumeric() || *character == '_' || *character == '$' + }) + .collect::(); + (!name.is_empty()).then_some(name) + }) + .collect() +} + fn write_lifetime_module(output_dir: &Path) -> Result<(), String> { let js = "'use strict';\n\ let activeScope = null;\n\ diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs index e2da22ee..c9827435 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -14,6 +14,7 @@ use std::fs; use std::path::{Path, PathBuf}; +use std::process::Command; use dynwinrt_codegen::codegen::com; use dynwinrt_codegen::codegen::project::{get_import_name, set_import_name}; @@ -33,6 +34,17 @@ fn win32_available() -> bool { Path::new(&win32_winmd()).exists() } +#[test] +fn required_win32_metadata_is_present() { + if std::env::var("DYNWINRT_REQUIRE_WIN32_METADATA").as_deref() == Ok("1") { + assert!( + win32_available(), + "DYNWINRT_REQUIRE_WIN32_METADATA=1 but metadata is missing at {}", + win32_winmd() + ); + } +} + /// Resolve a `Windows.winmd` from the newest installed Windows SDK, matching /// the discovery logic the codegen itself uses. Returns `None` if no SDK is /// installed on this machine (the test that calls this should skip in that @@ -52,9 +64,12 @@ fn parse_itaskbarlist3_iid() { eprintln!("Skipping: Win32 winmd not available at {}", &win32_winmd()); return; } - let com_iface = - com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") - .expect("ITaskbarList3 must exist in Win32 metadata"); + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .expect("ITaskbarList3 must exist in Win32 metadata"); assert_eq!(com_iface.interface.name, "ITaskbarList3"); assert_eq!(com_iface.interface.namespace, "Windows.Win32.UI.Shell"); assert_eq!( @@ -72,9 +87,12 @@ fn parse_itaskbarlist3_vtable_slots() { eprintln!("Skipping: Win32 winmd not available"); return; } - let com_iface = - com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") - .expect("ITaskbarList3 must exist"); + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .expect("ITaskbarList3 must exist"); let by_name = |n: &str| -> usize { com_iface @@ -123,9 +141,12 @@ fn itaskbarlist3_is_iunknown_rooted() { eprintln!("Skipping: Win32 winmd not available"); return; } - let com_iface = - com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") - .unwrap(); + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); assert_eq!(com_iface.base_offset, 3); assert!(com_iface.is_iunknown_rooted); // Base chain should include ITaskbarList2, ITaskbarList (and stop at IUnknown) @@ -145,9 +166,12 @@ fn itaskbarlist3_clsid_resolution() { eprintln!("Skipping: Win32 winmd not available"); return; } - let com_iface = - com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") - .unwrap(); + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); assert_eq!( com_iface.coclass_clsid.as_deref(), Some("56fdf344-fd6d-11d0-958a-006097c9a090") @@ -162,9 +186,12 @@ fn param_type_mapping() { eprintln!("Skipping: Win32 winmd not available"); return; } - let com_iface = - com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") - .unwrap(); + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); // Generate wrapper as a text bundle we can inspect for the mapping decisions let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) @@ -251,13 +278,21 @@ fn shelllink_scalar_out_pointers_preserve_pointee_types() { assert!(matches!(get_icon_location.params[2].typ, TypeMeta::I32)); let output = com::generate_com_interface_files(&interface, &win32_winmd()).unwrap(); - assert!(output.js.contains( - ".addMethod('GetHotkey', new DynComMethodSig().addOut(DynCom.u16Type()))" - )); - assert!(output.js.contains( - ".addMethod('GetShowCmd', new DynComMethodSig().addOut(DynCom.i32Type()))" - )); - assert!(output.dts.contains("getIconLocation(cch?: number): [string, number];")); + assert!( + output + .js + .contains(".addMethod('GetHotkey', new DynComMethodSig().addOut(DynCom.u16Type()))") + ); + assert!( + output + .js + .contains(".addMethod('GetShowCmd', new DynComMethodSig().addOut(DynCom.i32Type()))") + ); + assert!( + output + .dts + .contains("getIconLocation(cch?: number): [string, number];") + ); } /// 6. Partial generation: generating a single class-name yields ONLY that @@ -269,9 +304,12 @@ fn partial_generation_only_emits_target_interface() { eprintln!("Skipping: Win32 winmd not available"); return; } - let com_iface = - com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") - .unwrap(); + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for classic-COM interface"); @@ -309,9 +347,12 @@ fn dts_surface_is_natural_and_clean() { eprintln!("Skipping: Win32 winmd not available"); return; } - let com_iface = - com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") - .unwrap(); + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for classic-COM interface"); let dts = out.dts.as_str(); @@ -384,9 +425,12 @@ fn js_body_uses_cocreateinstance_and_correct_slots() { eprintln!("Skipping: Win32 winmd not available"); return; } - let com_iface = - com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") - .unwrap(); + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for classic-COM interface"); let js = out.js.as_str(); @@ -591,9 +635,12 @@ fn snapshot_itaskbarlist3() { eprintln!("Skipping snapshot test: Win32 winmd not available"); return; } - let com_iface = - com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "ITaskbarList3") - .expect("ITaskbarList3 must exist"); + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .expect("ITaskbarList3 must exist"); let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) .expect("codegen must succeed for classic-COM interface"); @@ -830,3 +877,319 @@ fn u16_input_param_uses_existing_u16_value_ctor_not_u16value() { out.js ); } + +#[test] +fn native_array_buffers_fail_closed() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.Storage.Imapi", + "IDiscRecorder", + ) + .expect("IDiscRecorder must exist"); + let error = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect_err("NativeArrayInfo byte buffers must not become scalar in/out storage"); + + assert!( + error.contains("GetRecorderGUID") + && error.contains("pbyUniqueID") + && error.contains("caller-sized native buffers are not supported"), + "generation must fail with a targeted buffer diagnostic: {error}" + ); +} + +#[test] +fn pointer_sized_integers_use_runtime_width() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.ClrHosting", + "IApartmentCallback", + ) + .expect("IApartmentCallback must exist"); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("pointer-sized parameters should be supported"); + + assert!( + output + .js + .contains(".addIn(DynCom.usizeType()).addIn(DynCom.usizeType())"), + "USize parameters must use runtime-width ABI types:\n{}", + output.js + ); + assert!( + output.js.contains("DynCom.usize(BigInt(pFunc))") + && output.js.contains("DynCom.usize(BigInt(pData))"), + "USize values must use runtime-width constructors:\n{}", + output.js + ); + assert!( + !output.js.contains("DynCom.u64Type()"), + "pointer-sized parameters must not be fixed to 64 bits:\n{}", + output.js + ); +} + +#[test] +fn required_parameters_after_string_buffer_count_remain_required() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IExtractImage", + ) + .expect("IExtractImage must exist"); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("IExtractImage generation should succeed"); + + assert!( + output + .js + .contains("getLocation(cch, pdwPriority, prgSize, recClrDepth, pdwFlags)"), + "required parameters, including cch before them, must not get defaults:\n{}", + output.js + ); + assert!( + !output.js.contains("prgSize = 0") + && !output.js.contains("dwRecClrDepth = 0") + && !output.js.contains("pdwFlags = 0"), + "required native arguments must not be silently defaulted:\n{}", + output.js + ); + assert!( + output.dts.contains( + "getLocation(cch: number, pdwPriority: number, prgSize: bigint | Buffer, recClrDepth: number, pdwFlags: number)" + ), + "declarations must keep the parameters required:\n{}", + output.dts + ); +} + +#[test] +fn bstr_outputs_are_decoded_and_freed() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.System.Com", "IErrorInfo") + .expect("IErrorInfo must exist"); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("IErrorInfo generation should succeed"); + + assert!( + output.js.contains("return DynCom.takeBstr(_out);"), + "BSTR outputs must be converted through the freeing helper:\n{}", + output.js + ); + assert!( + output.dts.contains("getDescription(): string;"), + "BSTR outputs must project as strings:\n{}", + output.dts + ); +} + +#[test] +fn unsigned_enum_values_preserve_their_value() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let enum_type = com_metadata::parse_com_enum( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "FILEOPERATION_FLAGS", + ) + .expect("FILEOPERATION_FLAGS must exist"); + let value = enum_type + .members + .iter() + .find(|member| member.name == "FOFX_DONTDISPLAYLOCATIONS") + .expect("FOFX_DONTDISPLAYLOCATIONS must exist") + .value + .clone(); + + assert!(matches!(enum_type.underlying, TypeMeta::U32)); + assert_eq!(value, com_metadata::ComEnumValue::Unsigned(2_147_483_648)); + + let shared_type = meta::parse_enums(&win32_winmd(), "Windows.Win32.UI.Shell") + .into_iter() + .find(|typ| { + matches!( + typ, + TypeMeta::Enum { name, .. } if name == "FILEOPERATION_FLAGS" + ) + }) + .expect("shared enum parser must still find FILEOPERATION_FLAGS"); + let TypeMeta::Enum { + underlying, + members, + .. + } = shared_type + else { + unreachable!() + }; + assert!( + matches!(*underlying, TypeMeta::I32), + "the shared WinRT model must remain unchanged" + ); + assert_eq!( + members + .iter() + .find(|member| member.name == "FOFX_DONTDISPLAYLOCATIONS") + .unwrap() + .value, + i32::MIN, + "unsigned Win32 values must be corrected only in the COM-local model" + ); +} + +#[test] +fn optional_string_buffer_placeholders_do_not_precede_required_parameters() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "IShellLinkW") + .expect("IShellLinkW must exist"); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("IShellLinkW generation should succeed"); + + assert!( + output + .dts + .contains("getPath(cch: number, pfd: bigint | Buffer, fFlags: number)"), + "a required parameter must not follow an optional pfd placeholder:\n{}", + output.dts + ); + assert!( + !output.js.contains("getPath(cch = 260") && !output.js.contains("pfd = 0, fFlags"), + "JavaScript defaults must obey the same trailing-optional rule:\n{}", + output.js + ); +} + +#[test] +fn namespace_mode_rejects_classic_com_instead_of_using_winrt_slots() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let output = Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args([ + "generate", + "--winmd", + &win32_winmd(), + "--namespace", + "Windows.Win32.UI.Shell", + "--dry-run", + ]) + .output() + .expect("spawn dynwinrt-codegen"); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!(!output.status.success(), "namespace mode must fail closed"); + assert!( + stderr.contains("classic-COM namespace projection is not supported") + && stderr.contains("--class-name"), + "failure must direct callers to the safe class mode:\n{stderr}" + ); +} + +#[test] +fn com_only_generation_emits_an_importable_package_shape() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let output_dir = std::env::temp_dir().join(format!( + "dynwinrt-codegen-com-package-{}", + std::process::id() + )); + if output_dir.exists() { + fs::remove_dir_all(&output_dir).expect("remove stale COM package test directory"); + } + + let output = Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args([ + "generate", + "--winmd", + &win32_winmd(), + "--namespace", + "Windows.Win32.UI.Shell", + "--class-name", + "ITaskbarList3", + "--output", + output_dir.to_str().unwrap(), + ]) + .output() + .expect("spawn dynwinrt-codegen"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(output.status.success(), "COM generation failed:\n{stderr}"); + + for name in ["index.js", "index.d.ts", "package.json"] { + assert!( + output_dir.join(name).is_file(), + "COM-only output must include {name}" + ); + } + let index = fs::read_to_string(output_dir.join("index.js")).unwrap(); + assert!(index.contains("ITaskbarList3") && index.contains("TBPFLAG")); + let package = fs::read_to_string(output_dir.join("package.json")).unwrap(); + assert!(package.contains("\"type\": \"module\"")); + assert!(package.contains("\"./ITaskbarList3\"")); + + let incremental = Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args([ + "generate", + "--winmd", + &win32_winmd(), + "--namespace", + "Windows.Win32.UI.Shell", + "--class-name", + "IShellLinkW", + "--output", + output_dir.to_str().unwrap(), + ]) + .output() + .expect("spawn incremental dynwinrt-codegen"); + let incremental_stderr = String::from_utf8_lossy(&incremental.stderr); + assert!( + incremental.status.success(), + "incremental COM generation failed:\n{incremental_stderr}" + ); + let incremental_index = fs::read_to_string(output_dir.join("index.js")).unwrap(); + assert!( + incremental_index.contains("ITaskbarList3") + && incremental_index.contains("IShellLinkW") + && incremental_index.contains("TBPFLAG") + && incremental_index.contains("SHOW_WINDOW_CMD"), + "incremental generation must preserve earlier exports:\n{incremental_index}" + ); + let incremental_package = fs::read_to_string(output_dir.join("package.json")).unwrap(); + assert!( + incremental_package.contains("\"./ITaskbarList3\"") + && incremental_package.contains("\"./IShellLinkW\""), + "incremental generation must preserve earlier package subpaths:\n{incremental_package}" + ); + + fs::remove_dir_all(&output_dir).expect("remove COM package test directory"); +} From 775f0d3f2a27879ff4edd1d742bb72e36d57550b Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Tue, 28 Jul 2026 23:56:31 +0800 Subject: [PATCH 23/28] Isolate Classic COM runtime entrypoint Keep the package root WinRT-only and expose DynCom APIs through @microsoft/dynwinrt/com over the same native binary. Generate typed CJS facades and wire them through codegen, CI, release, samples, and E2E. Expand stock-Windows coverage for IPersistFile, IMalloc, IStream, IFileOperation, IFileOpenDialog, and IWICImagingFactory, including ownership and pointer-width ABI paths. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 41a826d4-b04a-4932-b1b8-31173fc5ab40 --- .github/copilot-instructions.md | 2 +- .github/workflows/build.yml | 16 +- .pipelines/ci.yml | 10 +- .pipelines/release.yml | 11 +- CLAUDE.md | 3 +- README.md | 8 +- bench-electron/electron.vite.config.ts | 2 +- bindings/js/README.md | 14 ++ bindings/js/__test__/async-promise-child.mjs | 2 +- .../__test__/dispatcher-queue-winui-child.cjs | 2 +- .../js/__test__/dispatcher-shutdown-child.mjs | 2 +- bindings/js/__test__/env-cleanup-child.mjs | 2 +- bindings/js/__test__/index.spec.ts | 46 ++++- bindings/js/__test__/progress-exit-child.mjs | 2 +- bindings/js/__test__/sta-async-child.mjs | 2 +- bindings/js/package.json | 31 ++- bindings/js/samples/array_struct.ts | 2 +- bindings/js/samples/bench_3way.ts | 2 +- bindings/js/samples/benchmark.ts | 2 +- bindings/js/samples/ocr.ts | 2 +- bindings/js/samples/picker.ts | 2 +- bindings/js/samples/test_progress.ts | 2 +- .../js/samples/test_register_interface.ts | 2 +- bindings/js/scripts/generate-entrypoints.mjs | 61 ++++++ crates/dynwinrt/src/com.rs | 186 +++++++++++++++++- tests/e2e_test.ps1 | 35 +++- tests/runners/com/dtm.mjs | 2 +- tests/runners/com/file-open-dialog.mjs | 16 ++ tests/runners/com/file-operation.mjs | 21 ++ tests/runners/com/hwnd.mjs | 3 +- tests/runners/com/pointer-reject-object.mjs | 2 +- tests/runners/com/shelllink-buffer.mjs | 8 +- tests/runners/com/smtc.mjs | 2 +- tests/runners/com/wic-imaging-factory.mjs | 23 +++ tests/runners/ts_runner.ts | 2 +- tools/dynwinrt-codegen/E2E_TEST.md | 2 +- .../src/codegen/com/render.rs | 21 +- .../IDataTransferManagerInterop.d.ts | 2 +- .../IDataTransferManagerInterop.js | 2 +- .../snapshots/itaskbarlist3/ITaskbarList3.js | 2 +- .../dynwinrt-codegen/tests/win32_com_test.rs | 16 +- 41 files changed, 514 insertions(+), 61 deletions(-) create mode 100644 bindings/js/scripts/generate-entrypoints.mjs create mode 100644 tests/runners/com/file-open-dialog.mjs create mode 100644 tests/runners/com/file-operation.mjs create mode 100644 tests/runners/com/wic-imaging-factory.mjs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index edbe94e3..7ab9f476 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -33,7 +33,7 @@ python -m pytest tests/ -v # JS binding (requires Node.js 18+) cd bindings/js npm install -npx napi build --no-const-enum --platform --release -o dist +npm run build # Code generation (JS + .d.ts is the default; --lang py for Python) cargo run -p dynwinrt-codegen -- generate --namespace Windows.Foundation --class-name Uri --output ./generated diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9b2fe6e6..5a0b9950 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -80,6 +80,8 @@ jobs: run: | npm install npx napi build --no-const-enum --platform --release -o dist + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build:entrypoints --silent npm install --no-save tsx - name: Build Python binding run: | @@ -137,10 +139,16 @@ jobs: run: npm install - name: Build x64 working-directory: bindings/js - run: npx napi build --no-const-enum --platform --release -o dist + run: | + npx napi build --no-const-enum --platform --release -o dist + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build:entrypoints --silent - name: Build arm64 working-directory: bindings/js - run: npx napi build --no-const-enum --platform --release --target aarch64-pc-windows-msvc -o dist + run: | + npx napi build --no-const-enum --platform --release --target aarch64-pc-windows-msvc -o dist + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build:entrypoints --silent - uses: actions/upload-artifact@v4 with: name: dynwinrt @@ -149,3 +157,7 @@ jobs: bindings/js/dist/dynwinrt.win32-arm64-msvc.node bindings/js/dist/index.js bindings/js/dist/index.d.ts + bindings/js/dist/winrt.js + bindings/js/dist/winrt.d.ts + bindings/js/dist/com.js + bindings/js/dist/com.d.ts diff --git a/.pipelines/ci.yml b/.pipelines/ci.yml index 51b08a6a..71da2f28 100644 --- a/.pipelines/ci.yml +++ b/.pipelines/ci.yml @@ -174,14 +174,20 @@ extends: inputs: targetType: inline workingDirectory: bindings/js - script: npx napi build --no-const-enum --platform --release -o dist + script: | + npx napi build --no-const-enum --platform --release -o dist + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build:entrypoints --silent - task: PowerShell@2 displayName: Build dynwinrt (arm64) inputs: targetType: inline workingDirectory: bindings/js - script: npx napi build --no-const-enum --platform --release --target aarch64-pc-windows-msvc -o dist + script: | + npx napi build --no-const-enum --platform --release --target aarch64-pc-windows-msvc -o dist + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build:entrypoints --silent # Install tsx for E2E TS runner - task: PowerShell@2 diff --git a/.pipelines/release.yml b/.pipelines/release.yml index 671651e1..4e8c5218 100644 --- a/.pipelines/release.yml +++ b/.pipelines/release.yml @@ -164,14 +164,20 @@ extends: inputs: targetType: inline workingDirectory: bindings/js - script: npx napi build --no-const-enum --platform --release -o dist + script: | + npx napi build --no-const-enum --platform --release -o dist + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build:entrypoints --silent - task: PowerShell@2 displayName: Build dynwinrt (arm64) inputs: targetType: inline workingDirectory: bindings/js - script: npx napi build --no-const-enum --platform --release --target aarch64-pc-windows-msvc -o dist + script: | + npx napi build --no-const-enum --platform --release --target aarch64-pc-windows-msvc -o dist + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build:entrypoints --silent # Set version from tag - task: PowerShell@2 @@ -341,4 +347,3 @@ extends: mainpublisher: 'ESRPRELPACMAN' domaintenantid: ${{ parameters.signingIdentity.tenantId }} - diff --git a/CLAUDE.md b/CLAUDE.md index 64c204ef..87295f99 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,7 @@ cargo test -p dynwinrt cargo test -p dynwinrt-codegen # Build JS bindings -cd bindings/js && npm install && npx napi build --no-const-enum --platform --release -o dist +cd bindings/js && npm install && npm run build # Build Python bindings cd bindings/py && maturin develop @@ -195,4 +195,3 @@ The library uses `windows-core::IUnknown` smart pointers which automatically han ### Parameterized IID Computation Generic interfaces (IVector\, IMap\, IAsyncOperation\) have IIDs computed at runtime using the WinRT parameterized interface algorithm (SHA-1 hash of the PIID + type argument signatures). This is implemented in `metadata_table/iid.rs`. - diff --git a/README.md b/README.md index 6751c1b8..8d022ac4 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,10 @@ const uri = new Uri('https://example.com/path?q=1'); console.log(uri.host); // "example.com" ``` +Classic COM bindings import their runtime API from the separate +`@microsoft/dynwinrt/com` subpath. It is part of the same npm package; the +package root remains the WinRT-only API. + Generated bindings project unambiguous public WinRT activation metadata as JavaScript constructors, including overloads such as `new Uri(base, relative)`. Existing static factory methods remain available. Classes that can only be returned by @@ -119,7 +123,7 @@ cargo build -p dynwinrt cargo test -p dynwinrt # JS bindings (napi-rs) -cd bindings/js && npm install && npx napi build --no-const-enum --platform --release -o dist +cd bindings/js && npm install && npm run build # Python bindings (PyO3 + maturin) — experimental, not published to PyPI cd bindings/py && maturin develop && pytest @@ -153,7 +157,7 @@ For each WinRT class the codegen emits a typed wrapper, factory, interface regis Generated files import from `'@microsoft/dynwinrt'`. When iterating against a locally-built runtime, rewrite imports to the relative path: ```bash -find generated -name "*.js" -exec sed -i "s|from '@microsoft/dynwinrt'|from '../../dist/index.js'|g" {} + +find generated -name "*.js" -exec sed -i "s|from '@microsoft/dynwinrt'|from '../../dist/winrt.js'|g" {} + ``` ## Troubleshooting diff --git a/bench-electron/electron.vite.config.ts b/bench-electron/electron.vite.config.ts index dacc4ba8..d8ca927d 100644 --- a/bench-electron/electron.vite.config.ts +++ b/bench-electron/electron.vite.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ plugins: [externalizeDepsPlugin()], build: { rollupOptions: { - external: ['@microsoft/dynwinrt', /\.node$/] + external: [/^@microsoft\/dynwinrt(?:\/com)?$/, /\.node$/] } } }, diff --git a/bindings/js/README.md b/bindings/js/README.md index 5f584d16..00888b37 100644 --- a/bindings/js/README.md +++ b/bindings/js/README.md @@ -41,6 +41,20 @@ console.log(uri.host); // "example.com" console.log(uri.port); // 443 ``` +Classic COM uses a separate subpath from the same package, keeping the WinRT +root API unchanged: + +```js +const { DynCom } = require('@microsoft/dynwinrt/com'); +``` + +COM interface values returned by activation, `QueryInterface`, or typed +interface out-parameters own one reference and release it when their +`DynWinRtValue` is released or collected. `adoptComPointer()` is only for a +native output that transfers an existing `+1` reference; numeric pointers and +typed-array pointers are borrowed and cannot be adopted. Win32 handles are not +COM references and require their own type-specific cleanup function. + Unambiguous public WinRT activation metadata is projected as JavaScript constructors. Parameterized and composable activations support idiomatic forms such as `new Uri(base, relative)` and `new StackPanel()`. The generated static factory diff --git a/bindings/js/__test__/async-promise-child.mjs b/bindings/js/__test__/async-promise-child.mjs index 4f782ad2..72fbf8fe 100644 --- a/bindings/js/__test__/async-promise-child.mjs +++ b/bindings/js/__test__/async-promise-child.mjs @@ -7,7 +7,7 @@ import { createRequire } from 'node:module' const require = createRequire(import.meta.url) const { DynWinRtMethodSig, DynWinRtType, DynWinRtValue, WinGuid, roInitialize } = require( - process.env.DYNWINRT_TEST_RUNTIME ?? '../dist/index.js', + process.env.DYNWINRT_TEST_RUNTIME ?? '../dist/winrt.js', ) roInitialize(1) diff --git a/bindings/js/__test__/dispatcher-queue-winui-child.cjs b/bindings/js/__test__/dispatcher-queue-winui-child.cjs index bebafb61..d8229ce1 100644 --- a/bindings/js/__test__/dispatcher-queue-winui-child.cjs +++ b/bindings/js/__test__/dispatcher-queue-winui-child.cjs @@ -11,7 +11,7 @@ const { spawn } = require('node:child_process') const applicationModule = process.argv[2] const bootstrapDll = process.argv[3] -const runtimeModule = process.argv[4] ?? path.resolve(__dirname, '../dist/index.js') +const runtimeModule = process.argv[4] ?? path.resolve(__dirname, '../dist/winrt.js') const startMode = process.argv[5] ?? 'direct' if (!applicationModule || !bootstrapDll) { throw new Error( diff --git a/bindings/js/__test__/dispatcher-shutdown-child.mjs b/bindings/js/__test__/dispatcher-shutdown-child.mjs index d89ba55d..33f92dc3 100644 --- a/bindings/js/__test__/dispatcher-shutdown-child.mjs +++ b/bindings/js/__test__/dispatcher-shutdown-child.mjs @@ -14,7 +14,7 @@ const { registerWinuiDispatcherQueue, roInitialize, unregisterWinuiDispatcherQueue, -} = require(process.env.DYNWINRT_TEST_RUNTIME ?? '../dist/index.js') +} = require(process.env.DYNWINRT_TEST_RUNTIME ?? '../dist/winrt.js') roInitialize(0) diff --git a/bindings/js/__test__/env-cleanup-child.mjs b/bindings/js/__test__/env-cleanup-child.mjs index a1a4b6bc..0bd62d46 100644 --- a/bindings/js/__test__/env-cleanup-child.mjs +++ b/bindings/js/__test__/env-cleanup-child.mjs @@ -6,7 +6,7 @@ import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' import { Worker } from 'node:worker_threads' -const runtime = process.env.DYNWINRT_TEST_RUNTIME ?? fileURLToPath(new URL('../dist/index.js', import.meta.url)) +const runtime = process.env.DYNWINRT_TEST_RUNTIME ?? fileURLToPath(new URL('../dist/winrt.js', import.meta.url)) createRequire(import.meta.url)(runtime) const worker = new Worker(new URL('./env-cleanup-worker.mjs', import.meta.url), { workerData: { runtime }, diff --git a/bindings/js/__test__/index.spec.ts b/bindings/js/__test__/index.spec.ts index 0d4a563e..fc6e808e 100644 --- a/bindings/js/__test__/index.spec.ts +++ b/bindings/js/__test__/index.spec.ts @@ -2,13 +2,12 @@ // Licensed under the MIT License. import test from 'ava' -import { spawn } from 'node:child_process' +import { spawn, spawnSync } from 'node:child_process' import { existsSync } from 'node:fs' import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' import { - DynCom, DynWinRtArray, DynWinRtMethodSig, DynWinRtType, @@ -18,7 +17,46 @@ import { getWindowsDirectory, hasPackageIdentity, roInitialize, -} from '../dist/index.js' +} from '../dist/winrt.js' +import * as winrtRuntime from '../dist/winrt.js' +import { DynCom } from '../dist/com.js' + +test('Classic COM is isolated from the WinRT root entrypoint', (t) => { + t.false(Object.prototype.hasOwnProperty.call(winrtRuntime, 'DynCom')) + t.truthy(DynCom) + + const assertion = + "const assert = require('node:assert/strict');" + + "const winrt = require('@microsoft/dynwinrt');" + + "const com = require('@microsoft/dynwinrt/com');" + + "assert.equal(Object.prototype.hasOwnProperty.call(winrt, 'DynCom'), false);" + + "assert.equal(typeof winrt.DynWinRtType, 'function');" + + "assert.equal(typeof com.DynCom, 'function');" + + "console.log('runtime-entrypoints-ok')" + const cjs = spawnSync(process.execPath, ['--eval', assertion], { + cwd: resolve(process.cwd()), + encoding: 'utf8', + windowsHide: true, + }) + t.is(cjs.status, 0, cjs.stderr) + t.regex(cjs.stdout, /runtime-entrypoints-ok/) + + const esmAssertion = + "import assert from 'node:assert/strict';" + + "import * as winrt from '@microsoft/dynwinrt';" + + "import * as com from '@microsoft/dynwinrt/com';" + + "assert.equal(Object.prototype.hasOwnProperty.call(winrt, 'DynCom'), false);" + + "assert.equal(typeof winrt.DynWinRtType, 'function');" + + "assert.equal(typeof com.DynCom, 'function');" + + "console.log('runtime-entrypoints-ok')" + const esm = spawnSync(process.execPath, ['--input-type=module', '--eval', esmAssertion], { + cwd: resolve(process.cwd()), + encoding: 'utf8', + windowsHide: true, + }) + t.is(esm.status, 0, esm.stderr) + t.regex(esm.stdout, /runtime-entrypoints-ok/) +}) test('DynCom rejects pointers after their TypedArray backing store is detached', (t) => { const bytes = new Uint8Array(16) @@ -239,7 +277,7 @@ if (missingWinuiFixtures.length > 0) { test.skip('WinUI scheduled start drains Promise reactions inside Application.Start', () => {}) } else { test('WinUI scheduled start drains Promise reactions inside Application.Start', async (t) => { - const runtimeModule = fileURLToPath(new URL('../dist/index.js', import.meta.url)) + const runtimeModule = fileURLToPath(new URL('../dist/winrt.js', import.meta.url)) const child = spawn( process.execPath, [ diff --git a/bindings/js/__test__/progress-exit-child.mjs b/bindings/js/__test__/progress-exit-child.mjs index bda3701b..c213fa12 100644 --- a/bindings/js/__test__/progress-exit-child.mjs +++ b/bindings/js/__test__/progress-exit-child.mjs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { DynWinRtMethodSig, DynWinRtType, DynWinRtValue, WinGuid, roInitialize } from '../dist/index.js' +import { DynWinRtMethodSig, DynWinRtType, DynWinRtValue, WinGuid, roInitialize } from '../dist/winrt.js' roInitialize(1) diff --git a/bindings/js/__test__/sta-async-child.mjs b/bindings/js/__test__/sta-async-child.mjs index 66241a4f..11ef4cd1 100644 --- a/bindings/js/__test__/sta-async-child.mjs +++ b/bindings/js/__test__/sta-async-child.mjs @@ -5,7 +5,7 @@ import { createRequire } from 'node:module' const require = createRequire(import.meta.url) const { DynWinRtMethodSig, DynWinRtType, DynWinRtValue, WinGuid, roInitialize } = require( - process.env.DYNWINRT_TEST_RUNTIME ?? '../dist/index.js', + process.env.DYNWINRT_TEST_RUNTIME ?? '../dist/winrt.js', ) roInitialize(0) diff --git a/bindings/js/package.json b/bindings/js/package.json index e4f2d15f..779bdace 100644 --- a/bindings/js/package.json +++ b/bindings/js/package.json @@ -2,8 +2,30 @@ "name": "@microsoft/dynwinrt", "version": "0.1.0", "description": "Dynamic WinRT bindings for Node.js — call any Windows Runtime API without native code generation", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "dist/winrt.js", + "types": "dist/winrt.d.ts", + "exports": { + ".": { + "types": "./dist/winrt.d.ts", + "import": "./dist/winrt.js", + "require": "./dist/winrt.js", + "default": "./dist/winrt.js" + }, + "./com": { + "types": "./dist/com.d.ts", + "import": "./dist/com.js", + "require": "./dist/com.js", + "default": "./dist/com.js" + }, + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "com": [ + "dist/com.d.ts" + ] + } + }, "repository": { "type": "git", "url": "https://github.com/microsoft/dynwinrt" @@ -41,8 +63,9 @@ "scripts": { "artifacts": "napi artifacts", "bench": "node --import @oxc-node/core/register benchmark/bench.ts", - "build": "napi build --no-const-enum --platform --release -o dist", - "build:debug": "napi --no-const-enum build --platform -o dist", + "build": "napi build --no-const-enum --platform --release -o dist && npm run build:entrypoints", + "build:debug": "napi --no-const-enum build --platform -o dist && npm run build:entrypoints", + "build:entrypoints": "node scripts/generate-entrypoints.mjs", "format": "run-p format:prettier format:rs format:toml", "format:prettier": "prettier . -w", "format:toml": "taplo format", diff --git a/bindings/js/samples/array_struct.ts b/bindings/js/samples/array_struct.ts index 61d0d042..a553897a 100644 --- a/bindings/js/samples/array_struct.ts +++ b/bindings/js/samples/array_struct.ts @@ -18,7 +18,7 @@ import { DynWinRtStruct, WinGuid, roInitialize, -} from '../dist/index.js' +} from '../dist/winrt.js' // Initialize WinRT (MTA) roInitialize(1) diff --git a/bindings/js/samples/bench_3way.ts b/bindings/js/samples/bench_3way.ts index a17a15ca..25b3cca4 100644 --- a/bindings/js/samples/bench_3way.ts +++ b/bindings/js/samples/bench_3way.ts @@ -13,7 +13,7 @@ import { DynWinRtValue, DynWinRtType, DynWinRtMethodSig, DynWinRtStruct, WinGuid, roInitialize, RustStaticBench, rawGetString, rawGetI32, -} from '../dist/index.js' +} from '../dist/winrt.js' import { createRequire } from 'node:module' const require = createRequire(import.meta.url) diff --git a/bindings/js/samples/benchmark.ts b/bindings/js/samples/benchmark.ts index 44d8eccb..ad87132c 100644 --- a/bindings/js/samples/benchmark.ts +++ b/bindings/js/samples/benchmark.ts @@ -25,7 +25,7 @@ import { DynWinRtStruct, WinGuid, roInitialize, -} from '../dist/index.js' +} from '../dist/winrt.js' import { createRequire } from 'node:module' const require = createRequire(import.meta.url) diff --git a/bindings/js/samples/ocr.ts b/bindings/js/samples/ocr.ts index 67e95e44..8cf0a8e5 100644 --- a/bindings/js/samples/ocr.ts +++ b/bindings/js/samples/ocr.ts @@ -14,7 +14,7 @@ import { WinGuid, hasPackageIdentity, initWinappsdk, -} from '../dist/index.js' +} from '../dist/winrt.js' // ====================================================================== // IIDs diff --git a/bindings/js/samples/picker.ts b/bindings/js/samples/picker.ts index 7d4c4f6e..577b792e 100644 --- a/bindings/js/samples/picker.ts +++ b/bindings/js/samples/picker.ts @@ -8,7 +8,7 @@ import { DynWinRtType, DynWinRtMethodSig, WinGuid, -} from '../dist/index.js' +} from '../dist/winrt.js' // ====================================================================== // Register interfaces (once) diff --git a/bindings/js/samples/test_progress.ts b/bindings/js/samples/test_progress.ts index 794c9c35..67f95f7c 100644 --- a/bindings/js/samples/test_progress.ts +++ b/bindings/js/samples/test_progress.ts @@ -12,7 +12,7 @@ import { DynWinRtMethodSig, WinGuid, roInitialize, -} from '../dist/index.js' +} from '../dist/winrt.js' roInitialize(1) diff --git a/bindings/js/samples/test_register_interface.ts b/bindings/js/samples/test_register_interface.ts index 8b2e9719..16bdd83e 100644 --- a/bindings/js/samples/test_register_interface.ts +++ b/bindings/js/samples/test_register_interface.ts @@ -11,7 +11,7 @@ import { DynWinRtMethodSig, WinGuid, roInitialize, -} from '../dist/index.js' +} from '../dist/winrt.js' roInitialize(1) diff --git a/bindings/js/scripts/generate-entrypoints.mjs b/bindings/js/scripts/generate-entrypoints.mjs new file mode 100644 index 00000000..2fa73d4a --- /dev/null +++ b/bindings/js/scripts/generate-entrypoints.mjs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { readFileSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const packageDir = fileURLToPath(new URL('..', import.meta.url)) +const distDir = join(packageDir, 'dist') +const loader = readFileSync(join(distDir, 'index.js'), 'utf8') +const nativeExports = [ + ...loader.matchAll(/^module\.exports\.([A-Za-z_$][\w$]*) = nativeBinding\.\1$/gm), +].map((match) => match[1]) + +if (nativeExports.length === 0) { + throw new Error('No N-API exports found in dist/index.js') +} + +const comExports = new Set([ + 'DynCom', + 'DynComInterface', + 'DynComMethodHandle', + 'DynComMethodSig', + 'DynComType', + 'DynWinRtValue', + 'DynWinRTValue', + 'WinGuid', + 'WinGUID', +]) + +writeFacade( + 'winrt', + nativeExports.filter((name) => !name.startsWith('DynCom')), +) +writeFacade( + 'com', + nativeExports.filter((name) => comExports.has(name)), +) + +function writeFacade(name, exports) { + const missing = name === 'com' ? [...comExports].filter((value) => !exports.includes(value)) : [] + if (missing.length > 0) { + throw new Error(`Missing required ${name} exports: ${missing.join(', ')}`) + } + + const js = [ + '// Generated by scripts/generate-entrypoints.mjs - do not edit', + "'use strict'", + "const native = require('./index.js')", + ...exports.map((value) => `module.exports.${value} = native.${value}`), + '', + ].join('\n') + const dts = [ + '// Generated by scripts/generate-entrypoints.mjs - do not edit', + `export { ${exports.join(', ')} } from './index.js'`, + '', + ].join('\n') + + writeFileSync(join(distDir, `${name}.js`), js) + writeFileSync(join(distDir, `${name}.d.ts`), dts) +} diff --git a/crates/dynwinrt/src/com.rs b/crates/dynwinrt/src/com.rs index 6467a7ca..9ee69606 100644 --- a/crates/dynwinrt/src/com.rs +++ b/crates/dynwinrt/src/com.rs @@ -251,7 +251,8 @@ mod tests { use windows::{ ApplicationModel::DataTransfer::DataTransferManager, Win32::{ - UI::Shell::IDataTransferManagerInterop, + System::Com::{CoGetMalloc, IMalloc, IPersistFile, IStream}, + UI::Shell::{IDataTransferManagerInterop, SHCreateMemStream}, UI::WindowsAndMessaging::{ CreateWindowExW, DestroyWindow, WINDOW_EX_STYLE, WS_OVERLAPPED, }, @@ -460,6 +461,45 @@ mod tests { iface } + fn native_usize_type(table: &std::sync::Arc) -> Type { + #[cfg(target_pointer_width = "64")] + { + Type::winrt(table.u64_type()) + } + #[cfg(target_pointer_width = "32")] + { + Type::winrt(table.u32_type()) + } + } + + fn native_usize_value(value: usize) -> WinRTValue { + #[cfg(target_pointer_width = "64")] + { + WinRTValue::U64(value as u64) + } + #[cfg(target_pointer_width = "32")] + { + WinRTValue::U32(value as u32) + } + } + + fn read_native_usize(value: &WinRTValue) -> usize { + #[cfg(target_pointer_width = "64")] + { + match value { + WinRTValue::U64(value) => *value as usize, + value => panic!("expected native u64, got {value:?}"), + } + } + #[cfg(target_pointer_width = "32")] + { + match value { + WinRTValue::U32(value) => *value as usize, + value => panic!("expected native u32, got {value:?}"), + } + } + } + #[test] fn shell_link_set_get_show_cmd_round_trips_via_classic_com_vtable() -> result::Result<()> { let shell_link = shell_link()?.as_object().unwrap(); @@ -506,6 +546,150 @@ mod tests { Ok(()) } + #[test] + fn shell_link_query_interface_returns_owned_ipersistfile() -> result::Result<()> { + let shell_link = shell_link()?; + let persist = shell_link.cast(&IPersistFile::IID)?; + let persist = persist.as_object().expect("IPersistFile must be non-null"); + let table = MetadataTable::new(); + let result = call_method( + 3, + persist.as_raw(), + MethodSignature::new(&table).add_out(Type::winrt(table.guid_type())), + &[], + )?; + + assert!(matches!( + result.as_slice(), + [WinRTValue::Guid(clsid)] if *clsid == CLSID_SHELL_LINK + )); + Ok(()) + } + + #[test] + fn malloc_exercises_pointer_sized_and_non_hresult_abi() -> result::Result<()> { + initialize_apartment(ApartmentType::MultiThreaded)?; + let allocator = unsafe { CoGetMalloc(1) }.map_err(result::Error::WindowsError)?; + let table = MetadataTable::new(); + let requested = 64usize; + let allocated = call_method( + 3, + allocator.as_raw(), + MethodSignature::new(&table) + .add_in(native_usize_type(&table)) + .returns(Type::pointer()), + &[native_usize_value(requested)], + )?; + let WinRTValue::RawPtr(ptr) = allocated[0] else { + panic!("IMalloc::Alloc must return a native pointer"); + }; + assert!(!ptr.is_null()); + + struct AllocationGuard { + allocator: IMalloc, + ptr: *mut c_void, + } + impl Drop for AllocationGuard { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { self.allocator.Free(Some(self.ptr)) }; + } + } + } + let mut allocation = AllocationGuard { + allocator: allocator.clone(), + ptr, + }; + + let size = call_method( + 6, + allocator.as_raw(), + MethodSignature::new(&table) + .add_in(Type::pointer()) + .returns(native_usize_type(&table)), + &[WinRTValue::RawPtr(ptr)], + )?; + assert!(read_native_usize(&size[0]) >= requested); + + let owned = call_method( + 7, + allocator.as_raw(), + MethodSignature::new(&table) + .add_in(Type::pointer()) + .returns(Type::winrt(table.i32_type())), + &[WinRTValue::RawPtr(ptr)], + )?; + assert!(matches!(owned.as_slice(), [WinRTValue::I32(value)] if *value != 0)); + + let freed = call_method( + 5, + allocator.as_raw(), + MethodSignature::new(&table) + .add_in(Type::pointer()) + .returns_void(), + &[WinRTValue::RawPtr(ptr)], + )?; + allocation.ptr = std::ptr::null_mut(); + assert!(freed.is_empty()); + + let minimized = call_method( + 8, + allocator.as_raw(), + MethodSignature::new(&table).returns_void(), + &[], + )?; + assert!(minimized.is_empty()); + Ok(()) + } + + #[test] + fn memory_stream_exercises_counted_buffers_seek_and_interface_out() -> result::Result<()> { + initialize_apartment(ApartmentType::MultiThreaded)?; + let expected = b"dynwinrt"; + let stream = unsafe { SHCreateMemStream(Some(expected)) } + .expect("SHCreateMemStream must return an IStream"); + let table = MetadataTable::new(); + let mut buffer = vec![0u8; expected.len()]; + + let read = call_method( + 3, + stream.as_raw(), + MethodSignature::new(&table) + .add_in(Type::pointer()) + .add_in(Type::winrt(table.u32_type())) + .add_out(Type::winrt(table.u32_type())), + &[ + WinRTValue::RawPtr(buffer.as_mut_ptr().cast()), + WinRTValue::U32(buffer.len() as u32), + ], + )?; + assert!( + matches!(read.as_slice(), [WinRTValue::U32(count)] if *count == expected.len() as u32) + ); + assert_eq!(buffer, expected); + + let position = call_method( + 5, + stream.as_raw(), + MethodSignature::new(&table) + .add_in(Type::winrt(table.i64_type())) + .add_in(Type::winrt(table.u32_type())) + .add_out(Type::winrt(table.u64_type())), + &[WinRTValue::I64(0), WinRTValue::U32(0)], + )?; + assert!(matches!(position.as_slice(), [WinRTValue::U64(0)])); + + let cloned = call_method( + 13, + stream.as_raw(), + MethodSignature::new(&table).add_out(Type::winrt(table.object())), + &[], + )?; + let clone = cloned[0].as_object().expect("IStream::Clone returned null"); + let _: IStream = clone.cast().map_err(result::Error::WindowsError)?; + Ok(()) + } + #[test] fn adopt_com_pointer_accepts_addref_owned_pointer() -> result::Result<()> { let shell_link = shell_link()?.as_object().unwrap(); diff --git a/tests/e2e_test.ps1 b/tests/e2e_test.ps1 index 55a620ee..ffaf7f47 100644 --- a/tests/e2e_test.ps1 +++ b/tests/e2e_test.ps1 @@ -28,6 +28,7 @@ $pyBindingsDir = Join-Path $e2eDir "python_bindings" $comBindingsDir = Join-Path $e2eDir "com" $comShellDir = Join-Path $comBindingsDir "shell" $comInteropDir = Join-Path $comBindingsDir "interop" +$comWicDir = Join-Path $comBindingsDir "wic" $comSmtcDir = Join-Path $comBindingsDir "smtc" $env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH" @@ -119,6 +120,8 @@ if (-not $SkipBuild) { if ($LASTEXITCODE -ne 0) { Write-Error "npm install failed"; exit 1 } npx napi build --no-const-enum --platform --release -o dist 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { Write-Error "napi build failed"; exit 1 } + npm run build:entrypoints --silent + if ($LASTEXITCODE -ne 0) { Write-Error "runtime entrypoint generation failed"; exit 1 } Pop-Location } } else { @@ -173,29 +176,46 @@ foreach ($l in @($Lang | Where-Object { $_ -in @("py", "ts") })) { if ("com" -in $Lang) { Write-Host "`n--- Generate (Classic COM) ---" -ForegroundColor Yellow - $runtimeImport = "../../../../bindings/js/dist/index.js" + $comRuntimeImport = "../../../../bindings/js/dist/com.js" + $winrtRuntimeImport = "../../../../bindings/js/dist/winrt.js" & cargo run -p dynwinrt-codegen --release --quiet -- generate ` --winmd $win32Winmd ` --namespace Windows.Win32.UI.Shell ` - --class-name "ITaskbarList3,IDataTransferManagerInterop,IShellLinkW" ` + --class-name "ITaskbarList3,IDataTransferManagerInterop,IShellLinkW,IFileOperation,IFileOpenDialog" ` --output $comShellDir ` - --import-name $runtimeImport + --import-name $comRuntimeImport if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM Shell generation failed"; exit 1 } + & cargo run -p dynwinrt-codegen --release --quiet -- generate ` + --winmd $win32Winmd ` + --namespace Windows.Win32.System.Com ` + --class-name IPersistFile ` + --output $comShellDir ` + --import-name $comRuntimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM persistence generation failed"; exit 1 } + & cargo run -p dynwinrt-codegen --release --quiet -- generate ` --winmd $win32Winmd ` --namespace Windows.Win32.System.WinRT ` --class-name ISystemMediaTransportControlsInterop ` --output $comInteropDir ` - --import-name $runtimeImport + --import-name $comRuntimeImport if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM interop generation failed"; exit 1 } + & cargo run -p dynwinrt-codegen --release --quiet -- generate ` + --winmd $win32Winmd ` + --namespace Windows.Win32.Graphics.Imaging ` + --class-name IWICImagingFactory ` + --output $comWicDir ` + --import-name $comRuntimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM WIC generation failed"; exit 1 } + & cargo run -p dynwinrt-codegen --release --quiet -- generate ` --namespace Windows.Media ` --class-name SystemMediaTransportControls ` --output $comSmtcDir ` - --import-name $runtimeImport + --import-name $winrtRuntimeImport if ($LASTEXITCODE -ne 0) { Write-Error "SMTC WinRT generation failed"; exit 1 } } @@ -238,7 +258,7 @@ if ("ts" -in $Lang) { & $tsx (Join-Path $runnersDir "ts_runner.ts") ` --specs $specsFile ` --generated (Join-Path $e2eDir "ts") ` - --runtime (Join-Path $root "bindings\js\dist\index.js") ` + --runtime (Join-Path $root "bindings\js\dist\winrt.js") ` --output $tsResult if ($LASTEXITCODE -ne 0) { $totalFail++ } else { $totalPass++ } if (Test-Path $tsResult) { $allResults += (Get-Content $tsResult -Raw | ConvertFrom-Json) } @@ -251,6 +271,9 @@ if ("com" -in $Lang) { "taskbarlist.mjs", "electron-hwnd-buffer.mjs", "shelllink-buffer.mjs", + "file-operation.mjs", + "file-open-dialog.mjs", + "wic-imaging-factory.mjs", "dtm.mjs", "smtc.mjs" ) diff --git a/tests/runners/com/dtm.mjs b/tests/runners/com/dtm.mjs index 3c216e05..1c8d980b 100644 --- a/tests/runners/com/dtm.mjs +++ b/tests/runners/com/dtm.mjs @@ -6,7 +6,7 @@ // IDataTransferManagerInterop::GetForWindow(HWND, REFIID, void**) // Run: .\tests\e2e_test.ps1 -SkipBuild -Lang com -import { DynCom, DynComMethodSig, WinGuid } from '../../../bindings/js/dist/index.js'; +import { DynCom, DynComMethodSig, WinGuid } from '../../../bindings/js/dist/com.js'; import { IDataTransferManagerInterop } from '../../e2e_generated/com/shell/IDataTransferManagerInterop.js'; import { acquireHwndBigInt } from './hwnd.mjs'; diff --git a/tests/runners/com/file-open-dialog.mjs b/tests/runners/com/file-open-dialog.mjs new file mode 100644 index 00000000..7fdefde4 --- /dev/null +++ b/tests/runners/com/file-open-dialog.mjs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from 'node:assert/strict'; +import { DynCom } from '../../../bindings/js/dist/com.js'; +import { IFileOpenDialog } from '../../e2e_generated/com/shell/IFileOpenDialog.js'; + +DynCom.initialize(0); + +const dialog = IFileOpenDialog.create(); +const options = dialog.getOptions(); +dialog.setOptions(options); +assert.equal(dialog.getOptions(), options); +dialog._obj.release(); + +console.log('file-open-dialog ok'); diff --git a/tests/runners/com/file-operation.mjs b/tests/runners/com/file-operation.mjs new file mode 100644 index 00000000..09cd9297 --- /dev/null +++ b/tests/runners/com/file-operation.mjs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from 'node:assert/strict'; +import { DynCom } from '../../../bindings/js/dist/com.js'; +import { FILEOPERATION_FLAGS } from '../../e2e_generated/com/shell/FILEOPERATION_FLAGS.js'; +import { IFileOperation } from '../../e2e_generated/com/shell/IFileOperation.js'; + +DynCom.initialize(1); + +const operation = IFileOperation.create(); +const flags = + FILEOPERATION_FLAGS.FOF_NO_UI + + FILEOPERATION_FLAGS.FOFX_DONTDISPLAYLOCATIONS; + +assert.equal(flags, 2147485204); +operation.setOperationFlags(flags); +assert.equal(operation.getAnyOperationsAborted(), false); +operation._obj.release(); + +console.log('file-operation ok'); diff --git a/tests/runners/com/hwnd.mjs b/tests/runners/com/hwnd.mjs index 37ae9512..4afa0c1f 100644 --- a/tests/runners/com/hwnd.mjs +++ b/tests/runners/com/hwnd.mjs @@ -11,7 +11,8 @@ // napi `createTestHwnd()` export, which creates a hidden `WS_POPUP` // window in the Node process using the pre-registered `STATIC` class. -import { DynCom, roInitialize } from '../../../bindings/js/dist/index.js'; +import { DynCom } from '../../../bindings/js/dist/com.js'; +import { roInitialize } from '../../../bindings/js/dist/winrt.js'; roInitialize(1); diff --git a/tests/runners/com/pointer-reject-object.mjs b/tests/runners/com/pointer-reject-object.mjs index eb792794..30be9322 100644 --- a/tests/runners/com/pointer-reject-object.mjs +++ b/tests/runners/com/pointer-reject-object.mjs @@ -2,7 +2,7 @@ // DynWinRtValue inputs. Borrowing an owned COM object's raw pointer here would // make it indistinguishable from an owned raw pointer to adoptComPointer(), // which can double-release the original wrapper's COM object. -import { DynCom, WinGuid } from '../../../bindings/js/dist/index.js'; +import { DynCom, WinGuid } from '../../../bindings/js/dist/com.js'; // iidPointer() returns a DynWinRtValue — a representative value input. const someValue = DynCom.iidPointer(WinGuid.parse('a5caee9b-8708-49d1-8d36-67d25a8da00c')); diff --git a/tests/runners/com/shelllink-buffer.mjs b/tests/runners/com/shelllink-buffer.mjs index 2d2b1c65..87f882b4 100644 --- a/tests/runners/com/shelllink-buffer.mjs +++ b/tests/runners/com/shelllink-buffer.mjs @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; -import { DynCom } from '../../../bindings/js/dist/index.js'; +import { DynCom } from '../../../bindings/js/dist/com.js'; import { IShellLinkW, IID_IShellLinkW } from '../../e2e_generated/com/shell/IShellLinkW.js'; +import { IPersistFile, IID_IPersistFile } from '../../e2e_generated/com/shell/IPersistFile.js'; import { SHOW_WINDOW_CMD } from '../../e2e_generated/com/shell/SHOW_WINDOW_CMD.js'; const CLSID_SHELL_LINK = '00021401-0000-0000-c000-000000000046'; @@ -35,4 +36,9 @@ assert.equal(link.getHotkey(), expectedHotkey); link.setShowCmd(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED); assert.equal(link.getShowCmd(), SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED); +const persist = IPersistFile._fromNative(link._obj.cast(IID_IPersistFile)); +assert.equal(persist.getClassID().toLowerCase(), CLSID_SHELL_LINK); +persist._obj.release(); +link._obj.release(); + console.log('shelllink-buffer ok'); diff --git a/tests/runners/com/smtc.mjs b/tests/runners/com/smtc.mjs index 148aa896..3c95e8cf 100644 --- a/tests/runners/com/smtc.mjs +++ b/tests/runners/com/smtc.mjs @@ -15,7 +15,7 @@ // // Run: .\tests\e2e_test.ps1 -SkipBuild -Lang com -import { DynCom, DynComMethodSig, WinGuid } from '../../../bindings/js/dist/index.js'; +import { DynCom, DynComMethodSig, WinGuid } from '../../../bindings/js/dist/com.js'; // Classic-COM interop wrapper: gets the SMTC pointer from an HWND. import { ISystemMediaTransportControlsInterop } from '../../e2e_generated/com/interop/ISystemMediaTransportControlsInterop.js'; import { acquireHwndBigInt } from './hwnd.mjs'; diff --git a/tests/runners/com/wic-imaging-factory.mjs b/tests/runners/com/wic-imaging-factory.mjs new file mode 100644 index 00000000..4270b881 --- /dev/null +++ b/tests/runners/com/wic-imaging-factory.mjs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from 'node:assert/strict'; +import { DynCom } from '../../../bindings/js/dist/com.js'; +import { + IID_IWICImagingFactory, + IWICImagingFactory, +} from '../../e2e_generated/com/wic/IWICImagingFactory.js'; + +const CLSID_WIC_IMAGING_FACTORY = 'cacaf262-9370-4615-a13b-9f5539da4c0a'; + +DynCom.initialize(1); + +const factory = IWICImagingFactory._fromNative( + DynCom.coCreateInstance(CLSID_WIC_IMAGING_FACTORY, IID_IWICImagingFactory), +); +const stream = factory.createStream(); +assert.equal(stream.isNull(), false); +stream.release(); +factory._obj.release(); + +console.log('wic-imaging-factory ok'); diff --git a/tests/runners/ts_runner.ts b/tests/runners/ts_runner.ts index 37772f37..7ec1bd2e 100644 --- a/tests/runners/ts_runner.ts +++ b/tests/runners/ts_runner.ts @@ -8,7 +8,7 @@ * and executes checks against real WinRT APIs. * * Usage: - * npx tsx tests/runners/ts_runner.ts --specs tests/e2e_specs.json --generated tests/e2e_generated/ts --runtime bindings/js/dist/index.js [--output results.json] + * npx tsx tests/runners/ts_runner.ts --specs tests/e2e_specs.json --generated tests/e2e_generated/ts --runtime bindings/js/dist/winrt.js [--output results.json] */ import { strict as assert } from 'node:assert'; diff --git a/tools/dynwinrt-codegen/E2E_TEST.md b/tools/dynwinrt-codegen/E2E_TEST.md index 22abc429..0b1e7877 100644 --- a/tools/dynwinrt-codegen/E2E_TEST.md +++ b/tools/dynwinrt-codegen/E2E_TEST.md @@ -29,7 +29,7 @@ cargo build -p dynwinrt-codegen --release # Build the JS native binding cd bindings/js -npx napi build --no-const-enum --platform --release -o dist +npm run build cd ../.. ``` diff --git a/tools/dynwinrt-codegen/src/codegen/com/render.rs b/tools/dynwinrt-codegen/src/codegen/com/render.rs index d24a6dcb..6a13f4db 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/render.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/render.rs @@ -117,7 +117,7 @@ fn render_js(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { }; out.push_str(&format!( "import {{ {runtime_imports} }} from '{}';\n", - crate::codegen::project::get_import_name() + com_runtime_import_name() )); for en in enum_import_names(meta) { out.push_str(&format!("import {{ {} }} from './{}.js';\n", en, en)); @@ -620,7 +620,7 @@ fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { out.push_str(&format!( "import type {{ DynWinRtValue }} from '{}';\n", - crate::codegen::project::get_import_name() + com_runtime_import_name() )); } out.push('\n'); @@ -765,6 +765,15 @@ fn has_owned_pointer_output(meta: &ComInterfaceMeta) -> bool { }) } +fn com_runtime_import_name() -> String { + let import_name = crate::codegen::project::get_import_name(); + if import_name == "@microsoft/dynwinrt" { + format!("{import_name}/com") + } else { + import_name + } +} + // --------------------------------------------------------------------------- // Enum sibling files // --------------------------------------------------------------------------- @@ -839,6 +848,14 @@ mod tests { assert_eq!(camel_case("IOHandle"), "ioHandle"); } + #[test] + fn default_runtime_import_uses_com_subpath() { + let previous = crate::codegen::project::get_import_name(); + crate::codegen::project::set_import_name("@microsoft/dynwinrt"); + assert_eq!(com_runtime_import_name(), "@microsoft/dynwinrt/com"); + crate::codegen::project::set_import_name(&previous); + } + #[test] fn strip_hungarian_only_at_word_boundary() { assert_eq!(strip_hungarian("dwReserved"), "Reserved"); diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts index c86f4e33..26d67f73 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts @@ -1,5 +1,5 @@ // Generated by dynwinrt-codegen — do not edit -import type { DynWinRtValue } from '@microsoft/dynwinrt'; +import type { DynWinRtValue } from '@microsoft/dynwinrt/com'; /** Opaque Win32 handle. Pass either a raw pointer value as a `bigint` (safe for full 64-bit handle values) or a `number` (only for handles that fit in a JS safe integer, e.g. HWND with small window IDs). Do NOT pass a `Buffer` — `DynCom.pointer(Buffer)` uses the buffer's own address, not the bytes it contains; for an Electron `getNativeWindowHandle()` Buffer, read the handle value first (for example, `buf.readBigUInt64LE(0)`). */ export type HWND = bigint | number; diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js index 947f844a..c873fa72 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js @@ -1,5 +1,5 @@ // Generated by dynwinrt-codegen — do not edit -import { DynCom, DynComMethodSig, DynWinRtValue, WinGuid } from '@microsoft/dynwinrt'; +import { DynCom, DynComMethodSig, DynWinRtValue, WinGuid } from '@microsoft/dynwinrt/com'; export const IID_IDataTransferManagerInterop = WinGuid.parse('3a3dcd6c-3eab-43dc-bcde-45671ce800c8'); const IID_DataTransferManager_default = WinGuid.parse('a5caee9b-8708-49d1-8d36-67d25a8da00c'); diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js index 73d716b1..a8e3127c 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js @@ -1,5 +1,5 @@ // Generated by dynwinrt-codegen — do not edit -import { DynCom, DynComMethodSig, WinGuid } from '@microsoft/dynwinrt'; +import { DynCom, DynComMethodSig, WinGuid } from '@microsoft/dynwinrt/com'; import { TBPFLAG } from './TBPFLAG.js'; export const IID_ITaskbarList3 = WinGuid.parse('ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf'); diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs index c9827435..37c4b4cc 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -718,7 +718,7 @@ fn import_name_flag_is_honored_by_com_path() { } let previous = get_import_name(); - set_import_name("../dist/index.js"); + set_import_name("../dist/com.js"); let result = std::panic::catch_unwind(|| { let com_iface = com_metadata::parse_com_interface( @@ -738,8 +738,8 @@ fn import_name_flag_is_honored_by_com_path() { // Custom import must appear on the runtime import line... assert!( - out.js.contains("from '../dist/index.js'"), - "classic-COM .js must honor --import-name (expected `from '../dist/index.js'`):\n{}", + out.js.contains("from '../dist/com.js'"), + "classic-COM .js must honor --import-name (expected `from '../dist/com.js'`):\n{}", out.js ); // ...and the hardcoded default must NOT be present in the generated body. @@ -761,8 +761,8 @@ fn import_name_flag_is_honored_by_com_path() { .expect("codegen must succeed for classic-COM interface") }; assert!( - default_out.js.contains("from '@microsoft/dynwinrt'"), - "after restoring, default import name must be back to '@microsoft/dynwinrt':\n{}", + default_out.js.contains("from '@microsoft/dynwinrt/com'"), + "after restoring, default import must use '@microsoft/dynwinrt/com':\n{}", default_out.js ); } @@ -782,7 +782,7 @@ fn import_name_flag_is_honored_by_interop_wrapper() { } let previous = get_import_name(); - set_import_name("../dist/index.js"); + set_import_name("../dist/com.js"); let result = std::panic::catch_unwind(|| { let com_iface = com_metadata::parse_com_interface( @@ -800,7 +800,7 @@ fn import_name_flag_is_honored_by_interop_wrapper() { // The interop .js itself must honor the flag. assert!( - out.js.contains("from '../dist/index.js'"), + out.js.contains("from '../dist/com.js'"), "interop .js must honor --import-name:\n{}", out.js ); @@ -811,7 +811,7 @@ fn import_name_flag_is_honored_by_interop_wrapper() { ); assert!( - out.dts.contains("from '../dist/index.js'"), + out.dts.contains("from '../dist/com.js'"), "interop .d.ts must honor --import-name:\n{}", out.dts ); From 951d6f61ea1f57df21ffb722d1ba953f894e9475 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Wed, 29 Jul 2026 11:59:09 +0800 Subject: [PATCH 24/28] Document Classic COM support and demand Quantify the Windows.Win32 metadata surface and public-code frequency sample, document supported and unsupported ABI shapes, summarize implemented fixes, and define ownership and future type-system priorities. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 41a826d4-b04a-4932-b1b8-31173fc5ab40 --- README.md | 4 +- docs/classic-com-support.md | 585 ++++++++++++++++++++++++++++++++++++ 2 files changed, 588 insertions(+), 1 deletion(-) create mode 100644 docs/classic-com-support.md diff --git a/README.md b/README.md index 8d022ac4..516ee180 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,9 @@ console.log(uri.host); // "example.com" Classic COM bindings import their runtime API from the separate `@microsoft/dynwinrt/com` subpath. It is part of the same npm package; the -package root remains the WinRT-only API. +package root remains the WinRT-only API. See +[Classic COM support](docs/classic-com-support.md) for the supported ABI, +common-interface test matrix, unsupported native types, and ownership rules. Generated bindings project unambiguous public WinRT activation metadata as JavaScript constructors, including overloads such as `new Uri(base, relative)`. Existing diff --git a/docs/classic-com-support.md b/docs/classic-com-support.md new file mode 100644 index 00000000..dd8e1f5f --- /dev/null +++ b/docs/classic-com-support.md @@ -0,0 +1,585 @@ +# Classic COM support + +`dynwinrt` supports a deliberately limited subset of Classic COM. It is not a +general Automation or native Win32 projection. + +The design keeps the existing WinRT API separate: + +```js +import { DynWinRtType, DynWinRtValue } from '@microsoft/dynwinrt'; +import { DynCom, DynComMethodSig } from '@microsoft/dynwinrt/com'; +``` + +Both entrypoints use the same native N-API binary and private libffi call +machinery. Classic COM metadata, generated wrappers, ownership rules, and +public APIs remain separate from the WinRT projection. + +## Size of Windows.Win32.winmd + +The counts below are exact for +`Microsoft.Windows.SDK.Win32Metadata` **69.0.7-preview** +`Windows.Win32.winmd`, read with `windows-metadata` 0.59.0. `` is +excluded. + +There is no single canonical definition of an "API" in ECMA-335 metadata. For +callable entries, the most useful count is: + +```text +17,760 flat P/Invoke functions ++46,233 declared interface methods +=63,993 callable entries +``` + +| Metadata entity | Count | +|---|---:| +| Namespaces | 324 | +| Type definitions | 35,055 | +| Flat P/Invoke functions | 17,760 | +| Interfaces | 7,971 | +| `IUnknown`-rooted interfaces | 7,878 | +| `IInspectable`-rooted interfaces | 43 | +| Other/no-root interfaces | 50 | +| Declared interface methods | 46,233 | +| Structs | 15,944 | +| Enums | 7,784 | +| Enum members | 67,587 | +| Delegates | 3,002 | +| Classes/API containers | 316 | +| Metadata attributes | 38 | +| Non-enum literal constants | 88,931 | + +These numbers describe the metadata, not dynwinrt support: + +- The current Classic COM work targets interface methods. It does **not** + project the 17,760 flat DLL exports. +- An interface declaration may describe a caller-implemented callback rather + than an OS object that can be activated and called. +- The interface count includes graphics, media, WMI, Automation, Shell, and + other families whose native types are not all supported. +- Methods inherited by a derived interface are counted once where they are + declared, not repeated for every derived interface. + +The largest flat-function modules in this metadata version include +`KERNEL32.dll` (1,407), `USER32.dll` (767), `gdiplus.dll` (629), +`ADVAPI32.dll` (619), `GDI32.dll` (431), `OLEAUT32.dll` (405), +`OLE32.dll` (273), and `SHELL32.dll` (244). + +## Type-system problem map + +The following counts come from all 46,233 declared interface methods, not only +the 30-interface frequency sample. Nested pointee types are included in type +occurrence counts. + +| Signature characteristic | Count | +|---|---:| +| Parameters | 79,181 | +| Input parameters | 47,289 | +| Output parameters | 28,058 | +| In/out parameters | 3,834 | +| Optional parameters | 5,362 | +| `HRESULT` returns | 44,309 | +| Direct `void` returns | 1,018 | +| Direct value returns | 906 | +| Mutable pointer occurrences, depth 1 | 36,321 | +| Mutable pointer occurrences, depth 2 | 1,492 | +| Mutable pointer occurrences, depth 3 | 8 | +| Parameters with `NativeArrayInfo` | 2,973 | +| Parameters with `FreeWith` metadata | **13** | +| Unique referenced interfaces | 2,875 | +| Unique referenced structs | 1,491 | +| Unique referenced enums | 1,739 | +| Unique referenced delegates | 71 | +| BSTR occurrences | 7,697 | +| VARIANT-family occurrences | 3,586 | +| SAFEARRAY occurrences | 238 | +| PROPVARIANT occurrences | 156 | +| PROPERTYKEY occurrences | 138 | +| Representative audio-format struct occurrences | 69 | +| FORMATETC/STGMEDIUM occurrences | 27 | + +The implementation should therefore be planned around the following problems, +not around one-off interface fixes. + +### 1. Native layout engine + +**Problem:** A named native type is not enough to call a method. The ABI needs +its exact size, alignment, packing, field offsets, nested layout, architecture +variation, and whether it is a struct or union. + +This is the largest general blocker: 1,491 distinct structs appear in interface +signatures. It affects Direct3D, DXGI, Shell, drag-and-drop, streams, WMI, +audio, and the Property System. + +Required model: + +- sequential and explicit layout; +- nested structs and unions; +- fixed arrays and bitfields; +- x86/x64/ARM64 size and alignment; +- by-value, pointer-to, out, and in/out forms; and +- safe construction and field access in each language binding. + +### 2. Pointer depth and pointee semantics + +**Problem:** `T*`, `T**`, and `T***` are not interchangeable. A pointer may +mean a borrowed object, optional value, caller storage, callee allocation, +array, null-terminated string, interface reference, or opaque token. + +The metadata contains 37,821 pointer occurrences, including 1,500 with depth +greater than one. + +Required model: + +- pointee type and pointer depth; +- const versus writable storage; +- nullable versus required; +- interface pointer versus data pointer; +- input, output, and replacement/in-out semantics; and +- storage size before a native call is allowed. + +### 3. Counted buffers and native arrays + +**Problem:** A pointer plus count is one logical value. Allocating one scalar +for a writable `BYTE*` is a memory overwrite. + +There are 2,973 `NativeArrayInfo` parameters. The projection needs: + +- which parameter supplies the count; +- whether the count is bytes or elements; +- capacity versus actual returned length; +- caller-allocated, callee-allocated, and two-call sizing patterns; +- string termination and encoding; and +- partial writes and failure cleanup. + +Recognized UTF-16 output-buffer shapes are supported today. General writable +native arrays remain fail closed. + +### 4. Ownership and allocator contracts + +**Problem:** The type and pointer depth do not identify who owns memory or how +to release it. + +Only 13 parameters in this metadata carry `FreeWith`, despite thousands of +owned-output contracts. Metadata alone is therefore insufficient. + +The ABI/projection needs explicit ownership such as: + +- borrowed; +- COM `AddRef`/`Release`; +- BSTR / `SysFreeString`; +- `CoTaskMemFree`; +- `LocalFree`; +- allocator/interface-specific release; +- Win32 resource-specific cleanup; and +- custom or unknown ownership, which must fail closed. + +### 5. Discriminated unions: Automation and Property System + +**Problem:** `VARIANT` and `PROPVARIANT` combine a type tag, a union payload, +and nested ownership. Treating either as an opaque pointer is not a complete or +safe projection. + +Required support: + +- scalar and interface alternatives; +- BSTR and other owned strings; +- nested VARIANT values; +- SAFEARRAY and vector alternatives; +- `VariantInit`/`VariantClear` and `PropVariantClear`; +- language conversion and range checking; and +- DISPPARAMS argument order, named arguments, and EXCEPINFO. + +This unlocks `IDispatch`, XML Automation, Task Scheduler, `IPropertyStore`, and +many scripting/management APIs. + +### 6. SAFEARRAY + +**Problem:** SAFEARRAY is a descriptor, not a pointer to a flat JavaScript +array. It carries rank, bounds, element type, locks, ownership, and potentially +non-blittable elements. + +Required support includes multidimensional bounds, lower bounds, element +cleanup, interface/BSTR/VARIANT elements, and safe lock/unlock behavior. + +### 7. Interface in/out and callback implementations + +**Problem:** Replacing `IFoo*` through `IFoo**` requires precise release and +AddRef behavior. Event APIs additionally require dynwinrt to implement an +arbitrary caller-defined COM interface, not merely invoke one. + +Required support: + +- release of the old in/out reference when the contract requires it; +- ownership of the replacement reference; +- generated sink vtables; +- QueryInterface identity and reference counting for implemented objects; +- callback threading/apartment dispatch; and +- conversion of callback failures to HRESULT. + +### 8. Semantic HRESULT values + +**Problem:** Most HRESULTs are throw-or-success, but methods such as +`IPersistFile::IsDirty` use `S_OK` versus `S_FALSE` as their actual result. +Discarding every successful HRESULT loses information. + +The projection needs an explicit PreserveSig/semantic-HRESULT classification +instead of globally treating every non-negative HRESULT as `void`. + +### 9. Apartment affinity and marshaling + +**Problem:** A valid COM reference is not necessarily callable from every +thread. STA objects require the owning apartment or a marshaled proxy. + +Required support includes: + +- tracking the apartment where a value was acquired; +- preventing unsafe cross-thread calls; +- agile-object detection; +- Global Interface Table or COM marshaling integration; and +- deterministic callback dispatch to the correct apartment. + +### 10. Acquisition and flat-function boundary + +**Problem:** many common interfaces are not created with `CoCreateInstance`. +Examples include `CoGetMalloc`, `CreateBindCtx`, `D2D1CreateFactory`, +`DWriteCreateFactory`, `D3D11CreateDevice`, and shell helper functions. + +The current Classic COM layer can invoke an acquired interface, but a separate +flat-Win32 layer is needed for the 17,760 DLL exports, their calling +conventions, `GetLastError`, callbacks, and handle cleanup. + +### Recommended implementation order + +1. General native struct/union layout. +2. Pointer-depth plus counted-buffer contracts. +3. Explicit allocator/ownership metadata. +4. VARIANT/PROPVARIANT and semantic HRESULT handling. +5. SAFEARRAY. +6. Arbitrary COM sink/interface implementation. +7. Apartment-aware marshaling. +8. Separate flat-Win32 acquisition/invocation layer. + +## What the current PR handles + +The PR establishes a safe Classic COM subset and rejects the rest. It should +not be described as solving every problem in the map above. + +### Implemented + +| Problem | Current implementation | +|---|---| +| WinRT/Classic COM separation | Separate COM metadata/codegen path and `@microsoft/dynwinrt/com` public entrypoint. The WinRT generator and root runtime API remain unchanged. | +| Interface root and vtable layout | Distinguishes `IUnknown` slot 3 from `IInspectable` slot 6 and walks inherited Classic COM interfaces before assigning slots. | +| Method return conventions | Supports normal HRESULT methods plus native direct scalar, direct pointer at the runtime layer, and direct `void` returns. | +| Basic parameter direction | Supports input, output, and scalar in/out parameters without reducing in/out to out-only. | +| Primitive ABI types | Signed/unsigned integers, floats, BOOL, HRESULT, GUID, enums, and `char16`. | +| Pointer-sized values | `ISize`/`USize` select the correct x86/x64 ABI width and JavaScript uses `bigint`. | +| GUID ABI | Full 16-byte GUID output storage plus GUID value and REFIID/REFGUID pointer patterns. | +| Unsigned enum values | COM-local enum metadata preserves unsigned values, including 32-bit high-bit flags and 64-bit `bigint` literals. | +| Standard COM references | `CoCreateInstance`, QueryInterface, and typed interface outputs carry an owned `+1` reference and release automatically. | +| Ownership provenance | Borrowed numeric/TypedArray pointers cannot be re-adopted as a second COM owner. Native owned outputs are consumed once. | +| Backing-storage lifetime | Buffer/TypedArray owners are retained and detached ArrayBuffers are rejected before native use. | +| Common string ownership | Scalar BSTR output uses `SysFreeString`; supported `PWSTR`/`PSTR` allocations use `CoTaskMemFree`. | +| Common interop pattern | Supports HWND + REFIID + `void**` bridges and adopts the returned interface reference. | +| Explicit COM initialization | Activation no longer silently chooses MTA; callers select STA or MTA with `DynCom.initialize()`. | +| Fail-closed generation | Unsupported structs, arrays, pointer outputs, ownership, and in/out shapes stop generation with a targeted error. | +| Consumable output | COM-only generation emits index declarations and package exports and preserves them across incremental generation. | + +### Partially implemented + +| Problem family | Supported subset | Remaining gap | +|---|---|---| +| Native pointers | Pointer width, depth preservation, borrowed pointers, handles, REFIID, and known interface outputs | General nullable/required semantics, arbitrary pointee storage, and all allocator contracts | +| Counted buffers | Recognized caller-owned UTF-16 output buffers and input Buffer pointers | General byte/element output arrays, two-call sizing, actual-length returns, ANSI output decoding | +| Native layout | Primitives, GUID, enum, handle-shaped typedefs, and manually described runtime structs | General metadata-driven struct/union/packing/bitfield layout | +| Allocator ownership | COM Release, BSTR, CoTaskMem, boxed GUID, retained JS buffers | LocalFree, custom allocators, allocator interfaces, unknown ownership | +| Interface pointers | Typed input/output interfaces, QueryInterface, dynamic IID output | Interface in/out replacement and arbitrary implemented sink interfaces | +| Apartments | Explicit initialization and same-thread invocation | Cross-apartment marshaling, GIT/agility handling, callback dispatch | +| Activation | In-process `CoCreateInstance` | `CoGetClassObject`, aggregation, arbitrary CLSCTX, and non-CoCreate factory functions | +| Direct pointer returns | Runtime signature supports them | Metadata codegen does not yet preserve raw-pointer direct-return semantics, so `IMalloc` generation fails closed | + +### Not implemented + +- general struct/union native layout; +- VARIANT, VARIANTARG, DISPPARAMS, and EXCEPINFO; +- PROPVARIANT and the Property System value model; +- SAFEARRAY; +- FORMATETC and STGMEDIUM; +- arbitrary COM event/callback sink generation; +- semantic `S_OK`/`S_FALSE` HRESULT projection; +- cross-thread/apartment marshaling; and +- the general flat-Win32 DLL-export and handle-cleanup layer. + +## Supported ABI surface + +| Capability | Status | Notes | +|---|---|---| +| `IUnknown` and `IInspectable` roots | Supported | User methods begin at vtable slot 3 or 6 respectively. Full inherited Classic COM slot numbering is preserved. | +| `HRESULT` methods | Supported | Failed HRESULTs become errors. | +| Native `void` returns | Supported | Used by interfaces such as `IMalloc`. | +| Direct scalar returns | Supported | Includes signed/unsigned integers, floating point values, and enums. | +| Direct pointer returns | Runtime supported; codegen partial | The runtime can describe a pointer return explicitly. Metadata codegen currently fails closed for interfaces such as `IMalloc` because it does not preserve the raw-pointer return kind. | +| `[in]`, `[out]`, and scalar `[in, out]` parameters | Supported | Unsupported composite in/out types fail generation. | +| Primitive integer and floating-point types | Supported | `i8` through `u64`, `f32`, `f64`, `BOOL`, and `HRESULT`. | +| `ISize` / `USize` | Supported | Projected with the target pointer width; verified by an i686 compile check. | +| GUID values and `REFIID`/`REFGUID` pointers | Supported | GUID out storage uses the full 16-byte layout. | +| Signed and unsigned enums/flags | Supported | Values up to unsigned 64-bit are preserved; 64-bit JavaScript values use `bigint`. | +| Typed interface parameters and outputs | Supported | Interface outputs carry an owned COM reference. | +| Opaque pointers and handle-shaped typedefs | Supported with limits | They are pointer values, not COM objects. Cleanup remains type-specific. | +| NUL-terminated string pointer inputs | Supported | Callers pass a NUL-terminated `Buffer` or a borrowed numeric pointer. | +| Caller-owned UTF-16 output buffers | Supported for recognized shapes | The generator allocates and decodes the buffer when metadata identifies the count parameter. | +| Callee-allocated `PWSTR` / `PSTR` outputs | Supported | Generated code decodes and frees `CoTaskMem` storage. | +| Scalar `[out] BSTR*` | Supported | Generated code converts the BSTR and releases it with `SysFreeString`. | +| Explicit apartment initialization | Supported | `DynCom.initialize()` never silently chooses an apartment for the caller. | + +The runtime can manually describe some ABI shapes that the generator rejects. +For example, a carefully defined native struct can be called from Rust, but the +generator does not emit a struct until its native layout is known to be +correct. + +## Unsupported types and shapes + +The generator fails closed for unsupported signatures instead of emitting a +plausible but memory-unsafe binding. + +The native type rows below come from real signatures in +`Windows.Win32.winmd`, including the 30-interface survey, plus the exact +fail-closed diagnostics produced by the current generator. The policy rows +describe known runtime/public-API boundaries. This is not an exhaustive scan +of every type in the 24 MB metadata file. + +| Type or shape | Affected common APIs | Why it is unsupported | Basis | +|---|---|---|---| +| `VARIANT` / `VARIANTARG` | `IDispatch::Invoke`, Automation APIs | Requires a discriminated union with ownership rules for BSTR, interfaces, arrays, decimals, and nested values. | Win32 winmd signature | +| `DISPPARAMS` / `EXCEPINFO` | `IDispatch::Invoke` | Contains VARIANT arrays, BSTR fields, and nested pointer ownership. | Win32 winmd signature | +| `PROPVARIANT` | `IPropertyStore`, Windows Property System | Larger discriminated union with vector, string, stream, and interface ownership. | Win32 winmd signature | +| `PROPERTYKEY` and arbitrary native structs | `IPropertyStore::GetAt` | Native struct layout, alignment, and architecture must be modeled explicitly. | Win32 winmd + codegen diagnostic | +| `SAFEARRAY` | Automation and Office-style COM APIs | Requires rank, bounds, element type, locking, and element cleanup semantics. | Win32 winmd Automation signatures | +| `FORMATETC` / `STGMEDIUM` | `IDataObject`, clipboard, drag-and-drop | `STGMEDIUM` is a union of handles and interfaces with type-specific release behavior. | Win32 winmd + codegen diagnostic | +| Arbitrary unions, bitfields, and nested pointer-rich structs | `D3D11_COUNTER_INFO`, `STATSTG`, `STRRET`, `POINTL`, `BIND_OPTS`, audio/media formats | The current generator has no general native C layout engine. | Win32 winmd + codegen diagnostics | +| Writable caller-sized native arrays | `IDispatch::GetIDsOfNames`, counted byte/element output buffers | A scalar pointee is not sufficient storage. These are rejected unless a supported string-buffer projection applies. | Win32 winmd `NativeArrayInfo` + codegen diagnostic | +| `BSTR**` arrays and BSTR in/out arrays | Automation collection APIs | Each element has independent allocation and release semantics. | Win32 winmd signature + ownership analysis | +| Caller-owned ANSI output buffers | `PSTR` output-buffer APIs | Safe sizing and decoding are not yet projected. | Win32 winmd signature + renderer limitation | +| Untyped output pointers without allocator/ownership | `IDXGIFactory::GetPrivateData`, `IAudioClient::IsFormatSupported` | The runtime cannot infer whether the result is borrowed, COM-owned, `CoTaskMem`, or another allocator. | Win32 winmd + codegen diagnostics | +| Interface `[in, out]` ownership | `IWbemServices::OpenNamespace` | Replacing an existing interface pointer requires explicit release/AddRef transfer semantics. | Win32 winmd + codegen diagnostic | +| Arbitrary COM sink/interface implementation | Connection points and event sinks | `Advise` requires implementing a caller-defined COM interface, not only invoking one. | Runtime/public-API boundary | +| COM aggregation | `IClassFactory::CreateInstance` with `pUnkOuter` | The public activation helper always creates a non-aggregated in-process object. | Runtime/public-API boundary | +| General out-of-process activation controls | Custom `CLSCTX` scenarios | `DynCom.coCreateInstance()` currently uses `CLSCTX_INPROC_SERVER`. | Runtime/public-API boundary | +| Flat Win32 DLL exports | `CreateFile`, registry functions, GDI, etc. | These are not COM interfaces and need a separate DLL-export/handle model. | Architecture boundary | + +Consequently, `IDispatch`, `IPropertyStore`, and `IDataObject` are important +and widely encountered interfaces, but they are not currently supported as +complete generated bindings. + +## Public-code frequency snapshot + +There is no authoritative Microsoft ranking of COM interface usage. The table +below is a reproducible demand proxy based on public GitHub code, not runtime +telemetry. + +The snapshot was collected on **2026-07-29** with GitHub code search: + +```text + extension:cpp + NOT path:test + NOT path:tests + NOT path:third_party + NOT path:vendor + NOT path:external + NOT path:generated +``` + +The survey selected 30 representative desktop COM interfaces across COM +infrastructure, Shell, OLE, graphics, audio, WMI, XML, and WebView2. +`IID_IDispatch` and `IID_IStream` were searched instead of their bare names to +reduce collisions with unrelated classes and C++ `std::istream`. + +Two metrics are reported: + +- **`.cpp` hits** is GitHub's total matching-file count after the best-effort + path exclusions above. +- **Repos / first 100** is the number of distinct repositories represented in + the first 100 matching files. It prevents one large repository from being + mistaken for broad adoption, but it is not a count of every matching + repository. + +Vendored code can still appear under other directory names, search ranking and +repository contents change over time, and interfaces used through wrappers may +not mention the native symbol. Treat the numbers as relative prevalence only. + +Each candidate was then checked against +`Microsoft.Windows.SDK.Win32Metadata` **69.0.7-preview** +`Windows.Win32.winmd`, and the current generator was run with `--dry-run` +against the resolved namespace. + +| Rank | Interface/search token | `.cpp` hits | Repos / first 100 | In Win32 winmd | Current codegen | +|---:|---|---:|---:|---|---| +| 1 | `ID3D11Device` | 27,552 | 87 | Yes | Fail closed: native `D3D11_COUNTER_INFO` layout | +| 2 | `IDXGIFactory` | 17,432 | 83 | Yes | Fail closed: untyped output ownership | +| 3 | `IDataObject` | 10,648 | 44 | Yes | Fail closed: `STGMEDIUM`/union layout | +| 4 | `IMalloc` | 10,624 | 56 | Yes | Fail closed: direct raw-pointer return mapping; runtime tested | +| 5 | `IClassFactory` | 6,712 | 70 | Yes | Generates; acquisition helper and live test still needed | +| 6 | `IDispatch` via `IID_IDispatch` | 6,408 | 46 | Yes | Fail closed: counted arrays, VARIANT-family ABI | +| 7 | `IPersistFile` | 5,996 | 97 | Yes | Generates and live-tested | +| 8 | `IConnectionPoint` | 5,832 | 51 | Yes | Generates; implementing event sinks is not supported | +| 9 | `IWbemServices` | 5,680 | 76 | Yes | Fail closed: interface in/out ownership | +| 10 | `IWICImagingFactory` | 4,536 | 83 | Yes | Generates and live-tested | +| 11 | `IDropTarget` | 4,368 | 57 | Yes | Fail closed: native `POINTL` layout | +| 12 | `IShellFolder` | 4,056 | 33 | Yes | Fail closed: native `STRRET` union layout | +| 13 | `IFileDialog` | 4,048 | 98 | Yes | Generates; inherited methods tested through `IFileOpenDialog` | +| 14 | `IXMLDOMDocument` | 3,784 | 46 | Yes | Fail closed: inherits Automation/VARIANT ABI | +| 15 | `ID2D1Factory` | 3,752 | 92 | Yes | Generates; requires flat factory acquisition and native input structs | +| 16 | `IDWriteFactory` | 3,712 | 76 | Yes | Generates; requires flat factory acquisition | +| 17 | `IStream` via `IID_IStream` | 3,560 | 41 | Yes | Fail closed on `STATSTG`; safe runtime subset is live-tested | +| 18 | `IPropertyStore` | 3,400 | 77 | Yes | Fail closed: `PROPERTYKEY` and `PROPVARIANT` | +| 19 | `IShellItem` | 3,028 | 76 | Yes | Generates; acquisition/live test still needed | +| 20 | `IMMDeviceEnumerator` | 2,932 | 83 | Yes | Generates; live result depends on audio services/devices | +| 21 | `IBindCtx` | 2,660 | 42 | Yes | Fail closed: native `BIND_OPTS` layout | +| 22 | `IFileOpenDialog` | 2,536 | 92 | Yes | Generates and live-tested without showing UI | +| 23 | `IRunningObjectTable` | 2,532 | 50 | Yes | Fail closed: native `FILETIME` layout | +| 24 | `IAudioClient` | 2,500 | 82 | Yes | Fail closed: format pointer/output ownership | +| 25 | `IShellLinkW` | 2,128 | 67 | Yes | Generates and live-tested | +| 26 | `ITaskbarList3` | 1,672 | 87 | Yes | Generates and live-tested | +| 27 | `ICoreWebView2` | 1,608 | 35 | **No** | Defined in WebView2 metadata, not Windows.Win32.winmd | +| 28 | `IFileSaveDialog` | 1,188 | 96 | Yes | Generates; live test still needed | +| 29 | `IFileOperation` | 768 | 79 | Yes | Generates and live-tested | +| 30 | `ITaskService` | 461 | 72 | Yes | Fail closed: inherits Automation/VARIANT ABI | + +### What the snapshot shows + +- **29 of 30** candidates are defined as `IUnknown`-rooted interfaces in + Windows.Win32.winmd. `ICoreWebView2` is the only external-metadata case. +- **14 of 29** Win32-metadata candidates pass complete codegen validation. + **15 of 29** fail closed on an unsupported ABI or ownership shape. +- Among the **top 10** by `.cpp` hits, only `IClassFactory`, + `IPersistFile`, `IConnectionPoint`, and `IWICImagingFactory` pass complete + codegen. `IMalloc` has a tested runtime path but not a complete generated + interface. +- The largest unsupported demand clusters are: + - native structs/unions and layout (`D3D11`, `IDataObject`, Shell, streams); + - Automation types (`IDispatch`, XML, Task Scheduler); + - explicit output ownership (`DXGI`, audio); + - interface in/out semantics (WMI); and + - Property System types (`PROPERTYKEY`, `PROPVARIANT`). +- Seven frequency-survey candidates have generated live coverage: + `IPersistFile`, `IWICImagingFactory`, `IFileDialog` through + `IFileOpenDialog`, `IFileOpenDialog`, `IShellLinkW`, `ITaskbarList3`, and + `IFileOperation`. `IMalloc` and `IStream` add runtime-only live coverage. + +This means the current ten-interface suite provides useful ABI breadth, but it +does **not** cover every high-frequency interface. In particular, +`IDataObject`, `IDispatch`, `IPropertyStore`, graphics interfaces, WMI, and +audio remain material gaps. + +## Engineering priority map + +The frequency snapshot is only one input. Test priority also considers stock +Windows availability, deterministic behavior, whether an API requires UI or +hardware, and whether it adds a distinct ABI shape. + +| Interface | Typical use | Current status | +|---|---|---| +| `IStream` | OLE streams, imaging, shell, serialization | Core live test covers counted buffers, seek, and interface output. | +| `IMalloc` | COM task allocator | Core live test covers direct pointer, pointer-sized, scalar, and void returns. | +| `IPersistFile` | Loading and saving persistent COM objects | Core and Node tests query it from `IShellLinkW` and verify `GetClassID`. | +| `IShellLinkW` | Shortcut creation and inspection | Core and Node tests cover strings, `u16`, enums, and scalar outputs. | +| `IFileOpenDialog` | Desktop file selection | Node test covers activation and option round-trip without showing UI. | +| `IFileOperation` | Shell copy/move/delete operations | Node test covers activation, unsigned flags, and state without modifying files. | +| `IWICImagingFactory` | Windows Imaging Component | Node test activates WIC and creates an interface-valued stream. | +| `ITaskbarList3` | Taskbar progress and window state | Node test covers inherited vtable slots, HWND values, BOOL, enums, and `u64`. | +| `IDataTransferManagerInterop` | HWND-to-WinRT data-transfer bridge | Core and Node tests cover `IUnknown`-rooted interop and interface output. | +| `ISystemMediaTransportControlsInterop` | HWND-to-WinRT media controls | Node test covers `IInspectable`-rooted interop and use of the returned WinRT object. | +| `IClassFactory` | Low-level COM activation | High-value next test; needs a public `CoGetClassObject` acquisition path. | +| `IBindCtx` / `IRunningObjectTable` | Monikers and object binding | High-value next test; needs acquisition helpers and validated native structs. | +| `ICreateErrorInfo` / `IErrorInfo` | COM rich error information | Good next test for GUID, wide strings, BSTR, and thread-local error state. | +| `IMMDeviceEnumerator` | Audio endpoint discovery | Generates today, but live behavior depends on available audio endpoints. | +| `IAudioClient` | Low-level audio streaming | Fails closed because its format and output-pointer shapes are not fully modeled. | +| `IDispatch` | Automation and scripting | Unsupported until VARIANT-family marshaling exists. | +| `IPropertyStore` | Shell/property metadata | Unsupported until PROPERTYKEY and PROPVARIANT are modeled. | +| `IDataObject` | Clipboard and drag-and-drop | Unsupported until FORMATETC and STGMEDIUM are modeled. | + +## Automated coverage + +Ten unique Classic COM interfaces are currently exercised. +Core live tests are in +[`crates/dynwinrt/src/com.rs`](../crates/dynwinrt/src/com.rs). The nine Node +runners are in [`tests/runners/com`](../tests/runners/com) and are generated +and executed by [`tests/e2e_test.ps1`](../tests/e2e_test.ps1). + +| Interface | Test layer | Representative coverage | +|---|---|---| +| `IShellLinkW` | Core + Node E2E | Activation, wide strings, hotkeys, show command, and deterministic release. | +| `IPersistFile` | Core + Node E2E | `QueryInterface`, owned returned reference, and GUID output. | +| `IMalloc` | Core | Direct pointer return, `usize` return, direct `i32`, direct `void`, allocation cleanup. | +| `IStream` | Core | Counted byte buffer, `u32` output, `i64` seek, `u64` output, and `IStream**` clone. | +| `ITaskbarList3` | Node E2E | Inherited slots, HWND, BOOL, enum, and `u64`. | +| `IFileOperation` | Node E2E | Coclass activation, unsigned flags, and state query. | +| `IFileOpenDialog` | Node E2E | STA activation and get/set options without user interaction. | +| `IWICImagingFactory` | Node E2E | Explicit CLSID activation and typed interface output. | +| `IDataTransferManagerInterop` | Core + Node E2E | `IUnknown` base, HWND, REFIID, and WinRT interface output. | +| `ISystemMediaTransportControlsInterop` | Node E2E | `IInspectable` base and meaningful use of the returned WinRT projection. | + +Additional regression tests cover: + +- rejection of duplicate ownership through exported pointer bits; +- detached TypedArray backing storage; +- BSTR and `CoTaskMem` cleanup; +- x86 pointer width; +- unsupported native arrays and native struct layouts; +- required parameter preservation; +- unsigned enum values; +- COM-only package generation; and +- separation of `@microsoft/dynwinrt` from `@microsoft/dynwinrt/com`. + +Run the live Classic COM suite with: + +```powershell +$env:DYNWINRT_WIN32_WINMD = "C:\path\to\Windows.Win32.winmd" +.\tests\e2e_test.ps1 -SkipBuild -Lang com +``` + +## Reference counting and ownership + +COM interface references and Win32 handles must not be treated the same. + +| Value source | Ownership in dynwinrt | Cleanup | +|---|---|---| +| `CoCreateInstance` result | Owned `+1` COM reference | Automatic `Release` on `DynWinRtValue` drop/GC, or explicit `release()`. | +| `QueryInterface` / `cast()` result | Owned `+1` COM reference | Automatic `Release`, independently of the source wrapper. | +| Typed interface out parameter | Owned `+1` COM reference from the callee | Automatic `Release`. | +| Interface passed as `[in]` | Borrowed for the duration of the call | No ownership transfer unless the callee explicitly retains it with `AddRef`. | +| Numeric raw pointer | Borrowed | Never automatically released or freed. | +| Buffer/TypedArray pointer | Borrowed and owner-backed | Backing storage is retained and revalidated; it cannot be adopted as a COM owner. | +| `adoptComPointer()` input | Must be a native output carrying an existing `+1` reference | Ownership transfers to the returned wrapper. | +| Callee-allocated `CoTaskMem` string | Owned allocation | Generated conversion frees it with `CoTaskMemFree`. | +| Scalar BSTR output | Owned allocation | Generated conversion frees it with `SysFreeString`. | +| `HANDLE`, `HWND`, `HBITMAP`, etc. | Win32 resource value, not a COM reference | Use the resource-specific API such as `CloseHandle`, `DestroyWindow`, or `DeleteObject` when required. | + +The JavaScript ownership provenance checks intentionally prevent turning a +borrowed numeric or TypedArray pointer into a second owner. This avoids two +wrappers releasing the same COM reference. + +## Test selection guidance + +Prefer new CI tests that: + +1. use stock Windows components; +2. require no network, optional software, or user input; +3. avoid persistent filesystem or system-state changes; +4. assert meaningful results rather than activation alone; +5. add a distinct ABI or ownership shape; and +6. clean up every COM reference, native allocation, and Win32 resource. + +Interfaces that require Office, deprecated Internet Explorer automation, +active drag-and-drop, a populated clipboard, audio hardware, or an Explorer +desktop should remain optional or local-only tests. + +## Related Microsoft documentation + +- [Rules for managing COM reference counts](https://learn.microsoft.com/windows/win32/com/rules-for-managing-reference-counts) +- [IUnknown::QueryInterface](https://learn.microsoft.com/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(q)) +- [IMalloc](https://learn.microsoft.com/windows/win32/api/objidl/nn-objidl-imalloc) +- [IStream](https://learn.microsoft.com/windows/win32/api/objidl/nn-objidl-istream) +- [IPersistFile](https://learn.microsoft.com/windows/win32/api/objidl/nn-objidl-ipersistfile) +- [IFileOperation](https://learn.microsoft.com/windows/win32/api/shobjidl_core/nn-shobjidl_core-ifileoperation) +- [Windows Imaging Component overview](https://learn.microsoft.com/windows/win32/wic/-wic-about-windows-imaging-codec) From bbecf470894e37b441bb5c8d2550301e4aaaa0a1 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Wed, 29 Jul 2026 14:33:05 +0800 Subject: [PATCH 25/28] Document Classic COM ABI development rules Define type-and-contract-first modeling, runtime/codegen/renderer responsibilities, Buffer and ownership semantics, fail-closed requirements, validation expectations, and the invariant that Classic COM changes must not alter WinRT behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 41a826d4-b04a-4932-b1b8-31173fc5ab40 --- .github/copilot-instructions.md | 11 ++ .github/skills/classic-com-abi/SKILL.md | 229 ++++++++++++++++++++++++ docs/classic-com-support.md | 7 + 3 files changed, 247 insertions(+) create mode 100644 .github/skills/classic-com-abi/SKILL.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7ab9f476..20048eb5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -95,6 +95,17 @@ These APIs are available on any Windows 10/11 machine without WinAppSDK: - **Method invocation** returns a single `WinRTValue` (not a list) in Python binding - **Generated code** uses relative imports (`from .module import Class`) — must be in a Python package +### Classic COM implementation rule + +For Classic COM, Windows.Win32 metadata, pointer, handle, ownership, or native +ABI changes, follow +[`classic-com-abi`](skills/classic-com-abi/SKILL.md). Model native type plus +parameter contract before language projection, keep COM separate from WinRT, +and fail closed when layout or ownership is incomplete. JavaScript ergonomics +belong to the codegen projection layer; the renderer must not infer ABI +semantics. Classic COM changes must preserve existing WinRT models, generated +output, runtime behavior, and the `@microsoft/dynwinrt` root API. + ### Code Generator (dynwinrt-codegen) - `src/codegen/project.rs` + `src/codegen/projected.rs` — Build the language-neutral `ProjectedFile` IR from parsed metadata - `src/codegen/render_js.rs` + `src/codegen/render_dts.rs` — Render IR to `.js` and `.d.ts` diff --git a/.github/skills/classic-com-abi/SKILL.md b/.github/skills/classic-com-abi/SKILL.md new file mode 100644 index 00000000..8456a2f1 --- /dev/null +++ b/.github/skills/classic-com-abi/SKILL.md @@ -0,0 +1,229 @@ +--- +name: classic-com-abi +description: Use when implementing or reviewing Classic COM, Windows.Win32.winmd, native ABI, pointer, handle, ownership, libffi, or COM codegen changes in dynwinrt. +--- + +# Classic COM ABI development + +Use this skill for changes under: + +- `crates/dynwinrt/src/com.rs`, `signature.rs`, or `call.rs`; +- `bindings/js/src/com.rs`; +- `tools/dynwinrt-codegen/src/com_metadata.rs`; +- `tools/dynwinrt-codegen/src/codegen/com/`; or +- Classic COM runners in `tests/runners/com/`. + +Read [`docs/classic-com-support.md`](../../../docs/classic-com-support.md) +before changing supported types or claiming support for an interface. + +## Core principle + +Start from the native ABI type **and parameter contract**, never from the +desired JavaScript/Python representation. + +```text +Windows.Win32.winmd facts + -> COM-local semantic ABI model + -> validation and ownership plan + -> libffi call plan + -> language projection +``` + +`Buffer`, `bigint`, `string`, and generated wrappers are projection choices. +They must not determine native semantics. + +## Required semantic model + +Preserve these facts before rendering: + +- native type name and underlying type; +- pointer depth; +- const/mutability; +- `In`, `Out`, or `InOut`; +- nullable/required state; +- struct/union size, alignment, packing, and fields; +- count/capacity/actual-length parameter relationships; +- ownership transfer; +- allocator or cleanup function; +- interface IID and reference ownership; and +- return convention: HRESULT, semantic HRESULT, direct value, pointer, or + `void`. + +Do not erase these facts into a generic `Object` or pointer before validation. + +## Semantic categories + +Model at least these categories explicitly: + +```text +Scalar +Enum +NativeStruct +NativeUnion +HandleValue +DataPointer +StringPointer +Bstr +ComInterface +CountedBuffer +SafeArray +Variant +FunctionPointer +Unknown +``` + +Unknown or incomplete categories must fail closed. + +## Layer boundaries + +1. Keep Classic COM metadata and projected types COM-local. +2. Do not add Classic COM concepts to the existing WinRT metadata model or + `DynWinRt*` public surface. +3. Sharing private libffi storage and vtable dispatch is allowed. +4. Keep the npm root WinRT-only; generated COM bindings import + `@microsoft/dynwinrt/com`. +5. Renderers consume validated semantic IR. They must not infer ABI semantics + from names, JavaScript values, or struct shape. + +## Projection responsibility + +Keep these responsibilities separate: + +| Layer | Responsibility | +|---|---| +| Runtime / ABI | Faithfully and safely execute a fully described native call: storage, libffi types, vtable dispatch, HRESULT, ownership, and cleanup. | +| Codegen semantic projection | Turn validated COM semantics into an idiomatic language API: Buffer/string/bigint choices, camelCase, overloads, optional arguments, hidden ABI parameters, and projected return values. | +| Renderer | Serialize the projection decision into JavaScript and declarations. It must not discover or guess native semantics. | + +Electron/Node conveniences belong in the JavaScript projection. The runtime may +provide a small, centralized safety primitive such as `handleValue()`, but it +must not decide that an arbitrary Buffer represents a handle. + +## WinRT compatibility invariant + +Classic COM work must not change existing WinRT semantics. + +- Do not add COM-only types, directions, ownership, pointers, or return + conventions to the public WinRT model. +- Do not change existing `DynWinRt*` behavior or the + `@microsoft/dynwinrt` root surface. +- Do not change generated WinRT constructors, method signatures, imports, + naming, ownership, or output files as a side effect of COM support. +- Shared ABI/libffi helpers must remain private and behavior-neutral for WinRT. +- Route Classic COM through COM-local metadata and projection before any + language renderer. +- Require WinRT snapshot, package, runtime, and live E2E regression coverage + for every shared-infrastructure change. + +## Pointer and Buffer rules + +A Node Buffer can have different native meanings: + +| Semantic type | Buffer meaning | Projection | +|---|---|---| +| Handle value | Pointer-width bytes containing a numeric handle | Explicit `DynCom.handleValue()` | +| Data pointer | Native data stored in the Buffer | `DynCom.pointer(buffer)` passes and retains its address | +| String pointer | Encoded, terminated string bytes | Pass the backing address with encoding validation | +| BSTR | Length-prefixed Automation allocation | Dedicated BSTR allocation/conversion | +| COM interface | Reference-counted interface pointer | Managed COM wrapper, never a Buffer | + +Never apply one Buffer interpretation to every pointer-shaped typedef. + +For Electron HWND input: + +- accept Buffer/Uint8Array only for a confirmed `HWND` input; +- require exactly `size_of::()` bytes; +- decode little-endian handle bits in the centralized runtime helper; +- keep HWND output aliases numeric; and +- keep PSID, security descriptors, structs, and strings on address semantics. + +Do not infer `HandleValue` merely because a Win32 struct has one `Value` +pointer field. Use metadata attributes and an explicit conservative mapping. +Examples: + +- `HANDLE`: `RAIIFree(CloseHandle)`; +- `HKEY`: `RAIIFree(RegCloseKey)`; +- `HICON`: `RAIIFree(DestroyIcon)`; +- `HWND`: `AlsoUsableFor(HANDLE)`; +- `BSTR`: `RAIIFree(SysFreeString)`; +- `PSID`: data pointer, not a handle value. + +## Ownership rules + +- `CoCreateInstance`, QueryInterface, and typed interface out-parameters return + owned `+1` references. +- Managed COM values release automatically; explicit `release()` is only + deterministic early release. +- Interface inputs are borrowed unless the callee AddRefs them for retention. +- `adoptComPointer()` accepts only a native output known to transfer `+1`. +- Numeric and Buffer-backed pointers are borrowed and cannot be adopted. +- Pair BSTR with `SysFreeString`. +- Pair CoTaskMem allocations with `CoTaskMemFree`. +- Win32 handles are not COM references; cleanup is resource-specific. +- Unknown allocator or ownership contracts fail closed. + +## Metadata evidence + +Before supporting an interface: + +1. Parse the actual configured `Windows.Win32.winmd`. +2. Walk its full interface inheritance chain. +3. Inspect every method, not only the method intended for a sample. +4. Record `NativeArrayInfo`, `FreeWith`, `Const`, parameter direction, and + pointer depth. +5. Check Microsoft API documentation for ownership that metadata does not + encode. +6. Generate with `--dry-run` and verify unsupported methods stop the whole + unsafe interface projection. + +Do not claim general interface support when only a manually described runtime +subset works. + +## Fail-closed requirements + +Reject generation when any required fact is unknown, including: + +- native struct/union layout; +- writable caller-sized buffers without a modeled count relationship; +- untyped output pointers without ownership; +- unsupported interface in/out replacement; +- BSTR arrays or unknown string allocation; +- VARIANT, PROPVARIANT, SAFEARRAY, FORMATETC, or STGMEDIUM without dedicated + models; +- unsupported direct native returns; or +- incomplete inherited vtable layout. + +An error during generation is safer than plausible generated code with the +wrong ABI. + +## Validation + +Every new semantic type or ownership rule needs: + +1. a pure unit test for mapping and rendering; +2. a real `Windows.Win32.winmd` regression test; +3. a runtime test covering storage and cleanup; +4. a fail-before/fail-closed test for the nearest unsupported shape; +5. x64 and i686 compile validation for pointer-sized ABI; +6. WinRT regression coverage proving the existing generator and runtime did + not change; and +7. a live stock-Windows E2E when the API is deterministic and requires no + optional software, network, or user interaction. + +Prefer tests that add a new ABI shape. Do not add many interfaces that only +repeat activation. + +## Review checklist + +- Does the change start from metadata facts rather than JS convenience? +- Is the semantic type explicit? +- Are pointer depth and direction preserved? +- Is storage correctly sized before native invocation? +- Is ownership explicit on success and failure? +- Are x86 and x64 widths correct? +- Can a borrowed pointer become a second owner? +- Can Buffer contents be confused with Buffer address? +- Does an InOut path use the same conversion and helper availability as In? +- Does the renderer contain ABI heuristics that belong in projection? +- Does unsupported metadata fail during generation? +- Did any WinRT model, output, or root API change? diff --git a/docs/classic-com-support.md b/docs/classic-com-support.md index dd8e1f5f..c7dce764 100644 --- a/docs/classic-com-support.md +++ b/docs/classic-com-support.md @@ -14,6 +14,13 @@ Both entrypoints use the same native N-API binary and private libffi call machinery. Classic COM metadata, generated wrappers, ownership rules, and public APIs remain separate from the WinRT projection. +Language ergonomics belong to codegen projection, after native semantics have +been validated. The runtime executes a faithful ABI plan; the JavaScript +projection chooses Buffer/string/bigint, naming, hidden ABI parameters, and +return shapes; the renderer only serializes those decisions. Classic COM work +must not change existing WinRT metadata, generated output, ownership, runtime +behavior, or the `@microsoft/dynwinrt` root API. + ## Size of Windows.Win32.winmd The counts below are exact for From 96c459e8900ff843e789c3b3d1dbbd6d96d26dba Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Wed, 29 Jul 2026 23:19:10 +0800 Subject: [PATCH 26/28] Separate WinRT and COM call planners Keep WinRT signatures and metadata isolated while Classic COM owns its method registry and lowers through a private native-call backend. Add exact struct validation, preserve ABI-compatible array projections, and document the runtime boundaries. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 41a826d4-b04a-4932-b1b8-31173fc5ab40 --- .github/skills/classic-com-abi/SKILL.md | 39 +- bindings/js/src/com.rs | 2 +- crates/dynwinrt/src/call.rs | 2 +- crates/dynwinrt/src/com.rs | 189 ++- crates/dynwinrt/src/lib.rs | 1 + crates/dynwinrt/src/metadata_table/arena.rs | 45 +- crates/dynwinrt/src/metadata_table/mod.rs | 23 +- crates/dynwinrt/src/native_call.rs | 1382 +++++++++++++++++++ crates/dynwinrt/src/signature.rs | 1054 +------------- docs/classic-com-support.md | 38 + 10 files changed, 1661 insertions(+), 1114 deletions(-) create mode 100644 crates/dynwinrt/src/native_call.rs diff --git a/.github/skills/classic-com-abi/SKILL.md b/.github/skills/classic-com-abi/SKILL.md index 8456a2f1..8ee3a527 100644 --- a/.github/skills/classic-com-abi/SKILL.md +++ b/.github/skills/classic-com-abi/SKILL.md @@ -7,7 +7,7 @@ description: Use when implementing or reviewing Classic COM, Windows.Win32.winmd Use this skill for changes under: -- `crates/dynwinrt/src/com.rs`, `signature.rs`, or `call.rs`; +- `crates/dynwinrt/src/com.rs`, `signature.rs`, `native_call.rs`, or `call.rs`; - `bindings/js/src/com.rs`; - `tools/dynwinrt-codegen/src/com_metadata.rs`; - `tools/dynwinrt-codegen/src/codegen/com/`; or @@ -85,6 +85,43 @@ Unknown or incomplete categories must fail closed. 5. Renderers consume validated semantic IR. They must not infer ABI semantics from names, JavaScript values, or struct shape. +### Required runtime architecture + +```text +WinRT metadata -> signature.rs (WinRT planner) --------\ + -> native_call.rs -> call.rs -> native method +COM metadata -> com.rs (COM planner and method table) / +``` + +Keep these source-level responsibilities distinct: + +| Component | Required responsibility | +|---|---| +| `signature.rs` | WinRT-only signature facade preserving existing `In`, `Out`, fill-array, HRESULT, and out-value behavior. | +| `com.rs` | COM-local `Type`, `MethodSignature`, `Interface`, `MethodHandle`, interface roots, method registry, pointer/InOut semantics, and native return conventions. | +| `native_call.rs` | Private lowering backend for completed signatures: parameter/output indexing, value validation and coercion, array ABI expansion, fast-path selection, libffi CIF preparation, and result coordination. | +| `call.rs` | Private executor: vtable lookup, stable ABI storage, libffi argument construction and invocation, and decoding raw output slots according to the plan. | + +Apply these rules: + +- WinRT methods stay in the WinRT `MetadataTable`; COM methods stay in the + COM-local registry. +- Only the WinRT planner may define WinRT signature behavior. Do not add raw + pointers, `InOut`, direct native returns, or `void` returns to its public + model. +- Only the COM metadata/projection and planner layers may interpret pointer + categories, parameter direction, return convention, and ownership. +- `native_call.rs` may validate and lower an already-described call, but must + not infer metadata semantics, allocator ownership, or language projection. +- `call.rs` must execute the completed plan without inferring metadata, + ownership, or projection contracts from the caller or language-level value. +- Native methods published through a shared registry must be fully constructed + and immutable. Any manual `Send`/`Sync` implementation requires a documented + libffi read-only safety argument and compile-time trait tests. +- Exact identity checks are required for structs. Preserve established + ABI-compatible WinRT projection aliases such as Char16/U16 and enum/I32 + arrays. + ## Projection responsibility Keep these responsibilities separate: diff --git a/bindings/js/src/com.rs b/bindings/js/src/com.rs index bf5692e1..14cefe99 100644 --- a/bindings/js/src/com.rs +++ b/bindings/js/src/com.rs @@ -532,7 +532,7 @@ impl DynComInterface { } #[napi] -pub struct DynComMethodHandle(dynwinrt::MethodHandle); +pub struct DynComMethodHandle(dynwinrt::com::MethodHandle); #[napi] impl DynComMethodHandle { diff --git a/crates/dynwinrt/src/call.rs b/crates/dynwinrt/src/call.rs index 94bbeb85..31917204 100644 --- a/crates/dynwinrt/src/call.rs +++ b/crates/dynwinrt/src/call.rs @@ -7,7 +7,7 @@ use windows_core::{HRESULT, Interface}; use crate::{ abi::{AbiType, AbiValue}, - signature::{MethodReturn, Parameter}, + native_call::{MethodReturn, Parameter}, value::WinRTValue, }; diff --git a/crates/dynwinrt/src/com.rs b/crates/dynwinrt/src/com.rs index 9ee69606..189c41ae 100644 --- a/crates/dynwinrt/src/com.rs +++ b/crates/dynwinrt/src/com.rs @@ -2,7 +2,10 @@ // Licensed under the MIT License. use core::ffi::c_void; -use std::cell::RefCell; +use std::{ + cell::RefCell, + sync::{Arc, RwLock}, +}; use windows::Win32::System::Com::{ CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, COINIT_MULTITHREADED, CoCreateInstance, @@ -11,8 +14,9 @@ use windows::Win32::System::Com::{ use windows_core::{GUID, IUnknown, Interface as WindowsInterface}; use crate::{ - MetadataTable, MethodHandle, TypeHandle, WinRTValue, result, - signature::{AbiMethodSignature, ParameterType}, + MetadataTable, TypeHandle, WinRTValue, + native_call::{AbiMethodSignature, Method as NativeMethod, ParameterType}, + result, }; const RPC_E_CHANGED_MODE: windows_core::HRESULT = windows_core::HRESULT(0x80010106u32 as i32); @@ -78,29 +82,94 @@ impl MethodSignature { } } +#[derive(Debug)] +struct RegisteredMethod(NativeMethod); + +// Safety: a RegisteredMethod is fully built before publication and remains +// immutable. NativeMethod invokes libffi's CIF only through shared references; +// ffi_call treats the prepared CIF and its type graph as read-only. +unsafe impl Send for RegisteredMethod {} +unsafe impl Sync for RegisteredMethod {} + #[derive(Debug, Clone)] -pub struct Interface(TypeHandle); +pub struct Interface { + name: String, + iid: GUID, + base_slot: usize, + methods: Arc)>>>, +} impl Interface { + pub fn name(&self) -> &str { + &self.name + } + + pub fn iid(&self) -> GUID { + self.iid + } + pub fn add_method(self, name: &str, signature: MethodSignature) -> Self { - self.0 - .clone() - .add_method(name, crate::MethodSignature::from_abi(signature.0)); + let mut methods = self.methods.write().unwrap(); + if methods.iter().any(|(existing, _)| existing == name) { + drop(methods); + return self; + } + let vtable_index = self.base_slot + methods.len(); + methods.push(( + name.to_string(), + Arc::new(RegisteredMethod(signature.0.build(vtable_index))), + )); + drop(methods); self } pub fn method(&self, vtable_index: usize) -> Option { - self.0.method(vtable_index) + let local_index = vtable_index.checked_sub(self.base_slot)?; + self.methods + .read() + .unwrap() + .get(local_index) + .map(|(_, method)| MethodHandle(Arc::clone(method))) + } +} + +#[derive(Clone)] +pub struct MethodHandle(Arc); + +impl std::fmt::Debug for MethodHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MethodHandle").finish_non_exhaustive() + } +} + +impl MethodHandle { + pub fn invoke(&self, obj: *mut c_void, args: &[WinRTValue]) -> result::Result> { + self.0 + .0 + .call_dynamic(obj, args) + .map_err(result::Error::WindowsError) + } + + pub fn call_getter_hstring(&self, obj: *mut c_void) -> result::Result { + self.0 + .0 + .call_getter_hstring(obj) + .map_err(result::Error::WindowsError) } } pub fn register_interface( - table: &std::sync::Arc, + _table: &std::sync::Arc, name: &str, iid: GUID, base: InterfaceBase, ) -> Interface { - Interface(table.register_com_interface(name, iid, base.first_method_slot())) + Interface { + name: name.to_string(), + iid, + base_slot: base.first_method_slot(), + methods: Arc::new(RwLock::new(Vec::new())), + } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -244,7 +313,7 @@ fn wide_to_string(buffer: &[u16]) -> String { mod tests { use super::*; use crate::{ - InterfaceSignature, MetadataTable, com_helpers::E_NOINTERFACE, ro_get_activation_factory_2, + MetadataTable, com_helpers::E_NOINTERFACE, ro_get_activation_factory_2, roapi::query_interface, }; use std::sync::atomic::{AtomicU32, Ordering}; @@ -258,13 +327,21 @@ mod tests { }, }, }; - use windows_core::{HSTRING, Interface, w}; + use windows_core::{HSTRING, w}; #[repr(C)] struct FakeComObject { vtable: *const *mut c_void, } + #[test] + fn interface_and_method_handle_are_send_and_sync() { + fn assert_send_sync() {} + + assert_send_sync::(); + assert_send_sync::(); + } + unsafe extern "system" fn return_u32(_this: *mut c_void) -> u32 { u32::MAX } @@ -424,41 +501,58 @@ mod tests { } #[test] - #[should_panic(expected = "already registered with a different type or IID")] - fn interface_names_cannot_alias_different_iids() { + fn interfaces_with_the_same_name_do_not_alias() { let table = MetadataTable::new(); - register_interface( + let first = register_interface( &table, "Windows.Win32.Example.IThing", GUID::from_u128(1), InterfaceBase::IUnknown, ); - register_interface( + let second = register_interface( &table, "Windows.Win32.Example.IThing", GUID::from_u128(2), InterfaceBase::IUnknown, ); + + assert_eq!(first.name(), second.name()); + assert_ne!(first.iid(), second.iid()); + assert!(!Arc::ptr_eq(&first.methods, &second.methods)); } - fn shell_link_signature(table: &std::sync::Arc) -> InterfaceSignature { - let mut iface = - InterfaceSignature::define_from_iunknown("IShellLinkW", IID_ISHELL_LINK_W, table); - iface - .add_method(crate::MethodSignature::new(table)) // 3 GetPath - .add_method(crate::MethodSignature::new(table)) // 4 GetIDList - .add_method(crate::MethodSignature::new(table)) // 5 SetIDList - .add_method(crate::MethodSignature::new(table)) // 6 GetDescription - .add_method(crate::MethodSignature::new(table)) // 7 SetDescription - .add_method(crate::MethodSignature::new(table)) // 8 GetWorkingDirectory - .add_method(crate::MethodSignature::new(table)) // 9 SetWorkingDirectory - .add_method(crate::MethodSignature::new(table)) // 10 GetArguments - .add_method(crate::MethodSignature::new(table)) // 11 SetArguments - .add_method(crate::MethodSignature::new(table).add_out(table.u16_type())) // 12 GetHotkey - .add_method(crate::MethodSignature::new(table).add_in(table.u16_type())) // 13 SetHotkey - .add_method(crate::MethodSignature::new(table).add_out(table.i32_type())) // 14 GetShowCmd - .add_method(crate::MethodSignature::new(table).add_in(table.i32_type())); // 15 SetShowCmd - iface + fn shell_link_interface(table: &std::sync::Arc) -> Interface { + register_interface( + table, + "Windows.Win32.UI.Shell.IShellLinkW", + IID_ISHELL_LINK_W, + InterfaceBase::IUnknown, + ) + .add_method("GetPath", MethodSignature::new(table)) + .add_method("GetIDList", MethodSignature::new(table)) + .add_method("SetIDList", MethodSignature::new(table)) + .add_method("GetDescription", MethodSignature::new(table)) + .add_method("SetDescription", MethodSignature::new(table)) + .add_method("GetWorkingDirectory", MethodSignature::new(table)) + .add_method("SetWorkingDirectory", MethodSignature::new(table)) + .add_method("GetArguments", MethodSignature::new(table)) + .add_method("SetArguments", MethodSignature::new(table)) + .add_method( + "GetHotkey", + MethodSignature::new(table).add_out(Type::winrt(table.u16_type())), + ) + .add_method( + "SetHotkey", + MethodSignature::new(table).add_in(Type::winrt(table.u16_type())), + ) + .add_method( + "GetShowCmd", + MethodSignature::new(table).add_out(Type::winrt(table.i32_type())), + ) + .add_method( + "SetShowCmd", + MethodSignature::new(table).add_in(Type::winrt(table.i32_type())), + ) } fn native_usize_type(table: &std::sync::Arc) -> Type { @@ -504,10 +598,13 @@ mod tests { fn shell_link_set_get_show_cmd_round_trips_via_classic_com_vtable() -> result::Result<()> { let shell_link = shell_link()?.as_object().unwrap(); let table = MetadataTable::new(); - let iface = shell_link_signature(&table); + let iface = shell_link_interface(&table); - iface.methods[15].call_dynamic(shell_link.as_raw(), &[WinRTValue::I32(3)])?; - let result = iface.methods[14].call_dynamic(shell_link.as_raw(), &[])?; + iface + .method(15) + .unwrap() + .invoke(shell_link.as_raw(), &[WinRTValue::I32(3)])?; + let result = iface.method(14).unwrap().invoke(shell_link.as_raw(), &[])?; assert_eq!(result[0].as_i32().unwrap(), 3); Ok(()) @@ -517,10 +614,13 @@ mod tests { fn shell_link_set_get_hotkey_round_trips_u16() -> result::Result<()> { let shell_link = shell_link()?.as_object().unwrap(); let table = MetadataTable::new(); - let iface = shell_link_signature(&table); + let iface = shell_link_interface(&table); - iface.methods[13].call_dynamic(shell_link.as_raw(), &[WinRTValue::U16(0x0141)])?; - let result = iface.methods[12].call_dynamic(shell_link.as_raw(), &[])?; + iface + .method(13) + .unwrap() + .invoke(shell_link.as_raw(), &[WinRTValue::U16(0x0141)])?; + let result = iface.method(12).unwrap().invoke(shell_link.as_raw(), &[])?; assert_eq!(result[0].as_i32().unwrap() as u16, 0x0141); Ok(()) @@ -702,10 +802,13 @@ mod tests { let adopted = unsafe { adopt_com_pointer(raw) }; let adopted = adopted.as_object().expect("adopted value must be Object"); let table = MetadataTable::new(); - let iface = shell_link_signature(&table); + let iface = shell_link_interface(&table); - iface.methods[15].call_dynamic(adopted.as_raw(), &[WinRTValue::I32(7)])?; - let result = iface.methods[14].call_dynamic(adopted.as_raw(), &[])?; + iface + .method(15) + .unwrap() + .invoke(adopted.as_raw(), &[WinRTValue::I32(7)])?; + let result = iface.method(14).unwrap().invoke(adopted.as_raw(), &[])?; assert_eq!(result[0].as_i32().unwrap(), 7); Ok(()) diff --git a/crates/dynwinrt/src/lib.rs b/crates/dynwinrt/src/lib.rs index ec03c1b6..0f900016 100644 --- a/crates/dynwinrt/src/lib.rs +++ b/crates/dynwinrt/src/lib.rs @@ -7,6 +7,7 @@ mod abi; mod call; pub mod com; mod interfaces; +mod native_call; mod result; mod roapi; mod signature; diff --git a/crates/dynwinrt/src/metadata_table/arena.rs b/crates/dynwinrt/src/metadata_table/arena.rs index 28184a32..aa349349 100644 --- a/crates/dynwinrt/src/metadata_table/arena.rs +++ b/crates/dynwinrt/src/metadata_table/arena.rs @@ -40,7 +40,6 @@ pub(super) struct EnumData { pub(super) struct InterfaceMethodTable { pub(super) method_names: Vec, pub(super) method_indices: Vec, - pub(super) base_slot: usize, } // =========================================================================== @@ -117,29 +116,15 @@ impl MetadataTable { // ----------------------------------------------------------------------- /// Create an interface method table. Called only when dedup already checked by caller. - /// If a method table for this IID already exists, its base MUST match; - /// otherwise subsequent method registrations for the - /// IID would compute wrong vtable indices for one of the callers. - pub(super) fn create_interface_method_table(&self, iid: GUID, base_slot: usize) { - assert!(matches!(base_slot, 3 | 6)); - let mut tables = self.interface_methods.write().unwrap(); - match tables.entry(iid) { - std::collections::hash_map::Entry::Occupied(existing) => { - let existing_base = existing.get().base_slot; - assert_eq!( - existing_base, base_slot, - "interface IID {iid:?} registered twice with conflicting bases \ - (existing={existing_base}, new={base_slot})" - ); - } - std::collections::hash_map::Entry::Vacant(v) => { - v.insert(InterfaceMethodTable { - method_names: Vec::new(), - method_indices: Vec::new(), - base_slot, - }); - } - } + pub(super) fn create_interface_method_table(&self, iid: GUID) { + self.interface_methods + .write() + .unwrap() + .entry(iid) + .or_insert_with(|| InterfaceMethodTable { + method_names: Vec::new(), + method_indices: Vec::new(), + }); } /// Add a method to an interface's method table. Returns the vtable index. @@ -152,10 +137,10 @@ impl MetadataTable { // Dedup: if method name already registered, return existing vtable index if let Some(pos) = table.method_names.iter().position(|n| n == name) { - return (table.base_slot + pos) as u32; + return (6 + pos) as u32; } - let vtable_index = table.base_slot + table.method_indices.len(); + let vtable_index = 6 + table.method_indices.len(); let method = sig.build(vtable_index); let arena_index = self.methods.push(method); table.method_names.push(name.to_string()); @@ -252,12 +237,12 @@ impl MetadataTable { iid: &GUID, vtable_index: usize, ) -> Option { - let iface_methods = self.interface_methods.read().unwrap(); - let table = iface_methods.get(iid)?; - if vtable_index < table.base_slot { + if vtable_index < 6 { return None; } - let local_index = vtable_index - table.base_slot; + let local_index = vtable_index - 6; + let iface_methods = self.interface_methods.read().unwrap(); + let table = iface_methods.get(iid)?; table.method_indices.get(local_index).copied() } diff --git a/crates/dynwinrt/src/metadata_table/mod.rs b/crates/dynwinrt/src/metadata_table/mod.rs index 92db4fac..999e1d41 100644 --- a/crates/dynwinrt/src/metadata_table/mod.rs +++ b/crates/dynwinrt/src/metadata_table/mod.rs @@ -285,28 +285,7 @@ impl MetadataTable { if let Some(kind) = self.get_named_type(name) { return self.make(kind); } - self.create_interface_method_table(iid, 6); - let kind = TypeKind::Interface(iid); - self.insert_named_type(name, kind); - self.make(kind) - } - - pub(crate) fn register_com_interface( - self: &Arc, - name: &str, - iid: GUID, - base_slot: usize, - ) -> TypeHandle { - if let Some(kind) = self.get_named_type(name) { - assert_eq!( - kind, - TypeKind::Interface(iid), - "type name {name:?} is already registered with a different type or IID" - ); - self.create_interface_method_table(iid, base_slot); - return self.make(kind); - } - self.create_interface_method_table(iid, base_slot); + self.create_interface_method_table(iid); let kind = TypeKind::Interface(iid); self.insert_named_type(name, kind); self.make(kind) diff --git a/crates/dynwinrt/src/native_call.rs b/crates/dynwinrt/src/native_call.rs new file mode 100644 index 00000000..c2419a1a --- /dev/null +++ b/crates/dynwinrt/src/native_call.rs @@ -0,0 +1,1382 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Private native call planning and execution shared by WinRT and Classic COM. + +use libffi::middle::Cif; +use std::sync::Arc; +use windows::core::{GUID, IInspectable, Interface}; + +use crate::{ + abi::{AbiType, AbiValue}, + call, + call::ArgumentList, + metadata_table::{MetadataTable, TypeHandle, TypeKind}, + value::WinRTValue, +}; + +#[derive(Debug, Clone)] +pub(crate) enum ParameterType { + WinRT(TypeHandle), + Pointer, +} + +impl ParameterType { + pub(crate) fn winrt(typ: TypeHandle) -> Self { + Self::WinRT(typ) + } + + pub(crate) fn pointer() -> Self { + Self::Pointer + } + + pub(crate) fn as_winrt(&self) -> Option<&TypeHandle> { + match self { + Self::WinRT(typ) => Some(typ), + Self::Pointer => None, + } + } + + pub(crate) fn is_array(&self) -> bool { + self.as_winrt().is_some_and(TypeHandle::is_array) + } + + pub(crate) fn is_struct(&self) -> bool { + matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::Struct(_))) + } + + pub(crate) fn is_hstring(&self) -> bool { + matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::HString)) + } + + pub(crate) fn is_u32(&self) -> bool { + matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::U32)) + } + + pub(crate) fn is_guid(&self) -> bool { + matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::Guid)) + } + + pub(crate) fn supports_in_out(&self) -> bool { + matches!(self, Self::Pointer) + || matches!( + self, + Self::WinRT(typ) + if matches!( + typ.kind(), + TypeKind::Bool + | TypeKind::I8 + | TypeKind::U8 + | TypeKind::I16 + | TypeKind::U16 + | TypeKind::Char16 + | TypeKind::I32 + | TypeKind::U32 + | TypeKind::I64 + | TypeKind::U64 + | TypeKind::F32 + | TypeKind::F64 + | TypeKind::HResult + | TypeKind::Enum(_) + | TypeKind::Struct(_) + ) + ) + } + + pub(crate) fn supports_direct_return(&self) -> bool { + matches!(self, Self::Pointer) + || matches!( + self, + Self::WinRT(typ) + if matches!( + typ.kind(), + TypeKind::Bool + | TypeKind::I8 + | TypeKind::U8 + | TypeKind::I16 + | TypeKind::U16 + | TypeKind::Char16 + | TypeKind::I32 + | TypeKind::U32 + | TypeKind::I64 + | TypeKind::U64 + | TypeKind::F32 + | TypeKind::F64 + | TypeKind::HResult + | TypeKind::Enum(_) + ) + ) + } + + pub(crate) fn abi_type(&self) -> AbiType { + match self { + Self::WinRT(typ) => typ.abi_type(), + Self::Pointer => AbiType::Ptr, + } + } + + pub(crate) fn libffi_type(&self) -> libffi::middle::Type { + match self { + Self::WinRT(typ) => typ.libffi_type(), + Self::Pointer => libffi::middle::Type::pointer(), + } + } + + pub(crate) fn array_element_type(&self) -> TypeHandle { + self.as_winrt() + .expect("native pointer is not an array") + .array_element_type() + } + + pub(crate) fn default_struct_value(&self) -> crate::metadata_table::ValueTypeData { + self.as_winrt() + .expect("native pointer is not a struct") + .default_value() + } + + pub(crate) fn default_value(&self) -> WinRTValue { + match self { + Self::WinRT(typ) => typ.default_winrt_value(), + Self::Pointer => WinRTValue::RawPtr(std::ptr::null_mut()), + } + } + + pub(crate) fn from_out(&self, ptr: *mut std::ffi::c_void) -> crate::result::Result { + match self { + Self::WinRT(typ) => typ.from_out(ptr), + Self::Pointer => Ok(WinRTValue::RawPtr(ptr)), + } + } + + pub(crate) fn from_out_value(&self, value: &AbiValue) -> crate::result::Result { + match (self, value) { + (Self::WinRT(typ), value) => typ.from_out_value(value), + (Self::Pointer, AbiValue::Pointer(ptr)) => Ok(WinRTValue::RawPtr(*ptr)), + (Self::Pointer, value) => Err(crate::result::Error::InvalidTypeAbiToWinRT( + TypeKind::Object, + value.abi_type(), + )), + } + } +} + +/// How a parameter is passed at the ABI level. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParamKind { + In, + Out, + InOut, + /// FillArray: caller allocates buffer, callee fills it. + /// ABI expands to 2 params: (u32 capacity, T* items). + OutFillArray, +} + +#[derive(Debug, Clone)] +pub struct Parameter { + pub(crate) typ: ParameterType, + /// Index in the method result vector for out and FillArray parameters. + pub value_index: usize, + /// Index in the caller-provided argument slice. FillArray parameters have + /// both an input index (capacity buffer) and an output index (filled data). + pub input_index: Option, + pub kind: ParamKind, +} + +impl Parameter { + pub fn is_input(&self) -> bool { + matches!(self.kind, ParamKind::In | ParamKind::InOut) + } + + pub fn is_out(&self) -> bool { + matches!( + self.kind, + ParamKind::Out | ParamKind::InOut | ParamKind::OutFillArray + ) + } + + pub fn is_in_out(&self) -> bool { + self.kind == ParamKind::InOut + } + + pub fn is_fill_array(&self) -> bool { + self.kind == ParamKind::OutFillArray + } +} + +#[derive(Debug, Clone)] +pub(crate) struct AbiMethodSignature { + out_count: usize, + input_count: usize, + parameters: Vec, + return_kind: MethodReturn, + #[allow(dead_code)] + is_opaque: bool, + #[allow(dead_code)] + table: Arc, +} + +#[derive(Debug, Clone)] +pub(crate) enum MethodReturn { + HResult, + Void, + Value(ParameterType), +} + +impl MethodReturn { + fn libffi_type(&self) -> libffi::middle::Type { + match self { + Self::HResult => libffi::middle::Type::i32(), + Self::Void => libffi::middle::Type::void(), + Self::Value(typ) => typ.libffi_type(), + } + } +} + +impl AbiMethodSignature { + pub(crate) fn new(table: &Arc) -> Self { + AbiMethodSignature { + out_count: 0, + input_count: 0, + parameters: Vec::new(), + return_kind: MethodReturn::HResult, + is_opaque: false, + table: Arc::clone(table), + } + } + + pub(crate) fn add_in_type(mut self, typ: ParameterType) -> Self { + let input_index = self.input_count; + self.input_count += 1; + self.parameters.push(Parameter { + kind: ParamKind::In, + typ, + value_index: input_index, + input_index: Some(input_index), + }); + self + } + + pub(crate) fn add_out_type(mut self, typ: ParameterType) -> Self { + self.parameters.push(Parameter { + kind: ParamKind::Out, + typ, + value_index: self.out_count, + input_index: None, + }); + self.out_count += 1; + self + } + + pub(crate) fn add_in_out_type(mut self, typ: ParameterType) -> Self { + assert!( + typ.supports_in_out(), + "in/out currently supports native scalars, pointers, enums, and structs" + ); + let input_index = self.input_count; + self.input_count += 1; + self.parameters.push(Parameter { + kind: ParamKind::InOut, + typ, + value_index: self.out_count, + input_index: Some(input_index), + }); + self.out_count += 1; + self + } + + pub(crate) fn add_out_fill_type(mut self, typ: ParameterType) -> Self { + let input_index = self.input_count; + self.input_count += 1; + self.parameters.push(Parameter { + kind: ParamKind::OutFillArray, + typ, + value_index: self.out_count, + input_index: Some(input_index), + }); + self.out_count += 1; + self + } + + pub(crate) fn returns_type(mut self, typ: ParameterType) -> Self { + assert!( + typ.supports_direct_return(), + "direct native returns currently support scalars, enums, and pointers" + ); + self.return_kind = MethodReturn::Value(typ); + self + } + + pub(crate) fn returns_void(mut self) -> Self { + self.return_kind = MethodReturn::Void; + self + } + + pub(crate) fn build(self, index: usize) -> Method { + use libffi::middle::Type; + let mut types: Vec = Vec::with_capacity(self.parameters.len() + 1); + types.push(Type::pointer()); // com object's this pointer + for param in &self.parameters { + if param.is_fill_array() { + // FillArray: UINT32 capacity, T* items + types.push(Type::u32()); + types.push(Type::pointer()); + } else if param.typ.is_array() { + if param.is_out() { + // ReceiveArray: UINT32* out_length, T** out_data + types.push(Type::pointer()); + types.push(Type::pointer()); + } else { + // PassArray: UINT32 length, T* data + types.push(Type::u32()); + types.push(Type::pointer()); + } + } else if param.is_out() { + types.push(Type::pointer()); + } else { + types.push(param.typ.libffi_type()); + } + } + let in_count = self.parameters.iter().filter(|p| p.is_input()).count(); + let has_complex_param = self + .parameters + .iter() + .any(|p| p.typ.is_array() || p.is_fill_array() || p.is_in_out() || p.typ.is_struct()); + + // Check if the single in-param (if any) is a simple non-HString, non-Struct type + let simple_in = !has_complex_param && in_count == 1 && { + let in_param = self.parameters.iter().find(|p| p.is_input()).unwrap(); + !in_param.typ.is_hstring() + }; + + // Classify array parameters + let array_in_count = self + .parameters + .iter() + .filter(|p| p.is_input() && p.typ.is_array()) + .count(); + let fill_out_count = self.parameters.iter().filter(|p| p.is_fill_array()).count(); + let array_out_count = self + .parameters + .iter() + .filter(|p| p.is_out() && p.typ.is_array() && !p.is_fill_array()) + .count(); + let scalar_in_count = in_count - array_in_count; + let scalar_out_count = self.out_count - fill_out_count - array_out_count; + + let returns_hresult = matches!(self.return_kind, MethodReturn::HResult); + let strategy = if returns_hresult + && !has_complex_param + && in_count == 0 + && self.out_count == 1 + { + CallStrategy::Direct0In1Out + } else if returns_hresult && !has_complex_param && in_count == 0 && self.out_count == 0 { + CallStrategy::Direct0In0Out + } else if returns_hresult && simple_in && self.out_count == 0 { + CallStrategy::Direct1In0Out + } else if returns_hresult && simple_in && self.out_count == 1 { + CallStrategy::Direct1In1Out + // ReceiveArray only: fn(this, *mut u32, *mut *mut c_void) -> HRESULT + } else if returns_hresult + && scalar_in_count == 0 + && array_in_count == 0 + && array_out_count == 1 + && fill_out_count == 0 + && scalar_out_count == 0 + { + CallStrategy::DirectReceiveArray + // PassArray + 1 out: fn(this, u32, *const u8, out) -> HRESULT + } else if returns_hresult + && scalar_in_count == 0 + && array_in_count == 1 + && array_out_count == 0 + && fill_out_count == 0 + && scalar_out_count == 1 + { + CallStrategy::DirectPassArray1Out + // FillArray only: fn(this, u32, *mut u8, *mut u32) -> HRESULT + } else if returns_hresult + && scalar_in_count == 0 + && array_in_count == 0 + && fill_out_count == 1 + && array_out_count == 0 + && scalar_out_count == 0 + { + CallStrategy::DirectFillArray + // 1 scalar in + FillArray: fn(this, val, u32, *mut u8, *mut u32) -> HRESULT + } else if returns_hresult + && scalar_in_count == 1 + && array_in_count == 0 + && fill_out_count == 1 + && array_out_count == 0 + && scalar_out_count == 0 + { + let in_param = self + .parameters + .iter() + .find(|p| p.is_input() && !p.typ.is_array()) + .unwrap(); + if !in_param.typ.is_hstring() && !in_param.typ.is_struct() { + CallStrategy::Direct1InFillArray + } else { + CallStrategy::Libffi(Cif::new(types.into_iter(), self.return_kind.libffi_type())) + } + } else { + CallStrategy::Libffi(Cif::new(types.into_iter(), self.return_kind.libffi_type())) + }; + + Method { + info: MethodInfo { + index, + parameters: self.parameters, + out_count: self.out_count, + return_kind: self.return_kind, + }, + strategy, + } + } +} + +#[derive(Debug)] +pub struct MethodInfo { + pub index: usize, + pub parameters: Vec, + pub out_count: usize, + pub(crate) return_kind: MethodReturn, +} + +/// How a Method should be invoked — decided once at build time. +#[derive(Debug)] +enum CallStrategy { + /// 0 in + 0 out: fn(this) -> HRESULT. + Direct0In0Out, + /// 0 in + 1 out (getter): fn(this, out) -> HRESULT. + Direct0In1Out, + /// 1 in + 0 out (setter, non-HString): fn(this, val) -> HRESULT. + Direct1In0Out, + /// 1 in + 1 out (factory/query, non-HString in): fn(this, val, out) -> HRESULT. + Direct1In1Out, + /// ReceiveArray: fn(this, *mut u32, *mut *mut c_void) -> HRESULT. + DirectReceiveArray, + /// PassArray + 1 out: fn(this, u32, *const u8, out) -> HRESULT. + DirectPassArray1Out, + /// FillArray only: fn(this, u32, *mut u8) -> HRESULT. + DirectFillArray, + /// 1 scalar in + FillArray: fn(this, val, u32, *mut u8) -> HRESULT. + Direct1InFillArray, + /// General case → libffi via cached Cif. + Libffi(Cif), +} + +#[derive(Debug)] +pub struct Method { + info: MethodInfo, + strategy: CallStrategy, +} + +fn expected_object_iid(typ: &TypeHandle) -> Option { + match typ.kind() { + TypeKind::Object => Some(IInspectable::IID), + TypeKind::Interface(_) + | TypeKind::Delegate(_) + | TypeKind::RuntimeClass(_) + | TypeKind::Parameterized(_) + | TypeKind::IAsyncAction + | TypeKind::IAsyncActionWithProgress(_) + | TypeKind::IAsyncOperation(_) + | TypeKind::IAsyncOperationWithProgress(_) => typ.iid(), + _ => None, + } +} + +fn coerce_input_object( + expected: &TypeHandle, + value: &WinRTValue, +) -> windows_core::Result> { + let Some(iid) = expected_object_iid(expected) else { + return Ok(None); + }; + // Null objects are always allowed (`WinRTValue::Null` and the + // Object-typed null variant both project as "no coercion needed" — the + // ABI receives a null pointer directly). + if value.is_null_object() { + return Ok(None); + } + // Raw pointers never satisfy a WinRT object parameter. Otherwise arbitrary + // pointer bits could reach a typed COM slot without QueryInterface validation. + if matches!(value, WinRTValue::RawPtr(_)) { + return Err(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Refusing to pass a raw pointer as a typed COM parameter ({}). \ + Use a Pointer signature for native pointers and handles; for \ + COM parameters pass a real object (or one obtained via `.cast(IID)`).", + expected.signature_string(), + ), + )); + } + + let object = value.as_object().ok_or_else(|| { + windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Expected object argument for {}, found {:?}", + expected.signature_string(), + value.get_type_kind() + ), + ) + })?; + + WinRTValue::Object(object) + .cast(&iid) + .map(Some) + .map_err(|error| match error { + crate::result::Error::WindowsError(error) => error, + other => windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &other.message(), + ), + }) +} + +fn coerce_input_array( + expected: &TypeHandle, + value: &WinRTValue, +) -> windows_core::Result> { + if !expected.is_array() { + return Ok(None); + } + + let element_type = expected.array_element_type(); + let array = value.as_array().ok_or_else(|| { + windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Expected array argument for {}, found {:?}", + expected.signature_string(), + value.get_type_kind() + ), + ) + })?; + let is_object_array = expected_object_iid(&element_type).is_some(); + let element_type_matches = match (element_type.kind(), array.element_type.kind()) { + (TypeKind::Struct(_), TypeKind::Struct(_)) => array.element_type == element_type, + (TypeKind::Enum(_), TypeKind::Enum(_)) => array.element_type == element_type, + (TypeKind::Enum(_), TypeKind::I32) + | (TypeKind::Char16, TypeKind::U16) + | (TypeKind::U16, TypeKind::Char16) => true, + (expected, actual) => expected == actual, + }; + if !is_object_array && !element_type_matches { + return Err(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Array element type mismatch: expected {}, received {}", + element_type.signature_string(), + array.element_type.signature_string(), + ), + )); + } + + let mut values = Vec::with_capacity(array.len()); + let mut changed = false; + for index in 0..array.len() { + let value = array.get(index); + validate_array_element(&element_type, &value, index)?; + if is_object_array && let Some(coerced) = coerce_input_object(&element_type, &value)? { + values.push(coerced); + changed = true; + } else { + values.push(value); + } + } + + Ok(changed + .then(|| WinRTValue::Array(crate::array::ArrayData::from_values(element_type, &values)))) +} + +fn validate_input_struct(expected: &TypeHandle, value: &WinRTValue) -> windows_core::Result<()> { + if !matches!(expected.kind(), TypeKind::Struct(_)) { + return Ok(()); + } + + let actual = value.as_struct().ok_or_else(|| { + windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Expected struct argument for {}, found {:?}", + expected.signature_string(), + value.get_type_kind() + ), + ) + })?; + if actual.type_handle() != expected { + return Err(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Struct type mismatch: expected {} ({} bytes, align {}), \ + received {} ({} bytes, align {})", + expected.signature_string(), + expected.size_of(), + expected.align_of(), + actual.type_handle().signature_string(), + actual.type_handle().size_of(), + actual.type_handle().align_of(), + ), + )); + } + + Ok(()) +} + +fn validate_array_element( + expected: &TypeHandle, + value: &WinRTValue, + index: usize, +) -> windows_core::Result<()> { + if expected_object_iid(expected).is_some() { + return Ok(()); + } + if matches!(expected.kind(), TypeKind::Struct(_)) { + return validate_input_struct(expected, value).map_err(|error| { + windows_core::Error::new( + error.code(), + &format!("Array element {index}: {}", error.message()), + ) + }); + } + if let TypeKind::Enum(_) = expected.kind() { + let matches = matches!(value, WinRTValue::I32(_)) + || matches!( + value, + WinRTValue::Enum { type_handle, .. } if type_handle == expected + ); + if !matches { + return Err(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Array element {index} type mismatch: expected {}, found {:?}", + expected.signature_string(), + value.get_type_kind() + ), + )); + } + return Ok(()); + } + if matches!(expected.kind(), TypeKind::Char16) && matches!(value, WinRTValue::U16(_)) { + return Ok(()); + } + if value.get_type_kind() != expected.kind() { + return Err(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Array element {index} type mismatch: expected {}, found {:?}", + expected.signature_string(), + value.get_type_kind() + ), + )); + } + + Ok(()) +} + +struct InvocationArgs<'a> { + original: &'a [WinRTValue], + replacements: Option>>, +} + +impl<'a> InvocationArgs<'a> { + fn new(original: &'a [WinRTValue]) -> Self { + Self { + original, + replacements: None, + } + } + + fn replace(&mut self, index: usize, value: WinRTValue) { + self.replacements.get_or_insert_with(|| { + std::iter::repeat_with(|| None) + .take(self.original.len()) + .collect() + })[index] = Some(value); + } +} + +impl call::ArgumentList for InvocationArgs<'_> { + fn get_value(&self, index: usize) -> &WinRTValue { + self.replacements + .as_ref() + .and_then(|values| values[index].as_ref()) + .unwrap_or(&self.original[index]) + } +} + +impl Method { + // --- Fast getter paths: zero Vec/WinRTValue allocation --- + + /// Getter → i32 (0 in, 1 out). Writes directly to stack i32. + pub fn call_getter_i32(&self, obj: *mut std::ffi::c_void) -> windows_core::Result { + let mut out: i32 = 0; + let hr = call::call_winrt_method_1( + self.info.index, + obj, + &mut out as *mut i32 as *mut std::ffi::c_void, + ); + hr.ok()?; + Ok(out) + } + + /// Getter → bool (0 in, 1 out). Writes directly to stack bool. + pub fn call_getter_bool(&self, obj: *mut std::ffi::c_void) -> windows_core::Result { + let mut out: i32 = 0; // WinRT bool is i32 on ABI + let hr = call::call_winrt_method_1( + self.info.index, + obj, + &mut out as *mut i32 as *mut std::ffi::c_void, + ); + hr.ok()?; + Ok(out != 0) + } + + /// Getter → HSTRING (0 in, 1 out). Writes directly to stack HSTRING ptr. + pub fn call_getter_hstring( + &self, + obj: *mut std::ffi::c_void, + ) -> windows_core::Result { + // HSTRING is a pointer-sized handle on ABI. Let WinRT write it directly. + let mut out = windows_core::HSTRING::new(); + let hr = call::call_winrt_method_1( + self.info.index, + obj, + &mut out as *mut windows_core::HSTRING as *mut std::ffi::c_void, + ); + hr.ok()?; + Ok(out) + } + + /// Getter → COM object (0 in, 1 out). Writes directly to stack pointer. + pub fn call_getter_object( + &self, + obj: *mut std::ffi::c_void, + ) -> windows_core::Result { + let mut out: *mut std::ffi::c_void = std::ptr::null_mut(); + let hr = call::call_winrt_method_1( + self.info.index, + obj, + &mut out as *mut _ as *mut std::ffi::c_void, + ); + hr.ok()?; + if out.is_null() { + Ok(WinRTValue::Null) + } else { + Ok(WinRTValue::Object(unsafe { + windows_core::IUnknown::from_raw(out) + })) + } + } + + pub fn call_dynamic( + &self, + obj: *mut std::ffi::c_void, + args: &[WinRTValue], + ) -> windows_core::Result> { + let mut args = InvocationArgs::new(args); + for parameter in self.info.parameters.iter().filter(|p| p.is_input()) { + let input_index = parameter.input_index.expect("input parameter index"); + let value = args.get_value(input_index); + let coerced = if let Some(typ) = parameter.typ.as_winrt() { + validate_input_struct(typ, value)?; + if typ.is_array() { + coerce_input_array(typ, value)? + } else { + coerce_input_object(typ, value)? + } + } else { + None + }; + if let Some(value) = coerced { + args.replace(input_index, value); + } + } + + match &self.strategy { + CallStrategy::Direct0In0Out => { + // 0 in + 0 out: fn(this) -> HRESULT + let hr = call::call_winrt_method_0(self.info.index, obj); + hr.ok()?; + Ok(vec![]) + } + CallStrategy::Direct0In1Out => { + // 0 in + 1 out: fn(this, out) -> HRESULT + let param = &self.info.parameters[0]; + let mut out = param.typ.default_value(); + let hr = call::call_winrt_method_1(self.info.index, obj, out.out_ptr()); + hr.ok()?; + // COM pointer types use RawPtr(null) as buffer to avoid IUnknown::from_raw(null) UB. + // After COM writes the pointer, convert via from_out. + if let WinRTValue::RawPtr(raw_ptr) = out { + out = param.typ.from_out(raw_ptr).map_err(|e| { + windows_core::Error::new(windows_core::HRESULT(-1), &format!("{:?}", e)) + })?; + } + out.sanitize_null_object(); + Ok(vec![out]) + } + CallStrategy::Direct1In0Out => { + // 1 in + 0 out: fn(this, val) -> HRESULT + let hr = call::call_1in(self.info.index, obj, args.get_value(0)); + hr.ok()?; + Ok(vec![]) + } + CallStrategy::Direct1In1Out => { + // 1 in + 1 out: fn(this, val, out) -> HRESULT + let out_param = self.info.parameters.iter().find(|p| p.is_out()).unwrap(); + let mut out = out_param.typ.default_value(); + let hr = + call::call_1in_1out(self.info.index, obj, args.get_value(0), out.out_ptr()); + hr.ok()?; + if let WinRTValue::RawPtr(raw_ptr) = out { + out = out_param.typ.from_out(raw_ptr).map_err(|e| { + windows_core::Error::new(windows_core::HRESULT(-1), &format!("{:?}", e)) + })?; + } + out.sanitize_null_object(); + Ok(vec![out]) + } + CallStrategy::DirectReceiveArray => { + // fn(this, *mut u32, *mut *mut c_void) -> HRESULT + let param = &self.info.parameters[0]; + let elem_type = param.typ.array_element_type(); + let mut length: u32 = 0; + let mut data_ptr: *mut std::ffi::c_void = std::ptr::null_mut(); + let fptr = call::get_vtable_function_ptr(obj, self.info.index); + let hr: windows_core::HRESULT = unsafe { + let method: unsafe extern "system" fn( + *mut std::ffi::c_void, + *mut u32, + *mut *mut std::ffi::c_void, + ) + -> windows_core::HRESULT = std::mem::transmute(fptr); + method(obj, &mut length, &mut data_ptr) + }; + if hr.is_err() { + // Callee may have allocated a buffer before returning failure. + // Wrap in ArrayData to release elements + CoTaskMemFree. + if !data_ptr.is_null() { + let _ = crate::array::ArrayData::from_cotaskmem( + elem_type.clone(), + data_ptr, + length as usize, + ); + } + hr.ok()?; + } + let array = if data_ptr.is_null() || length == 0 { + if !data_ptr.is_null() { + unsafe { + windows::Win32::System::Com::CoTaskMemFree(Some(data_ptr)); + } + } + + crate::array::ArrayData::empty(elem_type) + } else { + crate::array::ArrayData::from_cotaskmem(elem_type, data_ptr, length as usize) + }; + Ok(vec![WinRTValue::Array(array)]) + } + CallStrategy::DirectPassArray1Out => { + // fn(this, u32, *const u8, out) -> HRESULT + let in_param = self.info.parameters.iter().find(|p| p.is_input()).unwrap(); + let out_param = self.info.parameters.iter().find(|p| p.is_out()).unwrap(); + let array_data = args.get_value(in_param.value_index).as_array().unwrap(); + let buffer = array_data.serialize_for_abi(); + let mut out = out_param.typ.default_value(); + let fptr = call::get_vtable_function_ptr(obj, self.info.index); + let hr: windows_core::HRESULT = unsafe { + let method: unsafe extern "system" fn( + *mut std::ffi::c_void, + u32, + *const u8, + *mut std::ffi::c_void, + ) + -> windows_core::HRESULT = std::mem::transmute(fptr); + method(obj, array_data.len() as u32, buffer.as_ptr(), out.out_ptr()) + }; + hr.ok()?; + if let WinRTValue::RawPtr(raw_ptr) = out { + out = out_param.typ.from_out(raw_ptr).map_err(|e| { + windows_core::Error::new(windows_core::HRESULT(-1), &format!("{:?}", e)) + })?; + } + out.sanitize_null_object(); + Ok(vec![out]) + } + CallStrategy::DirectFillArray => { + // fn(this, u32, *mut u8) -> HRESULT + // FillArray: caller provides buffer of known capacity, callee fills it. + let param = &self.info.parameters[0]; + let elem_type = param.typ.array_element_type(); + let fptr = call::get_vtable_function_ptr(obj, self.info.index); + + assert!( + param + .input_index + .is_some_and(|index| { args.get_value(index).as_array().is_some() }), + "DirectFillArray requires a pre-allocated array argument with the desired capacity. \ + Pass an ArrayData with the expected number of elements." + ); + let array_data = args + .get_value(param.input_index.unwrap()) + .as_array() + .unwrap(); + let capacity = array_data.len() as u32; + let total_bytes = capacity as usize * elem_type.element_size(); + let buffer_ptr = + unsafe { windows::Win32::System::Com::CoTaskMemAlloc(total_bytes) as *mut u8 }; + assert!(!buffer_ptr.is_null(), "CoTaskMemAlloc failed for FillArray"); + unsafe { std::ptr::write_bytes(buffer_ptr, 0, total_bytes) }; + let hr: windows_core::HRESULT = unsafe { + let method: unsafe extern "system" fn( + *mut std::ffi::c_void, + u32, + *mut u8, + ) + -> windows_core::HRESULT = std::mem::transmute(fptr); + method(obj, capacity, buffer_ptr) + }; + if hr.is_err() { + // Callee may have written elements before failing. + // Buffer was zero-initialized, so null slots are safe to release. + // Use capacity as cleanup length — ArrayData::Drop skips null elements. + let _ = crate::array::ArrayData::from_cotaskmem( + elem_type.clone(), + buffer_ptr as _, + capacity as usize, + ); + hr.ok()?; + } + let array = crate::array::ArrayData::from_cotaskmem( + elem_type, + buffer_ptr as _, + capacity as usize, + ); + Ok(vec![WinRTValue::Array(array)]) + } + CallStrategy::Direct1InFillArray => { + // fn(this, val, u32, *mut u8) -> HRESULT + let in_param = self.info.parameters.iter().find(|p| p.is_input()).unwrap(); + let fill_param = self + .info + .parameters + .iter() + .find(|p| p.is_fill_array()) + .unwrap(); + let array_data = args + .get_value(fill_param.input_index.unwrap()) + .as_array() + .unwrap(); + let elem_type = fill_param.typ.array_element_type(); + let capacity = array_data.len() as u32; + let total_bytes = capacity as usize * elem_type.element_size(); + let buffer_ptr = + unsafe { windows::Win32::System::Com::CoTaskMemAlloc(total_bytes) as *mut u8 }; + assert!(!buffer_ptr.is_null(), "CoTaskMemAlloc failed for FillArray"); + unsafe { std::ptr::write_bytes(buffer_ptr, 0, total_bytes) }; + let fptr = call::get_vtable_function_ptr(obj, self.info.index); + let hr = call::call_fill_array_1in( + fptr, + obj, + args.get_value(in_param.value_index), + capacity, + buffer_ptr, + ); + if hr.is_err() { + // Buffer was zero-initialized; use capacity for cleanup. + let _ = crate::array::ArrayData::from_cotaskmem( + elem_type.clone(), + buffer_ptr as _, + capacity as usize, + ); + hr.ok()?; + } + let array = crate::array::ArrayData::from_cotaskmem( + elem_type, + buffer_ptr as _, + capacity as usize, + ); + Ok(vec![WinRTValue::Array(array)]) + } + CallStrategy::Libffi(cif) => call::call_method_dynamic( + self.info.index, + obj, + &self.info.parameters, + &args, + self.info.out_count, + &self.info.return_kind, + cif, + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicU32, Ordering}; + use windows::Foundation::{IStringable, IUriRuntimeClass, Uri}; + use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize}; + use windows_core::{IInspectable, Interface, h}; + + #[repr(C)] + struct FakeComObject { + vtable: *const *mut std::ffi::c_void, + calls: AtomicU32, + } + + unsafe extern "system" fn increment_struct_first_field( + this: *mut std::ffi::c_void, + value: *mut i32, + ) -> windows_core::HRESULT { + let object = unsafe { &*(this as *const FakeComObject) }; + object.calls.fetch_add(1, Ordering::Relaxed); + unsafe { *value += 1 }; + windows_core::HRESULT(0) + } + + fn struct_in_out_method( + table: &Arc, + expected: TypeHandle, + ) -> (Method, FakeComObject, Box<[*mut std::ffi::c_void; 1]>) { + let method = AbiMethodSignature::new(table) + .add_in_out_type(ParameterType::winrt(expected)) + .build(0); + let vtable = Box::new([increment_struct_first_field as *mut std::ffi::c_void]); + let object = FakeComObject { + vtable: vtable.as_ptr(), + calls: AtomicU32::new(0), + }; + (method, object, vtable) + } + + #[test] + fn fill_array_tracks_distinct_input_and_output_indices() { + let table = MetadataTable::new(); + let method = AbiMethodSignature::new(&table) + .add_in_type(ParameterType::winrt(table.u32_type())) + .add_out_fill_type(ParameterType::winrt(table.array(&table.hstring()))) + .add_out_type(ParameterType::winrt(table.u32_type())) + .build(6); + + assert_eq!(method.info.parameters[0].value_index, 0); + assert_eq!(method.info.parameters[0].input_index, Some(0)); + assert_eq!(method.info.parameters[1].value_index, 0); + assert_eq!(method.info.parameters[1].input_index, Some(1)); + assert_eq!(method.info.parameters[2].value_index, 1); + assert_eq!(method.info.parameters[2].input_index, None); + } + + #[test] + fn coerces_object_inputs_to_the_expected_interface() -> windows_core::Result<()> { + let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; + let uri = Uri::CreateUri(h!("https://example.com"))?; + let default_interface: IUriRuntimeClass = uri.cast()?; + let expected_interface: IStringable = uri.cast()?; + assert_ne!( + default_interface.as_raw(), + expected_interface.as_raw(), + "test requires distinct default and requested interface pointers" + ); + + let table = MetadataTable::new(); + let expected_type = table.interface(IStringable::IID); + let value = WinRTValue::Object(default_interface.cast()?); + let coerced = coerce_input_object(&expected_type, &value)? + .expect("interface parameters must be coerced"); + assert_eq!( + coerced.as_object().unwrap().as_raw(), + expected_interface.as_raw() + ); + + let inspectable: IInspectable = uri.cast()?; + let coerced_object = coerce_input_object(&table.object(), &value)? + .expect("Object parameters must be coerced to IInspectable"); + assert_eq!( + coerced_object.as_object().unwrap().as_raw(), + inspectable.as_raw() + ); + Ok(()) + } + + #[test] + fn raw_pointer_is_rejected_for_winrt_object_params() { + let table = MetadataTable::new(); + let bogus = WinRTValue::RawPtr(0xDEADBEEF as *mut std::ffi::c_void); + + let object_ty = table.object(); + let object_err = coerce_input_object(&object_ty, &bogus) + .expect_err("RawPtr into Object must be rejected"); + assert_eq!(object_err.code().0, 0x80070057u32 as i32); + + let iface_ty = table.interface(IStringable::IID); + let err = coerce_input_object(&iface_ty, &bogus) + .expect_err("RawPtr into a typed interface must be rejected"); + assert_eq!( + err.code().0, + 0x80070057u32 as i32, + "typed-interface RawPtr rejection must use E_INVALIDARG (got {:?})", + err + ); + let msg = err.message(); + assert!( + msg.contains("raw pointer") && msg.contains("typed"), + "rejection error must explain the constraint, got: {}", + msg + ); + + let null_object = WinRTValue::Null; + assert!( + coerce_input_object(&object_ty, &null_object) + .expect("null into TypeKind::Object must be allowed") + .is_none(), + ); + assert!( + coerce_input_object(&iface_ty, &null_object) + .expect("null into typed interface must be allowed") + .is_none(), + ); + } + + #[test] + fn struct_in_out_rejects_different_sized_type_before_native_call() { + let table = MetadataTable::new(); + let expected = + table.struct_type("Test.ExpectedLarge", &[table.i64_type(), table.i64_type()]); + let actual = table.struct_type("Test.ActualSmall", &[table.i32_type()]); + let (method, mut object, _vtable) = struct_in_out_method(&table, expected); + + let error = method + .call_dynamic( + (&mut object as *mut FakeComObject).cast(), + &[WinRTValue::Struct(actual.default_value())], + ) + .expect_err("different-sized struct must be rejected"); + + assert_eq!(error.code().0, 0x80070057u32 as i32); + assert!(error.message().contains("Struct type mismatch")); + assert_eq!(object.calls.load(Ordering::Relaxed), 0); + } + + #[test] + fn struct_in_out_rejects_same_sized_different_type() { + let table = MetadataTable::new(); + let expected = table.struct_type("Test.ExpectedI64", &[table.i64_type()]); + let actual = table.struct_type("Test.ActualF64", &[table.f64_type()]); + assert_eq!(expected.layout(), actual.layout()); + let (method, mut object, _vtable) = struct_in_out_method(&table, expected); + + let error = method + .call_dynamic( + (&mut object as *mut FakeComObject).cast(), + &[WinRTValue::Struct(actual.default_value())], + ) + .expect_err("same-sized different struct must be rejected"); + + assert_eq!(error.code().0, 0x80070057u32 as i32); + assert!(error.message().contains("Struct type mismatch")); + assert_eq!(object.calls.load(Ordering::Relaxed), 0); + } + + #[test] + fn struct_in_out_accepts_exact_type_and_returns_updated_value() -> windows_core::Result<()> { + let table = MetadataTable::new(); + let expected = table.struct_type("Test.Counter", &[table.i32_type()]); + let mut actual = expected.default_value(); + actual.set_field(0, 41i32); + let (method, mut object, _vtable) = struct_in_out_method(&table, expected); + + let result = method.call_dynamic( + (&mut object as *mut FakeComObject).cast(), + &[WinRTValue::Struct(actual)], + )?; + let WinRTValue::Struct(value) = &result[0] else { + panic!("expected struct result"); + }; + + assert_eq!(value.get_field::(0), 42); + assert_eq!(object.calls.load(Ordering::Relaxed), 1); + Ok(()) + } + + #[test] + fn struct_array_rejects_different_element_type() { + let table = MetadataTable::new(); + let expected_element = table.struct_type( + "Test.ExpectedArrayElement", + &[table.i64_type(), table.i64_type()], + ); + let actual_element = table.struct_type("Test.ActualArrayElement", &[table.i32_type()]); + let expected_array = table.array(&expected_element); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + actual_element.clone(), + &[WinRTValue::Struct(actual_element.default_value())], + )); + + let error = coerce_input_array(&expected_array, &value) + .expect_err("different struct array element type must be rejected"); + + assert_eq!(error.code().0, 0x80070057u32 as i32); + assert!(error.message().contains("Array element type mismatch")); + } + + #[test] + fn struct_array_rejects_value_that_lies_about_declared_element_type() { + let table = MetadataTable::new(); + let expected_element = table.struct_type( + "Test.ExpectedDeclaredElement", + &[table.i64_type(), table.i64_type()], + ); + let actual_element = table.struct_type("Test.ActualStoredElement", &[table.i32_type()]); + let expected_array = table.array(&expected_element); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + expected_element.clone(), + &[WinRTValue::Struct(actual_element.default_value())], + )); + + let error = coerce_input_array(&expected_array, &value) + .expect_err("mismatched stored struct value must be rejected"); + + assert_eq!(error.code().0, 0x80070057u32 as i32); + assert!(error.message().contains("Array element 0")); + assert!(error.message().contains("Struct type mismatch")); + } + + #[test] + fn struct_array_accepts_exact_element_type() -> windows_core::Result<()> { + let table = MetadataTable::new(); + let element = table.struct_type("Test.ValidArrayElement", &[table.i32_type()]); + let expected_array = table.array(&element); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + element.clone(), + &[WinRTValue::Struct(element.default_value())], + )); + + assert!(coerce_input_array(&expected_array, &value)?.is_none()); + Ok(()) + } + + #[test] + fn primitive_array_accepts_equivalent_type_from_another_table() -> windows_core::Result<()> { + let signature_table = MetadataTable::new(); + let value_table = MetadataTable::new(); + let expected_array = signature_table.array(&signature_table.i32_type()); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + value_table.i32_type(), + &[WinRTValue::I32(42)], + )); + + assert!(coerce_input_array(&expected_array, &value)?.is_none()); + Ok(()) + } + + #[test] + fn char16_array_accepts_u16_projection() -> windows_core::Result<()> { + let table = MetadataTable::new(); + let expected_array = table.array(&table.char16_type()); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + table.u16_type(), + &[WinRTValue::U16('x' as u16)], + )); + + assert!(coerce_input_array(&expected_array, &value)?.is_none()); + Ok(()) + } + + #[test] + fn enum_array_accepts_i32_projection() -> windows_core::Result<()> { + let table = MetadataTable::new(); + let enum_type = table.enum_type("Test.ProjectedEnum", vec![("Value".to_string(), 7)]); + let expected_array = table.array(&enum_type); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + table.i32_type(), + &[WinRTValue::I32(7)], + )); + + assert!(coerce_input_array(&expected_array, &value)?.is_none()); + Ok(()) + } + + #[test] + fn enum_array_rejects_a_different_named_enum() { + let table = MetadataTable::new(); + let expected = table.enum_type("Test.ExpectedEnum", Vec::new()); + let actual = table.enum_type("Test.ActualEnum", Vec::new()); + let expected_array = table.array(&expected); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + actual.clone(), + &[WinRTValue::Enum { + value: 0, + type_handle: actual, + }], + )); + + let error = coerce_input_array(&expected_array, &value) + .expect_err("different named enum array must be rejected"); + + assert_eq!(error.code().0, 0x80070057u32 as i32); + assert!(error.message().contains("Array element type mismatch")); + } + + #[test] + fn struct_array_rejects_equivalent_layout_from_another_table() { + let signature_table = MetadataTable::new(); + let value_table = MetadataTable::new(); + let expected_element = + signature_table.struct_type("Test.CrossTable", &[signature_table.i32_type()]); + let actual_element = value_table.struct_type("Test.CrossTable", &[value_table.i32_type()]); + assert_eq!( + expected_element.signature_string(), + actual_element.signature_string() + ); + assert_eq!(expected_element.layout(), actual_element.layout()); + let expected_array = signature_table.array(&expected_element); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + actual_element.clone(), + &[WinRTValue::Struct(actual_element.default_value())], + )); + + let error = coerce_input_array(&expected_array, &value) + .expect_err("struct identity from another table must be rejected"); + + assert_eq!(error.code().0, 0x80070057u32 as i32); + assert!(error.message().contains("Array element type mismatch")); + } + + #[test] + fn coerces_object_array_elements_to_the_expected_interface() -> windows_core::Result<()> { + let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; + let uri = Uri::CreateUri(h!("https://example.com"))?; + let default_interface: IUriRuntimeClass = uri.cast()?; + let expected_interface: IStringable = uri.cast()?; + + let table = MetadataTable::new(); + let element_type = table.interface(IStringable::IID); + let array_type = table.array(&element_type); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + element_type, + &[WinRTValue::Object(default_interface.cast()?)], + )); + let coerced = coerce_input_array(&array_type, &value)? + .expect("object array elements must be coerced"); + assert_eq!( + coerced + .as_array() + .unwrap() + .get(0) + .as_object() + .unwrap() + .as_raw(), + expected_interface.as_raw() + ); + Ok(()) + } +} diff --git a/crates/dynwinrt/src/signature.rs b/crates/dynwinrt/src/signature.rs index 69e637cb..edb885a8 100644 --- a/crates/dynwinrt/src/signature.rs +++ b/crates/dynwinrt/src/signature.rs @@ -1,448 +1,26 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use libffi::middle::Cif; +//! WinRT method planning. +//! +//! This public surface intentionally exposes only WinRT's HRESULT plus +//! input/output conventions. Classic COM lowers through its own planner in +//! `com`, while both planners share the private `native_call` executor. + use std::sync::Arc; -use windows::core::{GUID, HSTRING, IInspectable, Interface}; + +use windows::core::{GUID, HSTRING}; use crate::{ - abi::{AbiType, AbiValue}, - call, - call::ArgumentList, - metadata_table::{MetadataTable, TypeHandle, TypeKind}, + metadata_table::{MetadataTable, TypeHandle}, + native_call::{AbiMethodSignature, Method as NativeMethod, ParameterType}, value::WinRTValue, }; -#[derive(Debug, Clone)] -pub(crate) enum ParameterType { - WinRT(TypeHandle), - Pointer, -} - -impl ParameterType { - pub(crate) fn winrt(typ: TypeHandle) -> Self { - Self::WinRT(typ) - } - - pub(crate) fn pointer() -> Self { - Self::Pointer - } - - pub(crate) fn as_winrt(&self) -> Option<&TypeHandle> { - match self { - Self::WinRT(typ) => Some(typ), - Self::Pointer => None, - } - } - - pub(crate) fn is_array(&self) -> bool { - self.as_winrt().is_some_and(TypeHandle::is_array) - } - - pub(crate) fn is_struct(&self) -> bool { - matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::Struct(_))) - } - - pub(crate) fn is_hstring(&self) -> bool { - matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::HString)) - } - - pub(crate) fn is_u32(&self) -> bool { - matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::U32)) - } - - pub(crate) fn is_guid(&self) -> bool { - matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::Guid)) - } - - pub(crate) fn supports_in_out(&self) -> bool { - matches!(self, Self::Pointer) - || matches!( - self, - Self::WinRT(typ) - if matches!( - typ.kind(), - TypeKind::Bool - | TypeKind::I8 - | TypeKind::U8 - | TypeKind::I16 - | TypeKind::U16 - | TypeKind::Char16 - | TypeKind::I32 - | TypeKind::U32 - | TypeKind::I64 - | TypeKind::U64 - | TypeKind::F32 - | TypeKind::F64 - | TypeKind::HResult - | TypeKind::Enum(_) - | TypeKind::Struct(_) - ) - ) - } - - pub(crate) fn supports_direct_return(&self) -> bool { - matches!(self, Self::Pointer) - || matches!( - self, - Self::WinRT(typ) - if matches!( - typ.kind(), - TypeKind::Bool - | TypeKind::I8 - | TypeKind::U8 - | TypeKind::I16 - | TypeKind::U16 - | TypeKind::Char16 - | TypeKind::I32 - | TypeKind::U32 - | TypeKind::I64 - | TypeKind::U64 - | TypeKind::F32 - | TypeKind::F64 - | TypeKind::HResult - | TypeKind::Enum(_) - ) - ) - } - - pub(crate) fn abi_type(&self) -> AbiType { - match self { - Self::WinRT(typ) => typ.abi_type(), - Self::Pointer => AbiType::Ptr, - } - } - - pub(crate) fn libffi_type(&self) -> libffi::middle::Type { - match self { - Self::WinRT(typ) => typ.libffi_type(), - Self::Pointer => libffi::middle::Type::pointer(), - } - } - - pub(crate) fn array_element_type(&self) -> TypeHandle { - self.as_winrt() - .expect("native pointer is not an array") - .array_element_type() - } - - pub(crate) fn default_struct_value(&self) -> crate::metadata_table::ValueTypeData { - self.as_winrt() - .expect("native pointer is not a struct") - .default_value() - } - - pub(crate) fn default_value(&self) -> WinRTValue { - match self { - Self::WinRT(typ) => typ.default_winrt_value(), - Self::Pointer => WinRTValue::RawPtr(std::ptr::null_mut()), - } - } - - pub(crate) fn from_out(&self, ptr: *mut std::ffi::c_void) -> crate::result::Result { - match self { - Self::WinRT(typ) => typ.from_out(ptr), - Self::Pointer => Ok(WinRTValue::RawPtr(ptr)), - } - } - - pub(crate) fn from_out_value(&self, value: &AbiValue) -> crate::result::Result { - match (self, value) { - (Self::WinRT(typ), value) => typ.from_out_value(value), - (Self::Pointer, AbiValue::Pointer(ptr)) => Ok(WinRTValue::RawPtr(*ptr)), - (Self::Pointer, value) => Err(crate::result::Error::InvalidTypeAbiToWinRT( - TypeKind::Object, - value.abi_type(), - )), - } - } -} - -/// How a parameter is passed at the ABI level. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ParamKind { - In, - Out, - InOut, - /// FillArray: caller allocates buffer, callee fills it. - /// ABI expands to 2 params: (u32 capacity, T* items). - OutFillArray, -} - -#[derive(Debug, Clone)] -pub struct Parameter { - pub(crate) typ: ParameterType, - /// Index in the method result vector for out and FillArray parameters. - pub value_index: usize, - /// Index in the caller-provided argument slice. FillArray parameters have - /// both an input index (capacity buffer) and an output index (filled data). - pub input_index: Option, - pub kind: ParamKind, -} - -impl Parameter { - pub fn is_input(&self) -> bool { - matches!(self.kind, ParamKind::In | ParamKind::InOut) - } - - pub fn is_out(&self) -> bool { - matches!( - self.kind, - ParamKind::Out | ParamKind::InOut | ParamKind::OutFillArray - ) - } - - pub fn is_in_out(&self) -> bool { - self.kind == ParamKind::InOut - } - - pub fn is_fill_array(&self) -> bool { - self.kind == ParamKind::OutFillArray - } -} - -#[derive(Debug, Clone)] -pub(crate) struct AbiMethodSignature { - out_count: usize, - input_count: usize, - parameters: Vec, - return_kind: MethodReturn, - #[allow(dead_code)] - is_opaque: bool, - #[allow(dead_code)] - table: Arc, -} - -#[derive(Debug, Clone)] -pub(crate) enum MethodReturn { - HResult, - Void, - Value(ParameterType), -} - -impl MethodReturn { - fn libffi_type(&self) -> libffi::middle::Type { - match self { - Self::HResult => libffi::middle::Type::i32(), - Self::Void => libffi::middle::Type::void(), - Self::Value(typ) => typ.libffi_type(), - } - } -} - -impl AbiMethodSignature { - pub(crate) fn new(table: &Arc) -> Self { - AbiMethodSignature { - out_count: 0, - input_count: 0, - parameters: Vec::new(), - return_kind: MethodReturn::HResult, - is_opaque: false, - table: Arc::clone(table), - } - } - - pub(crate) fn add_in_type(mut self, typ: ParameterType) -> Self { - let input_index = self.input_count; - self.input_count += 1; - self.parameters.push(Parameter { - kind: ParamKind::In, - typ, - value_index: input_index, - input_index: Some(input_index), - }); - self - } - - pub(crate) fn add_out_type(mut self, typ: ParameterType) -> Self { - self.parameters.push(Parameter { - kind: ParamKind::Out, - typ, - value_index: self.out_count, - input_index: None, - }); - self.out_count += 1; - self - } - - pub(crate) fn add_in_out_type(mut self, typ: ParameterType) -> Self { - assert!( - typ.supports_in_out(), - "in/out currently supports native scalars, pointers, enums, and structs" - ); - let input_index = self.input_count; - self.input_count += 1; - self.parameters.push(Parameter { - kind: ParamKind::InOut, - typ, - value_index: self.out_count, - input_index: Some(input_index), - }); - self.out_count += 1; - self - } - - pub(crate) fn add_out_fill_type(mut self, typ: ParameterType) -> Self { - let input_index = self.input_count; - self.input_count += 1; - self.parameters.push(Parameter { - kind: ParamKind::OutFillArray, - typ, - value_index: self.out_count, - input_index: Some(input_index), - }); - self.out_count += 1; - self - } - - pub(crate) fn returns_type(mut self, typ: ParameterType) -> Self { - assert!( - typ.supports_direct_return(), - "direct native returns currently support scalars, enums, and pointers" - ); - self.return_kind = MethodReturn::Value(typ); - self - } - - pub(crate) fn returns_void(mut self) -> Self { - self.return_kind = MethodReturn::Void; - self - } - - pub(crate) fn build(self, index: usize) -> Method { - use libffi::middle::Type; - let mut types: Vec = Vec::with_capacity(self.parameters.len() + 1); - types.push(Type::pointer()); // com object's this pointer - for param in &self.parameters { - if param.is_fill_array() { - // FillArray: UINT32 capacity, T* items - types.push(Type::u32()); - types.push(Type::pointer()); - } else if param.typ.is_array() { - if param.is_out() { - // ReceiveArray: UINT32* out_length, T** out_data - types.push(Type::pointer()); - types.push(Type::pointer()); - } else { - // PassArray: UINT32 length, T* data - types.push(Type::u32()); - types.push(Type::pointer()); - } - } else if param.is_out() { - types.push(Type::pointer()); - } else { - types.push(param.typ.libffi_type()); - } - } - let in_count = self.parameters.iter().filter(|p| p.is_input()).count(); - let has_complex_param = self - .parameters - .iter() - .any(|p| p.typ.is_array() || p.is_fill_array() || p.is_in_out() || p.typ.is_struct()); - - // Check if the single in-param (if any) is a simple non-HString, non-Struct type - let simple_in = !has_complex_param && in_count == 1 && { - let in_param = self.parameters.iter().find(|p| p.is_input()).unwrap(); - !in_param.typ.is_hstring() - }; - - // Classify array parameters - let array_in_count = self - .parameters - .iter() - .filter(|p| p.is_input() && p.typ.is_array()) - .count(); - let fill_out_count = self.parameters.iter().filter(|p| p.is_fill_array()).count(); - let array_out_count = self - .parameters - .iter() - .filter(|p| p.is_out() && p.typ.is_array() && !p.is_fill_array()) - .count(); - let scalar_in_count = in_count - array_in_count; - let scalar_out_count = self.out_count - fill_out_count - array_out_count; - - let returns_hresult = matches!(self.return_kind, MethodReturn::HResult); - let strategy = if returns_hresult - && !has_complex_param - && in_count == 0 - && self.out_count == 1 - { - CallStrategy::Direct0In1Out - } else if returns_hresult && !has_complex_param && in_count == 0 && self.out_count == 0 { - CallStrategy::Direct0In0Out - } else if returns_hresult && simple_in && self.out_count == 0 { - CallStrategy::Direct1In0Out - } else if returns_hresult && simple_in && self.out_count == 1 { - CallStrategy::Direct1In1Out - // ReceiveArray only: fn(this, *mut u32, *mut *mut c_void) -> HRESULT - } else if returns_hresult - && scalar_in_count == 0 - && array_in_count == 0 - && array_out_count == 1 - && fill_out_count == 0 - && scalar_out_count == 0 - { - CallStrategy::DirectReceiveArray - // PassArray + 1 out: fn(this, u32, *const u8, out) -> HRESULT - } else if returns_hresult - && scalar_in_count == 0 - && array_in_count == 1 - && array_out_count == 0 - && fill_out_count == 0 - && scalar_out_count == 1 - { - CallStrategy::DirectPassArray1Out - // FillArray only: fn(this, u32, *mut u8, *mut u32) -> HRESULT - } else if returns_hresult - && scalar_in_count == 0 - && array_in_count == 0 - && fill_out_count == 1 - && array_out_count == 0 - && scalar_out_count == 0 - { - CallStrategy::DirectFillArray - // 1 scalar in + FillArray: fn(this, val, u32, *mut u8, *mut u32) -> HRESULT - } else if returns_hresult - && scalar_in_count == 1 - && array_in_count == 0 - && fill_out_count == 1 - && array_out_count == 0 - && scalar_out_count == 0 - { - let in_param = self - .parameters - .iter() - .find(|p| p.is_input() && !p.typ.is_array()) - .unwrap(); - if !in_param.typ.is_hstring() && !in_param.typ.is_struct() { - CallStrategy::Direct1InFillArray - } else { - CallStrategy::Libffi(Cif::new(types.into_iter(), self.return_kind.libffi_type())) - } - } else { - CallStrategy::Libffi(Cif::new(types.into_iter(), self.return_kind.libffi_type())) - }; - - Method { - info: MethodInfo { - index, - parameters: self.parameters, - out_count: self.out_count, - return_kind: self.return_kind, - }, - strategy, - } - } -} - #[derive(Debug, Clone)] pub struct MethodSignature(AbiMethodSignature); impl MethodSignature { - pub(crate) fn from_abi(signature: AbiMethodSignature) -> Self { - Self(signature) - } - pub fn new(table: &Arc) -> Self { Self(AbiMethodSignature::new(table)) } @@ -464,244 +42,34 @@ impl MethodSignature { } pub fn build(self, index: usize) -> Method { - self.0.build(index) + Method(self.0.build(index)) } } #[derive(Debug)] -pub struct MethodInfo { - pub index: usize, - pub parameters: Vec, - pub out_count: usize, - pub(crate) return_kind: MethodReturn, -} - -/// How a Method should be invoked — decided once at build time. -#[derive(Debug)] -enum CallStrategy { - /// 0 in + 0 out: fn(this) -> HRESULT. - Direct0In0Out, - /// 0 in + 1 out (getter): fn(this, out) -> HRESULT. - Direct0In1Out, - /// 1 in + 0 out (setter, non-HString): fn(this, val) -> HRESULT. - Direct1In0Out, - /// 1 in + 1 out (factory/query, non-HString in): fn(this, val, out) -> HRESULT. - Direct1In1Out, - /// ReceiveArray: fn(this, *mut u32, *mut *mut c_void) -> HRESULT. - DirectReceiveArray, - /// PassArray + 1 out: fn(this, u32, *const u8, out) -> HRESULT. - DirectPassArray1Out, - /// FillArray only: fn(this, u32, *mut u8) -> HRESULT. - DirectFillArray, - /// 1 scalar in + FillArray: fn(this, val, u32, *mut u8) -> HRESULT. - Direct1InFillArray, - /// General case → libffi via cached Cif. - Libffi(Cif), -} - -#[derive(Debug)] -pub struct Method { - info: MethodInfo, - strategy: CallStrategy, -} - -fn expected_object_iid(typ: &TypeHandle) -> Option { - match typ.kind() { - TypeKind::Object => Some(IInspectable::IID), - TypeKind::Interface(_) - | TypeKind::Delegate(_) - | TypeKind::RuntimeClass(_) - | TypeKind::Parameterized(_) - | TypeKind::IAsyncAction - | TypeKind::IAsyncActionWithProgress(_) - | TypeKind::IAsyncOperation(_) - | TypeKind::IAsyncOperationWithProgress(_) => typ.iid(), - _ => None, - } -} - -fn coerce_input_object( - expected: &TypeHandle, - value: &WinRTValue, -) -> windows_core::Result> { - let Some(iid) = expected_object_iid(expected) else { - return Ok(None); - }; - // Null objects are always allowed (`WinRTValue::Null` and the - // Object-typed null variant both project as "no coercion needed" — the - // ABI receives a null pointer directly). - if value.is_null_object() { - return Ok(None); - } - // Raw pointers never satisfy a WinRT object parameter. Otherwise arbitrary - // pointer bits could reach a typed COM slot without QueryInterface validation. - if matches!(value, WinRTValue::RawPtr(_)) { - return Err(windows_core::Error::new( - windows_core::HRESULT(0x80070057u32 as i32), - &format!( - "Refusing to pass a raw pointer as a typed COM parameter ({}). \ - Use a Pointer signature for native pointers and handles; for \ - COM parameters pass a real object (or one obtained via `.cast(IID)`).", - expected.signature_string(), - ), - )); - } - - let object = value.as_object().ok_or_else(|| { - windows_core::Error::new( - windows_core::HRESULT(0x80070057u32 as i32), - &format!( - "Expected object argument for {}, found {:?}", - expected.signature_string(), - value.get_type_kind() - ), - ) - })?; - - WinRTValue::Object(object) - .cast(&iid) - .map(Some) - .map_err(|error| match error { - crate::result::Error::WindowsError(error) => error, - other => windows_core::Error::new( - windows_core::HRESULT(0x80070057u32 as i32), - &other.message(), - ), - }) -} - -fn coerce_input_array( - expected: &TypeHandle, - value: &WinRTValue, -) -> windows_core::Result> { - if !expected.is_array() { - return Ok(None); - } - - let element_type = expected.array_element_type(); - if expected_object_iid(&element_type).is_none() { - return Ok(None); - } - - let array = value.as_array().ok_or_else(|| { - windows_core::Error::new( - windows_core::HRESULT(0x80070057u32 as i32), - &format!( - "Expected array argument for {}, found {:?}", - expected.signature_string(), - value.get_type_kind() - ), - ) - })?; - let mut values = Vec::with_capacity(array.len()); - let mut changed = false; - for index in 0..array.len() { - let value = array.get(index); - if let Some(coerced) = coerce_input_object(&element_type, &value)? { - values.push(coerced); - changed = true; - } else { - values.push(value); - } - } - - Ok(changed - .then(|| WinRTValue::Array(crate::array::ArrayData::from_values(element_type, &values)))) -} - -struct InvocationArgs<'a> { - original: &'a [WinRTValue], - replacements: Option>>, -} - -impl<'a> InvocationArgs<'a> { - fn new(original: &'a [WinRTValue]) -> Self { - Self { - original, - replacements: None, - } - } - - fn replace(&mut self, index: usize, value: WinRTValue) { - self.replacements.get_or_insert_with(|| { - std::iter::repeat_with(|| None) - .take(self.original.len()) - .collect() - })[index] = Some(value); - } -} - -impl call::ArgumentList for InvocationArgs<'_> { - fn get_value(&self, index: usize) -> &WinRTValue { - self.replacements - .as_ref() - .and_then(|values| values[index].as_ref()) - .unwrap_or(&self.original[index]) - } -} +pub struct Method(NativeMethod); impl Method { - // --- Fast getter paths: zero Vec/WinRTValue allocation --- - - /// Getter → i32 (0 in, 1 out). Writes directly to stack i32. pub fn call_getter_i32(&self, obj: *mut std::ffi::c_void) -> windows_core::Result { - let mut out: i32 = 0; - let hr = call::call_winrt_method_1( - self.info.index, - obj, - &mut out as *mut i32 as *mut std::ffi::c_void, - ); - hr.ok()?; - Ok(out) + self.0.call_getter_i32(obj) } - /// Getter → bool (0 in, 1 out). Writes directly to stack bool. pub fn call_getter_bool(&self, obj: *mut std::ffi::c_void) -> windows_core::Result { - let mut out: i32 = 0; // WinRT bool is i32 on ABI - let hr = call::call_winrt_method_1( - self.info.index, - obj, - &mut out as *mut i32 as *mut std::ffi::c_void, - ); - hr.ok()?; - Ok(out != 0) + self.0.call_getter_bool(obj) } - /// Getter → HSTRING (0 in, 1 out). Writes directly to stack HSTRING ptr. pub fn call_getter_hstring( &self, obj: *mut std::ffi::c_void, ) -> windows_core::Result { - // HSTRING is a pointer-sized handle on ABI. Let WinRT write it directly. - let mut out = windows_core::HSTRING::new(); - let hr = call::call_winrt_method_1( - self.info.index, - obj, - &mut out as *mut windows_core::HSTRING as *mut std::ffi::c_void, - ); - hr.ok()?; - Ok(out) + self.0.call_getter_hstring(obj) } - /// Getter → COM object (0 in, 1 out). Writes directly to stack pointer. pub fn call_getter_object( &self, obj: *mut std::ffi::c_void, ) -> windows_core::Result { - let mut out: *mut std::ffi::c_void = std::ptr::null_mut(); - let hr = call::call_winrt_method_1( - self.info.index, - obj, - &mut out as *mut _ as *mut std::ffi::c_void, - ); - hr.ok()?; - if out.is_null() { - Ok(WinRTValue::Null) - } else { - Ok(WinRTValue::Object(unsafe { - windows_core::IUnknown::from_raw(out) - })) - } + self.0.call_getter_object(obj) } pub fn call_dynamic( @@ -709,260 +77,21 @@ impl Method { obj: *mut std::ffi::c_void, args: &[WinRTValue], ) -> windows_core::Result> { - let mut args = InvocationArgs::new(args); - for parameter in self.info.parameters.iter().filter(|p| p.is_input()) { - let input_index = parameter.input_index.expect("input parameter index"); - let value = args.get_value(input_index); - let coerced = if let Some(typ) = parameter.typ.as_winrt() { - if typ.is_array() { - coerce_input_array(typ, value)? - } else { - coerce_input_object(typ, value)? - } - } else { - None - }; - if let Some(value) = coerced { - args.replace(input_index, value); - } - } - - match &self.strategy { - CallStrategy::Direct0In0Out => { - // 0 in + 0 out: fn(this) -> HRESULT - let hr = call::call_winrt_method_0(self.info.index, obj); - hr.ok()?; - Ok(vec![]) - } - CallStrategy::Direct0In1Out => { - // 0 in + 1 out: fn(this, out) -> HRESULT - let param = &self.info.parameters[0]; - let mut out = param.typ.default_value(); - let hr = call::call_winrt_method_1(self.info.index, obj, out.out_ptr()); - hr.ok()?; - // COM pointer types use RawPtr(null) as buffer to avoid IUnknown::from_raw(null) UB. - // After COM writes the pointer, convert via from_out. - if let WinRTValue::RawPtr(raw_ptr) = out { - out = param.typ.from_out(raw_ptr).map_err(|e| { - windows_core::Error::new(windows_core::HRESULT(-1), &format!("{:?}", e)) - })?; - } - out.sanitize_null_object(); - Ok(vec![out]) - } - CallStrategy::Direct1In0Out => { - // 1 in + 0 out: fn(this, val) -> HRESULT - let hr = call::call_1in(self.info.index, obj, args.get_value(0)); - hr.ok()?; - Ok(vec![]) - } - CallStrategy::Direct1In1Out => { - // 1 in + 1 out: fn(this, val, out) -> HRESULT - let out_param = self.info.parameters.iter().find(|p| p.is_out()).unwrap(); - let mut out = out_param.typ.default_value(); - let hr = - call::call_1in_1out(self.info.index, obj, args.get_value(0), out.out_ptr()); - hr.ok()?; - if let WinRTValue::RawPtr(raw_ptr) = out { - out = out_param.typ.from_out(raw_ptr).map_err(|e| { - windows_core::Error::new(windows_core::HRESULT(-1), &format!("{:?}", e)) - })?; - } - out.sanitize_null_object(); - Ok(vec![out]) - } - CallStrategy::DirectReceiveArray => { - // fn(this, *mut u32, *mut *mut c_void) -> HRESULT - let param = &self.info.parameters[0]; - let elem_type = param.typ.array_element_type(); - let mut length: u32 = 0; - let mut data_ptr: *mut std::ffi::c_void = std::ptr::null_mut(); - let fptr = call::get_vtable_function_ptr(obj, self.info.index); - let hr: windows_core::HRESULT = unsafe { - let method: unsafe extern "system" fn( - *mut std::ffi::c_void, - *mut u32, - *mut *mut std::ffi::c_void, - ) - -> windows_core::HRESULT = std::mem::transmute(fptr); - method(obj, &mut length, &mut data_ptr) - }; - if hr.is_err() { - // Callee may have allocated a buffer before returning failure. - // Wrap in ArrayData to release elements + CoTaskMemFree. - if !data_ptr.is_null() { - let _ = crate::array::ArrayData::from_cotaskmem( - elem_type.clone(), - data_ptr, - length as usize, - ); - } - hr.ok()?; - } - let array = if data_ptr.is_null() || length == 0 { - if !data_ptr.is_null() { - unsafe { - windows::Win32::System::Com::CoTaskMemFree(Some(data_ptr)); - } - } - - crate::array::ArrayData::empty(elem_type) - } else { - crate::array::ArrayData::from_cotaskmem(elem_type, data_ptr, length as usize) - }; - Ok(vec![WinRTValue::Array(array)]) - } - CallStrategy::DirectPassArray1Out => { - // fn(this, u32, *const u8, out) -> HRESULT - let in_param = self.info.parameters.iter().find(|p| p.is_input()).unwrap(); - let out_param = self.info.parameters.iter().find(|p| p.is_out()).unwrap(); - let array_data = args.get_value(in_param.value_index).as_array().unwrap(); - let buffer = array_data.serialize_for_abi(); - let mut out = out_param.typ.default_value(); - let fptr = call::get_vtable_function_ptr(obj, self.info.index); - let hr: windows_core::HRESULT = unsafe { - let method: unsafe extern "system" fn( - *mut std::ffi::c_void, - u32, - *const u8, - *mut std::ffi::c_void, - ) - -> windows_core::HRESULT = std::mem::transmute(fptr); - method(obj, array_data.len() as u32, buffer.as_ptr(), out.out_ptr()) - }; - hr.ok()?; - if let WinRTValue::RawPtr(raw_ptr) = out { - out = out_param.typ.from_out(raw_ptr).map_err(|e| { - windows_core::Error::new(windows_core::HRESULT(-1), &format!("{:?}", e)) - })?; - } - out.sanitize_null_object(); - Ok(vec![out]) - } - CallStrategy::DirectFillArray => { - // fn(this, u32, *mut u8) -> HRESULT - // FillArray: caller provides buffer of known capacity, callee fills it. - let param = &self.info.parameters[0]; - let elem_type = param.typ.array_element_type(); - let fptr = call::get_vtable_function_ptr(obj, self.info.index); - - assert!( - param - .input_index - .is_some_and(|index| { args.get_value(index).as_array().is_some() }), - "DirectFillArray requires a pre-allocated array argument with the desired capacity. \ - Pass an ArrayData with the expected number of elements." - ); - let array_data = args - .get_value(param.input_index.unwrap()) - .as_array() - .unwrap(); - let capacity = array_data.len() as u32; - let total_bytes = capacity as usize * elem_type.element_size(); - let buffer_ptr = - unsafe { windows::Win32::System::Com::CoTaskMemAlloc(total_bytes) as *mut u8 }; - assert!(!buffer_ptr.is_null(), "CoTaskMemAlloc failed for FillArray"); - unsafe { std::ptr::write_bytes(buffer_ptr, 0, total_bytes) }; - let hr: windows_core::HRESULT = unsafe { - let method: unsafe extern "system" fn( - *mut std::ffi::c_void, - u32, - *mut u8, - ) - -> windows_core::HRESULT = std::mem::transmute(fptr); - method(obj, capacity, buffer_ptr) - }; - if hr.is_err() { - // Callee may have written elements before failing. - // Buffer was zero-initialized, so null slots are safe to release. - // Use capacity as cleanup length — ArrayData::Drop skips null elements. - let _ = crate::array::ArrayData::from_cotaskmem( - elem_type.clone(), - buffer_ptr as _, - capacity as usize, - ); - hr.ok()?; - } - let array = crate::array::ArrayData::from_cotaskmem( - elem_type, - buffer_ptr as _, - capacity as usize, - ); - Ok(vec![WinRTValue::Array(array)]) - } - CallStrategy::Direct1InFillArray => { - // fn(this, val, u32, *mut u8) -> HRESULT - let in_param = self.info.parameters.iter().find(|p| p.is_input()).unwrap(); - let fill_param = self - .info - .parameters - .iter() - .find(|p| p.is_fill_array()) - .unwrap(); - let array_data = args - .get_value(fill_param.input_index.unwrap()) - .as_array() - .unwrap(); - let elem_type = fill_param.typ.array_element_type(); - let capacity = array_data.len() as u32; - let total_bytes = capacity as usize * elem_type.element_size(); - let buffer_ptr = - unsafe { windows::Win32::System::Com::CoTaskMemAlloc(total_bytes) as *mut u8 }; - assert!(!buffer_ptr.is_null(), "CoTaskMemAlloc failed for FillArray"); - unsafe { std::ptr::write_bytes(buffer_ptr, 0, total_bytes) }; - let fptr = call::get_vtable_function_ptr(obj, self.info.index); - let hr = call::call_fill_array_1in( - fptr, - obj, - args.get_value(in_param.value_index), - capacity, - buffer_ptr, - ); - if hr.is_err() { - // Buffer was zero-initialized; use capacity for cleanup. - let _ = crate::array::ArrayData::from_cotaskmem( - elem_type.clone(), - buffer_ptr as _, - capacity as usize, - ); - hr.ok()?; - } - let array = crate::array::ArrayData::from_cotaskmem( - elem_type, - buffer_ptr as _, - capacity as usize, - ); - Ok(vec![WinRTValue::Array(array)]) - } - CallStrategy::Libffi(cif) => call::call_method_dynamic( - self.info.index, - obj, - &self.info.parameters, - &args, - self.info.out_count, - &self.info.return_kind, - cif, - ), - } + self.0.call_dynamic(obj, args) } } -#[derive(Debug)] pub struct InterfaceSignature { pub name: String, - pub iid: windows_core::GUID, + pub iid: GUID, pub methods: Vec, #[allow(dead_code)] table: Arc, } impl InterfaceSignature { - pub fn define_interface( - name: String, - iid: windows_core::GUID, - table: &Arc, - ) -> Self { - InterfaceSignature { + pub fn define_interface(name: String, iid: GUID, table: &Arc) -> Self { + Self { name, iid, methods: Vec::new(), @@ -971,19 +100,21 @@ impl InterfaceSignature { } pub fn define_from_iunknown(name: &str, iid: GUID, table: &Arc) -> Self { - let mut t = InterfaceSignature::define_interface(name.to_owned(), iid, table); - t.add_method(MethodSignature::new(table)) // 0 QueryInterface - .add_method(MethodSignature::new(table)) // 1 AddRef - .add_method(MethodSignature::new(table)); // 2 Release - t + let mut result = Self::define_interface(name.to_owned(), iid, table); + result + .add_method(MethodSignature::new(table)) + .add_method(MethodSignature::new(table)) + .add_method(MethodSignature::new(table)); + result } pub fn define_from_iinspectable(name: &str, iid: GUID, table: &Arc) -> Self { - let mut t = Self::define_from_iunknown(name, iid, table); - t.add_method(MethodSignature::new(table)) // 3 GetIids - .add_method(MethodSignature::new(table).add_out(table.hstring())) // 4 GetRuntimeClassName - .add_method(MethodSignature::new(table)); // 5 GetTrustLevel - t + let mut result = Self::define_from_iunknown(name, iid, table); + result + .add_method(MethodSignature::new(table)) + .add_method(MethodSignature::new(table).add_out(table.hstring())) + .add_method(MethodSignature::new(table)); + result } pub fn add_method(&mut self, signature: MethodSignature) -> &mut Self { @@ -1003,124 +134,15 @@ pub struct RuntimeClassSignature { #[cfg(test)] mod tests { use super::*; - use windows::Foundation::{IStringable, IUriRuntimeClass, Uri}; - use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize}; - use windows_core::{IInspectable, Interface, h}; #[test] - fn fill_array_tracks_distinct_input_and_output_indices() { + fn winrt_signature_exposes_only_winrt_parameter_contracts() { let table = MetadataTable::new(); - let method = MethodSignature::new(&table) - .add_in(table.u32_type()) - .add_out_fill(table.array(&table.hstring())) - .add_out(table.u32_type()) - .build(6); - - assert_eq!(method.info.parameters[0].value_index, 0); - assert_eq!(method.info.parameters[0].input_index, Some(0)); - assert_eq!(method.info.parameters[1].value_index, 0); - assert_eq!(method.info.parameters[1].input_index, Some(1)); - assert_eq!(method.info.parameters[2].value_index, 1); - assert_eq!(method.info.parameters[2].input_index, None); - } - - #[test] - fn coerces_object_inputs_to_the_expected_interface() -> windows_core::Result<()> { - let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; - let uri = Uri::CreateUri(h!("https://example.com"))?; - let default_interface: IUriRuntimeClass = uri.cast()?; - let expected_interface: IStringable = uri.cast()?; - assert_ne!( - default_interface.as_raw(), - expected_interface.as_raw(), - "test requires distinct default and requested interface pointers" - ); - - let table = MetadataTable::new(); - let expected_type = table.interface(IStringable::IID); - let value = WinRTValue::Object(default_interface.cast()?); - let coerced = coerce_input_object(&expected_type, &value)? - .expect("interface parameters must be coerced"); - assert_eq!( - coerced.as_object().unwrap().as_raw(), - expected_interface.as_raw() - ); - - let inspectable: IInspectable = uri.cast()?; - let coerced_object = coerce_input_object(&table.object(), &value)? - .expect("Object parameters must be coerced to IInspectable"); - assert_eq!( - coerced_object.as_object().unwrap().as_raw(), - inspectable.as_raw() - ); - Ok(()) - } + let signature = MethodSignature::new(&table) + .add_in(table.i32_type()) + .add_out(table.hstring()) + .add_out_fill(table.array(&table.object())); - #[test] - fn raw_pointer_is_rejected_for_winrt_object_params() { - let table = MetadataTable::new(); - let bogus = WinRTValue::RawPtr(0xDEADBEEF as *mut std::ffi::c_void); - - let object_ty = table.object(); - let object_err = coerce_input_object(&object_ty, &bogus) - .expect_err("RawPtr into Object must be rejected"); - assert_eq!(object_err.code().0, 0x80070057u32 as i32); - - let iface_ty = table.interface(IStringable::IID); - let err = coerce_input_object(&iface_ty, &bogus) - .expect_err("RawPtr into a typed interface must be rejected"); - assert_eq!( - err.code().0, - 0x80070057u32 as i32, - "typed-interface RawPtr rejection must use E_INVALIDARG (got {:?})", - err - ); - let msg = err.message(); - assert!( - msg.contains("raw pointer") && msg.contains("typed"), - "rejection error must explain the constraint, got: {}", - msg - ); - - let null_object = WinRTValue::Null; - assert!( - coerce_input_object(&object_ty, &null_object) - .expect("null into TypeKind::Object must be allowed") - .is_none(), - ); - assert!( - coerce_input_object(&iface_ty, &null_object) - .expect("null into typed interface must be allowed") - .is_none(), - ); - } - - #[test] - fn coerces_object_array_elements_to_the_expected_interface() -> windows_core::Result<()> { - let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; - let uri = Uri::CreateUri(h!("https://example.com"))?; - let default_interface: IUriRuntimeClass = uri.cast()?; - let expected_interface: IStringable = uri.cast()?; - - let table = MetadataTable::new(); - let element_type = table.interface(IStringable::IID); - let array_type = table.array(&element_type); - let value = WinRTValue::Array(crate::array::ArrayData::from_values( - element_type, - &[WinRTValue::Object(default_interface.cast()?)], - )); - let coerced = coerce_input_array(&array_type, &value)? - .expect("object array elements must be coerced"); - assert_eq!( - coerced - .as_array() - .unwrap() - .get(0) - .as_object() - .unwrap() - .as_raw(), - expected_interface.as_raw() - ); - Ok(()) + let _ = signature.build(6); } } diff --git a/docs/classic-com-support.md b/docs/classic-com-support.md index c7dce764..0c568020 100644 --- a/docs/classic-com-support.md +++ b/docs/classic-com-support.md @@ -21,6 +21,44 @@ return shapes; the renderer only serializes those decisions. Classic COM work must not change existing WinRT metadata, generated output, ownership, runtime behavior, or the `@microsoft/dynwinrt` root API. +## Runtime call architecture + +WinRT and Classic COM have separate semantic planners. They share only the +private native-call backend and executor: + +```text +WinRT metadata -> signature.rs (WinRT planner) --------\ + -> native_call.rs -> call.rs -> native method +COM metadata -> com.rs (COM planner and method table) / +``` + +| Layer | Responsibility | +|---|---| +| `signature.rs` | WinRT-only signature facade. It preserves the existing `In`, `Out`, fill-array, HRESULT, and out-value conventions. It must not expose raw pointers, `InOut`, native direct returns, or other Classic COM semantics. | +| `com.rs` | Classic COM types, method signatures, interface roots, vtable slot numbering, method registry, and method handles. It owns raw-pointer, `InOut`, direct-return, and `void` call semantics without registering methods in the WinRT `MetadataTable`. | +| `native_call.rs` | Private lowering backend. It converts a completed WinRT or COM signature into parameter/output slots, validates input values, expands array ABI parameters, chooses a fast path or prepares a libffi CIF, and coordinates result conversion. It does not own metadata, language projection, or public interface registries. | +| `call.rs` | Private native executor. It reads the vtable function pointer, creates stable ABI storage and libffi arguments, performs the call, and decodes raw output storage according to the completed plan. It must not infer WinRT, Classic COM, ownership, or JavaScript semantics. | + +This separation is semantic, not a duplication of the native executor. WinRT +and Classic COM may both lower primitive and struct layout information through +the same private backend, but only their respective semantic layers may decide +what a type, parameter direction, return convention, or ownership contract +means. + +In particular: + +- WinRT interface methods remain in the WinRT `MetadataTable` and begin at + `IInspectable` slot 6. +- Classic COM maintains its own interface method table and selects slot 3 or 6 + from its `IUnknown` or `IInspectable` root. +- shared native methods are fully built before publication and are immutable + during concurrent invocation; +- exact struct identity is validated before native dispatch, while established + WinRT ABI aliases such as Char16/U16 and enum/I32 arrays remain compatible; + and +- language-friendly choices remain a codegen responsibility after the COM + planner has validated the native contract. + ## Size of Windows.Win32.winmd The counts below are exact for From c9261480980303ad00dfa38bab621e45bfd3ae6d Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 30 Jul 2026 11:17:21 +0800 Subject: [PATCH 27/28] Harden Classic COM type semantics Project HSTRING with ownership, require resolved interface metadata, and preserve semantic HRESULT values. Close unsafe pointer fallbacks for unresolved, compound, callback, array, and dynamic-IID shapes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 41a826d4-b04a-4932-b1b8-31173fc5ab40 --- .github/skills/classic-com-abi/SKILL.md | 17 +- bindings/js/__test__/index.spec.ts | 8 +- bindings/js/src/com.rs | 12 + crates/dynwinrt/src/call.rs | 5 + crates/dynwinrt/src/com.rs | 99 +++++++ crates/dynwinrt/src/native_call.rs | 8 +- docs/classic-com-support.md | 21 +- .../src/codegen/com/projection.rs | 10 +- .../src/codegen/com/render.rs | 249 +++++++++++++++++- .../src/codegen/com/type_mapping.rs | 114 ++++++-- tools/dynwinrt-codegen/src/com_metadata.rs | 53 +++- .../dynwinrt-codegen/tests/win32_com_test.rs | 181 +++++++++++++ 12 files changed, 732 insertions(+), 45 deletions(-) diff --git a/.github/skills/classic-com-abi/SKILL.md b/.github/skills/classic-com-abi/SKILL.md index 8ee3a527..74c48291 100644 --- a/.github/skills/classic-com-abi/SKILL.md +++ b/.github/skills/classic-com-abi/SKILL.md @@ -111,6 +111,8 @@ Apply these rules: model. - Only the COM metadata/projection and planner layers may interpret pointer categories, parameter direction, return convention, and ownership. +- A by-value GUID is not REFIID. Dynamic-IID output adoption requires + pointer-shaped metadata plus an explicit `iid`/`riid` semantic parameter. - `native_call.rs` may validate and lower an already-described call, but must not infer metadata semantics, allocator ownership, or language projection. - `call.rs` must execute the completed plan without inferring metadata, @@ -195,6 +197,8 @@ Examples: - `adoptComPointer()` accepts only a native output known to transfer `+1`. - Numeric and Buffer-backed pointers are borrowed and cannot be adopted. - Pair BSTR with `SysFreeString`. +- Pair HSTRING ownership with `WindowsDeleteString`; never project HSTRING as a + numeric pointer. - Pair CoTaskMem allocations with `CoTaskMemFree`. - Win32 handles are not COM references; cleanup is resource-specific. - Unknown allocator or ownership contracts fail closed. @@ -208,9 +212,13 @@ Before supporting an interface: 3. Inspect every method, not only the method intended for a sample. 4. Record `NativeArrayInfo`, `FreeWith`, `Const`, parameter direction, and pointer depth. -5. Check Microsoft API documentation for ownership that metadata does not +5. Record `CanReturnMultipleSuccessValuesAttribute` before deciding whether an + HRESULT is throw-or-void or a semantic result. +6. Resolve every referenced interface IID from the loaded metadata. Require + callers to provide external definitions through `--ref`. +7. Check Microsoft API documentation for ownership that metadata does not encode. -6. Generate with `--dry-run` and verify unsupported methods stop the whole +8. Generate with `--dry-run` and verify unsupported methods stop the whole unsafe interface projection. Do not claim general interface support when only a manually described runtime @@ -228,6 +236,11 @@ Reject generation when any required fact is unknown, including: - VARIANT, PROPVARIANT, SAFEARRAY, FORMATETC, or STGMEDIUM without dedicated models; - unsupported direct native returns; or +- interface parameters whose IID or PIID cannot be resolved from loaded + metadata; +- parameterized or async interfaces without a computed closed IID; +- delegates without a managed callback projection; +- native arrays without explicit count and element-ownership contracts; - incomplete inherited vtable layout. An error during generation is safer than plausible generated code with the diff --git a/bindings/js/__test__/index.spec.ts b/bindings/js/__test__/index.spec.ts index 4ab0e87e..fbb7a73f 100644 --- a/bindings/js/__test__/index.spec.ts +++ b/bindings/js/__test__/index.spec.ts @@ -19,7 +19,7 @@ import { roInitialize, } from '../dist/winrt.js' import * as winrtRuntime from '../dist/winrt.js' -import { DynCom } from '../dist/com.js' +import { DynCom, DynComMethodSig } from '../dist/com.js' test('Classic COM is isolated from the WinRT root entrypoint', (t) => { t.false(Object.prototype.hasOwnProperty.call(winrtRuntime, 'DynCom')) @@ -75,6 +75,12 @@ test('DynCom does not adopt borrowed raw pointer bits as owned COM references', t.regex(error.message, /only owned native outputs may be consumed/) }) +test('DynCom exposes HSTRING and semantic HRESULT primitives', (t) => { + t.is(DynCom.hstring('dynwinrt').toString(), 'dynwinrt') + t.truthy(DynCom.hstringType()) + t.truthy(new DynComMethodSig().preserveHresult()) +}) + test('DynCom distinguishes handle-value bytes from data-pointer storage', (t) => { const width = process.arch === 'ia32' ? 4 : 8 const expected = 0x12345678n diff --git a/bindings/js/src/com.rs b/bindings/js/src/com.rs index 14cefe99..9f3882d9 100644 --- a/bindings/js/src/com.rs +++ b/bindings/js/src/com.rs @@ -507,6 +507,11 @@ impl DynComMethodSig { pub fn returns_void(&self) -> Self { Self(self.0.clone().returns_void()) } + + #[napi] + pub fn preserve_hresult(&self) -> Self { + Self(self.0.clone().preserve_hresult()) + } } #[napi] @@ -734,6 +739,13 @@ impl DynCom { DynComType(dynwinrt::com::Type::winrt(TABLE.hstring())) } + #[napi] + pub fn hstring(value: String) -> DynWinRTValue { + DynWinRTValue::new(dynwinrt::WinRTValue::HString(windows::core::HSTRING::from( + value, + ))) + } + #[napi] pub fn pointer_type() -> DynComType { DynComType(dynwinrt::com::Type::pointer()) diff --git a/crates/dynwinrt/src/call.rs b/crates/dynwinrt/src/call.rs index 31917204..cabf9d40 100644 --- a/crates/dynwinrt/src/call.rs +++ b/crates/dynwinrt/src/call.rs @@ -392,6 +392,11 @@ pub fn call_method_dynamic( hr.ok()?; None } + MethodReturn::SemanticHResult => { + let hr: windows_core::HRESULT = cif.call(CodePtr(fptr), &ffi_args); + hr.ok()?; + Some(WinRTValue::HResult(hr)) + } MethodReturn::Void => { cif.call::<()>(CodePtr(fptr), &ffi_args); None diff --git a/crates/dynwinrt/src/com.rs b/crates/dynwinrt/src/com.rs index 189c41ae..118bd258 100644 --- a/crates/dynwinrt/src/com.rs +++ b/crates/dynwinrt/src/com.rs @@ -80,6 +80,10 @@ impl MethodSignature { pub fn returns_void(self) -> Self { Self(self.0.returns_void()) } + + pub fn preserve_hresult(self) -> Self { + Self(self.0.preserve_hresult()) + } } #[derive(Debug)] @@ -346,6 +350,24 @@ mod tests { u32::MAX } + unsafe extern "system" fn return_s_false(_this: *mut c_void) -> windows_core::HRESULT { + windows_core::HRESULT(1) + } + + unsafe extern "system" fn return_failure(_this: *mut c_void) -> windows_core::HRESULT { + windows_core::HRESULT(0x80004005u32 as i32) + } + + unsafe extern "system" fn return_hstring( + _this: *mut c_void, + value: *mut *mut c_void, + ) -> windows_core::HRESULT { + unsafe { + *value = std::mem::transmute(HSTRING::from("dynwinrt HSTRING")); + } + windows_core::HRESULT(0) + } + static VOID_CALLS: AtomicU32 = AtomicU32::new(0); unsafe extern "system" fn return_void(_this: *mut c_void) { @@ -422,6 +444,68 @@ mod tests { assert_eq!(VOID_CALLS.load(Ordering::Relaxed), 1); } + #[test] + fn semantic_hresult_preserves_success_codes_and_throws_failures() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table).preserve_hresult(); + let success_vtable = [return_s_false as *mut c_void]; + let mut success = FakeComObject { + vtable: success_vtable.as_ptr(), + }; + + let result = call_method( + 0, + (&mut success as *mut FakeComObject).cast(), + signature.clone(), + &[], + ) + .unwrap(); + assert!(matches!( + result.as_slice(), + [WinRTValue::HResult(value)] if value.0 == 1 + )); + + let failure_vtable = [return_failure as *mut c_void]; + let mut failure = FakeComObject { + vtable: failure_vtable.as_ptr(), + }; + let error = call_method( + 0, + (&mut failure as *mut FakeComObject).cast(), + signature, + &[], + ) + .unwrap_err(); + match error { + result::Error::WindowsError(error) => { + assert_eq!(error.code(), windows_core::HRESULT(0x80004005u32 as i32)); + } + other => panic!("expected Windows error, got {other:?}"), + } + } + + #[test] + fn classic_com_hstring_output_is_owned_and_decoded() { + let table = MetadataTable::new(); + let vtable = [return_hstring as *mut c_void]; + let mut object = FakeComObject { + vtable: vtable.as_ptr(), + }; + + let result = call_method( + 0, + (&mut object as *mut FakeComObject).cast(), + MethodSignature::new(&table).add_out(Type::winrt(table.hstring())), + &[], + ) + .unwrap(); + + assert!(matches!( + result.as_slice(), + [WinRTValue::HString(value)] if value == "dynwinrt HSTRING" + )); + } + #[test] fn in_out_parameter_preserves_input_and_returns_updated_value() { let table = MetadataTable::new(); @@ -649,6 +733,11 @@ mod tests { #[test] fn shell_link_query_interface_returns_owned_ipersistfile() -> result::Result<()> { let shell_link = shell_link()?; + let shell_link_object = shell_link + .as_object() + .expect("IShellLinkW must be non-null"); + let description = wide_null("semantic HRESULT"); + call_method_1_ptr(7, shell_link_object.as_raw(), description.as_ptr().cast())?; let persist = shell_link.cast(&IPersistFile::IID)?; let persist = persist.as_object().expect("IPersistFile must be non-null"); let table = MetadataTable::new(); @@ -663,6 +752,16 @@ mod tests { result.as_slice(), [WinRTValue::Guid(clsid)] if *clsid == CLSID_SHELL_LINK )); + let dirty = call_method( + 4, + persist.as_raw(), + MethodSignature::new(&table).preserve_hresult(), + &[], + )?; + assert!(matches!( + dirty.as_slice(), + [WinRTValue::HResult(value)] if value.0 == 0 + )); Ok(()) } diff --git a/crates/dynwinrt/src/native_call.rs b/crates/dynwinrt/src/native_call.rs index c2419a1a..9e69d6ec 100644 --- a/crates/dynwinrt/src/native_call.rs +++ b/crates/dynwinrt/src/native_call.rs @@ -218,6 +218,7 @@ pub(crate) struct AbiMethodSignature { #[derive(Debug, Clone)] pub(crate) enum MethodReturn { HResult, + SemanticHResult, Void, Value(ParameterType), } @@ -225,7 +226,7 @@ pub(crate) enum MethodReturn { impl MethodReturn { fn libffi_type(&self) -> libffi::middle::Type { match self { - Self::HResult => libffi::middle::Type::i32(), + Self::HResult | Self::SemanticHResult => libffi::middle::Type::i32(), Self::Void => libffi::middle::Type::void(), Self::Value(typ) => typ.libffi_type(), } @@ -311,6 +312,11 @@ impl AbiMethodSignature { self } + pub(crate) fn preserve_hresult(mut self) -> Self { + self.return_kind = MethodReturn::SemanticHResult; + self + } + pub(crate) fn build(self, index: usize) -> Method { use libffi::middle::Type; let mut types: Vec = Vec::with_capacity(self.parameters.len() + 1); diff --git a/docs/classic-com-support.md b/docs/classic-com-support.md index 0c568020..ca7eed29 100644 --- a/docs/classic-com-support.md +++ b/docs/classic-com-support.md @@ -267,8 +267,10 @@ Required support: `IPersistFile::IsDirty` use `S_OK` versus `S_FALSE` as their actual result. Discarding every successful HRESULT loses information. -The projection needs an explicit PreserveSig/semantic-HRESULT classification -instead of globally treating every non-negative HRESULT as `void`. +Windows.Win32 metadata marks these methods with +`CanReturnMultipleSuccessValuesAttribute`. The COM projection preserves the +numeric successful HRESULT for marked methods while still throwing failed +HRESULTs. Unmarked HRESULT methods retain the normal throw-or-`void` behavior. ### 9. Apartment affinity and marshaling @@ -315,7 +317,7 @@ not be described as solving every problem in the map above. |---|---| | WinRT/Classic COM separation | Separate COM metadata/codegen path and `@microsoft/dynwinrt/com` public entrypoint. The WinRT generator and root runtime API remain unchanged. | | Interface root and vtable layout | Distinguishes `IUnknown` slot 3 from `IInspectable` slot 6 and walks inherited Classic COM interfaces before assigning slots. | -| Method return conventions | Supports normal HRESULT methods plus native direct scalar, direct pointer at the runtime layer, and direct `void` returns. | +| Method return conventions | Supports normal HRESULT methods, semantic HRESULT values marked with `CanReturnMultipleSuccessValuesAttribute`, native direct scalar, direct pointer at the runtime layer, and direct `void` returns. | | Basic parameter direction | Supports input, output, and scalar in/out parameters without reducing in/out to out-only. | | Primitive ABI types | Signed/unsigned integers, floats, BOOL, HRESULT, GUID, enums, and `char16`. | | Pointer-sized values | `ISize`/`USize` select the correct x86/x64 ABI width and JavaScript uses `bigint`. | @@ -325,6 +327,9 @@ not be described as solving every problem in the map above. | Ownership provenance | Borrowed numeric/TypedArray pointers cannot be re-adopted as a second COM owner. Native owned outputs are consumed once. | | Backing-storage lifetime | Buffer/TypedArray owners are retained and detached ArrayBuffers are rejected before native use. | | Common string ownership | Scalar BSTR output uses `SysFreeString`; supported `PWSTR`/`PSTR` allocations use `CoTaskMemFree`. | +| HSTRING ownership | Classic COM methods that explicitly use HSTRING project strings through owning HSTRING values; outputs release with `WindowsDeleteString`. | +| External interface metadata | Interface parameters require a resolvable IID. Missing referenced metadata fails generation with a `--ref` diagnostic instead of degrading an owned interface to a raw pointer. | +| WinRT runtime-class references | A resolved runtime class lowers through its default interface IID and remains a managed COM value. Missing defaults fail closed. | | Common interop pattern | Supports HWND + REFIID + `void**` bridges and adopts the returned interface reference. | | Explicit COM initialization | Activation no longer silently chooses MTA; callers select STA or MTA with `DynCom.initialize()`. | | Fail-closed generation | Unsupported structs, arrays, pointer outputs, ownership, and in/out shapes stop generation with a targeted error. | @@ -351,7 +356,6 @@ not be described as solving every problem in the map above. - SAFEARRAY; - FORMATETC and STGMEDIUM; - arbitrary COM event/callback sink generation; -- semantic `S_OK`/`S_FALSE` HRESULT projection; - cross-thread/apartment marshaling; and - the general flat-Win32 DLL-export and handle-cleanup layer. @@ -361,6 +365,7 @@ not be described as solving every problem in the map above. |---|---|---| | `IUnknown` and `IInspectable` roots | Supported | User methods begin at vtable slot 3 or 6 respectively. Full inherited Classic COM slot numbering is preserved. | | `HRESULT` methods | Supported | Failed HRESULTs become errors. | +| Semantic `HRESULT` methods | Supported | `CanReturnMultipleSuccessValuesAttribute` preserves successful values such as `S_OK` and `S_FALSE`; failed values still become errors. | | Native `void` returns | Supported | Used by interfaces such as `IMalloc`. | | Direct scalar returns | Supported | Includes signed/unsigned integers, floating point values, and enums. | | Direct pointer returns | Runtime supported; codegen partial | The runtime can describe a pointer return explicitly. Metadata codegen currently fails closed for interfaces such as `IMalloc` because it does not preserve the raw-pointer return kind. | @@ -375,6 +380,9 @@ not be described as solving every problem in the map above. | Caller-owned UTF-16 output buffers | Supported for recognized shapes | The generator allocates and decodes the buffer when metadata identifies the count parameter. | | Callee-allocated `PWSTR` / `PSTR` outputs | Supported | Generated code decodes and frees `CoTaskMem` storage. | | Scalar `[out] BSTR*` | Supported | Generated code converts the BSTR and releases it with `SysFreeString`. | +| HSTRING inputs and scalar outputs | Supported | JavaScript strings are converted to owning HSTRING values; returned HSTRING values are decoded and released automatically. | +| Referenced interface types | Supported when IID metadata is loaded | Missing external definitions fail closed and direct callers to pass the defining winmd with `--ref`. | +| Dynamic-IID `void**` outputs | Supported for explicit REFIID shapes | The IID argument must be a pointer-shaped `iid`/`riid` parameter. A GUID passed by value is not REFIID and cannot trigger interface adoption. | | Explicit apartment initialization | Supported | `DynCom.initialize()` never silently chooses an apartment for the caller. | The runtime can manually describe some ABI shapes that the generator rejects. @@ -382,6 +390,11 @@ For example, a carefully defined native struct can be called from Rust, but the generator does not emit a struct until its native layout is known to be correct. +Parameterized and async interfaces, delegates, and native arrays remain +fail-closed until the COM projection can compute their complete IID, callback, +count, and element-ownership contracts. They must never fall back to +`bigint | Buffer`. + ## Unsupported types and shapes The generator fails closed for unsupported signatures instead of emitting a diff --git a/tools/dynwinrt-codegen/src/codegen/com/projection.rs b/tools/dynwinrt-codegen/src/codegen/com/projection.rs index bad5e2e1..3e41311e 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/projection.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/projection.rs @@ -44,14 +44,8 @@ pub(super) fn method_is_interop_shape(m: &MethodMeta) -> Option> return None; } let riid = &m.params[last_idx - 1]; - let is_riid = match &riid.typ { - TypeMeta::Guid => true, - TypeMeta::Object => { - let name = riid.name.to_ascii_lowercase(); - name == "riid" || name == "iid" - } - _ => false, - }; + let is_riid = matches!(riid.typ, TypeMeta::Object) + && matches!(riid.name.to_ascii_lowercase().as_str(), "riid" | "iid"); is_riid.then(|| m.params[..last_idx - 1].to_vec()) } diff --git a/tools/dynwinrt-codegen/src/codegen/com/render.rs b/tools/dynwinrt-codegen/src/codegen/com/render.rs index 2564a1ad..1e48139b 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/render.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/render.rs @@ -348,6 +348,9 @@ fn build_method_sig_js(m: &MethodMeta) -> String { } match &m.return_type { None => parts.push(".returnsVoid()".to_string()), + Some(rt) if is_hresult(rt) && m.preserve_hresult => { + parts.push(".preserveHresult()".to_string()); + } Some(rt) if !is_hresult(rt) => { parts.push(format!(".returns({})", ts_type_expr_js(rt))); } @@ -1082,8 +1085,7 @@ mod tests { } #[test] - fn interop_shape_accepts_guid_typed_trailing_in() { - // Some winmds project REFIID as TypeMeta::Guid rather than Object. + fn interop_shape_rejects_guid_passed_by_value() { let m = MethodMeta { name: "GetSomething".into(), vtable_index: 3, @@ -1094,8 +1096,7 @@ mod tests { direction: ParamDirection::In, }, ParamMeta { - // Deliberately NOT named "riid" — the type alone is sufficient. - name: "interfaceId".into(), + name: "riid".into(), typ: TypeMeta::Guid, direction: ParamDirection::In, }, @@ -1108,10 +1109,10 @@ mod tests { return_type: Some(make_hresult()), ..Default::default() }; - let natural = method_is_interop_shape(&m) - .expect("System.Guid-typed trailing in-param must be recognised as interop"); - assert_eq!(natural.len(), 1); - assert_eq!(natural[0].name, "target"); + assert!( + method_is_interop_shape(&m).is_none(), + "a by-value GUID must not be passed as a REFIID pointer" + ); } /// FIX 3 REGRESSION: a method returning HRESULT with an [out] Object and a @@ -2133,4 +2134,236 @@ mod tests { .contains("bindToHandler(pbc: bigint | Buffer, iid: string): DynWinRtValue;") ); } + + #[test] + fn hstring_output_uses_owned_hstring_projection() { + let method = MethodMeta { + name: "get_CorrelationVector".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "cv".into(), + typ: TypeMeta::String, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); + + assert!(output.js.contains(".addOut(DynCom.hstringType())")); + assert!(output.js.contains("return _out.toString();")); + assert!(output.dts.contains("get_CorrelationVector(): string;")); + } + + #[test] + fn unresolved_interface_iid_fails_closed() { + let method = MethodMeta { + name: "CreateSurface".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::Interface { + namespace: "Windows.UI.Composition".into(), + name: "ICompositionSurface".into(), + iid: String::new(), + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("an unresolved interface must not degrade to a raw pointer"); + + assert!(error.contains("ICompositionSurface")); + assert!(error.contains("no resolvable IID")); + assert!(error.contains("--ref")); + } + + #[test] + fn parameterized_interface_fails_closed_even_with_a_piid() { + let method = MethodMeta { + name: "GetItems".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IVectorView`1".into(), + piid: "bbe1fa4c-b0e3-4583-baef-1f1b2e483e56".into(), + args: vec![TypeMeta::String], + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("a PIID alone is not a closed interface IID"); + + assert!(error.contains("computed closed IID")); + assert!(error.contains("raw-pointer fallback is not allowed")); + } + + #[test] + fn async_interface_fails_closed_without_a_closed_iid() { + let method = MethodMeta { + name: "OpenAsync".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::AsyncOperation(Box::new(TypeMeta::String)), + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("async interfaces must not degrade to raw pointers"); + + assert!(error.contains("async interface requires a computed closed IID")); + assert!(error.contains("raw-pointer fallback is not allowed")); + } + + #[test] + fn native_array_fails_closed_without_count_and_ownership() { + let method = MethodMeta { + name: "GetItems".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::Array(Box::new(TypeMeta::Interface { + namespace: "Contoso".into(), + name: "IItem".into(), + iid: "11111111-2222-3333-4444-555555555555".into(), + })), + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("native arrays must not degrade to raw pointers"); + + assert!(error.contains("explicit count and element-ownership projection")); + assert!(error.contains("raw-pointer fallback is not allowed")); + } + + #[test] + fn delegate_fails_closed_without_a_callback_projection() { + let method = MethodMeta { + name: "SetHandler".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "handler".into(), + typ: TypeMeta::Delegate { + namespace: "Contoso".into(), + name: "Handler".into(), + iid: "11111111-2222-3333-4444-555555555555".into(), + }, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("delegates require an explicit managed projection"); + + assert!(error.contains("managed callback projection")); + assert!(error.contains("raw-pointer fallback is not allowed")); + } + + #[test] + fn runtime_class_uses_its_resolved_default_interface() { + let method = MethodMeta { + name: "CreateDevice".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::RuntimeClass { + namespace: "Windows.UI.Composition".into(), + name: "CompositionGraphicsDevice".into(), + default_interface: Some(Box::new(TypeMeta::Interface { + namespace: "Windows.UI.Composition".into(), + name: "ICompositionGraphicsDevice".into(), + iid: "a329b321-0d69-4b89-9951-28de94dc998d".into(), + })), + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); + + assert!(output.js.contains( + ".addOut(DynCom.interfaceType(WinGuid.parse('a329b321-0d69-4b89-9951-28de94dc998d')))" + )); + assert!(output.js.contains("return _out;")); + assert!(output.dts.contains("createDevice(): DynWinRtValue;")); + } + + #[test] + fn runtime_class_without_a_default_interface_fails_closed() { + let method = MethodMeta { + name: "CreateDevice".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::RuntimeClass { + namespace: "Windows.UI.Composition".into(), + name: "CompositionGraphicsDevice".into(), + default_interface: None, + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("runtime classes require a resolved default interface"); + + assert!(error.contains("no resolvable default interface")); + assert!(error.contains("--ref")); + } + + #[test] + fn semantic_hresult_is_preserved_as_a_number() { + let method = MethodMeta { + name: "IsDirty".into(), + vtable_index: 4, + return_type: Some(make_hresult()), + preserve_hresult: true, + ..Default::default() + }; + + let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); + + assert!(output.js.contains(".preserveHresult()")); + assert!(output.js.contains("return DynCom.toNumber(_out);")); + assert!(output.dts.contains("isDirty(): number;")); + } + + #[test] + fn ordinary_hresult_remains_throw_or_void() { + let method = MethodMeta { + name: "Load".into(), + vtable_index: 5, + return_type: Some(make_hresult()), + ..Default::default() + }; + + let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); + + assert!(!output.js.contains(".preserveHresult()")); + assert!(output.dts.contains("load(): void;")); + } } diff --git a/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs b/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs index 0396947f..8d9694bb 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs @@ -26,6 +26,13 @@ pub(super) enum HandleAliasKind { pub(super) fn validate_com_abi(meta: &ComInterfaceMeta) -> Result<(), String> { for method in &meta.interface.methods { for param in &method.params { + validate_resolved_interfaces( + ¶m.typ, + &format!( + "{}.{} parameter `{}`", + meta.interface.name, method.name, param.name + ), + )?; if let ParamDirection::UnsupportedNativeArray { count_param_index } = param.direction { let count = count_param_index .map(|index| format!("parameter index {index}")) @@ -65,6 +72,10 @@ pub(super) fn validate_com_abi(meta: &ComInterfaceMeta) -> Result<(), String> { .as_ref() .filter(|return_type| !is_hresult(return_type)) { + validate_resolved_interfaces( + return_type, + &format!("{}.{} return value", meta.interface.name, method.name), + )?; if !supports_direct_return(return_type) { return Err(format!( "{}.{}: unsupported direct native return type {:?}", @@ -72,10 +83,76 @@ pub(super) fn validate_com_abi(meta: &ComInterfaceMeta) -> Result<(), String> { )); } } + if method.preserve_hresult && !method.return_type.as_ref().is_some_and(is_hresult) { + return Err(format!( + "{}.{}: semantic HRESULT metadata requires an HRESULT return", + meta.interface.name, method.name + )); + } } Ok(()) } +fn validate_resolved_interfaces(t: &TypeMeta, context: &str) -> Result<(), String> { + match t { + TypeMeta::Interface { + namespace, + name, + iid, + } if iid.is_empty() => Err(format!( + "{context}: interface `{namespace}.{name}` has no resolvable IID; \ + pass the metadata that defines it via --ref instead of projecting it as a raw pointer" + )), + TypeMeta::Parameterized { + namespace, name, .. + } => Err(format!( + "{context}: parameterized interface `{namespace}.{name}` requires a computed closed IID \ + and managed ownership projection; raw-pointer fallback is not allowed" + )), + TypeMeta::AsyncAction + | TypeMeta::AsyncActionWithProgress(_) + | TypeMeta::AsyncOperation(_) + | TypeMeta::AsyncOperationWithProgress(_, _) => Err(format!( + "{context}: async interface requires a computed closed IID and managed ownership \ + projection; raw-pointer fallback is not allowed" + )), + TypeMeta::Delegate { + namespace, name, .. + } => Err(format!( + "{context}: delegate `{namespace}.{name}` requires a managed callback projection; \ + raw-pointer fallback is not allowed" + )), + TypeMeta::Array(_) => Err(format!( + "{context}: native arrays require an explicit count and element-ownership projection; \ + raw-pointer fallback is not allowed" + )), + TypeMeta::RuntimeClass { + default_interface: Some(default_interface), + .. + } => validate_resolved_interfaces(default_interface, context), + TypeMeta::RuntimeClass { + namespace, + name, + default_interface: None, + } => Err(format!( + "{context}: runtime class `{namespace}.{name}` has no resolvable default interface; \ + pass the metadata that defines it via --ref" + )), + _ => Ok(()), + } +} + +fn managed_interface_iid(t: &TypeMeta) -> Option<&str> { + match t { + TypeMeta::Interface { iid, .. } if !iid.is_empty() => Some(iid), + TypeMeta::RuntimeClass { + default_interface: Some(default_interface), + .. + } => managed_interface_iid(default_interface), + _ => None, + } +} + fn supports_in_out(t: &TypeMeta) -> bool { is_native_isize(t) || is_native_usize(t) @@ -111,6 +188,12 @@ pub(super) fn unwrap_return_js(t: &TypeMeta, expr: &str) -> String { if is_native_usize(t) { return format!("DynCom.toUsizeBigint({expr})"); } + if is_hresult(t) { + return format!("DynCom.toNumber({expr})"); + } + if managed_interface_iid(t).is_some() { + return expr.to_string(); + } match string_buffer_encoding(t) { Some(StringEncoding::Wide) => { return format!("DynCom.takeCoTaskMemWideString({expr})"); @@ -141,7 +224,6 @@ pub(super) fn unwrap_return_js(t: &TypeMeta, expr: &str) -> String { TypeMeta::Guid => format!("DynCom.toGuidString({expr})"), TypeMeta::Enum { underlying, .. } => unwrap_return_js(underlying, expr), TypeMeta::String => format!("{expr}.toString()"), - TypeMeta::Interface { iid, .. } if !iid.is_empty() => expr.to_string(), _ => expr.to_string(), } } @@ -154,7 +236,11 @@ pub(super) struct MethodResult<'a> { pub(super) fn method_results(m: &MethodMeta) -> Vec> { let mut result = Vec::new(); - if let Some(typ) = m.return_type.as_ref().filter(|typ| !is_hresult(typ)) { + if let Some(typ) = m + .return_type + .as_ref() + .filter(|typ| !is_hresult(typ) || m.preserve_hresult) + { result.push(MethodResult { typ, param_index: None, @@ -284,10 +370,8 @@ pub(super) fn uses_winrt_bridge_value(meta: &ComInterfaceMeta) -> bool { .map(|param| ¶m.typ) .chain(method.return_type.iter()) { - if let TypeMeta::Interface { iid, .. } = typ { - if !iid.is_empty() { - return true; - } + if managed_interface_iid(typ).is_some() { + return true; } } } @@ -382,6 +466,9 @@ pub(super) fn ts_type_expr_dts(t: &TypeMeta) -> String { if let Some(handle) = handle_type_name(t) { return handle; } + if managed_interface_iid(t).is_some() { + return "DynWinRtValue".into(); + } match t { TypeMeta::Bool => "boolean".into(), TypeMeta::I8 @@ -396,7 +483,6 @@ pub(super) fn ts_type_expr_dts(t: &TypeMeta) -> String { TypeMeta::I64 | TypeMeta::U64 => "bigint".into(), TypeMeta::String => "string".into(), TypeMeta::Guid => "string".into(), - TypeMeta::Interface { iid, .. } if !iid.is_empty() => "DynWinRtValue".into(), TypeMeta::Enum { name, .. } | TypeMeta::Struct { name, .. } => name.clone(), _ => "bigint | Buffer".into(), } @@ -424,10 +510,8 @@ pub(super) fn ts_type_expr_js(t: &TypeMeta) -> String { if handle_type_name(t).is_some() { return "DynCom.pointerType()".into(); } - if let TypeMeta::Interface { iid, .. } = t { - if !iid.is_empty() { - return format!("DynCom.interfaceType(WinGuid.parse('{iid}'))"); - } + if let Some(iid) = managed_interface_iid(t) { + return format!("DynCom.interfaceType(WinGuid.parse('{iid}'))"); } match t { TypeMeta::Bool => "DynCom.boolType()".into(), @@ -442,6 +526,7 @@ pub(super) fn ts_type_expr_js(t: &TypeMeta) -> String { TypeMeta::F32 => "DynCom.f32Type()".into(), TypeMeta::F64 => "DynCom.f64Type()".into(), TypeMeta::Char16 => "DynCom.char16Type()".into(), + TypeMeta::String => "DynCom.hstringType()".into(), TypeMeta::Guid => "DynCom.guidType()".into(), TypeMeta::Enum { underlying, .. } => ts_type_expr_js(underlying), _ => "DynCom.pointerType()".into(), @@ -472,10 +557,8 @@ pub(super) fn wrap_arg_js(t: &TypeMeta, var: &str) -> String { } }; } - if let TypeMeta::Interface { iid, .. } = t { - if !iid.is_empty() { - return var.to_string(); - } + if managed_interface_iid(t).is_some() { + return var.to_string(); } match t { TypeMeta::Bool => format!("DynCom.boolValue({var})"), @@ -490,6 +573,7 @@ pub(super) fn wrap_arg_js(t: &TypeMeta, var: &str) -> String { TypeMeta::F32 => format!("DynCom.f32({var})"), TypeMeta::F64 => format!("DynCom.f64({var})"), TypeMeta::Char16 => format!("DynCom.char16({var})"), + TypeMeta::String => format!("DynCom.hstring({var})"), TypeMeta::Guid => format!("DynCom.guid(WinGuid.parse({var}))"), TypeMeta::Enum { underlying, .. } => wrap_arg_js(underlying, var), _ => format!("DynCom.pointer({var})"), diff --git a/tools/dynwinrt-codegen/src/com_metadata.rs b/tools/dynwinrt-codegen/src/com_metadata.rs index c0304638..fb2edcd5 100644 --- a/tools/dynwinrt-codegen/src/com_metadata.rs +++ b/tools/dynwinrt-codegen/src/com_metadata.rs @@ -50,6 +50,7 @@ pub struct MethodMeta { pub vtable_index: usize, pub params: Vec, pub return_type: Option, + pub preserve_hresult: bool, pub doc: Option, pub owned_outputs: Vec, } @@ -310,11 +311,13 @@ fn parse_methods( mark_caller_owned_string_buffers(&mut params); let return_type = (signature.return_type != windows_metadata::Type::Void) .then(|| map_return_type(&signature.return_type, index)); + let preserve_hresult = method.has_attribute("CanReturnMultipleSuccessValuesAttribute"); MethodMeta { name, vtable_index: base_offset + index_in_interface, params, return_type, + preserve_hresult, doc: None, owned_outputs, } @@ -392,16 +395,45 @@ fn map_com_type(typ: &windows_metadata::Type, index: &reader::Index) -> TypeMeta match typ { windows_metadata::Type::ISize => native_isize_type(), windows_metadata::Type::USize => native_usize_type(), - windows_metadata::Type::Name(name) => index - .get(&name.namespace, &name.name) - .next() - .and_then(|def| parse_com_enum_def(&def)) - .map(|enum_meta| enum_meta.as_type_meta()) - .unwrap_or_else(|| crate::meta::map_winmd_type_with_generics(typ, index, &[])), + windows_metadata::Type::Name(name) + if is_canonical_hstring_name(&name.namespace, &name.name) => + { + TypeMeta::String + } + windows_metadata::Type::Name(name) => { + if let Some(def) = index.get(&name.namespace, &name.name).next() { + if let Some(enum_meta) = parse_com_enum_def(&def) { + return enum_meta.as_type_meta(); + } + if let Some(delegate) = parse_com_delegate_def(&def) { + return delegate; + } + } + crate::meta::map_winmd_type_with_generics(typ, index, &[]) + } _ => crate::meta::map_winmd_type_with_generics(typ, index, &[]), } } +fn is_canonical_hstring_name(namespace: &str, name: &str) -> bool { + namespace == "Windows.Win32.System.WinRT" && name == "HSTRING" +} + +fn parse_com_delegate_def(def: &reader::TypeDef) -> Option { + let extends = def.extends()?; + if !matches!( + (extends.namespace(), extends.name()), + ("System", "Delegate") | ("System", "MulticastDelegate") + ) { + return None; + } + Some(TypeMeta::Delegate { + namespace: def.namespace().to_string(), + name: def.name().to_string(), + iid: crate::meta::extract_iid(def), + }) +} + impl ComEnumMeta { fn as_type_meta(&self) -> TypeMeta { TypeMeta::Enum { @@ -732,6 +764,15 @@ mod tests { ); } + #[test] + fn hstring_mapping_requires_the_canonical_namespace() { + assert!(is_canonical_hstring_name( + "Windows.Win32.System.WinRT", + "HSTRING" + )); + assert!(!is_canonical_hstring_name("Contoso.Interop", "HSTRING")); + } + #[test] fn find_data_after_string_buffer_is_caller_owned_pointer() { let mut params = vec![ diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs index a4de8879..b79996e6 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -1152,6 +1152,187 @@ fn namespace_mode_rejects_classic_com_instead_of_using_winrt_slots() { ); } +#[test] +fn correlation_vector_hstring_output_is_owned_and_projected_as_string() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.WinRT", + "ICorrelationVectorSource", + ) + .expect("ICorrelationVectorSource must exist"); + let method = interface + .interface + .methods + .iter() + .find(|method| method.name == "get_CorrelationVector") + .expect("get_CorrelationVector must exist"); + + assert!(matches!(method.params[0].typ, TypeMeta::String)); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("HSTRING output generation must succeed"); + assert!(output.js.contains(".addOut(DynCom.hstringType())")); + assert!(output.js.contains("return _out.toString();")); + assert!(output.dts.contains("get_CorrelationVector(): string;")); +} + +#[test] +fn unresolved_external_interface_fails_until_reference_metadata_is_loaded() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let unresolved = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.WinRT.Composition", + "ICompositorInterop", + ) + .expect("ICompositorInterop must exist"); + let error = com::generate_com_interface_files(&unresolved, &win32_winmd()) + .expect_err("missing Windows metadata must fail closed"); + assert!(error.contains("ICompositionSurface")); + assert!(error.contains("--ref")); + + let Some(windows_winmd) = discovered_windows_winmd() else { + eprintln!("Skipping resolved-reference half: Windows.winmd not available"); + return; + }; + let metadata = format!("{};{}", win32_winmd(), windows_winmd); + let resolved = com_metadata::parse_com_interface( + &metadata, + "Windows.Win32.System.WinRT.Composition", + "ICompositorInterop", + ) + .expect("ICompositorInterop must resolve with Windows.winmd"); + let output = com::generate_com_interface_files(&resolved, &metadata) + .expect("resolved external interface generation must succeed"); + + let create_graphics_device = resolved + .interface + .methods + .iter() + .find(|method| method.name == "CreateGraphicsDevice") + .expect("CreateGraphicsDevice must exist"); + let TypeMeta::RuntimeClass { + default_interface: Some(default_interface), + .. + } = &create_graphics_device.params[1].typ + else { + panic!("CreateGraphicsDevice must return a resolved runtime class"); + }; + let TypeMeta::Interface { iid, .. } = default_interface.as_ref() else { + panic!("runtime class default must resolve to an interface"); + }; + assert!(!iid.is_empty()); + assert!(output.js.contains(&format!( + ".addOut(DynCom.interfaceType(WinGuid.parse('{iid}')))" + ))); + assert!(output.dts.contains("DynWinRtValue")); +} + +#[test] +fn semantic_hresult_metadata_preserves_is_dirty_only() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let mut interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.Com", + "IPersistFile", + ) + .expect("IPersistFile must exist"); + let is_dirty = interface + .interface + .methods + .iter() + .find(|method| method.name == "IsDirty") + .expect("IsDirty must exist"); + let load = interface + .interface + .methods + .iter() + .find(|method| method.name == "Load") + .expect("Load must exist"); + + assert!(is_dirty.preserve_hresult); + assert!(!load.preserve_hresult); + + interface + .interface + .methods + .retain(|method| method.name == "IsDirty"); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("semantic HRESULT generation must succeed"); + assert!(output.js.contains(".preserveHresult()")); + assert!(output.js.contains("return DynCom.toNumber(_out);")); + assert!(output.dts.contains("isDirty(): number;")); +} + +#[test] +fn metadata_delegate_parameter_fails_closed_as_a_delegate() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.Graphics.Direct2D", + "ID2D1Factory1", + ) + .expect("ID2D1Factory1 must exist"); + let register = interface + .interface + .methods + .iter() + .find(|method| method.name == "RegisterEffectFromStream") + .expect("RegisterEffectFromStream must exist"); + assert!(register.params.iter().any(|param| { + matches!( + ¶m.typ, + TypeMeta::Delegate { name, .. } if name == "PD2D1_EFFECT_FACTORY" + ) + })); + + let error = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect_err("delegate parameters require a managed callback projection"); + assert!(error.contains("PD2D1_EFFECT_FACTORY")); + assert!(error.contains("managed callback projection")); +} + +#[test] +fn by_value_guid_is_not_treated_as_a_dynamic_iid_pointer() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.WinRT.Display", + "IDisplayDeviceInterop", + ) + .expect("IDisplayDeviceInterop must exist"); + let open = interface + .interface + .methods + .iter() + .find(|method| method.name == "OpenSharedHandle") + .expect("OpenSharedHandle must exist"); + assert!(matches!(open.params[1].typ, TypeMeta::Guid)); + + let error = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect_err("by-value GUID plus void** must not be treated as REFIID interop"); + assert!(error.contains("untyped pointer output has no ownership projection")); +} + #[test] fn com_only_generation_emits_an_importable_package_shape() { if !win32_available() { From 4d64b5d0c0b559f80817d66bc67a7c42eec5cc9d Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Thu, 30 Jul 2026 17:51:20 +0800 Subject: [PATCH 28/28] Separate WinRT and COM codegen domains Move WinRT generators under a dedicated domain and lower Classic COM metadata into validated ComType and ProjectedComMethod IR before rendering. Remove pointer fallbacks, encode ownership and return semantics explicitly, and retain current main WinRT generation behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 41a826d4-b04a-4932-b1b8-31173fc5ab40 --- .github/skills/classic-com-abi/SKILL.md | 28 + docs/classic-com-support.md | 49 +- tools/dynwinrt-codegen/src/codegen/com/ir.rs | 229 ++ .../src/codegen/com/javascript/mod.rs | 6 + .../codegen/com/{ => javascript}/naming.rs | 2 +- .../src/codegen/com/javascript/render.rs | 717 +++++ .../codegen/com/javascript/render_tests.rs | 1909 +++++++++++++ .../src/codegen/com/javascript/types.rs | 346 +++ tools/dynwinrt-codegen/src/codegen/com/mod.rs | 19 +- .../src/codegen/com/project/interop.rs | 23 + .../src/codegen/com/project/mod.rs | 762 ++++++ .../src/codegen/com/project/types.rs | 359 +++ .../src/codegen/com/projection.rs | 218 -- .../src/codegen/com/render.rs | 2369 ----------------- .../src/codegen/com/type_mapping.rs | 698 ----- tools/dynwinrt-codegen/src/codegen/common.rs | 14 +- tools/dynwinrt-codegen/src/codegen/mod.rs | 8 +- .../codegen/{ => winrt}/javascript/docs.rs | 2 +- .../{ => winrt}/javascript/generator.rs | 2 +- .../src/codegen/{ => winrt}/javascript/ir.rs | 0 .../codegen/{ => winrt}/javascript/method.rs | 2 +- .../src/codegen/{ => winrt}/javascript/mod.rs | 0 .../codegen/{ => winrt}/javascript/naming.rs | 0 .../javascript/project/collections.rs | 30 +- .../javascript/project/constructors.rs | 2 +- .../{ => winrt}/javascript/project/methods.rs | 0 .../{ => winrt}/javascript/project/mod.rs | 12 +- .../{ => winrt}/javascript/project/structs.rs | 0 .../javascript/render/declarations.rs | 6 +- .../javascript/render/javascript/commonjs.rs | 58 +- .../javascript/render/javascript/helpers.rs | 6 +- .../javascript/render/javascript/mod.rs | 6 +- .../{ => winrt}/javascript/render/mod.rs | 0 .../javascript/render/package_json.rs | 0 .../{ => winrt}/javascript/signature.rs | 2 +- .../codegen/{ => winrt}/javascript/structs.rs | 0 .../dynwinrt-codegen/src/codegen/winrt/mod.rs | 8 + .../codegen/{ => winrt}/python/collections.rs | 0 .../src/codegen/{ => winrt}/python/docs.rs | 2 +- .../{ => winrt}/python/generator/class.rs | 28 +- .../{ => winrt}/python/generator/imports.rs | 0 .../{ => winrt}/python/generator/index.rs | 0 .../{ => winrt}/python/generator/mod.rs | 4 +- .../{ => winrt}/python/generator/structs.rs | 2 +- .../{ => winrt}/python/generator/types.rs | 20 +- .../src/codegen/{ => winrt}/python/method.rs | 2 +- .../src/codegen/{ => winrt}/python/mod.rs | 0 .../src/codegen/{ => winrt}/python/naming.rs | 0 .../{ => winrt}/python/native_types.rs | 0 .../codegen/{ => winrt}/python/overloads.rs | 0 .../src/codegen/{ => winrt}/python/shared.rs | 0 .../codegen/{ => winrt}/python/signature.rs | 6 +- .../src/codegen/{ => winrt}/python/structs.rs | 0 .../{ => winrt}/python/stub_helpers.rs | 2 +- .../src/codegen/{ => winrt}/python/stubs.rs | 6 +- .../{ => winrt}/python/type_helpers.rs | 8 +- .../src/codegen/{ => winrt}/shared/docs.rs | 0 .../src/codegen/{ => winrt}/shared/imports.rs | 0 .../src/codegen/{ => winrt}/shared/mod.rs | 0 .../src/codegen/{ => winrt}/shared/structs.rs | 0 tools/dynwinrt-codegen/src/com_metadata.rs | 124 +- tools/dynwinrt-codegen/src/meta.rs | 82 + .../tests/observable_vector_test.rs | 8 + .../uri/IIterator_IWwwFormUrlDecoderEntry.js | 2 +- .../tests/snapshots/uri/WwwFormUrlDecoder.js | 2 +- .../dynwinrt-codegen/tests/win32_com_test.rs | 49 +- 66 files changed, 4855 insertions(+), 3384 deletions(-) create mode 100644 tools/dynwinrt-codegen/src/codegen/com/ir.rs create mode 100644 tools/dynwinrt-codegen/src/codegen/com/javascript/mod.rs rename tools/dynwinrt-codegen/src/codegen/com/{ => javascript}/naming.rs (97%) create mode 100644 tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs create mode 100644 tools/dynwinrt-codegen/src/codegen/com/javascript/render_tests.rs create mode 100644 tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs create mode 100644 tools/dynwinrt-codegen/src/codegen/com/project/interop.rs create mode 100644 tools/dynwinrt-codegen/src/codegen/com/project/mod.rs create mode 100644 tools/dynwinrt-codegen/src/codegen/com/project/types.rs delete mode 100644 tools/dynwinrt-codegen/src/codegen/com/projection.rs delete mode 100644 tools/dynwinrt-codegen/src/codegen/com/render.rs delete mode 100644 tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/docs.rs (98%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/generator.rs (99%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/ir.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/method.rs (99%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/mod.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/naming.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/project/collections.rs (95%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/project/constructors.rs (99%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/project/methods.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/project/mod.rs (99%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/project/structs.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/render/declarations.rs (99%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/render/javascript/commonjs.rs (88%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/render/javascript/helpers.rs (99%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/render/javascript/mod.rs (99%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/render/mod.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/render/package_json.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/signature.rs (99%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/javascript/structs.rs (100%) create mode 100644 tools/dynwinrt-codegen/src/codegen/winrt/mod.rs rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/collections.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/docs.rs (98%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/generator/class.rs (95%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/generator/imports.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/generator/index.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/generator/mod.rs (97%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/generator/structs.rs (98%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/generator/types.rs (94%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/method.rs (99%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/mod.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/naming.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/native_types.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/overloads.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/shared.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/signature.rs (99%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/structs.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/stub_helpers.rs (99%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/stubs.rs (99%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/python/type_helpers.rs (98%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/shared/docs.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/shared/imports.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/shared/mod.rs (100%) rename tools/dynwinrt-codegen/src/codegen/{ => winrt}/shared/structs.rs (100%) diff --git a/.github/skills/classic-com-abi/SKILL.md b/.github/skills/classic-com-abi/SKILL.md index 74c48291..5a33489a 100644 --- a/.github/skills/classic-com-abi/SKILL.md +++ b/.github/skills/classic-com-abi/SKILL.md @@ -124,6 +124,34 @@ Apply these rules: ABI-compatible WinRT projection aliases such as Char16/U16 and enum/I32 arrays. +### Required codegen architecture + +```text +ComInterfaceMeta + -> codegen/com/project + -> validated ComType / ProjectedComMethod + -> codegen/com/javascript renderer +``` + +- Keep WinRT generators under `codegen/winrt`; do not import COM semantic IR + into them. +- Convert shared `TypeMeta` values to the closed COM-local `ComType` set before + rendering. +- Encode parameter direction, native return convention, result ordering, + ownership, cleanup, string-buffer relationships, activation, and + dynamic-IID behavior in `ProjectedComMethod`. +- Production renderers may consume only projected COM IR. They must not import + `TypeMeta`, `MethodMeta`, `ParamMeta`, metadata attributes, or infer + ownership. +- Every renderer match over `ComType` must be exhaustive. Never use a wildcard + branch that emits `pointerType`, `Buffer`, bigint, or a raw value. +- Transparent scalar typedefs preserve their underlying scalar ABI. A + one-field Win32 struct is not automatically a handle. +- Pointer aliases require explicit `HandleValue`, `DataPointer`, or + `StringPointer` classification. Unknown aliases fail closed. +- Cleanup identifiers must match a single known allocator exactly. Do not use + substring matching. + ## Projection responsibility Keep these responsibilities separate: diff --git a/docs/classic-com-support.md b/docs/classic-com-support.md index ca7eed29..2889a1bf 100644 --- a/docs/classic-com-support.md +++ b/docs/classic-com-support.md @@ -59,6 +59,49 @@ In particular: - language-friendly choices remain a codegen responsibility after the COM planner has validated the native contract. +## Code generation architecture + +Code generation is organized by semantic domain before target language: + +```text +codegen/ +├── winrt/ +│ ├── shared/ +│ ├── javascript/ +│ └── python/ +└── com/ + ├── ir.rs + ├── project/ + │ ├── types.rs + │ └── interop.rs + └── javascript/ + ├── types.rs + └── render.rs +``` + +The Classic COM flow is: + +```text +ComInterfaceMeta + -> COM type projection + -> validated ComType / ProjectedComMethod + -> JavaScript and declaration renderer +``` + +`ComType` is a closed set of supported ABI semantics: primitives, transparent +scalar typedefs, pointer-sized scalars, BOOL/HRESULT, GUID, HSTRING, enums, +explicitly classified handle/data/string pointers, BSTR, raw input pointers, +and managed interfaces with resolved IIDs. Parameter direction, return +convention, result ownership, cleanup, string-buffer relationships, +activation, and dynamic-IID behavior are encoded in the projected IR. + +Arrays, parameterized and async interfaces, delegates, unknown layouts, +unclassified pointer typedefs, unresolved IIDs, unknown allocators, and +unsupported ownership transfers fail during projection. The renderer cannot +see `TypeMeta` or metadata attributes and has no default pointer/Buffer +fallback; it only serializes the validated projected IR with exhaustive type +matches. + ## Size of Windows.Win32.winmd The counts below are exact for @@ -270,7 +313,9 @@ Discarding every successful HRESULT loses information. Windows.Win32 metadata marks these methods with `CanReturnMultipleSuccessValuesAttribute`. The COM projection preserves the numeric successful HRESULT for marked methods while still throwing failed -HRESULTs. Unmarked HRESULT methods retain the normal throw-or-`void` behavior. +HRESULTs. Exact documented exceptions such as `IPersistFile::GetCurFile`, +whose metadata omits the marker, are classified explicitly. Other unmarked +HRESULT methods retain the normal throw-or-`void` behavior. ### 9. Apartment affinity and marshaling @@ -417,7 +462,7 @@ of every type in the 24 MB metadata file. | Arbitrary unions, bitfields, and nested pointer-rich structs | `D3D11_COUNTER_INFO`, `STATSTG`, `STRRET`, `POINTL`, `BIND_OPTS`, audio/media formats | The current generator has no general native C layout engine. | Win32 winmd + codegen diagnostics | | Writable caller-sized native arrays | `IDispatch::GetIDsOfNames`, counted byte/element output buffers | A scalar pointee is not sufficient storage. These are rejected unless a supported string-buffer projection applies. | Win32 winmd `NativeArrayInfo` + codegen diagnostic | | `BSTR**` arrays and BSTR in/out arrays | Automation collection APIs | Each element has independent allocation and release semantics. | Win32 winmd signature + ownership analysis | -| Caller-owned ANSI output buffers | `PSTR` output-buffer APIs | Safe sizing and decoding are not yet projected. | Win32 winmd signature + renderer limitation | +| Caller-owned ANSI output buffers | `PSTR` output-buffer APIs | Safe sizing and decoding are not yet projected. | Win32 winmd signature + projection limitation | | Untyped output pointers without allocator/ownership | `IDXGIFactory::GetPrivateData`, `IAudioClient::IsFormatSupported` | The runtime cannot infer whether the result is borrowed, COM-owned, `CoTaskMem`, or another allocator. | Win32 winmd + codegen diagnostics | | Interface `[in, out]` ownership | `IWbemServices::OpenNamespace` | Replacing an existing interface pointer requires explicit release/AddRef transfer semantics. | Win32 winmd + codegen diagnostic | | Arbitrary COM sink/interface implementation | Connection points and event sinks | `Advise` requires implementing a caller-defined COM interface, not only invoking one. | Runtime/public-API boundary | diff --git a/tools/dynwinrt-codegen/src/codegen/com/ir.rs b/tools/dynwinrt-codegen/src/codegen/com/ir.rs new file mode 100644 index 00000000..cf8129c0 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/ir.rs @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Validated Classic-COM semantic IR. +//! +//! Nothing in this module depends on the shared WinRT metadata model. A value +//! can enter this IR only after its ABI shape, ownership, and projection have +//! been validated by `project`. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ComPrimitive { + Bool, + I8, + U8, + I16, + U16, + I32, + U32, + I64, + U64, + F32, + F64, + Char16, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ComEnumUnderlying { + I8, + U8, + I16, + U16, + I32, + U32, + I64, + U64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ComScalarRepr { + Primitive(ComPrimitive), + NativeIsize, + NativeUsize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(super) enum PointerAliasKind { + HandleValue, + DataPointer, + StringPointer, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum ComType { + Primitive(ComPrimitive), + NativeIsize, + NativeUsize, + Win32Bool, + HResult, + Guid, + HString, + Enum { + name: String, + underlying: ComEnumUnderlying, + }, + ScalarAlias { + name: String, + underlying: ComScalarRepr, + }, + RawPointer, + PointerAlias { + name: String, + kind: PointerAliasKind, + }, + Bstr, + ManagedInterface { + iid: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum UnsupportedComType { + Array, + ParameterizedInterface { namespace: String, name: String }, + AsyncInterface, + Delegate { namespace: String, name: String }, + NativeStructLayout { namespace: String, name: String }, + UnknownPointerAlias { namespace: String, name: String }, + UnresolvedInterface { namespace: String, name: String }, + UnresolvedRuntimeClass { namespace: String, name: String }, + UnknownOwnership { type_name: String }, + UnsupportedDirectReturn { type_name: String }, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ComParamDirection { + In, + Out, + InOut, + OutStringBuffer, +} + +impl ComParamDirection { + pub(super) fn is_input(self) -> bool { + matches!(self, Self::In | Self::InOut) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct ProjectedComParam { + pub(super) name: String, + pub(super) typ: ComType, + pub(super) direction: ComParamDirection, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum ComReturnConvention { + HResult, + SemanticHResult, + Void, + Direct(ComType), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum StringEncoding { + Wide, + Ansi, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum ResultConversion { + Value, + ManagedCom, + Bstr, + CoTaskMemString(StringEncoding), + CoTaskMemData, + HString, + DynamicIidAdoption, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ResultSource { + DirectReturn, + Param(usize), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct ProjectedComResult { + pub(super) typ: ComType, + pub(super) source: ResultSource, + pub(super) conversion: ResultConversion, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct StringBufferPlan { + pub(super) buffer_param_index: usize, + pub(super) count_param_index: usize, + pub(super) encoding: StringEncoding, + pub(super) optional_param_indices: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum ProjectedComMethodKind { + Normal, + CallerSuppliedDynamicIid { + natural_param_count: usize, + }, + SynthesizedGetForWindow { + natural_param_count: usize, + target_iid: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct ProjectedComMethod { + pub(super) name: String, + pub(super) camel_name: String, + pub(super) vtable_index: usize, + pub(super) params: Vec, + pub(super) return_convention: ComReturnConvention, + pub(super) results: Vec, + pub(super) string_buffer: Option, + pub(super) kind: ProjectedComMethodKind, + pub(super) doc: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum ActivationPlan { + None, + Coclass { + clsid: String, + coclass_name: String, + }, + WinRtFactory { + class_name: String, + class_namespace: String, + target_iid: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum ProjectedEnumValue { + Signed(i64), + Unsigned(u64), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct ProjectedComEnumMember { + pub(super) name: String, + pub(super) value: ProjectedEnumValue, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct ProjectedComEnum { + pub(super) name: String, + pub(super) underlying: ComEnumUnderlying, + pub(super) members: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct ProjectedComInterface { + pub(super) name: String, + pub(super) namespace: String, + pub(super) iid: String, + pub(super) is_iunknown_rooted: bool, + pub(super) methods: Vec, + pub(super) activation: ActivationPlan, + pub(super) referenced_enums: Vec, +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/javascript/mod.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/mod.rs new file mode 100644 index 00000000..20afccfd --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/mod.rs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub(super) mod naming; +pub(super) mod render; +mod types; diff --git a/tools/dynwinrt-codegen/src/codegen/com/naming.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/naming.rs similarity index 97% rename from tools/dynwinrt-codegen/src/codegen/com/naming.rs rename to tools/dynwinrt-codegen/src/codegen/com/javascript/naming.rs index fa38ad79..4ab4c002 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/naming.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/naming.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -pub(super) fn camel_case(name: &str) -> String { +pub(in crate::codegen::com) fn camel_case(name: &str) -> String { if name.is_empty() { return String::new(); } diff --git a/tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs new file mode 100644 index 00000000..be9dc88b --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs @@ -0,0 +1,717 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Pure JavaScript and declaration rendering for validated Classic-COM IR. + +use std::collections::BTreeMap; + +#[cfg(test)] +use super::super::ir::ProjectedComEnumMember; +use super::super::ir::{ + ActivationPlan, ComEnumUnderlying, ComParamDirection, ComReturnConvention, ComType, + PointerAliasKind, ProjectedComEnum, ProjectedComInterface, ProjectedComMethod, + ProjectedComMethodKind, ProjectedComParam, ProjectedEnumValue, ResultConversion, + StringEncoding, +}; +use super::naming::js_param_name; +use super::types::{ + abi_type_js, input_type_dts, result_type_dts, scalar_type_dts, unwrap_result_js, wrap_arg_js, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ComGeneratedOutput { + pub js: String, + pub dts: String, + pub extra_files: Vec<(String, String)>, +} + +pub(in crate::codegen::com) fn render_com_interface( + meta: &ProjectedComInterface, +) -> ComGeneratedOutput { + let js = render_js(meta); + let dts = render_dts(meta); + let mut extra_files = Vec::new(); + for en in &meta.referenced_enums { + let (enum_js, enum_dts) = render_enum_files(en); + extra_files.push((format!("{}.js", en.name), enum_js)); + extra_files.push((format!("{}.d.ts", en.name), enum_dts)); + } + extra_files.sort_by(|a, b| a.0.cmp(&b.0)); + ComGeneratedOutput { + js, + dts, + extra_files, + } +} + +fn render_js(meta: &ProjectedComInterface) -> String { + let mut out = String::new(); + out.push_str("// Generated by dynwinrt-codegen — do not edit\n"); + let runtime_imports = if matches!(meta.activation, ActivationPlan::WinRtFactory { .. }) { + "DynCom, DynComMethodSig, DynWinRtValue, WinGuid" + } else { + "DynCom, DynComMethodSig, WinGuid" + }; + out.push_str(&format!( + "import {{ {runtime_imports} }} from '{}';\n", + com_runtime_import_name() + )); + for en in &meta.referenced_enums { + out.push_str(&format!( + "import {{ {} }} from './{}.js';\n", + en.name, en.name + )); + } + out.push('\n'); + if meta + .methods + .iter() + .any(|method| method.string_buffer.is_some()) + { + out.push_str("function _normalizeStringBufferCount(value, name) {\n"); + out.push_str(" if (!Number.isInteger(value) || value <= 0) throw new TypeError(`${name} must be a positive integer`);\n"); + out.push_str(" return value;\n}\n"); + out.push_str("function _decodeWideString(buffer) {\n let end = 0;\n"); + out.push_str( + " while (end + 1 < buffer.length && buffer.readUInt16LE(end) !== 0) end += 2;\n", + ); + out.push_str(" return buffer.subarray(0, end).toString('utf16le');\n}\n\n"); + } + out.push_str(&format!( + "export const IID_{} = WinGuid.parse('{}');\n", + meta.name, meta.iid + )); + if let ActivationPlan::WinRtFactory { + class_name, + target_iid, + .. + } = &meta.activation + { + out.push_str(&format!( + "const IID_{class_name}_default = WinGuid.parse('{target_iid}');\n" + )); + } + out.push('\n'); + let register_fn = if meta.is_iunknown_rooted { + "registerIUnknownInterface" + } else { + "registerIInspectableInterface" + }; + let cache_var = format!("_{}Cache", meta.name); + let iface_var = format!("_{}", meta.name); + out.push_str(&format!("let {cache_var};\n")); + out.push_str(&format!( + "const {iface_var} = new Proxy({{}}, {{\n get(_target, prop) {{\n {cache_var} ??= DynCom.{register_fn}('{}.{}', IID_{})\n", + meta.namespace, meta.name, meta.name + )); + for method in &meta.methods { + out.push_str(&format!( + " .addMethod('{}', {})\n", + method.name, + build_method_sig_js(method) + )); + } + if out.ends_with('\n') { + out.pop(); + } + out.push_str(";\n"); + out.push_str(&format!(" const value = {cache_var}[prop];\n return typeof value === 'function' ? value.bind({cache_var}) : value;\n }},\n}});\n\n")); + out.push_str(&format!("export class {} {{\n", meta.name)); + out.push_str(" _obj;\n constructor(obj) { this._obj = obj; }\n"); + out.push_str(&format!( + " static _fromNative(obj) {{ return new {}(obj); }}\n", + meta.name + )); + match &meta.activation { + ActivationPlan::None => {} + ActivationPlan::Coclass { + clsid, + coclass_name, + } => { + out.push_str(&format!( + " /** Create a new `{}` via `CoCreateInstance` on `CLSID_{coclass_name}`. */\n", + meta.name + )); + out.push_str(&format!(" static create() {{\n const _obj = DynCom.coCreateInstance('{clsid}', IID_{});\n return new {}(_obj);\n }}\n", meta.name, meta.name)); + } + ActivationPlan::WinRtFactory { + class_name, + class_namespace, + .. + } => { + let full = format!("{class_namespace}.{class_name}"); + out.push_str(&format!(" /** Create a new `{}` by activating the `{full}` factory and QI'ing to the interop. */\n", meta.name)); + out.push_str(&format!(" static create() {{\n const factory = DynWinRtValue.activationFactory('{full}');\n const _obj = factory.cast(IID_{});\n return new {}(_obj);\n }}\n", meta.name, meta.name)); + } + } + for method in &meta.methods { + match method.kind { + ProjectedComMethodKind::Normal => emit_method_js(&mut out, method, &iface_var), + ProjectedComMethodKind::CallerSuppliedDynamicIid { + natural_param_count, + } => emit_dynamic_iid_method_js(&mut out, method, natural_param_count, &iface_var), + ProjectedComMethodKind::SynthesizedGetForWindow { + natural_param_count, + ref target_iid, + } => emit_synthesized_interop_method_js( + &mut out, + method, + natural_param_count, + target_iid, + &iface_var, + meta, + ), + } + } + out.push_str("}\n"); + out +} + +fn build_method_sig_js(method: &ProjectedComMethod) -> String { + let mut parts = Vec::new(); + for (index, param) in method.params.iter().enumerate() { + match param.direction { + ComParamDirection::In => parts.push(format!(".addIn({})", abi_type_js(¶m.typ))), + ComParamDirection::InOut => { + parts.push(format!(".addInOut({})", abi_type_js(¶m.typ))) + } + ComParamDirection::OutStringBuffer => parts.push(".addIn(DynCom.pointerType())".into()), + ComParamDirection::Out => { + if method.string_buffer.as_ref().is_some_and(|plan| { + index > plan.count_param_index && plan.optional_param_indices.contains(&index) + }) { + parts.push(".addIn(DynCom.pointerType())".into()); + } else { + parts.push(format!(".addOut({})", abi_type_js(¶m.typ))); + } + } + } + } + match &method.return_convention { + ComReturnConvention::HResult => {} + ComReturnConvention::SemanticHResult => parts.push(".preserveHresult()".into()), + ComReturnConvention::Void => parts.push(".returnsVoid()".into()), + ComReturnConvention::Direct(typ) => parts.push(format!(".returns({})", abi_type_js(typ))), + } + if parts.is_empty() { + "new DynComMethodSig()".into() + } else { + format!("new DynComMethodSig(){}", parts.join("")) + } +} + +fn input_params(method: &ProjectedComMethod) -> Vec<(usize, &ProjectedComParam)> { + method + .params + .iter() + .enumerate() + .filter(|(_, param)| param.direction.is_input()) + .collect() +} + +fn emit_method_js(out: &mut String, method: &ProjectedComMethod, iface_var: &str) { + let inputs = input_params(method); + let params = inputs + .iter() + .enumerate() + .map(|(surface, (index, param))| { + let name = js_param_name(¶m.name, surface); + if let Some(plan) = &method.string_buffer { + if plan.optional_param_indices.contains(index) { + return if *index == plan.count_param_index { + format!("{name} = 260") + } else { + format!("{name} = 0") + }; + } + } + name + }) + .collect::>(); + out.push_str(&format!( + " {}({}) {{\n", + method.camel_name, + params.join(", ") + )); + if let Some(plan) = &method.string_buffer { + emit_string_buffer_method_body(out, method, plan, &inputs, iface_var); + out.push_str(" }\n"); + return; + } + let args = inputs + .iter() + .enumerate() + .map(|(surface, (_, param))| wrap_arg_js(¶m.typ, &js_param_name(¶m.name, surface))) + .collect::>(); + emit_invocation_and_results(out, method, iface_var, &args); + out.push_str(" }\n"); +} + +fn emit_string_buffer_method_body( + out: &mut String, + method: &ProjectedComMethod, + plan: &super::super::ir::StringBufferPlan, + inputs: &[(usize, &ProjectedComParam)], + iface_var: &str, +) { + let count_surface = inputs + .iter() + .position(|(index, _)| *index == plan.count_param_index) + .expect("validated count input"); + let count_name = js_param_name(&method.params[plan.count_param_index].name, count_surface); + if plan.encoding == StringEncoding::Ansi { + out.push_str(" throw new Error('PSTR out buffers are not yet decoded safely');\n"); + return; + } + let args = method + .params + .iter() + .enumerate() + .filter_map(|(index, param)| { + if index == plan.buffer_param_index { + Some("DynCom.pointer(_buffer)".into()) + } else if param.direction.is_input() { + let surface = inputs + .iter() + .position(|(input_index, _)| *input_index == index) + .expect("validated input"); + Some(wrap_arg_js( + ¶m.typ, + &js_param_name(¶m.name, surface), + )) + } else if index > plan.count_param_index && plan.optional_param_indices.contains(&index) + { + Some("DynCom.pointer(0n)".into()) + } else { + None + } + }) + .collect::>(); + out.push_str(&format!( + " {count_name} = _normalizeStringBufferCount({count_name}, '{count_name}');\n" + )); + out.push_str(&format!( + " const _buffer = Buffer.alloc({count_name} * 2);\n" + )); + match method.results.len() { + 0 => out.push_str(&format!( + " {iface_var}.method({}).invoke(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )), + 1 => out.push_str(&format!( + " const _out = {iface_var}.method({}).invoke(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )), + _ => out.push_str(&format!( + " const _out = {iface_var}.method({}).invokeAll(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )), + } + out.push_str(" const _text = _decodeWideString(_buffer);\n"); + match method.results.len() { + 0 => out.push_str(" return _text;\n"), + 1 => out.push_str(&format!( + " return [_text, {}];\n", + unwrap_result_js(&method.results[0], "_out") + )), + _ => { + let values = method + .results + .iter() + .enumerate() + .map(|(index, result)| unwrap_result_js(result, &format!("_out[{index}]"))) + .collect::>(); + out.push_str(&format!(" return [_text, {}];\n", values.join(", "))); + } + } +} + +fn emit_invocation_and_results( + out: &mut String, + method: &ProjectedComMethod, + iface_var: &str, + args: &[String], +) { + match method.results.len() { + 0 => out.push_str(&format!( + " {iface_var}.method({}).invoke(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )), + 1 => { + out.push_str(&format!( + " const _out = {iface_var}.method({}).invoke(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )); + out.push_str(&format!( + " return {};\n", + unwrap_result_js(&method.results[0], "_out") + )); + } + _ => { + out.push_str(&format!( + " const _r = {iface_var}.method({}).invokeAll(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )); + let values = method + .results + .iter() + .enumerate() + .map(|(index, result)| unwrap_result_js(result, &format!("_r[{index}]"))) + .collect::>(); + out.push_str(&format!(" return [{}];\n", values.join(", "))); + } + } +} + +fn emit_dynamic_iid_method_js( + out: &mut String, + method: &ProjectedComMethod, + natural_count: usize, + iface_var: &str, +) { + let natural = &method.params[..natural_count]; + let mut surface = natural + .iter() + .enumerate() + .map(|(index, param)| js_param_name(¶m.name, index)) + .collect::>(); + surface.push("iid".into()); + let mut args = natural + .iter() + .enumerate() + .map(|(index, param)| wrap_arg_js(¶m.typ, &js_param_name(¶m.name, index))) + .collect::>(); + args.push("DynCom.iidPointer(_iid)".into()); + out.push_str(&format!( + " {}({}) {{\n const _iid = WinGuid.parse(iid);\n", + method.camel_name, + surface.join(", ") + )); + out.push_str(&format!( + " const _raw = {iface_var}.method({}).invoke(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )); + out.push_str(" return DynCom.adoptComPointer(_raw, _iid);\n }\n"); +} + +fn emit_synthesized_interop_method_js( + out: &mut String, + method: &ProjectedComMethod, + natural_count: usize, + _target_iid: &str, + iface_var: &str, + meta: &ProjectedComInterface, +) { + let natural = &method.params[..natural_count]; + let params = natural + .iter() + .enumerate() + .map(|(index, param)| js_param_name(¶m.name, index)) + .collect::>(); + let mut args = natural + .iter() + .enumerate() + .map(|(index, param)| wrap_arg_js(¶m.typ, &js_param_name(¶m.name, index))) + .collect::>(); + let class_name = match &meta.activation { + ActivationPlan::WinRtFactory { class_name, .. } => class_name, + _ => unreachable!("validated interop activation"), + }; + args.push(format!("DynCom.iidPointer(IID_{class_name}_default)")); + out.push_str(&format!( + " {}({}) {{\n", + method.camel_name, + params.join(", ") + )); + out.push_str(&format!( + " const _raw = {iface_var}.method({}).invoke(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )); + out.push_str(&format!(" const _out = DynCom.adoptComPointer(_raw, IID_{class_name}_default);\n return _out;\n }}\n")); +} + +fn render_dts(meta: &ProjectedComInterface) -> String { + let mut out = String::new(); + out.push_str("// Generated by dynwinrt-codegen — do not edit\n"); + for en in &meta.referenced_enums { + out.push_str(&format!( + "import {{ {} }} from './{}.js';\n", + en.name, en.name + )); + } + if needs_bridge_import(meta) { + out.push_str(&format!( + "import type {{ DynWinRtValue }} from '{}';\n", + com_runtime_import_name() + )); + } + out.push('\n'); + for (name, underlying) in collect_scalar_aliases(meta) { + out.push_str(&format!( + "/** Transparent Win32 scalar typedef. */\nexport type {name} = {};\n", + scalar_type_dts(underlying) + )); + } + if !collect_scalar_aliases(meta).is_empty() { + out.push('\n'); + } + for (name, kind) in collect_pointer_aliases(meta) { + match kind { + PointerAliasKind::HandleValue => out.push_str(&format!("/** Opaque Win32 handle value. Pass a raw pointer value as a `bigint` (full pointer width) or `number` (safe integer). */\nexport type {name} = bigint | number;\n")), + PointerAliasKind::DataPointer => out.push_str(&format!("/** Opaque native data address. Inputs may also use a `Buffer`/`Uint8Array`, whose backing-store address is passed and retained for the call. */\nexport type {name} = bigint | number;\n")), + PointerAliasKind::StringPointer => out.push_str(&format!("/** Win32 NUL-terminated string pointer. Pass a `Buffer` holding the string bytes (including the NUL terminator), or pass a raw pointer as `bigint`. */\nexport type {name} = bigint | Buffer;\n")), + } + } + if !collect_pointer_aliases(meta).is_empty() { + out.push('\n'); + } + out.push_str(&format!( + "export declare const IID_{}: unknown;\n\nexport declare class {} {{\n", + meta.name, meta.name + )); + match &meta.activation { + ActivationPlan::None => {} + ActivationPlan::Coclass { .. } => out.push_str(&format!( + " /** Create a new instance via the coclass activation path. */\n static create(): {};\n", + meta.name + )), + ActivationPlan::WinRtFactory { .. } => out.push_str(&format!( + " /** Activate the projected WinRT class and QI to the interop. */\n static create(): {};\n", + meta.name + )), + } + out.push_str(&format!(" /** Wrap an existing native COM pointer (for QueryInterface bridging). */\n static _fromNative(obj: unknown): {};\n", meta.name)); + for method in &meta.methods { + let (params, ret) = match method.kind { + ProjectedComMethodKind::Normal => (dts_params(method), dts_return_type(method)), + ProjectedComMethodKind::CallerSuppliedDynamicIid { + natural_param_count, + } => { + let mut params = dts_natural_params(method, natural_param_count); + params.push("iid: string".into()); + (params, "DynWinRtValue".into()) + } + ProjectedComMethodKind::SynthesizedGetForWindow { + natural_param_count, + .. + } => ( + dts_natural_params(method, natural_param_count), + "DynWinRtValue".into(), + ), + }; + out.push_str(&format!( + " {}({}): {};\n", + method.camel_name, + params.join(", "), + ret + )); + } + out.push_str("}\n"); + out +} + +fn dts_natural_params(method: &ProjectedComMethod, count: usize) -> Vec { + method.params[..count] + .iter() + .enumerate() + .map(|(index, param)| { + format!( + "{}: {}", + js_param_name(¶m.name, index), + input_type_dts(¶m.typ) + ) + }) + .collect() +} + +fn dts_params(method: &ProjectedComMethod) -> Vec { + method + .params + .iter() + .enumerate() + .filter(|(_, param)| param.direction.is_input()) + .enumerate() + .map(|(surface, (index, param))| { + let mut name = js_param_name(¶m.name, surface); + if method + .string_buffer + .as_ref() + .is_some_and(|plan| plan.optional_param_indices.contains(&index)) + { + name.push('?'); + } + format!("{name}: {}", input_type_dts(¶m.typ)) + }) + .collect() +} + +fn dts_return_type(method: &ProjectedComMethod) -> String { + if method.string_buffer.is_some() { + return if method.results.is_empty() { + "string".into() + } else { + format!( + "[string, {}]", + method + .results + .iter() + .map(result_type_dts) + .collect::>() + .join(", ") + ) + }; + } + match method.results.len() { + 0 => "void".into(), + 1 => result_type_dts(&method.results[0]), + _ => format!( + "[{}]", + method + .results + .iter() + .map(result_type_dts) + .collect::>() + .join(", ") + ), + } +} + +fn collect_pointer_aliases(meta: &ProjectedComInterface) -> Vec<(String, PointerAliasKind)> { + let mut aliases = BTreeMap::new(); + for method in &meta.methods { + for typ in + method + .params + .iter() + .map(|param| ¶m.typ) + .chain(match &method.return_convention { + ComReturnConvention::Direct(typ) => Some(typ), + _ => None, + }) + { + match typ { + ComType::PointerAlias { name, kind } => { + aliases.insert(name.clone(), *kind); + } + ComType::Bstr => { + aliases.insert("BSTR".into(), PointerAliasKind::HandleValue); + } + ComType::Primitive(_) + | ComType::NativeIsize + | ComType::NativeUsize + | ComType::Win32Bool + | ComType::HResult + | ComType::Guid + | ComType::HString + | ComType::Enum { .. } + | ComType::ScalarAlias { .. } + | ComType::RawPointer + | ComType::ManagedInterface { .. } => {} + } + } + } + aliases.into_iter().collect() +} + +fn collect_scalar_aliases( + meta: &ProjectedComInterface, +) -> Vec<(String, super::super::ir::ComScalarRepr)> { + let mut aliases = BTreeMap::new(); + for method in &meta.methods { + for typ in + method + .params + .iter() + .map(|param| ¶m.typ) + .chain(match &method.return_convention { + ComReturnConvention::Direct(typ) => Some(typ), + _ => None, + }) + { + if let ComType::ScalarAlias { name, underlying } = typ { + aliases.insert(name.clone(), *underlying); + } + } + } + aliases.into_iter().collect() +} + +fn needs_bridge_import(meta: &ProjectedComInterface) -> bool { + matches!(meta.activation, ActivationPlan::WinRtFactory { .. }) + || meta.methods.iter().any(|method| { + !matches!(method.kind, ProjectedComMethodKind::Normal) + || method + .params + .iter() + .any(|param| matches!(param.typ, ComType::ManagedInterface { .. })) + || method.results.iter().any(|result| { + matches!( + result.conversion, + ResultConversion::ManagedCom + | ResultConversion::CoTaskMemData + | ResultConversion::DynamicIidAdoption + ) + }) + }) +} + +fn com_runtime_import_name() -> String { + let import_name = crate::codegen::project::get_import_name(); + if import_name == "@microsoft/dynwinrt" { + format!("{import_name}/com") + } else { + import_name + } +} + +fn render_enum_files(en: &ProjectedComEnum) -> (String, String) { + let mut js = String::from("// Generated by dynwinrt-codegen — do not edit\n"); + js.push_str(&format!("export const {} = Object.freeze({{\n", en.name)); + for member in &en.members { + js.push_str(&format!( + " {}: {},\n", + member.name, + render_enum_value(&member.value, en.underlying) + )); + } + js.push_str("});\n"); + let mut dts = String::from("// Generated by dynwinrt-codegen — do not edit\n"); + dts.push_str(&format!( + "export type {} = (typeof {})[keyof typeof {}];\n", + en.name, en.name, en.name + )); + dts.push_str(&format!("export declare const {}: {{\n", en.name)); + for member in &en.members { + dts.push_str(&format!( + " readonly {}: {};\n", + member.name, + render_enum_value(&member.value, en.underlying) + )); + } + dts.push_str("};\n"); + (js, dts) +} + +fn render_enum_value(value: &ProjectedEnumValue, underlying: ComEnumUnderlying) -> String { + let suffix = if matches!(underlying, ComEnumUnderlying::I64 | ComEnumUnderlying::U64) { + "n" + } else { + "" + }; + match value { + ProjectedEnumValue::Signed(value) => format!("{value}{suffix}"), + ProjectedEnumValue::Unsigned(value) => format!("{value}{suffix}"), + } +} + +#[cfg(test)] +#[path = "render_tests.rs"] +mod tests; diff --git a/tools/dynwinrt-codegen/src/codegen/com/javascript/render_tests.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/render_tests.rs new file mode 100644 index 00000000..09bff2d6 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/render_tests.rs @@ -0,0 +1,1909 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::*; +use crate::codegen::com::generate_com_interface_files; +use crate::codegen::com::javascript::naming::{camel_case, strip_hungarian}; +use crate::codegen::com::project::project_com_interface; +use crate::codegen::com::project::types::project_type; +use crate::com_metadata::{ + ComEnumMeta, ComEnumValue, ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta, +}; +use crate::types::TypeMeta; + +type HandleAliasKind = PointerAliasKind; + +#[test] +fn renderer_api_accepts_only_projected_ir() { + let projected = ProjectedComInterface { + name: "ITest".into(), + namespace: "Tests".into(), + iid: "00000000-0000-0000-0000-000000000001".into(), + is_iunknown_rooted: true, + methods: Vec::new(), + activation: ActivationPlan::None, + referenced_enums: Vec::new(), + }; + let output = render_com_interface(&projected); + assert!(output.js.contains("registerIUnknownInterface")); + assert!(output.dts.contains("export declare class ITest")); +} + +#[test] +fn allocator_contract_rejects_trailing_separator_and_whitespace() { + for free_with in ["CoTaskMemFree:", " CoTaskMemFree", "CoTaskMemFree "] { + let method = MethodMeta { + name: "GetData".into(), + params: vec![ParamMeta { + name: "data".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 0, + free_with: free_with.into(), + }], + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("malformed allocator names must fail closed"); + assert!(error.contains("unsupported output cleanup contract")); + } +} + +#[test] +fn cotaskmem_handle_and_inout_ownership_fail_closed() { + let handle = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + for (typ, direction, expected) in [ + ( + handle, + ParamDirection::Out, + "requires an Out data or string pointer", + ), + ( + TypeMeta::Object, + ParamDirection::InOut, + "allocator ownership transfer for [in, out]", + ), + ] { + let method = MethodMeta { + name: "GetData".into(), + params: vec![ParamMeta { + name: "data".into(), + typ, + direction, + }], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 0, + free_with: "CoTaskMemFree".into(), + }], + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("CoTaskMem ownership must not apply to handles or InOut"); + assert!(error.contains(expected), "{error}"); + } +} + +#[test] +fn dynamic_iid_output_rejects_cleanup_contract() { + let method = MethodMeta { + name: "GetThing".into(), + params: vec![ + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "result".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 1, + free_with: "CoTaskMemFree".into(), + }], + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("dynamic COM outputs cannot carry allocator cleanup"); + + assert!(error.contains("dynamic-IID interface output cannot declare an allocator")); +} + +fn handle_type_name(typ: &TypeMeta) -> Option { + match project_type(typ).ok()? { + ComType::PointerAlias { name, .. } => Some(name), + _ => None, + } +} + +fn handle_alias_kind(typ: &TypeMeta) -> Option { + match project_type(typ).ok()? { + ComType::PointerAlias { kind, .. } => Some(kind), + _ => None, + } +} + +fn is_hresult(typ: &TypeMeta) -> bool { + matches!(project_type(typ), Ok(ComType::HResult)) +} + +fn ts_type_expr_dts(typ: &TypeMeta) -> String { + super::super::types::type_dts(&project_type(typ).unwrap()) +} + +fn ts_type_expr_js(typ: &TypeMeta) -> String { + abi_type_js(&project_type(typ).unwrap()) +} + +fn wrap_arg_js(typ: &TypeMeta, variable: &str) -> String { + super::super::types::wrap_arg_js(&project_type(typ).unwrap(), variable) +} + +fn render_js(meta: &ComInterfaceMeta, _interop: Option<()>) -> String { + let projected = project_com_interface(meta, "").unwrap(); + super::render_js(&projected) +} + +fn render_dts(meta: &ComInterfaceMeta, _interop: Option<()>) -> String { + let projected = project_com_interface(meta, "").unwrap(); + super::render_dts(&projected) +} + +fn build_method_sig_js(method: &MethodMeta) -> String { + let projected = project_com_interface(&plain_iface_with_method(method.clone()), "").unwrap(); + super::build_method_sig_js(&projected.methods[0]) +} + +fn render_enum_files(en: &ComEnumMeta) -> (String, String) { + let underlying = match en.underlying { + TypeMeta::I8 => ComEnumUnderlying::I8, + TypeMeta::U8 => ComEnumUnderlying::U8, + TypeMeta::I16 => ComEnumUnderlying::I16, + TypeMeta::U16 => ComEnumUnderlying::U16, + TypeMeta::I32 => ComEnumUnderlying::I32, + TypeMeta::U32 => ComEnumUnderlying::U32, + TypeMeta::I64 => ComEnumUnderlying::I64, + TypeMeta::U64 => ComEnumUnderlying::U64, + _ => panic!("unsupported test enum underlying type"), + }; + let projected = ProjectedComEnum { + name: en.name.clone(), + underlying, + members: en + .members + .iter() + .map(|member| ProjectedComEnumMember { + name: member.name.clone(), + value: match member.value { + ComEnumValue::Signed(value) => ProjectedEnumValue::Signed(value), + ComEnumValue::Unsigned(value) => ProjectedEnumValue::Unsigned(value), + }, + }) + .collect(), + }; + super::render_enum_files(&projected) +} + +fn method_is_interop_shape(method: &MethodMeta) -> Option> { + if !method + .return_type + .as_ref() + .is_some_and(|typ| matches!(project_type(typ), Ok(ComType::HResult))) + || method.params.len() < 2 + { + return None; + } + let output = method.params.last()?; + let iid = &method.params[method.params.len() - 2]; + let iid_name = iid.name.to_ascii_lowercase(); + if output.direction != ParamDirection::Out + || output.typ != TypeMeta::Object + || iid.direction != ParamDirection::In + || iid.typ != TypeMeta::Object + || !matches!(iid_name.as_str(), "iid" | "riid") + || method.params[..method.params.len() - 2] + .iter() + .any(|param| param.direction != ParamDirection::In) + { + return None; + } + Some(method.params[..method.params.len() - 2].to_vec()) +} + +#[test] +fn camel_case_basic() { + assert_eq!(camel_case("HrInit"), "hrInit"); + assert_eq!(camel_case("SetProgressValue"), "setProgressValue"); + assert_eq!(camel_case("AddTab"), "addTab"); + assert_eq!(camel_case("URL"), "url"); + assert_eq!(camel_case("IOHandle"), "ioHandle"); +} + +#[test] +fn default_runtime_import_uses_com_subpath() { + let previous = crate::codegen::project::get_import_name(); + crate::codegen::project::set_import_name("@microsoft/dynwinrt"); + assert_eq!(com_runtime_import_name(), "@microsoft/dynwinrt/com"); + crate::codegen::project::set_import_name(&previous); +} + +#[test] +fn strip_hungarian_only_at_word_boundary() { + assert_eq!(strip_hungarian("dwReserved"), "Reserved"); + assert_eq!(strip_hungarian("hwndTab"), "Tab"); + // "hwnd" alone must NOT be stripped (no uppercase follow-up). + assert_eq!(strip_hungarian("hwnd"), "hwnd"); +} + +#[test] +fn handle_type_name_recognizes_hwnd_shape() { + let hwnd = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + assert_eq!(handle_type_name(&hwnd).as_deref(), Some("HWND")); +} + +#[test] +fn handle_alias_kind_distinguishes_handle_values_from_string_pointers() { + let hwnd = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + assert_eq!(handle_alias_kind(&hwnd), Some(HandleAliasKind::HandleValue)); + assert_eq!( + handle_alias_kind(&pwstr_struct()), + Some(HandleAliasKind::StringPointer) + ); + let psid = TypeMeta::Struct { + namespace: "Windows.Win32.Security".into(), + name: "PSID".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + assert_eq!(handle_alias_kind(&psid), Some(HandleAliasKind::DataPointer)); +} + +#[test] +fn hresult_is_not_a_handle() { + let hr = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HRESULT".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::I32, + }], + }; + assert!(handle_type_name(&hr).is_none()); + assert!(is_hresult(&hr)); +} + +#[test] +fn non_win32_struct_is_not_a_handle() { + let rect = TypeMeta::Struct { + namespace: "Windows.Foundation".into(), + name: "Rect".into(), + fields: vec![ + crate::types::FieldMeta { + name: "X".into(), + typ: TypeMeta::F32, + }, + crate::types::FieldMeta { + name: "Y".into(), + typ: TypeMeta::F32, + }, + crate::types::FieldMeta { + name: "Width".into(), + typ: TypeMeta::F32, + }, + crate::types::FieldMeta { + name: "Height".into(), + typ: TypeMeta::F32, + }, + ], + }; + assert!(handle_type_name(&rect).is_none()); +} + +// ---- Fix 2 (BOOL → boolean/i32) ---- + +fn win32_bool_struct() -> TypeMeta { + TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "BOOL".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::I32, + }], + } +} + +#[test] +fn win32_bool_is_not_a_handle() { + let b = win32_bool_struct(); + // Sanity: it's the exact shape of a handle (single Value: I32) — the + // special-case must WIN over the generic handle heuristic. + assert!( + handle_type_name(&b).is_none(), + "BOOL must not be emitted as an opaque handle typedef" + ); +} + +#[test] +fn win32_bool_projects_as_boolean_and_i32() { + let b = win32_bool_struct(); + // .d.ts surface: boolean (not `BOOL` or `bigint | Buffer`) + assert_eq!(ts_type_expr_dts(&b), "boolean"); + // .js registration: i32 type (not pointer) + assert_eq!(ts_type_expr_js(&b), "DynCom.i32Type()"); + // .js argument marshalling: truthy→1, falsy→0 as an i32 (not pointer) + assert_eq!( + wrap_arg_js(&b, "fFullscreen"), + "DynCom.i32(fFullscreen ? 1 : 0)" + ); +} + +#[test] +fn hresult_input_projects_as_number_and_i32_value() { + let hr = make_hresult(); + assert_eq!(ts_type_expr_dts(&hr), "number"); + assert_eq!(ts_type_expr_js(&hr), "DynCom.i32Type()"); + assert_eq!(wrap_arg_js(&hr, "hr"), "DynCom.i32(hr)"); + + let m = MethodMeta { + name: "Close".into(), + vtable_index: 4, + params: vec![ParamMeta { + name: "hr".into(), + typ: hr, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains(".addMethod('Close', new DynComMethodSig().addIn(DynCom.i32Type()))"), + ".js must register HRESULT in-param as i32:\n{}", + js + ); + assert!( + js.contains("DynCom.i32(hr)"), + ".js must pass HRESULT by value as i32:\n{}", + js + ); + assert!( + !js.contains("DynCom.pointer(hr)"), + ".js must not pass HRESULT as a pointer:\n{}", + js + ); + assert!( + dts.contains("close(hr: number): void;"), + ".d.ts must type HRESULT in-param as number:\n{}", + dts + ); + assert!( + !dts.contains("HRESULT"), + ".d.ts must not expose an undefined HRESULT alias:\n{}", + dts + ); +} + +// ---- Fix 3 (REFIID-guarded interop heuristic) ---- + +/// Helper: construct a MethodMeta with HRESULT return type. +fn make_hresult() -> TypeMeta { + TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HRESULT".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::I32, + }], + } +} + +#[test] +fn interop_shape_accepts_riid_named_object_trailing_in() { + // Real Windows.Win32 shape: `HRESULT GetForWindow(HWND appWindow, REFIID riid, out void** ppv)`. + // REFIID typically projects to TypeMeta::Object with name "riid". + let m = MethodMeta { + name: "GetForWindow".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "appWindow".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "ppv".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let natural = method_is_interop_shape(&m) + .expect("REFIID-shaped trailing in-param named `riid` must be recognised as interop"); + // Natural in-params = every in EXCEPT the trailing REFIID. + assert_eq!(natural.len(), 1); + assert_eq!(natural[0].name, "appWindow"); +} + +#[test] +fn interop_shape_rejects_guid_passed_by_value() { + let m = MethodMeta { + name: "GetSomething".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "target".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Guid, + direction: ParamDirection::In, + }, + ParamMeta { + name: "out".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + assert!( + method_is_interop_shape(&m).is_none(), + "a by-value GUID must not be passed as a REFIID pointer" + ); +} + +/// FIX 3 REGRESSION: a method returning HRESULT with an [out] Object and a +/// trailing In-Object whose name is NOT `riid`/`iid` (e.g. a real application +/// COM interface pointer like `original`) must NOT be mis-classified as +/// interop-shape. Otherwise the codegen would silently drop the caller's +/// meaningful argument. +#[test] +fn interop_shape_rejects_non_refiid_trailing_object() { + let m = MethodMeta { + name: "CloneWithOriginal".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "context".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + // NOT `riid`/`iid`, NOT Guid — a real COM pointer in-param. + name: "original".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "cloned".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + assert!( + method_is_interop_shape(&m).is_none(), + "trailing in-param `original` is a real Object argument, NOT a REFIID — \ + it must not be dropped by the interop heuristic" + ); +} + +#[test] +fn interop_shape_rejects_iid_named_non_object_param() { + // A parameter named `riid` but typed as a plain I32 is not a REFIID — + // reject rather than silently drop. + let m = MethodMeta { + name: "Weird".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "hwnd".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ParamMeta { + name: "out".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + assert!( + method_is_interop_shape(&m).is_none(), + "an I32 named `riid` is not a REFIID — must be rejected" + ); +} + +// ---- Fix 1 (winmd-derived interop IID, fail-loud on unresolved) ---- + +/// Build a fully synthetic ComInterfaceMeta for an `IFooInterop`-style +/// interface whose derived projected class name (`Foo`) does NOT exist +/// anywhere reachable. The generator must FAIL LOUDLY rather than emit +/// a NULL riid. +#[test] +fn interop_generation_fails_when_target_iid_unresolvable() { + use crate::com_metadata::{ComInterfaceMeta, InterfaceMeta}; + + let iface = InterfaceMeta { + name: "IThisRuntimeClassDoesNotExist_DynWinrtInterop".into(), + namespace: "Windows.Win32.System.WinRT".into(), + iid: "00000000-0000-0000-0000-000000000000".into(), + methods: vec![MethodMeta { + name: "GetForWindow".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "appWindow".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "ppv".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }], + generic_piid: None, + generic_args: Vec::new(), + doc: None, + deprecated: None, + }; + let com = ComInterfaceMeta { + interface: iface, + base_offset: 3, + is_iunknown_rooted: true, + base_chain: vec!["IUnknown".into()], + coclass_clsid: None, + coclass_name: None, + own_methods_start: 3, + referenced_enums: Vec::new(), + }; + // Pass empty winmd_paths — even with the newest-SDK fallback, the + // synthetic class name won't be found anywhere. + let result = generate_com_interface_files(&com, ""); + assert!( + result.is_err(), + "generator must fail loudly when the projected runtime-class IID \ + cannot be resolved; got Ok(_)" + ); + let err = result.unwrap_err(); + assert!( + err.contains("ThisRuntimeClassDoesNotExist_Dynwinrt") + || err.contains("ThisRuntimeClassDoesNotExist_DynWinrt"), + "error must name the class it failed to resolve: {}", + err + ); + assert!( + !err.is_empty(), + "error message must be non-empty (fail-loud contract)" + ); +} + +#[test] +fn non_interop_iunknown_interface_still_generates_without_winmd_lookup() { + // A vanilla IUnknown-rooted interface with no coclass and no + // interop shape must succeed even when we pass empty winmd paths. + use crate::com_metadata::{ComInterfaceMeta, InterfaceMeta}; + let iface = InterfaceMeta { + name: "IMyPlainClassicCom".into(), + namespace: "Windows.Win32.System.Com".into(), + iid: "11111111-2222-3333-4444-555555555555".into(), + methods: vec![MethodMeta { + name: "DoStuff".into(), + vtable_index: 3, + params: vec![], + return_type: Some(make_hresult()), + ..Default::default() + }], + generic_piid: None, + generic_args: Vec::new(), + doc: None, + deprecated: None, + }; + let com = ComInterfaceMeta { + interface: iface, + base_offset: 3, + is_iunknown_rooted: true, + base_chain: vec!["IUnknown".into()], + coclass_clsid: None, + coclass_name: None, + own_methods_start: 3, + referenced_enums: Vec::new(), + }; + let out = generate_com_interface_files(&com, "") + .expect("plain classic-COM codegen must succeed with no winmds"); + assert!(out.js.contains("DynCom.registerIUnknownInterface")); + assert!(out.js.contains("method(3)")); +} + +// ---- Fix 4 (classic-COM plain `[out]` param → return-value projection) ---- + +fn plain_iface_with_method(m: MethodMeta) -> crate::com_metadata::ComInterfaceMeta { + use crate::com_metadata::{ComInterfaceMeta, InterfaceMeta}; + let iface = InterfaceMeta { + name: "IHasOut".into(), + namespace: "Windows.Win32.System.Com".into(), + iid: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee".into(), + methods: vec![m], + generic_piid: None, + generic_args: Vec::new(), + doc: None, + deprecated: None, + }; + ComInterfaceMeta { + interface: iface, + base_offset: 3, + is_iunknown_rooted: true, + base_chain: vec!["IUnknown".into()], + coclass_clsid: None, + coclass_name: None, + own_methods_start: 3, + referenced_enums: Vec::new(), + } +} + +#[test] +fn unsupported_struct_in_out_fails_closed() { + let method = MethodMeta { + name: "Read".into(), + params: vec![ParamMeta { + name: "value".into(), + typ: TypeMeta::Struct { + namespace: "Windows.Win32.System.Com".into(), + name: "VARIANT".into(), + fields: vec![ + crate::types::FieldMeta { + name: "vt".into(), + typ: TypeMeta::U16, + }, + crate::types::FieldMeta { + name: "data".into(), + typ: TypeMeta::U64, + }, + ], + }, + direction: ParamDirection::InOut, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("unsupported struct in/out must not emit a wrong T** ABI"); + assert!(error.contains("requires native layout projection")); +} + +#[test] +fn unsupported_by_value_struct_fails_closed() { + let method = MethodMeta { + name: "DragEnter".into(), + params: vec![ParamMeta { + name: "point".into(), + typ: TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "POINTL".into(), + fields: vec![ + crate::types::FieldMeta { + name: "x".into(), + typ: TypeMeta::I32, + }, + crate::types::FieldMeta { + name: "y".into(), + typ: TypeMeta::I32, + }, + ], + }, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("struct layout must fail closed"); + assert!(error.contains("requires native layout projection")); +} + +#[test] +fn unsupported_struct_direct_return_fails_closed() { + let method = MethodMeta { + name: "GetPoint".into(), + return_type: Some(TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "POINT".into(), + fields: vec![ + crate::types::FieldMeta { + name: "x".into(), + typ: TypeMeta::I32, + }, + crate::types::FieldMeta { + name: "y".into(), + typ: TypeMeta::I32, + }, + ], + }), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("unsupported struct return must not panic at invocation time"); + assert!(error.contains("unsupported direct native return")); +} + +#[test] +fn plain_method_single_out_scalar_projects_as_return() { + // Model: `HRESULT GetShowCmd([out] int* pcmd)` — the classic single-out + // int shape. The out-int must become the method's return value. + let m = MethodMeta { + name: "GetShowCmd".into(), + vtable_index: 8, + params: vec![ParamMeta { + name: "pcmd".into(), + typ: TypeMeta::I32, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + // .js: must capture `_out` and return it as a JS number. + assert!( + js.contains("const _out = _IHasOut.method(8).invoke(this._obj, [])"), + ".js must capture invoke() result into _out:\n{}", + js + ); + assert!( + js.contains("return DynCom.toNumber(_out);"), + ".js must unwrap the I32 out:\n{}", + js + ); + // .d.ts: return type must be `number`, not `void`. + assert!( + dts.contains("getShowCmd(): number;"), + ".d.ts must project single-out I32 as `number`:\n{}", + dts + ); +} + +#[test] +fn plain_method_single_out_guid_projects_as_string() { + // Model: `HRESULT GetClassID([out] GUID* pClassID)` (IPersist shape). + let m = MethodMeta { + name: "GetClassID".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "pClassID".into(), + typ: TypeMeta::Guid, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains("const _out = _IHasOut.method(3).invoke(this._obj, [])"), + ".js must capture invoke() result into _out:\n{}", + js + ); + assert!( + js.contains("return DynCom.toGuidString(_out);"), + ".js must unwrap GUID out:\n{}", + js + ); + assert!( + dts.contains("getClassID(): string;"), + ".d.ts must project single-out GUID as `string`:\n{}", + dts + ); +} + +#[test] +fn plain_method_single_out_enum_projects_as_underlying() { + // Model: `HRESULT GetKind([out] MyKind* pk)` where MyKind is an I32 + // enum. Underlying-scalar unwrap → `.toNumber()`; .d.ts uses the enum + // type name. + let m = MethodMeta { + name: "GetKind".into(), + vtable_index: 5, + params: vec![ParamMeta { + name: "pk".into(), + typ: TypeMeta::Enum { + namespace: "Windows.Win32.System.Com".into(), + name: "MyKind".into(), + underlying: Box::new(TypeMeta::I32), + members: Vec::new(), + is_flags: false, + doc: None, + deprecated: None, + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains("return DynCom.toNumber(_out);"), + ".js must unwrap enum out via its underlying scalar:\n{}", + js + ); + assert!( + dts.contains("getKind(): MyKind;"), + ".d.ts must project enum out under the enum's declared name:\n{}", + dts + ); +} + +#[test] +fn plain_method_multi_out_uses_invoke_all_and_tuple_return() { + // Model: `HRESULT Q([out] uint32_t* a, [out] BOOL* found)` — two + // trailing out params must flip to `.invokeAll()` and a tuple return. + let m = MethodMeta { + name: "Q".into(), + vtable_index: 6, + params: vec![ + ParamMeta { + name: "a".into(), + typ: TypeMeta::U32, + direction: ParamDirection::Out, + }, + ParamMeta { + name: "found".into(), + typ: TypeMeta::Bool, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains("const _r = _IHasOut.method(6).invokeAll(this._obj, [])"), + ".js multi-out must use .invokeAll():\n{}", + js + ); + assert!( + js.contains("return [DynCom.toU32(_r[0]), DynCom.toBool(_r[1])];"), + ".js multi-out must return a tuple with each out unwrapped:\n{}", + js + ); + assert!( + dts.contains("q(): [number, boolean];"), + ".d.ts multi-out must project a tuple type:\n{}", + dts + ); +} + +#[test] +fn plain_method_zero_out_still_discards_result() { + // No out params: existing behavior — invoke and discard. + let m = MethodMeta { + name: "DoIt".into(), + vtable_index: 4, + params: vec![ParamMeta { + name: "arg".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + !js.contains("const _out ="), + ".js zero-out must not capture invoke() result:\n{}", + js + ); + assert!( + !js.contains("invokeAll"), + ".js zero-out must not use .invokeAll():\n{}", + js + ); + assert!( + js.contains("_IHasOut.method(4).invoke(this._obj,"), + ".js zero-out must call plain .invoke():\n{}", + js + ); + assert!( + dts.contains("doIt(arg: number): void;"), + ".d.ts zero-out must still be `void`:\n{}", + dts + ); +} + +#[test] +fn direct_native_return_uses_return_abi_instead_of_synthetic_out_param() { + let method = MethodMeta { + name: "RetryRejectedCall".into(), + vtable_index: 5, + return_type: Some(TypeMeta::U32), + ..Default::default() + }; + let signature = build_method_sig_js(&method); + assert_eq!(signature, "new DynComMethodSig().returns(DynCom.u32Type())"); + + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!(js.contains("const _out = _IHasOut.method(5).invoke(this._obj, [])")); + assert!(js.contains("return DynCom.toU32(_out);")); + assert!(dts.contains("retryRejectedCall(): number;")); +} + +#[test] +fn native_void_return_is_declared_explicitly() { + let method = MethodMeta { + name: "OnClose".into(), + vtable_index: 8, + return_type: None, + ..Default::default() + }; + assert_eq!( + build_method_sig_js(&method), + "new DynComMethodSig().returnsVoid()" + ); + + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + assert!(js.contains("_IHasOut.method(8).invoke(this._obj, [])")); + assert!(!js.contains("const _out =")); +} + +#[test] +fn direct_64_bit_returns_use_bigint_accessors() { + let i64_method = MethodMeta { + name: "GetSigned".into(), + return_type: Some(TypeMeta::I64), + ..Default::default() + }; + let u64_method = MethodMeta { + name: "GetUnsigned".into(), + return_type: Some(TypeMeta::U64), + ..Default::default() + }; + + let i64_js = render_js(&plain_iface_with_method(i64_method), None); + let u64_js = render_js(&plain_iface_with_method(u64_method), None); + assert!(i64_js.contains("return DynCom.toI64Bigint(_out);")); + assert!(u64_js.contains("return DynCom.toU64Bigint(_out);")); +} + +#[test] +fn return_only_handle_declares_its_alias() { + let hwnd = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + let method = MethodMeta { + name: "GetWindow".into(), + return_type: Some(hwnd), + ..Default::default() + }; + let com = plain_iface_with_method(method); + let dts = render_dts(&com, None); + assert!(dts.contains("export type HWND = bigint | number;")); + assert!(dts.contains("getWindow(): HWND;")); +} + +#[test] +fn handle_value_arg_accepts_buffer_and_string_pointer_keeps_buffer() { + let hwnd = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + let method = MethodMeta { + name: "SetOverlayIcon".into(), + params: vec![ + ParamMeta { + name: "hwnd".into(), + typ: hwnd, + direction: ParamDirection::In, + }, + ParamMeta { + name: "description".into(), + typ: pwstr_struct(), + direction: ParamDirection::In, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let iface = plain_iface_with_method(method); + let dts = render_dts(&iface, None); + let js = render_js(&iface, None); + + // HWND inputs accept Electron's pointer-width Buffer, but the HWND + // output alias remains a numeric handle value. + assert!(dts.contains("export type HWND = bigint | number;")); + assert!(dts.contains("export type PWSTR = bigint | Buffer;")); + assert!(dts.contains("Pass a `Buffer` holding the string bytes")); + assert!( + dts.contains("setOverlayIcon(hwnd: HWND | Buffer | Uint8Array, description: PWSTR): void;") + ); + + // Handle-value conversion is centralized in the runtime; string + // pointers continue to pass their backing-store address. + assert!( + js.contains("DynCom.pointer(DynCom.handleValue(hwnd))"), + "HWND arg must use DynCom.handleValue:\n{js}" + ); + assert!(!js.contains("function _handleArg(")); + assert!(!js.contains("handleValue(description)")); +} + +#[test] +fn data_pointer_alias_does_not_read_buffer_contents_as_a_handle() { + let psid = TypeMeta::Struct { + namespace: "Windows.Win32.Security".into(), + name: "PSID".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + let method = MethodMeta { + name: "AddUserSid".into(), + params: vec![ParamMeta { + name: "userSid".into(), + typ: psid, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let iface = plain_iface_with_method(method); + let js = render_js(&iface, None); + let dts = render_dts(&iface, None); + + assert!(dts.contains("export type PSID = bigint | number;")); + assert!(dts.contains("addUserSid(userSid: PSID | Buffer | Uint8Array): void;")); + assert!(js.contains("DynCom.pointer(userSid)")); + assert!(!js.contains("handleValue(userSid)")); +} + +#[test] +fn hwnd_in_out_uses_runtime_handle_conversion_without_inline_helper() { + let hwnd = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + let method = MethodMeta { + name: "Create".into(), + params: vec![ParamMeta { + name: "window".into(), + typ: hwnd, + direction: ParamDirection::InOut, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let iface = plain_iface_with_method(method); + let js = render_js(&iface, None); + + assert!(js.contains("DynCom.pointer(DynCom.handleValue(window))")); + assert!(!js.contains("function _handleArg(")); +} + +#[test] +fn return_only_enum_emits_import_and_sibling_files() { + let kind = TypeMeta::Enum { + namespace: "Windows.Win32.Example".into(), + name: "THING_KIND".into(), + underlying: Box::new(TypeMeta::I32), + members: Vec::new(), + is_flags: false, + doc: None, + deprecated: None, + }; + let method = MethodMeta { + name: "GetKind".into(), + return_type: Some(kind.clone()), + ..Default::default() + }; + let mut com = plain_iface_with_method(method); + com.referenced_enums.push(ComEnumMeta { + namespace: "Windows.Win32.Example".into(), + name: "THING_KIND".into(), + underlying: TypeMeta::I32, + members: Vec::new(), + is_flags: false, + }); + + let output = generate_com_interface_files(&com, "").unwrap(); + assert!( + output + .dts + .contains("import { THING_KIND } from './THING_KIND.js';") + ); + assert!( + output + .extra_files + .iter() + .any(|(name, _)| name == "THING_KIND.d.ts") + ); +} + +#[test] +fn unsigned_enum_literals_preserve_u32_and_u64_values() { + let u32_enum = ComEnumMeta { + namespace: "Windows.Win32.Example".into(), + name: "U32_FLAGS".into(), + underlying: TypeMeta::U32, + members: vec![crate::com_metadata::ComEnumMember { + name: "HIGH_BIT".into(), + value: ComEnumValue::Unsigned(2_147_483_648), + }], + is_flags: true, + }; + let u64_enum = ComEnumMeta { + namespace: "Windows.Win32.Example".into(), + name: "U64_FLAGS".into(), + underlying: TypeMeta::U64, + members: vec![crate::com_metadata::ComEnumMember { + name: "HIGH_BIT".into(), + value: ComEnumValue::Unsigned(9_223_372_036_854_775_808), + }], + is_flags: true, + }; + + let (u32_js, u32_dts) = render_enum_files(&u32_enum); + assert!(u32_js.contains("HIGH_BIT: 2147483648")); + assert!(u32_dts.contains("readonly HIGH_BIT: 2147483648;")); + let (u64_js, u64_dts) = render_enum_files(&u64_enum); + assert!(u64_js.contains("HIGH_BIT: 9223372036854775808n")); + assert!(u64_dts.contains("readonly HIGH_BIT: 9223372036854775808n;")); +} + +#[test] +fn in_out_parameter_is_both_argument_and_result() { + let method = MethodMeta { + name: "Adjust".into(), + vtable_index: 4, + params: vec![ParamMeta { + name: "value".into(), + typ: TypeMeta::I32, + direction: ParamDirection::InOut, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + assert_eq!( + build_method_sig_js(&method), + "new DynComMethodSig().addInOut(DynCom.i32Type())" + ); + + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!(js.contains("adjust(value)")); + assert!(js.contains("const _out = _IHasOut.method(4).invoke(this._obj, [DynCom.i32(value)])")); + assert!(js.contains("return DynCom.toNumber(_out);")); + assert!(dts.contains("adjust(value: number): number;")); +} + +#[test] +fn unsupported_outfill_fails_closed() { + let m = MethodMeta { + name: "GetPath".into(), + vtable_index: 2, + params: vec![ + ParamMeta { + name: "pszFile".into(), + typ: TypeMeta::String, // PWSTR buffer, caller-allocated + direction: ParamDirection::OutFill, + }, + ParamMeta { + name: "cch".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let error = generate_com_interface_files(&com, "") + .expect_err("unsupported caller-allocated arrays must fail closed"); + assert!(error.contains("caller-allocated array outputs are not supported")); +} + +fn pwstr_struct() -> TypeMeta { + TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "PWSTR".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + } +} + +#[test] +fn out_string_buffer_allocates_decodes_and_returns_string() { + let m = MethodMeta { + name: "GetDescription".into(), + vtable_index: 6, + params: vec![ + ParamMeta { + name: "pszName".into(), + typ: pwstr_struct(), + direction: ParamDirection::OutStringBuffer { + count_param_index: 1, + }, + }, + ParamMeta { + name: "cch".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains("function _normalizeStringBufferCount"), + ".js must emit string buffer validation helper:\n{}", + js + ); + assert!( + js.contains(".addMethod('GetDescription', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type()))"), + ".js must register string buffer as an input pointer:\n{}", + js + ); + assert!( + js.contains("getDescription(cch = 260)") && js.contains("Buffer.alloc(cch * 2)"), + ".js must default cch and allocate a UTF-16 buffer:\n{}", + js + ); + assert!( + js.contains("const _text = _decodeWideString(_buffer);") && js.contains("return _text;"), + ".js must return the decoded wide string:\n{}", + js + ); + assert!( + dts.contains("getDescription(cch?: number): string;"), + ".d.ts must expose optional count and string return:\n{}", + dts + ); +} + +#[test] +fn callee_allocated_pwstr_is_decoded_and_freed() { + let method = MethodMeta { + name: "GetDisplayName".into(), + vtable_index: 5, + params: vec![ParamMeta { + name: "name".into(), + typ: pwstr_struct(), + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 0, + free_with: "CoTaskMemFree".into(), + }], + ..Default::default() + }; + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + + assert!(js.contains("return DynCom.takeCoTaskMemWideString(_out);")); + assert!(dts.contains("getDisplayName(): string;")); +} + +#[test] +fn string_pointer_output_without_allocator_fails_closed() { + let method = MethodMeta { + name: "GetDisplayName".into(), + params: vec![ParamMeta { + name: "name".into(), + typ: pwstr_struct(), + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("string pointer outputs require an allocator contract"); + + assert!(error.contains("string pointer output")); + assert!(error.contains("no ownership projection")); +} + +#[test] +fn unknown_output_cleanup_contract_fails_closed() { + let method = MethodMeta { + name: "GetData".into(), + params: vec![ParamMeta { + name: "data".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 0, + free_with: "LocalFree".into(), + }], + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("unknown allocators must fail before rendering"); + + assert!(error.contains("unsupported output cleanup contract")); + assert!(error.contains("LocalFree")); +} + +#[test] +fn allocator_name_must_match_exactly() { + let method = MethodMeta { + name: "GetData".into(), + params: vec![ParamMeta { + name: "data".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 0, + free_with: "CoTaskMemFreeEx".into(), + }], + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("allocator prefixes must not be accepted"); + + assert!(error.contains("unsupported output cleanup contract")); + assert!(error.contains("CoTaskMemFreeEx")); +} + +#[test] +fn multiple_string_buffers_fail_closed_before_rendering() { + let method = MethodMeta { + name: "GetNames".into(), + params: vec![ + ParamMeta { + name: "first".into(), + typ: pwstr_struct(), + direction: ParamDirection::OutStringBuffer { + count_param_index: 1, + }, + }, + ParamMeta { + name: "firstCount".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ParamMeta { + name: "second".into(), + typ: pwstr_struct(), + direction: ParamDirection::OutStringBuffer { + count_param_index: 3, + }, + }, + ParamMeta { + name: "secondCount".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("only one validated string buffer can be rendered"); + + assert!(error.contains("multiple caller-owned string buffers")); +} + +#[test] +fn semantic_hresult_dynamic_iid_fails_closed() { + let method = MethodMeta { + name: "GetThing".into(), + params: vec![ + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "result".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + preserve_hresult: true, + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("dynamic IID rendering cannot discard semantic HRESULT"); + + assert!(error.contains("semantic HRESULT dynamic-IID methods are not supported")); +} + +#[test] +fn managed_interface_input_imports_the_bridge_type() { + let method = MethodMeta { + name: "SetThing".into(), + params: vec![ParamMeta { + name: "thing".into(), + typ: TypeMeta::Interface { + namespace: "Tests".into(), + name: "IThing".into(), + iid: "11111111-2222-3333-4444-555555555555".into(), + }, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); + + assert!(output.dts.contains("import type { DynWinRtValue }")); + assert!(output.dts.contains("setThing(thing: DynWinRtValue): void;")); +} + +#[test] +fn untyped_sysfree_output_fails_closed() { + let method = MethodMeta { + name: "GetAllFileTypes".into(), + vtable_index: 4, + params: vec![ParamMeta { + name: "types".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 0, + free_with: "SysFreeString".into(), + }], + ..Default::default() + }; + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("BSTR**-style untyped outputs must fail closed"); + assert!(error.contains("SysFreeString ownership requires a scalar Out BSTR")); +} + +#[test] +fn string_buffer_preserves_additional_outputs() { + let method = MethodMeta { + name: "GetIconLocation".into(), + vtable_index: 16, + params: vec![ + ParamMeta { + name: "path".into(), + typ: pwstr_struct(), + direction: ParamDirection::OutStringBuffer { + count_param_index: 1, + }, + }, + ParamMeta { + name: "cch".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ParamMeta { + name: "icon".into(), + typ: TypeMeta::I32, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + + assert!(js.contains("const _out = _IHasOut.method(16).invoke")); + assert!(js.contains("return [_text, DynCom.toNumber(_out)];")); + assert!(dts.contains("getIconLocation(cch?: number): [string, number];")); +} + +#[test] +fn interface_out_param_projects_as_explicit_bridge_value() { + let m = MethodMeta { + name: "GetThing".into(), + vtable_index: 7, + params: vec![ParamMeta { + name: "thing".into(), + typ: TypeMeta::Interface { + namespace: "Windows.Win32.System.Com".into(), + name: "IThing".into(), + iid: "11111111-2222-3333-4444-555555555555".into(), + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + !js.contains("from './IThing.js'"), + ".js must not depend on an ungenerated wrapper:\n{}", + js + ); + assert!(js.contains( + ".addOut(DynCom.interfaceType(WinGuid.parse('11111111-2222-3333-4444-555555555555')))" + )); + assert!( + js.contains("return _out;"), + ".js must return the managed bridge value:\n{}", + js + ); + assert!( + dts.contains("import type { DynWinRtValue }"), + ".d.ts must import the bridge type:\n{}", + dts + ); + assert!( + dts.contains("getThing(): DynWinRtValue;"), + ".d.ts must return the explicit bridge value:\n{}", + dts + ); +} + +#[test] +fn caller_supplied_riid_output_is_adopted() { + let method = MethodMeta { + name: "BindToHandler".into(), + vtable_index: 4, + params: vec![ + ParamMeta { + name: "pbc".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "ppv".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(method); + let output = generate_com_interface_files(&com, "").unwrap(); + + assert!(output.js.contains("bindToHandler(pbc, iid)")); + assert!(output.js.contains("DynCom.adoptComPointer(_raw, _iid)")); + assert!( + output + .dts + .contains("bindToHandler(pbc: bigint | Buffer, iid: string): DynWinRtValue;") + ); +} + +#[test] +fn hstring_output_uses_owned_hstring_projection() { + let method = MethodMeta { + name: "get_CorrelationVector".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "cv".into(), + typ: TypeMeta::String, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); + + assert!(output.js.contains(".addOut(DynCom.hstringType())")); + assert!(output.js.contains("return _out.toString();")); + assert!(output.dts.contains("get_CorrelationVector(): string;")); +} + +#[test] +fn unresolved_interface_iid_fails_closed() { + let method = MethodMeta { + name: "CreateSurface".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::Interface { + namespace: "Windows.UI.Composition".into(), + name: "ICompositionSurface".into(), + iid: String::new(), + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("an unresolved interface must not degrade to a raw pointer"); + + assert!(error.contains("ICompositionSurface")); + assert!(error.contains("no resolvable IID")); + assert!(error.contains("--ref")); +} + +#[test] +fn parameterized_interface_fails_closed_even_with_a_piid() { + let method = MethodMeta { + name: "GetItems".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IVectorView`1".into(), + piid: "bbe1fa4c-b0e3-4583-baef-1f1b2e483e56".into(), + args: vec![TypeMeta::String], + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("a PIID alone is not a closed interface IID"); + + assert!(error.contains("computed closed IID")); + assert!(error.contains("raw-pointer fallback is not allowed")); +} + +#[test] +fn async_interface_fails_closed_without_a_closed_iid() { + let method = MethodMeta { + name: "OpenAsync".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::AsyncOperation(Box::new(TypeMeta::String)), + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("async interfaces must not degrade to raw pointers"); + + assert!(error.contains("async interface requires a computed closed IID")); + assert!(error.contains("raw-pointer fallback is not allowed")); +} + +#[test] +fn native_array_fails_closed_without_count_and_ownership() { + let method = MethodMeta { + name: "GetItems".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::Array(Box::new(TypeMeta::Interface { + namespace: "Contoso".into(), + name: "IItem".into(), + iid: "11111111-2222-3333-4444-555555555555".into(), + })), + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("native arrays must not degrade to raw pointers"); + + assert!(error.contains("explicit count and element-ownership projection")); + assert!(error.contains("raw-pointer fallback is not allowed")); +} + +#[test] +fn delegate_fails_closed_without_a_callback_projection() { + let method = MethodMeta { + name: "SetHandler".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "handler".into(), + typ: TypeMeta::Delegate { + namespace: "Contoso".into(), + name: "Handler".into(), + iid: "11111111-2222-3333-4444-555555555555".into(), + }, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("delegates require an explicit managed projection"); + + assert!(error.contains("managed callback projection")); + assert!(error.contains("raw-pointer fallback is not allowed")); +} + +#[test] +fn runtime_class_uses_its_resolved_default_interface() { + let method = MethodMeta { + name: "CreateDevice".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::RuntimeClass { + namespace: "Windows.UI.Composition".into(), + name: "CompositionGraphicsDevice".into(), + default_interface: Some(Box::new(TypeMeta::Interface { + namespace: "Windows.UI.Composition".into(), + name: "ICompositionGraphicsDevice".into(), + iid: "a329b321-0d69-4b89-9951-28de94dc998d".into(), + })), + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); + + assert!(output.js.contains( + ".addOut(DynCom.interfaceType(WinGuid.parse('a329b321-0d69-4b89-9951-28de94dc998d')))" + )); + assert!(output.js.contains("return _out;")); + assert!(output.dts.contains("createDevice(): DynWinRtValue;")); +} + +#[test] +fn runtime_class_without_a_default_interface_fails_closed() { + let method = MethodMeta { + name: "CreateDevice".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::RuntimeClass { + namespace: "Windows.UI.Composition".into(), + name: "CompositionGraphicsDevice".into(), + default_interface: None, + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("runtime classes require a resolved default interface"); + + assert!(error.contains("no resolvable default interface")); + assert!(error.contains("--ref")); +} + +#[test] +fn semantic_hresult_is_preserved_as_a_number() { + let method = MethodMeta { + name: "IsDirty".into(), + vtable_index: 4, + return_type: Some(make_hresult()), + preserve_hresult: true, + ..Default::default() + }; + + let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); + + assert!(output.js.contains(".preserveHresult()")); + assert!(output.js.contains("return DynCom.toNumber(_out);")); + assert!(output.dts.contains("isDirty(): number;")); +} + +#[test] +fn ordinary_hresult_remains_throw_or_void() { + let method = MethodMeta { + name: "Load".into(), + vtable_index: 5, + return_type: Some(make_hresult()), + ..Default::default() + }; + + let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); + + assert!(!output.js.contains(".preserveHresult()")); + assert!(output.dts.contains("load(): void;")); +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs new file mode 100644 index 00000000..a3f6c2a8 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::super::ir::{ + ComEnumUnderlying, ComPrimitive, ComScalarRepr, ComType, PointerAliasKind, ProjectedComResult, + ResultConversion, StringEncoding, +}; + +pub(super) fn abi_type_js(typ: &ComType) -> String { + match typ { + ComType::Primitive(primitive) => match primitive { + ComPrimitive::Bool => "DynCom.boolType()", + ComPrimitive::I8 => "DynCom.i8Type()", + ComPrimitive::U8 => "DynCom.u8Type()", + ComPrimitive::I16 => "DynCom.i16Type()", + ComPrimitive::U16 => "DynCom.u16Type()", + ComPrimitive::I32 => "DynCom.i32Type()", + ComPrimitive::U32 => "DynCom.u32Type()", + ComPrimitive::I64 => "DynCom.i64Type()", + ComPrimitive::U64 => "DynCom.u64Type()", + ComPrimitive::F32 => "DynCom.f32Type()", + ComPrimitive::F64 => "DynCom.f64Type()", + ComPrimitive::Char16 => "DynCom.char16Type()", + } + .into(), + ComType::NativeIsize => "DynCom.isizeType()".into(), + ComType::NativeUsize => "DynCom.usizeType()".into(), + ComType::Win32Bool | ComType::HResult => "DynCom.i32Type()".into(), + ComType::Guid => "DynCom.guidType()".into(), + ComType::HString => "DynCom.hstringType()".into(), + ComType::Enum { underlying, .. } => enum_abi_type_js(*underlying).into(), + ComType::ScalarAlias { underlying, .. } => scalar_abi_type_js(*underlying).into(), + ComType::RawPointer | ComType::PointerAlias { .. } | ComType::Bstr => { + "DynCom.pointerType()".into() + } + ComType::ManagedInterface { iid } => { + format!("DynCom.interfaceType(WinGuid.parse('{iid}'))") + } + } +} + +pub(super) fn input_type_dts(typ: &ComType) -> String { + match typ { + ComType::PointerAlias { + name, + kind: PointerAliasKind::HandleValue, + } if name == "HWND" => format!("{name} | Buffer | Uint8Array"), + ComType::PointerAlias { + name, + kind: PointerAliasKind::DataPointer, + } => format!("{name} | Buffer | Uint8Array"), + _ => type_dts(typ), + } +} + +pub(super) fn type_dts(typ: &ComType) -> String { + match typ { + ComType::Primitive(primitive) => match primitive { + ComPrimitive::Bool => "boolean", + ComPrimitive::I8 + | ComPrimitive::U8 + | ComPrimitive::I16 + | ComPrimitive::U16 + | ComPrimitive::I32 + | ComPrimitive::U32 + | ComPrimitive::F32 + | ComPrimitive::F64 + | ComPrimitive::Char16 => "number", + ComPrimitive::I64 | ComPrimitive::U64 => "bigint", + } + .into(), + ComType::NativeIsize | ComType::NativeUsize => "bigint".into(), + ComType::Win32Bool => "boolean".into(), + ComType::HResult => "number".into(), + ComType::Guid => "string".into(), + ComType::HString => "string".into(), + ComType::Enum { name, .. } => name.clone(), + ComType::ScalarAlias { name, .. } => name.clone(), + ComType::RawPointer => "bigint | Buffer".into(), + ComType::PointerAlias { name, .. } => name.clone(), + ComType::Bstr => "BSTR".into(), + ComType::ManagedInterface { .. } => "DynWinRtValue".into(), + } +} + +pub(super) fn result_type_dts(result: &ProjectedComResult) -> String { + match result.conversion { + ResultConversion::Bstr | ResultConversion::CoTaskMemString(_) => "string".into(), + ResultConversion::CoTaskMemData + | ResultConversion::ManagedCom + | ResultConversion::DynamicIidAdoption => "DynWinRtValue".into(), + ResultConversion::Value | ResultConversion::HString => type_dts(&result.typ), + } +} + +pub(super) fn wrap_arg_js(typ: &ComType, variable: &str) -> String { + match typ { + ComType::Primitive(primitive) => match primitive { + ComPrimitive::Bool => format!("DynCom.boolValue({variable})"), + ComPrimitive::I8 => format!("DynCom.i8Value({variable})"), + ComPrimitive::U8 => format!("DynCom.u8Value({variable})"), + ComPrimitive::I16 => format!("DynCom.i16({variable})"), + ComPrimitive::U16 => format!("DynCom.u16({variable})"), + ComPrimitive::I32 => format!("DynCom.i32({variable})"), + ComPrimitive::U32 => format!("DynCom.u32({variable})"), + ComPrimitive::I64 => format!("DynCom.i64(BigInt({variable}))"), + ComPrimitive::U64 => format!("DynCom.u64(BigInt({variable}))"), + ComPrimitive::F32 => format!("DynCom.f32({variable})"), + ComPrimitive::F64 => format!("DynCom.f64({variable})"), + ComPrimitive::Char16 => format!("DynCom.char16({variable})"), + }, + ComType::NativeIsize => format!("DynCom.isize(BigInt({variable}))"), + ComType::NativeUsize => format!("DynCom.usize(BigInt({variable}))"), + ComType::Win32Bool => format!("DynCom.i32({variable} ? 1 : 0)"), + ComType::HResult => format!("DynCom.i32({variable})"), + ComType::Guid => format!("DynCom.guid(WinGuid.parse({variable}))"), + ComType::HString => format!("DynCom.hstring({variable})"), + ComType::Enum { underlying, .. } => wrap_enum_arg_js(*underlying, variable), + ComType::ScalarAlias { underlying, .. } => wrap_scalar_arg_js(*underlying, variable), + ComType::RawPointer | ComType::Bstr => format!("DynCom.pointer({variable})"), + ComType::PointerAlias { + name, + kind: PointerAliasKind::HandleValue, + } if name == "HWND" => { + format!("DynCom.pointer(DynCom.handleValue({variable}))") + } + ComType::PointerAlias { .. } => format!("DynCom.pointer({variable})"), + ComType::ManagedInterface { .. } => variable.to_string(), + } +} + +pub(super) fn unwrap_result_js(result: &ProjectedComResult, expression: &str) -> String { + match result.conversion { + ResultConversion::Bstr => format!("DynCom.takeBstr({expression})"), + ResultConversion::CoTaskMemString(StringEncoding::Wide) => { + format!("DynCom.takeCoTaskMemWideString({expression})") + } + ResultConversion::CoTaskMemString(StringEncoding::Ansi) => { + format!("DynCom.takeCoTaskMemAnsiString({expression})") + } + ResultConversion::CoTaskMemData => { + format!("DynCom.adoptCoTaskMemPointer({expression})") + } + ResultConversion::ManagedCom | ResultConversion::DynamicIidAdoption => { + expression.to_string() + } + ResultConversion::HString => format!("{expression}.toString()"), + ResultConversion::Value => unwrap_value_js(&result.typ, expression), + } +} + +fn unwrap_value_js(typ: &ComType, expression: &str) -> String { + match typ { + ComType::Primitive(primitive) => match primitive { + ComPrimitive::Bool => format!("DynCom.toBool({expression})"), + ComPrimitive::I8 + | ComPrimitive::U8 + | ComPrimitive::I16 + | ComPrimitive::U16 + | ComPrimitive::I32 + | ComPrimitive::Char16 => format!("DynCom.toNumber({expression})"), + ComPrimitive::U32 => format!("DynCom.toU32({expression})"), + ComPrimitive::I64 => format!("DynCom.toI64Bigint({expression})"), + ComPrimitive::U64 => format!("DynCom.toU64Bigint({expression})"), + ComPrimitive::F32 | ComPrimitive::F64 => { + format!("DynCom.toF64({expression})") + } + }, + ComType::NativeIsize => format!("DynCom.toIsizeBigint({expression})"), + ComType::NativeUsize => format!("DynCom.toUsizeBigint({expression})"), + ComType::Win32Bool => format!("(DynCom.toNumber({expression}) !== 0)"), + ComType::HResult => format!("DynCom.toNumber({expression})"), + ComType::Guid => format!("DynCom.toGuidString({expression})"), + ComType::HString => format!("{expression}.toString()"), + ComType::Enum { underlying, .. } => unwrap_enum_js(*underlying, expression), + ComType::ScalarAlias { underlying, .. } => unwrap_scalar_js(*underlying, expression), + ComType::RawPointer | ComType::PointerAlias { .. } | ComType::Bstr => { + format!("DynCom.asPointerBigint({expression})") + } + ComType::ManagedInterface { .. } => expression.to_string(), + } +} + +fn enum_abi_type_js(underlying: ComEnumUnderlying) -> &'static str { + match underlying { + ComEnumUnderlying::I8 => "DynCom.i8Type()", + ComEnumUnderlying::U8 => "DynCom.u8Type()", + ComEnumUnderlying::I16 => "DynCom.i16Type()", + ComEnumUnderlying::U16 => "DynCom.u16Type()", + ComEnumUnderlying::I32 => "DynCom.i32Type()", + ComEnumUnderlying::U32 => "DynCom.u32Type()", + ComEnumUnderlying::I64 => "DynCom.i64Type()", + ComEnumUnderlying::U64 => "DynCom.u64Type()", + } +} + +pub(super) fn scalar_type_dts(underlying: ComScalarRepr) -> &'static str { + match underlying { + ComScalarRepr::Primitive(ComPrimitive::Bool) => "boolean", + ComScalarRepr::Primitive(ComPrimitive::I64 | ComPrimitive::U64) + | ComScalarRepr::NativeIsize + | ComScalarRepr::NativeUsize => "bigint", + ComScalarRepr::Primitive(_) => "number", + } +} + +fn scalar_abi_type_js(underlying: ComScalarRepr) -> &'static str { + match underlying { + ComScalarRepr::Primitive(primitive) => match primitive { + ComPrimitive::Bool => "DynCom.boolType()", + ComPrimitive::I8 => "DynCom.i8Type()", + ComPrimitive::U8 => "DynCom.u8Type()", + ComPrimitive::I16 => "DynCom.i16Type()", + ComPrimitive::U16 => "DynCom.u16Type()", + ComPrimitive::I32 => "DynCom.i32Type()", + ComPrimitive::U32 => "DynCom.u32Type()", + ComPrimitive::I64 => "DynCom.i64Type()", + ComPrimitive::U64 => "DynCom.u64Type()", + ComPrimitive::F32 => "DynCom.f32Type()", + ComPrimitive::F64 => "DynCom.f64Type()", + ComPrimitive::Char16 => "DynCom.char16Type()", + }, + ComScalarRepr::NativeIsize => "DynCom.isizeType()", + ComScalarRepr::NativeUsize => "DynCom.usizeType()", + } +} + +fn wrap_scalar_arg_js(underlying: ComScalarRepr, variable: &str) -> String { + match underlying { + ComScalarRepr::Primitive(primitive) => match primitive { + ComPrimitive::Bool => format!("DynCom.boolValue({variable})"), + ComPrimitive::I8 => format!("DynCom.i8Value({variable})"), + ComPrimitive::U8 => format!("DynCom.u8Value({variable})"), + ComPrimitive::I16 => format!("DynCom.i16({variable})"), + ComPrimitive::U16 => format!("DynCom.u16({variable})"), + ComPrimitive::I32 => format!("DynCom.i32({variable})"), + ComPrimitive::U32 => format!("DynCom.u32({variable})"), + ComPrimitive::I64 => format!("DynCom.i64(BigInt({variable}))"), + ComPrimitive::U64 => format!("DynCom.u64(BigInt({variable}))"), + ComPrimitive::F32 => format!("DynCom.f32({variable})"), + ComPrimitive::F64 => format!("DynCom.f64({variable})"), + ComPrimitive::Char16 => format!("DynCom.char16({variable})"), + }, + ComScalarRepr::NativeIsize => format!("DynCom.isize(BigInt({variable}))"), + ComScalarRepr::NativeUsize => format!("DynCom.usize(BigInt({variable}))"), + } +} + +fn unwrap_scalar_js(underlying: ComScalarRepr, expression: &str) -> String { + match underlying { + ComScalarRepr::Primitive(primitive) => match primitive { + ComPrimitive::Bool => format!("DynCom.toBool({expression})"), + ComPrimitive::I8 + | ComPrimitive::U8 + | ComPrimitive::I16 + | ComPrimitive::U16 + | ComPrimitive::I32 + | ComPrimitive::Char16 => format!("DynCom.toNumber({expression})"), + ComPrimitive::U32 => format!("DynCom.toU32({expression})"), + ComPrimitive::I64 => format!("DynCom.toI64Bigint({expression})"), + ComPrimitive::U64 => format!("DynCom.toU64Bigint({expression})"), + ComPrimitive::F32 | ComPrimitive::F64 => { + format!("DynCom.toF64({expression})") + } + }, + ComScalarRepr::NativeIsize => format!("DynCom.toIsizeBigint({expression})"), + ComScalarRepr::NativeUsize => format!("DynCom.toUsizeBigint({expression})"), + } +} + +fn wrap_enum_arg_js(underlying: ComEnumUnderlying, variable: &str) -> String { + match underlying { + ComEnumUnderlying::I8 => format!("DynCom.i8Value({variable})"), + ComEnumUnderlying::U8 => format!("DynCom.u8Value({variable})"), + ComEnumUnderlying::I16 => format!("DynCom.i16({variable})"), + ComEnumUnderlying::U16 => format!("DynCom.u16({variable})"), + ComEnumUnderlying::I32 => format!("DynCom.i32({variable})"), + ComEnumUnderlying::U32 => format!("DynCom.u32({variable})"), + ComEnumUnderlying::I64 => format!("DynCom.i64(BigInt({variable}))"), + ComEnumUnderlying::U64 => format!("DynCom.u64(BigInt({variable}))"), + } +} + +fn unwrap_enum_js(underlying: ComEnumUnderlying, expression: &str) -> String { + match underlying { + ComEnumUnderlying::I8 + | ComEnumUnderlying::U8 + | ComEnumUnderlying::I16 + | ComEnumUnderlying::U16 + | ComEnumUnderlying::I32 => format!("DynCom.toNumber({expression})"), + ComEnumUnderlying::U32 => format!("DynCom.toU32({expression})"), + ComEnumUnderlying::I64 => format!("DynCom.toI64Bigint({expression})"), + ComEnumUnderlying::U64 => format!("DynCom.toU64Bigint({expression})"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mappings_cover_every_supported_com_type() { + let types = vec![ + ComType::Primitive(ComPrimitive::Bool), + ComType::Primitive(ComPrimitive::I8), + ComType::Primitive(ComPrimitive::U8), + ComType::Primitive(ComPrimitive::I16), + ComType::Primitive(ComPrimitive::U16), + ComType::Primitive(ComPrimitive::I32), + ComType::Primitive(ComPrimitive::U32), + ComType::Primitive(ComPrimitive::I64), + ComType::Primitive(ComPrimitive::U64), + ComType::Primitive(ComPrimitive::F32), + ComType::Primitive(ComPrimitive::F64), + ComType::Primitive(ComPrimitive::Char16), + ComType::NativeIsize, + ComType::NativeUsize, + ComType::Win32Bool, + ComType::HResult, + ComType::Guid, + ComType::HString, + ComType::Enum { + name: "E".into(), + underlying: ComEnumUnderlying::U32, + }, + ComType::ScalarAlias { + name: "COLORREF".into(), + underlying: ComScalarRepr::Primitive(ComPrimitive::U32), + }, + ComType::RawPointer, + ComType::PointerAlias { + name: "HWND".into(), + kind: PointerAliasKind::HandleValue, + }, + ComType::Bstr, + ComType::ManagedInterface { + iid: "00000000-0000-0000-0000-000000000000".into(), + }, + ]; + for typ in types { + assert!(!abi_type_js(&typ).is_empty()); + assert!(!type_dts(&typ).is_empty()); + assert!(!wrap_arg_js(&typ, "value").is_empty()); + } + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/mod.rs b/tools/dynwinrt-codegen/src/codegen/com/mod.rs index 920ee7c2..67d0f914 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/mod.rs @@ -3,9 +3,18 @@ //! Classic-COM metadata projection and JavaScript generation. -mod naming; -mod projection; -mod render; -mod type_mapping; +mod ir; +mod javascript; +mod project; -pub use render::{ComGeneratedOutput, generate_com_interface_files}; +use crate::com_metadata::ComInterfaceMeta; + +pub use javascript::render::ComGeneratedOutput; + +pub fn generate_com_interface_files( + meta: &ComInterfaceMeta, + winmd_paths: &str, +) -> Result { + let projected = project::project_com_interface(meta, winmd_paths)?; + Ok(javascript::render::render_com_interface(&projected)) +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/project/interop.rs b/tools/dynwinrt-codegen/src/codegen/com/project/interop.rs new file mode 100644 index 00000000..5a87fc9a --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/project/interop.rs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub(super) fn resolve_projected_default_iid( + winmd_paths: &str, + simple_class_name: &str, +) -> Option<(String, String, String)> { + if !winmd_paths.is_empty() { + if let Some(result) = + crate::com_metadata::find_runtime_class_default_iid(winmd_paths, simple_class_name) + { + return Some(result); + } + } + let sdk_winmd = crate::com_metadata::discover_newest_windows_winmd()?; + if winmd_paths + .split(';') + .any(|path| path.eq_ignore_ascii_case(&sdk_winmd)) + { + return None; + } + crate::com_metadata::find_runtime_class_default_iid(&sdk_winmd, simple_class_name) +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/project/mod.rs b/tools/dynwinrt-codegen/src/codegen/com/project/mod.rs new file mode 100644 index 00000000..81a852d2 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/project/mod.rs @@ -0,0 +1,762 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod interop; +pub(super) mod types; + +use crate::com_metadata::{ComEnumValue, ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; +use crate::types::TypeMeta; + +use super::ir::{ + ActivationPlan, ComParamDirection, ComReturnConvention, ComType, ProjectedComEnum, + ProjectedComEnumMember, ProjectedComInterface, ProjectedComMethod, ProjectedComMethodKind, + ProjectedComParam, ProjectedComResult, ProjectedEnumValue, ResultConversion, ResultSource, + StringBufferPlan, StringEncoding, UnsupportedComType, +}; +use super::javascript::naming::camel_case; +use interop::resolve_projected_default_iid; +use types::{is_scalar_in_out, is_supported_direct_return, project_enum_underlying, project_type}; + +pub(super) fn project_com_interface( + meta: &ComInterfaceMeta, + winmd_paths: &str, +) -> Result { + let interop_target = detect_interop_target(meta, winmd_paths)?; + let methods = meta + .interface + .methods + .iter() + .map(|method| project_method(meta, method, interop_target.as_ref())) + .collect::, _>>()?; + let activation = if let Some((class_name, class_namespace, target_iid)) = interop_target { + ActivationPlan::WinRtFactory { + class_name, + class_namespace, + target_iid, + } + } else if let Some(clsid) = &meta.coclass_clsid { + ActivationPlan::Coclass { + clsid: clsid.clone(), + coclass_name: meta + .coclass_name + .clone() + .unwrap_or_else(|| "Coclass".into()), + } + } else { + ActivationPlan::None + }; + let referenced_enums = meta + .referenced_enums + .iter() + .map(|en| { + Ok(ProjectedComEnum { + name: en.name.clone(), + underlying: project_enum_underlying(&en.underlying).map_err(|unsupported| { + unsupported_error(unsupported, "enum underlying type") + })?, + members: en + .members + .iter() + .map(|member| ProjectedComEnumMember { + name: member.name.clone(), + value: match member.value { + ComEnumValue::Signed(value) => ProjectedEnumValue::Signed(value), + ComEnumValue::Unsigned(value) => ProjectedEnumValue::Unsigned(value), + }, + }) + .collect(), + }) + }) + .collect::, String>>()?; + Ok(ProjectedComInterface { + name: meta.interface.name.clone(), + namespace: meta.interface.namespace.clone(), + iid: meta.interface.iid.clone(), + is_iunknown_rooted: meta.is_iunknown_rooted, + methods, + activation, + referenced_enums, + }) +} + +fn project_method( + interface: &ComInterfaceMeta, + method: &MethodMeta, + interop_target: Option<&(String, String, String)>, +) -> Result { + let context = || format!("{}.{}", interface.interface.name, method.name); + let dynamic_natural_count = dynamic_iid_natural_param_count(method); + if dynamic_natural_count.is_some() && method.preserve_hresult { + return Err(format!( + "{}: semantic HRESULT dynamic-IID methods are not supported", + context() + )); + } + let kind = match (interop_target, method.name.as_str(), dynamic_natural_count) { + (Some((_, _, target_iid)), "GetForWindow", Some(natural_param_count)) => { + ProjectedComMethodKind::SynthesizedGetForWindow { + natural_param_count, + target_iid: target_iid.clone(), + } + } + (_, _, Some(natural_param_count)) => ProjectedComMethodKind::CallerSuppliedDynamicIid { + natural_param_count, + }, + _ => ProjectedComMethodKind::Normal, + }; + + let mut params = Vec::with_capacity(method.params.len()); + for (index, param) in method.params.iter().enumerate() { + match param.direction { + ParamDirection::UnsupportedNativeArray { count_param_index } => { + let count = count_param_index + .map(|index| format!("parameter index {index}")) + .unwrap_or_else(|| "metadata-defined size".into()); + return Err(format!( + "{}: caller-sized native buffers are not supported (`{}` uses {count})", + context(), + param.name + )); + } + ParamDirection::OutFill => { + return Err(format!( + "{}: caller-allocated array outputs are not supported", + context() + )); + } + _ => {} + } + let typ = project_type(¶m.typ).map_err(|unsupported| { + unsupported_error( + unsupported, + &format!("{} parameter `{}`", context(), param.name), + ) + })?; + let cleanup = cleanup_for_param(method, index, &context())?; + if cleanup.is_some() && dynamic_natural_count.is_some() && index == method.params.len() - 1 + { + return Err(format!( + "{}: dynamic-IID interface output cannot declare an allocator cleanup contract", + context() + )); + } + if cleanup.is_some() && param.direction == ParamDirection::InOut { + return Err(format!( + "{}: allocator ownership transfer for [in, out] parameter `{}` is not supported", + context(), + param.name + )); + } + if param.direction == ParamDirection::InOut && !is_scalar_in_out(&typ) { + return Err(format!( + "{}: unsupported [in, out] parameter `{}` of type {:?}", + context(), + param.name, + param.typ + )); + } + if param.direction == ParamDirection::Out + && typ == ComType::RawPointer + && dynamic_natural_count.is_none() + && cleanup != Some(CleanupKind::CoTaskMemFree) + && cleanup.is_none() + { + return Err(unsupported_error( + UnsupportedComType::UnknownOwnership { + type_name: "untyped pointer output".into(), + }, + &context(), + )); + } + if param.direction == ParamDirection::Out + && typ == ComType::Bstr + && cleanup != Some(CleanupKind::SysFreeString) + && cleanup.is_none() + { + return Err(unsupported_error( + UnsupportedComType::UnknownOwnership { + type_name: "BSTR output".into(), + }, + &context(), + )); + } + if matches!(param.direction, ParamDirection::Out | ParamDirection::InOut) + && matches!( + typ, + ComType::PointerAlias { + kind: super::ir::PointerAliasKind::StringPointer, + .. + } + ) + && cleanup != Some(CleanupKind::CoTaskMemFree) + && cleanup.is_none() + { + return Err(unsupported_error( + UnsupportedComType::UnknownOwnership { + type_name: format!("string pointer output `{}`", param.name), + }, + &context(), + )); + } + params.push(ProjectedComParam { + name: param.name.clone(), + typ, + direction: match param.direction { + ParamDirection::In => ComParamDirection::In, + ParamDirection::Out => ComParamDirection::Out, + ParamDirection::InOut => ComParamDirection::InOut, + ParamDirection::OutStringBuffer { .. } => ComParamDirection::OutStringBuffer, + ParamDirection::OutFill | ParamDirection::UnsupportedNativeArray { .. } => { + unreachable!("unsupported directions returned above") + } + }, + }); + } + validate_owned_outputs(method, ¶ms, &context())?; + + let return_convention = match &method.return_type { + None => ComReturnConvention::Void, + Some(typ) => { + let projected = project_type(typ).map_err(|unsupported| match unsupported { + UnsupportedComType::NativeStructLayout { .. } | UnsupportedComType::Unknown => { + unsupported_error( + UnsupportedComType::UnsupportedDirectReturn { + type_name: format!("{typ:?}"), + }, + &context(), + ) + } + unsupported => { + unsupported_error(unsupported, &format!("{} return value", context())) + } + })?; + if projected == ComType::HResult { + if method.preserve_hresult { + ComReturnConvention::SemanticHResult + } else { + ComReturnConvention::HResult + } + } else if is_supported_direct_return(&projected) { + ComReturnConvention::Direct(projected) + } else { + return Err(unsupported_error( + UnsupportedComType::UnsupportedDirectReturn { + type_name: format!("{typ:?}"), + }, + &context(), + )); + } + } + }; + if method.preserve_hresult && !matches!(return_convention, ComReturnConvention::SemanticHResult) + { + return Err(format!( + "{}: semantic HRESULT metadata requires an HRESULT return", + context() + )); + } + + let string_buffer = project_string_buffer(method)?; + if params + .iter() + .any(|param| param.direction == ComParamDirection::OutStringBuffer) + && string_buffer.is_none() + { + return Err(format!( + "{}: unsupported string-buffer encoding or count relationship", + context() + )); + } + let mut results = Vec::new(); + if let ComReturnConvention::SemanticHResult | ComReturnConvention::Direct(_) = + &return_convention + { + let typ = match &return_convention { + ComReturnConvention::SemanticHResult => ComType::HResult, + ComReturnConvention::Direct(typ) => typ.clone(), + ComReturnConvention::HResult | ComReturnConvention::Void => unreachable!(), + }; + results.push(ProjectedComResult { + conversion: result_conversion(&typ, method, None, &kind), + typ, + source: ResultSource::DirectReturn, + }); + } + for (index, param) in params.iter().enumerate() { + if matches!( + param.direction, + ComParamDirection::Out | ComParamDirection::InOut + ) { + results.push(ProjectedComResult { + typ: param.typ.clone(), + source: ResultSource::Param(index), + conversion: result_conversion(¶m.typ, method, Some(index), &kind), + }); + } + } + + Ok(ProjectedComMethod { + name: method.name.clone(), + camel_name: camel_case(&method.name), + vtable_index: method.vtable_index, + params, + return_convention, + results, + string_buffer, + kind, + doc: method.doc.clone(), + }) +} + +fn result_conversion( + typ: &ComType, + method: &MethodMeta, + param_index: Option, + kind: &ProjectedComMethodKind, +) -> ResultConversion { + if matches!( + kind, + ProjectedComMethodKind::CallerSuppliedDynamicIid { .. } + | ProjectedComMethodKind::SynthesizedGetForWindow { .. } + ) && param_index == Some(method.params.len() - 1) + { + return ResultConversion::DynamicIidAdoption; + } + if let Some(index) = param_index { + let cleanup = cleanup_for_param(method, index, &method.name) + .expect("cleanup contract was validated during projection"); + if cleanup == Some(CleanupKind::SysFreeString) && matches!(typ, ComType::Bstr) { + return ResultConversion::Bstr; + } + if cleanup == Some(CleanupKind::CoTaskMemFree) { + return match typ { + ComType::PointerAlias { name, .. } if name == "PWSTR" => { + ResultConversion::CoTaskMemString(StringEncoding::Wide) + } + ComType::PointerAlias { name, .. } if name == "PSTR" => { + ResultConversion::CoTaskMemString(StringEncoding::Ansi) + } + _ => ResultConversion::CoTaskMemData, + }; + } + } + match typ { + ComType::ManagedInterface { .. } => ResultConversion::ManagedCom, + ComType::HString => ResultConversion::HString, + _ => ResultConversion::Value, + } +} + +fn validate_owned_outputs( + method: &MethodMeta, + params: &[ProjectedComParam], + context: &str, +) -> Result<(), String> { + for (index, param) in params.iter().enumerate() { + let Some(cleanup) = cleanup_for_param(method, index, context)? else { + continue; + }; + if !matches!( + param.direction, + ComParamDirection::Out | ComParamDirection::InOut + ) { + return Err(format!( + "{context}: ownership metadata applies to non-output parameter `{}`", + param.name + )); + } + if cleanup == CleanupKind::SysFreeString { + if param.direction != ComParamDirection::Out || param.typ != ComType::Bstr { + return Err(format!( + "{context}: SysFreeString ownership requires a scalar Out BSTR" + )); + } + } else if cleanup == CleanupKind::CoTaskMemFree { + if param.direction != ComParamDirection::Out + || !matches!( + param.typ, + ComType::RawPointer + | ComType::PointerAlias { + kind: super::ir::PointerAliasKind::DataPointer + | super::ir::PointerAliasKind::StringPointer, + .. + } + ) + { + return Err(format!( + "{context}: CoTaskMemFree ownership requires an Out data or string pointer" + )); + } + } + } + for owned in &method.owned_outputs { + if owned.param_index >= params.len() { + return Err(format!( + "{context}: ownership metadata references missing parameter index {}", + owned.param_index + )); + } + } + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CleanupKind { + SysFreeString, + CoTaskMemFree, +} + +fn cleanup_for_param( + method: &MethodMeta, + param_index: usize, + context: &str, +) -> Result, String> { + let contracts = method + .owned_outputs + .iter() + .filter(|owned| owned.param_index == param_index) + .map(|owned| owned.free_with.as_str()) + .collect::>(); + match contracts.as_slice() { + [] => Ok(None), + [contract] => parse_cleanup_contract(contract) + .map(Some) + .ok_or_else(|| format!("{context}: unsupported output cleanup contract `{contract}`")), + _ => Err(format!( + "{context}: output parameter index {param_index} has multiple cleanup contracts" + )), + } +} + +fn parse_cleanup_contract(contract: &str) -> Option { + if contract.is_empty() + || contract != contract.trim() + || contract + .chars() + .any(|character| character.is_whitespace() || matches!(character, ',' | ';')) + { + return None; + } + let identifier = contract + .rsplit_once(|character: char| matches!(character, '.' | '!' | ':')) + .map_or(contract, |(_, identifier)| identifier); + if identifier.is_empty() { + return None; + } + match identifier { + "SysFreeString" => Some(CleanupKind::SysFreeString), + "CoTaskMemFree" => Some(CleanupKind::CoTaskMemFree), + _ => None, + } +} + +fn project_string_buffer(method: &MethodMeta) -> Result, String> { + let buffers = method + .params + .iter() + .enumerate() + .filter_map(|(buffer_param_index, param)| { + let ParamDirection::OutStringBuffer { count_param_index } = param.direction else { + return None; + }; + Some((buffer_param_index, count_param_index, param)) + }) + .collect::>(); + if buffers.len() > 1 { + return Err(format!( + "{}: multiple caller-owned string buffers are not supported", + method.name + )); + } + let plan = + buffers + .into_iter() + .next() + .and_then(|(buffer_param_index, count_param_index, param)| { + let encoding = match pointer_alias_name(¶m.typ) { + Some("PWSTR") => StringEncoding::Wide, + Some("PSTR") => StringEncoding::Ansi, + _ => return None, + }; + method + .params + .get(count_param_index) + .filter(|count| count.direction == ParamDirection::In)?; + let optional_param_indices = (count_param_index..method.params.len()) + .filter(|index| string_buffer_param_is_optional(method, *index)) + .collect(); + Some(StringBufferPlan { + buffer_param_index, + count_param_index, + encoding, + optional_param_indices, + }) + }); + if plan + .as_ref() + .is_some_and(|plan| plan.encoding == StringEncoding::Ansi) + { + return Err(format!( + "{}: caller-owned ANSI output buffers are not yet decoded safely", + method.name + )); + } + Ok(plan) +} + +fn string_buffer_param_is_optional(method: &MethodMeta, param_index: usize) -> bool { + let Some((_, count_index)) = + method + .params + .iter() + .enumerate() + .find_map(|(buffer_index, param)| match param.direction { + ParamDirection::OutStringBuffer { count_param_index } => { + Some((buffer_index, count_param_index)) + } + _ => None, + }) + else { + return false; + }; + let Some(param) = method.params.get(param_index) else { + return false; + }; + let optional_shape = + param_index == count_index || (param_index > count_index && is_optional_find_data(param)); + optional_shape + && method + .params + .iter() + .skip(param_index + 1) + .filter(|param| param.direction.is_input()) + .all(is_optional_find_data) +} + +fn is_optional_find_data(param: &ParamMeta) -> bool { + if !matches!(param.direction, ParamDirection::In | ParamDirection::Out) { + return false; + } + let name = param.name.to_ascii_lowercase(); + name == "pfd" + || name.contains("finddata") + || name.contains("find_data") + || matches!( + ¶m.typ, + TypeMeta::Struct { name, .. } + if name == "WIN32_FIND_DATAW" || name == "WIN32_FIND_DATAA" + ) +} + +fn pointer_alias_name(typ: &TypeMeta) -> Option<&str> { + match typ { + TypeMeta::Struct { name, .. } => Some(name), + _ => None, + } +} + +fn dynamic_iid_natural_param_count(method: &MethodMeta) -> Option { + if !method + .return_type + .as_ref() + .is_some_and(|typ| matches!(project_type(typ), Ok(ComType::HResult))) + || method.params.len() < 2 + { + return None; + } + let output = method.params.last()?; + if output.direction != ParamDirection::Out || output.typ != TypeMeta::Object { + return None; + } + let iid = &method.params[method.params.len() - 2]; + let iid_name = iid.name.to_ascii_lowercase(); + if iid.direction != ParamDirection::In + || iid.typ != TypeMeta::Object + || !matches!(iid_name.as_str(), "iid" | "riid") + || method.params[..method.params.len() - 2] + .iter() + .any(|param| param.direction != ParamDirection::In) + { + return None; + } + Some(method.params.len() - 2) +} + +fn detect_interop_target( + meta: &ComInterfaceMeta, + winmd_paths: &str, +) -> Result, String> { + if !meta.interface.name.ends_with("Interop") + || !meta.interface.methods.iter().any(|method| { + method.name == "GetForWindow" && dynamic_iid_natural_param_count(method).is_some() + }) + { + return Ok(None); + } + let stripped_i = meta + .interface + .name + .strip_prefix('I') + .unwrap_or(&meta.interface.name); + let class_name = stripped_i + .strip_suffix("Interop") + .unwrap_or(stripped_i) + .to_string(); + let Some((namespace, _, iid)) = resolve_projected_default_iid(winmd_paths, &class_name) else { + return Err(format!( + "Classic-COM interop generator: cannot resolve default IID for the projected \ + WinRT runtime class `{class_name}` (derived from `{}`). \ + Neither the winmds passed to the generator ({winmd_paths:?}) nor the newest installed \ + `C:\\Program Files (x86)\\Windows Kits\\10\\UnionMetadata\\\\Windows.winmd` \ + contains a WinRT runtime class of that name with a resolvable default interface. \ + Pass the correct Windows.winmd via --ref or install a recent Windows SDK.", + meta.interface.name + )); + }; + Ok(Some((class_name, namespace, iid))) +} + +fn unsupported_error(unsupported: UnsupportedComType, context: &str) -> String { + match unsupported { + UnsupportedComType::Array => format!( + "{context}: native arrays require an explicit count and element-ownership projection; \ + raw-pointer fallback is not allowed" + ), + UnsupportedComType::ParameterizedInterface { namespace, name } => format!( + "{context}: parameterized interface `{namespace}.{name}` requires a computed closed IID \ + and managed ownership projection; raw-pointer fallback is not allowed" + ), + UnsupportedComType::AsyncInterface => format!( + "{context}: async interface requires a computed closed IID and managed ownership \ + projection; raw-pointer fallback is not allowed" + ), + UnsupportedComType::Delegate { namespace, name } => format!( + "{context}: delegate `{namespace}.{name}` requires a managed callback projection; \ + raw-pointer fallback is not allowed" + ), + UnsupportedComType::NativeStructLayout { namespace, name } => { + format!("{context}: struct `{namespace}.{name}` requires native layout projection") + } + UnsupportedComType::UnknownPointerAlias { namespace, name } => format!( + "{context}: pointer-shaped typedef `{namespace}.{name}` has no explicit semantic \ + classification; raw-pointer fallback is not allowed" + ), + UnsupportedComType::UnresolvedInterface { namespace, name } => format!( + "{context}: interface `{namespace}.{name}` has no resolvable IID; \ + pass the metadata that defines it via --ref instead of projecting it as a raw pointer" + ), + UnsupportedComType::UnresolvedRuntimeClass { namespace, name } => format!( + "{context}: runtime class `{namespace}.{name}` has no resolvable default interface; \ + pass the metadata that defines it via --ref" + ), + UnsupportedComType::UnknownOwnership { type_name } => { + format!("{context}: {type_name} has no ownership projection") + } + UnsupportedComType::UnsupportedDirectReturn { type_name } => { + format!("{context}: unsupported direct native return type {type_name}") + } + UnsupportedComType::Unknown => { + format!("{context}: unsupported Classic-COM type") + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::com_metadata::{InterfaceMeta, MethodMeta, ParamMeta}; + + fn interface(method: MethodMeta) -> ComInterfaceMeta { + ComInterfaceMeta { + interface: InterfaceMeta { + name: "ITest".into(), + namespace: "Tests".into(), + iid: "00000000-0000-0000-0000-000000000001".into(), + methods: vec![method], + ..Default::default() + }, + base_offset: 3, + is_iunknown_rooted: true, + base_chain: vec!["IUnknown".into()], + coclass_clsid: None, + coclass_name: None, + own_methods_start: 3, + referenced_enums: Vec::new(), + } + } + + #[test] + fn unsupported_type_fails_during_projection() { + let method = MethodMeta { + name: "Bad".into(), + params: vec![ParamMeta { + name: "values".into(), + typ: TypeMeta::Array(Box::new(TypeMeta::I32)), + direction: ParamDirection::In, + }], + ..Default::default() + }; + assert!( + project_com_interface(&interface(method), "") + .unwrap_err() + .contains("raw-pointer fallback is not allowed") + ); + } + + #[test] + fn by_value_guid_is_not_dynamic_iid() { + let method = MethodMeta { + name: "Get".into(), + params: vec![ + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Guid, + direction: ParamDirection::In, + }, + ParamMeta { + name: "result".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HRESULT".into(), + fields: Vec::new(), + }), + ..Default::default() + }; + assert!( + project_com_interface(&interface(method), "") + .unwrap_err() + .contains("untyped pointer output") + ); + } + + #[test] + fn owned_output_without_a_known_cleanup_fails_before_rendering() { + let method = MethodMeta { + name: "GetName".into(), + params: vec![ParamMeta { + name: "name".into(), + typ: TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "BSTR".into(), + fields: Vec::new(), + }, + direction: ParamDirection::Out, + }], + return_type: Some(TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HRESULT".into(), + fields: Vec::new(), + }), + ..Default::default() + }; + assert!( + project_com_interface(&interface(method), "") + .unwrap_err() + .contains("BSTR output has no ownership projection") + ); + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/project/types.rs b/tools/dynwinrt-codegen/src/codegen/com/project/types.rs new file mode 100644 index 00000000..45d27a77 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/project/types.rs @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::com_metadata::{is_native_isize, is_native_usize}; +use crate::types::TypeMeta; + +use super::super::ir::{ + ComEnumUnderlying, ComPrimitive, ComScalarRepr, ComType, PointerAliasKind, UnsupportedComType, +}; + +pub(in crate::codegen::com) fn project_type(typ: &TypeMeta) -> Result { + if is_native_isize(typ) { + return Ok(ComType::NativeIsize); + } + if is_native_usize(typ) { + return Ok(ComType::NativeUsize); + } + match typ { + TypeMeta::Bool => Ok(ComType::Primitive(ComPrimitive::Bool)), + TypeMeta::I8 => Ok(ComType::Primitive(ComPrimitive::I8)), + TypeMeta::U8 => Ok(ComType::Primitive(ComPrimitive::U8)), + TypeMeta::I16 => Ok(ComType::Primitive(ComPrimitive::I16)), + TypeMeta::U16 => Ok(ComType::Primitive(ComPrimitive::U16)), + TypeMeta::I32 => Ok(ComType::Primitive(ComPrimitive::I32)), + TypeMeta::U32 => Ok(ComType::Primitive(ComPrimitive::U32)), + TypeMeta::I64 => Ok(ComType::Primitive(ComPrimitive::I64)), + TypeMeta::U64 => Ok(ComType::Primitive(ComPrimitive::U64)), + TypeMeta::F32 => Ok(ComType::Primitive(ComPrimitive::F32)), + TypeMeta::F64 => Ok(ComType::Primitive(ComPrimitive::F64)), + TypeMeta::Char16 => Ok(ComType::Primitive(ComPrimitive::Char16)), + TypeMeta::String => Ok(ComType::HString), + TypeMeta::Guid => Ok(ComType::Guid), + TypeMeta::Object => Ok(ComType::RawPointer), + TypeMeta::Interface { + namespace, + name, + iid, + } if iid.is_empty() => Err(UnsupportedComType::UnresolvedInterface { + namespace: namespace.clone(), + name: name.clone(), + }), + TypeMeta::Interface { iid, .. } => Ok(ComType::ManagedInterface { iid: iid.clone() }), + TypeMeta::RuntimeClass { + default_interface: Some(default_interface), + .. + } => project_type(default_interface), + TypeMeta::RuntimeClass { + namespace, + name, + default_interface: None, + } => Err(UnsupportedComType::UnresolvedRuntimeClass { + namespace: namespace.clone(), + name: name.clone(), + }), + TypeMeta::Delegate { + namespace, name, .. + } => Err(UnsupportedComType::Delegate { + namespace: namespace.clone(), + name: name.clone(), + }), + TypeMeta::Parameterized { + namespace, name, .. + } => Err(UnsupportedComType::ParameterizedInterface { + namespace: namespace.clone(), + name: name.clone(), + }), + TypeMeta::AsyncAction + | TypeMeta::AsyncActionWithProgress(_) + | TypeMeta::AsyncOperation(_) + | TypeMeta::AsyncOperationWithProgress(_, _) => Err(UnsupportedComType::AsyncInterface), + TypeMeta::Array(_) => Err(UnsupportedComType::Array), + TypeMeta::Enum { + name, underlying, .. + } => Ok(ComType::Enum { + name: name.clone(), + underlying: project_enum_underlying(underlying)?, + }), + TypeMeta::Struct { + namespace, name, .. + } if namespace == "Windows.Win32.Foundation" && name == "BOOL" => Ok(ComType::Win32Bool), + TypeMeta::Struct { + namespace, name, .. + } if namespace == "Windows.Win32.Foundation" && name == "HRESULT" => Ok(ComType::HResult), + TypeMeta::Struct { + namespace, name, .. + } if namespace == "Windows.Win32.Foundation" && name == "BSTR" => Ok(ComType::Bstr), + TypeMeta::Struct { + namespace, + name, + fields, + } if namespace.starts_with("Windows.Win32.") + && fields.len() == 1 + && fields[0].name == "Value" => + { + if let Some(underlying) = scalar_alias_underlying(name, &fields[0].typ) { + Ok(ComType::ScalarAlias { + name: name.clone(), + underlying, + }) + } else if matches!(fields[0].typ, TypeMeta::Object) { + classify_pointer_alias(name) + .map(|kind| ComType::PointerAlias { + name: name.clone(), + kind, + }) + .ok_or_else(|| UnsupportedComType::UnknownPointerAlias { + namespace: namespace.clone(), + name: name.clone(), + }) + } else { + Err(UnsupportedComType::NativeStructLayout { + namespace: namespace.clone(), + name: name.clone(), + }) + } + } + TypeMeta::Struct { + namespace, name, .. + } => Err(UnsupportedComType::NativeStructLayout { + namespace: namespace.clone(), + name: name.clone(), + }), + } +} + +pub(super) fn project_enum_underlying( + typ: &TypeMeta, +) -> Result { + match typ { + TypeMeta::I8 => Ok(ComEnumUnderlying::I8), + TypeMeta::U8 => Ok(ComEnumUnderlying::U8), + TypeMeta::I16 => Ok(ComEnumUnderlying::I16), + TypeMeta::U16 => Ok(ComEnumUnderlying::U16), + TypeMeta::I32 => Ok(ComEnumUnderlying::I32), + TypeMeta::U32 => Ok(ComEnumUnderlying::U32), + TypeMeta::I64 => Ok(ComEnumUnderlying::I64), + TypeMeta::U64 => Ok(ComEnumUnderlying::U64), + _ => Err(UnsupportedComType::Unknown), + } +} + +fn scalar_alias_underlying(name: &str, typ: &TypeMeta) -> Option { + match name { + "LPARAM" | "LRESULT" => return Some(ComScalarRepr::NativeIsize), + "WPARAM" => return Some(ComScalarRepr::NativeUsize), + _ => {} + } + let primitive = match typ { + TypeMeta::Bool => ComPrimitive::Bool, + TypeMeta::I8 => ComPrimitive::I8, + TypeMeta::U8 => ComPrimitive::U8, + TypeMeta::I16 => ComPrimitive::I16, + TypeMeta::U16 => ComPrimitive::U16, + TypeMeta::I32 => ComPrimitive::I32, + TypeMeta::U32 => ComPrimitive::U32, + TypeMeta::I64 => ComPrimitive::I64, + TypeMeta::U64 => ComPrimitive::U64, + TypeMeta::F32 => ComPrimitive::F32, + TypeMeta::F64 => ComPrimitive::F64, + TypeMeta::Char16 => ComPrimitive::Char16, + _ => return None, + }; + Some(ComScalarRepr::Primitive(primitive)) +} + +fn classify_pointer_alias(name: &str) -> Option { + if matches!( + name, + "PWSTR" + | "PCWSTR" + | "PSTR" + | "PCSTR" + | "LPWSTR" + | "LPCWSTR" + | "LPSTR" + | "LPCSTR" + | "PWCHAR" + | "PCWCHAR" + | "LPWCH" + | "LPCWCH" + | "LPCH" + | "LPCCH" + ) { + Some(PointerAliasKind::StringPointer) + } else if matches!( + name, + "PSID" + | "PSECURITY_DESCRIPTOR" + | "MEMORY_MAPPED_VIEW_ADDRESS" + | "LPPROC_THREAD_ATTRIBUTE_LIST" + | "PVOID" + | "PCVOID" + | "LPVOID" + | "LPCVOID" + ) { + Some(PointerAliasKind::DataPointer) + } else if is_known_handle_alias(name) { + Some(PointerAliasKind::HandleValue) + } else { + None + } +} + +fn is_known_handle_alias(name: &str) -> bool { + matches!( + name, + "HANDLE" + | "HWND" + | "HACCEL" + | "HBITMAP" + | "HBRUSH" + | "HCURSOR" + | "HDC" + | "HDESK" + | "HDWP" + | "HENHMETAFILE" + | "HFILE" + | "HFONT" + | "HGDIOBJ" + | "HGLOBAL" + | "HHOOK" + | "HICON" + | "HIMAGELIST" + | "HINSTANCE" + | "HKEY" + | "HKL" + | "HLOCAL" + | "HMENU" + | "HMETAFILE" + | "HMODULE" + | "HMONITOR" + | "HPALETTE" + | "HPEN" + | "HRAWINPUT" + | "HRGN" + | "HRSRC" + | "HTHEME" + | "HWINSTA" + | "SC_HANDLE" + | "SERVICE_STATUS_HANDLE" + | "DPI_AWARENESS_CONTEXT" + ) +} + +pub(super) fn is_scalar_in_out(typ: &ComType) -> bool { + matches!( + typ, + ComType::Primitive(_) + | ComType::NativeIsize + | ComType::NativeUsize + | ComType::Win32Bool + | ComType::HResult + | ComType::Enum { .. } + | ComType::ScalarAlias { .. } + | ComType::PointerAlias { .. } + ) +} + +pub(super) fn is_supported_direct_return(typ: &ComType) -> bool { + is_scalar_in_out(typ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_supported_type_category_projects_without_fallback() { + let types = [ + TypeMeta::Bool, + TypeMeta::I8, + TypeMeta::U8, + TypeMeta::I16, + TypeMeta::U16, + TypeMeta::I32, + TypeMeta::U32, + TypeMeta::I64, + TypeMeta::U64, + TypeMeta::F32, + TypeMeta::F64, + TypeMeta::Char16, + TypeMeta::String, + TypeMeta::Guid, + TypeMeta::Object, + ]; + for typ in types { + assert!(project_type(&typ).is_ok(), "{typ:?}"); + } + } + + #[test] + fn reference_like_types_never_degrade_to_raw_pointer() { + let parameterized = TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IVector".into(), + piid: String::new(), + args: vec![TypeMeta::I32], + }; + assert!(matches!( + project_type(¶meterized), + Err(UnsupportedComType::ParameterizedInterface { .. }) + )); + assert!(matches!( + project_type(&TypeMeta::AsyncAction), + Err(UnsupportedComType::AsyncInterface) + )); + } + + #[test] + fn transparent_scalar_typedefs_preserve_scalar_abi() { + let colorref = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "COLORREF".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::U32, + }], + }; + assert_eq!( + project_type(&colorref), + Ok(ComType::ScalarAlias { + name: "COLORREF".into(), + underlying: ComScalarRepr::Primitive(ComPrimitive::U32), + }) + ); + + let lparam = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "LPARAM".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + assert_eq!( + project_type(&lparam), + Ok(ComType::ScalarAlias { + name: "LPARAM".into(), + underlying: ComScalarRepr::NativeIsize, + }) + ); + } + + #[test] + fn unknown_pointer_shaped_typedef_fails_closed() { + let unknown = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "MYSTERY_POINTER".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + assert!(matches!( + project_type(&unknown), + Err(UnsupportedComType::UnknownPointerAlias { .. }) + )); + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/projection.rs b/tools/dynwinrt-codegen/src/codegen/com/projection.rs deleted file mode 100644 index 3e41311e..00000000 --- a/tools/dynwinrt-codegen/src/codegen/com/projection.rs +++ /dev/null @@ -1,218 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use crate::com_metadata::{ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; -use crate::types::TypeMeta; - -use super::naming::camel_case; -use super::type_mapping::is_hresult; - -#[derive(Debug, Clone)] -pub(super) struct InteropMethod { - pub(super) camel: String, - pub(super) vtable_index: usize, - pub(super) natural_params: Option>, - pub(super) plain: Option, - pub(super) _doc: Option, -} - -#[derive(Debug, Clone)] -pub(super) struct InteropInfo { - pub(super) methods: Vec, - pub(super) class_name: String, - pub(super) class_namespace: String, - pub(super) target_iid: String, -} - -pub(super) fn method_is_interop_shape(m: &MethodMeta) -> Option> { - match &m.return_type { - Some(t) if is_hresult(t) => {} - _ => return None, - } - if m.params.len() < 2 { - return None; - } - let last_idx = m.params.len() - 1; - let out_param = &m.params[last_idx]; - if out_param.direction != ParamDirection::Out || !matches!(out_param.typ, TypeMeta::Object) { - return None; - } - if m.params[..last_idx] - .iter() - .any(|param| param.direction != ParamDirection::In) - { - return None; - } - let riid = &m.params[last_idx - 1]; - let is_riid = matches!(riid.typ, TypeMeta::Object) - && matches!(riid.name.to_ascii_lowercase().as_str(), "riid" | "iid"); - is_riid.then(|| m.params[..last_idx - 1].to_vec()) -} - -pub(super) fn detect_interop( - meta: &ComInterfaceMeta, - winmd_paths: &str, -) -> Result, String> { - let iface = &meta.interface; - if !iface.name.ends_with("Interop") || iface.methods.is_empty() { - return Ok(None); - } - - let mut has_interop_method = false; - let methods = iface - .methods - .iter() - .map(|method| match method_is_interop_shape(method) { - Some(natural_params) if method.name == "GetForWindow" => { - has_interop_method = true; - InteropMethod { - camel: camel_case(&method.name), - vtable_index: method.vtable_index, - natural_params: Some(natural_params), - plain: None, - _doc: method.doc.clone(), - } - } - _ => InteropMethod { - camel: camel_case(&method.name), - vtable_index: method.vtable_index, - natural_params: None, - plain: Some(method.clone()), - _doc: method.doc.clone(), - }, - }) - .collect(); - if !has_interop_method { - return Ok(None); - } - - let stripped_i = iface.name.strip_prefix('I').unwrap_or(&iface.name); - let class_name = stripped_i - .strip_suffix("Interop") - .unwrap_or(stripped_i) - .to_string(); - let (class_namespace, target_iid) = match resolve_projected_default_iid( - winmd_paths, - &class_name, - ) { - Some((namespace, _interface_name, iid)) => (namespace, iid), - None => { - return Err(format!( - "Classic-COM interop generator: cannot resolve default IID for the projected \ - WinRT runtime class `{class_name}` (derived from `{}`). \ - Neither the winmds passed to the generator ({winmd_paths:?}) nor the newest installed \ - `C:\\Program Files (x86)\\Windows Kits\\10\\UnionMetadata\\\\Windows.winmd` \ - contains a WinRT runtime class of that name with a resolvable default interface. \ - Pass the correct Windows.winmd via --ref or install a recent Windows SDK.", - iface.name - )); - } - }; - - Ok(Some(InteropInfo { - methods, - class_name, - class_namespace, - target_iid, - })) -} - -fn resolve_projected_default_iid( - winmd_paths: &str, - simple_class_name: &str, -) -> Option<(String, String, String)> { - if !winmd_paths.is_empty() { - if let Some(result) = - crate::com_metadata::find_runtime_class_default_iid(winmd_paths, simple_class_name) - { - return Some(result); - } - } - let sdk_winmd = crate::com_metadata::discover_newest_windows_winmd()?; - if winmd_paths - .split(';') - .any(|path| path.eq_ignore_ascii_case(&sdk_winmd)) - { - return None; - } - crate::com_metadata::find_runtime_class_default_iid(&sdk_winmd, simple_class_name) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn interop_shape_rejects_non_refiid_trailing_object() { - let method = MethodMeta { - params: vec![ - ParamMeta { - name: "value".into(), - typ: TypeMeta::Object, - direction: ParamDirection::In, - }, - ParamMeta { - name: "result".into(), - typ: TypeMeta::Object, - direction: ParamDirection::Out, - }, - ], - return_type: Some(TypeMeta::Struct { - namespace: "Windows.Win32.Foundation".into(), - name: "HRESULT".into(), - fields: Vec::new(), - }), - ..Default::default() - }; - - assert!(method_is_interop_shape(&method).is_none()); - } - - #[test] - fn non_get_for_window_interop_keeps_caller_iid() { - let method = MethodMeta { - name: "CreateSessionForWindow".into(), - params: vec![ - ParamMeta { - name: "window".into(), - typ: TypeMeta::Object, - direction: ParamDirection::In, - }, - ParamMeta { - name: "riid".into(), - typ: TypeMeta::Object, - direction: ParamDirection::In, - }, - ParamMeta { - name: "result".into(), - typ: TypeMeta::Object, - direction: ParamDirection::Out, - }, - ], - return_type: Some(TypeMeta::Struct { - namespace: "Windows.Win32.Foundation".into(), - name: "HRESULT".into(), - fields: Vec::new(), - }), - ..Default::default() - }; - let meta = ComInterfaceMeta { - interface: crate::com_metadata::InterfaceMeta { - name: "IUserActivityInterop".into(), - namespace: "Windows.Win32.System.WinRT".into(), - iid: "00000000-0000-0000-0000-000000000000".into(), - methods: vec![method], - ..Default::default() - }, - base_offset: 3, - is_iunknown_rooted: true, - base_chain: vec!["IUnknown".into()], - coclass_clsid: None, - coclass_name: None, - own_methods_start: 3, - referenced_enums: Vec::new(), - }; - - assert!(detect_interop(&meta, "").unwrap().is_none()); - } -} diff --git a/tools/dynwinrt-codegen/src/codegen/com/render.rs b/tools/dynwinrt-codegen/src/codegen/com/render.rs deleted file mode 100644 index 1e48139b..00000000 --- a/tools/dynwinrt-codegen/src/codegen/com/render.rs +++ /dev/null @@ -1,2369 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Classic-COM (option A) code generation. -//! -//! This module generates natural TypeScript/JS wrappers for IUnknown-rooted -//! Win32 COM interfaces described in Windows.Win32.winmd. It intentionally -//! keeps a **separate pipeline** from the WinRT projection: classic COM has -//! meaningfully different semantics (IUnknown base offset of 3 vs 6, HRESULT -//! throw-on-failure, `CoCreateInstance` activation, no IReference/async -//! projection) so mixing them into the existing IR would obscure both paths. -//! -//! What we emit today (phase 1): -//! - `.js`: registration via `DynCom.registerIUnknownInterface` -//! + a natural class with camelCase methods and static `create()` / -//! `_fromNative()`. -//! - `.d.ts`: PascalCase class, camelCase methods, handle-value -//! typedefs (HWND etc.) as `bigint | number`, string-pointer typedefs (PWSTR -//! etc.) as `bigint | Buffer`, HRESULT returns projected to `void` (throwing -//! on failure via the runtime). -//! - Per-enum sibling files for each enum referenced by any method parameter. - -use crate::com_metadata::{ - ComEnumMeta, ComEnumValue, ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta, -}; -use crate::types::TypeMeta; - -#[cfg(test)] -use super::naming::strip_hungarian; -use super::naming::{camel_case, js_param_name}; -use super::projection::method_is_interop_shape; -use super::projection::{InteropInfo, InteropMethod, detect_interop}; -use super::type_mapping::{ - HandleAliasKind, MethodResult, StringEncoding, collect_handle_aliases, dts_params_for_method, - dts_return_type, enum_import_names, has_string_buffer_method, is_bstr, is_cotaskmem_owned, - is_hresult, is_optional_find_data_out_after_string_count, is_sys_free_string_owned, - method_results, string_buffer_param_is_optional, string_buffer_pattern, ts_input_type_expr_dts, - ts_type_expr_js, unwrap_return_js, uses_winrt_bridge_value, validate_com_abi, wrap_arg_js, -}; -#[cfg(test)] -use super::type_mapping::{handle_alias_kind, handle_type_name, ts_type_expr_dts}; - -/// A rendered classic-COM output: primary `.js` + `.d.ts` for the interface, -/// plus zero or more sibling files (one `.js` + `.d.ts` per referenced enum). -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ComGeneratedOutput { - pub js: String, - pub dts: String, - /// Additional files (filename → content). Includes each enum's `.js` and - /// `.d.ts`. Stable-sorted by filename for deterministic output. - pub extra_files: Vec<(String, String)>, -} - -// --------------------------------------------------------------------------- -// Public entry -// --------------------------------------------------------------------------- - -/// Generate the `.js` + `.d.ts` for a classic-COM interface. -/// -/// `winmd_paths` is the semicolon-separated list of `.winmd` files loaded by -/// the generator. Interop `*Interop` interfaces consult these winmds FIRST -/// to resolve the projected WinRT runtime class's default IID; if that fails -/// (e.g. the caller only passed Win32 metadata), the generator falls back to -/// the NEWEST installed `UnionMetadata\\Windows.winmd`. If the target -/// IID still cannot be resolved for a confirmed interop shape, generation -/// **fails loudly** with `Err(...)` — the generator must never emit a NULL -/// riid that would silently break the wrapper at runtime. -pub fn generate_com_interface_files( - meta: &ComInterfaceMeta, - winmd_paths: &str, -) -> Result { - validate_com_abi(meta)?; - validate_untyped_outputs(meta)?; - - // Detect whether this is a `*Interop` interface whose every method has the - // `(HWND, [HSTRING…,] REFIID, out void**)` GetForWindow shape. Natural - // signatures hide the REFIID + void** and return an explicit bridge value. - let interop = detect_interop(meta, winmd_paths)?; - - let js = render_js(meta, interop.as_ref()); - let dts = render_dts(meta, interop.as_ref()); - - // Per-enum sibling files (referenced by parameter types). - let mut extra_files: Vec<(String, String)> = Vec::new(); - for en in &meta.referenced_enums { - let (enum_js, enum_dts) = render_enum_files(en); - extra_files.push((format!("{}.js", en.name), enum_js)); - extra_files.push((format!("{}.d.ts", en.name), enum_dts)); - } - - extra_files.sort_by(|a, b| a.0.cmp(&b.0)); - - Ok(ComGeneratedOutput { - js, - dts, - extra_files, - }) -} - -// --------------------------------------------------------------------------- -// .js rendering -// --------------------------------------------------------------------------- - -fn render_js(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { - let iface = &meta.interface; - let iid = &iface.iid; - let name = &iface.name; - - let mut out = String::new(); - out.push_str("// Generated by dynwinrt-codegen — do not edit\n"); - - // Imports (runtime + any referenced enums) - let runtime_imports = if interop.is_some() { - "DynCom, DynComMethodSig, DynWinRtValue, WinGuid" - } else { - "DynCom, DynComMethodSig, WinGuid" - }; - out.push_str(&format!( - "import {{ {runtime_imports} }} from '{}';\n", - com_runtime_import_name() - )); - for en in enum_import_names(meta) { - out.push_str(&format!("import {{ {} }} from './{}.js';\n", en, en)); - } - out.push('\n'); - - if has_string_buffer_method(meta) { - out.push_str("function _normalizeStringBufferCount(value, name) {\n"); - out.push_str(" if (!Number.isInteger(value) || value <= 0) throw new TypeError(`${name} must be a positive integer`);\n"); - out.push_str(" return value;\n"); - out.push_str("}\n"); - out.push_str("function _decodeWideString(buffer) {\n"); - out.push_str(" let end = 0;\n"); - out.push_str( - " while (end + 1 < buffer.length && buffer.readUInt16LE(end) !== 0) end += 2;\n", - ); - out.push_str(" return buffer.subarray(0, end).toString('utf16le');\n"); - out.push_str("}\n\n"); - } - - out.push_str(&format!( - "export const IID_{name} = WinGuid.parse('{iid}');\n", - name = name, - iid = iid - )); - // For interop wrappers with a resolved target IID, also emit the target - // interface IID as a private constant used by the getForWindow call. - if let Some(info) = interop { - if !info.target_iid.is_empty() { - out.push_str(&format!( - "const IID_{cls}_default = WinGuid.parse('{iid}');\n", - cls = info.class_name, - iid = info.target_iid, - )); - } - } - out.push('\n'); - - // Interface registration is base-aware. - let register_fn = if meta.is_iunknown_rooted { - "registerIUnknownInterface" - } else { - "registerIInspectableInterface" - }; - let registration_name = format!("{}.{}", iface.namespace, name); - let cache_var = format!("_{name}Cache", name = name); - let iface_var = format!("_{name}", name = name); - out.push_str(&format!("let {cache_var};\n", cache_var = cache_var)); - out.push_str(&format!( - "const {iface_var} = new Proxy({{}}, {{\n get(_target, prop) {{\n {cache_var} ??= DynCom.{register_fn}('{registration_name}', IID_{name})\n", - iface_var = iface_var, - cache_var = cache_var, - name = name, - registration_name = registration_name, - register_fn = register_fn, - )); - for m in &iface.methods { - out.push_str(&format!( - " .addMethod('{}', {})\n", - m.name, - build_method_sig_js(m) - )); - } - // Trim trailing newline before closing the block, then close. - if out.ends_with('\n') { - out.truncate(out.len() - 1); - } - out.push_str(";\n"); - out.push_str(&format!( - " const value = {cache_var}[prop];\n return typeof value === 'function' ? value.bind({cache_var}) : value;\n }},\n}});\n", - cache_var = cache_var, - )); - out.push('\n'); - - // Class body - out.push_str(&format!("export class {name} {{\n", name = name)); - out.push_str(" _obj;\n"); - out.push_str(" constructor(obj) { this._obj = obj; }\n"); - out.push_str(&format!( - " static _fromNative(obj) {{ return new {name}(obj); }}\n", - name = name - )); - - if let Some(ref clsid) = meta.coclass_clsid { - // static create() — classic COM CLSID-based activation. - out.push_str(&format!( - " /** Create a new `{name}` via `CoCreateInstance` on `CLSID_{cc}`. */\n", - name = name, - cc = meta.coclass_name.as_deref().unwrap_or("Coclass") - )); - out.push_str(&format!( - " static create() {{\n const _obj = DynCom.coCreateInstance('{clsid}', IID_{name});\n return new {name}(_obj);\n }}\n", - clsid = clsid, - name = name, - )); - } else if let Some(info) = interop { - if !info.class_namespace.is_empty() { - // static create() — interop activation: activate the projected - // WinRT runtime class's factory, then QI to the interop IID. - let full_class_name = format!("{}.{}", info.class_namespace, info.class_name); - out.push_str(&format!( - " /** Create a new `{name}` by activating the `{full_class_name}` factory and QI'ing to the interop. */\n", - name = name, - full_class_name = full_class_name, - )); - out.push_str(&format!( - " static create() {{\n const factory = DynWinRtValue.activationFactory('{full_class_name}');\n const _obj = factory.cast(IID_{name});\n return new {name}(_obj);\n }}\n", - full_class_name = full_class_name, - name = name, - )); - } - } - - // Emit methods: natural interop shape when available, otherwise pass-through. - if let Some(info) = interop { - for im in &info.methods { - emit_interop_method_js(&mut out, im, &iface_var, info); - } - } else { - for m in &iface.methods { - if let Some(natural_params) = method_is_interop_shape(m) { - emit_dynamic_iid_method_js(&mut out, m, &natural_params, &iface_var); - } else { - emit_method_js(&mut out, m, &iface_var); - } - } - } - out.push_str("}\n"); - out -} - -fn unwrap_method_result_js( - method: &MethodMeta, - result: MethodResult<'_>, - expression: &str, -) -> String { - if is_sys_free_string_owned(method, result) { - format!("DynCom.takeBstr({expression})") - } else if is_cotaskmem_owned(method, result) - && !matches!( - result.typ, - TypeMeta::Struct { namespace, name, .. } - if namespace == "Windows.Win32.Foundation" - && (name == "PWSTR" || name == "PSTR") - ) - { - format!("DynCom.adoptCoTaskMemPointer({expression})") - } else { - unwrap_return_js(result.typ, expression) - } -} - -fn validate_untyped_outputs(meta: &ComInterfaceMeta) -> Result<(), String> { - for method in &meta.interface.methods { - for (param_index, param) in method.params.iter().enumerate() { - let is_untyped = - param.direction == ParamDirection::Out && param.typ == TypeMeta::Object; - let is_owned = method.owned_outputs.iter().any(|owned| { - owned.param_index == param_index - && (owned.free_with.contains("CoTaskMemFree") - || (owned.free_with.contains("SysFreeString") && is_bstr(¶m.typ))) - }); - if is_untyped && !is_owned && method_is_interop_shape(method).is_none() { - return Err(format!( - "{}.{}: untyped pointer output has no ownership projection", - meta.interface.name, method.name - )); - } - } - } - Ok(()) -} - -fn emit_dynamic_iid_method_js( - out: &mut String, - method: &MethodMeta, - natural_params: &[ParamMeta], - interface_var: &str, -) { - let mut surface_params = natural_params - .iter() - .enumerate() - .map(|(index, param)| js_param_name(¶m.name, index)) - .collect::>(); - surface_params.push("iid".into()); - let mut args = natural_params - .iter() - .enumerate() - .map(|(index, param)| wrap_arg_js(¶m.typ, &js_param_name(¶m.name, index))) - .collect::>(); - args.push("DynCom.iidPointer(_iid)".into()); - out.push_str(&format!( - " {name}({params}) {{\n", - name = camel_case(&method.name), - params = surface_params.join(", ") - )); - out.push_str(" const _iid = WinGuid.parse(iid);\n"); - out.push_str(&format!( - " const _raw = {interface_var}.method({slot}).invoke(this._obj, [{args}]);\n", - slot = method.vtable_index, - args = args.join(", ") - )); - out.push_str(" return DynCom.adoptComPointer(_raw, _iid);\n"); - out.push_str(" }\n"); -} - -fn build_method_sig_js(m: &MethodMeta) -> String { - let mut parts = Vec::new(); - let string_buffer = string_buffer_pattern(m); - for (idx, p) in m.params.iter().enumerate() { - if p.direction == ParamDirection::In { - parts.push(format!(".addIn({})", ts_type_expr_js(&p.typ))); - } else if p.direction == ParamDirection::InOut { - parts.push(format!(".addInOut({})", ts_type_expr_js(&p.typ))); - } else if matches!(p.direction, ParamDirection::OutStringBuffer { .. }) { - parts.push(".addIn(DynCom.pointerType())".to_string()); - } else if p.direction == ParamDirection::Out { - if string_buffer.is_some_and(|(_, count_idx, _)| { - idx > count_idx && is_optional_find_data_out_after_string_count(p) - }) { - parts.push(".addIn(DynCom.pointerType())".to_string()); - } else { - parts.push(format!(".addOut({})", ts_type_expr_js(&p.typ))); - } - } else if p.direction == ParamDirection::OutFill { - parts.push(format!(".addOutFill({})", ts_type_expr_js(&p.typ))); - } - } - match &m.return_type { - None => parts.push(".returnsVoid()".to_string()), - Some(rt) if is_hresult(rt) && m.preserve_hresult => { - parts.push(".preserveHresult()".to_string()); - } - Some(rt) if !is_hresult(rt) => { - parts.push(format!(".returns({})", ts_type_expr_js(rt))); - } - _ => {} - } - if parts.is_empty() { - "new DynComMethodSig()".to_string() - } else { - format!("new DynComMethodSig(){}", parts.join("")) - } -} - -fn emit_method_js(out: &mut String, m: &MethodMeta, iface_var: &str) { - let camel = camel_case(&m.name); - let in_params: Vec<(usize, &ParamMeta)> = m - .params - .iter() - .enumerate() - .filter(|(_, p)| p.direction.is_input()) - .collect(); - let results = method_results(m); - let has_outfill = m - .params - .iter() - .any(|p| p.direction == ParamDirection::OutFill); - - let param_list: Vec = in_params - .iter() - .enumerate() - .map(|(surface_i, (idx, p))| { - let name = js_param_name(&p.name, surface_i); - if let Some((_, count_idx, _)) = string_buffer_pattern(m) { - if *idx == count_idx && string_buffer_param_is_optional(m, *idx) { - return format!("{name} = 260"); - } - if *idx > count_idx && string_buffer_param_is_optional(m, *idx) { - return format!("{name} = 0"); - } - } - name - }) - .collect(); - - let args_exprs: Vec = in_params - .iter() - .enumerate() - .map(|(i, (_, p))| wrap_arg_js(&p.typ, &js_param_name(&p.name, i))) - .collect(); - - out.push_str(&format!( - " {camel}({params}) {{\n", - camel = camel, - params = param_list.join(", ") - )); - if let Some((buffer_idx, count_idx, encoding)) = string_buffer_pattern(m) { - let count_surface_idx = in_params - .iter() - .position(|(idx, _)| *idx == count_idx) - .expect("count param must be an input"); - let count_name = js_param_name(&m.params[count_idx].name, count_surface_idx); - if encoding == StringEncoding::Ansi { - out.push_str( - " throw new Error('PSTR out buffers are not yet decoded safely');\n", - ); - out.push_str(" }\n"); - return; - } - let args: Vec = m - .params - .iter() - .enumerate() - .filter_map(|(idx, p)| { - if idx == buffer_idx { - Some("DynCom.pointer(_buffer)".to_string()) - } else if p.direction.is_input() { - let surface_idx = in_params - .iter() - .position(|(param_idx, _)| *param_idx == idx) - .expect("input param must have a surface index"); - Some(wrap_arg_js(&p.typ, &js_param_name(&p.name, surface_idx))) - } else if idx > count_idx && is_optional_find_data_out_after_string_count(p) { - Some("DynCom.pointer(0n)".to_string()) - } else { - None - } - }) - .collect(); - out.push_str(&format!( - " {count_name} = _normalizeStringBufferCount({count_name}, '{count_name}');\n" - )); - out.push_str(&format!( - " const _buffer = Buffer.alloc({count_name} * 2);\n" - )); - match results.len() { - 0 => out.push_str(&format!( - " {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", - iface_var = iface_var, - slot = m.vtable_index, - args = args.join(", ") - )), - 1 => out.push_str(&format!( - " const _out = {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", - iface_var = iface_var, - slot = m.vtable_index, - args = args.join(", ") - )), - _ => out.push_str(&format!( - " const _out = {iface_var}.method({slot}).invokeAll(this._obj, [{args}]);\n", - iface_var = iface_var, - slot = m.vtable_index, - args = args.join(", ") - )), - } - out.push_str(" const _text = _decodeWideString(_buffer);\n"); - match results.len() { - 0 => out.push_str(" return _text;\n"), - 1 => out.push_str(&format!( - " return [_text, {}];\n", - unwrap_method_result_js(m, results[0], "_out") - )), - _ => { - let values = results - .iter() - .enumerate() - .map(|(index, result)| { - unwrap_method_result_js(m, *result, &format!("_out[{index}]")) - }) - .collect::>(); - out.push_str(&format!(" return [_text, {}];\n", values.join(", "))); - } - } - out.push_str(" }\n"); - return; - } - // Project trailing `[out]` params as JS return values, mirroring how the - // WinRT codegen already handles out-params (see - // `codegen/javascript/project/methods.rs` — `is_multi_output` / `invokeAll`). - // OutFill (caller-allocated buffers, e.g. GetPath(LPWSTR, cchMax)) are - // NOT projected — see the TODO note below. - if has_outfill { - out.push_str(" // TODO: caller-allocated [out, sizeis] buffers are not yet projected as returns.\n"); - } - match results.len() { - 0 => { - out.push_str(&format!( - " {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", - iface_var = iface_var, - slot = m.vtable_index, - args = args_exprs.join(", ") - )); - } - 1 => { - out.push_str(&format!( - " const _out = {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", - iface_var = iface_var, - slot = m.vtable_index, - args = args_exprs.join(", ") - )); - out.push_str(&format!( - " return {};\n", - unwrap_method_result_js(m, results[0], "_out") - )); - } - _ => { - out.push_str(&format!( - " const _r = {iface_var}.method({slot}).invokeAll(this._obj, [{args}]);\n", - iface_var = iface_var, - slot = m.vtable_index, - args = args_exprs.join(", ") - )); - let items: Vec = results - .iter() - .enumerate() - .map(|(i, result)| unwrap_method_result_js(m, *result, &format!("_r[{i}]"))) - .collect(); - out.push_str(&format!(" return [{}];\n", items.join(", "))); - } - } - out.push_str(" }\n"); -} - -/// Emit an interop method: either natural (hide trailing REFIID + void**) or -/// plain (fall back to the normal classic-COM emission). -fn emit_interop_method_js( - out: &mut String, - im: &InteropMethod, - iface_var: &str, - info: &InteropInfo, -) { - let Some(natural_params) = &im.natural_params else { - if let Some(m) = &im.plain { - if let Some(natural) = method_is_interop_shape(m) { - emit_dynamic_iid_method_js(out, m, &natural, iface_var); - } else { - emit_method_js(out, m, iface_var); - } - } - return; - }; - let param_list: Vec = natural_params - .iter() - .enumerate() - .map(|(i, p)| js_param_name(&p.name, i)) - .collect(); - - let mut arg_exprs: Vec = natural_params - .iter() - .enumerate() - .map(|(i, p)| wrap_arg_js(&p.typ, &js_param_name(&p.name, i))) - .collect(); - - // The synthesised REFIID pointer. When we have a resolved target IID we - // pass the cached pointer; otherwise the method is unusable (still emitted - // for completeness so `.d.ts` doesn't lie about the surface). - let riid_arg = if !info.target_iid.is_empty() { - format!("DynCom.iidPointer(IID_{}_default)", info.class_name) - } else { - "DynCom.pointer(0n)".to_string() - }; - arg_exprs.push(riid_arg); - - out.push_str(&format!( - " {camel}({params}) {{\n", - camel = im.camel, - params = param_list.join(", "), - )); - if !info.target_iid.is_empty() { - out.push_str(&format!( - " const _raw = {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", - iface_var = iface_var, - slot = im.vtable_index, - args = arg_exprs.join(", "), - )); - out.push_str(&format!( - " const _out = DynCom.adoptComPointer(_raw, IID_{cls}_default);\n", - cls = info.class_name, - )); - out.push_str(" return _out;\n"); - } else { - // Fallback: no projection available. Return the raw object. - out.push_str(&format!( - " return {iface_var}.method({slot}).invoke(this._obj, [{args}]);\n", - iface_var = iface_var, - slot = im.vtable_index, - args = arg_exprs.join(", "), - )); - } - out.push_str(" }\n"); -} - -// --------------------------------------------------------------------------- -// .d.ts rendering -// --------------------------------------------------------------------------- - -fn render_dts(meta: &ComInterfaceMeta, interop: Option<&InteropInfo>) -> String { - let iface = &meta.interface; - let name = &iface.name; - - let mut out = String::new(); - out.push_str("// Generated by dynwinrt-codegen — do not edit\n"); - // Import Buffer type hint via `import type` from Node ambient — HWND uses `Buffer`. - // Node.js `Buffer` is a global type; no import needed. We DO import enum types. - for en in enum_import_names(meta) { - out.push_str(&format!("import {{ {} }} from './{}.js';\n", en, en)); - } - if interop.is_some() - || uses_winrt_bridge_value(meta) - || has_dynamic_iid_method(meta) - || has_owned_pointer_output(meta) - { - out.push_str(&format!( - "import type {{ DynWinRtValue }} from '{}';\n", - com_runtime_import_name() - )); - } - out.push('\n'); - - // Emit typedef aliases for handles seen in method parameters. - let handle_aliases = collect_handle_aliases(meta); - for (h, kind) in &handle_aliases { - match kind { - HandleAliasKind::HandleValue => out.push_str(&format!( - "/** Opaque Win32 handle value. Pass a raw pointer value as a `bigint` (full pointer width) or `number` (safe integer). */\nexport type {h} = bigint | number;\n" - )), - HandleAliasKind::DataPointer => out.push_str(&format!( - "/** Opaque native data address. Inputs may also use a `Buffer`/`Uint8Array`, whose backing-store address is passed and retained for the call. */\nexport type {h} = bigint | number;\n" - )), - HandleAliasKind::StringPointer => out.push_str(&format!( - "/** Win32 NUL-terminated string pointer. Pass a `Buffer` holding the string bytes (including the NUL terminator), or pass a raw pointer as `bigint`. */\nexport type {h} = bigint | Buffer;\n" - )), - } - } - if !handle_aliases.is_empty() { - out.push('\n'); - } - - out.push_str(&format!( - "export declare const IID_{name}: unknown;\n\n", - name = name - )); - - out.push_str(&format!("export declare class {name} {{\n", name = name)); - if meta.coclass_clsid.is_some() { - out.push_str(" /** Create a new instance via the coclass activation path. */\n"); - out.push_str(&format!(" static create(): {name};\n", name = name)); - } else if let Some(info) = interop { - if !info.class_namespace.is_empty() { - out.push_str(&format!( - " /** Activate the projected WinRT class and QI to the interop. */\n static create(): {name};\n", - name = name - )); - } - } - out.push_str(&format!( - " /** Wrap an existing native COM pointer (for QueryInterface bridging). */\n static _fromNative(obj: unknown): {name};\n", - name = name - )); - - if let Some(info) = interop { - // Interop methods: NATURAL signatures for interop-shape methods (no - // riid, no void**). Plain methods fall through to the normal - // classic-COM emission. - for im in &info.methods { - match (&im.natural_params, &im.plain) { - (Some(natural), _) => { - let ts_params: Vec = natural - .iter() - .enumerate() - .map(|(i, p)| { - format!( - "{}: {}", - js_param_name(&p.name, i), - ts_input_type_expr_dts(&p.typ) - ) - }) - .collect(); - let ret = "DynWinRtValue"; - out.push_str(&format!( - " {camel}({params}): {ret};\n", - camel = im.camel, - params = ts_params.join(", "), - ret = ret, - )); - } - (None, Some(m)) => { - let camel = camel_case(&m.name); - let (ts_params, ret) = if let Some(natural) = method_is_interop_shape(m) { - let mut params = natural - .iter() - .enumerate() - .map(|(index, param)| { - format!( - "{}: {}", - js_param_name(¶m.name, index), - ts_input_type_expr_dts(¶m.typ) - ) - }) - .collect::>(); - params.push("iid: string".into()); - (params, "DynWinRtValue".to_string()) - } else { - (dts_params_for_method(m), dts_return_type(m)) - }; - out.push_str(&format!( - " {camel}({params}): {ret};\n", - camel = camel, - params = ts_params.join(", "), - ret = ret, - )); - } - _ => {} - } - } - } else { - for m in &iface.methods { - let camel = camel_case(&m.name); - let (ts_params, ret) = if let Some(natural) = method_is_interop_shape(m) { - let mut params = natural - .iter() - .enumerate() - .map(|(index, param)| { - format!( - "{}: {}", - js_param_name(¶m.name, index), - ts_input_type_expr_dts(¶m.typ) - ) - }) - .collect::>(); - params.push("iid: string".into()); - (params, "DynWinRtValue".to_string()) - } else { - (dts_params_for_method(m), dts_return_type(m)) - }; - out.push_str(&format!( - " {camel}({params}): {ret};\n", - camel = camel, - params = ts_params.join(", "), - ret = ret, - )); - } - } - out.push_str("}\n"); - out -} - -fn has_dynamic_iid_method(meta: &ComInterfaceMeta) -> bool { - meta.interface - .methods - .iter() - .any(|method| method_is_interop_shape(method).is_some()) -} - -fn has_owned_pointer_output(meta: &ComInterfaceMeta) -> bool { - meta.interface.methods.iter().any(|method| { - method.owned_outputs.iter().any(|owned| { - owned.free_with.contains("CoTaskMemFree") || owned.free_with.contains("SysFreeString") - }) - }) -} - -fn com_runtime_import_name() -> String { - let import_name = crate::codegen::project::get_import_name(); - if import_name == "@microsoft/dynwinrt" { - format!("{import_name}/com") - } else { - import_name - } -} - -// --------------------------------------------------------------------------- -// Enum sibling files -// --------------------------------------------------------------------------- - -fn render_enum_files(en: &ComEnumMeta) -> (String, String) { - let name = en.name.as_str(); - // .js: a frozen object. - let mut js = String::new(); - js.push_str("// Generated by dynwinrt-codegen — do not edit\n"); - js.push_str(&format!( - "export const {name} = Object.freeze({{\n", - name = name - )); - for member in &en.members { - js.push_str(&format!( - " {}: {},\n", - member.name, - render_enum_value(&member.value, &en.underlying) - )); - } - js.push_str("});\n"); - - // .d.ts: emit a const object + companion type — matches the JS `Object.freeze({...})` - // runtime shape and mirrors the WinRT enum generator (see - // `codegen::javascript::render::declarations::render_enum_dts`). Using `const enum` - // breaks under TS `isolatedModules`, so we intentionally avoid it. - let mut dts = String::new(); - dts.push_str("// Generated by dynwinrt-codegen — do not edit\n"); - dts.push_str(&format!( - "export type {name} = (typeof {name})[keyof typeof {name}];\n", - name = name - )); - dts.push_str(&format!("export declare const {name}: {{\n", name = name)); - for member in &en.members { - dts.push_str(&format!( - " readonly {}: {};\n", - member.name, - render_enum_value(&member.value, &en.underlying) - )); - } - dts.push_str("};\n"); - - (js, dts) -} - -fn render_enum_value(value: &ComEnumValue, underlying: &TypeMeta) -> String { - let suffix = if matches!(underlying, TypeMeta::I64 | TypeMeta::U64) { - "n" - } else { - "" - }; - match value { - ComEnumValue::Signed(value) => format!("{value}{suffix}"), - ComEnumValue::Unsigned(value) => format!("{value}{suffix}"), - } -} - -// --------------------------------------------------------------------------- -// Unit tests (fast, no winmd — pure logic) -// --------------------------------------------------------------------------- - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn camel_case_basic() { - assert_eq!(camel_case("HrInit"), "hrInit"); - assert_eq!(camel_case("SetProgressValue"), "setProgressValue"); - assert_eq!(camel_case("AddTab"), "addTab"); - assert_eq!(camel_case("URL"), "url"); - assert_eq!(camel_case("IOHandle"), "ioHandle"); - } - - #[test] - fn default_runtime_import_uses_com_subpath() { - let previous = crate::codegen::project::get_import_name(); - crate::codegen::project::set_import_name("@microsoft/dynwinrt"); - assert_eq!(com_runtime_import_name(), "@microsoft/dynwinrt/com"); - crate::codegen::project::set_import_name(&previous); - } - - #[test] - fn strip_hungarian_only_at_word_boundary() { - assert_eq!(strip_hungarian("dwReserved"), "Reserved"); - assert_eq!(strip_hungarian("hwndTab"), "Tab"); - // "hwnd" alone must NOT be stripped (no uppercase follow-up). - assert_eq!(strip_hungarian("hwnd"), "hwnd"); - } - - #[test] - fn handle_type_name_recognizes_hwnd_shape() { - let hwnd = TypeMeta::Struct { - namespace: "Windows.Win32.Foundation".into(), - name: "HWND".into(), - fields: vec![crate::types::FieldMeta { - name: "Value".into(), - typ: TypeMeta::Object, - }], - }; - assert_eq!(handle_type_name(&hwnd).as_deref(), Some("HWND")); - } - - #[test] - fn handle_alias_kind_distinguishes_handle_values_from_string_pointers() { - let hwnd = TypeMeta::Struct { - namespace: "Windows.Win32.Foundation".into(), - name: "HWND".into(), - fields: vec![crate::types::FieldMeta { - name: "Value".into(), - typ: TypeMeta::Object, - }], - }; - assert_eq!(handle_alias_kind(&hwnd), Some(HandleAliasKind::HandleValue)); - assert_eq!( - handle_alias_kind(&pwstr_struct()), - Some(HandleAliasKind::StringPointer) - ); - let psid = TypeMeta::Struct { - namespace: "Windows.Win32.Security".into(), - name: "PSID".into(), - fields: vec![crate::types::FieldMeta { - name: "Value".into(), - typ: TypeMeta::Object, - }], - }; - assert_eq!(handle_alias_kind(&psid), Some(HandleAliasKind::DataPointer)); - } - - #[test] - fn hresult_is_not_a_handle() { - let hr = TypeMeta::Struct { - namespace: "Windows.Win32.Foundation".into(), - name: "HRESULT".into(), - fields: vec![crate::types::FieldMeta { - name: "Value".into(), - typ: TypeMeta::I32, - }], - }; - assert!(handle_type_name(&hr).is_none()); - assert!(is_hresult(&hr)); - } - - #[test] - fn non_win32_struct_is_not_a_handle() { - let rect = TypeMeta::Struct { - namespace: "Windows.Foundation".into(), - name: "Rect".into(), - fields: vec![ - crate::types::FieldMeta { - name: "X".into(), - typ: TypeMeta::F32, - }, - crate::types::FieldMeta { - name: "Y".into(), - typ: TypeMeta::F32, - }, - crate::types::FieldMeta { - name: "Width".into(), - typ: TypeMeta::F32, - }, - crate::types::FieldMeta { - name: "Height".into(), - typ: TypeMeta::F32, - }, - ], - }; - assert!(handle_type_name(&rect).is_none()); - } - - // ---- Fix 2 (BOOL → boolean/i32) ---- - - fn win32_bool_struct() -> TypeMeta { - TypeMeta::Struct { - namespace: "Windows.Win32.Foundation".into(), - name: "BOOL".into(), - fields: vec![crate::types::FieldMeta { - name: "Value".into(), - typ: TypeMeta::I32, - }], - } - } - - #[test] - fn win32_bool_is_not_a_handle() { - let b = win32_bool_struct(); - // Sanity: it's the exact shape of a handle (single Value: I32) — the - // special-case must WIN over the generic handle heuristic. - assert!( - handle_type_name(&b).is_none(), - "BOOL must not be emitted as an opaque handle typedef" - ); - } - - #[test] - fn win32_bool_projects_as_boolean_and_i32() { - let b = win32_bool_struct(); - // .d.ts surface: boolean (not `BOOL` or `bigint | Buffer`) - assert_eq!(ts_type_expr_dts(&b), "boolean"); - // .js registration: i32 type (not pointer) - assert_eq!(ts_type_expr_js(&b), "DynCom.i32Type()"); - // .js argument marshalling: truthy→1, falsy→0 as an i32 (not pointer) - assert_eq!( - wrap_arg_js(&b, "fFullscreen"), - "DynCom.i32(fFullscreen ? 1 : 0)" - ); - } - - #[test] - fn hresult_input_projects_as_number_and_i32_value() { - let hr = make_hresult(); - assert_eq!(ts_type_expr_dts(&hr), "number"); - assert_eq!(ts_type_expr_js(&hr), "DynCom.i32Type()"); - assert_eq!(wrap_arg_js(&hr, "hr"), "DynCom.i32(hr)"); - - let m = MethodMeta { - name: "Close".into(), - vtable_index: 4, - params: vec![ParamMeta { - name: "hr".into(), - typ: hr, - direction: ParamDirection::In, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - let com = plain_iface_with_method(m); - let js = render_js(&com, None); - let dts = render_dts(&com, None); - assert!( - js.contains(".addMethod('Close', new DynComMethodSig().addIn(DynCom.i32Type()))"), - ".js must register HRESULT in-param as i32:\n{}", - js - ); - assert!( - js.contains("DynCom.i32(hr)"), - ".js must pass HRESULT by value as i32:\n{}", - js - ); - assert!( - !js.contains("DynCom.pointer(hr)"), - ".js must not pass HRESULT as a pointer:\n{}", - js - ); - assert!( - dts.contains("close(hr: number): void;"), - ".d.ts must type HRESULT in-param as number:\n{}", - dts - ); - assert!( - !dts.contains("HRESULT"), - ".d.ts must not expose an undefined HRESULT alias:\n{}", - dts - ); - } - - // ---- Fix 3 (REFIID-guarded interop heuristic) ---- - - /// Helper: construct a MethodMeta with HRESULT return type. - fn make_hresult() -> TypeMeta { - TypeMeta::Struct { - namespace: "Windows.Win32.Foundation".into(), - name: "HRESULT".into(), - fields: vec![crate::types::FieldMeta { - name: "Value".into(), - typ: TypeMeta::I32, - }], - } - } - - #[test] - fn interop_shape_accepts_riid_named_object_trailing_in() { - // Real Windows.Win32 shape: `HRESULT GetForWindow(HWND appWindow, REFIID riid, out void** ppv)`. - // REFIID typically projects to TypeMeta::Object with name "riid". - let m = MethodMeta { - name: "GetForWindow".into(), - vtable_index: 3, - params: vec![ - ParamMeta { - name: "appWindow".into(), - typ: TypeMeta::Object, - direction: ParamDirection::In, - }, - ParamMeta { - name: "riid".into(), - typ: TypeMeta::Object, - direction: ParamDirection::In, - }, - ParamMeta { - name: "ppv".into(), - typ: TypeMeta::Object, - direction: ParamDirection::Out, - }, - ], - return_type: Some(make_hresult()), - ..Default::default() - }; - let natural = method_is_interop_shape(&m) - .expect("REFIID-shaped trailing in-param named `riid` must be recognised as interop"); - // Natural in-params = every in EXCEPT the trailing REFIID. - assert_eq!(natural.len(), 1); - assert_eq!(natural[0].name, "appWindow"); - } - - #[test] - fn interop_shape_rejects_guid_passed_by_value() { - let m = MethodMeta { - name: "GetSomething".into(), - vtable_index: 3, - params: vec![ - ParamMeta { - name: "target".into(), - typ: TypeMeta::Object, - direction: ParamDirection::In, - }, - ParamMeta { - name: "riid".into(), - typ: TypeMeta::Guid, - direction: ParamDirection::In, - }, - ParamMeta { - name: "out".into(), - typ: TypeMeta::Object, - direction: ParamDirection::Out, - }, - ], - return_type: Some(make_hresult()), - ..Default::default() - }; - assert!( - method_is_interop_shape(&m).is_none(), - "a by-value GUID must not be passed as a REFIID pointer" - ); - } - - /// FIX 3 REGRESSION: a method returning HRESULT with an [out] Object and a - /// trailing In-Object whose name is NOT `riid`/`iid` (e.g. a real application - /// COM interface pointer like `original`) must NOT be mis-classified as - /// interop-shape. Otherwise the codegen would silently drop the caller's - /// meaningful argument. - #[test] - fn interop_shape_rejects_non_refiid_trailing_object() { - let m = MethodMeta { - name: "CloneWithOriginal".into(), - vtable_index: 3, - params: vec![ - ParamMeta { - name: "context".into(), - typ: TypeMeta::Object, - direction: ParamDirection::In, - }, - ParamMeta { - // NOT `riid`/`iid`, NOT Guid — a real COM pointer in-param. - name: "original".into(), - typ: TypeMeta::Object, - direction: ParamDirection::In, - }, - ParamMeta { - name: "cloned".into(), - typ: TypeMeta::Object, - direction: ParamDirection::Out, - }, - ], - return_type: Some(make_hresult()), - ..Default::default() - }; - assert!( - method_is_interop_shape(&m).is_none(), - "trailing in-param `original` is a real Object argument, NOT a REFIID — \ - it must not be dropped by the interop heuristic" - ); - } - - #[test] - fn interop_shape_rejects_iid_named_non_object_param() { - // A parameter named `riid` but typed as a plain I32 is not a REFIID — - // reject rather than silently drop. - let m = MethodMeta { - name: "Weird".into(), - vtable_index: 3, - params: vec![ - ParamMeta { - name: "hwnd".into(), - typ: TypeMeta::Object, - direction: ParamDirection::In, - }, - ParamMeta { - name: "riid".into(), - typ: TypeMeta::I32, - direction: ParamDirection::In, - }, - ParamMeta { - name: "out".into(), - typ: TypeMeta::Object, - direction: ParamDirection::Out, - }, - ], - return_type: Some(make_hresult()), - ..Default::default() - }; - assert!( - method_is_interop_shape(&m).is_none(), - "an I32 named `riid` is not a REFIID — must be rejected" - ); - } - - // ---- Fix 1 (winmd-derived interop IID, fail-loud on unresolved) ---- - - /// Build a fully synthetic ComInterfaceMeta for an `IFooInterop`-style - /// interface whose derived projected class name (`Foo`) does NOT exist - /// anywhere reachable. The generator must FAIL LOUDLY rather than emit - /// a NULL riid. - #[test] - fn interop_generation_fails_when_target_iid_unresolvable() { - use crate::com_metadata::{ComInterfaceMeta, InterfaceMeta}; - - let iface = InterfaceMeta { - name: "IThisRuntimeClassDoesNotExist_DynWinrtInterop".into(), - namespace: "Windows.Win32.System.WinRT".into(), - iid: "00000000-0000-0000-0000-000000000000".into(), - methods: vec![MethodMeta { - name: "GetForWindow".into(), - vtable_index: 3, - params: vec![ - ParamMeta { - name: "appWindow".into(), - typ: TypeMeta::Object, - direction: ParamDirection::In, - }, - ParamMeta { - name: "riid".into(), - typ: TypeMeta::Object, - direction: ParamDirection::In, - }, - ParamMeta { - name: "ppv".into(), - typ: TypeMeta::Object, - direction: ParamDirection::Out, - }, - ], - return_type: Some(make_hresult()), - ..Default::default() - }], - generic_piid: None, - generic_args: Vec::new(), - doc: None, - deprecated: None, - }; - let com = ComInterfaceMeta { - interface: iface, - base_offset: 3, - is_iunknown_rooted: true, - base_chain: vec!["IUnknown".into()], - coclass_clsid: None, - coclass_name: None, - own_methods_start: 3, - referenced_enums: Vec::new(), - }; - // Pass empty winmd_paths — even with the newest-SDK fallback, the - // synthetic class name won't be found anywhere. - let result = generate_com_interface_files(&com, ""); - assert!( - result.is_err(), - "generator must fail loudly when the projected runtime-class IID \ - cannot be resolved; got Ok(_)" - ); - let err = result.unwrap_err(); - assert!( - err.contains("ThisRuntimeClassDoesNotExist_Dynwinrt") - || err.contains("ThisRuntimeClassDoesNotExist_DynWinrt"), - "error must name the class it failed to resolve: {}", - err - ); - assert!( - !err.is_empty(), - "error message must be non-empty (fail-loud contract)" - ); - } - - #[test] - fn non_interop_iunknown_interface_still_generates_without_winmd_lookup() { - // A vanilla IUnknown-rooted interface with no coclass and no - // interop shape must succeed even when we pass empty winmd paths. - use crate::com_metadata::{ComInterfaceMeta, InterfaceMeta}; - let iface = InterfaceMeta { - name: "IMyPlainClassicCom".into(), - namespace: "Windows.Win32.System.Com".into(), - iid: "11111111-2222-3333-4444-555555555555".into(), - methods: vec![MethodMeta { - name: "DoStuff".into(), - vtable_index: 3, - params: vec![], - return_type: Some(make_hresult()), - ..Default::default() - }], - generic_piid: None, - generic_args: Vec::new(), - doc: None, - deprecated: None, - }; - let com = ComInterfaceMeta { - interface: iface, - base_offset: 3, - is_iunknown_rooted: true, - base_chain: vec!["IUnknown".into()], - coclass_clsid: None, - coclass_name: None, - own_methods_start: 3, - referenced_enums: Vec::new(), - }; - let out = generate_com_interface_files(&com, "") - .expect("plain classic-COM codegen must succeed with no winmds"); - assert!(out.js.contains("DynCom.registerIUnknownInterface")); - assert!(out.js.contains("method(3)")); - } - - // ---- Fix 4 (classic-COM plain `[out]` param → return-value projection) ---- - - fn plain_iface_with_method(m: MethodMeta) -> crate::com_metadata::ComInterfaceMeta { - use crate::com_metadata::{ComInterfaceMeta, InterfaceMeta}; - let iface = InterfaceMeta { - name: "IHasOut".into(), - namespace: "Windows.Win32.System.Com".into(), - iid: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee".into(), - methods: vec![m], - generic_piid: None, - generic_args: Vec::new(), - doc: None, - deprecated: None, - }; - ComInterfaceMeta { - interface: iface, - base_offset: 3, - is_iunknown_rooted: true, - base_chain: vec!["IUnknown".into()], - coclass_clsid: None, - coclass_name: None, - own_methods_start: 3, - referenced_enums: Vec::new(), - } - } - - #[test] - fn unsupported_struct_in_out_fails_closed() { - let method = MethodMeta { - name: "Read".into(), - params: vec![ParamMeta { - name: "value".into(), - typ: TypeMeta::Struct { - namespace: "Windows.Win32.System.Com".into(), - name: "VARIANT".into(), - fields: vec![ - crate::types::FieldMeta { - name: "vt".into(), - typ: TypeMeta::U16, - }, - crate::types::FieldMeta { - name: "data".into(), - typ: TypeMeta::U64, - }, - ], - }, - direction: ParamDirection::InOut, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - - let error = generate_com_interface_files(&plain_iface_with_method(method), "") - .expect_err("unsupported struct in/out must not emit a wrong T** ABI"); - assert!(error.contains("requires native layout projection")); - } - - #[test] - fn unsupported_by_value_struct_fails_closed() { - let method = MethodMeta { - name: "DragEnter".into(), - params: vec![ParamMeta { - name: "point".into(), - typ: TypeMeta::Struct { - namespace: "Windows.Win32.Foundation".into(), - name: "POINTL".into(), - fields: vec![ - crate::types::FieldMeta { - name: "x".into(), - typ: TypeMeta::I32, - }, - crate::types::FieldMeta { - name: "y".into(), - typ: TypeMeta::I32, - }, - ], - }, - direction: ParamDirection::In, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - let error = generate_com_interface_files(&plain_iface_with_method(method), "") - .expect_err("struct layout must fail closed"); - assert!(error.contains("requires native layout projection")); - } - - #[test] - fn unsupported_struct_direct_return_fails_closed() { - let method = MethodMeta { - name: "GetPoint".into(), - return_type: Some(TypeMeta::Struct { - namespace: "Windows.Win32.Foundation".into(), - name: "POINT".into(), - fields: vec![ - crate::types::FieldMeta { - name: "x".into(), - typ: TypeMeta::I32, - }, - crate::types::FieldMeta { - name: "y".into(), - typ: TypeMeta::I32, - }, - ], - }), - ..Default::default() - }; - - let error = generate_com_interface_files(&plain_iface_with_method(method), "") - .expect_err("unsupported struct return must not panic at invocation time"); - assert!(error.contains("unsupported direct native return")); - } - - #[test] - fn plain_method_single_out_scalar_projects_as_return() { - // Model: `HRESULT GetShowCmd([out] int* pcmd)` — the classic single-out - // int shape. The out-int must become the method's return value. - let m = MethodMeta { - name: "GetShowCmd".into(), - vtable_index: 8, - params: vec![ParamMeta { - name: "pcmd".into(), - typ: TypeMeta::I32, - direction: ParamDirection::Out, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - let com = plain_iface_with_method(m); - let js = render_js(&com, None); - let dts = render_dts(&com, None); - // .js: must capture `_out` and return it as a JS number. - assert!( - js.contains("const _out = _IHasOut.method(8).invoke(this._obj, [])"), - ".js must capture invoke() result into _out:\n{}", - js - ); - assert!( - js.contains("return DynCom.toNumber(_out);"), - ".js must unwrap the I32 out:\n{}", - js - ); - // .d.ts: return type must be `number`, not `void`. - assert!( - dts.contains("getShowCmd(): number;"), - ".d.ts must project single-out I32 as `number`:\n{}", - dts - ); - } - - #[test] - fn plain_method_single_out_guid_projects_as_string() { - // Model: `HRESULT GetClassID([out] GUID* pClassID)` (IPersist shape). - let m = MethodMeta { - name: "GetClassID".into(), - vtable_index: 3, - params: vec![ParamMeta { - name: "pClassID".into(), - typ: TypeMeta::Guid, - direction: ParamDirection::Out, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - let com = plain_iface_with_method(m); - let js = render_js(&com, None); - let dts = render_dts(&com, None); - assert!( - js.contains("const _out = _IHasOut.method(3).invoke(this._obj, [])"), - ".js must capture invoke() result into _out:\n{}", - js - ); - assert!( - js.contains("return DynCom.toGuidString(_out);"), - ".js must unwrap GUID out:\n{}", - js - ); - assert!( - dts.contains("getClassID(): string;"), - ".d.ts must project single-out GUID as `string`:\n{}", - dts - ); - } - - #[test] - fn plain_method_single_out_enum_projects_as_underlying() { - // Model: `HRESULT GetKind([out] MyKind* pk)` where MyKind is an I32 - // enum. Underlying-scalar unwrap → `.toNumber()`; .d.ts uses the enum - // type name. - let m = MethodMeta { - name: "GetKind".into(), - vtable_index: 5, - params: vec![ParamMeta { - name: "pk".into(), - typ: TypeMeta::Enum { - namespace: "Windows.Win32.System.Com".into(), - name: "MyKind".into(), - underlying: Box::new(TypeMeta::I32), - members: Vec::new(), - is_flags: false, - doc: None, - deprecated: None, - }, - direction: ParamDirection::Out, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - let com = plain_iface_with_method(m); - let js = render_js(&com, None); - let dts = render_dts(&com, None); - assert!( - js.contains("return DynCom.toNumber(_out);"), - ".js must unwrap enum out via its underlying scalar:\n{}", - js - ); - assert!( - dts.contains("getKind(): MyKind;"), - ".d.ts must project enum out under the enum's declared name:\n{}", - dts - ); - } - - #[test] - fn plain_method_multi_out_uses_invoke_all_and_tuple_return() { - // Model: `HRESULT Q([out] uint32_t* a, [out] BOOL* found)` — two - // trailing out params must flip to `.invokeAll()` and a tuple return. - let m = MethodMeta { - name: "Q".into(), - vtable_index: 6, - params: vec![ - ParamMeta { - name: "a".into(), - typ: TypeMeta::U32, - direction: ParamDirection::Out, - }, - ParamMeta { - name: "found".into(), - typ: TypeMeta::Bool, - direction: ParamDirection::Out, - }, - ], - return_type: Some(make_hresult()), - ..Default::default() - }; - let com = plain_iface_with_method(m); - let js = render_js(&com, None); - let dts = render_dts(&com, None); - assert!( - js.contains("const _r = _IHasOut.method(6).invokeAll(this._obj, [])"), - ".js multi-out must use .invokeAll():\n{}", - js - ); - assert!( - js.contains("return [DynCom.toU32(_r[0]), DynCom.toBool(_r[1])];"), - ".js multi-out must return a tuple with each out unwrapped:\n{}", - js - ); - assert!( - dts.contains("q(): [number, boolean];"), - ".d.ts multi-out must project a tuple type:\n{}", - dts - ); - } - - #[test] - fn plain_method_zero_out_still_discards_result() { - // No out params: existing behavior — invoke and discard. - let m = MethodMeta { - name: "DoIt".into(), - vtable_index: 4, - params: vec![ParamMeta { - name: "arg".into(), - typ: TypeMeta::I32, - direction: ParamDirection::In, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - let com = plain_iface_with_method(m); - let js = render_js(&com, None); - let dts = render_dts(&com, None); - assert!( - !js.contains("const _out ="), - ".js zero-out must not capture invoke() result:\n{}", - js - ); - assert!( - !js.contains("invokeAll"), - ".js zero-out must not use .invokeAll():\n{}", - js - ); - assert!( - js.contains("_IHasOut.method(4).invoke(this._obj,"), - ".js zero-out must call plain .invoke():\n{}", - js - ); - assert!( - dts.contains("doIt(arg: number): void;"), - ".d.ts zero-out must still be `void`:\n{}", - dts - ); - } - - #[test] - fn direct_native_return_uses_return_abi_instead_of_synthetic_out_param() { - let method = MethodMeta { - name: "RetryRejectedCall".into(), - vtable_index: 5, - return_type: Some(TypeMeta::U32), - ..Default::default() - }; - let signature = build_method_sig_js(&method); - assert_eq!(signature, "new DynComMethodSig().returns(DynCom.u32Type())"); - - let com = plain_iface_with_method(method); - let js = render_js(&com, None); - let dts = render_dts(&com, None); - assert!(js.contains("const _out = _IHasOut.method(5).invoke(this._obj, [])")); - assert!(js.contains("return DynCom.toU32(_out);")); - assert!(dts.contains("retryRejectedCall(): number;")); - } - - #[test] - fn native_void_return_is_declared_explicitly() { - let method = MethodMeta { - name: "OnClose".into(), - vtable_index: 8, - return_type: None, - ..Default::default() - }; - assert_eq!( - build_method_sig_js(&method), - "new DynComMethodSig().returnsVoid()" - ); - - let com = plain_iface_with_method(method); - let js = render_js(&com, None); - assert!(js.contains("_IHasOut.method(8).invoke(this._obj, [])")); - assert!(!js.contains("const _out =")); - } - - #[test] - fn direct_64_bit_returns_use_bigint_accessors() { - let i64_method = MethodMeta { - name: "GetSigned".into(), - return_type: Some(TypeMeta::I64), - ..Default::default() - }; - let u64_method = MethodMeta { - name: "GetUnsigned".into(), - return_type: Some(TypeMeta::U64), - ..Default::default() - }; - - let i64_js = render_js(&plain_iface_with_method(i64_method), None); - let u64_js = render_js(&plain_iface_with_method(u64_method), None); - assert!(i64_js.contains("return DynCom.toI64Bigint(_out);")); - assert!(u64_js.contains("return DynCom.toU64Bigint(_out);")); - } - - #[test] - fn return_only_handle_declares_its_alias() { - let hwnd = TypeMeta::Struct { - namespace: "Windows.Win32.Foundation".into(), - name: "HWND".into(), - fields: vec![crate::types::FieldMeta { - name: "Value".into(), - typ: TypeMeta::Object, - }], - }; - let method = MethodMeta { - name: "GetWindow".into(), - return_type: Some(hwnd), - ..Default::default() - }; - let com = plain_iface_with_method(method); - let dts = render_dts(&com, None); - assert!(dts.contains("export type HWND = bigint | number;")); - assert!(dts.contains("getWindow(): HWND;")); - } - - #[test] - fn handle_value_arg_accepts_buffer_and_string_pointer_keeps_buffer() { - let hwnd = TypeMeta::Struct { - namespace: "Windows.Win32.Foundation".into(), - name: "HWND".into(), - fields: vec![crate::types::FieldMeta { - name: "Value".into(), - typ: TypeMeta::Object, - }], - }; - let method = MethodMeta { - name: "SetOverlayIcon".into(), - params: vec![ - ParamMeta { - name: "hwnd".into(), - typ: hwnd, - direction: ParamDirection::In, - }, - ParamMeta { - name: "description".into(), - typ: pwstr_struct(), - direction: ParamDirection::In, - }, - ], - return_type: Some(make_hresult()), - ..Default::default() - }; - let iface = plain_iface_with_method(method); - let dts = render_dts(&iface, None); - let js = render_js(&iface, None); - - // HWND inputs accept Electron's pointer-width Buffer, but the HWND - // output alias remains a numeric handle value. - assert!(dts.contains("export type HWND = bigint | number;")); - assert!(dts.contains("export type PWSTR = bigint | Buffer;")); - assert!(dts.contains("Pass a `Buffer` holding the string bytes")); - assert!(dts.contains( - "setOverlayIcon(hwnd: HWND | Buffer | Uint8Array, description: PWSTR): void;" - )); - - // Handle-value conversion is centralized in the runtime; string - // pointers continue to pass their backing-store address. - assert!( - js.contains("DynCom.pointer(DynCom.handleValue(hwnd))"), - "HWND arg must use DynCom.handleValue:\n{js}" - ); - assert!(!js.contains("function _handleArg(")); - assert!(!js.contains("handleValue(description)")); - } - - #[test] - fn data_pointer_alias_does_not_read_buffer_contents_as_a_handle() { - let psid = TypeMeta::Struct { - namespace: "Windows.Win32.Security".into(), - name: "PSID".into(), - fields: vec![crate::types::FieldMeta { - name: "Value".into(), - typ: TypeMeta::Object, - }], - }; - let method = MethodMeta { - name: "AddUserSid".into(), - params: vec![ParamMeta { - name: "userSid".into(), - typ: psid, - direction: ParamDirection::In, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - let iface = plain_iface_with_method(method); - let js = render_js(&iface, None); - let dts = render_dts(&iface, None); - - assert!(dts.contains("export type PSID = bigint | number;")); - assert!(dts.contains("addUserSid(userSid: PSID | Buffer | Uint8Array): void;")); - assert!(js.contains("DynCom.pointer(userSid)")); - assert!(!js.contains("handleValue(userSid)")); - } - - #[test] - fn hwnd_in_out_uses_runtime_handle_conversion_without_inline_helper() { - let hwnd = TypeMeta::Struct { - namespace: "Windows.Win32.Foundation".into(), - name: "HWND".into(), - fields: vec![crate::types::FieldMeta { - name: "Value".into(), - typ: TypeMeta::Object, - }], - }; - let method = MethodMeta { - name: "Create".into(), - params: vec![ParamMeta { - name: "window".into(), - typ: hwnd, - direction: ParamDirection::InOut, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - let iface = plain_iface_with_method(method); - let js = render_js(&iface, None); - - assert!(js.contains("DynCom.pointer(DynCom.handleValue(window))")); - assert!(!js.contains("function _handleArg(")); - } - - #[test] - fn return_only_enum_emits_import_and_sibling_files() { - let kind = TypeMeta::Enum { - namespace: "Windows.Win32.Example".into(), - name: "THING_KIND".into(), - underlying: Box::new(TypeMeta::I32), - members: Vec::new(), - is_flags: false, - doc: None, - deprecated: None, - }; - let method = MethodMeta { - name: "GetKind".into(), - return_type: Some(kind.clone()), - ..Default::default() - }; - let mut com = plain_iface_with_method(method); - com.referenced_enums.push(ComEnumMeta { - namespace: "Windows.Win32.Example".into(), - name: "THING_KIND".into(), - underlying: TypeMeta::I32, - members: Vec::new(), - is_flags: false, - }); - - let output = generate_com_interface_files(&com, "").unwrap(); - assert!( - output - .dts - .contains("import { THING_KIND } from './THING_KIND.js';") - ); - assert!( - output - .extra_files - .iter() - .any(|(name, _)| name == "THING_KIND.d.ts") - ); - } - - #[test] - fn unsigned_enum_literals_preserve_u32_and_u64_values() { - let u32_enum = ComEnumMeta { - namespace: "Windows.Win32.Example".into(), - name: "U32_FLAGS".into(), - underlying: TypeMeta::U32, - members: vec![crate::com_metadata::ComEnumMember { - name: "HIGH_BIT".into(), - value: ComEnumValue::Unsigned(2_147_483_648), - }], - is_flags: true, - }; - let u64_enum = ComEnumMeta { - namespace: "Windows.Win32.Example".into(), - name: "U64_FLAGS".into(), - underlying: TypeMeta::U64, - members: vec![crate::com_metadata::ComEnumMember { - name: "HIGH_BIT".into(), - value: ComEnumValue::Unsigned(9_223_372_036_854_775_808), - }], - is_flags: true, - }; - - let (u32_js, u32_dts) = render_enum_files(&u32_enum); - assert!(u32_js.contains("HIGH_BIT: 2147483648")); - assert!(u32_dts.contains("readonly HIGH_BIT: 2147483648;")); - let (u64_js, u64_dts) = render_enum_files(&u64_enum); - assert!(u64_js.contains("HIGH_BIT: 9223372036854775808n")); - assert!(u64_dts.contains("readonly HIGH_BIT: 9223372036854775808n;")); - } - - #[test] - fn in_out_parameter_is_both_argument_and_result() { - let method = MethodMeta { - name: "Adjust".into(), - vtable_index: 4, - params: vec![ParamMeta { - name: "value".into(), - typ: TypeMeta::I32, - direction: ParamDirection::InOut, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - assert_eq!( - build_method_sig_js(&method), - "new DynComMethodSig().addInOut(DynCom.i32Type())" - ); - - let com = plain_iface_with_method(method); - let js = render_js(&com, None); - let dts = render_dts(&com, None); - assert!(js.contains("adjust(value)")); - assert!( - js.contains("const _out = _IHasOut.method(4).invoke(this._obj, [DynCom.i32(value)])") - ); - assert!(js.contains("return DynCom.toNumber(_out);")); - assert!(dts.contains("adjust(value: number): number;")); - } - - #[test] - fn unsupported_outfill_fails_closed() { - let m = MethodMeta { - name: "GetPath".into(), - vtable_index: 2, - params: vec![ - ParamMeta { - name: "pszFile".into(), - typ: TypeMeta::String, // PWSTR buffer, caller-allocated - direction: ParamDirection::OutFill, - }, - ParamMeta { - name: "cch".into(), - typ: TypeMeta::I32, - direction: ParamDirection::In, - }, - ], - return_type: Some(make_hresult()), - ..Default::default() - }; - let com = plain_iface_with_method(m); - let error = generate_com_interface_files(&com, "") - .expect_err("unsupported caller-allocated arrays must fail closed"); - assert!(error.contains("caller-allocated array outputs are not supported")); - } - - fn pwstr_struct() -> TypeMeta { - TypeMeta::Struct { - namespace: "Windows.Win32.Foundation".into(), - name: "PWSTR".into(), - fields: vec![crate::types::FieldMeta { - name: "Value".into(), - typ: TypeMeta::Object, - }], - } - } - - #[test] - fn out_string_buffer_allocates_decodes_and_returns_string() { - let m = MethodMeta { - name: "GetDescription".into(), - vtable_index: 6, - params: vec![ - ParamMeta { - name: "pszName".into(), - typ: pwstr_struct(), - direction: ParamDirection::OutStringBuffer { - count_param_index: 1, - }, - }, - ParamMeta { - name: "cch".into(), - typ: TypeMeta::I32, - direction: ParamDirection::In, - }, - ], - return_type: Some(make_hresult()), - ..Default::default() - }; - let com = plain_iface_with_method(m); - let js = render_js(&com, None); - let dts = render_dts(&com, None); - assert!( - js.contains("function _normalizeStringBufferCount"), - ".js must emit string buffer validation helper:\n{}", - js - ); - assert!( - js.contains(".addMethod('GetDescription', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type()))"), - ".js must register string buffer as an input pointer:\n{}", - js - ); - assert!( - js.contains("getDescription(cch = 260)") && js.contains("Buffer.alloc(cch * 2)"), - ".js must default cch and allocate a UTF-16 buffer:\n{}", - js - ); - assert!( - js.contains("const _text = _decodeWideString(_buffer);") - && js.contains("return _text;"), - ".js must return the decoded wide string:\n{}", - js - ); - assert!( - dts.contains("getDescription(cch?: number): string;"), - ".d.ts must expose optional count and string return:\n{}", - dts - ); - } - - #[test] - fn callee_allocated_pwstr_is_decoded_and_freed() { - let method = MethodMeta { - name: "GetDisplayName".into(), - vtable_index: 5, - params: vec![ParamMeta { - name: "name".into(), - typ: pwstr_struct(), - direction: ParamDirection::Out, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - let com = plain_iface_with_method(method); - let js = render_js(&com, None); - let dts = render_dts(&com, None); - - assert!(js.contains("return DynCom.takeCoTaskMemWideString(_out);")); - assert!(dts.contains("getDisplayName(): string;")); - } - - #[test] - fn untyped_sysfree_output_fails_closed() { - let method = MethodMeta { - name: "GetAllFileTypes".into(), - vtable_index: 4, - params: vec![ParamMeta { - name: "types".into(), - typ: TypeMeta::Object, - direction: ParamDirection::Out, - }], - return_type: Some(make_hresult()), - owned_outputs: vec![crate::com_metadata::OwnedOutput { - param_index: 0, - free_with: "SysFreeString".into(), - }], - ..Default::default() - }; - let error = generate_com_interface_files(&plain_iface_with_method(method), "") - .expect_err("BSTR**-style untyped outputs must fail closed"); - assert!(error.contains("untyped pointer output has no ownership projection")); - } - - #[test] - fn string_buffer_preserves_additional_outputs() { - let method = MethodMeta { - name: "GetIconLocation".into(), - vtable_index: 16, - params: vec![ - ParamMeta { - name: "path".into(), - typ: pwstr_struct(), - direction: ParamDirection::OutStringBuffer { - count_param_index: 1, - }, - }, - ParamMeta { - name: "cch".into(), - typ: TypeMeta::I32, - direction: ParamDirection::In, - }, - ParamMeta { - name: "icon".into(), - typ: TypeMeta::I32, - direction: ParamDirection::Out, - }, - ], - return_type: Some(make_hresult()), - ..Default::default() - }; - let com = plain_iface_with_method(method); - let js = render_js(&com, None); - let dts = render_dts(&com, None); - - assert!(js.contains("const _out = _IHasOut.method(16).invoke")); - assert!(js.contains("return [_text, DynCom.toNumber(_out)];")); - assert!(dts.contains("getIconLocation(cch?: number): [string, number];")); - } - - #[test] - fn interface_out_param_projects_as_explicit_bridge_value() { - let m = MethodMeta { - name: "GetThing".into(), - vtable_index: 7, - params: vec![ParamMeta { - name: "thing".into(), - typ: TypeMeta::Interface { - namespace: "Windows.Win32.System.Com".into(), - name: "IThing".into(), - iid: "11111111-2222-3333-4444-555555555555".into(), - }, - direction: ParamDirection::Out, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - let com = plain_iface_with_method(m); - let js = render_js(&com, None); - let dts = render_dts(&com, None); - assert!( - !js.contains("from './IThing.js'"), - ".js must not depend on an ungenerated wrapper:\n{}", - js - ); - assert!(js.contains( - ".addOut(DynCom.interfaceType(WinGuid.parse('11111111-2222-3333-4444-555555555555')))" - )); - assert!( - js.contains("return _out;"), - ".js must return the managed bridge value:\n{}", - js - ); - assert!( - dts.contains("import type { DynWinRtValue }"), - ".d.ts must import the bridge type:\n{}", - dts - ); - assert!( - dts.contains("getThing(): DynWinRtValue;"), - ".d.ts must return the explicit bridge value:\n{}", - dts - ); - } - - #[test] - fn caller_supplied_riid_output_is_adopted() { - let method = MethodMeta { - name: "BindToHandler".into(), - vtable_index: 4, - params: vec![ - ParamMeta { - name: "pbc".into(), - typ: TypeMeta::Object, - direction: ParamDirection::In, - }, - ParamMeta { - name: "riid".into(), - typ: TypeMeta::Object, - direction: ParamDirection::In, - }, - ParamMeta { - name: "ppv".into(), - typ: TypeMeta::Object, - direction: ParamDirection::Out, - }, - ], - return_type: Some(make_hresult()), - ..Default::default() - }; - let com = plain_iface_with_method(method); - let output = generate_com_interface_files(&com, "").unwrap(); - - assert!(output.js.contains("bindToHandler(pbc, iid)")); - assert!(output.js.contains("DynCom.adoptComPointer(_raw, _iid)")); - assert!( - output - .dts - .contains("bindToHandler(pbc: bigint | Buffer, iid: string): DynWinRtValue;") - ); - } - - #[test] - fn hstring_output_uses_owned_hstring_projection() { - let method = MethodMeta { - name: "get_CorrelationVector".into(), - vtable_index: 3, - params: vec![ParamMeta { - name: "cv".into(), - typ: TypeMeta::String, - direction: ParamDirection::Out, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - - let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); - - assert!(output.js.contains(".addOut(DynCom.hstringType())")); - assert!(output.js.contains("return _out.toString();")); - assert!(output.dts.contains("get_CorrelationVector(): string;")); - } - - #[test] - fn unresolved_interface_iid_fails_closed() { - let method = MethodMeta { - name: "CreateSurface".into(), - vtable_index: 3, - params: vec![ParamMeta { - name: "result".into(), - typ: TypeMeta::Interface { - namespace: "Windows.UI.Composition".into(), - name: "ICompositionSurface".into(), - iid: String::new(), - }, - direction: ParamDirection::Out, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - - let error = generate_com_interface_files(&plain_iface_with_method(method), "") - .expect_err("an unresolved interface must not degrade to a raw pointer"); - - assert!(error.contains("ICompositionSurface")); - assert!(error.contains("no resolvable IID")); - assert!(error.contains("--ref")); - } - - #[test] - fn parameterized_interface_fails_closed_even_with_a_piid() { - let method = MethodMeta { - name: "GetItems".into(), - vtable_index: 3, - params: vec![ParamMeta { - name: "result".into(), - typ: TypeMeta::Parameterized { - namespace: "Windows.Foundation.Collections".into(), - name: "IVectorView`1".into(), - piid: "bbe1fa4c-b0e3-4583-baef-1f1b2e483e56".into(), - args: vec![TypeMeta::String], - }, - direction: ParamDirection::Out, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - - let error = generate_com_interface_files(&plain_iface_with_method(method), "") - .expect_err("a PIID alone is not a closed interface IID"); - - assert!(error.contains("computed closed IID")); - assert!(error.contains("raw-pointer fallback is not allowed")); - } - - #[test] - fn async_interface_fails_closed_without_a_closed_iid() { - let method = MethodMeta { - name: "OpenAsync".into(), - vtable_index: 3, - params: vec![ParamMeta { - name: "result".into(), - typ: TypeMeta::AsyncOperation(Box::new(TypeMeta::String)), - direction: ParamDirection::Out, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - - let error = generate_com_interface_files(&plain_iface_with_method(method), "") - .expect_err("async interfaces must not degrade to raw pointers"); - - assert!(error.contains("async interface requires a computed closed IID")); - assert!(error.contains("raw-pointer fallback is not allowed")); - } - - #[test] - fn native_array_fails_closed_without_count_and_ownership() { - let method = MethodMeta { - name: "GetItems".into(), - vtable_index: 3, - params: vec![ParamMeta { - name: "result".into(), - typ: TypeMeta::Array(Box::new(TypeMeta::Interface { - namespace: "Contoso".into(), - name: "IItem".into(), - iid: "11111111-2222-3333-4444-555555555555".into(), - })), - direction: ParamDirection::Out, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - - let error = generate_com_interface_files(&plain_iface_with_method(method), "") - .expect_err("native arrays must not degrade to raw pointers"); - - assert!(error.contains("explicit count and element-ownership projection")); - assert!(error.contains("raw-pointer fallback is not allowed")); - } - - #[test] - fn delegate_fails_closed_without_a_callback_projection() { - let method = MethodMeta { - name: "SetHandler".into(), - vtable_index: 3, - params: vec![ParamMeta { - name: "handler".into(), - typ: TypeMeta::Delegate { - namespace: "Contoso".into(), - name: "Handler".into(), - iid: "11111111-2222-3333-4444-555555555555".into(), - }, - direction: ParamDirection::In, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - - let error = generate_com_interface_files(&plain_iface_with_method(method), "") - .expect_err("delegates require an explicit managed projection"); - - assert!(error.contains("managed callback projection")); - assert!(error.contains("raw-pointer fallback is not allowed")); - } - - #[test] - fn runtime_class_uses_its_resolved_default_interface() { - let method = MethodMeta { - name: "CreateDevice".into(), - vtable_index: 3, - params: vec![ParamMeta { - name: "result".into(), - typ: TypeMeta::RuntimeClass { - namespace: "Windows.UI.Composition".into(), - name: "CompositionGraphicsDevice".into(), - default_interface: Some(Box::new(TypeMeta::Interface { - namespace: "Windows.UI.Composition".into(), - name: "ICompositionGraphicsDevice".into(), - iid: "a329b321-0d69-4b89-9951-28de94dc998d".into(), - })), - }, - direction: ParamDirection::Out, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - - let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); - - assert!(output.js.contains( - ".addOut(DynCom.interfaceType(WinGuid.parse('a329b321-0d69-4b89-9951-28de94dc998d')))" - )); - assert!(output.js.contains("return _out;")); - assert!(output.dts.contains("createDevice(): DynWinRtValue;")); - } - - #[test] - fn runtime_class_without_a_default_interface_fails_closed() { - let method = MethodMeta { - name: "CreateDevice".into(), - vtable_index: 3, - params: vec![ParamMeta { - name: "result".into(), - typ: TypeMeta::RuntimeClass { - namespace: "Windows.UI.Composition".into(), - name: "CompositionGraphicsDevice".into(), - default_interface: None, - }, - direction: ParamDirection::Out, - }], - return_type: Some(make_hresult()), - ..Default::default() - }; - - let error = generate_com_interface_files(&plain_iface_with_method(method), "") - .expect_err("runtime classes require a resolved default interface"); - - assert!(error.contains("no resolvable default interface")); - assert!(error.contains("--ref")); - } - - #[test] - fn semantic_hresult_is_preserved_as_a_number() { - let method = MethodMeta { - name: "IsDirty".into(), - vtable_index: 4, - return_type: Some(make_hresult()), - preserve_hresult: true, - ..Default::default() - }; - - let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); - - assert!(output.js.contains(".preserveHresult()")); - assert!(output.js.contains("return DynCom.toNumber(_out);")); - assert!(output.dts.contains("isDirty(): number;")); - } - - #[test] - fn ordinary_hresult_remains_throw_or_void() { - let method = MethodMeta { - name: "Load".into(), - vtable_index: 5, - return_type: Some(make_hresult()), - ..Default::default() - }; - - let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); - - assert!(!output.js.contains(".preserveHresult()")); - assert!(output.dts.contains("load(): void;")); - } -} diff --git a/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs b/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs deleted file mode 100644 index 8d9694bb..00000000 --- a/tools/dynwinrt-codegen/src/codegen/com/type_mapping.rs +++ /dev/null @@ -1,698 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -use std::collections::BTreeMap; - -use crate::com_metadata::{ - ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta, is_native_isize, is_native_usize, -}; -use crate::types::TypeMeta; - -use super::naming::js_param_name; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum StringEncoding { - Wide, - Ansi, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum HandleAliasKind { - HandleValue, - DataPointer, - StringPointer, -} - -pub(super) fn validate_com_abi(meta: &ComInterfaceMeta) -> Result<(), String> { - for method in &meta.interface.methods { - for param in &method.params { - validate_resolved_interfaces( - ¶m.typ, - &format!( - "{}.{} parameter `{}`", - meta.interface.name, method.name, param.name - ), - )?; - if let ParamDirection::UnsupportedNativeArray { count_param_index } = param.direction { - let count = count_param_index - .map(|index| format!("parameter index {index}")) - .unwrap_or_else(|| "metadata-defined size".into()); - return Err(format!( - "{}.{}: caller-sized native buffers are not supported (`{}` uses {count})", - meta.interface.name, method.name, param.name - )); - } - if matches!(param.typ, TypeMeta::Struct { .. }) - && !is_win32_bool(¶m.typ) - && !is_hresult(¶m.typ) - && !is_native_isize(¶m.typ) - && !is_native_usize(¶m.typ) - && handle_type_name(¶m.typ).is_none() - { - return Err(format!( - "{}.{}: struct parameter `{}` requires native layout projection", - meta.interface.name, method.name, param.name - )); - } - if param.direction == ParamDirection::OutFill { - return Err(format!( - "{}.{}: caller-allocated array outputs are not supported", - meta.interface.name, method.name - )); - } - if param.direction == ParamDirection::InOut && !supports_in_out(¶m.typ) { - return Err(format!( - "{}.{}: unsupported [in, out] parameter `{}` of type {:?}", - meta.interface.name, method.name, param.name, param.typ - )); - } - } - if let Some(return_type) = method - .return_type - .as_ref() - .filter(|return_type| !is_hresult(return_type)) - { - validate_resolved_interfaces( - return_type, - &format!("{}.{} return value", meta.interface.name, method.name), - )?; - if !supports_direct_return(return_type) { - return Err(format!( - "{}.{}: unsupported direct native return type {:?}", - meta.interface.name, method.name, return_type - )); - } - } - if method.preserve_hresult && !method.return_type.as_ref().is_some_and(is_hresult) { - return Err(format!( - "{}.{}: semantic HRESULT metadata requires an HRESULT return", - meta.interface.name, method.name - )); - } - } - Ok(()) -} - -fn validate_resolved_interfaces(t: &TypeMeta, context: &str) -> Result<(), String> { - match t { - TypeMeta::Interface { - namespace, - name, - iid, - } if iid.is_empty() => Err(format!( - "{context}: interface `{namespace}.{name}` has no resolvable IID; \ - pass the metadata that defines it via --ref instead of projecting it as a raw pointer" - )), - TypeMeta::Parameterized { - namespace, name, .. - } => Err(format!( - "{context}: parameterized interface `{namespace}.{name}` requires a computed closed IID \ - and managed ownership projection; raw-pointer fallback is not allowed" - )), - TypeMeta::AsyncAction - | TypeMeta::AsyncActionWithProgress(_) - | TypeMeta::AsyncOperation(_) - | TypeMeta::AsyncOperationWithProgress(_, _) => Err(format!( - "{context}: async interface requires a computed closed IID and managed ownership \ - projection; raw-pointer fallback is not allowed" - )), - TypeMeta::Delegate { - namespace, name, .. - } => Err(format!( - "{context}: delegate `{namespace}.{name}` requires a managed callback projection; \ - raw-pointer fallback is not allowed" - )), - TypeMeta::Array(_) => Err(format!( - "{context}: native arrays require an explicit count and element-ownership projection; \ - raw-pointer fallback is not allowed" - )), - TypeMeta::RuntimeClass { - default_interface: Some(default_interface), - .. - } => validate_resolved_interfaces(default_interface, context), - TypeMeta::RuntimeClass { - namespace, - name, - default_interface: None, - } => Err(format!( - "{context}: runtime class `{namespace}.{name}` has no resolvable default interface; \ - pass the metadata that defines it via --ref" - )), - _ => Ok(()), - } -} - -fn managed_interface_iid(t: &TypeMeta) -> Option<&str> { - match t { - TypeMeta::Interface { iid, .. } if !iid.is_empty() => Some(iid), - TypeMeta::RuntimeClass { - default_interface: Some(default_interface), - .. - } => managed_interface_iid(default_interface), - _ => None, - } -} - -fn supports_in_out(t: &TypeMeta) -> bool { - is_native_isize(t) - || is_native_usize(t) - || is_win32_bool(t) - || is_hresult(t) - || handle_type_name(t).is_some() - || matches!( - t, - TypeMeta::Bool - | TypeMeta::I8 - | TypeMeta::U8 - | TypeMeta::I16 - | TypeMeta::U16 - | TypeMeta::I32 - | TypeMeta::U32 - | TypeMeta::I64 - | TypeMeta::U64 - | TypeMeta::F32 - | TypeMeta::F64 - | TypeMeta::Char16 - | TypeMeta::Enum { .. } - ) -} - -fn supports_direct_return(t: &TypeMeta) -> bool { - supports_in_out(t) -} - -pub(super) fn unwrap_return_js(t: &TypeMeta, expr: &str) -> String { - if is_native_isize(t) { - return format!("DynCom.toIsizeBigint({expr})"); - } - if is_native_usize(t) { - return format!("DynCom.toUsizeBigint({expr})"); - } - if is_hresult(t) { - return format!("DynCom.toNumber({expr})"); - } - if managed_interface_iid(t).is_some() { - return expr.to_string(); - } - match string_buffer_encoding(t) { - Some(StringEncoding::Wide) => { - return format!("DynCom.takeCoTaskMemWideString({expr})"); - } - Some(StringEncoding::Ansi) => { - return format!("DynCom.takeCoTaskMemAnsiString({expr})"); - } - None => {} - } - if is_win32_bool(t) { - return format!("(DynCom.toNumber({expr}) !== 0)"); - } - if handle_type_name(t).is_some() { - return format!("DynCom.asPointerBigint({expr})"); - } - match t { - TypeMeta::Bool => format!("DynCom.toBool({expr})"), - TypeMeta::I8 - | TypeMeta::U8 - | TypeMeta::I16 - | TypeMeta::U16 - | TypeMeta::I32 - | TypeMeta::Char16 => format!("DynCom.toNumber({expr})"), - TypeMeta::U32 => format!("DynCom.toU32({expr})"), - TypeMeta::I64 => format!("DynCom.toI64Bigint({expr})"), - TypeMeta::U64 => format!("DynCom.toU64Bigint({expr})"), - TypeMeta::F32 | TypeMeta::F64 => format!("DynCom.toF64({expr})"), - TypeMeta::Guid => format!("DynCom.toGuidString({expr})"), - TypeMeta::Enum { underlying, .. } => unwrap_return_js(underlying, expr), - TypeMeta::String => format!("{expr}.toString()"), - _ => expr.to_string(), - } -} - -#[derive(Clone, Copy)] -pub(super) struct MethodResult<'a> { - pub(super) typ: &'a TypeMeta, - pub(super) param_index: Option, -} - -pub(super) fn method_results(m: &MethodMeta) -> Vec> { - let mut result = Vec::new(); - if let Some(typ) = m - .return_type - .as_ref() - .filter(|typ| !is_hresult(typ) || m.preserve_hresult) - { - result.push(MethodResult { - typ, - param_index: None, - }); - } - result.extend( - m.params - .iter() - .enumerate() - .filter(|(_, param)| { - matches!(param.direction, ParamDirection::Out | ParamDirection::InOut) - }) - .map(|(param_index, param)| MethodResult { - typ: ¶m.typ, - param_index: Some(param_index), - }), - ); - result -} - -pub(super) fn dts_return_type(m: &MethodMeta) -> String { - if string_buffer_pattern(m).is_some() { - let outputs = method_results(m); - if outputs.is_empty() { - return "string".to_string(); - } - return format!( - "[string, {}]", - outputs - .iter() - .map(|result| ts_result_type(m, *result)) - .collect::>() - .join(", ") - ); - } - let result_types = method_results(m); - match result_types.len() { - 0 => "void".to_string(), - 1 => ts_result_type(m, result_types[0]), - _ => format!( - "[{}]", - result_types - .iter() - .map(|result| ts_result_type(m, *result)) - .collect::>() - .join(", ") - ), - } -} - -fn ts_result_type(method: &MethodMeta, result: MethodResult<'_>) -> String { - if is_sys_free_string_owned(method, result) || string_buffer_encoding(result.typ).is_some() { - "string".into() - } else if is_cotaskmem_owned(method, result) { - "DynWinRtValue".into() - } else { - ts_type_expr_dts(result.typ) - } -} - -pub(super) fn is_cotaskmem_owned(method: &MethodMeta, result: MethodResult<'_>) -> bool { - let Some(param_index) = result.param_index else { - return false; - }; - method - .owned_outputs - .iter() - .any(|owned| owned.param_index == param_index && owned.free_with.contains("CoTaskMemFree")) -} - -pub(super) fn is_sys_free_string_owned(method: &MethodMeta, result: MethodResult<'_>) -> bool { - let Some(param_index) = result.param_index else { - return false; - }; - is_bstr(result.typ) - && method.owned_outputs.iter().any(|owned| { - owned.param_index == param_index && owned.free_with.contains("SysFreeString") - }) -} - -pub(super) fn dts_params_for_method(m: &MethodMeta) -> Vec { - let string_buffer = string_buffer_pattern(m); - m.params - .iter() - .enumerate() - .filter(|(_, param)| param.direction.is_input()) - .enumerate() - .map(|(surface_index, (param_index, param))| { - let mut name = js_param_name(¶m.name, surface_index); - if let Some((_, count_index, _)) = string_buffer { - if param_index >= count_index && string_buffer_param_is_optional(m, param_index) { - name.push('?'); - } - } - format!("{}: {}", name, ts_input_type_expr_dts(¶m.typ)) - }) - .collect() -} - -pub(super) fn collect_handle_aliases(meta: &ComInterfaceMeta) -> Vec<(String, HandleAliasKind)> { - let mut aliases = BTreeMap::new(); - for method in &meta.interface.methods { - for param in &method.params { - if let Some((alias, kind)) = handle_alias(¶m.typ) { - aliases.insert(alias, kind); - } - } - if let Some((alias, kind)) = method.return_type.as_ref().and_then(handle_alias) { - aliases.insert(alias, kind); - } - } - aliases.into_iter().collect() -} - -pub(super) fn enum_import_names(meta: &ComInterfaceMeta) -> Vec { - meta.referenced_enums - .iter() - .map(|enum_meta| enum_meta.name.clone()) - .collect() -} - -pub(super) fn uses_winrt_bridge_value(meta: &ComInterfaceMeta) -> bool { - for method in &meta.interface.methods { - for typ in method - .params - .iter() - .map(|param| ¶m.typ) - .chain(method.return_type.iter()) - { - if managed_interface_iid(typ).is_some() { - return true; - } - } - } - false -} - -pub(super) fn has_string_buffer_method(meta: &ComInterfaceMeta) -> bool { - meta.interface - .methods - .iter() - .any(|method| string_buffer_pattern(method).is_some()) -} - -pub(super) fn string_buffer_pattern(method: &MethodMeta) -> Option<(usize, usize, StringEncoding)> { - for (index, param) in method.params.iter().enumerate() { - let ParamDirection::OutStringBuffer { count_param_index } = param.direction else { - continue; - }; - let encoding = string_buffer_encoding(¶m.typ)?; - if method - .params - .get(count_param_index) - .is_some_and(|count| count.direction == ParamDirection::In) - { - return Some((index, count_param_index, encoding)); - } - } - None -} - -pub(super) fn string_buffer_param_is_optional(method: &MethodMeta, param_index: usize) -> bool { - let Some((_, count_index, _)) = string_buffer_pattern(method) else { - return false; - }; - let Some(param) = method.params.get(param_index) else { - return false; - }; - let is_optional_shape = param_index == count_index - || (param_index > count_index && is_optional_find_data_out_after_string_count(param)); - if !is_optional_shape { - return false; - } - method - .params - .iter() - .enumerate() - .skip(param_index + 1) - .filter(|(_, param)| param.direction.is_input()) - .all(|(_, param)| is_optional_find_data_out_after_string_count(param)) -} - -fn string_buffer_encoding(t: &TypeMeta) -> Option { - match t { - TypeMeta::Struct { - namespace, name, .. - } if namespace == "Windows.Win32.Foundation" && name == "PWSTR" => { - Some(StringEncoding::Wide) - } - TypeMeta::Struct { - namespace, name, .. - } if namespace == "Windows.Win32.Foundation" && name == "PSTR" => { - Some(StringEncoding::Ansi) - } - _ => None, - } -} - -pub(super) fn is_optional_find_data_out_after_string_count(param: &ParamMeta) -> bool { - if !matches!(param.direction, ParamDirection::In | ParamDirection::Out) { - return false; - } - let name = param.name.to_ascii_lowercase(); - if name == "pfd" || name.contains("finddata") || name.contains("find_data") { - return true; - } - matches!( - ¶m.typ, - TypeMeta::Struct { name, .. } if name == "WIN32_FIND_DATAW" || name == "WIN32_FIND_DATAA" - ) -} - -pub(super) fn ts_type_expr_dts(t: &TypeMeta) -> String { - if is_native_isize(t) || is_native_usize(t) { - return "bigint".into(); - } - if is_win32_bool(t) { - return "boolean".into(); - } - if is_hresult(t) { - return "number".into(); - } - if let Some(handle) = handle_type_name(t) { - return handle; - } - if managed_interface_iid(t).is_some() { - return "DynWinRtValue".into(); - } - match t { - TypeMeta::Bool => "boolean".into(), - TypeMeta::I8 - | TypeMeta::U8 - | TypeMeta::I16 - | TypeMeta::U16 - | TypeMeta::I32 - | TypeMeta::U32 - | TypeMeta::F32 - | TypeMeta::F64 - | TypeMeta::Char16 => "number".into(), - TypeMeta::I64 | TypeMeta::U64 => "bigint".into(), - TypeMeta::String => "string".into(), - TypeMeta::Guid => "string".into(), - TypeMeta::Enum { name, .. } | TypeMeta::Struct { name, .. } => name.clone(), - _ => "bigint | Buffer".into(), - } -} - -pub(super) fn ts_input_type_expr_dts(t: &TypeMeta) -> String { - if accepts_handle_value_buffer(t) - || matches!(handle_alias(t), Some((_, HandleAliasKind::DataPointer))) - { - return format!("{} | Buffer | Uint8Array", handle_type_name(t).unwrap()); - } - ts_type_expr_dts(t) -} - -pub(super) fn ts_type_expr_js(t: &TypeMeta) -> String { - if is_native_isize(t) { - return "DynCom.isizeType()".into(); - } - if is_native_usize(t) { - return "DynCom.usizeType()".into(); - } - if is_win32_bool(t) || is_hresult(t) { - return "DynCom.i32Type()".into(); - } - if handle_type_name(t).is_some() { - return "DynCom.pointerType()".into(); - } - if let Some(iid) = managed_interface_iid(t) { - return format!("DynCom.interfaceType(WinGuid.parse('{iid}'))"); - } - match t { - TypeMeta::Bool => "DynCom.boolType()".into(), - TypeMeta::I8 => "DynCom.i8Type()".into(), - TypeMeta::U8 => "DynCom.u8Type()".into(), - TypeMeta::I16 => "DynCom.i16Type()".into(), - TypeMeta::U16 => "DynCom.u16Type()".into(), - TypeMeta::I32 => "DynCom.i32Type()".into(), - TypeMeta::U32 => "DynCom.u32Type()".into(), - TypeMeta::I64 => "DynCom.i64Type()".into(), - TypeMeta::U64 => "DynCom.u64Type()".into(), - TypeMeta::F32 => "DynCom.f32Type()".into(), - TypeMeta::F64 => "DynCom.f64Type()".into(), - TypeMeta::Char16 => "DynCom.char16Type()".into(), - TypeMeta::String => "DynCom.hstringType()".into(), - TypeMeta::Guid => "DynCom.guidType()".into(), - TypeMeta::Enum { underlying, .. } => ts_type_expr_js(underlying), - _ => "DynCom.pointerType()".into(), - } -} - -pub(super) fn wrap_arg_js(t: &TypeMeta, var: &str) -> String { - if is_native_isize(t) { - return format!("DynCom.isize(BigInt({var}))"); - } - if is_native_usize(t) { - return format!("DynCom.usize(BigInt({var}))"); - } - if is_win32_bool(t) { - return format!("DynCom.i32({var} ? 1 : 0)"); - } - if is_hresult(t) { - return format!("DynCom.i32({var})"); - } - if let Some((_, kind)) = handle_alias(t) { - return match kind { - HandleAliasKind::HandleValue if accepts_handle_value_buffer(t) => { - format!("DynCom.pointer(DynCom.handleValue({var}))") - } - HandleAliasKind::HandleValue => format!("DynCom.pointer({var})"), - HandleAliasKind::DataPointer | HandleAliasKind::StringPointer => { - format!("DynCom.pointer({var})") - } - }; - } - if managed_interface_iid(t).is_some() { - return var.to_string(); - } - match t { - TypeMeta::Bool => format!("DynCom.boolValue({var})"), - TypeMeta::I8 => format!("DynCom.i8Value({var})"), - TypeMeta::U8 => format!("DynCom.u8Value({var})"), - TypeMeta::I16 => format!("DynCom.i16({var})"), - TypeMeta::U16 => format!("DynCom.u16({var})"), - TypeMeta::I32 => format!("DynCom.i32({var})"), - TypeMeta::U32 => format!("DynCom.u32({var})"), - TypeMeta::I64 => format!("DynCom.i64(BigInt({var}))"), - TypeMeta::U64 => format!("DynCom.u64(BigInt({var}))"), - TypeMeta::F32 => format!("DynCom.f32({var})"), - TypeMeta::F64 => format!("DynCom.f64({var})"), - TypeMeta::Char16 => format!("DynCom.char16({var})"), - TypeMeta::String => format!("DynCom.hstring({var})"), - TypeMeta::Guid => format!("DynCom.guid(WinGuid.parse({var}))"), - TypeMeta::Enum { underlying, .. } => wrap_arg_js(underlying, var), - _ => format!("DynCom.pointer({var})"), - } -} - -pub(super) fn handle_type_name(t: &TypeMeta) -> Option { - handle_alias(t).map(|(name, _)| name) -} - -#[cfg(test)] -pub(super) fn handle_alias_kind(t: &TypeMeta) -> Option { - handle_alias(t).map(|(_, kind)| kind) -} - -fn handle_alias(t: &TypeMeta) -> Option<(String, HandleAliasKind)> { - if is_win32_bool(t) { - return None; - } - match t { - TypeMeta::Struct { - namespace, - name, - fields, - } if is_win32_handle_namespace(namespace) - && !is_hresult_by_name(namespace, name) - && fields.len() == 1 - && fields[0].name == "Value" - && matches!( - fields[0].typ, - TypeMeta::Object | TypeMeta::U64 | TypeMeta::I64 | TypeMeta::U32 | TypeMeta::I32 - ) => - { - Some((name.clone(), classify_handle_alias(namespace, name))) - } - _ => None, - } -} - -fn classify_handle_alias(_namespace: &str, name: &str) -> HandleAliasKind { - if is_string_pointer_alias_name(name) { - HandleAliasKind::StringPointer - } else if is_data_pointer_alias_name(name) { - HandleAliasKind::DataPointer - } else { - HandleAliasKind::HandleValue - } -} - -fn accepts_handle_value_buffer(t: &TypeMeta) -> bool { - matches!( - handle_alias(t), - Some((name, HandleAliasKind::HandleValue)) if name == "HWND" - ) -} - -fn is_data_pointer_alias_name(name: &str) -> bool { - matches!( - name, - "PSID" - | "PSECURITY_DESCRIPTOR" - | "MEMORY_MAPPED_VIEW_ADDRESS" - | "LPPROC_THREAD_ATTRIBUTE_LIST" - ) -} - -fn is_string_pointer_alias_name(name: &str) -> bool { - // Classic COM handle typedefs lose pointer-pointee detail by the time they - // reach TypeMeta (`Value: *mut u16` and `Value: *mut c_void` both become - // `Value: Object`). Keep the known Win32 NUL-terminated character-pointer - // aliases as Buffer-capable pointer parameters; all other handle-shaped - // structs are handle values and must not accept Buffer-of-bits inputs. - matches!( - name, - "PWSTR" - | "PCWSTR" - | "PSTR" - | "PCSTR" - | "LPWSTR" - | "LPCWSTR" - | "LPSTR" - | "LPCSTR" - | "PWCHAR" - | "PCWCHAR" - | "LPWCH" - | "LPCWCH" - | "LPCH" - | "LPCCH" - ) -} - -fn is_win32_handle_namespace(namespace: &str) -> bool { - namespace.starts_with("Windows.Win32.") -} - -pub(super) fn is_hresult(t: &TypeMeta) -> bool { - matches!( - t, - TypeMeta::Struct { namespace, name, .. } - if is_hresult_by_name(namespace, name) - ) -} - -fn is_hresult_by_name(namespace: &str, name: &str) -> bool { - namespace == "Windows.Win32.Foundation" && name == "HRESULT" -} - -pub(super) fn is_win32_bool(t: &TypeMeta) -> bool { - matches!( - t, - TypeMeta::Struct { namespace, name, .. } - if namespace == "Windows.Win32.Foundation" && name == "BOOL" - ) -} - -pub(super) fn is_bstr(t: &TypeMeta) -> bool { - matches!( - t, - TypeMeta::Struct { namespace, name, .. } - if namespace == "Windows.Win32.Foundation" && name == "BSTR" - ) -} diff --git a/tools/dynwinrt-codegen/src/codegen/common.rs b/tools/dynwinrt-codegen/src/codegen/common.rs index 5fccc4e2..0aba4e6b 100644 --- a/tools/dynwinrt-codegen/src/codegen/common.rs +++ b/tools/dynwinrt-codegen/src/codegen/common.rs @@ -10,13 +10,13 @@ pub use super::python::naming::to_snake_case_filename; #[cfg(test)] mod tests { - use crate::codegen::javascript::naming::*; - use crate::codegen::javascript::signature::*; - use crate::codegen::javascript::structs::*; - use crate::codegen::python::naming::*; - use crate::codegen::python::signature::*; - use crate::codegen::python::structs::*; - use crate::codegen::shared::imports::*; + use crate::codegen::winrt::javascript::naming::*; + use crate::codegen::winrt::javascript::signature::*; + use crate::codegen::winrt::javascript::structs::*; + use crate::codegen::winrt::python::naming::*; + use crate::codegen::winrt::python::signature::*; + use crate::codegen::winrt::python::structs::*; + use crate::codegen::winrt::shared::imports::*; use crate::meta::{MethodMeta, ParamDirection, ParamMeta}; use crate::types::TypeMeta; use std::collections::HashSet; diff --git a/tools/dynwinrt-codegen/src/codegen/mod.rs b/tools/dynwinrt-codegen/src/codegen/mod.rs index 9704d9d1..3a0893e4 100644 --- a/tools/dynwinrt-codegen/src/codegen/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/mod.rs @@ -3,9 +3,11 @@ pub mod com; pub mod common; -pub mod javascript; -pub mod python; -pub(crate) mod shared; +pub mod winrt; + +// Preserve the existing public module paths while callers migrate to +// `codegen::winrt::{javascript, python}`. +pub use winrt::{javascript, python}; // Preserve the existing public API while the implementations live under // language-specific modules. diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/docs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/docs.rs similarity index 98% rename from tools/dynwinrt-codegen/src/codegen/javascript/docs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/docs.rs index 02873d28..1aec2e17 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/docs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/docs.rs @@ -3,7 +3,7 @@ //! JSDoc rendering for generated JavaScript declarations. -use crate::codegen::shared::docs::DocText; +use crate::codegen::winrt::shared::docs::DocText; /// Escape `*/` sequences so a JSDoc block comment cannot terminate early. fn escape_jsdoc(s: &str) -> String { diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/generator.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/generator.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/generator.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/generator.rs index 240ec4b9..86012582 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/generator.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/generator.rs @@ -6,7 +6,7 @@ use std::collections::HashSet; use crate::meta::{ClassMeta, InterfaceMeta}; use crate::types::TypeMeta; -use crate::codegen::shared::structs::{ +use crate::codegen::winrt::shared::structs::{ collect_used_structs_from_class, collect_used_structs_from_iface, }; diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/ir.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/ir.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/ir.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/ir.rs diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/method.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/method.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/method.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/method.rs index 55dc2b08..78fa48d1 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/method.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/method.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; -use crate::codegen::shared::imports::ireference_inner_type; +use crate::codegen::winrt::shared::imports::ireference_inner_type; use crate::types::TypeMeta; // ====================================================================== diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/mod.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/mod.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/mod.rs diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/naming.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/naming.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/naming.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/naming.rs diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/project/collections.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/collections.rs similarity index 95% rename from tools/dynwinrt-codegen/src/codegen/javascript/project/collections.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/collections.rs index c3233616..08dcb567 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/project/collections.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/collections.rs @@ -567,6 +567,34 @@ pub(super) fn project_collection_create( dts_only: false, is_runtime_package: false, }); + members.push(ProjectedMember::Method(ProjectedMethod { + name: "asVector".into(), + doc: Some(DocInfo { + summary: Some( + "Cast this observable collection to its mutable vector interface.".into(), + ), + deprecated: None, + returns: None, + params: vec![], + }), + params: vec![], + argument_kinds: vec![], + return_type: vector_name.clone(), + async_kind: AsyncKind::None, + is_static: false, + invoke_expr: String::new(), + sync_return_expr: Some(format!( + "new ((__load_{vector}()).{vector})(this._obj)", + vector = vector_name, + )), + async_convert_v: None, + is_void: false, + array_return_expr: None, + delegate_wraps: vec![], + progress_convert: None, + js_only: false, + overload_of: None, + })); members.push(ProjectedMember::Method(ProjectedMethod { name: "create".into(), doc: Some(DocInfo { @@ -593,7 +621,7 @@ pub(super) fn project_collection_create( is_static: true, invoke_expr: String::new(), sync_return_expr: Some(format!( - "(() => {{ const value = DynWinRtValue.createVector(items.map(i => _unwrap(i)), {elem_type}); const observable = new {observable}(value); const vector = new ((__load_{vector}()).{vector})(value); Object.defineProperties(vector, {{ onVectorChanged: {{ value: observable.onVectorChanged.bind(observable) }}, onceVectorChanged: {{ value: observable.onceVectorChanged.bind(observable) }}, offVectorChanged: {{ value: observable.offVectorChanged.bind(observable) }} }}); return vector; }})()", + "(() => {{ const value = DynWinRtValue.createVector(items.map(i => _unwrap(i)), {elem_type}); const observable = new {observable}(value); const vector = new ((__load_{vector}()).{vector})(value); Object.defineProperties(vector, {{ asVector: {{ value: observable.asVector.bind(observable) }}, onVectorChanged: {{ value: observable.onVectorChanged.bind(observable) }}, onceVectorChanged: {{ value: observable.onceVectorChanged.bind(observable) }}, offVectorChanged: {{ value: observable.offVectorChanged.bind(observable) }} }}); return vector; }})()", observable = iface.name, vector = vector_name, )), diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/project/constructors.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/constructors.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/project/constructors.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/constructors.rs index bf211f71..ceeb28bd 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/project/constructors.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/constructors.rs @@ -9,7 +9,7 @@ use crate::meta::{ConstructorKind, ConstructorMeta, InterfaceMeta, MethodMeta, P use crate::types::TypeMeta; use super::*; -use crate::codegen::javascript::signature::ref_marker; +use crate::codegen::winrt::javascript::signature::ref_marker; struct ConstructorCandidate { params: Vec, diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/project/methods.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/methods.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/project/methods.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/methods.rs diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/project/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/mod.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/project/mod.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/mod.rs index c42f6225..95a18bee 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/project/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/mod.rs @@ -15,7 +15,9 @@ mod structs; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; -use crate::meta::{ClassMeta, InterfaceMeta, MethodMeta, ParamDirection}; +use crate::meta::{ + ClassMeta, InterfaceMeta, MethodMeta, PIID_IOBSERVABLE_VECTOR, PIID_IVECTOR, ParamDirection, +}; use crate::types::{TypeKind, TypeMeta}; thread_local! { @@ -32,12 +34,12 @@ pub fn get_import_name() -> String { RUNTIME_IMPORT_NAME.with(|n| n.borrow().clone()) } -use crate::codegen::shared::imports::{ +use crate::codegen::winrt::shared::imports::{ NO_DEFERRED, collect_iface_type_imports, collect_type_imports, collect_used_generics_from_class, collect_used_generics_from_methods, fill_array_output_index, fill_array_uses_retval_count, get_in_params, ireference_inner_type, method_abi_output_count, }; -use crate::codegen::shared::structs::{ +use crate::codegen::winrt::shared::structs::{ collect_used_structs_from_class, collect_used_structs_from_iface, }; @@ -62,9 +64,7 @@ use structs::project_struct_helpers; // ====================================================================== // PIIDs of well-known collection interfaces // ====================================================================== -const PIID_IVECTOR: &str = "913337e9-11a1-4345-a3a2-4e7f956e222d"; const PIID_IVECTOR_VIEW: &str = "bbe1fa4c-b0e3-4583-baef-1f1b2e483e56"; -const PIID_IOBSERVABLE_VECTOR: &str = "5917eb53-50b4-4a0d-b309-65862b3f1dbc"; const PIID_IITERATOR: &str = "6a79e863-4300-459a-9966-cbb660963ee1"; const PIID_IITERABLE: &str = "faa585ea-6214-4217-afda-7f46de5869b3"; const PIID_IMAP: &str = "3c2925fe-8519-45c1-aa79-197b6718c1c1"; @@ -1531,7 +1531,7 @@ fn build_method_doc(method: &MethodMeta, in_params: &[&crate::meta::ParamMeta]) let params_display: Vec<(String, String)> = in_params .iter() .filter_map(|p| { - crate::codegen::shared::docs::find_param_doc(&method.param_docs, &p.name) + crate::codegen::winrt::shared::docs::find_param_doc(&method.param_docs, &p.name) .map(|d| (to_camel_case(&p.name), d.to_string())) }) .collect(); diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/project/structs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/structs.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/project/structs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/structs.rs diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/render/declarations.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/declarations.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/render/declarations.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/declarations.rs index 9c49225b..27edbcee 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/render/declarations.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/declarations.rs @@ -11,7 +11,7 @@ //! - Emits TSDoc comments //! - Enums as `export enum` (not `Object.freeze`) -use crate::codegen::javascript::ir::*; +use crate::codegen::winrt::javascript::ir::*; /// Render a projected file as a `.d.ts` declaration. pub fn render(file: &ProjectedFile) -> String { @@ -577,7 +577,7 @@ fn render_enum_dts(out: &mut String, en: &ProjectedEnum) { // ====================================================================== fn render_tsdoc(doc: &DocInfo, indent: &str) -> String { - let doc_text = crate::codegen::shared::docs::DocText { + let doc_text = crate::codegen::winrt::shared::docs::DocText { summary: doc.summary.as_deref(), deprecated: doc.deprecated.as_deref(), returns: doc.returns.as_deref(), @@ -587,7 +587,7 @@ fn render_tsdoc(doc: &DocInfo, indent: &str) -> String { .map(|(n, d)| (n.as_str(), d.as_str())) .collect(), }; - crate::codegen::javascript::docs::format_jsdoc(&doc_text, indent) + crate::codegen::winrt::javascript::docs::format_jsdoc(&doc_text, indent) } /// Check if any method in the file uses AsyncWithProgress. diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/commonjs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/commonjs.rs similarity index 88% rename from tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/commonjs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/commonjs.rs index bf54daec..44750772 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/commonjs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/commonjs.rs @@ -123,7 +123,15 @@ pub(super) fn convert_to_cjs_with_lazy( let symbols = &sibling_symbols[module]; let var_name = sanitize_ident(module); - let eager: Vec<&String> = symbols.iter().filter(|s| is_eager_symbol(s)).collect(); + let all_eager: Vec<&String> = symbols + .iter() + .filter(|symbol| is_eager_symbol(symbol)) + .collect(); + let eager: Vec<&String> = all_eager + .iter() + .copied() + .filter(|symbol| transformed_body.contains(symbol.as_str())) + .collect(); let lazy: Vec<&String> = symbols.iter().filter(|s| !is_eager_symbol(s)).collect(); // Eager block first: `const { IID_X, X_PARAM_TYPES } = require('./X.js');` @@ -137,6 +145,10 @@ pub(super) fn convert_to_cjs_with_lazy( "const {{ {} }} = require('./{}.js');\n", list, module )); + } else if !all_eager.is_empty() { + // Keep the sibling's initialization side effects without reading + // an export that may not exist yet during a CommonJS cycle. + out.push_str(&format!("require('./{}.js');\n", module)); } if !lazy.is_empty() { @@ -183,6 +195,44 @@ pub(super) fn convert_to_cjs_with_lazy( out } +#[cfg(test)] +mod tests { + use super::convert_to_cjs_with_lazy; + use std::collections::HashSet; + + #[test] + fn unused_eager_symbol_keeps_module_initialization_only() { + let esm = "\ +import { IXamlType, IID_IXamlType } from './IXamlType.js';\n\ +export function getType(value) {\n\ + return new __DWRT_REF__IXamlType__(value);\n\ +}\n"; + + let cjs = convert_to_cjs_with_lazy(esm, &HashSet::new()); + + assert!(!cjs.contains("const { IID_IXamlType }"), "{cjs}"); + assert!(cjs.contains("require('./IXamlType.js');"), "{cjs}"); + assert!(cjs.contains("const __get_IXamlType"), "{cjs}"); + assert!(cjs.contains("new (__get_IXamlType())(value)"), "{cjs}"); + } + + #[test] + fn used_eager_symbol_is_still_destructured() { + let esm = "\ +import { IID_IFoo } from './IFoo.js';\n\ +export function cast(value) {\n\ + return value.cast(IID_IFoo);\n\ +}\n"; + + let cjs = convert_to_cjs_with_lazy(esm, &HashSet::new()); + + assert!( + cjs.contains("const { IID_IFoo } = require('./IFoo.js');"), + "{cjs}" + ); + } +} + /// Parse `import { A, B } from 'source';` (single-line). Returns Some(symbols, source) /// on success. Returns None if the line isn't a well-formed static import. fn parse_import_line(line: &str) -> Option<(Vec, String)> { @@ -324,15 +374,15 @@ fn extract_export_decl_name(line: &str) -> Option { /// import classification of the surrounding file. fn resolve_ref_markers(body: &str, lazy_symbols: &std::collections::HashSet) -> String { // Fast path: no markers present. - if !body.contains(crate::codegen::javascript::signature::REF_MARKER_PREFIX) { + if !body.contains(crate::codegen::winrt::javascript::signature::REF_MARKER_PREFIX) { return body.to_string(); } let mut out = String::with_capacity(body.len()); let mut cursor = 0usize; let bytes = body.as_bytes(); - let prefix = crate::codegen::javascript::signature::REF_MARKER_PREFIX; - let suffix = crate::codegen::javascript::signature::REF_MARKER_SUFFIX; + let prefix = crate::codegen::winrt::javascript::signature::REF_MARKER_PREFIX; + let suffix = crate::codegen::winrt::javascript::signature::REF_MARKER_SUFFIX; while let Some(rel_start) = body[cursor..].find(prefix) { let start = cursor + rel_start; diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/helpers.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/helpers.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/helpers.rs index 9b0c8d3e..349a0313 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/helpers.rs @@ -3,7 +3,7 @@ //! Member, overload, and asynchronous JavaScript emitters. -use crate::codegen::javascript::ir::*; +use crate::codegen::winrt::javascript::ir::*; // ====================================================================== // Async scaffolding @@ -84,7 +84,7 @@ pub(super) fn emit_with_progress_body( // ====================================================================== pub(super) fn render_jsdoc(doc: &DocInfo, indent: &str) -> String { - let doc_text = crate::codegen::shared::docs::DocText { + let doc_text = crate::codegen::winrt::shared::docs::DocText { summary: doc.summary.as_deref(), deprecated: doc.deprecated.as_deref(), returns: doc.returns.as_deref(), @@ -94,7 +94,7 @@ pub(super) fn render_jsdoc(doc: &DocInfo, indent: &str) -> String { .map(|(n, d)| (n.as_str(), d.as_str())) .collect(), }; - crate::codegen::javascript::docs::format_jsdoc(&doc_text, indent) + crate::codegen::winrt::javascript::docs::format_jsdoc(&doc_text, indent) } pub(super) fn inject_unwrap(code: String) -> String { diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/mod.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/mod.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/mod.rs index 81ebf20a..d14e0a1f 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/mod.rs @@ -10,8 +10,8 @@ mod commonjs; mod helpers; -use crate::codegen::javascript::ir::*; -use crate::codegen::javascript::signature::ref_marker; +use crate::codegen::winrt::javascript::ir::*; +use crate::codegen::winrt::javascript::signature::ref_marker; use commonjs::convert_to_cjs_with_lazy; use helpers::{ @@ -222,7 +222,7 @@ fn render_class_js(out: &mut String, class: &ProjectedClass) { .filter_map(|m| match m { ProjectedMember::Method(method) => Some((method.name.clone(), method.params.len())), ProjectedMember::Symbol(s) => Some(( - crate::codegen::javascript::project::symbol_dedup_key(&s.kind), + crate::codegen::winrt::javascript::project::symbol_dedup_key(&s.kind), 0, )), ProjectedMember::Close => Some(("close".into(), 0)), diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/render/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/mod.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/render/mod.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/mod.rs diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/render/package_json.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/package_json.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/render/package_json.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/package_json.rs diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/signature.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/signature.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/signature.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/signature.rs index e18be78a..7cf80e92 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/signature.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/signature.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; -use crate::codegen::shared::imports::ireference_inner_type; +use crate::codegen::winrt::shared::imports::ireference_inner_type; use crate::meta::{InterfaceMeta, MethodMeta, ParamDirection}; use crate::types::TypeMeta; diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/structs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/structs.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/structs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/structs.rs diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/mod.rs new file mode 100644 index 00000000..91de89cb --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/winrt/mod.rs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Windows Runtime code generation. + +pub mod javascript; +pub mod python; +pub(crate) mod shared; diff --git a/tools/dynwinrt-codegen/src/codegen/python/collections.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/collections.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/collections.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/collections.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/docs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/docs.rs similarity index 98% rename from tools/dynwinrt-codegen/src/codegen/python/docs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/docs.rs index ce75610e..fcc2bb06 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/docs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/docs.rs @@ -3,7 +3,7 @@ //! Docstring rendering for generated Python bindings. -use crate::codegen::shared::docs::DocText; +use crate::codegen::winrt::shared::docs::DocText; /// Escape `"""` so a Python triple-quoted string cannot terminate early. fn escape_pydoc(s: &str) -> String { diff --git a/tools/dynwinrt-codegen/src/codegen/python/generator/class.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs similarity index 95% rename from tools/dynwinrt-codegen/src/codegen/python/generator/class.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs index 8cb7f2b9..5c9245c8 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/generator/class.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs @@ -6,7 +6,7 @@ use super::imports::{emit_type_checking_imports, format_py_type_import}; use super::structs::generate_struct_helpers; use super::*; -use crate::codegen::python::collections::{ +use crate::codegen::winrt::python::collections::{ CollectionKind, class_interface, interface_kind, map_iterable_name, runtime_mixin, }; @@ -222,12 +222,14 @@ pub fn generate_class( out.push_str(&format!("\nclass {}:\n", class.name)); } { - let doc = crate::codegen::shared::docs::DocText { + let doc = crate::codegen::winrt::shared::docs::DocText { summary: class.doc.as_deref(), deprecated: class.deprecated.as_deref(), ..Default::default() }; - out.push_str(&crate::codegen::python::docs::format_pydoc(&doc, " ")); + out.push_str(&crate::codegen::winrt::python::docs::format_pydoc( + &doc, " ", + )); } out.push_str(&generate_python_constructor( @@ -303,7 +305,7 @@ pub fn generate_class( ) .collect::>(); let static_method_names = - crate::codegen::python::overloads::method_names(static_methods.iter().copied()); + crate::codegen::winrt::python::overloads::method_names(static_methods.iter().copied()); let mut static_groups: Vec<(String, Vec>)> = Vec::new(); for (kind, interfaces) in [ (StaticOverloadKind::Factory, &class.factory_interfaces), @@ -311,7 +313,7 @@ pub fn generate_class( ] { for iface in interfaces { for method in &iface.methods { - let mut key = crate::codegen::python::overloads::method_group_key( + let mut key = crate::codegen::winrt::python::overloads::method_group_key( method, &static_method_names, ); @@ -355,7 +357,7 @@ pub fn generate_class( .chain(class.required_interfaces.iter()) .filter(|iface| iface.iid != "30d5a829-7fa4-4026-83bb-d75bae4ea99e") .collect::>(); - let instance_method_names = crate::codegen::python::overloads::method_names( + let instance_method_names = crate::codegen::winrt::python::overloads::method_names( instance_ifaces .iter() .flat_map(|iface| iface.methods.iter()), @@ -378,8 +380,10 @@ pub fn generate_class( obj_expr.to_string() }; for method in reorder_getters_before_setters(&iface.methods) { - let key = - crate::codegen::python::overloads::method_group_key(method, &instance_method_names); + let key = crate::codegen::winrt::python::overloads::method_group_key( + method, + &instance_method_names, + ); let overload = InstanceOverload { iface_var: format!("_{}", iface.name), obj_expr: obj_expr.clone(), @@ -570,7 +574,7 @@ fn generate_python_constructor( .flat_map(|iface| iface.methods.iter()) .collect::>(); let factory_names = - crate::codegen::python::overloads::method_names(factory_methods.iter().copied()); + crate::codegen::winrt::python::overloads::method_names(factory_methods.iter().copied()); let has_create_factory = factory_methods.iter().any(|method| { let name = to_snake_case(&method.name); name == "create" || name.starts_with("create") @@ -585,7 +589,7 @@ fn generate_python_constructor( )); } for method in factory_methods { - let in_params = crate::codegen::shared::imports::get_in_params(method); + let in_params = crate::codegen::winrt::shared::imports::get_in_params(method); let parameter_names = in_params .iter() .map(|param| format!("'{}'", to_snake_case(¶m.name))) @@ -597,7 +601,7 @@ fn generate_python_constructor( format!("({parameter_names},)") }; let public_name = - crate::codegen::python::overloads::method_group_key(method, &factory_names); + crate::codegen::winrt::python::overloads::method_group_key(method, &factory_names); let overload_count = factory_methods_for_name(class, &factory_names, &public_name); let call_name = if overload_count > 1 { format!("_{public_name}_{}", method.vtable_index) @@ -647,7 +651,7 @@ fn factory_methods_for_name( .iter() .flat_map(|iface| iface.methods.iter()) .filter(|method| { - crate::codegen::python::overloads::method_group_key(method, names) == public_name + crate::codegen::winrt::python::overloads::method_group_key(method, names) == public_name }) .count() } diff --git a/tools/dynwinrt-codegen/src/codegen/python/generator/imports.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/imports.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/generator/imports.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/generator/imports.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/generator/index.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/index.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/generator/index.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/generator/index.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/generator/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs similarity index 97% rename from tools/dynwinrt-codegen/src/codegen/python/generator/mod.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs index ef09aef5..0649a78a 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/generator/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs @@ -12,11 +12,11 @@ use std::collections::HashSet; use crate::meta::{ClassMeta, InterfaceMeta, MethodMeta, ParamDirection}; use crate::types::{TypeKind, TypeMeta}; -use crate::codegen::shared::imports::{ +use crate::codegen::winrt::shared::imports::{ collect_iface_type_imports, collect_type_imports, collect_used_generics_from_class, collect_used_generics_from_methods, ireference_inner_type, }; -use crate::codegen::shared::structs::{ +use crate::codegen::winrt::shared::structs::{ collect_used_structs_from_class, collect_used_structs_from_iface, }; diff --git a/tools/dynwinrt-codegen/src/codegen/python/generator/structs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs similarity index 98% rename from tools/dynwinrt-codegen/src/codegen/python/generator/structs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs index b00cecca..ac9792f3 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/generator/structs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs @@ -4,7 +4,7 @@ //! Python struct projection helpers. use super::*; -use crate::codegen::python::native_types::{FoundationType, foundation_type}; +use crate::codegen::winrt::python::native_types::{FoundationType, foundation_type}; // ====================================================================== // Struct helpers: Python dataclass-style + _unpack/_pack functions diff --git a/tools/dynwinrt-codegen/src/codegen/python/generator/types.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs similarity index 94% rename from tools/dynwinrt-codegen/src/codegen/python/generator/types.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs index 8cbb41b7..5a28c1bf 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/generator/types.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs @@ -6,7 +6,7 @@ use super::imports::{emit_type_checking_imports, format_py_type_import}; use super::structs::generate_struct_helpers; use super::*; -use crate::codegen::python::collections::{ +use crate::codegen::winrt::python::collections::{ CollectionKind, interface_kind, map_iterable_name, runtime_mixin, }; @@ -35,13 +35,13 @@ pub fn generate_enum(en: &TypeMeta) -> Option { let enum_base = if is_flags { "IntFlag" } else { "IntEnum" }; out.push_str(&format!("from enum import {enum_base}\n\n\n")); out.push_str(&format!("class {}({enum_base}):\n", name)); - let type_doc = crate::codegen::shared::docs::DocText { + let type_doc = crate::codegen::winrt::shared::docs::DocText { summary: enum_doc, deprecated: enum_dep, returns: None, params: Vec::new(), }; - let type_ds = crate::codegen::python::docs::format_pydoc(&type_doc, " "); + let type_ds = crate::codegen::winrt::python::docs::format_pydoc(&type_doc, " "); if !type_ds.is_empty() { out.push_str(&type_ds); out.push('\n'); @@ -190,12 +190,14 @@ pub fn generate_interface( out.push_str(&format!("\nclass {}:\n", iface.name)); } { - let doc = crate::codegen::shared::docs::DocText { + let doc = crate::codegen::winrt::shared::docs::DocText { summary: iface.doc.as_deref(), deprecated: iface.deprecated.as_deref(), ..Default::default() }; - out.push_str(&crate::codegen::python::docs::format_pydoc(&doc, " ")); + out.push_str(&crate::codegen::winrt::python::docs::format_pydoc( + &doc, " ", + )); } out.push_str(" def __init__(self, obj: DynWinRTValue):\n"); if iface.generic_piid.is_some() { @@ -226,7 +228,7 @@ pub fn generate_interface( if let Some(ref piid) = iface.generic_piid { if piid == "913337e9-11a1-4345-a3a2-4e7f956e222d" && iface.generic_args.len() == 1 { let elem_type = py_dynwinrt_type(&iface.generic_args[0]); - let elem_annotation = crate::codegen::python::type_helpers::py_return_type_safe( + let elem_annotation = crate::codegen::winrt::python::type_helpers::py_return_type_safe( Some(&iface.generic_args[0]), known_types, ); @@ -244,11 +246,11 @@ pub fn generate_interface( } else if piid == "3c2925fe-8519-45c1-aa79-197b6718c1c1" && iface.generic_args.len() == 2 { let key_type = py_dynwinrt_type(&iface.generic_args[0]); let val_type = py_dynwinrt_type(&iface.generic_args[1]); - let key_annotation = crate::codegen::python::type_helpers::py_return_type_safe( + let key_annotation = crate::codegen::winrt::python::type_helpers::py_return_type_safe( Some(&iface.generic_args[0]), known_types, ); - let val_annotation = crate::codegen::python::type_helpers::py_return_type_safe( + let val_annotation = crate::codegen::winrt::python::type_helpers::py_return_type_safe( Some(&iface.generic_args[1]), known_types, ); @@ -281,7 +283,7 @@ pub fn generate_interface( // Instance methods (reorder so @property comes before @x.setter) let iface_var = format!("_{}", iface.name); - for methods in crate::codegen::python::overloads::grouped_methods( + for methods in crate::codegen::winrt::python::overloads::grouped_methods( reorder_getters_before_setters(&iface.methods), ) { out.push('\n'); diff --git a/tools/dynwinrt-codegen/src/codegen/python/method.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/python/method.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs index 70ce49b1..4c7936cd 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/method.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs @@ -6,7 +6,7 @@ use std::collections::HashSet; use crate::meta::{ClassMeta, InterfaceMeta, MethodMeta}; use crate::types::TypeMeta; -use crate::codegen::shared::imports::{ +use crate::codegen::winrt::shared::imports::{ fill_array_output_index, fill_array_uses_retval_count, get_in_params, }; diff --git a/tools/dynwinrt-codegen/src/codegen/python/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/mod.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/naming.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/naming.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/native_types.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/native_types.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/native_types.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/native_types.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/overloads.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/overloads.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/overloads.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/overloads.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/shared.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/shared.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/shared.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/shared.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/signature.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/python/signature.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs index 97a080d6..e7405d11 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/signature.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs @@ -9,9 +9,9 @@ use crate::meta::{InterfaceMeta, MethodMeta, ParamDirection}; use crate::types::TypeMeta; use super::naming::{to_snake_case, to_snake_case_filename}; -use crate::codegen::python::collections::{CollectionKind, is_mapping_input, type_kind}; -use crate::codegen::python::native_types::{FoundationType, foundation_type}; -use crate::codegen::shared::imports::ireference_inner_type; +use crate::codegen::winrt::python::collections::{CollectionKind, is_mapping_input, type_kind}; +use crate::codegen::winrt::python::native_types::{FoundationType, foundation_type}; +use crate::codegen::winrt::shared::imports::ireference_inner_type; pub(crate) fn py_runtime_symbol(type_name: &str, symbol_name: &str) -> String { format!( diff --git a/tools/dynwinrt-codegen/src/codegen/python/structs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/structs.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/structs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/structs.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/stub_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/python/stub_helpers.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs index 03dc721b..062c59be 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/stub_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; -use crate::codegen::shared::imports::get_in_params; +use crate::codegen::winrt::shared::imports::get_in_params; use crate::meta::MethodMeta; use crate::types::{TypeKind, TypeMeta}; diff --git a/tools/dynwinrt-codegen/src/codegen/python/stubs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/python/stubs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs index a9d8676c..b705c8ba 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/stubs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs @@ -11,11 +11,11 @@ use std::collections::HashSet; use crate::meta::{ClassMeta, InterfaceMeta, MethodMeta}; use crate::types::{TypeKind, TypeMeta}; -use crate::codegen::shared::imports::{ +use crate::codegen::winrt::shared::imports::{ collect_iface_type_imports, collect_type_imports, collect_used_generics_from_class, collect_used_generics_from_methods, }; -use crate::codegen::shared::structs::{ +use crate::codegen::winrt::shared::structs::{ collect_used_structs_from_class, collect_used_structs_from_iface, }; @@ -611,7 +611,7 @@ fn emit_constructor_stubs( if count > 1 { out.push_str(" @overload\n"); } - let in_params = crate::codegen::shared::imports::get_in_params(method); + let in_params = crate::codegen::winrt::shared::imports::get_in_params(method); let params = super::type_helpers::py_param_list(&in_params, known_types, delegate_type_names); if params.is_empty() { diff --git a/tools/dynwinrt-codegen/src/codegen/python/type_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs similarity index 98% rename from tools/dynwinrt-codegen/src/codegen/python/type_helpers.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs index ffedcd14..8b6c5f56 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/type_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs @@ -5,8 +5,8 @@ use std::collections::HashSet; -use crate::codegen::shared::docs::{DocText, find_param_doc}; -use crate::codegen::shared::imports::{ +use crate::codegen::winrt::shared::docs::{DocText, find_param_doc}; +use crate::codegen::winrt::shared::imports::{ fill_array_uses_retval_count, ireference_inner_type, method_abi_output_count, }; use crate::meta::MethodMeta; @@ -525,11 +525,11 @@ mod tests { "list[str]" ); assert_eq!( - crate::codegen::python::signature::py_build_method_sig(&method), + crate::codegen::winrt::python::signature::py_build_method_sig(&method), "DynWinRTMethodSig().add_in(DynWinRTType.u32_type()).add_out_fill(DynWinRTType.array_type(DynWinRTType.hstring())).add_out(DynWinRTType.u32_type())" ); assert_eq!( - crate::codegen::javascript::signature::build_method_sig(&method), + crate::codegen::winrt::javascript::signature::build_method_sig(&method), "new DynWinRtMethodSig().addIn(DynWinRtType.u32()).addOutFill(DynWinRtType.arrayType(DynWinRtType.hstring())).addOut(DynWinRtType.u32())" ); } diff --git a/tools/dynwinrt-codegen/src/codegen/shared/docs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/shared/docs.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/shared/docs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/shared/docs.rs diff --git a/tools/dynwinrt-codegen/src/codegen/shared/imports.rs b/tools/dynwinrt-codegen/src/codegen/winrt/shared/imports.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/shared/imports.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/shared/imports.rs diff --git a/tools/dynwinrt-codegen/src/codegen/shared/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/shared/mod.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/shared/mod.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/shared/mod.rs diff --git a/tools/dynwinrt-codegen/src/codegen/shared/structs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/shared/structs.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/shared/structs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/shared/structs.rs diff --git a/tools/dynwinrt-codegen/src/com_metadata.rs b/tools/dynwinrt-codegen/src/com_metadata.rs index fb2edcd5..93d3fad2 100644 --- a/tools/dynwinrt-codegen/src/com_metadata.rs +++ b/tools/dynwinrt-codegen/src/com_metadata.rs @@ -284,18 +284,21 @@ fn parse_methods( direction = ParamDirection::UnsupportedNativeArray { count_param_index }; } } - let free_with = - param - .find_attribute("FreeWithAttribute") - .and_then(|attribute| { - attribute.value().into_iter().next().and_then( - |(_, value)| match value { - windows_metadata::Value::Utf8(value) => Some(value), - _ => None, - }, - ) - }) - .or_else(|| known_free_with(typ, &direction)); + let free_with = param + .find_attribute("FreeWithAttribute") + .and_then(|attribute| { + attribute + .value() + .into_iter() + .next() + .and_then(|(_, value)| match value { + windows_metadata::Value::Utf8(value) => Some(value), + _ => None, + }) + }) + .or_else(|| { + known_free_with(def.namespace(), def.name(), method.name(), typ, &direction) + }); if let Some(free_with) = free_with { owned_outputs.push(OwnedOutput { param_index, @@ -311,7 +314,8 @@ fn parse_methods( mark_caller_owned_string_buffers(&mut params); let return_type = (signature.return_type != windows_metadata::Type::Void) .then(|| map_return_type(&signature.return_type, index)); - let preserve_hresult = method.has_attribute("CanReturnMultipleSuccessValuesAttribute"); + let preserve_hresult = method.has_attribute("CanReturnMultipleSuccessValuesAttribute") + || is_known_semantic_hresult(def.namespace(), def.name(), method.name()); MethodMeta { name, vtable_index: base_offset + index_in_interface, @@ -325,7 +329,13 @@ fn parse_methods( .collect() } -fn known_free_with(typ: &windows_metadata::Type, direction: &ParamDirection) -> Option { +fn known_free_with( + interface_namespace: &str, + interface_name: &str, + method_name: &str, + typ: &windows_metadata::Type, + direction: &ParamDirection, +) -> Option { let (windows_metadata::Type::PtrMut(inner, depth) | windows_metadata::Type::PtrConst(inner, depth)) = typ else { @@ -343,6 +353,22 @@ fn known_free_with(typ: &windows_metadata::Type, direction: &ParamDirection) -> { return Some("SysFreeString".into()); } + let is_known_cotaskmem_wide_string = matches!( + (interface_namespace, interface_name, method_name), + ("Windows.Win32.UI.Shell", "IShellItem", "GetDisplayName") + | ("Windows.Win32.UI.Shell", "IFileDialog", "GetFileName") + | ("Windows.Win32.System.Com", "IPersistFile", "GetCurFile") + ); + if *depth == 1 + && is_known_cotaskmem_wide_string + && matches!( + inner.as_ref(), + windows_metadata::Type::Name(name) + if name.namespace == "Windows.Win32.Foundation" && name.name == "PWSTR" + ) + { + return Some("CoTaskMemFree".into()); + } // Windows.Win32.winmd omits FreeWith on IShellLink::GetIDList. if *depth < 2 { return None; @@ -357,6 +383,17 @@ fn known_free_with(typ: &windows_metadata::Type, direction: &ParamDirection) -> } } +fn is_known_semantic_hresult( + interface_namespace: &str, + interface_name: &str, + method_name: &str, +) -> bool { + matches!( + (interface_namespace, interface_name, method_name), + ("Windows.Win32.System.Com", "IPersistFile", "GetCurFile") + ) +} + fn map_parameter_type( typ: &windows_metadata::Type, direction: &ParamDirection, @@ -819,7 +856,7 @@ mod tests { 2, ); assert_eq!( - known_free_with(&typ, &ParamDirection::Out).as_deref(), + known_free_with("", "", "", &typ, &ParamDirection::Out).as_deref(), Some("CoTaskMemFree") ); } @@ -833,6 +870,61 @@ mod tests { )), 2, ); - assert_eq!(known_free_with(&typ, &ParamDirection::Out), None); + assert_eq!( + known_free_with("", "", "", &typ, &ParamDirection::Out), + None + ); + } + + #[test] + fn documented_shell_wide_string_outputs_use_cotaskmem() { + let typ = windows_metadata::Type::PtrMut( + Box::new(windows_metadata::Type::named( + "Windows.Win32.Foundation", + "PWSTR", + )), + 1, + ); + for (interface, method) in [ + ("IShellItem", "GetDisplayName"), + ("IFileDialog", "GetFileName"), + ] { + assert_eq!( + known_free_with( + "Windows.Win32.UI.Shell", + interface, + method, + &typ, + &ParamDirection::Out + ) + .as_deref(), + Some("CoTaskMemFree") + ); + } + assert_eq!( + known_free_with( + "Windows.Win32.System.Com", + "IPersistFile", + "GetCurFile", + &typ, + &ParamDirection::Out + ) + .as_deref(), + Some("CoTaskMemFree") + ); + } + + #[test] + fn documented_get_cur_file_hresult_is_semantic() { + assert!(is_known_semantic_hresult( + "Windows.Win32.System.Com", + "IPersistFile", + "GetCurFile" + )); + assert!(!is_known_semantic_hresult( + "Windows.Win32.System.Com", + "IPersistFile", + "Load" + )); } } diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index e0e6b45e..d4417047 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -8,6 +8,10 @@ use windows_metadata::{HasAttributes, reader}; use crate::types::{EnumMember, TypeKind, TypeMeta, TypeRef}; +pub const WINDOWS_FOUNDATION_COLLECTIONS_NAMESPACE: &str = "Windows.Foundation.Collections"; +pub const PIID_IVECTOR: &str = "913337e9-11a1-4345-a3a2-4e7f956e222d"; +pub const PIID_IOBSERVABLE_VECTOR: &str = "5917eb53-50b4-4a0d-b309-65862b3f1dbc"; + /// Direction of a method parameter at the ABI level. #[derive(Debug, Clone, PartialEq)] pub enum ParamDirection { @@ -619,6 +623,18 @@ fn collect_all_refs_from_interfaces( ) { for i in interfaces { collect_all_refs_from_methods(&i.methods, known, named_out, param_out); + if i.generic_piid.as_deref() == Some(PIID_IOBSERVABLE_VECTOR) && i.generic_args.len() == 1 { + let vector = TypeMeta::Parameterized { + namespace: WINDOWS_FOUNDATION_COLLECTIONS_NAMESPACE.into(), + name: "IVector".into(), + piid: PIID_IVECTOR.into(), + args: i.generic_args.clone(), + }; + let concrete_name = make_parameterized_name("IVector", &i.generic_args); + if !known.contains(&concrete_name) { + param_out.push(vector); + } + } } } @@ -1817,6 +1833,9 @@ mod tests { #[cfg(test)] mod iface_tests { + use std::collections::HashSet; + + use crate::types::TypeMeta; use windows_metadata::reader; const WINDOWS_WINMD: &str = r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.26100.0\Windows.winmd"; @@ -1887,4 +1906,67 @@ mod iface_tests { ); } } + + #[test] + fn observable_vector_discovers_mutable_vector_dependency() { + let interface = super::InterfaceMeta { + name: "IObservableVector_ICommandBarElement".into(), + namespace: "Windows.Foundation.Collections".into(), + generic_piid: Some(super::PIID_IOBSERVABLE_VECTOR.into()), + generic_args: vec![TypeMeta::Interface { + namespace: "Microsoft.UI.Xaml.Controls".into(), + name: "ICommandBarElement".into(), + iid: "f8eb20b4-373e-5327-9942-66a1ea21f5f9".into(), + }], + ..Default::default() + }; + let mut named = Vec::new(); + let mut parameterized = Vec::new(); + + super::collect_all_refs_from_interfaces( + &[interface], + &HashSet::new(), + &mut named, + &mut parameterized, + ); + + assert!(named.is_empty()); + assert_eq!(parameterized.len(), 1); + assert!(matches!( + ¶meterized[0], + TypeMeta::Parameterized { + namespace, + name, + piid, + args, + } if namespace == super::WINDOWS_FOUNDATION_COLLECTIONS_NAMESPACE + && name == "IVector" + && piid == super::PIID_IVECTOR + && args == &vec![TypeMeta::Interface { + namespace: "Microsoft.UI.Xaml.Controls".into(), + name: "ICommandBarElement".into(), + iid: "f8eb20b4-373e-5327-9942-66a1ea21f5f9".into(), + }] + )); + } + + #[test] + fn observable_vector_dependency_is_resolved_for_emission() { + let observable = super::InterfaceMeta { + name: "IObservableVector_String".into(), + namespace: "Windows.Foundation.Collections".into(), + generic_piid: Some(super::PIID_IOBSERVABLE_VECTOR.into()), + generic_args: vec![TypeMeta::String], + ..Default::default() + }; + + let dependencies = super::resolve_dependencies(WINDOWS_WINMD, &[], &[observable], &[]); + + assert!( + dependencies + .interfaces + .iter() + .any(|interface| interface.name == "IVector_String") + ); + } } diff --git a/tools/dynwinrt-codegen/tests/observable_vector_test.rs b/tools/dynwinrt-codegen/tests/observable_vector_test.rs index d8c0a718..512f1758 100644 --- a/tools/dynwinrt-codegen/tests/observable_vector_test.rs +++ b/tools/dynwinrt-codegen/tests/observable_vector_test.rs @@ -34,6 +34,14 @@ fn observable_vector_projects_mutable_create_helper() { "{js}", ); assert!(js.contains("onVectorChanged")); + assert!( + js.contains("asVector: { value: observable.asVector.bind(observable) }"), + "{js}", + ); + assert!(js.contains( + "asVector() {\n return new ((__load_IVector_Object()).IVector_Object)(this._obj);", + )); + assert!(dts.contains("asVector(): IVector_Object;")); assert!(dts.contains( "static create(items: unknown[]): IObservableVector_Object & IVector_Object;", )); diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri/IIterator_IWwwFormUrlDecoderEntry.js b/tools/dynwinrt-codegen/tests/snapshots/uri/IIterator_IWwwFormUrlDecoderEntry.js index 6118ec7c..2559d151 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri/IIterator_IWwwFormUrlDecoderEntry.js +++ b/tools/dynwinrt-codegen/tests/snapshots/uri/IIterator_IWwwFormUrlDecoderEntry.js @@ -1,6 +1,6 @@ // Generated by dynwinrt-codegen — do not edit const { DynWinRtType, DynWinRtMethodSig, DynWinRtValue, DynWinRtArray, DynWinRtDelegate, WinGuid } = require('@microsoft/dynwinrt'); -const { IID_IWwwFormUrlDecoderEntry } = require('./IWwwFormUrlDecoderEntry.js'); +require('./IWwwFormUrlDecoderEntry.js'); let __m_IWwwFormUrlDecoderEntry; const __load_IWwwFormUrlDecoderEntry = () => (__m_IWwwFormUrlDecoderEntry ??= require('./IWwwFormUrlDecoderEntry.js')); const __get_IWwwFormUrlDecoderEntry = () => __load_IWwwFormUrlDecoderEntry().IWwwFormUrlDecoderEntry; diff --git a/tools/dynwinrt-codegen/tests/snapshots/uri/WwwFormUrlDecoder.js b/tools/dynwinrt-codegen/tests/snapshots/uri/WwwFormUrlDecoder.js index b5986ad0..6c5274fe 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/uri/WwwFormUrlDecoder.js +++ b/tools/dynwinrt-codegen/tests/snapshots/uri/WwwFormUrlDecoder.js @@ -3,7 +3,7 @@ const { DynWinRtType, DynWinRtMethodSig, DynWinRtValue, DynWinRtArray, DynWinRtD let __m_IIterator_IWwwFormUrlDecoderEntry; const __load_IIterator_IWwwFormUrlDecoderEntry = () => (__m_IIterator_IWwwFormUrlDecoderEntry ??= require('./IIterator_IWwwFormUrlDecoderEntry.js')); const __get_IIterator_IWwwFormUrlDecoderEntry = () => __load_IIterator_IWwwFormUrlDecoderEntry().IIterator_IWwwFormUrlDecoderEntry; -const { IID_IWwwFormUrlDecoderEntry } = require('./IWwwFormUrlDecoderEntry.js'); +require('./IWwwFormUrlDecoderEntry.js'); let __m_IWwwFormUrlDecoderEntry; const __load_IWwwFormUrlDecoderEntry = () => (__m_IWwwFormUrlDecoderEntry ??= require('./IWwwFormUrlDecoderEntry.js')); const __get_IWwwFormUrlDecoderEntry = () => __load_IWwwFormUrlDecoderEntry().IWwwFormUrlDecoderEntry; diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs index b79996e6..8d0655d5 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -1236,7 +1236,7 @@ fn unresolved_external_interface_fails_until_reference_metadata_is_loaded() { } #[test] -fn semantic_hresult_metadata_preserves_is_dirty_only() { +fn semantic_hresult_preserves_metadata_and_documented_contracts() { if !win32_available() { eprintln!("Skipping: Win32 winmd not available"); return; @@ -1260,10 +1260,18 @@ fn semantic_hresult_metadata_preserves_is_dirty_only() { .iter() .find(|method| method.name == "Load") .expect("Load must exist"); + let get_cur_file = interface + .interface + .methods + .iter() + .find(|method| method.name == "GetCurFile") + .expect("GetCurFile must exist"); assert!(is_dirty.preserve_hresult); + assert!(get_cur_file.preserve_hresult); assert!(!load.preserve_hresult); + let mut get_cur_file_interface = interface.clone(); interface .interface .methods @@ -1273,6 +1281,45 @@ fn semantic_hresult_metadata_preserves_is_dirty_only() { assert!(output.js.contains(".preserveHresult()")); assert!(output.js.contains("return DynCom.toNumber(_out);")); assert!(output.dts.contains("isDirty(): number;")); + + get_cur_file_interface + .interface + .methods + .retain(|method| method.name == "GetCurFile"); + let output = com::generate_com_interface_files(&get_cur_file_interface, &win32_winmd()) + .expect("GetCurFile semantic HRESULT generation must succeed"); + assert!(output.js.contains(".preserveHresult()")); + assert!(output.js.contains(".invokeAll(")); + assert!(output.js.contains("DynCom.toNumber(_r[0])")); + assert!(output.js.contains("DynCom.takeCoTaskMemWideString(_r[1])")); + assert!(output.dts.contains("getCurFile(): [number, string];")); +} + +#[test] +fn scalar_typedef_uses_its_underlying_abi_not_pointer_abi() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IPreviewHandlerVisuals", + ) + .expect("IPreviewHandlerVisuals must exist"); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("COLORREF scalar typedef must generate"); + + assert!(output.dts.contains("export type COLORREF = number;")); + assert!( + output + .dts + .contains("setBackgroundColor(color: COLORREF): void;") + ); + assert!(output.js.contains(".addIn(DynCom.u32Type())")); + assert!(output.js.contains("DynCom.u32(color)")); + assert!(!output.js.contains("DynCom.pointer(color)")); } #[test]