From 80959b2b03140abefbe75eb3dad4aa474319af55 Mon Sep 17 00:00:00 2001 From: "Leilei Zhang (from Dev Box)" Date: Thu, 20 Aug 2026 16:38:48 +0800 Subject: [PATCH] Add dynamic JavaScript COM implementations Add cached libffi callback closures, metadata-validated callback marshalling, multi-interface identity, inherited QueryInterface aliases, and generated implement/implementation APIs. Preserve fail-closed ownership and apartment rules, including transactional callback output publication. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 565c4300-9e07-40c2-8ad3-875138379a86 --- README.md | 3 +- .../tsfn-com-sink-exception-child.mjs | 32 + .../js/__test__/tsfn-com-sink-helpers.mjs | 18 + .../__test__/tsfn-worker-com-sink-child.mjs | 23 + .../__test__/tsfn-worker-com-sink-worker.mjs | 13 + bindings/js/__test__/tsfn.spec.ts | 437 +- bindings/js/scripts/generate-entrypoints.mjs | 4 + bindings/js/src/com.rs | 299 +- bindings/js/src/lib.rs | 213 +- bindings/js/src/tsfn_test_hooks.rs | 351 +- crates/dynwinrt/src/com.rs | 8429 ++++++++++++----- crates/dynwinrt/src/lib.rs | 1 + crates/dynwinrt/src/native_callback.rs | 313 + docs/architecture/classic-com-support.md | 55 +- docs/guides/windows/classic-com-usage.md | 87 + tests/e2e/e2e_test.ps1 | 11 +- tests/e2e/runners/com/drop-target.mjs | 72 + tests/e2e/runners/com/file-open-dialog.mjs | 49 +- tools/dynwinrt-codegen/src/codegen/com/ir.rs | 23 + .../src/codegen/com/javascript/render.rs | 295 +- .../codegen/com/javascript/render_tests.rs | 294 +- .../src/codegen/com/javascript/types.rs | 35 + .../codegen/com/project/legacy_diagnostics.rs | 3 + .../src/codegen/com/project/mod.rs | 401 +- tools/dynwinrt-codegen/src/com_metadata.rs | 12 + .../tests/com_sink_tsc_check_test.rs | 212 + .../snapshots/itaskbarlist3/ITaskbarList.d.ts | 19 +- .../snapshots/itaskbarlist3/ITaskbarList.js | 84 +- .../itaskbarlist3/ITaskbarList2.d.ts | 20 +- .../snapshots/itaskbarlist3/ITaskbarList2.js | 91 +- .../itaskbarlist3/ITaskbarList3.d.ts | 32 +- .../snapshots/itaskbarlist3/ITaskbarList3.js | 164 +- .../itaskbarlist3/ITaskbarList4.d.ts | 33 +- .../snapshots/itaskbarlist3/ITaskbarList4.js | 171 +- .../dynwinrt-codegen/tests/win32_com_test.rs | 54 + 35 files changed, 9593 insertions(+), 2760 deletions(-) create mode 100644 bindings/js/__test__/tsfn-com-sink-exception-child.mjs create mode 100644 bindings/js/__test__/tsfn-com-sink-helpers.mjs create mode 100644 bindings/js/__test__/tsfn-worker-com-sink-child.mjs create mode 100644 bindings/js/__test__/tsfn-worker-com-sink-worker.mjs create mode 100644 crates/dynwinrt/src/native_callback.rs create mode 100644 tests/e2e/runners/com/drop-target.mjs create mode 100644 tools/dynwinrt-codegen/tests/com_sink_tsc_check_test.rs diff --git a/README.md b/README.md index ad63e80c..a62214b3 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,8 @@ part of the same npm package, while the package root remains WinRT-only. See [Classic COM support](docs/architecture/classic-com-support.md) for the supported ABI, common-interface test matrix, unsupported native types, and ownership rules. See [Classic COM JavaScript usage](docs/guides/windows/classic-com-usage.md) for codegen, -GUID/IID/CLSID, lifecycle, Automation, and explicit unsafe ABI examples. +GUID/IID/CLSID, lifecycle, generated same-thread event sinks, Automation, and explicit unsafe ABI +examples. Generated bindings project unambiguous public WinRT activation metadata as JavaScript constructors, including overloads such as `new Uri(base, relative)`. Existing diff --git a/bindings/js/__test__/tsfn-com-sink-exception-child.mjs b/bindings/js/__test__/tsfn-com-sink-exception-child.mjs new file mode 100644 index 00000000..5c07b7fe --- /dev/null +++ b/bindings/js/__test__/tsfn-com-sink-exception-child.mjs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from 'node:assert/strict' +import { createRequire } from 'node:module' +import { registerTestComSinkInterface } from './tsfn-com-sink-helpers.mjs' + +const runtime = createRequire(import.meta.url)(process.env.DYNWINRT_TEST_RUNTIME) +let observed +process.once('uncaughtException', (error) => { + observed = error +}) + +const sink = runtime.DynCom.createIUnknownSink(registerTestComSinkInterface(runtime), () => { + throw new Error('COM sink callback failure') +}) +runtime.tsfnTestRetainComSink(sink) +assert.equal(runtime.tsfnTestRegisteredHandleCount(), 1) + +const hr = runtime.tsfnTestInvokeRetainedComSink() +assert.equal(hr, 0x80004005 | 0) +runtime.tsfnTestReleaseRetainedComSink() +sink.release() + +const deadline = Date.now() + 2_000 +while ((observed === undefined || runtime.tsfnTestRegisteredHandleCount() !== 0) && Date.now() < deadline) { + await new Promise((resolve) => setTimeout(resolve, 5)) +} + +assert.equal(observed?.message, 'COM sink callback failure') +assert.equal(runtime.tsfnTestRegisteredHandleCount(), 0) +console.log(`com-sink-uncaught:${observed.message};hr:${hr}`) diff --git a/bindings/js/__test__/tsfn-com-sink-helpers.mjs b/bindings/js/__test__/tsfn-com-sink-helpers.mjs new file mode 100644 index 00000000..ff135188 --- /dev/null +++ b/bindings/js/__test__/tsfn-com-sink-helpers.mjs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +export function registerTestComSinkInterface(runtime, withOutput = false) { + const iid = runtime.WinGuid.parse('7ac2eaa2-97a4-43f0-9b0f-421c2363ef11') + const interfaceType = runtime.DynCom.interfaceType(runtime.WinGuid.parse('00000000-0000-0000-c000-000000000046')) + let signature = new runtime.DynComMethodSig().addIn(interfaceType) + if (withOutput) { + signature = signature.addIn(interfaceType).addOut(runtime.DynCom.i32Type()) + } + return runtime.DynCom.registerIUnknownInterface('Tests.IComSink', iid).addMethodAt(3, 'Invoke', signature) +} + +export function registerTestComI32SinkInterface(runtime) { + const iid = runtime.WinGuid.parse('7ac2eaa2-97a4-43f0-9b0f-421c2363ef11') + const signature = new runtime.DynComMethodSig().addIn(runtime.DynCom.i32Type()) + return runtime.DynCom.registerIUnknownInterface('Tests.IComI32Sink', iid).addMethodAt(3, 'Invoke', signature) +} diff --git a/bindings/js/__test__/tsfn-worker-com-sink-child.mjs b/bindings/js/__test__/tsfn-worker-com-sink-child.mjs new file mode 100644 index 00000000..90c474d0 --- /dev/null +++ b/bindings/js/__test__/tsfn-worker-com-sink-child.mjs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from 'node:assert/strict' +import { createRequire } from 'node:module' +import { Worker } from 'node:worker_threads' + +const runtimePath = process.env.DYNWINRT_TEST_RUNTIME +const runtime = createRequire(import.meta.url)(runtimePath) +const worker = new Worker(new URL('./tsfn-worker-com-sink-worker.mjs', import.meta.url), { + workerData: { runtimePath }, +}) + +await new Promise((resolve, reject) => { + worker.once('message', resolve) + worker.once('error', reject) +}) +await worker.terminate() + +const hr = runtime.tsfnTestInvokeRetainedComSink() +console.log(`late-com-sink-hr:${hr}`) +assert.equal(hr, 0x8001010e | 0) +runtime.tsfnTestReleaseRetainedComSink() diff --git a/bindings/js/__test__/tsfn-worker-com-sink-worker.mjs b/bindings/js/__test__/tsfn-worker-com-sink-worker.mjs new file mode 100644 index 00000000..fa5b5957 --- /dev/null +++ b/bindings/js/__test__/tsfn-worker-com-sink-worker.mjs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { createRequire } from 'node:module' +import { parentPort, workerData } from 'node:worker_threads' +import { registerTestComSinkInterface } from './tsfn-com-sink-helpers.mjs' + +const runtime = createRequire(import.meta.url)(workerData.runtimePath) +const sink = runtime.DynCom.createIUnknownSink(registerTestComSinkInterface(runtime), () => 0) +runtime.tsfnTestRetainComSink(sink) +parentPort.postMessage('retained') + +Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5_000) diff --git a/bindings/js/__test__/tsfn.spec.ts b/bindings/js/__test__/tsfn.spec.ts index 7e2efb57..4f8e3c07 100644 --- a/bindings/js/__test__/tsfn.spec.ts +++ b/bindings/js/__test__/tsfn.spec.ts @@ -21,6 +21,17 @@ interface TsfnDelegateInvokeStats { failed: number } +interface TsfnComSinkInvokeResult { + hresult: number + output: number +} + +interface TsfnComAllocatedBufferResult { + hresult: number + count: number + byteSum: number +} + interface TsfnTestRuntime { tsfnTestReset(): void tsfnTestStats(): TsfnStats @@ -36,6 +47,88 @@ interface TsfnTestRuntime { tsfnTestInvokeRetainedDelegateOnThread(): number tsfnTestInvokeRetainedDelegateOnThreadMany(count: number): TsfnDelegateInvokeStats tsfnTestReleaseRetainedDelegate(): void + tsfnTestRetainComSink(value: unknown): void + tsfnTestInvokeRetainedComSink(): number + tsfnTestInvokeRetainedComSinkOnThread(): number + tsfnTestInvokeRetainedComSinkOut(): TsfnComSinkInvokeResult + tsfnTestInvokeRetainedComSinkI32(value: number): number + tsfnTestInvokeRetainedComSinkI32OnThread(value: number): number + tsfnTestInvokeRetainedComSinkDirectI32(value: number): number + tsfnTestInvokeRetainedComSinkDirectI32OnThread(value: number): number + tsfnTestInvokeRetainedComSinkVoidI32(value: number): void + tsfnTestInvokeRetainedComSinkGuid(): number + tsfnTestInvokeRetainedComSinkBstr(): number + tsfnTestInvokeRetainedComSinkWideString(): number + tsfnTestInvokeRetainedComSinkAnsiString(): number + tsfnTestInvokeRetainedComSinkAllocatedBuffer(): TsfnComAllocatedBufferResult + tsfnTestInvokeRetainedComSinkCallerBuffer(): TsfnComAllocatedBufferResult + tsfnTestInvokeRetainedComObjectI32(iid: unknown, value: number): number + tsfnTestReleaseRetainedComSink(): void +} + +interface TestComMethodSig { + addIn(type: unknown): TestComMethodSig + addOut(type: unknown): TestComMethodSig + returns(type: unknown): TestComMethodSig + returnsVoid(): TestComMethodSig + addCoTaskMemOutputBuffer(elementType: unknown, countParam: number, countIsBytes: boolean): TestComMethodSig + addCallerOutputBuffer( + elementType: unknown, + capacityParam: number, + actualLengthParam: number | undefined, + countIsBytes: boolean, + twoCall: boolean, + ): TestComMethodSig +} + +interface TestComInterface { + addMethodAt(vtableIndex: number, name: string, signature: TestComMethodSig): TestComInterface +} + +interface ComSinkTestRuntime extends TsfnTestRuntime { + DynCom: { + registerIUnknownInterface(name: string, iid: unknown): TestComInterface + interfaceType(iid: unknown): unknown + i32Type(): unknown + u8Type(): unknown + u32Type(): unknown + bstrType(): unknown + pointerType(): unknown + buffer(value: ArrayBufferView): unknown + copyCallbackGuid(value: unknown): string + copyCallbackBstr(value: unknown): string | null + copyCallbackWideString(value: unknown): string | null + copyCallbackAnsiString(value: unknown): string | null + createIUnknownSink( + interfaceType: TestComInterface, + callback: (vtableIndex: number, ...args: unknown[]) => unknown, + ): { release(): void } + createComObject( + interfaces: TestComInterface[], + callback: (interfaceIid: string, vtableIndex: number, ...args: unknown[]) => unknown, + ): { release(): void } + } + DynComMethodSig: new () => TestComMethodSig + WinGuid: { + parse(value: string): unknown + } +} + +function registerTestComSinkInterface(native: ComSinkTestRuntime, withOutput: boolean): TestComInterface { + const iid = native.WinGuid.parse('7ac2eaa2-97a4-43f0-9b0f-421c2363ef11') + const interfaceType = native.DynCom.interfaceType(native.WinGuid.parse('00000000-0000-0000-c000-000000000046')) + let signature = new native.DynComMethodSig().addIn(interfaceType) + if (withOutput) { + signature = signature.addIn(interfaceType).addOut(native.DynCom.i32Type()) + } + + return native.DynCom.registerIUnknownInterface('Tests.IComSink', iid).addMethodAt(3, 'Invoke', signature) +} + +function registerTestComI32SinkInterface(native: ComSinkTestRuntime): TestComInterface { + const iid = native.WinGuid.parse('7ac2eaa2-97a4-43f0-9b0f-421c2363ef11') + const signature = new native.DynComMethodSig().addIn(native.DynCom.i32Type()) + return native.DynCom.registerIUnknownInterface('Tests.IComI32Sink', iid).addMethodAt(3, 'Invoke', signature) } const runtime = createRequire(import.meta.url)('../dist/index.js') as Partial @@ -58,9 +151,13 @@ if (!hasTestHooks) { const count = 10_000 let callbacks = 0 runtime.tsfnTestReset!() - runtime.tsfnTestStartUnbounded!(() => { - callbacks += 1 - }, count, 0) + runtime.tsfnTestStartUnbounded!( + () => { + callbacks += 1 + }, + count, + 0, + ) t.true(runtime.tsfnTestWaitProduced!(count, 2_000)) const queued = runtime.tsfnTestStats!() @@ -87,18 +184,14 @@ if (!hasTestHooks) { }) test.serial('environment teardown releases every payload already queued in a TSFN', async (t) => { - const child = spawn( - process.execPath, - [fileURLToPath(new URL('./tsfn-teardown-child.mjs', import.meta.url))], - { - env: { - ...process.env, - DYNWINRT_TEST_RUNTIME: fileURLToPath(new URL('../dist/index.js', import.meta.url)), - }, - stdio: ['ignore', 'pipe', 'pipe'], - windowsHide: true, + const child = spawn(process.execPath, [fileURLToPath(new URL('./tsfn-teardown-child.mjs', import.meta.url))], { + env: { + ...process.env, + DYNWINRT_TEST_RUNTIME: fileURLToPath(new URL('../dist/index.js', import.meta.url)), }, - ) + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + }) let stdout = '' let stderr = '' child.stdout.on('data', (chunk) => { @@ -116,18 +209,14 @@ if (!hasTestHooks) { }) test.serial('a thrown TSFN callback reaches uncaughtException', async (t) => { - const child = spawn( - process.execPath, - [fileURLToPath(new URL('./tsfn-exception-child.mjs', import.meta.url))], - { - env: { - ...process.env, - DYNWINRT_TEST_RUNTIME: fileURLToPath(new URL('../dist/index.js', import.meta.url)), - }, - stdio: ['ignore', 'pipe', 'pipe'], - windowsHide: true, + const child = spawn(process.execPath, [fileURLToPath(new URL('./tsfn-exception-child.mjs', import.meta.url))], { + env: { + ...process.env, + DYNWINRT_TEST_RUNTIME: fileURLToPath(new URL('../dist/index.js', import.meta.url)), }, - ) + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + }) let stdout = '' let stderr = '' child.stdout.on('data', (chunk) => { @@ -228,6 +317,64 @@ if (!hasTestHooks) { t.regex(stdout, /late-delegate-hr:-2147467259/) }) + test.serial('a COM sink retained past Worker teardown rejects late invocation without crashing', async (t) => { + const child = spawn( + process.execPath, + [fileURLToPath(new URL('./tsfn-worker-com-sink-child.mjs', import.meta.url))], + { + env: { + ...process.env, + DYNWINRT_TEST_RUNTIME: fileURLToPath(new URL('../dist/index.js', import.meta.url)), + }, + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + }, + ) + let stdout = '' + let stderr = '' + child.stdout.on('data', (chunk) => { + stdout += String(chunk) + }) + child.stderr.on('data', (chunk) => { + stderr += String(chunk) + }) + const code = await new Promise((resolve) => { + child.once('close', resolve) + }) + + t.is(code, 0, `${stdout}\n${stderr}`) + t.regex(stdout, /late-com-sink-hr:-2147417842/) + }) + + test.serial('a thrown COM sink callback fails the call and releases callback resources', async (t) => { + const child = spawn( + process.execPath, + [fileURLToPath(new URL('./tsfn-com-sink-exception-child.mjs', import.meta.url))], + { + env: { + ...process.env, + DYNWINRT_TEST_RUNTIME: fileURLToPath(new URL('../dist/index.js', import.meta.url)), + }, + stdio: ['ignore', 'pipe', 'pipe'], + windowsHide: true, + }, + ) + let stdout = '' + let stderr = '' + child.stdout.on('data', (chunk) => { + stdout += String(chunk) + }) + child.stderr.on('data', (chunk) => { + stderr += String(chunk) + }) + const code = await new Promise((resolve) => { + child.once('close', resolve) + }) + + t.is(code, 0, `${stdout}\n${stderr}`) + t.regex(stdout, /com-sink-uncaught:COM sink callback failure;hr:-2147467259/) + }) + test.serial('same-thread delegate dispatch preserves AsyncLocalStorage context', async (t) => { const native = runtime as TsfnTestRuntime & { DynWinRtDelegate: { @@ -240,13 +387,9 @@ if (!hasTestHooks) { const storage = new AsyncLocalStorage() let observed: string | undefined const delegate = storage.run('tsfn-context', () => - native.DynWinRtDelegate.create( - native.WinGuid.parse('45396ba0-cd24-42d4-9685-6863e032d69d'), - [], - () => { - observed = storage.getStore() - }, - ), + native.DynWinRtDelegate.create(native.WinGuid.parse('45396ba0-cd24-42d4-9685-6863e032d69d'), [], () => { + observed = storage.getStore() + }), ) native.tsfnTestRetainDelegate(delegate) @@ -266,6 +409,236 @@ if (!hasTestHooks) { t.is(observed, 'tsfn-context') }) + test.serial('COM sinks execute synchronously on their owner thread and reject other threads', (t) => { + const native = runtime as ComSinkTestRuntime + let calls = 0 + const sink = native.DynCom.createIUnknownSink(registerTestComSinkInterface(native, false), (vtableIndex, value) => { + t.is(vtableIndex, 3) + t.truthy(value) + calls += 1 + return 0 + }) + runtime.tsfnTestRetainComSink!(sink) + + t.is(runtime.tsfnTestInvokeRetainedComSink!(), 0) + t.is(calls, 1) + t.is(runtime.tsfnTestInvokeRetainedComSinkOnThread!(), -2147417842) + t.is(calls, 1) + + runtime.tsfnTestReleaseRetainedComSink!() + sink.release() + }) + + test.serial('COM sinks synchronously return HRESULT and required i32 output', (t) => { + const native = runtime as ComSinkTestRuntime + const sink = native.DynCom.createIUnknownSink( + registerTestComSinkInterface(native, true), + (vtableIndex, first, second) => { + t.is(vtableIndex, 3) + t.truthy(first) + t.truthy(second) + return [1, native.DynCom.i32(42)] + }, + ) + runtime.tsfnTestRetainComSink!(sink) + + t.deepEqual(runtime.tsfnTestInvokeRetainedComSinkOut!(), { + hresult: 1, + output: 42, + }) + + runtime.tsfnTestReleaseRetainedComSink!() + sink.release() + }) + + test.serial('COM sinks use libffi for runtime scalar callback signatures', (t) => { + const native = runtime as ComSinkTestRuntime + let calls = 0 + const sink = native.DynCom.createIUnknownSink(registerTestComI32SinkInterface(native), (vtableIndex, value) => { + t.is(vtableIndex, 3) + calls += 1 + const scalar = value as { toNumber(): number } + return scalar.toNumber() + 1 + }) + runtime.tsfnTestRetainComSink!(sink) + + t.is(runtime.tsfnTestInvokeRetainedComSinkI32!(41), 42) + t.is(runtime.tsfnTestInvokeRetainedComSinkI32OnThread!(41), -2147417842) + t.is(calls, 1) + + runtime.tsfnTestReleaseRetainedComSink!() + sink.release() + }) + + test.serial('COM sinks use libffi for direct and void native returns', (t) => { + const native = runtime as ComSinkTestRuntime + const iid = native.WinGuid.parse('16787a9f-b53c-41ba-87e7-f368950a79df') + const directInterface = native.DynCom.registerIUnknownInterface('Tests.IDirectSink', iid).addMethodAt( + 3, + 'Invoke', + new native.DynComMethodSig().addIn(native.DynCom.i32Type()).returns(native.DynCom.i32Type()), + ) + const direct = native.DynCom.createIUnknownSink(directInterface, (_slot, value) => + native.DynCom.i32((value as { toNumber(): number }).toNumber() + 1), + ) + runtime.tsfnTestRetainComSink!(direct) + t.is(runtime.tsfnTestInvokeRetainedComSinkDirectI32!(41), 42) + t.is(runtime.tsfnTestInvokeRetainedComSinkDirectI32OnThread!(41), 0) + runtime.tsfnTestReleaseRetainedComSink!() + direct.release() + + let observed = 0 + const voidInterface = native.DynCom.registerIUnknownInterface('Tests.IVoidSink', iid).addMethodAt( + 3, + 'Invoke', + new native.DynComMethodSig().addIn(native.DynCom.i32Type()).returnsVoid(), + ) + const voidSink = native.DynCom.createIUnknownSink(voidInterface, (_slot, value) => { + observed = (value as { toNumber(): number }).toNumber() + }) + runtime.tsfnTestRetainComSink!(voidSink) + runtime.tsfnTestInvokeRetainedComSinkVoidI32!(27) + t.is(observed, 27) + runtime.tsfnTestReleaseRetainedComSink!() + voidSink.release() + }) + + test.serial('COM sinks copy REFGUID inputs and allocate CoTaskMem output buffers', (t) => { + const native = runtime as ComSinkTestRuntime + const iid = native.WinGuid.parse('f25cce61-827f-4b11-b13f-8e276b0e67a9') + let observedGuid = '' + const guidInterface = native.DynCom.registerIUnknownInterface('Tests.IGuidSink', iid).addMethodAt( + 3, + 'Invoke', + new native.DynComMethodSig().addIn(native.DynCom.pointerType()), + ) + const guidSink = native.DynCom.createIUnknownSink(guidInterface, (_slot, value) => { + observedGuid = native.DynCom.copyCallbackGuid(value) + return 0 + }) + runtime.tsfnTestRetainComSink!(guidSink) + t.is(runtime.tsfnTestInvokeRetainedComSinkGuid!(), 0) + t.is(observedGuid.toLowerCase(), '990c600e-60c7-4d28-af4c-bf148a92b11a') + runtime.tsfnTestReleaseRetainedComSink!() + guidSink.release() + + let observedBstr: string | null = null + const bstrInterface = native.DynCom.registerIUnknownInterface('Tests.IBstrSink', iid).addMethodAt( + 3, + 'Invoke', + new native.DynComMethodSig().addIn(native.DynCom.bstrType()), + ) + const bstrSink = native.DynCom.createIUnknownSink(bstrInterface, (_slot, value) => { + observedBstr = native.DynCom.copyCallbackBstr(value) + return 0 + }) + runtime.tsfnTestRetainComSink!(bstrSink) + t.is(runtime.tsfnTestInvokeRetainedComSinkBstr!(), 0) + t.is(observedBstr, 'embedded\0callback') + runtime.tsfnTestReleaseRetainedComSink!() + bstrSink.release() + + const stringInterface = (name: string) => + native.DynCom.registerIUnknownInterface(name, iid).addMethodAt( + 3, + 'Invoke', + new native.DynComMethodSig().addIn(native.DynCom.pointerType()), + ) + let observedString: string | null = null + const wideSink = native.DynCom.createIUnknownSink(stringInterface('Tests.IWideStringSink'), (_slot, value) => { + observedString = native.DynCom.copyCallbackWideString(value) + return 0 + }) + runtime.tsfnTestRetainComSink!(wideSink) + t.is(runtime.tsfnTestInvokeRetainedComSinkWideString!(), 0) + t.is(observedString, 'wide callback') + runtime.tsfnTestReleaseRetainedComSink!() + wideSink.release() + + const ansiSink = native.DynCom.createIUnknownSink(stringInterface('Tests.IAnsiStringSink'), (_slot, value) => { + observedString = native.DynCom.copyCallbackAnsiString(value) + return 0 + }) + runtime.tsfnTestRetainComSink!(ansiSink) + t.is(runtime.tsfnTestInvokeRetainedComSinkAnsiString!(), 0) + t.is(observedString, 'ansi callback') + runtime.tsfnTestReleaseRetainedComSink!() + ansiSink.release() + + const bufferInterface = native.DynCom.registerIUnknownInterface('Tests.IBufferSink', iid).addMethodAt( + 3, + 'Invoke', + new native.DynComMethodSig() + .addCoTaskMemOutputBuffer(native.DynCom.u8Type(), 1, false) + .addOut(native.DynCom.u32Type()), + ) + const bufferSink = native.DynCom.createIUnknownSink(bufferInterface, () => [ + 0, + native.DynCom.buffer(Uint8Array.from([4, 5, 6])), + ]) + runtime.tsfnTestRetainComSink!(bufferSink) + t.deepEqual(runtime.tsfnTestInvokeRetainedComSinkAllocatedBuffer!(), { + hresult: 0, + count: 3, + byteSum: 15, + }) + runtime.tsfnTestReleaseRetainedComSink!() + bufferSink.release() + + const callerBufferInterface = native.DynCom.registerIUnknownInterface('Tests.ICallerBufferSink', iid).addMethodAt( + 3, + 'Invoke', + new native.DynComMethodSig() + .addCallerOutputBuffer(native.DynCom.u8Type(), 1, 2, false, false) + .addIn(native.DynCom.u32Type()) + .addOut(native.DynCom.u32Type()), + ) + const callerBufferSink = native.DynCom.createIUnknownSink(callerBufferInterface, (_slot, capacity) => { + t.is((capacity as { toNumber(): number }).toNumber(), 5) + return [0, native.DynCom.buffer(Uint8Array.from([9, 8, 7]))] + }) + runtime.tsfnTestRetainComSink!(callerBufferSink) + t.deepEqual(runtime.tsfnTestInvokeRetainedComSinkCallerBuffer!(), { + hresult: 0, + count: 3, + byteSum: 24, + }) + runtime.tsfnTestReleaseRetainedComSink!() + callerBufferSink.release() + }) + + test.serial('COM objects dispatch multiple interface views through one identity', (t) => { + const native = runtime as ComSinkTestRuntime + const firstIid = native.WinGuid.parse('a4c5b87d-f5cc-420b-93bd-a01b9415de83') + const secondIid = native.WinGuid.parse('8a28f0f7-8d77-46aa-a9d1-95d01f6b3179') + const register = (name: string, iid: unknown) => + native.DynCom.registerIUnknownInterface(name, iid).addMethodAt( + 3, + 'Invoke', + new native.DynComMethodSig().addIn(native.DynCom.i32Type()), + ) + const seen = new Map() + const object = native.DynCom.createComObject( + [register('Tests.IFirst', firstIid), register('Tests.ISecond', secondIid)], + (iid, vtableIndex, value) => { + t.is(vtableIndex, 3) + seen.set(iid, (value as { toNumber(): number }).toNumber()) + return 0 + }, + ) + runtime.tsfnTestRetainComSink!(object) + + t.is(runtime.tsfnTestInvokeRetainedComObjectI32!(firstIid, 11), 0) + t.is(runtime.tsfnTestInvokeRetainedComObjectI32!(secondIid, 22), 0) + t.deepEqual( + [...seen.values()].sort((left, right) => left - right), + [11, 22], + ) + + runtime.tsfnTestReleaseRetainedComSink!() + object.release() + }) + test.serial('cross-thread delegate S_OK means queued, not executed', async (t) => { const native = runtime as TsfnTestRuntime & { DynWinRtDelegate: { diff --git a/bindings/js/scripts/generate-entrypoints.mjs b/bindings/js/scripts/generate-entrypoints.mjs index f193dc1d..dbef763d 100644 --- a/bindings/js/scripts/generate-entrypoints.mjs +++ b/bindings/js/scripts/generate-entrypoints.mjs @@ -40,11 +40,15 @@ const comTypeExports = [ ] const opaqueComDeclarations = [ 'declare const dynComAllocationBrand: unique symbol', + 'declare const dynComImplementationBrand: unique symbol', 'export interface DynComAllocation {', ' readonly [dynComAllocationBrand]: never', ' readonly released: boolean', ' release(): void', '}', + 'export interface DynComImplementation {', + ' readonly [dynComImplementationBrand]: never', + '}', ] const comUnsafeExports = new Set([ ...comExports, diff --git a/bindings/js/src/com.rs b/bindings/js/src/com.rs index 52f892f6..ceffc97e 100644 --- a/bindings/js/src/com.rs +++ b/bindings/js/src/com.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use napi::bindgen_prelude::{BigInt, Buffer, FromNapiValue, ToNapiValue, Unknown}; +use napi::bindgen_prelude::{BigInt, Buffer, FromNapiValue, Function, ToNapiValue, Unknown}; use napi::JsValue; use napi_derive::napi; use windows::core::{IUnknown, Interface as _, GUID}; @@ -299,6 +299,223 @@ fn parse_clsid(clsid: &str) -> napi::Result { .map_err(|_| napi::Error::from_reason(format!("Invalid CLSID: '{clsid}'"))) } +fn callback_string_pointer(value: &DynWinRTValue) -> napi::Result<*const std::ffi::c_void> { + match &value.0 { + dynwinrt::WinRTValue::RawPtr(value) => Ok(value.cast_const()), + dynwinrt::WinRTValue::Null => Ok(std::ptr::null()), + _ => Err(napi::Error::from_reason( + "COM callback string value is not a native pointer", + )), + } +} + +fn sink_callback_i32( + env: napi::sys::napi_env, + value: napi::sys::napi_value, + context: &str, +) -> napi::Result { + let mut value_type = napi::sys::ValueType::napi_undefined; + super::napi_status("napi_typeof(COM sink result)", unsafe { + napi::sys::napi_typeof(env, value, &mut value_type) + })?; + if value_type != napi::sys::ValueType::napi_number { + return Err(napi::Error::from_reason(format!( + "{context} must be a 32-bit integer", + ))); + } + let mut number = 0.0; + super::napi_status("napi_get_value_double(COM sink result)", unsafe { + napi::sys::napi_get_value_double(env, value, &mut number) + })?; + if !number.is_finite() + || number.fract() != 0.0 + || number < i32::MIN as f64 + || number > i32::MAX as f64 + { + return Err(napi::Error::from_reason(format!( + "{context} must be a 32-bit integer", + ))); + } + Ok(number as i32) +} + +fn parse_sink_callback_result( + env: napi::sys::napi_env, + value: napi::sys::napi_value, + contract: dynwinrt::com::CallbackContract, +) -> napi::Result { + if contract.return_kind == dynwinrt::com::CallbackReturnKind::HResult + && contract.output_count == 0 + { + return sink_callback_i32(env, value, "COM sink HRESULT") + .map(|value| dynwinrt::com::SinkCallbackResult::hresult(windows::core::HRESULT(value))); + } + if contract.return_kind == dynwinrt::com::CallbackReturnKind::Void && contract.output_count == 0 { + return Ok(dynwinrt::com::SinkCallbackResult::hresult( + windows::core::HRESULT(0), + )); + } + if contract.return_kind == dynwinrt::com::CallbackReturnKind::Value && contract.output_count == 0 + { + return Ok(dynwinrt::com::SinkCallbackResult::with_return( + callback_com_value(env, value)?, + )); + } + let mut is_array = false; + super::napi_status("napi_is_array(COM sink result)", unsafe { + napi::sys::napi_is_array(env, value, &mut is_array) + })?; + if !is_array { + return Err(napi::Error::from_reason( + "COM sink callback with multiple native results must return an array", + )); + } + let mut length = 0; + super::napi_status("napi_get_array_length(COM sink result)", unsafe { + napi::sys::napi_get_array_length(env, value, &mut length) + })?; + let prefix = usize::from(contract.return_kind != dynwinrt::com::CallbackReturnKind::Void); + if length as usize != contract.output_count + prefix { + return Err(napi::Error::from_reason( + "COM sink callback returned an unexpected number of outputs", + )); + } + let mut hresult = windows::core::HRESULT(0); + let mut return_value = None; + if contract.return_kind == dynwinrt::com::CallbackReturnKind::HResult { + let mut raw = std::ptr::null_mut(); + super::napi_status("napi_get_element(COM sink HRESULT)", unsafe { + napi::sys::napi_get_element(env, value, 0, &mut raw) + })?; + hresult = windows::core::HRESULT(sink_callback_i32(env, raw, "COM sink HRESULT")?); + } else if contract.return_kind == dynwinrt::com::CallbackReturnKind::Value { + let mut raw = std::ptr::null_mut(); + super::napi_status("napi_get_element(COM sink return)", unsafe { + napi::sys::napi_get_element(env, value, 0, &mut raw) + })?; + return_value = Some(callback_com_value(env, raw)?); + } + let mut outputs = Vec::with_capacity(contract.output_count); + for index in 0..contract.output_count { + let mut output = std::ptr::null_mut(); + super::napi_status("napi_get_element(COM sink output)", unsafe { + napi::sys::napi_get_element(env, value, (index + prefix) as u32, &mut output) + })?; + outputs.push(callback_com_value(env, output)?); + } + Ok(dynwinrt::com::SinkCallbackResult { + hresult, + return_value, + outputs, + }) +} + +fn callback_com_value( + env: napi::sys::napi_env, + value: napi::sys::napi_value, +) -> napi::Result { + let value = unsafe { <&DynWinRTValue>::from_napi_value(env, value) }?; + let value = value.to_com_value()?; + Ok(match value { + dynwinrt::com::Value::Buffer(buffer) => { + let count = buffer.count(); + dynwinrt::com::Value::Buffer(dynwinrt::com::ComBufferValue::from_owned_bytes( + buffer.copy_bytes().map_err(com_error)?, + count, + )) + } + value => value, + }) +} + +fn create_iunknown_sink( + interface: &DynComInterface, + callback: Function<'static, Vec, ()>, +) -> napi::Result { + create_com_object_value(vec![interface], callback, false) +} + +fn create_com_object_value( + interfaces: Vec<&DynComInterface>, + callback: Function<'static, Vec, ()>, + include_iid: bool, +) -> napi::Result { + if interfaces.is_empty() { + return Err(napi::Error::from_reason( + "COM object requires at least one interface", + )); + } + let owner_thread = std::thread::current().id(); + let env = callback.value().env; + let raw_callback = napi::JsValue::raw(&callback); + let (callback_ref, async_context, finalizer) = + super::create_direct_callback_resources(env, raw_callback, b"dynwinrt.comSink")?; + let tsfn = super::managed_tsfn::ManagedTsfn::create( + env, + raw_callback, + 1, + false, + |(), _env| Ok(Vec::new()), + Some(finalizer), + )?; + let direct = std::sync::Arc::new(super::DirectJsCallback { + env, + callback_ref, + async_context, + lifecycle: tsfn.lifecycle(), + }); + let sink_callback: dynwinrt::com::SinkCallback = + std::sync::Arc::new(move |iid, vtable_index, values, output| { + let _keep_callback_resources_alive = &tsfn; + if std::thread::current().id() != owner_thread { + return dynwinrt::com::SinkCallbackResult::hresult(windows::core::HRESULT( + 0x8001010Eu32 as i32, + )); + } + let js_values = values + .iter() + .cloned() + .map(|value| DynWinRTValue::from_com_value(value, dynwinrt::com::PointerOutputKind::None)) + .collect::>(); + let result = super::invoke_direct_js_callback( + &direct, + |env| { + let mut slot = std::ptr::null_mut(); + super::napi_status("napi_create_uint32(COM sink slot)", unsafe { + napi::sys::napi_create_uint32(env, vtable_index as u32, &mut slot) + })?; + let mut args = Vec::with_capacity(js_values.len() + 1); + if include_iid { + args.push(unsafe { String::to_napi_value(env, format!("{iid:?}")) }?); + } + args.push(slot); + for value in js_values { + args.push(unsafe { DynWinRTValue::to_napi_value(env, value) }?); + } + Ok(args) + }, + |env, value| parse_sink_callback_result(env, value, output), + ); + result.unwrap_or_else(|error| { + eprintln!("[dynwinrt] COM sink callback failed: {error}"); + dynwinrt::com::SinkCallbackResult::hresult(windows::core::HRESULT(0x80004005u32 as i32)) + }) + }); + + let interfaces = interfaces + .into_iter() + .map(|interface| interface.0.clone()) + .collect::>(); + let result = if include_iid { + dynwinrt::com::create_object(&interfaces, sink_callback) + } else { + dynwinrt::com::create_sink(&interfaces[0], sink_callback) + }; + result + .map(|value| DynWinRTValue::new(dynwinrt::WinRTValue::Object(value))) + .map_err(|error| napi::Error::from_reason(error.message())) +} + fn bind_com_result(result: dynwinrt::Result) -> napi::Result { let mut value = result .map(DynWinRTValue::new) @@ -1077,6 +1294,13 @@ fn take_bstr(value: &mut DynWinRTValue) -> napi::Result { String::try_from(&value).map_err(|error| napi::Error::from_reason(error.to_string())) } +fn copy_callback_bstr(value: &DynWinRTValue) -> napi::Result> { + let dynwinrt::com::Value::Bstr(value) = value.to_com_value()? else { + return Err(napi::Error::from_reason("COM callback value is not a BSTR")); + }; + Ok(value.as_deref().map(str::to_owned)) +} + fn validate_pointer_owner(value: &DynWinRTValue) -> napi::Result<()> { if let Some(owner) = &value.1 { owner.validate()?; @@ -1734,6 +1958,16 @@ pub struct DynComInterface(dynwinrt::com::Interface); #[napi] impl DynComInterface { + #[napi] + pub fn add_base_interface(&self, iid: &WinGUID) -> napi::Result { + self + .0 + .clone() + .add_base_interface(iid.0) + .map(Self) + .map_err(com_error) + } + #[napi] pub fn add_method(&self, name: String, signature: &DynComMethodSig) -> Self { Self(self.0.clone().add_method(&name, signature.0.clone())) @@ -3877,6 +4111,28 @@ impl DynCom { initialize_com(apartment_type) } + #[napi(js_name = "createIUnknownSink")] + pub fn create_iunknown_sink( + interface: &DynComInterface, + #[napi( + ts_arg_type = "(vtableIndex: number, ...args: DynWinRtValue[]) => number | [number, ...DynWinRtValue[]]" + )] + callback: Function<'static, Vec, ()>, + ) -> napi::Result { + self::create_iunknown_sink(interface, callback) + } + + #[napi(js_name = "createComObject")] + pub fn create_com_object( + interfaces: Vec<&DynComInterface>, + #[napi( + ts_arg_type = "(interfaceIid: string, vtableIndex: number, ...args: DynWinRtValue[]) => number | [number, ...DynWinRtValue[]]" + )] + callback: Function<'static, Vec, ()>, + ) -> napi::Result { + create_com_object_value(interfaces, callback, true) + } + #[napi(js_name = "registerIUnknownInterface")] pub fn register_iunknown_interface(name: String, iid: &WinGUID) -> DynComInterface { DynComInterface(dynwinrt::com::register_interface( @@ -4003,6 +4259,47 @@ impl DynCom { DynComType(dynwinrt::com::Type::pointer()) } + #[napi] + pub fn copy_callback_wide_string(value: &DynWinRTValue) -> napi::Result> { + let pointer = callback_string_pointer(value)?; + if pointer.is_null() { + return Ok(None); + } + unsafe { windows::core::PCWSTR(pointer.cast()).to_string() } + .map(Some) + .map_err(|_| napi::Error::from_reason("COM callback string is not valid UTF-16")) + } + + #[napi] + pub fn copy_callback_ansi_string(value: &DynWinRTValue) -> napi::Result> { + let pointer = callback_string_pointer(value)?; + if pointer.is_null() { + return Ok(None); + } + Ok(Some( + unsafe { std::ffi::CStr::from_ptr(pointer.cast()) } + .to_string_lossy() + .into_owned(), + )) + } + + #[napi] + pub fn copy_callback_guid(value: &DynWinRTValue) -> napi::Result { + let pointer = callback_string_pointer(value)?; + if pointer.is_null() { + return Err(napi::Error::from_reason( + "COM callback GUID pointer is null", + )); + } + let value = unsafe { pointer.cast::().read_unaligned() }; + Ok(format!("{value:?}")) + } + + #[napi] + pub fn copy_callback_bstr(value: &DynWinRTValue) -> napi::Result> { + self::copy_callback_bstr(value) + } + #[napi] pub fn borrowed_handle_output_type() -> DynComType { DynComType(dynwinrt::com::Type::borrowed_handle_output()) diff --git a/bindings/js/src/lib.rs b/bindings/js/src/lib.rs index 54d9fa2c..fb259abb 100644 --- a/bindings/js/src/lib.rs +++ b/bindings/js/src/lib.rs @@ -2463,15 +2463,15 @@ impl RustStaticBench { // DynWinRtDelegate — dynamic WinRT delegate (callback) binding // ====================================================================== -struct DirectDelegateCallback { +struct DirectJsCallback { env: napi::sys::napi_env, callback_ref: napi::sys::napi_ref, async_context: napi::sys::napi_async_context, lifecycle: Arc, } -unsafe impl Send for DirectDelegateCallback {} -unsafe impl Sync for DirectDelegateCallback {} +unsafe impl Send for DirectJsCallback {} +unsafe impl Sync for DirectJsCallback {} fn napi_status(name: &str, status: napi::sys::napi_status) -> napi::Result<()> { if status == napi::sys::Status::napi_ok { @@ -2484,9 +2484,10 @@ fn napi_status(name: &str, status: napi::sys::napi_status) -> napi::Result<()> { } } -fn create_direct_delegate_resources( +fn create_direct_callback_resources( env: napi::sys::napi_env, callback: napi::sys::napi_value, + resource_name_bytes: &[u8], ) -> napi::Result<( napi::sys::napi_ref, napi::sys::napi_async_context, @@ -2499,28 +2500,27 @@ fn create_direct_delegate_resources( let result = (|| { let mut resource = std::ptr::null_mut(); - napi_status("napi_create_object(delegate resource)", unsafe { + napi_status("napi_create_object(callback resource)", unsafe { napi::sys::napi_create_object(env, &mut resource) })?; let mut resource_ref = std::ptr::null_mut(); - napi_status("napi_create_reference(delegate resource)", unsafe { + napi_status("napi_create_reference(callback resource)", unsafe { napi::sys::napi_create_reference(env, resource, 1, &mut resource_ref) })?; let async_context = (|| { let mut resource_name = std::ptr::null_mut(); - let name = b"dynwinrt.delegate"; - napi_status("napi_create_string_utf8(delegate resource)", unsafe { + napi_status("napi_create_string_utf8(callback resource)", unsafe { napi::sys::napi_create_string_utf8( env, - name.as_ptr().cast(), - name.len() as isize, + resource_name_bytes.as_ptr().cast(), + resource_name_bytes.len() as isize, &mut resource_name, ) })?; let mut async_context = std::ptr::null_mut(); - napi_status("napi_async_init(delegate)", unsafe { + napi_status("napi_async_init(callback)", unsafe { napi::sys::napi_async_init(env, resource, resource_name, &mut async_context) })?; Ok::<_, napi::Error>(async_context) @@ -2543,7 +2543,7 @@ fn create_direct_delegate_resources( let async_status = unsafe { napi::sys::napi_async_destroy(env, async_context) }; if async_status != napi::sys::Status::napi_ok { eprintln!( - "[dynwinrt] delegate async context cleanup failed: {}", + "[dynwinrt] callback async context cleanup failed: {}", napi::Status::from(async_status) ); } @@ -2551,7 +2551,7 @@ fn create_direct_delegate_resources( let status = unsafe { napi::sys::napi_delete_reference(env, reference) }; if status != napi::sys::Status::napi_ok { eprintln!( - "[dynwinrt] delegate reference cleanup failed: {}", + "[dynwinrt] callback reference cleanup failed: {}", napi::Status::from(status) ); } @@ -2568,6 +2568,79 @@ fn create_direct_delegate_resources( result } +fn invoke_direct_js_callback( + direct: &DirectJsCallback, + build_args: impl FnOnce(napi::sys::napi_env) -> napi::Result>, + parse_result: impl FnOnce(napi::sys::napi_env, napi::sys::napi_value) -> napi::Result, +) -> napi::Result { + if direct.lifecycle.is_closing() { + return Err(napi::Error::from_reason( + "Cannot invoke a callback while the Node environment is closing", + )); + } + + let env = direct.env; + unsafe { + let mut scope: napi::sys::napi_handle_scope = std::ptr::null_mut(); + napi_status( + "napi_open_handle_scope(callback)", + napi::sys::napi_open_handle_scope(env, &mut scope), + )?; + + let result = (|| { + let mut function = std::ptr::null_mut(); + napi_status( + "napi_get_reference_value(callback)", + napi::sys::napi_get_reference_value(env, direct.callback_ref, &mut function), + )?; + let args = build_args(env)?; + let mut receiver = std::ptr::null_mut(); + napi_status( + "napi_get_global(callback)", + napi::sys::napi_get_global(env, &mut receiver), + )?; + let mut result = std::ptr::null_mut(); + let status = napi::sys::napi_make_callback( + env, + direct.async_context, + receiver, + function, + args.len(), + args.as_ptr(), + &mut result, + ); + if status != napi::sys::Status::napi_ok { + let mut is_pending = false; + napi_status( + "napi_is_exception_pending(callback)", + napi::sys::napi_is_exception_pending(env, &mut is_pending), + )?; + if is_pending { + let mut error = std::ptr::null_mut(); + napi_status( + "napi_get_and_clear_last_exception(callback)", + napi::sys::napi_get_and_clear_last_exception(env, &mut error), + )?; + napi_status( + "napi_fatal_exception(callback)", + napi::sys::napi_fatal_exception(env, error), + )?; + } + return Err(napi::Error::from_reason(format!( + "napi_make_callback failed with status {status}", + ))); + } + parse_result(env, result) + })(); + + napi_status( + "napi_close_handle_scope(callback)", + napi::sys::napi_close_handle_scope(env, scope), + )?; + result + } +} + #[napi] pub struct DynWinRtDelegate(dynwinrt::WinRTValue); @@ -2599,7 +2672,7 @@ impl DynWinRtDelegate { let raw_env = callback.value().env; let raw_callback = napi::JsValue::raw(&callback); let (callback_ref, async_context, finalizer) = - create_direct_delegate_resources(raw_env, raw_callback)?; + create_direct_callback_resources(raw_env, raw_callback, b"dynwinrt.delegate")?; let tsfn = managed_tsfn::ManagedTsfn::create( raw_env, raw_callback, @@ -2614,7 +2687,7 @@ impl DynWinRtDelegate { Some(finalizer), )?; let lifecycle = tsfn.lifecycle(); - let direct = Arc::new(DirectDelegateCallback { + let direct = Arc::new(DirectJsCallback { env: raw_env, callback_ref, async_context, @@ -2636,9 +2709,6 @@ impl DynWinRtDelegate { args.iter().map(|a| DynWinRTValue::new(a.clone())).collect(); if current_tid == register_tid { - if direct.lifecycle.is_closing() { - return E_FAIL; - } // Same-thread synchronous direct invocation. Bypass the TSFN because // libuv may be blocked (e.g. DispatcherQueue.runEventLoop), so // uv_async_send would queue the callback but never fire it. @@ -2647,102 +2717,23 @@ impl DynWinRtDelegate { // ultimately called by an `extern "system"` COM stub, and letting a // Rust panic unwind through the FFI boundary is UB. On panic we // convert to E_UNEXPECTED so the WinRT caller sees a clean failure. - let raw_env = direct.env; let unwind_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe( || -> windows::core::HRESULT { - unsafe { - let mut scope: napi::sys::napi_handle_scope = std::ptr::null_mut(); - if napi::sys::napi_open_handle_scope(raw_env, &mut scope) - != napi::sys::Status::napi_ok - { - // Env is probably being torn down. Don't lie to WinRT that we - // ran successfully — surface E_FAIL so async ops etc. don't - // silently hang waiting for a completion that never happens. - eprintln!("[dynwinrt] delegate: napi_open_handle_scope failed (env teardown?)"); - return E_FAIL; - } - let call_result = (|| -> napi::Result<()> { - let mut fn_val = std::ptr::null_mut(); - napi_status("napi_get_reference_value(delegate callback)", { - napi::sys::napi_get_reference_value(raw_env, direct.callback_ref, &mut fn_val) - })?; - // Spread each DynWinRTValue as its own napi_value so the JS - // callback receives them as positional args. The blanket - // `Vec::into_vec` impl wraps the whole vec as a single JS - // Array, which is wrong here — we need one arg per element. - let mut argv: Vec = Vec::with_capacity(js_args.len()); - for v in js_args { - let raw = DynWinRTValue::to_napi_value(raw_env, v)?; - argv.push(raw); - } - let mut receiver: napi::sys::napi_value = std::ptr::null_mut(); - let status = napi::sys::napi_get_global(raw_env, &mut receiver); - if status != napi::sys::Status::napi_ok { - return Err(napi::Error::from_reason(format!( - "napi_get_global failed with status {status}", - ))); - } - let mut result: napi::sys::napi_value = std::ptr::null_mut(); - let status = napi::sys::napi_make_callback( - raw_env, - direct.async_context, - receiver, - fn_val, - argv.len(), - argv.as_ptr(), - &mut result, - ); - if status != napi::sys::Status::napi_ok { - // Surface any pending JS exception so it doesn't silently poison - // future calls. Delegates return HRESULT; there's no clean way to - // propagate a JS throw back through WinRT, so we route it through - // napi_fatal_exception (same policy tsfn uses). - let mut is_pending: bool = false; - let pending_status = - napi::sys::napi_is_exception_pending(raw_env, &mut is_pending); - if pending_status != napi::sys::Status::napi_ok { - return Err(napi::Error::from_reason(format!( - "napi_is_exception_pending failed with status {pending_status}", - ))); - } - if is_pending { - let mut err: napi::sys::napi_value = std::ptr::null_mut(); - let clear_status = - napi::sys::napi_get_and_clear_last_exception(raw_env, &mut err); - if clear_status != napi::sys::Status::napi_ok { - return Err(napi::Error::from_reason(format!( - "napi_get_and_clear_last_exception failed with status {clear_status}", - ))); - } - let fatal_status = napi::sys::napi_fatal_exception(raw_env, err); - if fatal_status != napi::sys::Status::napi_ok { - return Err(napi::Error::from_reason(format!( - "napi_fatal_exception failed with status {fatal_status}", - ))); - } - } - return Err(napi::Error::from_reason(format!( - "napi_make_callback failed with status {status}", - ))); - } - Ok(()) - })(); - let close_status = napi::sys::napi_close_handle_scope(raw_env, scope); - if close_status != napi::sys::Status::napi_ok { - eprintln!( - "[dynwinrt] delegate: napi_close_handle_scope failed with status {close_status}" - ); - return E_FAIL; - } - // Log and report any non-exception error to WinRT. Without this, - // failures like invalid handles or marshaler errors would be - // silently dropped and the delegate would appear to have run. - if let Err(e) = call_result { - eprintln!("[dynwinrt] delegate dispatch error: {e}"); - return E_FAIL; - } - windows::core::HRESULT(0) + let call_result = invoke_direct_js_callback( + &direct, + |env| { + js_args + .into_iter() + .map(|value| unsafe { DynWinRTValue::to_napi_value(env, value) }) + .collect() + }, + |_env, _result| Ok(()), + ); + if let Err(error) = call_result { + eprintln!("[dynwinrt] delegate dispatch error: {error}"); + return E_FAIL; } + windows::core::HRESULT(0) }, )); return match unwind_result { diff --git a/bindings/js/src/tsfn_test_hooks.rs b/bindings/js/src/tsfn_test_hooks.rs index 68caf9f3..a3371f04 100644 --- a/bindings/js/src/tsfn_test_hooks.rs +++ b/bindings/js/src/tsfn_test_hooks.rs @@ -20,7 +20,7 @@ use windows::core::{IUnknown, Interface}; use crate::{ managed_tsfn::{self, ManagedTsfn}, - DynWinRtDelegate, + DynWinRTValue, DynWinRtDelegate, }; static PRODUCED: AtomicUsize = AtomicUsize::new(0); @@ -33,6 +33,7 @@ static OTHER_FAILURE: AtomicUsize = AtomicUsize::new(0); static HELD_STRONG: Mutex>> = Mutex::new(None); static HELD_WEAK: Mutex>> = Mutex::new(None); static RETAINED_DELEGATE: AtomicUsize = AtomicUsize::new(0); +static RETAINED_COM_SINK: AtomicUsize = AtomicUsize::new(0); static DELEGATE_STRESS_DONE: AtomicUsize = AtomicUsize::new(0); static DELEGATE_STRESS_SUCCEEDED: AtomicUsize = AtomicUsize::new(0); static DELEGATE_STRESS_FAILED: AtomicUsize = AtomicUsize::new(0); @@ -63,6 +64,19 @@ pub struct TsfnDelegateInvokeStats { pub failed: u32, } +#[napi(object)] +pub struct TsfnComSinkInvokeResult { + pub hresult: i32, + pub output: i32, +} + +#[napi(object)] +pub struct TsfnComAllocatedBufferResult { + pub hresult: i32, + pub count: u32, + pub byte_sum: u32, +} + fn count(value: &AtomicUsize) -> u32 { value.load(Ordering::SeqCst).min(u32::MAX as usize) as u32 } @@ -325,6 +339,341 @@ pub fn tsfn_test_release_retained_delegate() { } } +#[napi] +pub fn tsfn_test_retain_com_sink(value: &DynWinRTValue) -> napi::Result<()> { + let object = value + .0 + .as_object() + .ok_or_else(|| napi::Error::from_reason("COM sink test value is not a COM object"))? + .clone(); + let raw = object.into_raw() as usize; + let previous = RETAINED_COM_SINK.swap(raw, Ordering::SeqCst); + if previous != 0 { + unsafe { drop(IUnknown::from_raw(previous as *mut c_void)) }; + } + Ok(()) +} + +unsafe fn invoke_com_sink(raw: *mut c_void) -> i32 { + let vtable = unsafe { *(raw as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *mut c_void) -> windows::core::HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + unsafe { invoke(raw, std::ptr::null_mut()) }.0 +} + +unsafe fn invoke_com_sink_out(raw: *mut c_void) -> TsfnComSinkInvokeResult { + let vtable = unsafe { *(raw as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn( + *mut c_void, + *mut c_void, + *mut c_void, + *mut i32, + ) -> windows::core::HRESULT = unsafe { std::mem::transmute(*vtable.add(3)) }; + let mut output = -1; + let hresult = unsafe { invoke(raw, std::ptr::null_mut(), std::ptr::null_mut(), &mut output) }; + TsfnComSinkInvokeResult { + hresult: hresult.0, + output, + } +} + +unsafe fn invoke_com_sink_i32(raw: *mut c_void, value: i32) -> i32 { + let vtable = unsafe { *(raw as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, i32) -> windows::core::HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + unsafe { invoke(raw, value) }.0 +} + +unsafe fn invoke_com_sink_direct_i32(raw: *mut c_void, value: i32) -> i32 { + let vtable = unsafe { *(raw as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, i32) -> i32 = + unsafe { std::mem::transmute(*vtable.add(3)) }; + unsafe { invoke(raw, value) } +} + +unsafe fn invoke_com_sink_void_i32(raw: *mut c_void, value: i32) { + let vtable = unsafe { *(raw as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, i32) = + unsafe { std::mem::transmute(*vtable.add(3)) }; + unsafe { invoke(raw, value) }; +} + +unsafe fn invoke_com_sink_guid(raw: *mut c_void, value: &windows::core::GUID) -> i32 { + let vtable = unsafe { *(raw as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn( + *mut c_void, + *const windows::core::GUID, + ) -> windows::core::HRESULT = unsafe { std::mem::transmute(*vtable.add(3)) }; + unsafe { invoke(raw, value) }.0 +} + +unsafe fn invoke_com_sink_bstr(raw: *mut c_void) -> i32 { + let vtable = unsafe { *(raw as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *const u16) -> windows::core::HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + let value = windows::core::BSTR::from("embedded\0callback"); + unsafe { invoke(raw, value.as_ptr()) }.0 +} + +unsafe fn invoke_com_sink_wide_string(raw: *mut c_void) -> i32 { + let vtable = unsafe { *(raw as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *const u16) -> windows::core::HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + let value = "wide callback\0".encode_utf16().collect::>(); + unsafe { invoke(raw, value.as_ptr()) }.0 +} + +unsafe fn invoke_com_sink_ansi_string(raw: *mut c_void) -> i32 { + let vtable = unsafe { *(raw as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *const i8) -> windows::core::HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + unsafe { invoke(raw, c"ansi callback".as_ptr()) }.0 +} + +unsafe fn invoke_com_sink_allocated_buffer(raw: *mut c_void) -> TsfnComAllocatedBufferResult { + let vtable = unsafe { *(raw as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn( + *mut c_void, + *mut *mut u8, + *mut u32, + ) -> windows::core::HRESULT = unsafe { std::mem::transmute(*vtable.add(3)) }; + let mut bytes = std::ptr::null_mut(); + let mut count = u32::MAX; + let hresult = unsafe { invoke(raw, &mut bytes, &mut count) }; + let byte_sum = if bytes.is_null() || count == 0 { + 0 + } else { + unsafe { std::slice::from_raw_parts(bytes, count as usize) } + .iter() + .map(|value| u32::from(*value)) + .sum() + }; + if !bytes.is_null() { + unsafe { windows::Win32::System::Com::CoTaskMemFree(Some(bytes.cast())) }; + } + TsfnComAllocatedBufferResult { + hresult: hresult.0, + count, + byte_sum, + } +} + +unsafe fn invoke_com_sink_caller_buffer(raw: *mut c_void) -> TsfnComAllocatedBufferResult { + let vtable = unsafe { *(raw as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn( + *mut c_void, + *mut u8, + u32, + *mut u32, + ) -> windows::core::HRESULT = unsafe { std::mem::transmute(*vtable.add(3)) }; + let mut bytes = [0xff; 5]; + let mut count = u32::MAX; + let hresult = unsafe { invoke(raw, bytes.as_mut_ptr(), bytes.len() as u32, &mut count) }; + TsfnComAllocatedBufferResult { + hresult: hresult.0, + count, + byte_sum: bytes.iter().map(|value| u32::from(*value)).sum(), + } +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_sink() -> napi::Result { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst) as *mut c_void; + if raw.is_null() { + return Err(napi::Error::from_reason( + "No COM sink is retained by the TSFN test harness", + )); + } + Ok(unsafe { invoke_com_sink(raw) }) +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_sink_on_thread() -> napi::Result { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst); + if raw == 0 { + return Err(napi::Error::from_reason( + "No COM sink is retained by the TSFN test harness", + )); + } + thread::spawn(move || unsafe { invoke_com_sink(raw as *mut c_void) }) + .join() + .map_err(|_| napi::Error::from_reason("COM sink test thread panicked")) +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_sink_out() -> napi::Result { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst) as *mut c_void; + if raw.is_null() { + return Err(napi::Error::from_reason( + "No COM sink is retained by the TSFN test harness", + )); + } + Ok(unsafe { invoke_com_sink_out(raw) }) +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_sink_i32(value: i32) -> napi::Result { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst) as *mut c_void; + if raw.is_null() { + return Err(napi::Error::from_reason( + "No COM sink is retained by the TSFN test harness", + )); + } + Ok(unsafe { invoke_com_sink_i32(raw, value) }) +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_sink_i32_on_thread(value: i32) -> napi::Result { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst); + if raw == 0 { + return Err(napi::Error::from_reason( + "No COM sink is retained by the TSFN test harness", + )); + } + thread::spawn(move || unsafe { invoke_com_sink_i32(raw as *mut c_void, value) }) + .join() + .map_err(|_| napi::Error::from_reason("COM sink test thread panicked")) +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_sink_direct_i32(value: i32) -> napi::Result { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst) as *mut c_void; + if raw.is_null() { + return Err(napi::Error::from_reason( + "No COM sink is retained by the TSFN test harness", + )); + } + Ok(unsafe { invoke_com_sink_direct_i32(raw, value) }) +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_sink_direct_i32_on_thread(value: i32) -> napi::Result { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst); + if raw == 0 { + return Err(napi::Error::from_reason( + "No COM sink is retained by the TSFN test harness", + )); + } + thread::spawn(move || unsafe { invoke_com_sink_direct_i32(raw as *mut c_void, value) }) + .join() + .map_err(|_| napi::Error::from_reason("COM sink test thread panicked")) +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_sink_void_i32(value: i32) -> napi::Result<()> { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst) as *mut c_void; + if raw.is_null() { + return Err(napi::Error::from_reason( + "No COM sink is retained by the TSFN test harness", + )); + } + unsafe { invoke_com_sink_void_i32(raw, value) }; + Ok(()) +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_sink_guid() -> napi::Result { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst) as *mut c_void; + if raw.is_null() { + return Err(napi::Error::from_reason( + "No COM sink is retained by the TSFN test harness", + )); + } + let value = windows::core::GUID::from_u128(0x990c600e_60c7_4d28_af4c_bf148a92b11a); + Ok(unsafe { invoke_com_sink_guid(raw, &value) }) +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_sink_bstr() -> napi::Result { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst) as *mut c_void; + if raw.is_null() { + return Err(napi::Error::from_reason( + "No COM sink is retained by the TSFN test harness", + )); + } + Ok(unsafe { invoke_com_sink_bstr(raw) }) +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_sink_wide_string() -> napi::Result { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst) as *mut c_void; + if raw.is_null() { + return Err(napi::Error::from_reason( + "No COM sink is retained by the TSFN test harness", + )); + } + Ok(unsafe { invoke_com_sink_wide_string(raw) }) +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_sink_ansi_string() -> napi::Result { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst) as *mut c_void; + if raw.is_null() { + return Err(napi::Error::from_reason( + "No COM sink is retained by the TSFN test harness", + )); + } + Ok(unsafe { invoke_com_sink_ansi_string(raw) }) +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_sink_allocated_buffer( +) -> napi::Result { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst) as *mut c_void; + if raw.is_null() { + return Err(napi::Error::from_reason( + "No COM sink is retained by the TSFN test harness", + )); + } + Ok(unsafe { invoke_com_sink_allocated_buffer(raw) }) +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_sink_caller_buffer( +) -> napi::Result { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst) as *mut c_void; + if raw.is_null() { + return Err(napi::Error::from_reason( + "No COM sink is retained by the TSFN test harness", + )); + } + Ok(unsafe { invoke_com_sink_caller_buffer(raw) }) +} + +#[napi] +pub fn tsfn_test_invoke_retained_com_object_i32( + iid: &crate::WinGUID, + value: i32, +) -> napi::Result { + let raw = RETAINED_COM_SINK.load(Ordering::SeqCst) as *mut c_void; + if raw.is_null() { + return Err(napi::Error::from_reason( + "No COM object is retained by the TSFN test harness", + )); + } + let identity = unsafe { IUnknown::from_raw_borrowed(&raw) } + .ok_or_else(|| napi::Error::from_reason("Retained COM identity is null"))?; + let mut view = std::ptr::null_mut(); + unsafe { identity.query(&iid.0, &mut view) } + .ok() + .map_err(|error| napi::Error::from_reason(error.message()))?; + if view.is_null() { + return Err(napi::Error::from_reason( + "Retained COM object did not expose the requested interface", + )); + } + let view = unsafe { IUnknown::from_raw(view) }; + Ok(unsafe { invoke_com_sink_i32(view.as_raw(), value) }) +} + +#[napi] +pub fn tsfn_test_release_retained_com_sink() { + let raw = RETAINED_COM_SINK.swap(0, Ordering::SeqCst); + if raw != 0 { + unsafe { drop(IUnknown::from_raw(raw as *mut c_void)) }; + } +} + #[napi] pub fn tsfn_test_arm_call_pause() { managed_tsfn::test_arm_call_pause(); diff --git a/crates/dynwinrt/src/com.rs b/crates/dynwinrt/src/com.rs index 2a2fdaf2..d52d9875 100644 --- a/crates/dynwinrt/src/com.rs +++ b/crates/dynwinrt/src/com.rs @@ -2,10 +2,13 @@ // Licensed under the MIT License. use core::ffi::c_void; +#[cfg(test)] +use std::cell::Cell; use std::{ cell::{RefCell, UnsafeCell}, - collections::{BTreeMap, BTreeSet}, + collections::{BTreeMap, BTreeSet, HashSet}, mem::{align_of, size_of}, + panic::{AssertUnwindSafe, catch_unwind}, sync::{Arc, Mutex, MutexGuard, RwLock, TryLockError}, }; @@ -13,7 +16,7 @@ use windows::Win32::System::Com::{ CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, COINIT_MULTITHREADED, CoCreateInstance, CoGetClassObject, CoGetMalloc, CoInitializeEx, CoUninitialize, }; -use windows_core::{GUID, IUnknown, Interface as WindowsInterface, PCWSTR}; +use windows_core::{GUID, HRESULT, IInspectable, IUnknown, Interface as WindowsInterface, PCWSTR}; use crate::{ MetadataTable, TypeHandle, TypeKind, WinRTValue, @@ -1123,6 +1126,10 @@ impl ComBufferValue { } } + pub fn from_owned_bytes(bytes: Vec, count: usize) -> Self { + Self::owned(bytes, count) + } + fn owned_com(values: Vec) -> Self { Self { storage: ComBufferStorage::OwnedCom { values }, @@ -1189,6 +1196,7 @@ impl ComBufferValue { ComBufferStorage::Owned { bytes, .. } | ComBufferStorage::OwnedInput { bytes, .. } => { Ok(Some(bytes.clone())) } + ComBufferStorage::CallerOutput { blocks, byte_len, .. } => { @@ -1211,6 +1219,28 @@ impl ComBufferValue { } } + pub fn copy_bytes(&self) -> result::Result> { + if let Some(bytes) = self.snapshot_bytes()? { + return Ok(bytes); + } + match &self.storage { + ComBufferStorage::Borrowed { ptr, byte_len, .. } => { + if *byte_len == 0 { + Ok(Vec::new()) + } else if ptr.is_null() { + Err(invalid_argument( + "non-empty borrowed COM buffer has a null pointer", + )) + } else { + Ok(unsafe { std::slice::from_raw_parts(*ptr, *byte_len) }.to_vec()) + } + } + _ => Err(invalid_argument( + "COM buffer representation cannot be copied as contiguous bytes", + )), + } + } + pub fn count(&self) -> usize { match &self.storage { ComBufferStorage::Owned { count, .. } => *count, @@ -1385,6 +1415,7 @@ impl BufferElementPlan { | TypeKind::F32 | TypeKind::F64 | TypeKind::Guid + | TypeKind::HString | TypeKind::HResult | TypeKind::Enum(_) ) => @@ -1671,25 +1702,25 @@ impl Type { fn supports_direct_return(&self) -> bool { matches!(self.abi, ParameterType::Pointer) || matches!( - &self.abi, - ParameterType::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(_) - ) + &self.abi, + ParameterType::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(_) + ) ) } } @@ -1970,2826 +2001,6140 @@ enum ComReturnPlan { Direct(Type), } -#[derive(Debug)] -struct ComCallPlan { - native: NativeMethod, - arguments: Vec, - results: Vec, +#[derive(Debug, Clone)] +struct CallbackMethodPlan { + // Preserve the complete validated COM contract. Static thunks and dynamic + // libffi closures are lowering choices over the same plan. + parameters: Vec, return_plan: ComReturnPlan, } -impl ComCallPlan { - fn new( - native: NativeMethod, - parameters: Vec, - return_plan: ComReturnPlan, - ) -> Self { - let mut buffer_roles = vec![Vec::new(); parameters.len()]; - for (buffer_param, parameter) in parameters.iter().enumerate() { - let Some(buffer) = ¶meter.buffer else { - continue; - }; - for related in buffer.relation.related_params() { - assert!( - related < parameters.len() && related != buffer_param, - "counted COM buffer relationships must reference another parameter" - ); - } - match buffer.relation { - ComBufferRelation::Input { - count_param, - actual_length_param, - .. - } => { - set_buffer_role( - &mut buffer_roles, - count_param, - ComBufferParamRole::InputCount { buffer_param }, - ); - if let Some(actual) = actual_length_param { - set_buffer_role( - &mut buffer_roles, - actual, - ComBufferParamRole::InputActual { buffer_param }, - ); +struct PreparedCallback { + output_count: usize, + caller_capacities: Vec>, +} + +impl PreparedCallback { + fn caller_capacity(&self, buffer_param: usize) -> Result { + self.caller_capacities + .get(buffer_param) + .copied() + .flatten() + .ok_or(SINK_E_FAIL) + } +} + +enum PreparedNativeCallbackOutput { + Bool(u8), + I8(i8), + U8(u8), + I16(i16), + U16(u16), + I32(i32), + U32(u32), + I64(i64), + U64(u64), + F32(f32), + F64(f64), + Guid(GUID), + Pointer(*mut c_void), + Bstr(Option), + HString(windows_core::HSTRING), + Interface(Option), + NativeStruct(Vec), +} + +impl PreparedNativeCallbackOutput { + unsafe fn commit(self, target: *mut c_void) { + match self { + Self::Bool(value) | Self::U8(value) => unsafe { target.cast::().write(value) }, + Self::I8(value) => unsafe { target.cast::().write(value) }, + Self::I16(value) => unsafe { target.cast::().write(value) }, + Self::U16(value) => unsafe { target.cast::().write(value) }, + Self::I32(value) => unsafe { target.cast::().write(value) }, + Self::U32(value) => unsafe { target.cast::().write(value) }, + Self::I64(value) => unsafe { target.cast::().write(value) }, + Self::U64(value) => unsafe { target.cast::().write(value) }, + Self::F32(value) => unsafe { target.cast::().write(value) }, + Self::F64(value) => unsafe { target.cast::().write(value) }, + Self::Guid(value) => unsafe { target.cast::().write(value) }, + Self::Pointer(value) => unsafe { target.cast::<*mut c_void>().write(value) }, + Self::Bstr(value) => unsafe { + target.cast::<*mut u16>().write( + value + .map(|value| value.into_raw().cast_mut()) + .unwrap_or(std::ptr::null_mut()), + ) + }, + Self::HString(value) => unsafe { target.cast::().write(value) }, + Self::Interface(value) => unsafe { + target.cast::<*mut c_void>().write( + value + .map(IUnknown::into_raw) + .unwrap_or(std::ptr::null_mut()), + ) + }, + Self::NativeStruct(bytes) => unsafe { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), target.cast::(), bytes.len()) + }, + } + } +} + +enum PreparedCallbackWrite { + Native { + target: *mut c_void, + value: PreparedNativeCallbackOutput, + replace_bstr: bool, + }, + CallerBuffer { + target: *mut u8, + bytes: Vec, + actual: Option<(*mut c_void, PreparedNativeCallbackOutput)>, + }, + CalleeBuffer { + target: *mut *mut c_void, + allocation: BufferAllocationGuard, + count_target: *mut c_void, + count: PreparedNativeCallbackOutput, + }, +} + +impl PreparedCallbackWrite { + unsafe fn commit(self) { + match self { + Self::Native { + target, + value, + replace_bstr, + } => { + if replace_bstr { + let current = unsafe { target.cast::<*mut u16>().read() }; + if !current.is_null() { + drop(unsafe { windows_core::BSTR::from_raw(current.cast_const()) }); } } - ComBufferRelation::CallerCapacity { - capacity_param, - actual_length_param, - .. - } => { - if actual_length_param == Some(capacity_param) { - set_buffer_role( - &mut buffer_roles, - capacity_param, - ComBufferParamRole::CallerCapacityActual { buffer_param }, - ); - } else { - set_buffer_role( - &mut buffer_roles, - capacity_param, - ComBufferParamRole::CallerCapacity { buffer_param }, - ); - if let Some(actual) = actual_length_param { - set_buffer_role( - &mut buffer_roles, - actual, - ComBufferParamRole::CallerActual { buffer_param }, - ); - } + unsafe { value.commit(target) }; + } + Self::CallerBuffer { + target, + bytes, + actual, + } => { + if !bytes.is_empty() { + unsafe { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), target, bytes.len()); } } - ComBufferRelation::EnumeratorNext { - capacity_param, - fetched_param, - } => { - set_buffer_role( - &mut buffer_roles, - capacity_param, - ComBufferParamRole::CallerCapacity { buffer_param }, - ); - set_buffer_role( - &mut buffer_roles, - fetched_param, - ComBufferParamRole::CallerActual { buffer_param }, - ); - } - ComBufferRelation::CalleeAllocated { count_param, .. } => { - set_buffer_role( - &mut buffer_roles, - count_param, - ComBufferParamRole::CalleeCount { buffer_param }, - ); + if let Some((target, actual)) = actual { + unsafe { actual.commit(target) }; } } + Self::CalleeBuffer { + target, + allocation, + count_target, + count, + } => { + unsafe { target.write(allocation.into_raw()) }; + unsafe { count.commit(count_target) }; + } } + } +} - let mut input_index = 0; - let mut output_index = 0; - let mut arguments = Vec::with_capacity(parameters.len()); - let mut results = Vec::new(); +struct PreparedCallbackWrites(Vec); - match &return_plan { - ComReturnPlan::SemanticHResult | ComReturnPlan::EnumeratorNextHResult => { - results.push(ComResultPlan { - source: ComResultSource::DirectReturn, - typ: None, - success: ComSuccessDisposition::Value, - failure_cleanup: OutputCleanup::None, - }) - } - ComReturnPlan::Direct(typ) => results.push(ComResultPlan { - source: ComResultSource::DirectReturn, - typ: Some(typ.abi.clone()), - success: typ.pointer_output.into(), - failure_cleanup: typ.output_cleanup(), - }), - ComReturnPlan::HResult - | ComReturnPlan::DispatchInvokeHResult(_) - | ComReturnPlan::Void => {} +impl PreparedCallbackWrites { + unsafe fn commit(self) { + for write in self.0 { + unsafe { write.commit() }; } + } +} - for (parameter_index, parameter) in parameters.into_iter().enumerate() { - let direction = parameter.direction; - let typ = parameter.typ; - let roles = &buffer_roles[parameter_index]; - let has_logical_input = direction.is_input() && !buffer_roles_hide_input(roles); - let parameter_input_index = has_logical_input.then_some(input_index); - if has_logical_input { - input_index += 1; - } - let native_kind = direction.native_kind(); - let has_native_output = matches!( - native_kind, - ParamKind::Out - | ParamKind::OptionalOut - | ParamKind::InOut - | ParamKind::OutFillArray - ); - let parameter_output_index = has_native_output.then_some(output_index); - if has_native_output { - output_index += 1; - } - let (pointer_output, failure_cleanup) = match direction { - ComParameterDirection::Out | ComParameterDirection::OptionalOut => { - (typ.pointer_output, typ.output_cleanup()) +impl CallbackMethodPlan { + fn new(parameters: Vec, return_plan: ComReturnPlan) -> Self { + Self { + parameters, + return_plan, + } + } + + fn static_shape(&self) -> Option { + let interface_input = |parameter: &ComParameterSpec| { + parameter.direction == ComParameterDirection::In + && parameter.buffer.is_none() + && matches!( + ¶meter.typ.abi, + ParameterType::WinRT(typ) + if matches!( + typ.kind(), + TypeKind::Object + | TypeKind::Interface(_) + | TypeKind::RuntimeClass(_) + ) + ) + }; + let i32_output = |parameter: &ComParameterSpec| { + parameter.direction == ComParameterDirection::Out + && parameter.buffer.is_none() + && matches!( + ¶meter.typ.abi, + ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::I32) + ) + }; + let hresult = matches!( + self.return_plan, + ComReturnPlan::HResult | ComReturnPlan::SemanticHResult + ); + if !hresult { + None + } else { + match self.parameters.as_slice() { + [first] if interface_input(first) => Some(StaticCallbackShape::InterfaceIn1), + [first, second] if interface_input(first) && interface_input(second) => { + Some(StaticCallbackShape::InterfaceIn2) } - ComParameterDirection::InOut => { - if typ.abi.is_bstr() { - (PointerOutputKind::Bstr, OutputCleanup::BstrFree) - } else { - (PointerOutputKind::Unclassified, OutputCleanup::None) - } + [first, second, output] + if interface_input(first) && interface_input(second) && i32_output(output) => + { + Some(StaticCallbackShape::InterfaceIn2OutI32) } - ComParameterDirection::CalleeAllocatedBuffer => { - (PointerOutputKind::None, typ.output_cleanup()) + _ => None, + } + } + } + + fn libffi_signature(&self) -> Option { + let parameters = self + .parameters + .iter() + .map(|parameter| { + if let Some(buffer) = ¶meter.buffer { + return matches!( + (¶meter.direction, &buffer.relation, &buffer.element.kind), + ( + ComParameterDirection::InputBuffer, + ComBufferRelation::Input { .. }, + BufferElementKind::Plain + ) | ( + ComParameterDirection::CallerOutputBuffer, + ComBufferRelation::CallerCapacity { + two_call: false, + .. + }, + BufferElementKind::Plain + ) | ( + ComParameterDirection::CalleeAllocatedBuffer, + ComBufferRelation::CalleeAllocated { + allocator: BufferAllocator::CoTaskMem, + .. + }, + BufferElementKind::Plain + ) + ) + .then_some(( + crate::native_callback::CallbackAbiType::Pointer, + libffi::middle::Type::pointer(), + )); } - ComParameterDirection::OutFill - | ComParameterDirection::In - | ComParameterDirection::InputBuffer - | ComParameterDirection::CallerOutputBuffer => { - (PointerOutputKind::None, OutputCleanup::None) + match parameter.direction { + ComParameterDirection::In => { + let abi = Self::callback_abi_type(¶meter.typ.abi)?; + Some((abi, parameter.typ.abi.libffi_type())) + } + ComParameterDirection::Out + if Self::supports_callback_output(¶meter.typ) => + { + Some(( + crate::native_callback::CallbackAbiType::Pointer, + libffi::middle::Type::pointer(), + )) + } + ComParameterDirection::InOut + if Self::supports_callback_inout(¶meter.typ) => + { + Some(( + crate::native_callback::CallbackAbiType::Pointer, + libffi::middle::Type::pointer(), + )) + } + _ => None, } - }; - let storage = match direction { - ComParameterDirection::In => ComArgumentStorage::Value, - ComParameterDirection::Out => ComArgumentStorage::OutputPointer, - ComParameterDirection::OptionalOut => ComArgumentStorage::OutputPointer, - ComParameterDirection::InOut => ComArgumentStorage::InOutPointer, - ComParameterDirection::OutFill => ComArgumentStorage::FillBuffer, - ComParameterDirection::InputBuffer => ComArgumentStorage::InputBuffer, - ComParameterDirection::CallerOutputBuffer => ComArgumentStorage::CallerOutputBuffer, - ComParameterDirection::CalleeAllocatedBuffer => { - ComArgumentStorage::CalleeAllocatedBuffer - } - }; - if matches!( - direction, - ComParameterDirection::CallerOutputBuffer - | ComParameterDirection::CalleeAllocatedBuffer - ) { - results.push(ComResultPlan { - source: ComResultSource::Buffer(parameter_index), - typ: None, - success: ComSuccessDisposition::Value, - failure_cleanup, - }); - } else if direction.is_output() && !buffer_roles_hide_output(roles) { - results.push(ComResultPlan { - source: ComResultSource::Parameter(parameter_index), - typ: Some(typ.abi.clone()), - success: pointer_output.into(), - failure_cleanup, - }); + }) + .collect::>>()?; + match &self.return_plan { + ComReturnPlan::HResult | ComReturnPlan::SemanticHResult => Some( + crate::native_callback::CallbackSignature::hresult(parameters), + ), + ComReturnPlan::Void => { + Some(crate::native_callback::CallbackSignature::void(parameters)) } - arguments.push(ComArgumentPlan { - typ: typ.abi, - direction, - nullable: parameter.nullable, - storage, - input_index: parameter_input_index, - output_index: parameter_output_index, - failure_cleanup, - buffer: parameter.buffer, - buffer_roles: buffer_roles[parameter_index].clone(), - }); + ComReturnPlan::Direct(typ) => { + let abi = Self::callback_abi_type(&typ.abi)?; + Some(crate::native_callback::CallbackSignature::direct( + parameters, + (abi, typ.abi.libffi_type()), + )) + } + ComReturnPlan::EnumeratorNextHResult | ComReturnPlan::DispatchInvokeHResult(_) => None, } - - let plan = Self { - native, - arguments, - results, - return_plan, - }; - plan.assert_invariants(); - plan } - fn assert_invariants(&self) { - let mut expected_input = 0; - let mut expected_output = 0; - for (parameter_index, argument) in self.arguments.iter().enumerate() { - let has_logical_input = - argument.direction.is_input() && !buffer_roles_hide_input(&argument.buffer_roles); - assert_eq!( - argument.input_index, - has_logical_input.then_some(expected_input) - ); - if has_logical_input { - expected_input += 1; + fn callback_contract(&self, output_count: usize) -> CallbackContract { + match self.return_plan { + ComReturnPlan::HResult | ComReturnPlan::SemanticHResult => { + CallbackContract::hresult(output_count) } - let has_native_output = matches!( - argument.direction.native_kind(), - ParamKind::Out - | ParamKind::OptionalOut - | ParamKind::InOut - | ParamKind::OutFillArray - ); - assert_eq!( - argument.output_index, - has_native_output.then_some(expected_output) - ); - if has_native_output { - expected_output += 1; - assert_eq!( - self.native.output_cleanup(parameter_index), - argument.failure_cleanup - ); + ComReturnPlan::Void => CallbackContract::void(output_count), + ComReturnPlan::Direct(_) => CallbackContract::direct(output_count), + ComReturnPlan::EnumeratorNextHResult | ComReturnPlan::DispatchInvokeHResult(_) => { + unreachable!("unsupported callback return plan was not lowered") } - assert_eq!( - argument.storage, - match argument.direction { - ComParameterDirection::In => ComArgumentStorage::Value, - ComParameterDirection::Out | ComParameterDirection::OptionalOut => { - ComArgumentStorage::OutputPointer - } - ComParameterDirection::InOut => ComArgumentStorage::InOutPointer, - ComParameterDirection::OutFill => ComArgumentStorage::FillBuffer, - ComParameterDirection::InputBuffer => ComArgumentStorage::InputBuffer, - ComParameterDirection::CallerOutputBuffer => { - ComArgumentStorage::CallerOutputBuffer - } - ComParameterDirection::CalleeAllocatedBuffer => { - ComArgumentStorage::CalleeAllocatedBuffer - } - } - ); - assert_eq!(&argument.typ, self.native.parameter_type(parameter_index)); } - for result in &self.results { - if result.typ.is_none() { - assert!(matches!( - result.source, - ComResultSource::DirectReturn | ComResultSource::Buffer(_) - )); - } - if let ComResultSource::Parameter(index) | ComResultSource::Buffer(index) = - result.source - { - assert_eq!( - result.failure_cleanup, - self.arguments[index].failure_cleanup - ); + } + + fn callback_abi_type(typ: &ParameterType) -> Option { + match typ { + ParameterType::WinRT(typ) => match typ.kind() { + TypeKind::I8 => Some(crate::native_callback::CallbackAbiType::I8), + TypeKind::Bool | TypeKind::U8 => Some(crate::native_callback::CallbackAbiType::U8), + TypeKind::I16 => Some(crate::native_callback::CallbackAbiType::I16), + TypeKind::U16 | TypeKind::Char16 => { + Some(crate::native_callback::CallbackAbiType::U16) + } + TypeKind::I32 | TypeKind::HResult | TypeKind::Enum(_) => { + Some(crate::native_callback::CallbackAbiType::I32) + } + TypeKind::U32 => Some(crate::native_callback::CallbackAbiType::U32), + TypeKind::I64 => Some(crate::native_callback::CallbackAbiType::I64), + TypeKind::U64 => Some(crate::native_callback::CallbackAbiType::U64), + TypeKind::F32 => Some(crate::native_callback::CallbackAbiType::F32), + TypeKind::F64 => Some(crate::native_callback::CallbackAbiType::F64), + TypeKind::Guid => Some(crate::native_callback::CallbackAbiType::Guid), + TypeKind::HString + | TypeKind::Object + | TypeKind::Interface(_) + | TypeKind::Delegate(_) + | TypeKind::RuntimeClass(_) + | TypeKind::Parameterized(_) => { + Some(crate::native_callback::CallbackAbiType::Pointer) + } + _ => None, + }, + ParameterType::Pointer + | ParameterType::CoTaskMemWideString + | ParameterType::Bstr { .. } + | ParameterType::NativeStructPointer { .. } + | ParameterType::NativeUnionPointer(_) + | ParameterType::Variant + | ParameterType::SafeArray { .. } + | ParameterType::PropVariant + | ParameterType::DispatchParams + | ParameterType::ExcepInfo + | ParameterType::StatStg => Some(crate::native_callback::CallbackAbiType::Pointer), + ParameterType::NativeStruct(layout) => { + Some(crate::native_callback::CallbackAbiType::NativeStruct( + format!("{layout:?}"), + layout.size(), + )) } + ParameterType::VariantByValue => None, } - let planned_direct_type = self - .results - .first() - .filter(|result| result.source == ComResultSource::DirectReturn) - .and_then(|result| result.typ.as_ref()); - assert_eq!(planned_direct_type, self.native.direct_return_type()); - let has_direct_result = self - .results - .first() - .is_some_and(|result| result.source == ComResultSource::DirectReturn); - assert_eq!( - has_direct_result, - matches!( - self.return_plan, - ComReturnPlan::SemanticHResult - | ComReturnPlan::EnumeratorNextHResult - | ComReturnPlan::Direct(_) - ) - ); } - fn invoke(&self, obj: *mut c_void, args: &[WinRTValue]) -> result::Result> { - if self.native.uses_com_value_path() - || self - .arguments - .iter() - .any(|argument| argument.buffer.is_some()) - || self - .results - .iter() - .any(|result| matches!(result.source, ComResultSource::Buffer(_))) - { - return Err(invalid_argument( - "COM-local struct, union, Automation, or buffer signatures require the COM value invocation path", - )); - } - let values = args.iter().cloned().map(Value::WinRt).collect::>(); - self.invoke_values(obj, &values)? - .into_iter() - .map(|value| match value { - Value::WinRt(value) => Ok(value), - Value::NativeStruct(_) => Err(invalid_argument( - "native POD result requires the COM value invocation path", - )), - Value::NativeUnion(_) - | Value::Bstr(_) - | Value::Variant(_) - | Value::SafeArray(_) - | Value::PropVariant(_) - | Value::DispatchParams(_) - | Value::ExcepInfo(_) - | Value::StatStg(_) => Err(invalid_argument( - "COM-local result requires the COM value invocation path", - )), - Value::Buffer(_) => Err(invalid_argument( - "counted COM buffer result requires the COM value invocation path", - )), + fn supports_callback_output(typ: &Type) -> bool { + matches!( + &typ.abi, + ParameterType::Pointer | ParameterType::Bstr { .. } | ParameterType::NativeStruct(_) + ) || matches!( + &typ.abi, + ParameterType::WinRT(value) + if matches!( + value.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::Guid + | TypeKind::HString + | TypeKind::HResult + | TypeKind::Enum(_) + | TypeKind::Object + | TypeKind::Interface(_) + | TypeKind::Delegate(_) + | TypeKind::RuntimeClass(_) + | TypeKind::Parameterized(_) + ) + ) + } + + fn supports_callback_inout(typ: &Type) -> bool { + matches!( + &typ.abi, + ParameterType::Pointer | ParameterType::Bstr { .. } | ParameterType::NativeStruct(_) + ) || matches!( + &typ.abi, + ParameterType::WinRT(value) + if matches!( + value.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::Guid + | TypeKind::HResult + | TypeKind::Enum(_) + ) + ) + } + + fn callback_hidden_params(&self) -> BTreeSet { + self.parameters + .iter() + .filter_map(|parameter| parameter.buffer.as_ref()) + .flat_map(|buffer| { + let (first, second) = match &buffer.relation { + ComBufferRelation::Input { + count_param, + actual_length_param, + .. + } => (Some(*count_param), *actual_length_param), + ComBufferRelation::CallerCapacity { + actual_length_param, + .. + } => (*actual_length_param, None), + ComBufferRelation::EnumeratorNext { fetched_param, .. } => { + (Some(*fetched_param), None) + } + ComBufferRelation::CalleeAllocated { count_param, .. } => { + (Some(*count_param), None) + } + }; + first.into_iter().chain(second) }) .collect() } - fn invoke_values(&self, obj: *mut c_void, args: &[Value]) -> result::Result> { - if matches!(self.return_plan, ComReturnPlan::DispatchInvokeHResult(_)) { - return Err(invalid_argument( - "IDispatch::Invoke captured HRESULT calls require invoke_dispatch()", - )); - } - let expected_args = self - .arguments - .iter() - .filter(|argument| argument.input_index.is_some()) - .count(); - if args.len() != expected_args { - return Err(invalid_argument(format!( - "COM call expects {expected_args} argument(s), received {}", - args.len() - ))); - } - for (parameter_index, argument) in self.arguments.iter().enumerate() { - let Some(input_index) = argument.input_index else { - continue; - }; - if !argument.nullable - && !argument.typ.is_bstr() - && is_null_input_value(&args[input_index]) - { - return Err(invalid_argument(format!( - "required COM parameter {parameter_index} cannot be null" - ))); + unsafe fn prepare_callback_outputs( + &self, + args: *const *const c_void, + ) -> Result { + let hidden = self.callback_hidden_params(); + let mut prepared = PreparedCallback { + output_count: 0, + caller_capacities: vec![None; self.parameters.len()], + }; + for (index, parameter) in self.parameters.iter().enumerate() { + if let Some(buffer) = ¶meter.buffer { + match &buffer.relation { + ComBufferRelation::Input { .. } => continue, + ComBufferRelation::CallerCapacity { + capacity_param, + actual_length_param, + unit, + two_call: false, + } if parameter.direction == ComParameterDirection::CallerOutputBuffer + && matches!(buffer.element.kind, BufferElementKind::Plain) => + { + let capacity_value = + unsafe { self.read_callback_parameter_input(args, *capacity_param)? }; + let capacity = + count_from_value(&capacity_value).map_err(|_| SINK_E_FAIL)?; + prepared.caller_capacities[index] = Some(capacity); + let byte_len = match unit { + BufferCountUnit::Bytes => capacity, + BufferCountUnit::Elements => capacity + .checked_mul(buffer.element.size) + .ok_or(SINK_E_FAIL)?, + }; + let target = unsafe { *(*args.add(index + 1)).cast::<*mut u8>() }; + if byte_len > 0 && target.is_null() { + return Err(SINK_E_POINTER); + } + if byte_len > 0 { + unsafe { std::ptr::write_bytes(target, 0, byte_len) }; + } + if let Some(actual_index) = actual_length_param { + let actual = + unsafe { *(*args.add(*actual_index + 1)).cast::<*mut c_void>() }; + if actual.is_null() { + if self.parameters[*actual_index].direction + != ComParameterDirection::OptionalOut + { + return Err(SINK_E_POINTER); + } + } else { + let size = + Self::callback_output_size(&self.parameters[*actual_index].typ) + .ok_or(SINK_E_FAIL)?; + unsafe { std::ptr::write_bytes(actual, 0, size) }; + } + } + prepared.output_count += 1; + continue; + } + ComBufferRelation::CalleeAllocated { + count_param, + allocator: BufferAllocator::CoTaskMem, + .. + } if parameter.direction == ComParameterDirection::CalleeAllocatedBuffer + && matches!(buffer.element.kind, BufferElementKind::Plain) => + { + let target = unsafe { *(*args.add(index + 1)).cast::<*mut *mut c_void>() }; + if target.is_null() { + return Err(SINK_E_POINTER); + } + unsafe { target.write(std::ptr::null_mut()) }; + let count_target = + unsafe { *(*args.add(*count_param + 1)).cast::<*mut c_void>() }; + if count_target.is_null() { + return Err(SINK_E_POINTER); + } + let size = Self::callback_output_size(&self.parameters[*count_param].typ) + .ok_or(SINK_E_FAIL)?; + unsafe { std::ptr::write_bytes(count_target, 0, size) }; + prepared.output_count += 1; + continue; + } + _ => return Err(SINK_E_FAIL), + } + } + if hidden.contains(&index) { + continue; } - } - - let mut prepared_buffers = (0..self.arguments.len()).map(|_| None).collect::>(); - for (parameter_index, argument) in self.arguments.iter().enumerate() { if !matches!( - argument.direction, - ComParameterDirection::InputBuffer | ComParameterDirection::CallerOutputBuffer + parameter.direction, + ComParameterDirection::Out | ComParameterDirection::InOut ) { continue; } - let input_index = argument - .input_index - .expect("borrowed buffer is a logical input"); - let Value::Buffer(value) = &args[input_index] else { - return Err(invalid_argument(format!( - "COM buffer parameter {parameter_index} requires DynCom.buffer() storage" - ))); - }; - let contract = argument - .buffer - .as_ref() - .expect("buffer parameter has a contract"); - let prepared = prepare_borrowed_buffer( - value, - &contract.element, - argument.direction == ComParameterDirection::CallerOutputBuffer, - )?; - if argument.direction == ComParameterDirection::CallerOutputBuffer { - prepared.initialize_output(&contract.element); + let target = unsafe { *(*args.add(index + 1)).cast::<*mut c_void>() }; + if target.is_null() { + return Err(SINK_E_POINTER); } - prepared_buffers[parameter_index] = Some(prepared); - } - - for (buffer_param, argument) in self.arguments.iter().enumerate() { - let Some(ComBufferContract { - relation: ComBufferRelation::EnumeratorNext { fetched_param, .. }, - .. - }) = &argument.buffer - else { - continue; - }; - let buffer = prepared_buffers[buffer_param] - .as_ref() - .expect("enumerator output buffer prepared"); - let capacity = buffer_count( - buffer.byte_len, - &argument.buffer.as_ref().unwrap().element, - BufferCountUnit::Elements, - )?; - if self.arguments[*fetched_param].direction == ComParameterDirection::OptionalOut { - let input_index = self.arguments[*fetched_param] - .input_index - .expect("optional fetched output has a request argument"); - let requested = matches!( - args.get(input_index), - Some(Value::WinRt(WinRTValue::Bool(true))) - ); - if !requested && capacity != 1 { - return Err(invalid_argument( - "IEnum::Next permits a null pceltFetched only when requested capacity is exactly one", - )); - } + let size = Self::callback_output_size(¶meter.typ).ok_or(SINK_E_FAIL)?; + if parameter.direction == ComParameterDirection::Out { + unsafe { std::ptr::write_bytes(target, 0, size) }; } + prepared.output_count += 1; + } + Ok(prepared) + } + + fn callback_output_size(typ: &Type) -> Option { + match &typ.abi { + ParameterType::WinRT(typ) => match typ.kind() { + TypeKind::Bool | TypeKind::I8 | TypeKind::U8 => Some(1), + TypeKind::I16 | TypeKind::U16 | TypeKind::Char16 => Some(2), + TypeKind::I32 + | TypeKind::U32 + | TypeKind::F32 + | TypeKind::HResult + | TypeKind::Enum(_) => Some(4), + TypeKind::I64 | TypeKind::U64 | TypeKind::F64 => Some(8), + TypeKind::Guid => Some(size_of::()), + TypeKind::Object + | TypeKind::Interface(_) + | TypeKind::Delegate(_) + | TypeKind::RuntimeClass(_) + | TypeKind::Parameterized(_) => Some(size_of::<*mut c_void>()), + TypeKind::HString => Some(size_of::<*mut c_void>()), + _ => None, + }, + ParameterType::Pointer + | ParameterType::CoTaskMemWideString + | ParameterType::NativeStructPointer { .. } + | ParameterType::NativeUnionPointer(_) + | ParameterType::Variant + | ParameterType::SafeArray { .. } + | ParameterType::PropVariant + | ParameterType::DispatchParams + | ParameterType::ExcepInfo + | ParameterType::StatStg => Some(size_of::<*mut c_void>()), + ParameterType::Bstr { .. } => Some(size_of::<*mut u16>()), + ParameterType::NativeStruct(layout) => Some(layout.size()), + _ => None, } + } - let mut native_args = Vec::new(); - for (parameter_index, argument) in self.arguments.iter().enumerate() { - if !matches!( - argument.direction.native_kind(), - ParamKind::In | ParamKind::OptionalOut | ParamKind::InOut - ) { - continue; - } - if matches!( - argument.direction, - ComParameterDirection::InputBuffer | ComParameterDirection::CallerOutputBuffer - ) { - let buffer = prepared_buffers[parameter_index] - .as_ref() - .expect("borrowed buffer prepared"); - native_args.push(Value::WinRt(WinRTValue::RawPtr(buffer.ptr.cast()))); + unsafe fn callback_inputs(&self, args: *const *const c_void) -> Result, HRESULT> { + let hidden = self.callback_hidden_params(); + let mut values = Vec::new(); + for (index, parameter) in self.parameters.iter().enumerate() { + if hidden.contains(&index) { continue; } - if buffer_roles_hide_input(&argument.buffer_roles) { - let mut derived_count = None; - for role in argument - .buffer_roles - .iter() - .copied() - .filter(|role| role.hides_input()) - { - let buffer_param = role.buffer_param(); - let buffer = prepared_buffers[buffer_param] - .as_ref() - .expect("count source buffer prepared"); - let contract = self.arguments[buffer_param] - .buffer - .as_ref() - .expect("buffer contract"); - let count = buffer_count( - buffer.byte_len, - &contract.element, - relation_unit(&contract.relation), - )?; - if derived_count - .replace(count) - .is_some_and(|existing| existing != count) - { - return Err(invalid_argument( - "COM buffers sharing an authoritative count have different lengths", - )); + if let Some(buffer) = ¶meter.buffer { + if matches!( + parameter.direction, + ComParameterDirection::CallerOutputBuffer + | ComParameterDirection::CalleeAllocatedBuffer + ) { + continue; + } + let ComBufferRelation::Input { + count_param, unit, .. + } = &buffer.relation + else { + return Err(SINK_E_FAIL); + }; + if !matches!(buffer.element.kind, BufferElementKind::Plain) { + return Err(SINK_E_FAIL); + } + let count_value = + unsafe { self.read_callback_parameter_input(args, *count_param)? }; + let count = count_from_value(&count_value).map_err(|_| SINK_E_FAIL)?; + let byte_len = match *unit { + BufferCountUnit::Bytes => count, + BufferCountUnit::Elements => { + count.checked_mul(buffer.element.size).ok_or(SINK_E_FAIL)? } + }; + if byte_len % buffer.element.size != 0 { + return Err(SINK_E_FAIL); } - native_args.push(Value::WinRt(count_value( - &argument.typ, - derived_count.expect("hidden count has a source buffer"), - )?)); + let pointer = unsafe { *(*args.add(index + 1)).cast::<*mut u8>() }; + if byte_len > 0 && pointer.is_null() { + return Err(SINK_E_POINTER); + } + let bytes = if byte_len == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(pointer, byte_len) }.to_vec() + }; + values.push(Value::Buffer(ComBufferValue::owned( + bytes, + byte_len / buffer.element.size, + ))); continue; } - native_args.push(args[argument.input_index.expect("visible native input")].clone()); + match parameter.direction { + ComParameterDirection::In | ComParameterDirection::InOut => { + values.push(unsafe { self.read_callback_parameter_input(args, index)? }) + } + _ => {} + } } + Ok(values) + } - let native_result = if self.native.uses_com_value_path() - || native_args - .iter() - .any(|value| !matches!(value, Value::WinRt(_) | Value::Buffer(_))) - { - self.native - .call_com_dynamic(obj, &native_args) - .map_err(result::Error::WindowsError) - } else { - let winrt_args = native_args - .iter() - .map(|value| match value { - Value::WinRt(value) => Ok(value.clone()), - Value::NativeStruct(_) => Err(invalid_argument( - "native POD value passed to a non-struct COM method", - )), - Value::NativeUnion(_) - | Value::Bstr(_) - | Value::Variant(_) - | Value::SafeArray(_) - | Value::PropVariant(_) - | Value::DispatchParams(_) - | Value::ExcepInfo(_) - | Value::StatStg(_) => Err(invalid_argument( - "COM-local value passed to a scalar COM method", - )), - Value::Buffer(_) => Err(invalid_argument( - "COM buffer reached the private native-call backend", - )), - }) - .collect::>>()?; - self.native - .call_dynamic(obj, &winrt_args) - .map(|values| values.into_iter().map(Value::WinRt).collect()) - .map_err(result::Error::WindowsError) - }; - let native_values = match native_result { - Ok(values) => values, - Err(error) => { - cleanup_prepared_owning_outputs(&self.arguments, &prepared_buffers); - return Err(error); + unsafe fn read_callback_parameter_input( + &self, + args: *const *const c_void, + index: usize, + ) -> Result { + let parameter = self.parameters.get(index).ok_or(SINK_E_FAIL)?; + let argument = unsafe { *args.add(index + 1) }; + let value = match parameter.direction { + ComParameterDirection::In => argument, + ComParameterDirection::InOut => { + if argument.is_null() { + return Err(SINK_E_POINTER); + } + unsafe { *argument.cast::<*const c_void>() } } + _ => return Err(SINK_E_FAIL), }; + unsafe { Self::read_callback_input(¶meter.typ.abi, value) } + } - let direct_offset = usize::from(matches!( - self.return_plan, - ComReturnPlan::SemanticHResult - | ComReturnPlan::EnumeratorNextHResult - | ComReturnPlan::Direct(_) - )); - let enumerator_hresult = matches!(self.return_plan, ComReturnPlan::EnumeratorNextHResult) - .then(|| match native_values.first() { - Some(Value::WinRt(WinRTValue::HResult(value))) => Ok(*value), - _ => Err(invalid_argument( - "IEnum::Next native call did not preserve its HRESULT", - )), - }) - .transpose()?; - let native_param_result = |parameter_index: usize| -> result::Result<&Value> { - let output_index = self.arguments[parameter_index] - .output_index - .ok_or_else(|| { - invalid_argument("COM buffer relation does not reference an output") - })?; - native_values - .get(direct_offset + output_index) - .ok_or_else(|| invalid_argument("native COM output result is missing")) - }; - - let mut guarded_buffer_allocations = BTreeSet::new(); - if let Some(hresult) = enumerator_hresult.filter(|value| value.is_err()) { - for (parameter_index, argument) in self.arguments.iter().enumerate() { - let Some(contract) = &argument.buffer else { - continue; - }; - if !matches!(contract.relation, ComBufferRelation::EnumeratorNext { .. }) - || contract.element.cleanup == BufferElementCleanup::None - { - continue; + unsafe fn read_callback_input( + typ: &ParameterType, + value: *const c_void, + ) -> Result { + if value.is_null() { + return Err(SINK_E_POINTER); + } + let winrt = |value| Ok(Value::WinRt(value)); + match typ { + ParameterType::WinRT(typ) => match typ.kind() { + TypeKind::Bool => winrt(WinRTValue::Bool(unsafe { *value.cast::() } != 0)), + TypeKind::I8 => winrt(WinRTValue::I8(unsafe { *value.cast::() })), + TypeKind::U8 => winrt(WinRTValue::U8(unsafe { *value.cast::() })), + TypeKind::I16 => winrt(WinRTValue::I16(unsafe { *value.cast::() })), + TypeKind::U16 | TypeKind::Char16 => { + winrt(WinRTValue::U16(unsafe { *value.cast::() })) } - let buffer = prepared_buffers[parameter_index] - .as_ref() - .expect("enumerator output buffer prepared"); - let capacity = buffer_count( - buffer.byte_len, - &contract.element, - BufferCountUnit::Elements, - )?; - buffer.cleanup_slots(&contract.element, 0, capacity); - } - self.cleanup_post_call_outputs( - &native_values, - direct_offset, - &guarded_buffer_allocations, - ); - return Err(result::Error::WindowsError( - hresult - .ok() - .expect_err("failed IEnum::Next HRESULT must produce an error"), - )); - } - let processed = (|| -> result::Result> { - let mut values = Vec::with_capacity(self.results.len()); - for result_plan in &self.results { - let value = match result_plan.source { - ComResultSource::DirectReturn => native_values - .first() - .cloned() - .ok_or_else(|| invalid_argument("native COM direct result is missing"))?, - ComResultSource::Parameter(parameter_index) => { - native_param_result(parameter_index)?.clone() + TypeKind::I32 => winrt(WinRTValue::I32(unsafe { *value.cast::() })), + TypeKind::U32 => winrt(WinRTValue::U32(unsafe { *value.cast::() })), + TypeKind::I64 => winrt(WinRTValue::I64(unsafe { *value.cast::() })), + TypeKind::U64 => winrt(WinRTValue::U64(unsafe { *value.cast::() })), + TypeKind::F32 => winrt(WinRTValue::F32(unsafe { *value.cast::() })), + TypeKind::F64 => winrt(WinRTValue::F64(unsafe { *value.cast::() })), + TypeKind::Guid => winrt(WinRTValue::Guid(unsafe { *value.cast::() })), + TypeKind::HString => { + let raw = unsafe { *value.cast::<*mut c_void>() }; + if raw.is_null() { + winrt(WinRTValue::HString(windows_core::HSTRING::new())) + } else { + let value = unsafe { + &*(&raw as *const *mut c_void as *const windows_core::HSTRING) + }; + winrt(WinRTValue::HString(value.clone())) } - ComResultSource::Buffer(parameter_index) => { - let argument = &self.arguments[parameter_index]; - let contract = argument.buffer.as_ref().expect("buffer result contract"); - match &contract.relation { - ComBufferRelation::CallerCapacity { - actual_length_param, - unit, - two_call, - .. - } => { - let buffer = prepared_buffers[parameter_index] - .as_ref() - .expect("caller output buffer prepared"); - let capacity = - buffer_count(buffer.byte_len, &contract.element, *unit)?; - let actual = actual_length_param - .map(|index| count_from_value(native_param_result(index)?)) - .transpose()? - .unwrap_or(capacity); - if actual > capacity && !*two_call { - return Err(invalid_argument(format!( - "COM buffer actual length {actual} exceeds capacity {capacity}" - ))); - } - if contract.element.cleanup != BufferElementCleanup::None { - if actual > capacity { - return Err(invalid_argument(format!( - "COM owning array actual length {actual} exceeds capacity {capacity}" - ))); - } - Value::Buffer(buffer.take_owned_slots( - &contract.element, - actual, - capacity, - )?) - } else { - let copy_count = actual.min(capacity); - let copy_bytes = - count_bytes(copy_count, &contract.element, *unit)?; - let bytes = if copy_bytes == 0 { - Vec::new() - } else { - unsafe { - std::slice::from_raw_parts(buffer.ptr, copy_bytes) - .to_vec() - } - }; - Value::Buffer(ComBufferValue::owned(bytes, actual)) - } - } - ComBufferRelation::EnumeratorNext { fetched_param, .. } => { - let buffer = prepared_buffers[parameter_index] - .as_ref() - .expect("enumerator output buffer prepared"); - let capacity = buffer_count( - buffer.byte_len, - &contract.element, - BufferCountUnit::Elements, - )?; - let fetched_argument = &self.arguments[*fetched_param]; - let fetched_requested = fetched_argument.direction - != ComParameterDirection::OptionalOut - || matches!( - args.get( - fetched_argument - .input_index - .expect("optional fetched request input") - ), - Some(Value::WinRt(WinRTValue::Bool(true))) - ); - let actual = if fetched_requested { - count_from_value(native_param_result(*fetched_param)?)? + } + TypeKind::HResult => winrt(WinRTValue::HResult(HRESULT(unsafe { + *value.cast::() + }))), + TypeKind::Enum(_) => winrt(WinRTValue::Enum { + value: unsafe { *value.cast::() }, + type_handle: typ.clone(), + }), + TypeKind::Object + | TypeKind::Interface(_) + | TypeKind::Delegate(_) + | TypeKind::RuntimeClass(_) + | TypeKind::Parameterized(_) => { + let raw = unsafe { *value.cast::<*mut c_void>() }; + if raw.is_null() { + winrt(WinRTValue::Null) + } else { + winrt(WinRTValue::Object( + unsafe { IUnknown::from_raw_borrowed(&raw) } + .ok_or(SINK_E_POINTER)? + .clone(), + )) + } + } + _ => Err(SINK_E_FAIL), + }, + ParameterType::Pointer + | ParameterType::CoTaskMemWideString + | ParameterType::NativeUnionPointer(_) + | ParameterType::Variant + | ParameterType::SafeArray { .. } + | ParameterType::PropVariant + | ParameterType::DispatchParams + | ParameterType::ExcepInfo + | ParameterType::StatStg => { + winrt(WinRTValue::RawPtr(unsafe { *value.cast::<*mut c_void>() })) + } + ParameterType::Bstr { nullable } => { + let raw = unsafe { *value.cast::<*const u16>() }; + if raw.is_null() { + if *nullable { + Ok(Value::Bstr(BstrValue::null())) + } else { + Err(SINK_E_POINTER) + } + } else { + let value = + std::mem::ManuallyDrop::new(unsafe { windows_core::BSTR::from_raw(raw) }); + String::try_from(&*value) + .map(BstrValue::new) + .map(Value::Bstr) + .map_err(|_| SINK_E_FAIL) + } + } + ParameterType::NativeStruct(layout) => { + let bytes = + unsafe { std::slice::from_raw_parts(value.cast::(), layout.size()) } + .to_vec(); + NativeStructValue::new(layout.clone(), bytes) + .map(Value::NativeStruct) + .map_err(|_| SINK_E_FAIL) + } + ParameterType::NativeStructPointer { layout, nullable } => { + let raw = unsafe { *value.cast::<*mut u8>() }; + if raw.is_null() { + if *nullable { + Ok(Value::WinRt(WinRTValue::Null)) + } else { + Err(SINK_E_POINTER) + } + } else { + let bytes = unsafe { std::slice::from_raw_parts(raw, layout.size()) }.to_vec(); + NativeStructValue::new(layout.clone(), bytes) + .map(Value::NativeStruct) + .map_err(|_| SINK_E_FAIL) + } + } + ParameterType::VariantByValue => Err(SINK_E_FAIL), + } + } + + unsafe fn prepare_callback_writes( + &self, + args: *const *const c_void, + outputs: &[Value], + prepared: &PreparedCallback, + ) -> Result { + let hidden = self.callback_hidden_params(); + let mut output_index = 0; + let mut writes = Vec::with_capacity(outputs.len()); + for (index, parameter) in self.parameters.iter().enumerate() { + if let Some(buffer) = ¶meter.buffer { + if parameter.direction == ComParameterDirection::InputBuffer { + continue; + } + let supported = matches!( + (¶meter.direction, &buffer.relation, &buffer.element.kind), + ( + ComParameterDirection::CallerOutputBuffer, + ComBufferRelation::CallerCapacity { + two_call: false, + .. + }, + BufferElementKind::Plain + ) | ( + ComParameterDirection::CalleeAllocatedBuffer, + ComBufferRelation::CalleeAllocated { + allocator: BufferAllocator::CoTaskMem, + .. + }, + BufferElementKind::Plain + ) + ); + let Some(Value::Buffer(value)) = outputs.get(output_index) else { + return Err(SINK_E_FAIL); + }; + if !supported { + return Err(SINK_E_FAIL); + } + let bytes = value.copy_bytes().map_err(|_| SINK_E_FAIL)?; + match &buffer.relation { + ComBufferRelation::CallerCapacity { + actual_length_param, + unit, + two_call: false, + .. + } if parameter.direction == ComParameterDirection::CallerOutputBuffer => { + let capacity = prepared.caller_capacity(index)?; + let capacity_bytes = match unit { + BufferCountUnit::Bytes => capacity, + BufferCountUnit::Elements => capacity + .checked_mul(buffer.element.size) + .ok_or(SINK_E_FAIL)?, + }; + if (actual_length_param.is_none() && bytes.len() != capacity_bytes) + || bytes.len() > capacity_bytes + || (matches!(unit, BufferCountUnit::Elements) + && bytes.len() % buffer.element.size != 0) + { + return Err(SINK_E_FAIL); + } + let target = unsafe { *(*args.add(index + 1)).cast::<*mut u8>() }; + if !bytes.is_empty() && target.is_null() { + return Err(SINK_E_POINTER); + } + let actual = if let Some(actual_index) = actual_length_param { + let actual_target = + unsafe { *(*args.add(*actual_index + 1)).cast::<*mut c_void>() }; + if actual_target.is_null() { + if self.parameters[*actual_index].direction + == ComParameterDirection::OptionalOut + { + None } else { - match enumerator_hresult - .expect("enumerator result has an HRESULT") - .0 - { - 0 => 1, - 1 => 0, - value => { - if contract.element.cleanup - != BufferElementCleanup::None - { - buffer.cleanup_slots( - &contract.element, - 0, - capacity, - ); - } - return Err(invalid_argument(format!( - "IEnum::Next returned unexpected success HRESULT 0x{:08X} without pceltFetched", - value as u32 - ))); - } - } - }; - if actual > capacity { - if contract.element.cleanup != BufferElementCleanup::None { - buffer.cleanup_slots(&contract.element, 0, capacity); - } - return Err(invalid_argument(format!( - "IEnum::Next fetched count {actual} exceeds requested capacity {capacity}" - ))); - } - match contract.element.cleanup { - BufferElementCleanup::None => { - let copy_bytes = count_bytes( - actual, - &contract.element, - BufferCountUnit::Elements, - )?; - let bytes = if copy_bytes == 0 { - Vec::new() - } else { - unsafe { - std::slice::from_raw_parts(buffer.ptr, copy_bytes) - .to_vec() - } - }; - Value::Buffer(ComBufferValue::owned(bytes, actual)) - } - BufferElementCleanup::ComRelease - | BufferElementCleanup::BstrFree - | BufferElementCleanup::VariantClear - | BufferElementCleanup::CoTaskMemFree => { - Value::Buffer(buffer.take_owned_slots( - &contract.element, - actual, - capacity, - )?) - } - } - } - ComBufferRelation::CalleeAllocated { - count_param, - unit, - allocator, - } => { - let ptr = - pointer_from_value(native_param_result(parameter_index)?)?; - guarded_buffer_allocations.insert(parameter_index); - let mut allocation = BufferAllocationGuard::new(ptr, *allocator); - let count = count_from_value(native_param_result(*count_param)?)?; - let bytes = count_bytes(count, &contract.element, *unit)?; - if bytes > 0 && ptr.is_null() { - return Err(invalid_argument( - "callee returned a null buffer with a non-zero count", - )); + return Err(SINK_E_POINTER); } - let copied = if bytes == 0 { - Vec::new() - } else { - unsafe { std::slice::from_raw_parts(ptr.cast::(), bytes) } - .to_vec() + } else { + let actual = match unit { + BufferCountUnit::Bytes => bytes.len(), + BufferCountUnit::Elements => bytes.len() / buffer.element.size, }; - allocation.free(); - Value::Buffer(ComBufferValue::owned(copied, count)) - } - ComBufferRelation::Input { .. } => { - return Err(invalid_argument( - "input-only COM buffer cannot produce a buffer result", - )); + let actual = Value::WinRt( + count_value(&self.parameters[*actual_index].typ.abi, actual) + .map_err(|_| SINK_E_FAIL)?, + ); + Some(( + actual_target, + Self::prepare_native_callback_output( + &self.parameters[*actual_index].typ, + false, + &actual, + )?, + )) } + } else { + None + }; + writes.push(PreparedCallbackWrite::CallerBuffer { + target, + bytes, + actual, + }); + } + ComBufferRelation::CalleeAllocated { + count_param, + unit, + allocator: BufferAllocator::CoTaskMem, + } if parameter.direction == ComParameterDirection::CalleeAllocatedBuffer => { + if matches!(unit, BufferCountUnit::Elements) + && bytes.len() % buffer.element.size != 0 + { + return Err(SINK_E_FAIL); + } + let count = match unit { + BufferCountUnit::Bytes => bytes.len(), + BufferCountUnit::Elements => bytes.len() / buffer.element.size, + }; + let count = Value::WinRt( + count_value(&self.parameters[*count_param].typ.abi, count) + .map_err(|_| SINK_E_FAIL)?, + ); + let count = Self::prepare_native_callback_output( + &self.parameters[*count_param].typ, + false, + &count, + )?; + let allocation = if bytes.is_empty() { + std::ptr::null_mut() + } else { + callback_co_task_mem_alloc(bytes.len()) + }; + if !bytes.is_empty() && allocation.is_null() { + return Err(SINK_E_OUTOFMEMORY); + } + let allocation = + BufferAllocationGuard::new(allocation, BufferAllocator::CoTaskMem); + if !bytes.is_empty() { + unsafe { + std::ptr::copy_nonoverlapping( + bytes.as_ptr(), + allocation.ptr.cast::(), + bytes.len(), + ) + }; + } + let target = unsafe { *(*args.add(index + 1)).cast::<*mut *mut c_void>() }; + if target.is_null() { + return Err(SINK_E_POINTER); + } + let count_target = + unsafe { *(*args.add(*count_param + 1)).cast::<*mut c_void>() }; + if count_target.is_null() { + return Err(SINK_E_POINTER); } + writes.push(PreparedCallbackWrite::CalleeBuffer { + target, + allocation, + count_target, + count, + }); } - }; - values.push(value); + _ => return Err(SINK_E_FAIL), + } + output_index += 1; + continue; + } + if hidden.contains(&index) { + continue; + } + if matches!( + parameter.direction, + ComParameterDirection::Out | ComParameterDirection::InOut + ) { + let value = outputs.get(output_index).ok_or(SINK_E_FAIL)?; + let target = unsafe { *(*args.add(index + 1)).cast::<*mut c_void>() }; + if target.is_null() { + return Err(SINK_E_POINTER); + } + writes.push(PreparedCallbackWrite::Native { + target, + value: Self::prepare_native_callback_output( + ¶meter.typ, + parameter.nullable, + value, + )?, + replace_bstr: parameter.direction == ComParameterDirection::InOut + && matches!(¶meter.typ.abi, ParameterType::Bstr { .. }), + }); + output_index += 1; } - Ok(values) - })(); - if processed.is_err() { - cleanup_prepared_owning_outputs(&self.arguments, &prepared_buffers); - self.cleanup_post_call_outputs( - &native_values, - direct_offset, - &guarded_buffer_allocations, - ); } - processed + if output_index != outputs.len() { + return Err(SINK_E_FAIL); + } + Ok(PreparedCallbackWrites(writes)) + } + + fn prepare_native_callback_output( + typ: &Type, + nullable: bool, + value: &Value, + ) -> Result { + match (&typ.abi, value) { + (ParameterType::Pointer, Value::WinRt(WinRTValue::RawPtr(value))) => { + Ok(PreparedNativeCallbackOutput::Pointer(*value)) + } + (ParameterType::Bstr { nullable }, Value::Bstr(value)) => match value.as_deref() { + Some(value) => Ok(PreparedNativeCallbackOutput::Bstr(Some( + windows_core::BSTR::from(value), + ))), + None if *nullable => Ok(PreparedNativeCallbackOutput::Bstr(None)), + None => Err(SINK_E_FAIL), + }, + (ParameterType::NativeStruct(expected), Value::NativeStruct(value)) => { + if value.layout() == expected { + Ok(PreparedNativeCallbackOutput::NativeStruct( + value.bytes().to_vec(), + )) + } else { + Err(SINK_E_FAIL) + } + } + (ParameterType::WinRT(typ), Value::WinRt(value)) => match (typ.kind(), value) { + (TypeKind::Bool, WinRTValue::Bool(value)) => { + Ok(PreparedNativeCallbackOutput::Bool(u8::from(*value))) + } + (TypeKind::I8, WinRTValue::I8(value)) => { + Ok(PreparedNativeCallbackOutput::I8(*value)) + } + (TypeKind::U8, WinRTValue::U8(value)) => { + Ok(PreparedNativeCallbackOutput::U8(*value)) + } + (TypeKind::I16, WinRTValue::I16(value)) => { + Ok(PreparedNativeCallbackOutput::I16(*value)) + } + (TypeKind::U16 | TypeKind::Char16, WinRTValue::U16(value)) => { + Ok(PreparedNativeCallbackOutput::U16(*value)) + } + (TypeKind::I32, WinRTValue::I32(value)) => { + Ok(PreparedNativeCallbackOutput::I32(*value)) + } + (TypeKind::U32, WinRTValue::U32(value)) => { + Ok(PreparedNativeCallbackOutput::U32(*value)) + } + (TypeKind::I64, WinRTValue::I64(value)) => { + Ok(PreparedNativeCallbackOutput::I64(*value)) + } + (TypeKind::U64, WinRTValue::U64(value)) => { + Ok(PreparedNativeCallbackOutput::U64(*value)) + } + (TypeKind::F32, WinRTValue::F32(value)) => { + Ok(PreparedNativeCallbackOutput::F32(*value)) + } + (TypeKind::F64, WinRTValue::F64(value)) => { + Ok(PreparedNativeCallbackOutput::F64(*value)) + } + (TypeKind::Guid, WinRTValue::Guid(value)) => { + Ok(PreparedNativeCallbackOutput::Guid(*value)) + } + (TypeKind::HString, WinRTValue::HString(value)) => { + Ok(PreparedNativeCallbackOutput::HString(value.clone())) + } + (TypeKind::HResult, WinRTValue::HResult(value)) => { + Ok(PreparedNativeCallbackOutput::I32(value.0)) + } + (TypeKind::Enum(_), WinRTValue::Enum { value, type_handle }) + if typ == type_handle => + { + Ok(PreparedNativeCallbackOutput::I32(*value)) + } + ( + TypeKind::Object + | TypeKind::Interface(_) + | TypeKind::Delegate(_) + | TypeKind::RuntimeClass(_) + | TypeKind::Parameterized(_), + WinRTValue::Object(value), + ) => Self::query_callback_interface_output(typ, value) + .map(|value| PreparedNativeCallbackOutput::Interface(Some(value))), + ( + TypeKind::Object + | TypeKind::Interface(_) + | TypeKind::Delegate(_) + | TypeKind::RuntimeClass(_) + | TypeKind::Parameterized(_), + WinRTValue::Null, + ) if nullable => Ok(PreparedNativeCallbackOutput::Interface(None)), + _ => Err(SINK_E_FAIL), + }, + _ => Err(SINK_E_FAIL), + } + } + + fn query_callback_interface_output( + typ: &TypeHandle, + value: &IUnknown, + ) -> Result { + let iid = if typ.kind() == TypeKind::Object { + IInspectable::IID + } else { + typ.iid().ok_or(SINK_E_FAIL)? + }; + let mut result = std::ptr::null_mut(); + let hresult = unsafe { value.query(&iid, &mut result) }; + if hresult.is_err() { + return Err(hresult); + } + if result.is_null() { + return Err(SINK_E_NOINTERFACE); + } + Ok(unsafe { IUnknown::from_raw(result) }) + } +} + +#[derive(Debug)] +struct ComCallPlan { + native: NativeMethod, + arguments: Vec, + results: Vec, + return_plan: ComReturnPlan, +} + +impl ComCallPlan { + fn new( + native: NativeMethod, + parameters: Vec, + return_plan: ComReturnPlan, + ) -> Self { + let mut buffer_roles = vec![Vec::new(); parameters.len()]; + for (buffer_param, parameter) in parameters.iter().enumerate() { + let Some(buffer) = ¶meter.buffer else { + continue; + }; + for related in buffer.relation.related_params() { + assert!( + related < parameters.len() && related != buffer_param, + "counted COM buffer relationships must reference another parameter" + ); + } + match buffer.relation { + ComBufferRelation::Input { + count_param, + actual_length_param, + .. + } => { + set_buffer_role( + &mut buffer_roles, + count_param, + ComBufferParamRole::InputCount { buffer_param }, + ); + if let Some(actual) = actual_length_param { + set_buffer_role( + &mut buffer_roles, + actual, + ComBufferParamRole::InputActual { buffer_param }, + ); + } + } + ComBufferRelation::CallerCapacity { + capacity_param, + actual_length_param, + .. + } => { + if actual_length_param == Some(capacity_param) { + set_buffer_role( + &mut buffer_roles, + capacity_param, + ComBufferParamRole::CallerCapacityActual { buffer_param }, + ); + } else { + set_buffer_role( + &mut buffer_roles, + capacity_param, + ComBufferParamRole::CallerCapacity { buffer_param }, + ); + if let Some(actual) = actual_length_param { + set_buffer_role( + &mut buffer_roles, + actual, + ComBufferParamRole::CallerActual { buffer_param }, + ); + } + } + } + ComBufferRelation::EnumeratorNext { + capacity_param, + fetched_param, + } => { + set_buffer_role( + &mut buffer_roles, + capacity_param, + ComBufferParamRole::CallerCapacity { buffer_param }, + ); + set_buffer_role( + &mut buffer_roles, + fetched_param, + ComBufferParamRole::CallerActual { buffer_param }, + ); + } + ComBufferRelation::CalleeAllocated { count_param, .. } => { + set_buffer_role( + &mut buffer_roles, + count_param, + ComBufferParamRole::CalleeCount { buffer_param }, + ); + } + } + } + + let mut input_index = 0; + let mut output_index = 0; + let mut arguments = Vec::with_capacity(parameters.len()); + let mut results = Vec::new(); + + match &return_plan { + ComReturnPlan::SemanticHResult | ComReturnPlan::EnumeratorNextHResult => { + results.push(ComResultPlan { + source: ComResultSource::DirectReturn, + typ: None, + success: ComSuccessDisposition::Value, + failure_cleanup: OutputCleanup::None, + }) + } + ComReturnPlan::Direct(typ) => results.push(ComResultPlan { + source: ComResultSource::DirectReturn, + typ: Some(typ.abi.clone()), + success: typ.pointer_output.into(), + failure_cleanup: typ.output_cleanup(), + }), + ComReturnPlan::HResult + | ComReturnPlan::DispatchInvokeHResult(_) + | ComReturnPlan::Void => {} + } + + for (parameter_index, parameter) in parameters.into_iter().enumerate() { + let direction = parameter.direction; + let typ = parameter.typ; + let roles = &buffer_roles[parameter_index]; + let has_logical_input = direction.is_input() && !buffer_roles_hide_input(roles); + let parameter_input_index = has_logical_input.then_some(input_index); + if has_logical_input { + input_index += 1; + } + let native_kind = direction.native_kind(); + let has_native_output = matches!( + native_kind, + ParamKind::Out + | ParamKind::OptionalOut + | ParamKind::InOut + | ParamKind::OutFillArray + ); + let parameter_output_index = has_native_output.then_some(output_index); + if has_native_output { + output_index += 1; + } + let (pointer_output, failure_cleanup) = match direction { + ComParameterDirection::Out | ComParameterDirection::OptionalOut => { + (typ.pointer_output, typ.output_cleanup()) + } + ComParameterDirection::InOut => { + if typ.abi.is_bstr() { + (PointerOutputKind::Bstr, OutputCleanup::BstrFree) + } else { + (PointerOutputKind::Unclassified, OutputCleanup::None) + } + } + ComParameterDirection::CalleeAllocatedBuffer => { + (PointerOutputKind::None, typ.output_cleanup()) + } + ComParameterDirection::OutFill + | ComParameterDirection::In + | ComParameterDirection::InputBuffer + | ComParameterDirection::CallerOutputBuffer => { + (PointerOutputKind::None, OutputCleanup::None) + } + }; + let storage = match direction { + ComParameterDirection::In => ComArgumentStorage::Value, + ComParameterDirection::Out => ComArgumentStorage::OutputPointer, + ComParameterDirection::OptionalOut => ComArgumentStorage::OutputPointer, + ComParameterDirection::InOut => ComArgumentStorage::InOutPointer, + ComParameterDirection::OutFill => ComArgumentStorage::FillBuffer, + ComParameterDirection::InputBuffer => ComArgumentStorage::InputBuffer, + ComParameterDirection::CallerOutputBuffer => ComArgumentStorage::CallerOutputBuffer, + ComParameterDirection::CalleeAllocatedBuffer => { + ComArgumentStorage::CalleeAllocatedBuffer + } + }; + if matches!( + direction, + ComParameterDirection::CallerOutputBuffer + | ComParameterDirection::CalleeAllocatedBuffer + ) { + results.push(ComResultPlan { + source: ComResultSource::Buffer(parameter_index), + typ: None, + success: ComSuccessDisposition::Value, + failure_cleanup, + }); + } else if direction.is_output() && !buffer_roles_hide_output(roles) { + results.push(ComResultPlan { + source: ComResultSource::Parameter(parameter_index), + typ: Some(typ.abi.clone()), + success: pointer_output.into(), + failure_cleanup, + }); + } + arguments.push(ComArgumentPlan { + typ: typ.abi, + direction, + nullable: parameter.nullable, + storage, + input_index: parameter_input_index, + output_index: parameter_output_index, + failure_cleanup, + buffer: parameter.buffer, + buffer_roles: buffer_roles[parameter_index].clone(), + }); + } + + let plan = Self { + native, + arguments, + results, + return_plan, + }; + plan.assert_invariants(); + plan + } + + fn assert_invariants(&self) { + let mut expected_input = 0; + let mut expected_output = 0; + for (parameter_index, argument) in self.arguments.iter().enumerate() { + let has_logical_input = + argument.direction.is_input() && !buffer_roles_hide_input(&argument.buffer_roles); + assert_eq!( + argument.input_index, + has_logical_input.then_some(expected_input) + ); + if has_logical_input { + expected_input += 1; + } + let has_native_output = matches!( + argument.direction.native_kind(), + ParamKind::Out + | ParamKind::OptionalOut + | ParamKind::InOut + | ParamKind::OutFillArray + ); + assert_eq!( + argument.output_index, + has_native_output.then_some(expected_output) + ); + if has_native_output { + expected_output += 1; + assert_eq!( + self.native.output_cleanup(parameter_index), + argument.failure_cleanup + ); + } + assert_eq!( + argument.storage, + match argument.direction { + ComParameterDirection::In => ComArgumentStorage::Value, + ComParameterDirection::Out | ComParameterDirection::OptionalOut => { + ComArgumentStorage::OutputPointer + } + ComParameterDirection::InOut => ComArgumentStorage::InOutPointer, + ComParameterDirection::OutFill => ComArgumentStorage::FillBuffer, + ComParameterDirection::InputBuffer => ComArgumentStorage::InputBuffer, + ComParameterDirection::CallerOutputBuffer => { + ComArgumentStorage::CallerOutputBuffer + } + ComParameterDirection::CalleeAllocatedBuffer => { + ComArgumentStorage::CalleeAllocatedBuffer + } + } + ); + assert_eq!(&argument.typ, self.native.parameter_type(parameter_index)); + } + for result in &self.results { + if result.typ.is_none() { + assert!(matches!( + result.source, + ComResultSource::DirectReturn | ComResultSource::Buffer(_) + )); + } + if let ComResultSource::Parameter(index) | ComResultSource::Buffer(index) = + result.source + { + assert_eq!( + result.failure_cleanup, + self.arguments[index].failure_cleanup + ); + } + } + let planned_direct_type = self + .results + .first() + .filter(|result| result.source == ComResultSource::DirectReturn) + .and_then(|result| result.typ.as_ref()); + assert_eq!(planned_direct_type, self.native.direct_return_type()); + let has_direct_result = self + .results + .first() + .is_some_and(|result| result.source == ComResultSource::DirectReturn); + assert_eq!( + has_direct_result, + matches!( + self.return_plan, + ComReturnPlan::SemanticHResult + | ComReturnPlan::EnumeratorNextHResult + | ComReturnPlan::Direct(_) + ) + ); + } + + fn invoke(&self, obj: *mut c_void, args: &[WinRTValue]) -> result::Result> { + if self.native.uses_com_value_path() + || self + .arguments + .iter() + .any(|argument| argument.buffer.is_some()) + || self + .results + .iter() + .any(|result| matches!(result.source, ComResultSource::Buffer(_))) + { + return Err(invalid_argument( + "COM-local struct, union, Automation, or buffer signatures require the COM value invocation path", + )); + } + let values = args.iter().cloned().map(Value::WinRt).collect::>(); + self.invoke_values(obj, &values)? + .into_iter() + .map(|value| match value { + Value::WinRt(value) => Ok(value), + Value::NativeStruct(_) => Err(invalid_argument( + "native POD result requires the COM value invocation path", + )), + Value::NativeUnion(_) + | Value::Bstr(_) + | Value::Variant(_) + | Value::SafeArray(_) + | Value::PropVariant(_) + | Value::DispatchParams(_) + | Value::ExcepInfo(_) + | Value::StatStg(_) => Err(invalid_argument( + "COM-local result requires the COM value invocation path", + )), + Value::Buffer(_) => Err(invalid_argument( + "counted COM buffer result requires the COM value invocation path", + )), + }) + .collect() + } + + fn invoke_values(&self, obj: *mut c_void, args: &[Value]) -> result::Result> { + if matches!(self.return_plan, ComReturnPlan::DispatchInvokeHResult(_)) { + return Err(invalid_argument( + "IDispatch::Invoke captured HRESULT calls require invoke_dispatch()", + )); + } + let expected_args = self + .arguments + .iter() + .filter(|argument| argument.input_index.is_some()) + .count(); + if args.len() != expected_args { + return Err(invalid_argument(format!( + "COM call expects {expected_args} argument(s), received {}", + args.len() + ))); + } + for (parameter_index, argument) in self.arguments.iter().enumerate() { + let Some(input_index) = argument.input_index else { + continue; + }; + if !argument.nullable + && !argument.typ.is_bstr() + && is_null_input_value(&args[input_index]) + { + return Err(invalid_argument(format!( + "required COM parameter {parameter_index} cannot be null" + ))); + } + } + + let mut prepared_buffers = (0..self.arguments.len()).map(|_| None).collect::>(); + for (parameter_index, argument) in self.arguments.iter().enumerate() { + if !matches!( + argument.direction, + ComParameterDirection::InputBuffer | ComParameterDirection::CallerOutputBuffer + ) { + continue; + } + let input_index = argument + .input_index + .expect("borrowed buffer is a logical input"); + let Value::Buffer(value) = &args[input_index] else { + return Err(invalid_argument(format!( + "COM buffer parameter {parameter_index} requires DynCom.buffer() storage" + ))); + }; + let contract = argument + .buffer + .as_ref() + .expect("buffer parameter has a contract"); + let prepared = prepare_borrowed_buffer( + value, + &contract.element, + argument.direction == ComParameterDirection::CallerOutputBuffer, + )?; + if argument.direction == ComParameterDirection::CallerOutputBuffer { + prepared.initialize_output(&contract.element); + } + prepared_buffers[parameter_index] = Some(prepared); + } + + for (buffer_param, argument) in self.arguments.iter().enumerate() { + let Some(ComBufferContract { + relation: ComBufferRelation::EnumeratorNext { fetched_param, .. }, + .. + }) = &argument.buffer + else { + continue; + }; + let buffer = prepared_buffers[buffer_param] + .as_ref() + .expect("enumerator output buffer prepared"); + let capacity = buffer_count( + buffer.byte_len, + &argument.buffer.as_ref().unwrap().element, + BufferCountUnit::Elements, + )?; + if self.arguments[*fetched_param].direction == ComParameterDirection::OptionalOut { + let input_index = self.arguments[*fetched_param] + .input_index + .expect("optional fetched output has a request argument"); + let requested = matches!( + args.get(input_index), + Some(Value::WinRt(WinRTValue::Bool(true))) + ); + if !requested && capacity != 1 { + return Err(invalid_argument( + "IEnum::Next permits a null pceltFetched only when requested capacity is exactly one", + )); + } + } + } + + let mut native_args = Vec::new(); + for (parameter_index, argument) in self.arguments.iter().enumerate() { + if !matches!( + argument.direction.native_kind(), + ParamKind::In | ParamKind::OptionalOut | ParamKind::InOut + ) { + continue; + } + if matches!( + argument.direction, + ComParameterDirection::InputBuffer | ComParameterDirection::CallerOutputBuffer + ) { + let buffer = prepared_buffers[parameter_index] + .as_ref() + .expect("borrowed buffer prepared"); + native_args.push(Value::WinRt(WinRTValue::RawPtr(buffer.ptr.cast()))); + continue; + } + if buffer_roles_hide_input(&argument.buffer_roles) { + let mut derived_count = None; + for role in argument + .buffer_roles + .iter() + .copied() + .filter(|role| role.hides_input()) + { + let buffer_param = role.buffer_param(); + let buffer = prepared_buffers[buffer_param] + .as_ref() + .expect("count source buffer prepared"); + let contract = self.arguments[buffer_param] + .buffer + .as_ref() + .expect("buffer contract"); + let count = buffer_count( + buffer.byte_len, + &contract.element, + relation_unit(&contract.relation), + )?; + if derived_count + .replace(count) + .is_some_and(|existing| existing != count) + { + return Err(invalid_argument( + "COM buffers sharing an authoritative count have different lengths", + )); + } + } + native_args.push(Value::WinRt(count_value( + &argument.typ, + derived_count.expect("hidden count has a source buffer"), + )?)); + continue; + } + native_args.push(args[argument.input_index.expect("visible native input")].clone()); + } + + let native_result = if self.native.uses_com_value_path() + || native_args + .iter() + .any(|value| !matches!(value, Value::WinRt(_) | Value::Buffer(_))) + { + self.native + .call_com_dynamic(obj, &native_args) + .map_err(result::Error::WindowsError) + } else { + let winrt_args = native_args + .iter() + .map(|value| match value { + Value::WinRt(value) => Ok(value.clone()), + Value::NativeStruct(_) => Err(invalid_argument( + "native POD value passed to a non-struct COM method", + )), + Value::NativeUnion(_) + | Value::Bstr(_) + | Value::Variant(_) + | Value::SafeArray(_) + | Value::PropVariant(_) + | Value::DispatchParams(_) + | Value::ExcepInfo(_) + | Value::StatStg(_) => Err(invalid_argument( + "COM-local value passed to a scalar COM method", + )), + Value::Buffer(_) => Err(invalid_argument( + "COM buffer reached the private native-call backend", + )), + }) + .collect::>>()?; + self.native + .call_dynamic(obj, &winrt_args) + .map(|values| values.into_iter().map(Value::WinRt).collect()) + .map_err(result::Error::WindowsError) + }; + let native_values = match native_result { + Ok(values) => values, + Err(error) => { + cleanup_prepared_owning_outputs(&self.arguments, &prepared_buffers); + return Err(error); + } + }; + + let direct_offset = usize::from(matches!( + self.return_plan, + ComReturnPlan::SemanticHResult + | ComReturnPlan::EnumeratorNextHResult + | ComReturnPlan::Direct(_) + )); + let enumerator_hresult = matches!(self.return_plan, ComReturnPlan::EnumeratorNextHResult) + .then(|| match native_values.first() { + Some(Value::WinRt(WinRTValue::HResult(value))) => Ok(*value), + _ => Err(invalid_argument( + "IEnum::Next native call did not preserve its HRESULT", + )), + }) + .transpose()?; + let native_param_result = |parameter_index: usize| -> result::Result<&Value> { + let output_index = self.arguments[parameter_index] + .output_index + .ok_or_else(|| { + invalid_argument("COM buffer relation does not reference an output") + })?; + native_values + .get(direct_offset + output_index) + .ok_or_else(|| invalid_argument("native COM output result is missing")) + }; + + let mut guarded_buffer_allocations = BTreeSet::new(); + if let Some(hresult) = enumerator_hresult.filter(|value| value.is_err()) { + for (parameter_index, argument) in self.arguments.iter().enumerate() { + let Some(contract) = &argument.buffer else { + continue; + }; + if !matches!(contract.relation, ComBufferRelation::EnumeratorNext { .. }) + || contract.element.cleanup == BufferElementCleanup::None + { + continue; + } + let buffer = prepared_buffers[parameter_index] + .as_ref() + .expect("enumerator output buffer prepared"); + let capacity = buffer_count( + buffer.byte_len, + &contract.element, + BufferCountUnit::Elements, + )?; + buffer.cleanup_slots(&contract.element, 0, capacity); + } + self.cleanup_post_call_outputs( + &native_values, + direct_offset, + &guarded_buffer_allocations, + ); + return Err(result::Error::WindowsError( + hresult + .ok() + .expect_err("failed IEnum::Next HRESULT must produce an error"), + )); + } + let processed = (|| -> result::Result> { + let mut values = Vec::with_capacity(self.results.len()); + for result_plan in &self.results { + let value = match result_plan.source { + ComResultSource::DirectReturn => native_values + .first() + .cloned() + .ok_or_else(|| invalid_argument("native COM direct result is missing"))?, + ComResultSource::Parameter(parameter_index) => { + native_param_result(parameter_index)?.clone() + } + ComResultSource::Buffer(parameter_index) => { + let argument = &self.arguments[parameter_index]; + let contract = argument.buffer.as_ref().expect("buffer result contract"); + match &contract.relation { + ComBufferRelation::CallerCapacity { + actual_length_param, + unit, + two_call, + .. + } => { + let buffer = prepared_buffers[parameter_index] + .as_ref() + .expect("caller output buffer prepared"); + let capacity = + buffer_count(buffer.byte_len, &contract.element, *unit)?; + let actual = actual_length_param + .map(|index| count_from_value(native_param_result(index)?)) + .transpose()? + .unwrap_or(capacity); + if actual > capacity && !*two_call { + return Err(invalid_argument(format!( + "COM buffer actual length {actual} exceeds capacity {capacity}" + ))); + } + if contract.element.cleanup != BufferElementCleanup::None { + if actual > capacity { + return Err(invalid_argument(format!( + "COM owning array actual length {actual} exceeds capacity {capacity}" + ))); + } + Value::Buffer(buffer.take_owned_slots( + &contract.element, + actual, + capacity, + )?) + } else { + let copy_count = actual.min(capacity); + let copy_bytes = + count_bytes(copy_count, &contract.element, *unit)?; + let bytes = if copy_bytes == 0 { + Vec::new() + } else { + unsafe { + std::slice::from_raw_parts(buffer.ptr, copy_bytes) + .to_vec() + } + }; + Value::Buffer(ComBufferValue::owned(bytes, actual)) + } + } + ComBufferRelation::EnumeratorNext { fetched_param, .. } => { + let buffer = prepared_buffers[parameter_index] + .as_ref() + .expect("enumerator output buffer prepared"); + let capacity = buffer_count( + buffer.byte_len, + &contract.element, + BufferCountUnit::Elements, + )?; + let fetched_argument = &self.arguments[*fetched_param]; + let fetched_requested = fetched_argument.direction + != ComParameterDirection::OptionalOut + || matches!( + args.get( + fetched_argument + .input_index + .expect("optional fetched request input") + ), + Some(Value::WinRt(WinRTValue::Bool(true))) + ); + let actual = if fetched_requested { + count_from_value(native_param_result(*fetched_param)?)? + } else { + match enumerator_hresult + .expect("enumerator result has an HRESULT") + .0 + { + 0 => 1, + 1 => 0, + value => { + if contract.element.cleanup + != BufferElementCleanup::None + { + buffer.cleanup_slots( + &contract.element, + 0, + capacity, + ); + } + return Err(invalid_argument(format!( + "IEnum::Next returned unexpected success HRESULT 0x{:08X} without pceltFetched", + value as u32 + ))); + } + } + }; + if actual > capacity { + if contract.element.cleanup != BufferElementCleanup::None { + buffer.cleanup_slots(&contract.element, 0, capacity); + } + return Err(invalid_argument(format!( + "IEnum::Next fetched count {actual} exceeds requested capacity {capacity}" + ))); + } + match contract.element.cleanup { + BufferElementCleanup::None => { + let copy_bytes = count_bytes( + actual, + &contract.element, + BufferCountUnit::Elements, + )?; + let bytes = if copy_bytes == 0 { + Vec::new() + } else { + unsafe { + std::slice::from_raw_parts(buffer.ptr, copy_bytes) + .to_vec() + } + }; + Value::Buffer(ComBufferValue::owned(bytes, actual)) + } + BufferElementCleanup::ComRelease + | BufferElementCleanup::BstrFree + | BufferElementCleanup::VariantClear + | BufferElementCleanup::CoTaskMemFree => { + Value::Buffer(buffer.take_owned_slots( + &contract.element, + actual, + capacity, + )?) + } + } + } + ComBufferRelation::CalleeAllocated { + count_param, + unit, + allocator, + } => { + let ptr = + pointer_from_value(native_param_result(parameter_index)?)?; + guarded_buffer_allocations.insert(parameter_index); + let mut allocation = BufferAllocationGuard::new(ptr, *allocator); + let count = count_from_value(native_param_result(*count_param)?)?; + let bytes = count_bytes(count, &contract.element, *unit)?; + if bytes > 0 && ptr.is_null() { + return Err(invalid_argument( + "callee returned a null buffer with a non-zero count", + )); + } + let copied = if bytes == 0 { + Vec::new() + } else { + unsafe { std::slice::from_raw_parts(ptr.cast::(), bytes) } + .to_vec() + }; + allocation.free(); + Value::Buffer(ComBufferValue::owned(copied, count)) + } + ComBufferRelation::Input { .. } => { + return Err(invalid_argument( + "input-only COM buffer cannot produce a buffer result", + )); + } + } + } + }; + values.push(value); + } + Ok(values) + })(); + if processed.is_err() { + cleanup_prepared_owning_outputs(&self.arguments, &prepared_buffers); + self.cleanup_post_call_outputs( + &native_values, + direct_offset, + &guarded_buffer_allocations, + ); + } + processed + } + + fn invoke_dispatch( + &self, + obj: *mut c_void, + args: &[Value], + ) -> result::Result { + let ComReturnPlan::DispatchInvokeHResult(plan) = &self.return_plan else { + return Err(invalid_argument( + "method does not use the IDispatch::Invoke captured HRESULT convention", + )); + }; + if self + .arguments + .iter() + .any(|argument| argument.buffer.is_some()) + { + return Err(invalid_argument( + "IDispatch::Invoke captured HRESULT calls cannot use buffer plans", + )); + } + + let captured = self + .native + .call_com_dynamic_captured(obj, args) + .map_err(result::Error::WindowsError)?; + let mut outputs = captured.outputs; + let result = match outputs[plan.result_output_index].take() { + Some(crate::native_call::NativeCallValue::Variant(value)) => Some(value), + None => None, + Some(_) => { + return Err(invalid_argument( + "IDispatch::Invoke result output was not a VARIANT", + )); + } + }; + let excep_info = match outputs[plan.excep_info_output_index].take() { + Some(crate::native_call::NativeCallValue::ExcepInfo(value)) => Some(value), + None => None, + Some(_) => { + return Err(invalid_argument( + "IDispatch::Invoke exception output was not EXCEPINFO", + )); + } + }; + let arg_err = match outputs[plan.arg_err_output_index].take() { + Some(crate::native_call::NativeCallValue::WinRt(WinRTValue::U32(value))) => Some(value), + None => None, + Some(_) => { + return Err(invalid_argument( + "IDispatch::Invoke argument error output was not UINT", + )); + } + }; + if outputs.into_iter().any(|value| value.is_some()) { + return Err(invalid_argument( + "IDispatch::Invoke captured an unexpected native output", + )); + } + + Ok(DispatchInvokeResult { + hresult: captured.hresult, + result, + excep_info, + arg_err, + finalization_error: captured.finalization_error, + }) + } + + fn cleanup_post_call_outputs( + &self, + native_values: &[Value], + direct_offset: usize, + guarded_parameters: &BTreeSet, + ) { + let mut cleaned = BTreeSet::new(); + let mut direct_cleaned = false; + for result in &self.results { + let parameter_index = match result.source { + ComResultSource::Parameter(index) | ComResultSource::Buffer(index) => index, + ComResultSource::DirectReturn => { + if direct_cleaned || result.failure_cleanup == OutputCleanup::None { + continue; + } + direct_cleaned = true; + let Some(value) = native_values.first() else { + continue; + }; + let Ok(ptr) = pointer_from_value(value) else { + continue; + }; + unsafe { result.failure_cleanup.cleanup(ptr) }; + continue; + } + }; + if guarded_parameters.contains(¶meter_index) || !cleaned.insert(parameter_index) { + continue; + } + let cleanup = self.arguments[parameter_index].failure_cleanup; + if cleanup == OutputCleanup::None { + continue; + } + let Some(output_index) = self.arguments[parameter_index].output_index else { + continue; + }; + let Some(value) = native_values.get(direct_offset + output_index) else { + continue; + }; + let Ok(ptr) = pointer_from_value(value) else { + continue; + }; + unsafe { cleanup.cleanup(ptr) }; + } + } + + fn invoke_with_output_kinds( + &self, + obj: *mut c_void, + args: &[WinRTValue], + ) -> result::Result> { + let values = self.invoke(obj, args)?; + if values.len() != self.results.len() { + return Err(invalid_argument(format!( + "COM result plan mismatch: native call returned {} value(s), plan describes {}", + values.len(), + self.results.len() + ))); + } + Ok(values + .into_iter() + .zip( + self.results + .iter() + .map(|result| result.success.pointer_output_kind()), + ) + .collect()) + } + + fn invoke_values_with_output_kinds( + &self, + obj: *mut c_void, + args: &[Value], + ) -> result::Result> { + let values = self.invoke_values(obj, args)?; + if values.len() != self.results.len() { + return Err(invalid_argument(format!( + "COM result plan mismatch: native call returned {} value(s), plan describes {}", + values.len(), + self.results.len() + ))); + } + Ok(values + .into_iter() + .zip( + self.results + .iter() + .map(|result| result.success.pointer_output_kind()), + ) + .collect()) + } +} + +fn cleanup_prepared_owning_outputs( + arguments: &[ComArgumentPlan], + prepared_buffers: &[Option>], +) { + for (index, argument) in arguments.iter().enumerate() { + let Some(contract) = &argument.buffer else { + continue; + }; + if argument.direction != ComParameterDirection::CallerOutputBuffer + || contract.element.cleanup == BufferElementCleanup::None + { + continue; + } + let Some(buffer) = prepared_buffers[index].as_ref() else { + continue; + }; + let Ok(capacity) = buffer_count( + buffer.byte_len, + &contract.element, + relation_unit(&contract.relation), + ) else { + continue; + }; + buffer.cleanup_slots(&contract.element, 0, capacity); + } +} + +fn set_buffer_role(roles: &mut [Vec], index: usize, role: ComBufferParamRole) { + roles[index].push(role); +} + +fn buffer_roles_hide_input(roles: &[ComBufferParamRole]) -> bool { + roles.iter().any(|role| role.hides_input()) +} + +fn buffer_roles_hide_output(roles: &[ComBufferParamRole]) -> bool { + !roles.is_empty() && roles.iter().all(|role| role.hides_output()) +} + +#[derive(Debug)] +struct PreparedBuffer<'a> { + ptr: *mut u8, + byte_len: usize, + element_kind: BufferElementKind, + _owned_input: Option, + _caller_output_guard: Option>>, +} + +enum PreparedOwnedInput { + Bstr(Vec), + Variant(crate::com::automation::VariantArrayCopyValue), +} + +impl std::fmt::Debug for PreparedOwnedInput { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self { + Self::Bstr(_) => "PreparedOwnedInput::Bstr", + Self::Variant(_) => "PreparedOwnedInput::Variant", + }) + } +} + +impl PreparedBuffer<'_> { + fn initialize_output(&self, element: &BufferElementPlan) { + debug_assert_eq!(self.element_kind, element.kind); + if self.byte_len == 0 { + return; + } + match element.cleanup { + BufferElementCleanup::VariantClear => { + for index in 0..(self.byte_len / element.size) { + unsafe { + crate::com::automation::initialize_variant_slot( + self.ptr.add(index * element.size).cast(), + ) + }; + } + } + BufferElementCleanup::None + | BufferElementCleanup::ComRelease + | BufferElementCleanup::BstrFree + | BufferElementCleanup::CoTaskMemFree => unsafe { + std::ptr::write_bytes(self.ptr, 0, self.byte_len) + }, + } + } + + fn cleanup_slots(&self, element: &BufferElementPlan, start: usize, end: usize) { + for index in start..end { + let slot = unsafe { self.ptr.add(index * element.size) }; + match element.cleanup { + BufferElementCleanup::None => {} + BufferElementCleanup::ComRelease + | BufferElementCleanup::BstrFree + | BufferElementCleanup::CoTaskMemFree => { + let slot = slot.cast::<*mut c_void>(); + let value = unsafe { slot.read() }; + if !value.is_null() { + unsafe { + match element.cleanup { + BufferElementCleanup::ComRelease => { + OutputCleanup::ComRelease.cleanup(value) + } + BufferElementCleanup::BstrFree => { + OutputCleanup::BstrFree.cleanup(value) + } + BufferElementCleanup::CoTaskMemFree => { + OutputCleanup::CoTaskMemFree.cleanup(value) + } + BufferElementCleanup::None | BufferElementCleanup::VariantClear => { + unreachable!() + } + } + }; + unsafe { slot.write(std::ptr::null_mut()) }; + } + } + BufferElementCleanup::VariantClear => unsafe { + crate::com::automation::clear_variant_slot(slot.cast()); + crate::com::automation::initialize_variant_slot(slot.cast()); + }, + } + } + } + + fn take_owned_slots( + &self, + element: &BufferElementPlan, + actual: usize, + capacity: usize, + ) -> result::Result { + match element.cleanup { + BufferElementCleanup::ComRelease => self + .take_com_slots(element, actual, capacity) + .map(ComBufferValue::owned_com), + BufferElementCleanup::BstrFree => self + .take_bstr_slots(element, actual, capacity) + .map(ComBufferValue::owned_strings), + BufferElementCleanup::VariantClear => self + .take_variant_slots(element, actual, capacity) + .map(ComBufferValue::owned_variants), + BufferElementCleanup::CoTaskMemFree => self + .take_wide_string_slots(element, actual, capacity) + .map(ComBufferValue::owned_strings), + BufferElementCleanup::None => Err(invalid_argument( + "plain COM buffers do not use owning element transfer", + )), + } + } + + fn take_com_slots( + &self, + element: &BufferElementPlan, + fetched: usize, + capacity: usize, + ) -> result::Result> { + let mut values = Vec::with_capacity(fetched); + for index in 0..fetched { + let slot = unsafe { self.ptr.add(index * element.size).cast::<*mut c_void>() }; + let value = unsafe { slot.read() }; + if value.is_null() { + self.cleanup_slots(element, index, capacity); + return Err(invalid_argument( + "COM array returned a null interface pointer within the initialized range", + )); + } + unsafe { slot.write(std::ptr::null_mut()) }; + values.push(WinRTValue::Object(unsafe { IUnknown::from_raw(value) })); + } + self.cleanup_slots(element, fetched, capacity); + Ok(values) + } + + fn take_bstr_slots( + &self, + element: &BufferElementPlan, + actual: usize, + capacity: usize, + ) -> result::Result> { + let mut values = Vec::with_capacity(actual); + for index in 0..actual { + let slot = unsafe { self.ptr.add(index * element.size).cast::<*mut u16>() }; + let raw = unsafe { slot.read() }; + if raw.is_null() { + values.push(String::new()); + continue; + } + let value = unsafe { windows_core::BSTR::from_raw(raw.cast_const()) }; + values.push(value.to_string()); + unsafe { slot.write(std::ptr::null_mut()) }; + } + self.cleanup_slots(element, actual, capacity); + Ok(values) + } + + fn take_variant_slots( + &self, + element: &BufferElementPlan, + actual: usize, + capacity: usize, + ) -> result::Result> { + for index in 0..actual { + let slot = unsafe { self.ptr.add(index * element.size) }; + if let Err(error) = + unsafe { crate::com::automation::validate_variant_slot(slot.cast()) } + { + self.cleanup_slots(element, 0, capacity); + return Err(error); + } + } + let mut values = Vec::with_capacity(actual); + for index in 0..actual { + let slot = unsafe { self.ptr.add(index * element.size) }; + values.push(unsafe { crate::com::automation::take_variant_slot(slot.cast()) }?); + } + self.cleanup_slots(element, actual, capacity); + Ok(values) + } + + fn take_wide_string_slots( + &self, + element: &BufferElementPlan, + actual: usize, + capacity: usize, + ) -> result::Result> { + let mut values = Vec::with_capacity(actual); + for index in 0..actual { + let slot = unsafe { self.ptr.add(index * element.size).cast::<*mut u16>() }; + let raw = unsafe { slot.read() }; + if raw.is_null() { + self.cleanup_slots(element, 0, capacity); + return Err(invalid_argument( + "COM string array returned a null pointer within the initialized range", + )); + } + let value = unsafe { windows_core::PWSTR(raw).to_string() } + .map_err(|error| invalid_argument(format!("invalid UTF-16 COM string: {error}"))); + match value { + Ok(value) => values.push(value), + Err(error) => { + self.cleanup_slots(element, 0, capacity); + return Err(error); + } + } + } + self.cleanup_slots(element, 0, capacity); + Ok(values) + } +} + +fn prepare_borrowed_buffer<'a>( + value: &'a ComBufferValue, + element: &BufferElementPlan, + require_writable: bool, +) -> result::Result> { + let mut caller_output_guard = None; + let mut owned_input = None; + let ( + ptr, + byte_len, + source_element_size, + raw_bytes, + writable, + native_layout_name, + string_encoding, + source_element_kind, + ) = match &value.storage { + ComBufferStorage::CallerOutput { + blocks, + byte_len, + source_element_size, + native_layout_name, + element_kind, + } => { + let mut guard = blocks.try_lock().map_err(|error| match error { + TryLockError::WouldBlock => invalid_argument( + "caller-output COM storage cannot be aliased or used concurrently", + ), + TryLockError::Poisoned(_) => { + invalid_argument("caller-output COM storage lock is poisoned") + } + })?; + let ptr = guard.as_mut_ptr().cast::(); + caller_output_guard = Some(guard); + ( + ptr, + *byte_len, + *source_element_size, + false, + true, + native_layout_name.as_deref(), + None, + Some(*element_kind), + ) + } + ComBufferStorage::InterfaceArray { iid, pointers, .. } => ( + pointers.as_ptr().cast_mut().cast(), + pointers.len() * size_of::<*mut c_void>(), + size_of::<*mut c_void>(), + false, + false, + None, + None, + Some(BufferElementKind::ComInterface(*iid)), + ), + ComBufferStorage::BstrArray { values } => { + let mut allocated = Vec::with_capacity(values.len()); + for value in values { + let utf16 = value.encode_utf16().collect::>(); + let bstr = unsafe { windows::Win32::Foundation::SysAllocStringLen(Some(&utf16)) }; + if bstr.is_empty() && !utf16.is_empty() { + return Err(result::Error::WindowsError( + windows_core::Error::from_hresult(windows_core::HRESULT( + 0x8007000Eu32 as i32, + )), + )); + } + allocated.push(bstr); + } + owned_input = Some(PreparedOwnedInput::Bstr(allocated)); + let PreparedOwnedInput::Bstr(values) = + owned_input.as_mut().expect("BSTR input storage") + else { + unreachable!() + }; + ( + values.as_mut_ptr().cast(), + values.len() * size_of::<*mut c_void>(), + size_of::<*mut c_void>(), + false, + false, + None, + None, + Some(BufferElementKind::Bstr), + ) + } + ComBufferStorage::VariantArray { values } => { + owned_input = Some(PreparedOwnedInput::Variant( + crate::com::automation::VariantArrayCopyValue::new(values)?, + )); + let PreparedOwnedInput::Variant(values) = + owned_input.as_mut().expect("VARIANT input storage") + else { + unreachable!() + }; + ( + values.as_mut_ptr().cast(), + values.len() * crate::com::automation::variant_size(), + crate::com::automation::variant_size(), + false, + false, + None, + None, + Some(BufferElementKind::Variant), + ) + } + _ => { + let parts = value.borrowed_parts()?; + let source_element_kind = match parts.6 { + Some(encoding) => Some(BufferElementKind::StringPointer(encoding)), + None => Some(BufferElementKind::Plain), + }; + ( + parts.0, + parts.1, + parts.2, + parts.3, + parts.4, + parts.5, + parts.6, + source_element_kind, + ) + } + }; + if require_writable && !writable { + return Err(invalid_argument( + "caller-owned COM output buffers require writable backing storage", + )); + } + let _ = string_encoding; + if source_element_kind != Some(element.kind) { + return Err(invalid_argument( + "COM caller-output storage element contract does not match the method", + )); + } + if !raw_bytes && source_element_size != element.size { + return Err(invalid_argument(format!( + "COM typed buffer element width mismatch: expected {}, received {}", + element.size, source_element_size + ))); + } + if native_layout_name != element.native_layout_name.as_deref() { + return Err(invalid_argument( + "COM native struct buffer element layout identity mismatch", + )); + } + if byte_len % element.size != 0 { + return Err(invalid_argument(format!( + "COM buffer byte length {byte_len} is not a multiple of element width {}", + element.size + ))); + } + if byte_len > 0 && ptr as usize % element.alignment != 0 { + return Err(invalid_argument(format!( + "COM buffer backing address is not aligned to {} bytes", + element.alignment + ))); + } + Ok(PreparedBuffer { + ptr, + byte_len, + element_kind: element.kind, + _owned_input: owned_input, + _caller_output_guard: caller_output_guard, + }) +} + +fn relation_unit(relation: &ComBufferRelation) -> BufferCountUnit { + match relation { + ComBufferRelation::Input { unit, .. } + | ComBufferRelation::CallerCapacity { unit, .. } + | ComBufferRelation::CalleeAllocated { unit, .. } => *unit, + ComBufferRelation::EnumeratorNext { .. } => BufferCountUnit::Elements, } +} - fn invoke_dispatch( - &self, - obj: *mut c_void, - args: &[Value], - ) -> result::Result { - let ComReturnPlan::DispatchInvokeHResult(plan) = &self.return_plan else { - return Err(invalid_argument( - "method does not use the IDispatch::Invoke captured HRESULT convention", - )); - }; - if self - .arguments - .iter() - .any(|argument| argument.buffer.is_some()) - { - return Err(invalid_argument( - "IDispatch::Invoke captured HRESULT calls cannot use buffer plans", - )); - } - - let captured = self - .native - .call_com_dynamic_captured(obj, args) - .map_err(result::Error::WindowsError)?; - let mut outputs = captured.outputs; - let result = match outputs[plan.result_output_index].take() { - Some(crate::native_call::NativeCallValue::Variant(value)) => Some(value), - None => None, - Some(_) => { - return Err(invalid_argument( - "IDispatch::Invoke result output was not a VARIANT", - )); - } - }; - let excep_info = match outputs[plan.excep_info_output_index].take() { - Some(crate::native_call::NativeCallValue::ExcepInfo(value)) => Some(value), - None => None, - Some(_) => { +fn buffer_count( + byte_len: usize, + element: &BufferElementPlan, + unit: BufferCountUnit, +) -> result::Result { + match unit { + BufferCountUnit::Elements => { + if byte_len % element.size != 0 { return Err(invalid_argument( - "IDispatch::Invoke exception output was not EXCEPINFO", + "COM buffer length does not contain a whole number of elements", )); } - }; - let arg_err = match outputs[plan.arg_err_output_index].take() { - Some(crate::native_call::NativeCallValue::WinRt(WinRTValue::U32(value))) => Some(value), - None => None, - Some(_) => { + Ok(byte_len / element.size) + } + BufferCountUnit::Bytes => Ok(byte_len), + } +} + +fn count_bytes( + count: usize, + element: &BufferElementPlan, + unit: BufferCountUnit, +) -> result::Result { + let bytes = match unit { + BufferCountUnit::Elements => count + .checked_mul(element.size) + .ok_or_else(|| invalid_argument("COM buffer byte length overflow")), + BufferCountUnit::Bytes => { + if element.size != 1 { return Err(invalid_argument( - "IDispatch::Invoke argument error output was not UINT", + "byte-counted COM buffers currently require one-byte elements", )); } - }; - if outputs.into_iter().any(|value| value.is_some()) { + Ok(count) + } + }?; + const MAX_PROJECTED_BUFFER_BYTES: usize = i32::MAX as usize; + if bytes > isize::MAX as usize || bytes > MAX_PROJECTED_BUFFER_BYTES { + return Err(invalid_argument( + "COM buffer byte length exceeds the supported projected Buffer size", + )); + } + Ok(bytes) +} + +fn count_value(typ: &ParameterType, value: usize) -> result::Result { + let ParameterType::WinRT(typ) = typ else { + return Err(invalid_argument( + "COM buffer count parameters must use integer scalar ABI types", + )); + }; + match typ.kind() { + TypeKind::I8 => i8::try_from(value) + .map(WinRTValue::I8) + .map_err(|_| invalid_argument("COM buffer count does not fit i8")), + TypeKind::U8 => u8::try_from(value) + .map(WinRTValue::U8) + .map_err(|_| invalid_argument("COM buffer count does not fit u8")), + TypeKind::I16 => i16::try_from(value) + .map(WinRTValue::I16) + .map_err(|_| invalid_argument("COM buffer count does not fit i16")), + TypeKind::U16 | TypeKind::Char16 => u16::try_from(value) + .map(WinRTValue::U16) + .map_err(|_| invalid_argument("COM buffer count does not fit u16")), + TypeKind::I32 => i32::try_from(value) + .map(WinRTValue::I32) + .map_err(|_| invalid_argument("COM buffer count does not fit i32")), + TypeKind::U32 => u32::try_from(value) + .map(WinRTValue::U32) + .map_err(|_| invalid_argument("COM buffer count does not fit u32")), + TypeKind::I64 => i64::try_from(value) + .map(WinRTValue::I64) + .map_err(|_| invalid_argument("COM buffer count does not fit i64")), + TypeKind::U64 => u64::try_from(value) + .map(WinRTValue::U64) + .map_err(|_| invalid_argument("COM buffer count does not fit u64")), + _ => Err(invalid_argument( + "COM buffer count parameters must use an integer scalar ABI type", + )), + } +} + +fn count_from_value(value: &Value) -> result::Result { + let Value::WinRt(value) = value else { + return Err(invalid_argument( + "COM buffer count output must use an integer scalar ABI type", + )); + }; + let value = match value { + WinRTValue::I8(value) => usize::try_from(*value) + .map_err(|_| invalid_argument("COM buffer count cannot be negative"))?, + WinRTValue::U8(value) => usize::from(*value), + WinRTValue::I16(value) => usize::try_from(*value) + .map_err(|_| invalid_argument("COM buffer count cannot be negative"))?, + WinRTValue::U16(value) => usize::from(*value), + WinRTValue::I32(value) => usize::try_from(*value) + .map_err(|_| invalid_argument("COM buffer count cannot be negative"))?, + WinRTValue::U32(value) => usize::try_from(*value) + .map_err(|_| invalid_argument("COM buffer count does not fit usize"))?, + WinRTValue::I64(value) => usize::try_from(*value) + .map_err(|_| invalid_argument("COM buffer count cannot be negative or exceed usize"))?, + WinRTValue::U64(value) => usize::try_from(*value) + .map_err(|_| invalid_argument("COM buffer count does not fit usize"))?, + _ => { return Err(invalid_argument( - "IDispatch::Invoke captured an unexpected native output", + "COM buffer count output must use an integer scalar ABI type", )); } + }; + Ok(value) +} + +fn pointer_from_value(value: &Value) -> result::Result<*mut c_void> { + match value { + Value::WinRt(WinRTValue::RawPtr(ptr)) => Ok(*ptr), + Value::WinRt(WinRTValue::Null) => Ok(std::ptr::null_mut()), + _ => Err(invalid_argument( + "callee-allocated COM buffer output did not return a native pointer", + )), + } +} + +struct BufferAllocationGuard { + ptr: *mut c_void, + allocator: BufferAllocator, +} + +impl BufferAllocationGuard { + fn new(ptr: *mut c_void, allocator: BufferAllocator) -> Self { + Self { ptr, allocator } + } + + fn free(&mut self) { + if self.ptr.is_null() { + return; + } + match self.allocator { + BufferAllocator::CoTaskMem => unsafe { + windows::Win32::System::Com::CoTaskMemFree(Some(self.ptr)); + }, + } + self.ptr = std::ptr::null_mut(); + } + + fn into_raw(mut self) -> *mut c_void { + let ptr = self.ptr; + self.ptr = std::ptr::null_mut(); + ptr + } +} + +impl Drop for BufferAllocationGuard { + fn drop(&mut self) { + self.free(); + } +} + +#[cfg(test)] +thread_local! { + static FAIL_NEXT_CALLBACK_COTASKMEM_ALLOC: Cell = const { Cell::new(false) }; +} + +fn callback_co_task_mem_alloc(size: usize) -> *mut c_void { + #[cfg(test)] + if FAIL_NEXT_CALLBACK_COTASKMEM_ALLOC.with(|fail| fail.replace(false)) { + return std::ptr::null_mut(); + } + unsafe { windows::Win32::System::Com::CoTaskMemAlloc(size) } +} + +// Safety: a ComCallPlan 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 ComCallPlan {} +unsafe impl Sync for ComCallPlan {} + +#[derive(Debug, Clone)] +pub struct MethodSignature { + table: Arc, + parameters: Vec, + return_plan: ComReturnPlan, + enumerator_next_vtable_index: Option, +} + +impl MethodSignature { + pub fn new(table: &std::sync::Arc) -> Self { + Self { + table: Arc::clone(table), + parameters: Vec::new(), + return_plan: ComReturnPlan::HResult, + enumerator_next_vtable_index: None, + } + } + + pub fn add_in(mut self, typ: Type) -> Self { + let nullable = typ.input_is_intrinsically_nullable(); + self.parameters.push(ComParameterSpec { + direction: ComParameterDirection::In, + typ, + nullable, + buffer: None, + }); + self + } + + pub fn add_nullable_in(mut self, typ: Type) -> Self { + self.parameters.push(ComParameterSpec { + direction: ComParameterDirection::In, + typ, + nullable: true, + buffer: None, + }); + self + } + + pub fn add_out(mut self, typ: Type) -> Self { + self.parameters.push(ComParameterSpec { + direction: ComParameterDirection::Out, + typ, + nullable: false, + buffer: None, + }); + self + } - Ok(DispatchInvokeResult { - hresult: captured.hresult, - result, - excep_info, - arg_err, - finalization_error: captured.finalization_error, - }) + pub fn add_optional_out(mut self, typ: Type) -> Self { + self.parameters.push(ComParameterSpec { + direction: ComParameterDirection::OptionalOut, + typ, + nullable: false, + buffer: None, + }); + self } - fn cleanup_post_call_outputs( - &self, - native_values: &[Value], - direct_offset: usize, - guarded_parameters: &BTreeSet, - ) { - let mut cleaned = BTreeSet::new(); - let mut direct_cleaned = false; - for result in &self.results { - let parameter_index = match result.source { - ComResultSource::Parameter(index) | ComResultSource::Buffer(index) => index, - ComResultSource::DirectReturn => { - if direct_cleaned || result.failure_cleanup == OutputCleanup::None { - continue; - } - direct_cleaned = true; - let Some(value) = native_values.first() else { - continue; - }; - let Ok(ptr) = pointer_from_value(value) else { - continue; - }; - unsafe { result.failure_cleanup.cleanup(ptr) }; - continue; - } - }; - if guarded_parameters.contains(¶meter_index) || !cleaned.insert(parameter_index) { - continue; - } - let cleanup = self.arguments[parameter_index].failure_cleanup; - if cleanup == OutputCleanup::None { - continue; - } - let Some(output_index) = self.arguments[parameter_index].output_index else { - continue; - }; - let Some(value) = native_values.get(direct_offset + output_index) else { - continue; - }; - let Ok(ptr) = pointer_from_value(value) else { - continue; - }; - unsafe { cleanup.cleanup(ptr) }; - } + pub fn add_in_out(mut self, typ: Type) -> Self { + let nullable = typ.input_is_intrinsically_nullable(); + self.parameters.push(ComParameterSpec { + direction: ComParameterDirection::InOut, + typ, + nullable, + buffer: None, + }); + self } - fn invoke_with_output_kinds( - &self, - obj: *mut c_void, - args: &[WinRTValue], - ) -> result::Result> { - let values = self.invoke(obj, args)?; - if values.len() != self.results.len() { - return Err(invalid_argument(format!( - "COM result plan mismatch: native call returned {} value(s), plan describes {}", - values.len(), - self.results.len() - ))); - } - Ok(values - .into_iter() - .zip( - self.results - .iter() - .map(|result| result.success.pointer_output_kind()), - ) - .collect()) + pub fn add_nullable_in_out(mut self, typ: Type) -> Self { + self.parameters.push(ComParameterSpec { + direction: ComParameterDirection::InOut, + typ, + nullable: true, + buffer: None, + }); + self } - fn invoke_values_with_output_kinds( - &self, - obj: *mut c_void, - args: &[Value], - ) -> result::Result> { - let values = self.invoke_values(obj, args)?; - if values.len() != self.results.len() { - return Err(invalid_argument(format!( - "COM result plan mismatch: native call returned {} value(s), plan describes {}", - values.len(), - self.results.len() - ))); - } - Ok(values - .into_iter() - .zip( - self.results - .iter() - .map(|result| result.success.pointer_output_kind()), - ) - .collect()) + pub fn add_out_fill(mut self, typ: Type) -> Self { + self.parameters.push(ComParameterSpec { + direction: ComParameterDirection::OutFill, + typ, + nullable: false, + buffer: None, + }); + self } -} -fn cleanup_prepared_owning_outputs( - arguments: &[ComArgumentPlan], - prepared_buffers: &[Option>], -) { - for (index, argument) in arguments.iter().enumerate() { - let Some(contract) = &argument.buffer else { - continue; - }; - if argument.direction != ComParameterDirection::CallerOutputBuffer - || contract.element.cleanup == BufferElementCleanup::None - { - continue; - } - let Some(buffer) = prepared_buffers[index].as_ref() else { - continue; - }; - let Ok(capacity) = buffer_count( - buffer.byte_len, - &contract.element, - relation_unit(&contract.relation), - ) else { - continue; + pub fn add_input_buffer( + mut self, + element_type: Type, + count_param: usize, + actual_length_param: Option, + unit: BufferCountUnit, + ) -> result::Result { + let element = BufferElementPlan::from_type(&element_type)?; + self.parameters.push(ComParameterSpec { + direction: ComParameterDirection::InputBuffer, + typ: Type::pointer(), + nullable: false, + buffer: Some(ComBufferContract { + element, + relation: ComBufferRelation::Input { + count_param, + actual_length_param, + unit, + }, + }), + }); + Ok(self) + } + + pub fn add_input_string_array( + mut self, + encoding: StringEncoding, + count_param: usize, + ) -> result::Result { + self.parameters.push(ComParameterSpec { + direction: ComParameterDirection::InputBuffer, + typ: Type::pointer(), + nullable: false, + buffer: Some(ComBufferContract { + element: BufferElementPlan::string_pointer(encoding), + relation: ComBufferRelation::Input { + count_param, + actual_length_param: None, + unit: BufferCountUnit::Elements, + }, + }), + }); + Ok(self) + } + + pub fn add_caller_output_buffer( + mut self, + element_type: Type, + capacity_param: usize, + actual_length_param: Option, + unit: BufferCountUnit, + two_call: bool, + ) -> result::Result { + let element = BufferElementPlan::from_type(&element_type)?; + self.parameters.push(ComParameterSpec { + direction: ComParameterDirection::CallerOutputBuffer, + typ: Type::pointer(), + nullable: false, + buffer: Some(ComBufferContract { + element, + relation: ComBufferRelation::CallerCapacity { + capacity_param, + actual_length_param, + unit, + two_call, + }, + }), + }); + Ok(self) + } + + pub fn add_enumerator_next_buffer( + mut self, + element_type: Type, + capacity_param: usize, + fetched_param: usize, + ) -> result::Result { + let element = BufferElementPlan::from_enumerator_type(&element_type)?; + self.parameters.push(ComParameterSpec { + direction: ComParameterDirection::CallerOutputBuffer, + typ: Type::pointer(), + nullable: false, + buffer: Some(ComBufferContract { + element, + relation: ComBufferRelation::EnumeratorNext { + capacity_param, + fetched_param, + }, + }), + }); + Ok(self) + } + + pub fn add_callee_allocated_buffer( + mut self, + element_type: Type, + count_param: usize, + unit: BufferCountUnit, + allocator: BufferAllocator, + ) -> result::Result { + let element = BufferElementPlan::from_type(&element_type)?; + let typ = match allocator { + BufferAllocator::CoTaskMem => Type::co_task_mem_pointer(), }; - buffer.cleanup_slots(&contract.element, 0, capacity); + self.parameters.push(ComParameterSpec { + direction: ComParameterDirection::CalleeAllocatedBuffer, + typ, + nullable: false, + buffer: Some(ComBufferContract { + element, + relation: ComBufferRelation::CalleeAllocated { + count_param, + unit, + allocator, + }, + }), + }); + Ok(self) } -} -fn set_buffer_role(roles: &mut [Vec], index: usize, role: ComBufferParamRole) { - roles[index].push(role); -} + pub fn returns(mut self, typ: Type) -> Self { + assert!( + typ.supports_direct_return(), + "direct native returns currently support scalars, enums, and pointers" + ); + self.return_plan = ComReturnPlan::Direct(typ); + self + } -fn buffer_roles_hide_input(roles: &[ComBufferParamRole]) -> bool { - roles.iter().any(|role| role.hides_input()) -} + pub fn returns_void(mut self) -> Self { + self.return_plan = ComReturnPlan::Void; + self + } -fn buffer_roles_hide_output(roles: &[ComBufferParamRole]) -> bool { - !roles.is_empty() && roles.iter().all(|role| role.hides_output()) -} + pub fn preserve_hresult(mut self) -> Self { + self.return_plan = ComReturnPlan::SemanticHResult; + self + } -#[derive(Debug)] -struct PreparedBuffer<'a> { - ptr: *mut u8, - byte_len: usize, - element_kind: BufferElementKind, - _owned_input: Option, - _caller_output_guard: Option>>, -} + pub fn preserve_enumerator_next_hresult(mut self) -> Self { + self.enumerator_next_vtable_index = Some(3); + self.return_plan = ComReturnPlan::EnumeratorNextHResult; + self + } -enum PreparedOwnedInput { - Bstr(Vec), - Variant(crate::com::automation::VariantArrayCopyValue), -} + pub fn preserve_enumerator_next_hresult_at(mut self, vtable_index: usize) -> Self { + self.enumerator_next_vtable_index = Some(vtable_index); + self.return_plan = ComReturnPlan::EnumeratorNextHResult; + self + } -impl std::fmt::Debug for PreparedOwnedInput { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter.write_str(match self { - Self::Bstr(_) => "PreparedOwnedInput::Bstr", - Self::Variant(_) => "PreparedOwnedInput::Variant", - }) + pub fn capture_dispatch_invoke_hresult(mut self) -> Self { + self.return_plan = ComReturnPlan::DispatchInvokeHResult(CapturedHResultPlan { + result_output_index: 0, + excep_info_output_index: 1, + arg_err_output_index: 2, + }); + self } -} -impl PreparedBuffer<'_> { - fn initialize_output(&self, element: &BufferElementPlan) { - debug_assert_eq!(self.element_kind, element.kind); - if self.byte_len == 0 { - return; + fn validate_registration( + &self, + interface_iid: GUID, + method_name: &str, + vtable_index: usize, + ) -> result::Result<()> { + const IID_IDISPATCH: GUID = GUID::from_u128(0x00020400_0000_0000_c000_000000000046); + if matches!(self.return_plan, ComReturnPlan::EnumeratorNextHResult) { + let exact_contract = interface_iid != GUID::zeroed() + && method_name == "Next" + && self.enumerator_next_vtable_index == Some(vtable_index) + && self.parameters.len() == 3 + && self.parameters[0].direction == ComParameterDirection::In + && self.parameters[1].direction == ComParameterDirection::CallerOutputBuffer + && matches!( + self.parameters[2].direction, + ComParameterDirection::Out | ComParameterDirection::OptionalOut + ) + && matches!( + &self.parameters[0].typ.abi, + ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::U32) + ) + && matches!( + &self.parameters[2].typ.abi, + ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::U32) + ) + && matches!( + self.parameters[1] + .buffer + .as_ref() + .map(|contract| &contract.relation), + Some(ComBufferRelation::EnumeratorNext { + capacity_param: 0, + fetched_param: 2, + }) + ); + return exact_contract.then_some(()).ok_or_else(|| { + invalid_argument( + "enumerator HRESULT convention is restricted to the exact IEnum*::Next ABI shape", + ) + }); } - match element.cleanup { - BufferElementCleanup::VariantClear => { - for index in 0..(self.byte_len / element.size) { - unsafe { - crate::com::automation::initialize_variant_slot( - self.ptr.add(index * element.size).cast(), - ) - }; - } - } - BufferElementCleanup::None - | BufferElementCleanup::ComRelease - | BufferElementCleanup::BstrFree - | BufferElementCleanup::CoTaskMemFree => unsafe { - std::ptr::write_bytes(self.ptr, 0, self.byte_len) - }, + if !matches!(self.return_plan, ComReturnPlan::DispatchInvokeHResult(_)) { + return Ok(()); + } + + let direction = |index: usize| self.parameters[index].direction; + let exact_contract = interface_iid == IID_IDISPATCH + && method_name == "Invoke" + && vtable_index == 6 + && self.parameters.len() == 8 + && self + .parameters + .iter() + .all(|parameter| parameter.buffer.is_none()) + && (0..5).all(|index| direction(index) == ComParameterDirection::In) + && (5..8).all(|index| direction(index) == ComParameterDirection::OptionalOut) + && matches!( + &self.parameters[0].typ.abi, + ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::I32) + ) + && matches!(&self.parameters[1].typ.abi, ParameterType::Pointer) + && matches!( + &self.parameters[2].typ.abi, + ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::U32) + ) + && matches!( + &self.parameters[3].typ.abi, + ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::U16) + ) + && self.parameters[4].typ.abi.is_dispatch_params() + && self.parameters[5].typ.abi.is_variant() + && self.parameters[6].typ.abi.is_excep_info() + && matches!( + &self.parameters[7].typ.abi, + ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::U32) + ); + if exact_contract { + Ok(()) + } else { + Err(invalid_argument( + "captured HRESULT convention is restricted to the exact IDispatch::Invoke ABI contract", + )) } } - fn cleanup_slots(&self, element: &BufferElementPlan, start: usize, end: usize) { - for index in start..end { - let slot = unsafe { self.ptr.add(index * element.size) }; - match element.cleanup { - BufferElementCleanup::None => {} - BufferElementCleanup::ComRelease - | BufferElementCleanup::BstrFree - | BufferElementCleanup::CoTaskMemFree => { - let slot = slot.cast::<*mut c_void>(); - let value = unsafe { slot.read() }; - if !value.is_null() { - unsafe { - match element.cleanup { - BufferElementCleanup::ComRelease => { - OutputCleanup::ComRelease.cleanup(value) - } - BufferElementCleanup::BstrFree => { - OutputCleanup::BstrFree.cleanup(value) - } - BufferElementCleanup::CoTaskMemFree => { - OutputCleanup::CoTaskMemFree.cleanup(value) - } - BufferElementCleanup::None | BufferElementCleanup::VariantClear => { - unreachable!() - } - } - }; - unsafe { slot.write(std::ptr::null_mut()) }; - } - } - BufferElementCleanup::VariantClear => unsafe { - crate::com::automation::clear_variant_slot(slot.cast()); - crate::com::automation::initialize_variant_slot(slot.cast()); - }, + fn build(self, vtable_index: usize) -> result::Result { + validate_automation_contracts(&self.parameters, &self.return_plan)?; + validate_in_out_ownership(&self.parameters)?; + validate_buffer_contracts(&self.parameters)?; + let enumerator_buffers = self + .parameters + .iter() + .filter(|parameter| { + parameter.buffer.as_ref().is_some_and(|contract| { + matches!(contract.relation, ComBufferRelation::EnumeratorNext { .. }) + }) + }) + .count(); + if matches!(self.return_plan, ComReturnPlan::EnumeratorNextHResult) { + if enumerator_buffers != 1 { + return Err(invalid_argument( + "enumerator HRESULT calls require exactly one EnumeratorNext buffer", + )); } + } else if enumerator_buffers != 0 { + return Err(invalid_argument( + "EnumeratorNext buffers require the enumerator HRESULT return convention", + )); } + let callback_plan = + CallbackMethodPlan::new(self.parameters.clone(), self.return_plan.clone()); + let native_parameters = self + .parameters + .iter() + .map(|parameter| { + let cleanup = match parameter.direction { + ComParameterDirection::Out + | ComParameterDirection::OptionalOut + | ComParameterDirection::CalleeAllocatedBuffer => { + parameter.typ.output_cleanup() + } + ComParameterDirection::InOut if parameter.typ.abi.is_bstr() => { + OutputCleanup::BstrFree + } + ComParameterDirection::In + | ComParameterDirection::InOut + | ComParameterDirection::OutFill + | ComParameterDirection::InputBuffer + | ComParameterDirection::CallerOutputBuffer => OutputCleanup::None, + }; + ( + parameter.direction.native_kind(), + parameter.typ.abi.clone(), + cleanup, + ) + }) + .collect(); + let native_return = match &self.return_plan { + ComReturnPlan::HResult => MethodReturn::HResult, + ComReturnPlan::SemanticHResult => MethodReturn::SemanticHResult, + ComReturnPlan::EnumeratorNextHResult => MethodReturn::PreservedHResult, + ComReturnPlan::DispatchInvokeHResult(plan) => MethodReturn::CapturedHResult(*plan), + ComReturnPlan::Void => MethodReturn::Void, + ComReturnPlan::Direct(typ) => MethodReturn::Value { + typ: typ.abi.clone(), + cleanup: typ.output_cleanup(), + }, + }; + let native = + lower_completed_method(&self.table, vtable_index, native_parameters, native_return); + Ok(RegisteredMethod { + plan: ComCallPlan::new(native, self.parameters, self.return_plan), + callback_plan, + }) } +} - fn take_owned_slots( - &self, - element: &BufferElementPlan, - actual: usize, - capacity: usize, - ) -> result::Result { - match element.cleanup { - BufferElementCleanup::ComRelease => self - .take_com_slots(element, actual, capacity) - .map(ComBufferValue::owned_com), - BufferElementCleanup::BstrFree => self - .take_bstr_slots(element, actual, capacity) - .map(ComBufferValue::owned_strings), - BufferElementCleanup::VariantClear => self - .take_variant_slots(element, actual, capacity) - .map(ComBufferValue::owned_variants), - BufferElementCleanup::CoTaskMemFree => self - .take_wide_string_slots(element, actual, capacity) - .map(ComBufferValue::owned_strings), - BufferElementCleanup::None => Err(invalid_argument( - "plain COM buffers do not use owning element transfer", - )), +fn validate_in_out_ownership(parameters: &[ComParameterSpec]) -> result::Result<()> { + for parameter in parameters { + if parameter.direction == ComParameterDirection::InOut + && !parameter.typ.abi.is_bstr() + && parameter.typ.output_cleanup() != OutputCleanup::None + { + return Err(invalid_argument( + "owned COM InOut parameters require an explicit replacement and cleanup contract; only dedicated BSTR replacement is currently supported", + )); } } + Ok(()) +} - fn take_com_slots( - &self, - element: &BufferElementPlan, - fetched: usize, - capacity: usize, - ) -> result::Result> { - let mut values = Vec::with_capacity(fetched); - for index in 0..fetched { - let slot = unsafe { self.ptr.add(index * element.size).cast::<*mut c_void>() }; - let value = unsafe { slot.read() }; - if value.is_null() { - self.cleanup_slots(element, index, capacity); +fn validate_automation_contracts( + parameters: &[ComParameterSpec], + return_plan: &ComReturnPlan, +) -> result::Result<()> { + for parameter in parameters { + if parameter.typ.abi.native_union_layout().is_some() + && parameter.direction != ComParameterDirection::In + { + return Err(invalid_argument( + "native union pointers are input-only because outputs lack a proven active-field contract", + )); + } + if parameter.typ.abi.is_dispatch_params() + && parameter.direction != ComParameterDirection::In + { + return Err(invalid_argument("DISPPARAMS is input-only")); + } + if parameter.typ.abi.is_excep_info() + && !matches!( + parameter.direction, + ComParameterDirection::Out | ComParameterDirection::OptionalOut + ) + { + return Err(invalid_argument("EXCEPINFO is output-only")); + } + if parameter.typ.abi.is_stat_stg() + && !matches!( + parameter.direction, + ComParameterDirection::Out | ComParameterDirection::OptionalOut + ) + { + return Err(invalid_argument("STATSTG is output-only")); + } + if parameter.typ.abi.is_stat_stg() + && !matches!( + return_plan, + ComReturnPlan::HResult | ComReturnPlan::SemanticHResult + ) + { + return Err(invalid_argument( + "STATSTG outputs require an HRESULT return convention", + )); + } + if parameter.typ.abi.is_excep_info() + && !matches!( + return_plan, + ComReturnPlan::HResult + | ComReturnPlan::SemanticHResult + | ComReturnPlan::DispatchInvokeHResult(_) + ) + { + return Err(invalid_argument( + "EXCEPINFO outputs require an HRESULT return convention", + )); + } + if parameter.typ.abi.is_variant() + || parameter.typ.abi.is_safe_array() + || parameter.typ.abi.is_prop_variant() + { + if !matches!( + parameter.direction, + ComParameterDirection::In + | ComParameterDirection::Out + | ComParameterDirection::OptionalOut + ) { return Err(invalid_argument( - "COM array returned a null interface pointer within the initialized range", + "Automation values support only explicit input or owned output parameters; BYREF/InOut and buffer combinations are rejected", )); } - unsafe { slot.write(std::ptr::null_mut()) }; - values.push(WinRTValue::Object(unsafe { IUnknown::from_raw(value) })); } - self.cleanup_slots(element, fetched, capacity); - Ok(values) - } - - fn take_bstr_slots( - &self, - element: &BufferElementPlan, - actual: usize, - capacity: usize, - ) -> result::Result> { - let mut values = Vec::with_capacity(actual); - for index in 0..actual { - let slot = unsafe { self.ptr.add(index * element.size).cast::<*mut u16>() }; - let raw = unsafe { slot.read() }; - if raw.is_null() { - values.push(String::new()); - continue; - } - let value = unsafe { windows_core::BSTR::from_raw(raw.cast_const()) }; - values.push(value.to_string()); - unsafe { slot.write(std::ptr::null_mut()) }; + if parameter.typ.abi.is_nullable_safe_array() + && parameter.direction != ComParameterDirection::Out + { + return Err(invalid_argument( + "nullable SAFEARRAY is supported only for an exact documented owned output", + )); + } + if parameter.typ.abi.is_variant_by_value() + && parameter.direction != ComParameterDirection::In + { + return Err(invalid_argument( + "by-value VARIANT is input-only; pointer output and InOut contracts remain unsupported", + )); } - self.cleanup_slots(element, actual, capacity); - Ok(values) } + Ok(()) +} - fn take_variant_slots( - &self, - element: &BufferElementPlan, - actual: usize, - capacity: usize, - ) -> result::Result> { - for index in 0..actual { - let slot = unsafe { self.ptr.add(index * element.size) }; - if let Err(error) = - unsafe { crate::com::automation::validate_variant_slot(slot.cast()) } - { - self.cleanup_slots(element, 0, capacity); - return Err(error); +fn validate_buffer_contracts(parameters: &[ComParameterSpec]) -> result::Result<()> { + let mut related = vec![Vec::new(); parameters.len()]; + for (buffer_index, parameter) in parameters.iter().enumerate() { + let Some(contract) = ¶meter.buffer else { + continue; + }; + if contract.element.cleanup != BufferElementCleanup::None { + let supported = matches!( + (¶meter.direction, &contract.relation), + ( + ComParameterDirection::InputBuffer, + ComBufferRelation::Input { + unit: BufferCountUnit::Elements, + .. + } + ) | ( + ComParameterDirection::CallerOutputBuffer, + ComBufferRelation::CallerCapacity { + unit: BufferCountUnit::Elements, + two_call: false, + .. + } + ) | ( + ComParameterDirection::CallerOutputBuffer, + ComBufferRelation::EnumeratorNext { .. } + ) + ); + if !supported { + return Err(invalid_argument( + "owned COM buffer elements require an authoritative initialized range and per-element cleanup", + )); } } - let mut values = Vec::with_capacity(actual); - for index in 0..actual { - let slot = unsafe { self.ptr.add(index * element.size) }; - values.push(unsafe { crate::com::automation::take_variant_slot(slot.cast()) }?); + if relation_unit(&contract.relation) == BufferCountUnit::Bytes && contract.element.size != 1 + { + return Err(invalid_argument( + "byte-counted COM buffers require one-byte elements", + )); + } + if matches!(contract.element.kind, BufferElementKind::StringPointer(_)) + && !matches!( + (¶meter.direction, &contract.relation), + ( + ComParameterDirection::InputBuffer, + ComBufferRelation::Input { + actual_length_param: None, + unit: BufferCountUnit::Elements, + .. + } + ) + ) + { + return Err(invalid_argument( + "COM string pointer arrays must be borrowed, element-counted inputs", + )); + } + if matches!( + contract.element.kind, + BufferElementKind::CoTaskMemWideString + ) && !matches!( + (¶meter.direction, &contract.relation), + ( + ComParameterDirection::CallerOutputBuffer, + ComBufferRelation::EnumeratorNext { .. } + ) + ) { + return Err(invalid_argument( + "CoTaskMem string array elements require the exact EnumeratorNext contract", + )); } - self.cleanup_slots(element, actual, capacity); - Ok(values) - } - - fn take_wide_string_slots( - &self, - element: &BufferElementPlan, - actual: usize, - capacity: usize, - ) -> result::Result> { - let mut values = Vec::with_capacity(actual); - for index in 0..actual { - let slot = unsafe { self.ptr.add(index * element.size).cast::<*mut u16>() }; - let raw = unsafe { slot.read() }; - if raw.is_null() { - self.cleanup_slots(element, 0, capacity); + let mut contract_indices = BTreeSet::new(); + for index in contract.relation.related_params() { + if !contract_indices.insert(index) { + continue; + } + if index >= parameters.len() || index == buffer_index { return Err(invalid_argument( - "COM string array returned a null pointer within the initialized range", + "COM buffer count relationship references an invalid parameter index", )); } - let value = unsafe { windows_core::PWSTR(raw).to_string() } - .map_err(|error| invalid_argument(format!("invalid UTF-16 COM string: {error}"))); - match value { - Ok(value) => values.push(value), - Err(error) => { - self.cleanup_slots(element, 0, capacity); - return Err(error); - } + if !related[index].contains(&buffer_index) { + related[index].push(buffer_index); } + validate_count_type(¶meters[index].typ)?; } - self.cleanup_slots(element, 0, capacity); - Ok(values) - } -} - -fn prepare_borrowed_buffer<'a>( - value: &'a ComBufferValue, - element: &BufferElementPlan, - require_writable: bool, -) -> result::Result> { - let mut caller_output_guard = None; - let mut owned_input = None; - let ( - ptr, - byte_len, - source_element_size, - raw_bytes, - writable, - native_layout_name, - string_encoding, - source_element_kind, - ) = match &value.storage { - ComBufferStorage::CallerOutput { - blocks, - byte_len, - source_element_size, - native_layout_name, - element_kind, - } => { - let mut guard = blocks.try_lock().map_err(|error| match error { - TryLockError::WouldBlock => invalid_argument( - "caller-output COM storage cannot be aliased or used concurrently", - ), - TryLockError::Poisoned(_) => { - invalid_argument("caller-output COM storage lock is poisoned") + match (¶meter.direction, &contract.relation) { + ( + ComParameterDirection::InputBuffer, + ComBufferRelation::Input { + count_param, + actual_length_param, + .. + }, + ) => { + require_direction(parameters, *count_param, &[ComParameterDirection::In])?; + if let Some(actual) = actual_length_param { + require_direction(parameters, *actual, &[ComParameterDirection::Out])?; } - })?; - let ptr = guard.as_mut_ptr().cast::(); - caller_output_guard = Some(guard); + } ( - ptr, - *byte_len, - *source_element_size, - false, - true, - native_layout_name.as_deref(), - None, - Some(*element_kind), - ) - } - ComBufferStorage::InterfaceArray { iid, pointers, .. } => ( - pointers.as_ptr().cast_mut().cast(), - pointers.len() * size_of::<*mut c_void>(), - size_of::<*mut c_void>(), - false, - false, - None, - None, - Some(BufferElementKind::ComInterface(*iid)), - ), - ComBufferStorage::BstrArray { values } => { - let mut allocated = Vec::with_capacity(values.len()); - for value in values { - let utf16 = value.encode_utf16().collect::>(); - let bstr = unsafe { windows::Win32::Foundation::SysAllocStringLen(Some(&utf16)) }; - if bstr.is_empty() && !utf16.is_empty() { - return Err(result::Error::WindowsError( - windows_core::Error::from_hresult(windows_core::HRESULT( - 0x8007000Eu32 as i32, - )), + ComParameterDirection::CallerOutputBuffer, + ComBufferRelation::CallerCapacity { + capacity_param, + actual_length_param, + two_call, + .. + }, + ) => { + if actual_length_param == &Some(*capacity_param) { + require_direction( + parameters, + *capacity_param, + &[ComParameterDirection::InOut], + )?; + } else { + require_direction(parameters, *capacity_param, &[ComParameterDirection::In])?; + if let Some(actual) = actual_length_param { + require_direction(parameters, *actual, &[ComParameterDirection::Out])?; + } + } + if *two_call && actual_length_param.is_none() { + return Err(invalid_argument( + "two-call COM buffer sizing requires an actual-length output", )); } - allocated.push(bstr); } - owned_input = Some(PreparedOwnedInput::Bstr(allocated)); - let PreparedOwnedInput::Bstr(values) = - owned_input.as_mut().expect("BSTR input storage") - else { - unreachable!() - }; - ( - values.as_mut_ptr().cast(), - values.len() * size_of::<*mut c_void>(), - size_of::<*mut c_void>(), - false, - false, - None, - None, - Some(BufferElementKind::Bstr), - ) - } - ComBufferStorage::VariantArray { values } => { - owned_input = Some(PreparedOwnedInput::Variant( - crate::com::automation::VariantArrayCopyValue::new(values)?, - )); - let PreparedOwnedInput::Variant(values) = - owned_input.as_mut().expect("VARIANT input storage") - else { - unreachable!() - }; - ( - values.as_mut_ptr().cast(), - values.len() * crate::com::automation::variant_size(), - crate::com::automation::variant_size(), - false, - false, - None, - None, - Some(BufferElementKind::Variant), - ) - } - _ => { - let parts = value.borrowed_parts()?; - let source_element_kind = match parts.6 { - Some(encoding) => Some(BufferElementKind::StringPointer(encoding)), - None => Some(BufferElementKind::Plain), - }; ( - parts.0, - parts.1, - parts.2, - parts.3, - parts.4, - parts.5, - parts.6, - source_element_kind, - ) - } - }; - if require_writable && !writable { - return Err(invalid_argument( - "caller-owned COM output buffers require writable backing storage", - )); - } - let _ = string_encoding; - if source_element_kind != Some(element.kind) { - return Err(invalid_argument( - "COM caller-output storage element contract does not match the method", - )); - } - if !raw_bytes && source_element_size != element.size { - return Err(invalid_argument(format!( - "COM typed buffer element width mismatch: expected {}, received {}", - element.size, source_element_size - ))); - } - if native_layout_name != element.native_layout_name.as_deref() { - return Err(invalid_argument( - "COM native struct buffer element layout identity mismatch", - )); - } - if byte_len % element.size != 0 { - return Err(invalid_argument(format!( - "COM buffer byte length {byte_len} is not a multiple of element width {}", - element.size - ))); - } - if byte_len > 0 && ptr as usize % element.alignment != 0 { - return Err(invalid_argument(format!( - "COM buffer backing address is not aligned to {} bytes", - element.alignment - ))); - } - Ok(PreparedBuffer { - ptr, - byte_len, - element_kind: element.kind, - _owned_input: owned_input, - _caller_output_guard: caller_output_guard, - }) -} - -fn relation_unit(relation: &ComBufferRelation) -> BufferCountUnit { - match relation { - ComBufferRelation::Input { unit, .. } - | ComBufferRelation::CallerCapacity { unit, .. } - | ComBufferRelation::CalleeAllocated { unit, .. } => *unit, - ComBufferRelation::EnumeratorNext { .. } => BufferCountUnit::Elements, - } -} - -fn buffer_count( - byte_len: usize, - element: &BufferElementPlan, - unit: BufferCountUnit, -) -> result::Result { - match unit { - BufferCountUnit::Elements => { - if byte_len % element.size != 0 { + ComParameterDirection::CallerOutputBuffer, + ComBufferRelation::EnumeratorNext { + capacity_param, + fetched_param, + }, + ) => { + require_direction(parameters, *capacity_param, &[ComParameterDirection::In])?; + require_direction( + parameters, + *fetched_param, + &[ + ComParameterDirection::Out, + ComParameterDirection::OptionalOut, + ], + )?; + validate_u32_count_type(¶meters[*capacity_param].typ)?; + validate_u32_count_type(¶meters[*fetched_param].typ)?; + } + ( + ComParameterDirection::CalleeAllocatedBuffer, + ComBufferRelation::CalleeAllocated { count_param, .. }, + ) => { + require_direction(parameters, *count_param, &[ComParameterDirection::Out])?; + } + _ => { return Err(invalid_argument( - "COM buffer length does not contain a whole number of elements", + "COM buffer direction and count relationship do not agree", )); } - Ok(byte_len / element.size) } - BufferCountUnit::Bytes => Ok(byte_len), } -} - -fn count_bytes( - count: usize, - element: &BufferElementPlan, - unit: BufferCountUnit, -) -> result::Result { - let bytes = match unit { - BufferCountUnit::Elements => count - .checked_mul(element.size) - .ok_or_else(|| invalid_argument("COM buffer byte length overflow")), - BufferCountUnit::Bytes => { - if element.size != 1 { - return Err(invalid_argument( - "byte-counted COM buffers currently require one-byte elements", - )); - } - Ok(count) + for (count_index, buffers) in related.iter().enumerate() { + if buffers.len() > 1 { + validate_shared_count_group(parameters, count_index, buffers)?; } - }?; - const MAX_PROJECTED_BUFFER_BYTES: usize = i32::MAX as usize; - if bytes > isize::MAX as usize || bytes > MAX_PROJECTED_BUFFER_BYTES { - return Err(invalid_argument( - "COM buffer byte length exceeds the supported projected Buffer size", - )); } - Ok(bytes) + Ok(()) } -fn count_value(typ: &ParameterType, value: usize) -> result::Result { - let ParameterType::WinRT(typ) = typ else { - return Err(invalid_argument( - "COM buffer count parameters must use integer scalar ABI types", - )); - }; - match typ.kind() { - TypeKind::I8 => i8::try_from(value) - .map(WinRTValue::I8) - .map_err(|_| invalid_argument("COM buffer count does not fit i8")), - TypeKind::U8 => u8::try_from(value) - .map(WinRTValue::U8) - .map_err(|_| invalid_argument("COM buffer count does not fit u8")), - TypeKind::I16 => i16::try_from(value) - .map(WinRTValue::I16) - .map_err(|_| invalid_argument("COM buffer count does not fit i16")), - TypeKind::U16 | TypeKind::Char16 => u16::try_from(value) - .map(WinRTValue::U16) - .map_err(|_| invalid_argument("COM buffer count does not fit u16")), - TypeKind::I32 => i32::try_from(value) - .map(WinRTValue::I32) - .map_err(|_| invalid_argument("COM buffer count does not fit i32")), - TypeKind::U32 => u32::try_from(value) - .map(WinRTValue::U32) - .map_err(|_| invalid_argument("COM buffer count does not fit u32")), - TypeKind::I64 => i64::try_from(value) - .map(WinRTValue::I64) - .map_err(|_| invalid_argument("COM buffer count does not fit i64")), - TypeKind::U64 => u64::try_from(value) - .map(WinRTValue::U64) - .map_err(|_| invalid_argument("COM buffer count does not fit u64")), - _ => Err(invalid_argument( - "COM buffer count parameters must use an integer scalar ABI type", - )), +fn validate_shared_count_group( + parameters: &[ComParameterSpec], + count_index: usize, + buffers: &[usize], +) -> result::Result<()> { + let shared_input_units = buffers + .iter() + .map(|&buffer_index| { + let parameter = ¶meters[buffer_index]; + let contract = parameter.buffer.as_ref().expect("validated buffer"); + match (¶meter.direction, &contract.relation) { + ( + ComParameterDirection::InputBuffer, + ComBufferRelation::Input { + count_param, + actual_length_param: None, + unit, + }, + ) if *count_param == count_index => Some(*unit), + _ => None, + } + }) + .collect::>>(); + if shared_input_units.is_some_and(|units| { + units + .first() + .is_some_and(|first| units.iter().all(|unit| unit == first)) + }) { + return Ok(()); } -} - -fn count_from_value(value: &Value) -> result::Result { - let Value::WinRt(value) = value else { - return Err(invalid_argument( - "COM buffer count output must use an integer scalar ABI type", - )); - }; - let value = match value { - WinRTValue::I8(value) => usize::try_from(*value) - .map_err(|_| invalid_argument("COM buffer count cannot be negative"))?, - WinRTValue::U8(value) => usize::from(*value), - WinRTValue::I16(value) => usize::try_from(*value) - .map_err(|_| invalid_argument("COM buffer count cannot be negative"))?, - WinRTValue::U16(value) => usize::from(*value), - WinRTValue::I32(value) => usize::try_from(*value) - .map_err(|_| invalid_argument("COM buffer count cannot be negative"))?, - WinRTValue::U32(value) => usize::try_from(*value) - .map_err(|_| invalid_argument("COM buffer count does not fit usize"))?, - WinRTValue::I64(value) => usize::try_from(*value) - .map_err(|_| invalid_argument("COM buffer count cannot be negative or exceed usize"))?, - WinRTValue::U64(value) => usize::try_from(*value) - .map_err(|_| invalid_argument("COM buffer count does not fit usize"))?, - _ => { - return Err(invalid_argument( - "COM buffer count output must use an integer scalar ABI type", - )); + let mut parallel_inputs = 0usize; + let mut parallel_outputs = 0usize; + let parallel = buffers.iter().all(|&buffer_index| { + let parameter = ¶meters[buffer_index]; + let contract = parameter.buffer.as_ref().expect("validated buffer"); + match (¶meter.direction, &contract.relation) { + ( + ComParameterDirection::InputBuffer, + ComBufferRelation::Input { + count_param, + actual_length_param: None, + unit: BufferCountUnit::Elements, + }, + ) if *count_param == count_index => { + parallel_inputs += 1; + true + } + ( + ComParameterDirection::CallerOutputBuffer, + ComBufferRelation::CallerCapacity { + capacity_param, + actual_length_param: None, + unit: BufferCountUnit::Elements, + two_call: false, + }, + ) if *capacity_param == count_index => { + parallel_outputs += 1; + true + } + _ => false, } - }; - Ok(value) -} - -fn pointer_from_value(value: &Value) -> result::Result<*mut c_void> { - match value { - Value::WinRt(WinRTValue::RawPtr(ptr)) => Ok(*ptr), - Value::WinRt(WinRTValue::Null) => Ok(std::ptr::null_mut()), - _ => Err(invalid_argument( - "callee-allocated COM buffer output did not return a native pointer", - )), + }); + if parallel && parallel_inputs != 0 && parallel_outputs != 0 { + return Ok(()); } -} - -struct BufferAllocationGuard { - ptr: *mut c_void, - allocator: BufferAllocator, -} - -impl BufferAllocationGuard { - fn new(ptr: *mut c_void, allocator: BufferAllocator) -> Self { - Self { ptr, allocator } + if buffers.len() != 2 { + return Err(invalid_argument( + "shared COM counts require exactly one string input array and one caller output array", + )); } - - fn free(&mut self) { - if self.ptr.is_null() { - return; - } - match self.allocator { - BufferAllocator::CoTaskMem => unsafe { - windows::Win32::System::Com::CoTaskMemFree(Some(self.ptr)); - }, + let mut string_input = false; + let mut caller_output = false; + for &buffer_index in buffers { + let parameter = ¶meters[buffer_index]; + let contract = parameter.buffer.as_ref().expect("validated buffer"); + match ( + ¶meter.direction, + &contract.element.kind, + &contract.relation, + ) { + ( + ComParameterDirection::InputBuffer, + BufferElementKind::StringPointer(_), + ComBufferRelation::Input { + count_param, + actual_length_param: None, + unit: BufferCountUnit::Elements, + }, + ) if *count_param == count_index && !string_input => string_input = true, + ( + ComParameterDirection::CallerOutputBuffer, + BufferElementKind::Plain, + ComBufferRelation::CallerCapacity { + capacity_param, + actual_length_param: None, + unit: BufferCountUnit::Elements, + two_call: false, + }, + ) if *capacity_param == count_index && !caller_output => caller_output = true, + ( + ComParameterDirection::CallerOutputBuffer, + _, + ComBufferRelation::EnumeratorNext { .. }, + ) => { + return Err(invalid_argument( + "enumerator counts cannot be shared with unrelated COM buffers", + )); + } + _ => { + return Err(invalid_argument( + "unrelated COM buffers cannot share one count parameter", + )); + } } - self.ptr = std::ptr::null_mut(); } -} - -impl Drop for BufferAllocationGuard { - fn drop(&mut self) { - self.free(); + if string_input && caller_output { + Ok(()) + } else { + Err(invalid_argument( + "shared COM counts require one string input array and one caller output array", + )) } } -// Safety: a ComCallPlan 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 ComCallPlan {} -unsafe impl Sync for ComCallPlan {} - -#[derive(Debug, Clone)] -pub struct MethodSignature { - table: Arc, - parameters: Vec, - return_plan: ComReturnPlan, - enumerator_next_vtable_index: Option, -} - -impl MethodSignature { - pub fn new(table: &std::sync::Arc) -> Self { - Self { - table: Arc::clone(table), - parameters: Vec::new(), - return_plan: ComReturnPlan::HResult, - enumerator_next_vtable_index: None, - } - } - - pub fn add_in(mut self, typ: Type) -> Self { - let nullable = typ.input_is_intrinsically_nullable(); - self.parameters.push(ComParameterSpec { - direction: ComParameterDirection::In, - typ, - nullable, - buffer: None, - }); - self - } - - pub fn add_nullable_in(mut self, typ: Type) -> Self { - self.parameters.push(ComParameterSpec { - direction: ComParameterDirection::In, - typ, - nullable: true, - buffer: None, - }); - self +fn validate_count_type(typ: &Type) -> result::Result<()> { + let ParameterType::WinRT(typ) = &typ.abi else { + return Err(invalid_argument( + "COM buffer count parameters require integer scalar ABI types", + )); + }; + if matches!( + typ.kind(), + TypeKind::I8 + | TypeKind::U8 + | TypeKind::I16 + | TypeKind::U16 + | TypeKind::I32 + | TypeKind::U32 + | TypeKind::I64 + | TypeKind::U64 + ) { + Ok(()) + } else { + Err(invalid_argument( + "COM buffer count parameters require an integer scalar ABI type", + )) } +} - pub fn add_out(mut self, typ: Type) -> Self { - self.parameters.push(ComParameterSpec { - direction: ComParameterDirection::Out, - typ, - nullable: false, - buffer: None, - }); - self +fn validate_u32_count_type(typ: &Type) -> result::Result<()> { + if matches!( + &typ.abi, + ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::U32) + ) { + Ok(()) + } else { + Err(invalid_argument( + "IEnum::Next capacity and fetched parameters must use ULONG/u32", + )) } +} - pub fn add_optional_out(mut self, typ: Type) -> Self { - self.parameters.push(ComParameterSpec { - direction: ComParameterDirection::OptionalOut, - typ, - nullable: false, - buffer: None, - }); - self +fn require_direction( + parameters: &[ComParameterSpec], + index: usize, + allowed: &[ComParameterDirection], +) -> result::Result<()> { + if allowed.contains(¶meters[index].direction) { + Ok(()) + } else { + Err(invalid_argument(format!( + "COM buffer relationship parameter {index} has direction {:?}, expected one of {allowed:?}", + parameters[index].direction + ))) } +} - pub fn add_in_out(mut self, typ: Type) -> Self { - let nullable = typ.input_is_intrinsically_nullable(); - self.parameters.push(ComParameterSpec { - direction: ComParameterDirection::InOut, - typ, - nullable, - buffer: None, - }); - self - } +#[derive(Debug)] +struct RegisteredMethod { + plan: ComCallPlan, + callback_plan: CallbackMethodPlan, +} - pub fn add_nullable_in_out(mut self, typ: Type) -> Self { - self.parameters.push(ComParameterSpec { - direction: ComParameterDirection::InOut, - typ, - nullable: true, - buffer: None, - }); - self - } +type RegisteredMethods = BTreeMap)>; +type CallbackInterfaceDefinition = ( + GUID, + Vec, + Vec, + Vec, +); - pub fn add_out_fill(mut self, typ: Type) -> Self { - self.parameters.push(ComParameterSpec { - direction: ComParameterDirection::OutFill, - typ, - nullable: false, - buffer: None, - }); - self - } +#[derive(Debug, Clone)] +pub struct Interface { + name: String, + iid: GUID, + base_slot: usize, + base_iids: Arc>>, + methods: Arc>, +} - pub fn add_input_buffer( - mut self, - element_type: Type, - count_param: usize, - actual_length_param: Option, - unit: BufferCountUnit, - ) -> result::Result { - let element = BufferElementPlan::from_type(&element_type)?; - self.parameters.push(ComParameterSpec { - direction: ComParameterDirection::InputBuffer, - typ: Type::pointer(), - nullable: false, - buffer: Some(ComBufferContract { - element, - relation: ComBufferRelation::Input { - count_param, - actual_length_param, - unit, - }, - }), - }); - Ok(self) +impl Interface { + pub fn name(&self) -> &str { + &self.name } - pub fn add_input_string_array( - mut self, - encoding: StringEncoding, - count_param: usize, - ) -> result::Result { - self.parameters.push(ComParameterSpec { - direction: ComParameterDirection::InputBuffer, - typ: Type::pointer(), - nullable: false, - buffer: Some(ComBufferContract { - element: BufferElementPlan::string_pointer(encoding), - relation: ComBufferRelation::Input { - count_param, - actual_length_param: None, - unit: BufferCountUnit::Elements, - }, - }), - }); - Ok(self) + pub fn iid(&self) -> GUID { + self.iid } - pub fn add_caller_output_buffer( - mut self, - element_type: Type, - capacity_param: usize, - actual_length_param: Option, - unit: BufferCountUnit, - two_call: bool, - ) -> result::Result { - let element = BufferElementPlan::from_type(&element_type)?; - self.parameters.push(ComParameterSpec { - direction: ComParameterDirection::CallerOutputBuffer, - typ: Type::pointer(), - nullable: false, - buffer: Some(ComBufferContract { - element, - relation: ComBufferRelation::CallerCapacity { - capacity_param, - actual_length_param, - unit, - two_call, - }, - }), - }); - Ok(self) + pub fn add_method(self, name: &str, signature: MethodSignature) -> Self { + let vtable_index = self + .methods + .read() + .unwrap() + .last_key_value() + .map_or(self.base_slot, |(slot, _)| slot + 1); + self.add_method_at(vtable_index, name, signature) + .expect("sequential COM method registration must use a free vtable slot") } - pub fn add_enumerator_next_buffer( - mut self, - element_type: Type, - capacity_param: usize, - fetched_param: usize, - ) -> result::Result { - let element = BufferElementPlan::from_enumerator_type(&element_type)?; - self.parameters.push(ComParameterSpec { - direction: ComParameterDirection::CallerOutputBuffer, - typ: Type::pointer(), - nullable: false, - buffer: Some(ComBufferContract { - element, - relation: ComBufferRelation::EnumeratorNext { - capacity_param, - fetched_param, - }, - }), - }); + pub fn add_base_interface(self, iid: GUID) -> result::Result { + if iid == IUnknown::IID || iid == self.iid { + return Err(invalid_argument( + "COM base interface IID must be distinct from IUnknown and the interface IID", + )); + } + let mut base_iids = self.base_iids.write().unwrap(); + if base_iids.contains(&iid) { + return Err(invalid_argument("COM base interface IID is duplicated")); + } + base_iids.push(iid); + drop(base_iids); Ok(self) } - pub fn add_callee_allocated_buffer( - mut self, - element_type: Type, - count_param: usize, - unit: BufferCountUnit, - allocator: BufferAllocator, + pub fn add_method_at( + self, + vtable_index: usize, + name: &str, + signature: MethodSignature, ) -> result::Result { - let element = BufferElementPlan::from_type(&element_type)?; - let typ = match allocator { - BufferAllocator::CoTaskMem => Type::co_task_mem_pointer(), - }; - self.parameters.push(ComParameterSpec { - direction: ComParameterDirection::CalleeAllocatedBuffer, - typ, - nullable: false, - buffer: Some(ComBufferContract { - element, - relation: ComBufferRelation::CalleeAllocated { - count_param, - unit, - allocator, - }, - }), - }); + if vtable_index < self.base_slot { + return Err(invalid_argument(format!( + "COM method '{name}' uses vtable slot {vtable_index}, before the interface base slot {}", + self.base_slot + ))); + } + signature.validate_registration(self.iid, name, vtable_index)?; + + let mut methods = self.methods.write().unwrap(); + if methods.contains_key(&vtable_index) { + return Err(invalid_argument(format!( + "COM vtable slot {vtable_index} is already registered on '{}'", + self.name + ))); + } + methods.insert( + vtable_index, + (name.to_string(), Arc::new(signature.build(vtable_index)?)), + ); + drop(methods); Ok(self) } - pub fn returns(mut self, typ: Type) -> Self { - assert!( - typ.supports_direct_return(), - "direct native returns currently support scalars, enums, and pointers" - ); - self.return_plan = ComReturnPlan::Direct(typ); - self + pub fn method(&self, vtable_index: usize) -> Option { + self.methods + .read() + .unwrap() + .get(&vtable_index) + .map(|(_, method)| MethodHandle(Arc::clone(method))) } - pub fn returns_void(mut self) -> Self { - self.return_plan = ComReturnPlan::Void; - self + fn callback_backends(&self) -> result::Result { + if self.base_slot != InterfaceBase::IUnknown.first_method_slot() { + return Err(invalid_argument( + "COM sink interfaces must use the IUnknown vtable root", + )); + } + let methods = self.methods.read().unwrap(); + if methods.is_empty() { + return Err(invalid_argument( + "COM sink interface must contain at least one method", + )); + } + let mut backends = Vec::with_capacity(methods.len()); + let mut plans = Vec::with_capacity(methods.len()); + for (index, (&slot, (name, method))) in methods.iter().enumerate() { + if slot != self.base_slot + index { + return Err(invalid_argument(format!( + "COM sink method '{name}' uses non-contiguous vtable slot {slot}", + ))); + } + let backend = if index < SINK_INTERFACE_IN1_THUNKS.len() { + method + .callback_plan + .static_shape() + .map(CallbackBackendMethod::Static) + .or_else(|| { + method + .callback_plan + .libffi_signature() + .map(CallbackBackendMethod::Libffi) + }) + } else { + method + .callback_plan + .libffi_signature() + .map(CallbackBackendMethod::Libffi) + }; + let Some(backend) = backend else { + return Err(invalid_argument(format!( + "COM sink method '{name}' at vtable slot {slot} requires a callback ABI that is not supported", + ))); + }; + backends.push(backend); + plans.push(method.callback_plan.clone()); + } + Ok(( + self.iid, + self.base_iids.read().unwrap().clone(), + backends, + plans, + )) } +} - pub fn preserve_hresult(mut self) -> Self { - self.return_plan = ComReturnPlan::SemanticHResult; - self +#[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() } +} - pub fn preserve_enumerator_next_hresult(mut self) -> Self { - self.enumerator_next_vtable_index = Some(3); - self.return_plan = ComReturnPlan::EnumeratorNextHResult; - self +impl MethodHandle { + pub fn result_count(&self) -> usize { + self.0.plan.results.len() } - pub fn preserve_enumerator_next_hresult_at(mut self, vtable_index: usize) -> Self { - self.enumerator_next_vtable_index = Some(vtable_index); - self.return_plan = ComReturnPlan::EnumeratorNextHResult; - self + /// # Safety + /// + /// `obj` must point to a live COM interface whose vtable contains this + /// method at its registered slot for the duration of the call. + pub unsafe fn invoke( + &self, + obj: *mut c_void, + args: &[WinRTValue], + ) -> result::Result> { + self.0.plan.invoke(obj, args) } - pub fn capture_dispatch_invoke_hresult(mut self) -> Self { - self.return_plan = ComReturnPlan::DispatchInvokeHResult(CapturedHResultPlan { - result_output_index: 0, - excep_info_output_index: 1, - arg_err_output_index: 2, - }); - self + /// # Safety + /// + /// `obj` must point to a live COM interface whose vtable contains this + /// method at its registered slot for the duration of the call. + pub unsafe fn invoke_with_output_kinds( + &self, + obj: *mut c_void, + args: &[WinRTValue], + ) -> result::Result> { + self.0.plan.invoke_with_output_kinds(obj, args) } - fn validate_registration( + /// # Safety + /// + /// `obj` must point to a live COM interface whose vtable contains this + /// method at its registered slot for the duration of the call. + pub unsafe fn invoke_values_with_output_kinds( &self, - interface_iid: GUID, - method_name: &str, - vtable_index: usize, - ) -> result::Result<()> { - const IID_IDISPATCH: GUID = GUID::from_u128(0x00020400_0000_0000_c000_000000000046); - if matches!(self.return_plan, ComReturnPlan::EnumeratorNextHResult) { - let exact_contract = interface_iid != GUID::zeroed() - && method_name == "Next" - && self.enumerator_next_vtable_index == Some(vtable_index) - && self.parameters.len() == 3 - && self.parameters[0].direction == ComParameterDirection::In - && self.parameters[1].direction == ComParameterDirection::CallerOutputBuffer - && matches!( - self.parameters[2].direction, - ComParameterDirection::Out | ComParameterDirection::OptionalOut - ) - && matches!( - &self.parameters[0].typ.abi, - ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::U32) - ) - && matches!( - &self.parameters[2].typ.abi, - ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::U32) - ) - && matches!( - self.parameters[1] - .buffer - .as_ref() - .map(|contract| &contract.relation), - Some(ComBufferRelation::EnumeratorNext { - capacity_param: 0, - fetched_param: 2, - }) - ); - return exact_contract.then_some(()).ok_or_else(|| { - invalid_argument( - "enumerator HRESULT convention is restricted to the exact IEnum*::Next ABI shape", - ) - }); - } - if !matches!(self.return_plan, ComReturnPlan::DispatchInvokeHResult(_)) { - return Ok(()); - } + obj: *mut c_void, + args: &[Value], + ) -> result::Result> { + self.0.plan.invoke_values_with_output_kinds(obj, args) + } - let direction = |index: usize| self.parameters[index].direction; - let exact_contract = interface_iid == IID_IDISPATCH - && method_name == "Invoke" - && vtable_index == 6 - && self.parameters.len() == 8 - && self - .parameters - .iter() - .all(|parameter| parameter.buffer.is_none()) - && (0..5).all(|index| direction(index) == ComParameterDirection::In) - && (5..8).all(|index| direction(index) == ComParameterDirection::OptionalOut) - && matches!( - &self.parameters[0].typ.abi, - ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::I32) - ) - && matches!(&self.parameters[1].typ.abi, ParameterType::Pointer) - && matches!( - &self.parameters[2].typ.abi, - ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::U32) - ) - && matches!( - &self.parameters[3].typ.abi, - ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::U16) - ) - && self.parameters[4].typ.abi.is_dispatch_params() - && self.parameters[5].typ.abi.is_variant() - && self.parameters[6].typ.abi.is_excep_info() - && matches!( - &self.parameters[7].typ.abi, - ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::U32) - ); - if exact_contract { - Ok(()) - } else { - Err(invalid_argument( - "captured HRESULT convention is restricted to the exact IDispatch::Invoke ABI contract", - )) + /// # Safety + /// + /// `obj` must point to a live IDispatch interface whose vtable contains + /// Invoke at slot 6 for the duration of the call. + pub unsafe fn invoke_dispatch( + &self, + obj: *mut c_void, + args: &[Value], + ) -> result::Result { + self.0.plan.invoke_dispatch(obj, args) + } + + /// # Safety + /// + /// `obj` must point to a live COM interface whose vtable contains this + /// HSTRING getter at its registered slot for the duration of the call. + pub unsafe fn call_getter_hstring( + &self, + obj: *mut c_void, + ) -> result::Result { + self.0 + .plan + .native + .call_getter_hstring(obj) + .map_err(result::Error::WindowsError) + } +} + +pub fn register_interface( + _table: &std::sync::Arc, + name: &str, + iid: GUID, + base: InterfaceBase, +) -> Interface { + Interface { + name: name.to_string(), + iid, + base_slot: base.first_method_slot(), + base_iids: Arc::new(RwLock::new(Vec::new())), + methods: Arc::new(RwLock::new(BTreeMap::new())), + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum StaticCallbackShape { + InterfaceIn1, + InterfaceIn2, + InterfaceIn2OutI32, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +enum CallbackBackendMethod { + Static(StaticCallbackShape), + Libffi(crate::native_callback::CallbackSignature), +} + +#[derive(Debug, Clone)] +pub struct SinkCallbackResult { + pub hresult: HRESULT, + pub return_value: Option, + pub outputs: Vec, +} + +impl SinkCallbackResult { + pub fn hresult(hresult: HRESULT) -> Self { + Self { + hresult, + return_value: None, + outputs: Vec::new(), } } - fn build(self, vtable_index: usize) -> result::Result { - validate_automation_contracts(&self.parameters, &self.return_plan)?; - validate_in_out_ownership(&self.parameters)?; - validate_buffer_contracts(&self.parameters)?; - let enumerator_buffers = self - .parameters - .iter() - .filter(|parameter| { - parameter.buffer.as_ref().is_some_and(|contract| { - matches!(contract.relation, ComBufferRelation::EnumeratorNext { .. }) - }) - }) - .count(); - if matches!(self.return_plan, ComReturnPlan::EnumeratorNextHResult) { - if enumerator_buffers != 1 { - return Err(invalid_argument( - "enumerator HRESULT calls require exactly one EnumeratorNext buffer", - )); - } - } else if enumerator_buffers != 0 { - return Err(invalid_argument( - "EnumeratorNext buffers require the enumerator HRESULT return convention", - )); + pub fn with_output(hresult: HRESULT, value: Value) -> Self { + Self { + hresult, + return_value: None, + outputs: vec![value], } - let native_parameters = self - .parameters - .iter() - .map(|parameter| { - let cleanup = match parameter.direction { - ComParameterDirection::Out - | ComParameterDirection::OptionalOut - | ComParameterDirection::CalleeAllocatedBuffer => { - parameter.typ.output_cleanup() - } - ComParameterDirection::InOut if parameter.typ.abi.is_bstr() => { - OutputCleanup::BstrFree - } - ComParameterDirection::In - | ComParameterDirection::InOut - | ComParameterDirection::OutFill - | ComParameterDirection::InputBuffer - | ComParameterDirection::CallerOutputBuffer => OutputCleanup::None, - }; - ( - parameter.direction.native_kind(), - parameter.typ.abi.clone(), - cleanup, - ) - }) - .collect(); - let native_return = match &self.return_plan { - ComReturnPlan::HResult => MethodReturn::HResult, - ComReturnPlan::SemanticHResult => MethodReturn::SemanticHResult, - ComReturnPlan::EnumeratorNextHResult => MethodReturn::PreservedHResult, - ComReturnPlan::DispatchInvokeHResult(plan) => MethodReturn::CapturedHResult(*plan), - ComReturnPlan::Void => MethodReturn::Void, - ComReturnPlan::Direct(typ) => MethodReturn::Value { - typ: typ.abi.clone(), - cleanup: typ.output_cleanup(), - }, - }; - let native = - lower_completed_method(&self.table, vtable_index, native_parameters, native_return); - Ok(RegisteredMethod { - plan: ComCallPlan::new(native, self.parameters, self.return_plan), - }) } -} -fn validate_in_out_ownership(parameters: &[ComParameterSpec]) -> result::Result<()> { - for parameter in parameters { - if parameter.direction == ComParameterDirection::InOut - && !parameter.typ.abi.is_bstr() - && parameter.typ.output_cleanup() != OutputCleanup::None - { - return Err(invalid_argument( - "owned COM InOut parameters require an explicit replacement and cleanup contract; only dedicated BSTR replacement is currently supported", - )); + pub fn with_outputs(hresult: HRESULT, values: Vec) -> Self { + Self { + hresult, + return_value: None, + outputs: values, } } - Ok(()) -} -fn validate_automation_contracts( - parameters: &[ComParameterSpec], - return_plan: &ComReturnPlan, -) -> result::Result<()> { - for parameter in parameters { - if parameter.typ.abi.native_union_layout().is_some() - && parameter.direction != ComParameterDirection::In - { - return Err(invalid_argument( - "native union pointers are input-only because outputs lack a proven active-field contract", - )); + pub fn with_return(value: Value) -> Self { + Self { + hresult: HRESULT(0), + return_value: Some(value), + outputs: Vec::new(), } - if parameter.typ.abi.is_dispatch_params() - && parameter.direction != ComParameterDirection::In - { - return Err(invalid_argument("DISPPARAMS is input-only")); + } + + pub fn with_return_and_outputs(value: Value, outputs: Vec) -> Self { + Self { + hresult: HRESULT(0), + return_value: Some(value), + outputs, } - if parameter.typ.abi.is_excep_info() - && !matches!( - parameter.direction, - ComParameterDirection::Out | ComParameterDirection::OptionalOut - ) - { - return Err(invalid_argument("EXCEPINFO is output-only")); + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CallbackReturnKind { + HResult, + Void, + Value, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CallbackContract { + pub return_kind: CallbackReturnKind, + pub output_count: usize, +} + +impl CallbackContract { + pub const fn hresult(output_count: usize) -> Self { + Self { + return_kind: CallbackReturnKind::HResult, + output_count, } - if parameter.typ.abi.is_stat_stg() - && !matches!( - parameter.direction, - ComParameterDirection::Out | ComParameterDirection::OptionalOut - ) - { - return Err(invalid_argument("STATSTG is output-only")); + } + + pub const fn void(output_count: usize) -> Self { + Self { + return_kind: CallbackReturnKind::Void, + output_count, } - if parameter.typ.abi.is_stat_stg() - && !matches!( - return_plan, - ComReturnPlan::HResult | ComReturnPlan::SemanticHResult - ) - { - return Err(invalid_argument( - "STATSTG outputs require an HRESULT return convention", - )); + } + + pub const fn direct(output_count: usize) -> Self { + Self { + return_kind: CallbackReturnKind::Value, + output_count, } - if parameter.typ.abi.is_excep_info() - && !matches!( - return_plan, - ComReturnPlan::HResult - | ComReturnPlan::SemanticHResult - | ComReturnPlan::DispatchInvokeHResult(_) - ) - { + } +} + +pub type SinkCallback = + Arc SinkCallbackResult + Send + Sync>; + +const SINK_E_FAIL: HRESULT = HRESULT(0x80004005u32 as i32); +const SINK_E_NOINTERFACE: HRESULT = HRESULT(0x80004002u32 as i32); +const SINK_E_OUTOFMEMORY: HRESULT = HRESULT(0x8007000Eu32 as i32); +const SINK_E_POINTER: HRESULT = HRESULT(0x80004003u32 as i32); + +#[repr(C)] +struct DynamicComInterfaceView { + vtable: *const *const c_void, + owner: *mut DynamicComSink, + interface_index: usize, + _vtable_storage: Box<[*const c_void]>, +} + +#[repr(C)] +struct DynamicComSink { + vtable: *const windows_core::IUnknown_Vtbl, + ref_count: windows_core::imp::RefCount, + callback: SinkCallback, + // Each view must keep a stable address after its owner pointer is published. + #[allow(clippy::vec_box)] + interfaces: Vec>, + iids: Vec, + iid_map: Vec<(GUID, usize)>, + callback_plans: Vec>, +} + +// The refcount is atomic, the vtable is immutable after publication, and the +// callback is explicitly Send + Sync. Language bindings may impose a stricter +// apartment/thread policy before invoking their callback. +unsafe impl Send for DynamicComSink {} +unsafe impl Sync for DynamicComSink {} + +macro_rules! define_sink_thunks { + ($(($index:expr, $one:ident, $two:ident, $two_out:ident)),+ $(,)?) => { + $( + unsafe extern "system" fn $one( + this: *mut c_void, + value: *mut c_void, + ) -> HRESULT { + unsafe { DynamicComSink::dispatch_interface_in1(this, $index, value) } + } + + unsafe extern "system" fn $two( + this: *mut c_void, + first: *mut c_void, + second: *mut c_void, + ) -> HRESULT { + unsafe { + DynamicComSink::dispatch_interface_in2(this, $index, first, second) + } + } + + unsafe extern "system" fn $two_out( + this: *mut c_void, + first: *mut c_void, + second: *mut c_void, + result: *mut i32, + ) -> HRESULT { + unsafe { + DynamicComSink::dispatch_interface_in2_out_i32( + this, $index, first, second, result, + ) + } + } + )+ + + const SINK_INTERFACE_IN1_THUNKS: + [unsafe extern "system" fn(*mut c_void, *mut c_void) -> HRESULT; + define_sink_thunks!(@count $($index),+)] = + [$($one),+]; + const SINK_INTERFACE_IN2_THUNKS: + [unsafe extern "system" fn(*mut c_void, *mut c_void, *mut c_void) -> HRESULT; + define_sink_thunks!(@count $($index),+)] = + [$($two),+]; + const SINK_INTERFACE_IN2_OUT_I32_THUNKS: + [unsafe extern "system" fn( + *mut c_void, + *mut c_void, + *mut c_void, + *mut i32, + ) -> HRESULT; define_sink_thunks!(@count $($index),+)] = + [$($two_out),+]; + }; + (@count $($item:expr),+) => { + <[()]>::len(&[$(define_sink_thunks!(@replace $item)),+]) + }; + (@replace $_item:expr) => { () }; +} + +define_sink_thunks!( + (0, sink_one_0, sink_two_0, sink_two_out_0), + (1, sink_one_1, sink_two_1, sink_two_out_1), + (2, sink_one_2, sink_two_2, sink_two_out_2), + (3, sink_one_3, sink_two_3, sink_two_out_3), + (4, sink_one_4, sink_two_4, sink_two_out_4), + (5, sink_one_5, sink_two_5, sink_two_out_5), + (6, sink_one_6, sink_two_6, sink_two_out_6), + (7, sink_one_7, sink_two_7, sink_two_out_7), + (8, sink_one_8, sink_two_8, sink_two_out_8), + (9, sink_one_9, sink_two_9, sink_two_out_9), + (10, sink_one_10, sink_two_10, sink_two_out_10), + (11, sink_one_11, sink_two_11, sink_two_out_11), + (12, sink_one_12, sink_two_12, sink_two_out_12), + (13, sink_one_13, sink_two_13, sink_two_out_13), + (14, sink_one_14, sink_two_14, sink_two_out_14), + (15, sink_one_15, sink_two_15, sink_two_out_15), +); + +impl DynamicComSink { + fn create( + interfaces: Vec, + callback: SinkCallback, + ) -> result::Result { + if interfaces.is_empty() { return Err(invalid_argument( - "EXCEPINFO outputs require an HRESULT return convention", + "COM object requires at least one interface", )); } - if parameter.typ.abi.is_variant() - || parameter.typ.abi.is_safe_array() - || parameter.typ.abi.is_prop_variant() + let mut seen = HashSet::new(); + let mut views = Vec::with_capacity(interfaces.len()); + let mut iids = Vec::with_capacity(interfaces.len()); + let mut iid_map = Vec::new(); + let mut all_plans = Vec::with_capacity(interfaces.len()); + for (interface_index, (iid, base_iids, methods, plans)) in + interfaces.into_iter().enumerate() { - if !matches!( - parameter.direction, - ComParameterDirection::In - | ComParameterDirection::Out - | ComParameterDirection::OptionalOut - ) { + if iid == IUnknown::IID || !seen.insert(iid) { return Err(invalid_argument( - "Automation values support only explicit input or owned output parameters; BYREF/InOut and buffer combinations are rejected", + "COM object interface IIDs must be unique and cannot be IID_IUnknown", )); } + iid_map.push((iid, interface_index)); + for base_iid in base_iids { + if base_iid == IUnknown::IID || !seen.insert(base_iid) { + return Err(invalid_argument( + "COM object base interface IIDs must be unique across interface roots", + )); + } + iid_map.push((base_iid, interface_index)); + } + let mut slots = vec![ + Self::view_query_interface as *const () as *const c_void, + Self::view_add_ref as *const () as *const c_void, + Self::view_release as *const () as *const c_void, + ]; + for (index, method) in methods.iter().enumerate() { + slots.push(match method { + CallbackBackendMethod::Static(StaticCallbackShape::InterfaceIn1) => { + SINK_INTERFACE_IN1_THUNKS[index] as *const () as *const c_void + } + CallbackBackendMethod::Static(StaticCallbackShape::InterfaceIn2) => { + SINK_INTERFACE_IN2_THUNKS[index] as *const () as *const c_void + } + CallbackBackendMethod::Static(StaticCallbackShape::InterfaceIn2OutI32) => { + SINK_INTERFACE_IN2_OUT_I32_THUNKS[index] as *const () as *const c_void + } + CallbackBackendMethod::Libffi(signature) => { + crate::native_callback::callback_code( + index + 3, + signature.clone(), + Self::dispatch_libffi, + ) + .map_err(|error| { + invalid_argument(format!( + "failed to create COM callback at vtable slot {}: {error}", + index + 3 + )) + })? + } + }); + } + let storage = slots.into_boxed_slice(); + views.push(Box::new(DynamicComInterfaceView { + vtable: storage.as_ptr(), + owner: std::ptr::null_mut(), + interface_index, + _vtable_storage: storage, + })); + iids.push(iid); + all_plans.push(plans); + } + let mut sink = Box::new(Self { + vtable: &Self::IDENTITY_VTABLE, + ref_count: windows_core::imp::RefCount::new(1), + callback, + interfaces: views, + iids, + iid_map, + callback_plans: all_plans, + }); + let owner = (&mut *sink) as *mut Self; + for view in &mut sink.interfaces { + view.owner = owner; } - if parameter.typ.abi.is_nullable_safe_array() - && parameter.direction != ComParameterDirection::Out - { - return Err(invalid_argument( - "nullable SAFEARRAY is supported only for an exact documented owned output", - )); + Ok(unsafe { IUnknown::from_raw(Box::into_raw(sink).cast()) }) + } + + const IDENTITY_VTABLE: windows_core::IUnknown_Vtbl = windows_core::IUnknown_Vtbl { + QueryInterface: Self::query_interface, + AddRef: Self::add_ref, + Release: Self::release, + }; + + unsafe extern "system" fn query_interface( + this: *mut c_void, + iid: *const GUID, + result: *mut *mut c_void, + ) -> HRESULT { + if result.is_null() { + return SINK_E_POINTER; } - if parameter.typ.abi.is_variant_by_value() - && parameter.direction != ComParameterDirection::In + unsafe { *result = std::ptr::null_mut() }; + if iid.is_null() { + return SINK_E_POINTER; + } + unsafe { Self::query_interface_impl(this.cast(), &*iid, result) } + } + + unsafe extern "system" fn add_ref(this: *mut c_void) -> u32 { + unsafe { &*this.cast::() }.ref_count.add_ref() + } + + unsafe extern "system" fn release(this: *mut c_void) -> u32 { + let sink = unsafe { &*this.cast::() }; + let remaining = sink.ref_count.release(); + if remaining == 0 { + unsafe { drop(Box::from_raw(this.cast::())) }; + } + remaining + } + + unsafe fn query_interface_impl( + owner: *mut Self, + iid: &GUID, + result: *mut *mut c_void, + ) -> HRESULT { + let sink = unsafe { &*owner }; + let pointer = if *iid == IUnknown::IID { + owner.cast() + } else if let Some((_, index)) = sink.iid_map.iter().find(|(candidate, _)| candidate == iid) { - return Err(invalid_argument( - "by-value VARIANT is input-only; pointer output and InOut contracts remain unsupported", - )); + (&*sink.interfaces[*index] as *const DynamicComInterfaceView) + .cast_mut() + .cast() + } else { + return SINK_E_NOINTERFACE; + }; + unsafe { *result = pointer }; + sink.ref_count.add_ref(); + HRESULT(0) + } + + unsafe fn view_from_ptr(this: *mut c_void) -> &'static DynamicComInterfaceView { + unsafe { &*this.cast::() } + } + + unsafe fn owner_from_view(this: *mut c_void) -> &'static Self { + unsafe { &*Self::view_from_ptr(this).owner } + } + + unsafe extern "system" fn view_query_interface( + this: *mut c_void, + iid: *const GUID, + result: *mut *mut c_void, + ) -> HRESULT { + if result.is_null() { + return SINK_E_POINTER; + } + unsafe { *result = std::ptr::null_mut() }; + if iid.is_null() { + return SINK_E_POINTER; } + let owner = unsafe { Self::view_from_ptr(this).owner }; + unsafe { Self::query_interface_impl(owner, &*iid, result) } } - Ok(()) -} -fn validate_buffer_contracts(parameters: &[ComParameterSpec]) -> result::Result<()> { - let mut related = vec![Vec::new(); parameters.len()]; - for (buffer_index, parameter) in parameters.iter().enumerate() { - let Some(contract) = ¶meter.buffer else { - continue; - }; - if contract.element.cleanup != BufferElementCleanup::None { - let supported = matches!( - (¶meter.direction, &contract.relation), - ( - ComParameterDirection::InputBuffer, - ComBufferRelation::Input { - unit: BufferCountUnit::Elements, - .. - } - ) | ( - ComParameterDirection::CallerOutputBuffer, - ComBufferRelation::CallerCapacity { - unit: BufferCountUnit::Elements, - two_call: false, - .. - } - ) | ( - ComParameterDirection::CallerOutputBuffer, - ComBufferRelation::EnumeratorNext { .. } - ) + unsafe extern "system" fn view_add_ref(this: *mut c_void) -> u32 { + unsafe { Self::owner_from_view(this) }.ref_count.add_ref() + } + + unsafe extern "system" fn view_release(this: *mut c_void) -> u32 { + let owner = unsafe { Self::view_from_ptr(this).owner }; + unsafe { Self::release(owner.cast()) } + } + + unsafe fn borrowed_interface(value: *mut c_void) -> Value { + if value.is_null() { + Value::WinRt(WinRTValue::Null) + } else { + Value::WinRt(WinRTValue::Object( + unsafe { IUnknown::from_raw_borrowed(&value) } + .expect("non-null COM callback interface") + .clone(), + )) + } + } + + unsafe fn dispatch_interface_in1( + this: *mut c_void, + method_index: usize, + value: *mut c_void, + ) -> HRESULT { + catch_unwind(AssertUnwindSafe(|| { + let view = unsafe { Self::view_from_ptr(this) }; + let sink = unsafe { &*view.owner }; + let callback = sink.callback.clone(); + let values = [unsafe { Self::borrowed_interface(value) }]; + let result = callback( + sink.iids[view.interface_index], + method_index + 3, + &values, + CallbackContract::hresult(0), ); - if !supported { - return Err(invalid_argument( - "owned COM buffer elements require an authoritative initialized range and per-element cleanup", - )); + if result.return_value.is_some() || !result.outputs.is_empty() { + SINK_E_FAIL + } else { + result.hresult } - } - if relation_unit(&contract.relation) == BufferCountUnit::Bytes && contract.element.size != 1 - { - return Err(invalid_argument( - "byte-counted COM buffers require one-byte elements", - )); - } - if matches!(contract.element.kind, BufferElementKind::StringPointer(_)) + })) + .unwrap_or(SINK_E_FAIL) + } + + unsafe fn dispatch_interface_in2( + this: *mut c_void, + method_index: usize, + first: *mut c_void, + second: *mut c_void, + ) -> HRESULT { + catch_unwind(AssertUnwindSafe(|| { + let view = unsafe { Self::view_from_ptr(this) }; + let sink = unsafe { &*view.owner }; + let callback = sink.callback.clone(); + let values = [unsafe { Self::borrowed_interface(first) }, unsafe { + Self::borrowed_interface(second) + }]; + let result = callback( + sink.iids[view.interface_index], + method_index + 3, + &values, + CallbackContract::hresult(0), + ); + if result.return_value.is_some() || !result.outputs.is_empty() { + SINK_E_FAIL + } else { + result.hresult + } + })) + .unwrap_or(SINK_E_FAIL) + } + + unsafe fn dispatch_interface_in2_out_i32( + this: *mut c_void, + method_index: usize, + first: *mut c_void, + second: *mut c_void, + result: *mut i32, + ) -> HRESULT { + if result.is_null() { + return SINK_E_POINTER; + } + unsafe { *result = 0 }; + catch_unwind(AssertUnwindSafe(|| { + let view = unsafe { Self::view_from_ptr(this) }; + let sink = unsafe { &*view.owner }; + let callback = sink.callback.clone(); + let values = [unsafe { Self::borrowed_interface(first) }, unsafe { + Self::borrowed_interface(second) + }]; + let callback_result = callback( + sink.iids[view.interface_index], + method_index + 3, + &values, + CallbackContract::hresult(1), + ); + if callback_result.return_value.is_some() { + return SINK_E_FAIL; + } + if callback_result.hresult.is_ok() { + let [Value::WinRt(WinRTValue::I32(value))] = callback_result.outputs.as_slice() + else { + return SINK_E_FAIL; + }; + unsafe { *result = *value }; + } + callback_result.hresult + })) + .unwrap_or(SINK_E_FAIL) + } + + unsafe fn dispatch_libffi( + slot: usize, + signature: &crate::native_callback::CallbackSignature, + args: *const *const c_void, + result: *mut c_void, + ) { + if result.is_null() && !matches!( - (¶meter.direction, &contract.relation), - ( - ComParameterDirection::InputBuffer, - ComBufferRelation::Input { - actual_length_param: None, - unit: BufferCountUnit::Elements, - .. - } - ) + signature.return_abi(), + crate::native_callback::CallbackReturnAbi::Void ) { - return Err(invalid_argument( - "COM string pointer arrays must be borrowed, element-counted inputs", - )); - } - if matches!( - contract.element.kind, - BufferElementKind::CoTaskMemWideString - ) && !matches!( - (¶meter.direction, &contract.relation), - ( - ComParameterDirection::CallerOutputBuffer, - ComBufferRelation::EnumeratorNext { .. } - ) - ) { - return Err(invalid_argument( - "CoTaskMem string array elements require the exact EnumeratorNext contract", - )); + return; } - let mut contract_indices = BTreeSet::new(); - for index in contract.relation.related_params() { - if !contract_indices.insert(index) { - continue; + unsafe { signature.initialize_failure_result(result, SINK_E_FAIL) }; + let invocation = catch_unwind(AssertUnwindSafe(|| -> Result<(), HRESULT> { + if args.is_null() { + return Err(SINK_E_POINTER); } - if index >= parameters.len() || index == buffer_index { - return Err(invalid_argument( - "COM buffer count relationship references an invalid parameter index", - )); + let this = unsafe { *(*args).cast::<*mut c_void>() }; + if this.is_null() { + return Err(SINK_E_POINTER); } - if !related[index].contains(&buffer_index) { - related[index].push(buffer_index); + let (callback, plan, iid) = { + let view = unsafe { Self::view_from_ptr(this) }; + let sink = unsafe { &*view.owner }; + let Some(plan) = sink + .callback_plans + .get(view.interface_index) + .and_then(|plans| plans.get(slot - 3)) + else { + return Err(SINK_E_FAIL); + }; + ( + sink.callback.clone(), + plan.clone(), + sink.iids[view.interface_index], + ) + }; + if signature.parameters().len() != plan.parameters.len() { + return Err(SINK_E_FAIL); } - validate_count_type(¶meters[index].typ)?; - } - match (¶meter.direction, &contract.relation) { - ( - ComParameterDirection::InputBuffer, - ComBufferRelation::Input { - count_param, - actual_length_param, - .. - }, - ) => { - require_direction(parameters, *count_param, &[ComParameterDirection::In])?; - if let Some(actual) = actual_length_param { - require_direction(parameters, *actual, &[ComParameterDirection::Out])?; - } + let prepared = unsafe { plan.prepare_callback_outputs(args)? }; + let values = unsafe { plan.callback_inputs(args)? }; + let callback_result = callback( + iid, + slot, + &values, + plan.callback_contract(prepared.output_count), + ); + if callback_result.hresult.is_err() { + return Err(callback_result.hresult); } - ( - ComParameterDirection::CallerOutputBuffer, - ComBufferRelation::CallerCapacity { - capacity_param, - actual_length_param, - two_call, - .. - }, - ) => { - if actual_length_param == &Some(*capacity_param) { - require_direction( - parameters, - *capacity_param, - &[ComParameterDirection::InOut], - )?; - } else { - require_direction(parameters, *capacity_param, &[ComParameterDirection::In])?; - if let Some(actual) = actual_length_param { - require_direction(parameters, *actual, &[ComParameterDirection::Out])?; - } + let prepared_return = match &plan.return_plan { + ComReturnPlan::HResult | ComReturnPlan::SemanticHResult | ComReturnPlan::Void + if callback_result.return_value.is_some() => + { + return Err(SINK_E_FAIL); } - if *two_call && actual_length_param.is_none() { - return Err(invalid_argument( - "two-call COM buffer sizing requires an actual-length output", - )); + ComReturnPlan::Direct(typ) => { + let value = callback_result.return_value.as_ref().ok_or(SINK_E_FAIL)?; + Some(CallbackMethodPlan::prepare_native_callback_output( + typ, false, value, + )?) } - } - ( - ComParameterDirection::CallerOutputBuffer, - ComBufferRelation::EnumeratorNext { - capacity_param, - fetched_param, + ComReturnPlan::EnumeratorNextHResult | ComReturnPlan::DispatchInvokeHResult(_) => { + return Err(SINK_E_FAIL); + } + _ => None, + }; + let prepared_writes = + unsafe { plan.prepare_callback_writes(args, &callback_result.outputs, &prepared)? }; + unsafe { prepared_writes.commit() }; + match &plan.return_plan { + ComReturnPlan::HResult | ComReturnPlan::SemanticHResult => unsafe { + result.cast::().write(callback_result.hresult.0); }, - ) => { - require_direction(parameters, *capacity_param, &[ComParameterDirection::In])?; - require_direction( - parameters, - *fetched_param, - &[ - ComParameterDirection::Out, - ComParameterDirection::OptionalOut, - ], - )?; - validate_u32_count_type(¶meters[*capacity_param].typ)?; - validate_u32_count_type(¶meters[*fetched_param].typ)?; - } - ( - ComParameterDirection::CalleeAllocatedBuffer, - ComBufferRelation::CalleeAllocated { count_param, .. }, - ) => { - require_direction(parameters, *count_param, &[ComParameterDirection::Out])?; - } - _ => { - return Err(invalid_argument( - "COM buffer direction and count relationship do not agree", - )); + ComReturnPlan::Void => {} + ComReturnPlan::Direct(_) => unsafe { + prepared_return + .expect("direct callback result was prepared") + .commit(result); + }, + ComReturnPlan::EnumeratorNextHResult | ComReturnPlan::DispatchInvokeHResult(_) => { + unreachable!() + } } + Ok(()) + })); + if let Err(error) = invocation.unwrap_or(Err(SINK_E_FAIL)) { + unsafe { signature.initialize_failure_result(result, error) }; } } - for (count_index, buffers) in related.iter().enumerate() { - if buffers.len() > 1 { - validate_shared_count_group(parameters, count_index, buffers)?; +} + +pub fn create_sink(interface: &Interface, callback: SinkCallback) -> result::Result { + let identity = create_object(std::slice::from_ref(interface), callback)?; + let mut view = std::ptr::null_mut(); + unsafe { identity.query(&interface.iid(), &mut view) } + .ok() + .map_err(result::Error::WindowsError)?; + if view.is_null() { + return Err(invalid_argument( + "COM object did not expose its registered sink interface", + )); + } + Ok(unsafe { IUnknown::from_raw(view) }) +} + +pub fn create_object(interfaces: &[Interface], callback: SinkCallback) -> result::Result { + let interfaces = interfaces + .iter() + .map(Interface::callback_backends) + .collect::>>()?; + DynamicComSink::create(interfaces, callback) +} + +fn invalid_argument(message: impl Into) -> result::Error { + let message = message.into(); + result::Error::WindowsError(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &message, + )) +} + +#[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) })) +} + +pub fn co_get_class_object(clsid: GUID, iid: GUID) -> result::Result { + let unknown: IUnknown = unsafe { CoGetClassObject(&clsid, CLSCTX_INPROC_SERVER, None) } + .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 co_get_malloc() -> result::Result { + let allocator = unsafe { CoGetMalloc(1) }.map_err(result::Error::WindowsError)?; + Ok(WinRTValue::Object(allocator.into())) +} + +pub fn create_error_info() -> result::Result { + windows_link::link!("oleaut32.dll" "system" fn CreateErrorInfo( + error_info: *mut *mut c_void + ) -> windows_core::HRESULT); + + let mut error_info = std::ptr::null_mut(); + unsafe { CreateErrorInfo(&mut error_info) } + .ok() + .map_err(result::Error::WindowsError)?; + if error_info.is_null() { + return Err(invalid_argument( + "CreateErrorInfo succeeded without returning an interface", + )); } - Ok(()) + Ok(unsafe { adopt_com_pointer(error_info) }) } -fn validate_shared_count_group( - parameters: &[ComParameterSpec], - count_index: usize, - buffers: &[usize], -) -> result::Result<()> { - let shared_input_units = buffers - .iter() - .map(|&buffer_index| { - let parameter = ¶meters[buffer_index]; - let contract = parameter.buffer.as_ref().expect("validated buffer"); - match (¶meter.direction, &contract.relation) { - ( - ComParameterDirection::InputBuffer, - ComBufferRelation::Input { - count_param, - actual_length_param: None, - unit, - }, - ) if *count_param == count_index => Some(*unit), - _ => None, +pub fn set_error_info(value: Option<&WinRTValue>) -> result::Result<()> { + windows_link::link!("oleaut32.dll" "system" fn SetErrorInfo( + reserved: u32, + error_info: *mut c_void + ) -> windows_core::HRESULT); + + let error_info = value + .map(|value| { + let unknown = value + .as_object() + .ok_or_else(|| invalid_argument("SetErrorInfo requires a COM object"))?; + let mut error_info = std::ptr::null_mut(); + unsafe { + unknown.query( + &GUID::from_u128(0x1cf2b120_547d_101b_8e65_08002b2bd119), + &mut error_info, + ) } + .ok() + .map_err(result::Error::WindowsError)?; + Ok::(unsafe { IUnknown::from_raw(error_info) }) }) - .collect::>>(); - if shared_input_units.is_some_and(|units| { - units - .first() - .is_some_and(|first| units.iter().all(|unit| unit == first)) - }) { - return Ok(()); + .transpose()?; + unsafe { + SetErrorInfo( + 0, + error_info + .as_ref() + .map_or(std::ptr::null_mut(), |value| value.as_raw()), + ) } - let mut parallel_inputs = 0usize; - let mut parallel_outputs = 0usize; - let parallel = buffers.iter().all(|&buffer_index| { - let parameter = ¶meters[buffer_index]; - let contract = parameter.buffer.as_ref().expect("validated buffer"); - match (¶meter.direction, &contract.relation) { - ( - ComParameterDirection::InputBuffer, - ComBufferRelation::Input { - count_param, - actual_length_param: None, - unit: BufferCountUnit::Elements, - }, - ) if *count_param == count_index => { - parallel_inputs += 1; - true - } - ( - ComParameterDirection::CallerOutputBuffer, - ComBufferRelation::CallerCapacity { - capacity_param, - actual_length_param: None, - unit: BufferCountUnit::Elements, - two_call: false, - }, - ) if *capacity_param == count_index => { - parallel_outputs += 1; - true - } - _ => false, - } - }); - if parallel && parallel_inputs != 0 && parallel_outputs != 0 { - return Ok(()); + .ok() + .map_err(result::Error::WindowsError) +} + +pub fn get_error_info() -> result::Result> { + windows_link::link!("oleaut32.dll" "system" fn GetErrorInfo( + reserved: u32, + error_info: *mut *mut c_void + ) -> windows_core::HRESULT); + + let mut error_info = std::ptr::null_mut(); + let status = unsafe { GetErrorInfo(0, &mut error_info) }; + if status == windows_core::HRESULT(1) { + return Ok(None); } - if buffers.len() != 2 { + status.ok().map_err(result::Error::WindowsError)?; + if error_info.is_null() { return Err(invalid_argument( - "shared COM counts require exactly one string input array and one caller output array", + "GetErrorInfo succeeded without returning an interface", )); } - let mut string_input = false; - let mut caller_output = false; - for &buffer_index in buffers { - let parameter = ¶meters[buffer_index]; - let contract = parameter.buffer.as_ref().expect("validated buffer"); - match ( - ¶meter.direction, - &contract.element.kind, - &contract.relation, - ) { - ( - ComParameterDirection::InputBuffer, - BufferElementKind::StringPointer(_), - ComBufferRelation::Input { - count_param, - actual_length_param: None, - unit: BufferCountUnit::Elements, - }, - ) if *count_param == count_index && !string_input => string_input = true, - ( - ComParameterDirection::CallerOutputBuffer, - BufferElementKind::Plain, - ComBufferRelation::CallerCapacity { - capacity_param, - actual_length_param: None, - unit: BufferCountUnit::Elements, - two_call: false, - }, - ) if *capacity_param == count_index && !caller_output => caller_output = true, - ( - ComParameterDirection::CallerOutputBuffer, - _, - ComBufferRelation::EnumeratorNext { .. }, - ) => { - return Err(invalid_argument( - "enumerator counts cannot be shared with unrelated COM buffers", - )); - } - _ => { - return Err(invalid_argument( - "unrelated COM buffers cannot share one count parameter", - )); - } - } - } - if string_input && caller_output { - Ok(()) + Ok(Some(unsafe { adopt_com_pointer(error_info) })) +} + +/// 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 { - Err(invalid_argument( - "shared COM counts require one string input array and one caller output array", - )) + WinRTValue::Object(unsafe { IUnknown::from_raw(ptr) }) } } -fn validate_count_type(typ: &Type) -> result::Result<()> { - let ParameterType::WinRT(typ) = &typ.abi else { - return Err(invalid_argument( - "COM buffer count parameters require integer scalar ABI types", - )); - }; - if matches!( - typ.kind(), - TypeKind::I8 - | TypeKind::U8 - | TypeKind::I16 - | TypeKind::U16 - | TypeKind::I32 - | TypeKind::U32 - | TypeKind::I64 - | TypeKind::U64 - ) { - Ok(()) - } else { - Err(invalid_argument( - "COM buffer count parameters require an integer scalar ABI type", - )) - } +/// Project a managed COM object as a typed WinRT async operation. +/// +/// The input remains owned by the caller. The returned `Async` value holds a +/// separate `IAsyncInfo` reference and can be awaited independently. +pub fn project_winrt_async( + value: &WinRTValue, + async_type: TypeHandle, +) -> result::Result { + let async_type = async_type.normalized_async_type()?; + let object = value + .as_object() + .ok_or_else(|| invalid_argument("project_winrt_async requires a COM object"))?; + let iid = async_type + .iid() + .ok_or_else(|| invalid_argument("project_winrt_async requires a closed async IID"))?; + let mut concrete_ptr = std::ptr::null_mut(); + unsafe { object.query(&iid, &mut concrete_ptr) } + .ok() + .map_err(result::Error::WindowsError)?; + let concrete = unsafe { IUnknown::from_raw(concrete_ptr) }; + let info: windows_future::IAsyncInfo = concrete.cast().map_err(result::Error::WindowsError)?; + Ok(WinRTValue::Async(crate::value::AsyncInfo { + info, + async_type, + })) +} + +#[cfg(test)] +fn call_method( + vtable_index: usize, + obj: *mut c_void, + signature: MethodSignature, + args: &[WinRTValue], +) -> result::Result> { + signature.build(vtable_index)?.plan.invoke(obj, args) +} + +#[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) } -fn validate_u32_count_type(typ: &Type) -> result::Result<()> { - if matches!( - &typ.abi, - ParameterType::WinRT(typ) if matches!(typ.kind(), TypeKind::U32) - ) { - Ok(()) - } else { - Err(invalid_argument( - "IEnum::Next capacity and fetched parameters must use ULONG/u32", - )) - } +#[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) } -fn require_direction( - parameters: &[ComParameterSpec], - index: usize, - allowed: &[ComParameterDirection], -) -> result::Result<()> { - if allowed.contains(¶meters[index].direction) { - Ok(()) - } else { - Err(invalid_argument(format!( - "COM buffer relationship parameter {index} has direction {:?}, expected one of {allowed:?}", - parameters[index].direction - ))) - } +#[cfg(test)] +fn wide_null(text: &str) -> Vec { + text.encode_utf16().chain(std::iter::once(0)).collect() } -#[derive(Debug)] -struct RegisteredMethod { - plan: ComCallPlan, +#[cfg(test)] +fn wide_buffer(characters: usize) -> Vec { + vec![0; characters] } -#[derive(Debug, Clone)] -pub struct Interface { - name: String, - iid: GUID, - base_slot: usize, - methods: Arc)>>>, +#[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]) } -impl Interface { - pub fn name(&self) -> &str { - &self.name +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + MetadataTable, com_helpers::E_NOINTERFACE, ro_get_activation_factory_2, + roapi::query_interface, + }; + use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering}; + use windows::{ + ApplicationModel::DataTransfer::DataTransferManager, + System::Threading::{ThreadPool, WorkItemHandler}, + Win32::{ + System::Com::{CoGetMalloc, CreateBindCtx, IBindCtx, IMalloc, IPersistFile, IStream}, + System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize}, + UI::Shell::{ + FDE_SHAREVIOLATION_RESPONSE, IDataTransferManagerInterop, IFileDialog, + IFileDialogEvents, IShellItem, SHCreateMemStream, + }, + UI::WindowsAndMessaging::{ + CreateWindowExW, DestroyWindow, WINDOW_EX_STYLE, WS_OVERLAPPED, + }, + }, + }; + + const IID_TEST_SINK: GUID = GUID::from_u128(0x7ac2eaa2_97a4_43f0_9b0f_421c2363ef11); + + fn sink_interface(iid: GUID, shapes: &[StaticCallbackShape]) -> Interface { + let table = MetadataTable::new(); + let interface_type = Type::winrt(table.interface(IUnknown::IID)); + let mut interface = register_interface(&table, "Test.ISink", iid, InterfaceBase::IUnknown); + for (index, shape) in shapes.iter().enumerate() { + let signature = match shape { + StaticCallbackShape::InterfaceIn1 => { + MethodSignature::new(&table).add_in(interface_type.clone()) + } + StaticCallbackShape::InterfaceIn2 => MethodSignature::new(&table) + .add_in(interface_type.clone()) + .add_in(interface_type.clone()), + StaticCallbackShape::InterfaceIn2OutI32 => MethodSignature::new(&table) + .add_in(interface_type.clone()) + .add_in(interface_type.clone()) + .add_out(Type::winrt(table.i32_type())), + }; + interface = interface + .add_method_at(index + 3, &format!("Method{index}"), signature) + .unwrap(); + } + interface } - pub fn iid(&self) -> GUID { - self.iid + #[test] + fn dynamic_com_sink_is_send_and_sync() { + fn assert_send_sync() {} + assert_send_sync::(); } - pub fn add_method(self, name: &str, signature: MethodSignature) -> Self { - let vtable_index = self - .methods - .read() - .unwrap() - .last_key_value() - .map_or(self.base_slot, |(slot, _)| slot + 1); - self.add_method_at(vtable_index, name, signature) - .expect("sequential COM method registration must use a free vtable slot") + #[test] + fn dynamic_com_sink_dispatches_exact_shapes_and_preserves_identity() { + let calls = Arc::new(Mutex::new(Vec::new())); + let retained = calls.clone(); + let callback: SinkCallback = Arc::new(move |iid, slot, values, output| { + assert_eq!(iid, IID_TEST_SINK); + assert_eq!( + output, + if slot == 5 { + CallbackContract::hresult(1) + } else { + CallbackContract::hresult(0) + } + ); + retained.lock().unwrap().push(( + slot, + values + .iter() + .map(|value| matches!(value, Value::WinRt(WinRTValue::Object(_)))) + .collect::>(), + )); + match slot { + 3 => SinkCallbackResult::hresult(HRESULT(1)), + 4 => SinkCallbackResult::hresult(HRESULT(0)), + 5 => SinkCallbackResult::with_output(HRESULT(0), Value::WinRt(WinRTValue::I32(7))), + _ => unreachable!(), + } + }); + let interface = sink_interface( + IID_TEST_SINK, + &[ + StaticCallbackShape::InterfaceIn1, + StaticCallbackShape::InterfaceIn2, + StaticCallbackShape::InterfaceIn2OutI32, + ], + ); + let sink = create_sink(&interface, callback).unwrap(); + + let mut queried = std::ptr::null_mut(); + unsafe { sink.query(&IID_TEST_SINK, &mut queried) } + .ok() + .unwrap(); + assert_eq!(queried, sink.as_raw()); + unsafe { drop(IUnknown::from_raw(queried)) }; + + let mut unsupported = 1usize as *mut c_void; + assert_eq!( + unsafe { sink.query(&GUID::zeroed(), &mut unsupported) }, + SINK_E_NOINTERFACE + ); + assert!(unsupported.is_null()); + + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let one: unsafe extern "system" fn(*mut c_void, *mut c_void) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + let two: unsafe extern "system" fn(*mut c_void, *mut c_void, *mut c_void) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(4)) }; + let two_out: unsafe extern "system" fn( + *mut c_void, + *mut c_void, + *mut c_void, + *mut i32, + ) -> HRESULT = unsafe { std::mem::transmute(*vtable.add(5)) }; + + assert_eq!(unsafe { one(sink.as_raw(), sink.as_raw()) }, HRESULT(1)); + assert_eq!( + unsafe { two(sink.as_raw(), sink.as_raw(), std::ptr::null_mut()) }, + HRESULT(0) + ); + let mut output = -1; + assert_eq!( + unsafe { two_out(sink.as_raw(), sink.as_raw(), sink.as_raw(), &mut output,) }, + HRESULT(0) + ); + assert_eq!(output, 7); + assert_eq!( + *calls.lock().unwrap(), + vec![ + (3, vec![true]), + (4, vec![true, false]), + (5, vec![true, true]), + ] + ); } - pub fn add_method_at( - self, - vtable_index: usize, - name: &str, - signature: MethodSignature, - ) -> result::Result { - if vtable_index < self.base_slot { - return Err(invalid_argument(format!( - "COM method '{name}' uses vtable slot {vtable_index}, before the interface base slot {}", - self.base_slot - ))); - } - signature.validate_registration(self.iid, name, vtable_index)?; + #[test] + fn dynamic_com_sink_fails_closed_for_invalid_output_and_panics() { + let interface = sink_interface(IID_TEST_SINK, &[StaticCallbackShape::InterfaceIn2OutI32]); + let missing_output = create_sink( + &interface, + Arc::new(|_, _, _, _| SinkCallbackResult::hresult(HRESULT(0))), + ) + .unwrap(); + let vtable = unsafe { *(missing_output.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn( + *mut c_void, + *mut c_void, + *mut c_void, + *mut i32, + ) -> HRESULT = unsafe { std::mem::transmute(*vtable.add(3)) }; + assert_eq!( + unsafe { + invoke( + missing_output.as_raw(), + std::ptr::null_mut(), + std::ptr::null_mut(), + std::ptr::null_mut(), + ) + }, + SINK_E_POINTER + ); + let mut output = 99; + assert_eq!( + unsafe { + invoke( + missing_output.as_raw(), + std::ptr::null_mut(), + std::ptr::null_mut(), + &mut output, + ) + }, + SINK_E_FAIL + ); + assert_eq!(output, 0); + + let interface = sink_interface(IID_TEST_SINK, &[StaticCallbackShape::InterfaceIn1]); + let panicking = create_sink( + &interface, + Arc::new(|_, _, _, _| panic!("sink callback panic")), + ) + .unwrap(); + let vtable = unsafe { *(panicking.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *mut c_void) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + assert_eq!( + unsafe { invoke(panicking.as_raw(), std::ptr::null_mut()) }, + SINK_E_FAIL + ); + + let empty = sink_interface(IID_TEST_SINK, &[]); + assert!(create_sink(&empty, Arc::new(|_, _, _, _| unreachable!())).is_err()); + let too_many = sink_interface(IID_TEST_SINK, &[StaticCallbackShape::InterfaceIn1; 17]); + let many = create_sink( + &too_many, + Arc::new(|_, slot, values, output| { + assert_eq!(slot, 19); + assert_eq!(values.len(), 1); + assert_eq!(output, CallbackContract::hresult(0)); + SinkCallbackResult::hresult(HRESULT(0)) + }), + ) + .unwrap(); + let vtable = unsafe { *(many.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *mut c_void) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(19)) }; + assert_eq!( + unsafe { invoke(many.as_raw(), std::ptr::null_mut()) }, + HRESULT(0) + ); + + let table = MetadataTable::new(); + let unsupported = register_interface( + &table, + "Test.IUnsupportedSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method( + "Invoke", + MethodSignature::new(&table).add_in(Type::variant_by_value()), + ); + assert!( + create_sink(&unsupported, Arc::new(|_, _, _, _| unreachable!())) + .unwrap_err() + .message() + .contains("callback ABI") + ); + let interface_inout = register_interface( + &table, + "Test.IInterfaceInOutSink", + GUID::from_u128(0x0cd81fa8_b496_4ce5_b073_77e9dce985f1), + InterfaceBase::IUnknown, + ); + assert!( + interface_inout + .add_method_at( + 3, + "Invoke", + MethodSignature::new(&table) + .add_in_out(Type::winrt(table.interface(IUnknown::IID))), + ) + .unwrap_err() + .message() + .contains("replacement and cleanup") + ); + + let non_contiguous = register_interface( + &table, + "Test.INonContiguousSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method_at( + 4, + "Invoke", + MethodSignature::new(&table).add_in(Type::winrt(table.interface(IUnknown::IID))), + ) + .unwrap(); + assert!( + create_sink(&non_contiguous, Arc::new(|_, _, _, _| unreachable!())) + .unwrap_err() + .message() + .contains("non-contiguous") + ); + + let inspectable = register_interface( + &table, + "Test.IInspectableSink", + IID_TEST_SINK, + InterfaceBase::IInspectable, + ) + .add_method( + "Invoke", + MethodSignature::new(&table).add_in(Type::winrt(table.interface(IUnknown::IID))), + ); + assert!( + create_sink(&inspectable, Arc::new(|_, _, _, _| unreachable!())) + .unwrap_err() + .message() + .contains("IUnknown vtable root") + ); + } + + #[test] + fn dynamic_com_sink_matches_windows_rs_ifiledialogevents_abi() { + let calls = Arc::new(Mutex::new(Vec::new())); + let retained = calls.clone(); + let interface = sink_interface( + IFileDialogEvents::IID, + &[ + StaticCallbackShape::InterfaceIn1, + StaticCallbackShape::InterfaceIn2, + StaticCallbackShape::InterfaceIn1, + StaticCallbackShape::InterfaceIn1, + StaticCallbackShape::InterfaceIn2OutI32, + StaticCallbackShape::InterfaceIn1, + StaticCallbackShape::InterfaceIn2OutI32, + ], + ); + let sink = create_sink( + &interface, + Arc::new(move |_, slot, values, output| { + assert_eq!( + output, + if matches!(slot, 7 | 9) { + CallbackContract::hresult(1) + } else { + CallbackContract::hresult(0) + } + ); + retained.lock().unwrap().push(( + slot, + values + .iter() + .map(|value| matches!(value, Value::WinRt(WinRTValue::Object(_)))) + .collect::>(), + )); + if matches!(slot, 7 | 9) { + SinkCallbackResult::with_output(HRESULT(0), Value::WinRt(WinRTValue::I32(2))) + } else { + SinkCallbackResult::hresult(HRESULT(0)) + } + }), + ) + .unwrap(); + let events: IFileDialogEvents = sink.cast().unwrap(); - let mut methods = self.methods.write().unwrap(); - if methods.contains_key(&vtable_index) { - return Err(invalid_argument(format!( - "COM vtable slot {vtable_index} is already registered on '{}'", - self.name - ))); - } - methods.insert( - vtable_index, - (name.to_string(), Arc::new(signature.build(vtable_index)?)), + unsafe { events.OnFileOk(None::<&IFileDialog>) }.unwrap(); + let response = + unsafe { events.OnShareViolation(None::<&IFileDialog>, None::<&IShellItem>) }.unwrap(); + assert_eq!(response, FDE_SHAREVIOLATION_RESPONSE(2)); + assert_eq!( + *calls.lock().unwrap(), + vec![(3, vec![false]), (7, vec![false, false])] ); - drop(methods); - Ok(self) - } - - pub fn method(&self, vtable_index: usize) -> Option { - self.methods - .read() - .unwrap() - .get(&vtable_index) - .map(|(_, method)| MethodHandle(Arc::clone(method))) } -} -#[derive(Clone)] -pub struct MethodHandle(Arc); + #[test] + fn dynamic_com_sink_survives_reentrant_final_release() { + let retained = Arc::new(AtomicUsize::new(0)); + let callback_retained = retained.clone(); + let calls = Arc::new(AtomicU32::new(0)); + let callback_calls = calls.clone(); + let interface = sink_interface(IID_TEST_SINK, &[StaticCallbackShape::InterfaceIn1]); + let sink = create_sink( + &interface, + Arc::new(move |_, _, _, _| { + let raw = callback_retained.swap(0, Ordering::SeqCst); + assert_ne!(raw, 0); + unsafe { drop(IUnknown::from_raw(raw as *mut c_void)) }; + callback_calls.fetch_add(1, Ordering::SeqCst); + SinkCallbackResult::hresult(HRESULT(0)) + }), + ) + .unwrap(); + let raw = sink.into_raw(); + retained.store(raw as usize, Ordering::SeqCst); -impl std::fmt::Debug for MethodHandle { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("MethodHandle").finish_non_exhaustive() + let vtable = unsafe { *(raw as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *mut c_void) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + assert_eq!(unsafe { invoke(raw, std::ptr::null_mut()) }, HRESULT(0)); + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert_eq!(retained.load(Ordering::SeqCst), 0); } -} -impl MethodHandle { - pub fn result_count(&self) -> usize { - self.0.plan.results.len() - } + #[test] + fn dynamic_com_sink_libffi_survives_reentrant_final_release() { + let table = MetadataTable::new(); + let retained = Arc::new(AtomicUsize::new(0)); + let callback_retained = retained.clone(); + let calls = Arc::new(AtomicU32::new(0)); + let callback_calls = calls.clone(); + let interface = register_interface( + &table, + "Test.IReentrantLibffiSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method( + "Invoke", + MethodSignature::new(&table).add_in(Type::winrt(table.i32_type())), + ); + let sink = create_sink( + &interface, + Arc::new(move |_, _, _, _| { + let raw = callback_retained.swap(0, Ordering::SeqCst); + assert_ne!(raw, 0); + unsafe { drop(IUnknown::from_raw(raw as *mut c_void)) }; + callback_calls.fetch_add(1, Ordering::SeqCst); + SinkCallbackResult::hresult(HRESULT(0)) + }), + ) + .unwrap(); + let raw = sink.into_raw(); + retained.store(raw as usize, Ordering::SeqCst); - /// # Safety - /// - /// `obj` must point to a live COM interface whose vtable contains this - /// method at its registered slot for the duration of the call. - pub unsafe fn invoke( - &self, - obj: *mut c_void, - args: &[WinRTValue], - ) -> result::Result> { - self.0.plan.invoke(obj, args) + let vtable = unsafe { *(raw as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, i32) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + assert_eq!(unsafe { invoke(raw, 1) }, HRESULT(0)); + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert_eq!(retained.load(Ordering::SeqCst), 0); } - /// # Safety - /// - /// `obj` must point to a live COM interface whose vtable contains this - /// method at its registered slot for the duration of the call. - pub unsafe fn invoke_with_output_kinds( - &self, - obj: *mut c_void, - args: &[WinRTValue], - ) -> result::Result> { - self.0.plan.invoke_with_output_kinds(obj, args) + #[test] + fn dynamic_com_sink_uses_libffi_for_runtime_i32_signature() { + let table = MetadataTable::new(); + let interface = register_interface( + &table, + "Test.ILibffiSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method( + "Invoke", + MethodSignature::new(&table).add_in(Type::winrt(table.i32_type())), + ); + let calls = Arc::new(Mutex::new(Vec::new())); + let retained = calls.clone(); + let sink = create_sink( + &interface, + Arc::new(move |_, slot, values, output| { + assert_eq!(slot, 3); + assert_eq!(output, CallbackContract::hresult(0)); + let [Value::WinRt(WinRTValue::I32(value))] = values else { + panic!("expected one i32 callback value"); + }; + retained.lock().unwrap().push(*value); + SinkCallbackResult::hresult(HRESULT(*value + 1)) + }), + ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, i32) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + assert_eq!(unsafe { invoke(sink.as_raw(), 41) }, HRESULT(42)); + assert_eq!(*calls.lock().unwrap(), vec![41]); } - /// # Safety - /// - /// `obj` must point to a live COM interface whose vtable contains this - /// method at its registered slot for the duration of the call. - pub unsafe fn invoke_values_with_output_kinds( - &self, - obj: *mut c_void, - args: &[Value], - ) -> result::Result> { - self.0.plan.invoke_values_with_output_kinds(obj, args) + #[test] + fn dynamic_com_sink_libffi_supports_direct_and_void_returns() { + let table = MetadataTable::new(); + let interface = register_interface( + &table, + "Test.ILibffiNativeReturnSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method( + "Direct", + MethodSignature::new(&table) + .add_in(Type::winrt(table.i32_type())) + .returns(Type::winrt(table.i32_type())), + ) + .add_method( + "Void", + MethodSignature::new(&table) + .add_in(Type::winrt(table.u32_type())) + .returns_void(), + ) + .add_method( + "DirectOut", + MethodSignature::new(&table) + .add_out(Type::winrt(table.u32_type())) + .returns(Type::winrt(table.i32_type())), + ) + .add_method( + "VoidOut", + MethodSignature::new(&table) + .add_out(Type::winrt(table.u32_type())) + .returns_void(), + ); + let calls = Arc::new(Mutex::new(Vec::new())); + let retained = calls.clone(); + let sink = create_sink( + &interface, + Arc::new(move |_, slot, values, contract| { + retained.lock().unwrap().push((slot, contract)); + match (slot, values) { + (3, [Value::WinRt(WinRTValue::I32(value))]) => { + SinkCallbackResult::with_return(Value::WinRt(WinRTValue::I32(value + 1))) + } + (4, [Value::WinRt(WinRTValue::U32(42))]) => { + SinkCallbackResult::hresult(HRESULT(0)) + } + (5, []) => SinkCallbackResult::with_return_and_outputs( + Value::WinRt(WinRTValue::I32(7)), + vec![Value::WinRt(WinRTValue::U32(8))], + ), + (6, []) => SinkCallbackResult::with_output( + HRESULT(0), + Value::WinRt(WinRTValue::U32(9)), + ), + _ => panic!("unexpected native-return callback"), + } + }), + ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let direct: unsafe extern "system" fn(*mut c_void, i32) -> i32 = + unsafe { std::mem::transmute(*vtable.add(3)) }; + let void: unsafe extern "system" fn(*mut c_void, u32) = + unsafe { std::mem::transmute(*vtable.add(4)) }; + let direct_out: unsafe extern "system" fn(*mut c_void, *mut u32) -> i32 = + unsafe { std::mem::transmute(*vtable.add(5)) }; + let void_out: unsafe extern "system" fn(*mut c_void, *mut u32) = + unsafe { std::mem::transmute(*vtable.add(6)) }; + + assert_eq!(unsafe { direct(sink.as_raw(), 41) }, 42); + unsafe { void(sink.as_raw(), 42) }; + let mut direct_output = u32::MAX; + assert_eq!(unsafe { direct_out(sink.as_raw(), &mut direct_output) }, 7); + assert_eq!(direct_output, 8); + let mut void_output = u32::MAX; + unsafe { void_out(sink.as_raw(), &mut void_output) }; + assert_eq!(void_output, 9); + assert_eq!( + *calls.lock().unwrap(), + vec![ + (3, CallbackContract::direct(0)), + (4, CallbackContract::void(0)), + (5, CallbackContract::direct(1)), + (6, CallbackContract::void(1)), + ] + ); } - /// # Safety - /// - /// `obj` must point to a live IDispatch interface whose vtable contains - /// Invoke at slot 6 for the duration of the call. - pub unsafe fn invoke_dispatch( - &self, - obj: *mut c_void, - args: &[Value], - ) -> result::Result { - self.0.plan.invoke_dispatch(obj, args) + #[test] + fn dynamic_com_sink_libffi_zeroes_failed_direct_returns() { + let table = MetadataTable::new(); + let interface = register_interface( + &table, + "Test.ILibffiFailedDirectSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method( + "Direct", + MethodSignature::new(&table).returns(Type::winrt(table.i64_type())), + ); + let sink = create_sink( + &interface, + Arc::new(|_, _, _, _| SinkCallbackResult::hresult(SINK_E_FAIL)), + ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let direct: unsafe extern "system" fn(*mut c_void) -> i64 = + unsafe { std::mem::transmute(*vtable.add(3)) }; + assert_eq!(unsafe { direct(sink.as_raw()) }, 0); } - /// # Safety - /// - /// `obj` must point to a live COM interface whose vtable contains this - /// HSTRING getter at its registered slot for the duration of the call. - pub unsafe fn call_getter_hstring( - &self, - obj: *mut c_void, - ) -> result::Result { - self.0 - .plan - .native - .call_getter_hstring(obj) - .map_err(result::Error::WindowsError) + #[test] + fn dynamic_com_sink_libffi_returns_every_scalar_width() { + let table = MetadataTable::new(); + let types = [ + Type::winrt(table.i8_type()), + Type::winrt(table.u8_type()), + Type::winrt(table.i16_type()), + Type::winrt(table.u16_type()), + Type::winrt(table.i32_type()), + Type::winrt(table.u32_type()), + Type::winrt(table.i64_type()), + Type::winrt(table.u64_type()), + Type::winrt(table.f32_type()), + Type::winrt(table.f64_type()), + ]; + let mut interface = register_interface( + &table, + "Test.ILibffiScalarReturnSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ); + for (index, typ) in types.into_iter().enumerate() { + interface = interface.add_method( + &format!("Get{index}"), + MethodSignature::new(&table).returns(typ), + ); + } + let sink = create_sink( + &interface, + Arc::new(|_, slot, _, contract| { + assert_eq!(contract, CallbackContract::direct(0)); + SinkCallbackResult::with_return(Value::WinRt(match slot { + 3 => WinRTValue::I8(-8), + 4 => WinRTValue::U8(250), + 5 => WinRTValue::I16(-1600), + 6 => WinRTValue::U16(65000), + 7 => WinRTValue::I32(-32000), + 8 => WinRTValue::U32(4_000_000_000), + 9 => WinRTValue::I64(-9_000_000_000), + 10 => WinRTValue::U64(18_000_000_000), + 11 => WinRTValue::F32(1.5), + 12 => WinRTValue::F64(-2.25), + _ => unreachable!(), + })) + }), + ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + macro_rules! invoke { + ($slot:literal, $return_type:ty) => {{ + let method: unsafe extern "system" fn(*mut c_void) -> $return_type = + unsafe { std::mem::transmute(*vtable.add($slot)) }; + unsafe { method(sink.as_raw()) } + }}; + } + assert_eq!(invoke!(3, i8), -8); + assert_eq!(invoke!(4, u8), 250); + assert_eq!(invoke!(5, i16), -1600); + assert_eq!(invoke!(6, u16), 65000); + assert_eq!(invoke!(7, i32), -32000); + assert_eq!(invoke!(8, u32), 4_000_000_000); + assert_eq!(invoke!(9, i64), -9_000_000_000); + assert_eq!(invoke!(10, u64), 18_000_000_000); + assert_eq!(invoke!(11, f32), 1.5); + assert_eq!(invoke!(12, f64), -2.25); } -} -pub fn register_interface( - _table: &std::sync::Arc, - name: &str, - iid: GUID, - base: InterfaceBase, -) -> Interface { - Interface { - name: name.to_string(), - iid, - base_slot: base.first_method_slot(), - methods: Arc::new(RwLock::new(BTreeMap::new())), + #[test] + fn dynamic_com_sink_libffi_round_trips_scalars_guid_and_multiple_outputs() { + let table = MetadataTable::new(); + let interface = register_interface( + &table, + "Test.ILibffiScalarSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method( + "Invoke", + MethodSignature::new(&table) + .add_in(Type::winrt(table.i8_type())) + .add_in(Type::winrt(table.u8_type())) + .add_in(Type::winrt(table.bool_type())) + .add_in(Type::winrt(table.i16_type())) + .add_in(Type::winrt(table.u16_type())) + .add_in(Type::winrt(table.i32_type())) + .add_in(Type::winrt(table.u32_type())) + .add_in(Type::winrt(table.i64_type())) + .add_in(Type::winrt(table.u64_type())) + .add_in(Type::winrt(table.f32_type())) + .add_in(Type::winrt(table.f64_type())) + .add_in(Type::winrt(table.guid_type())) + .add_out(Type::winrt(table.u64_type())) + .add_out(Type::winrt(table.guid_type())), + ); + let guid = GUID::from_u128(0x991ea999_c420_4a2d_a13c_ab577d0f5f79); + let output_guid = GUID::from_u128(0xc63ad19c_6528_47f7_8995_d5f1508d157a); + let sink = create_sink( + &interface, + Arc::new(move |_, slot, values, output| { + assert_eq!(slot, 3); + assert_eq!(output, CallbackContract::hresult(2)); + assert!(matches!(values[0], Value::WinRt(WinRTValue::I8(-8)))); + assert!(matches!(values[1], Value::WinRt(WinRTValue::U8(250)))); + assert!(matches!(values[2], Value::WinRt(WinRTValue::Bool(true)))); + assert!(matches!(values[3], Value::WinRt(WinRTValue::I16(-1600)))); + assert!(matches!(values[4], Value::WinRt(WinRTValue::U16(65000)))); + assert!(matches!(values[5], Value::WinRt(WinRTValue::I32(-32000)))); + assert!(matches!( + values[6], + Value::WinRt(WinRTValue::U32(4_000_000_000)) + )); + assert!(matches!(values[7], Value::WinRt(WinRTValue::I64(-64_000)))); + assert!(matches!( + values[8], + Value::WinRt(WinRTValue::U64(18_000_000_000)) + )); + assert!(matches!(values[9], Value::WinRt(WinRTValue::F32(value)) if value == 1.25)); + assert!(matches!(values[10], Value::WinRt(WinRTValue::F64(value)) if value == 2.5)); + assert!(matches!( + values[11], + Value::WinRt(WinRTValue::Guid(value)) if value == guid + )); + SinkCallbackResult::with_outputs( + HRESULT(0), + vec![ + Value::WinRt(WinRTValue::U64(99)), + Value::WinRt(WinRTValue::Guid(output_guid)), + ], + ) + }), + ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn( + *mut c_void, + i8, + u8, + u8, + i16, + u16, + i32, + u32, + i64, + u64, + f32, + f64, + GUID, + *mut u64, + *mut GUID, + ) -> HRESULT = unsafe { std::mem::transmute(*vtable.add(3)) }; + let mut output_u64 = 0; + let mut actual_guid = GUID::zeroed(); + assert_eq!( + unsafe { + invoke( + sink.as_raw(), + -8, + 250, + 1, + -1600, + 65000, + -32000, + 4_000_000_000, + -64_000, + 18_000_000_000, + 1.25, + 2.5, + guid, + &mut output_u64, + &mut actual_guid, + ) + }, + HRESULT(0) + ); + assert_eq!(output_u64, 99); + assert_eq!(actual_guid, output_guid); } -} - -fn invalid_argument(message: impl Into) -> result::Error { - let message = message.into(); - result::Error::WindowsError(windows_core::Error::new( - windows_core::HRESULT(0x80070057u32 as i32), - &message, - )) -} -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ApartmentType { - SingleThreaded, - MultiThreaded, -} + #[test] + fn dynamic_com_sink_libffi_round_trips_bstr_and_native_pod() { + let table = MetadataTable::new(); + let layout = Arc::new( + NativeStructLayout::new( + "Test.Pair", + 8, + 4, + vec![ + NativeStructField::new( + "first", + 0, + 1, + NativeStructFieldType::Scalar(NativeStructScalar::I32), + ) + .unwrap(), + NativeStructField::new( + "second", + 4, + 1, + NativeStructFieldType::Scalar(NativeStructScalar::I32), + ) + .unwrap(), + ], + ) + .unwrap(), + ); + let interface = register_interface( + &table, + "Test.ILibffiCompoundSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method( + "Invoke", + MethodSignature::new(&table) + .add_in(Type::bstr()) + .add_in(Type::native_struct(layout.clone())) + .add_out(Type::bstr()) + .add_out(Type::native_struct(layout.clone())), + ); + let output_layout = layout.clone(); + let sink = create_sink( + &interface, + Arc::new(move |_, _, values, output| { + assert_eq!(output, CallbackContract::hresult(2)); + let [Value::Bstr(text), Value::NativeStruct(pair)] = values else { + panic!("expected BSTR and native struct inputs"); + }; + assert_eq!(text.as_deref(), Some("embedded\0nul")); + assert_eq!(pair.bytes(), &[10, 0, 0, 0, 20, 0, 0, 0]); + SinkCallbackResult::with_outputs( + HRESULT(0), + vec![ + Value::Bstr(BstrValue::new("returned\0value")), + Value::NativeStruct( + NativeStructValue::new( + output_layout.clone(), + vec![30, 0, 0, 0, 40, 0, 0, 0], + ) + .unwrap(), + ), + ], + ) + }), + ) + .unwrap(); -impl ApartmentType { - fn as_flag(self) -> windows::Win32::System::Com::COINIT { - match self { - Self::SingleThreaded => COINIT_APARTMENTTHREADED, - Self::MultiThreaded => COINIT_MULTITHREADED, + #[repr(C)] + #[derive(Clone, Copy)] + struct Pair { + first: i32, + second: i32, } - } -} - -struct ComApartment { - apartment_type: ApartmentType, -} -impl Drop for ComApartment { - fn drop(&mut self) { - unsafe { CoUninitialize() }; + let input_text = windows_core::BSTR::from("embedded\0nul"); + let input_pair = Pair { + first: 10, + second: 20, + }; + let mut output_text = std::ptr::null(); + let mut output_pair = Pair { + first: 0, + second: 0, + }; + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn( + *mut c_void, + *const u16, + Pair, + *mut *const u16, + *mut Pair, + ) -> HRESULT = unsafe { std::mem::transmute(*vtable.add(3)) }; + assert_eq!( + unsafe { + invoke( + sink.as_raw(), + input_text.as_ptr(), + input_pair, + &mut output_text, + &mut output_pair, + ) + }, + HRESULT(0) + ); + let output_text = unsafe { windows_core::BSTR::from_raw(output_text) }; + assert_eq!(String::try_from(&output_text).unwrap(), "returned\0value"); + assert_eq!((output_pair.first, output_pair.second), (30, 40)); + + let mut rejected_text = std::ptr::dangling(); + let mut rejected_pair = Pair { + first: -1, + second: -1, + }; + assert_eq!( + unsafe { + invoke( + sink.as_raw(), + std::ptr::null(), + input_pair, + &mut rejected_text, + &mut rejected_pair, + ) + }, + SINK_E_POINTER + ); + assert!(rejected_text.is_null()); + assert_eq!((rejected_pair.first, rejected_pair.second), (0, 0)); } -} - -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), - )) + #[test] + fn dynamic_com_sink_libffi_round_trips_hstring() { + let table = MetadataTable::new(); + let interface = register_interface( + &table, + "Test.ILibffiHStringSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method( + "Invoke", + MethodSignature::new(&table) + .add_in(Type::winrt(table.hstring())) + .add_out(Type::winrt(table.hstring())), + ); + let method = interface.method(3).unwrap(); + let sink = create_sink( + &interface, + Arc::new(move |_, _, values, output| { + assert_eq!(output, CallbackContract::hresult(1)); + let [Value::WinRt(WinRTValue::HString(value))] = values else { + panic!("expected HSTRING callback input"); + }; + assert_eq!(value, "input"); + SinkCallbackResult::with_output( + HRESULT(0), + Value::WinRt(WinRTValue::HString(windows_core::HSTRING::from("output"))), + ) + }), + ) + .unwrap(); + let result = unsafe { + method.invoke( + sink.as_raw(), + &[WinRTValue::HString(windows_core::HSTRING::from("input"))], + ) } - }) -} - -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) })) -} - -pub fn co_get_class_object(clsid: GUID, iid: GUID) -> result::Result { - let unknown: IUnknown = unsafe { CoGetClassObject(&clsid, CLSCTX_INPROC_SERVER, None) } - .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) })) -} + .unwrap(); + assert!(matches!( + result.as_slice(), + [WinRTValue::HString(value)] if value == "output" + )); + } -pub fn co_get_malloc() -> result::Result { - let allocator = unsafe { CoGetMalloc(1) }.map_err(result::Error::WindowsError)?; - Ok(WinRTValue::Object(allocator.into())) -} + #[test] + fn dynamic_com_sink_libffi_copies_counted_input_buffer() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table) + .add_input_buffer( + Type::winrt(table.u8_type()), + 1, + None, + BufferCountUnit::Elements, + ) + .unwrap() + .add_in(Type::winrt(table.u32_type())); + let interface = register_interface( + &table, + "Test.ILibffiBufferSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method("Invoke", signature); + let sink = create_sink( + &interface, + Arc::new(move |_, _, values, output| { + assert_eq!(output, CallbackContract::hresult(0)); + let [Value::Buffer(buffer)] = values else { + panic!("expected one counted buffer input"); + }; + assert_eq!(buffer.snapshot_bytes().unwrap().unwrap(), vec![1, 2, 3, 4]); + assert_eq!(buffer.count(), 4); + SinkCallbackResult::hresult(HRESULT(0)) + }), + ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *const u8, u32) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + let bytes = [1u8, 2, 3, 4]; + assert_eq!( + unsafe { invoke(sink.as_raw(), bytes.as_ptr(), bytes.len() as u32) }, + HRESULT(0) + ); + } -pub fn create_error_info() -> result::Result { - windows_link::link!("oleaut32.dll" "system" fn CreateErrorInfo( - error_info: *mut *mut c_void - ) -> windows_core::HRESULT); + #[test] + fn dynamic_com_sink_libffi_fills_caller_output_buffer() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table) + .add_caller_output_buffer( + Type::winrt(table.u8_type()), + 1, + Some(2), + BufferCountUnit::Elements, + false, + ) + .unwrap() + .add_in(Type::winrt(table.u32_type())) + .add_out(Type::winrt(table.u32_type())); + let interface = register_interface( + &table, + "Test.ILibffiOutputBufferSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method("Invoke", signature); + let sink = create_sink( + &interface, + Arc::new(move |_, _, values, output| { + assert!(matches!(values, [Value::WinRt(WinRTValue::U32(5))])); + assert_eq!(output, CallbackContract::hresult(1)); + SinkCallbackResult::with_output( + HRESULT(0), + Value::Buffer(ComBufferValue::from_owned_bytes(vec![9, 8, 7], 3)), + ) + }), + ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *mut u8, u32, *mut u32) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + let mut bytes = [0xffu8; 5]; + let mut actual = u32::MAX; + assert_eq!( + unsafe { + invoke( + sink.as_raw(), + bytes.as_mut_ptr(), + bytes.len() as u32, + &mut actual, + ) + }, + HRESULT(0) + ); + assert_eq!(bytes, [9, 8, 7, 0, 0]); + assert_eq!(actual, 3); + } - let mut error_info = std::ptr::null_mut(); - unsafe { CreateErrorInfo(&mut error_info) } - .ok() - .map_err(result::Error::WindowsError)?; - if error_info.is_null() { - return Err(invalid_argument( - "CreateErrorInfo succeeded without returning an interface", - )); + #[test] + fn dynamic_com_sink_libffi_reads_aliased_inout_capacity_value() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table) + .add_caller_output_buffer( + Type::winrt(table.u8_type()), + 1, + Some(1), + BufferCountUnit::Elements, + false, + ) + .unwrap() + .add_in_out(Type::winrt(table.u32_type())); + let interface = register_interface( + &table, + "Test.ILibffiAliasedCapacitySink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method("Invoke", signature); + let sink = create_sink( + &interface, + Arc::new(|_, _, values, output| { + assert!(values.is_empty()); + assert_eq!(output, CallbackContract::hresult(1)); + SinkCallbackResult::with_output( + HRESULT(0), + Value::Buffer(ComBufferValue::from_owned_bytes(vec![9, 8, 7], 3)), + ) + }), + ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *mut u8, *mut u32) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + let mut bytes = [0xffu8; 7]; + let mut capacity_actual = 5; + assert_eq!( + unsafe { invoke(sink.as_raw(), bytes.as_mut_ptr(), &mut capacity_actual,) }, + HRESULT(0) + ); + assert_eq!(capacity_actual, 3); + assert_eq!(bytes, [9, 8, 7, 0, 0, 0xff, 0xff]); } - Ok(unsafe { adopt_com_pointer(error_info) }) -} -pub fn set_error_info(value: Option<&WinRTValue>) -> result::Result<()> { - windows_link::link!("oleaut32.dll" "system" fn SetErrorInfo( - reserved: u32, - error_info: *mut c_void - ) -> windows_core::HRESULT); + #[test] + fn dynamic_com_sink_libffi_rejects_short_exact_output_buffer() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table) + .add_caller_output_buffer( + Type::winrt(table.u8_type()), + 1, + None, + BufferCountUnit::Elements, + false, + ) + .unwrap() + .add_in(Type::winrt(table.u32_type())); + let interface = register_interface( + &table, + "Test.ILibffiExactOutputBufferSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method("Invoke", signature); + let sink = create_sink( + &interface, + Arc::new(|_, _, values, output| { + assert!(matches!(values, [Value::WinRt(WinRTValue::U32(5))])); + assert_eq!(output, CallbackContract::hresult(1)); + SinkCallbackResult::with_output( + HRESULT(0), + Value::Buffer(ComBufferValue::from_owned_bytes(vec![9, 8, 7], 3)), + ) + }), + ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *mut u8, u32) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + let mut bytes = [0xffu8; 7]; + assert_eq!( + unsafe { invoke(sink.as_raw(), bytes.as_mut_ptr(), 5) }, + SINK_E_FAIL + ); + assert_eq!(bytes, [0, 0, 0, 0, 0, 0xff, 0xff]); + } - let error_info = value - .map(|value| { - let unknown = value - .as_object() - .ok_or_else(|| invalid_argument("SetErrorInfo requires a COM object"))?; - let mut error_info = std::ptr::null_mut(); - unsafe { - unknown.query( - &GUID::from_u128(0x1cf2b120_547d_101b_8e65_08002b2bd119), - &mut error_info, + #[test] + fn dynamic_com_sink_libffi_allocates_co_task_mem_output_buffer() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table) + .add_callee_allocated_buffer( + Type::winrt(table.u8_type()), + 1, + BufferCountUnit::Elements, + BufferAllocator::CoTaskMem, + ) + .unwrap() + .add_out(Type::winrt(table.u32_type())); + let interface = register_interface( + &table, + "Test.ILibffiAllocatedBufferSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method("Invoke", signature); + let sink = create_sink( + &interface, + Arc::new(move |_, _, values, output| { + assert!(values.is_empty()); + assert_eq!(output, CallbackContract::hresult(1)); + SinkCallbackResult::with_output( + HRESULT(0), + Value::Buffer(ComBufferValue::from_owned_bytes(vec![4, 5, 6], 3)), ) - } - .ok() - .map_err(result::Error::WindowsError)?; - Ok::(unsafe { IUnknown::from_raw(error_info) }) - }) - .transpose()?; - unsafe { - SetErrorInfo( - 0, - error_info - .as_ref() - .map_or(std::ptr::null_mut(), |value| value.as_raw()), + }), ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *mut *mut u8, *mut u32) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + let mut bytes = std::ptr::dangling_mut::(); + let mut count = u32::MAX; + assert_eq!( + unsafe { invoke(sink.as_raw(), &mut bytes, &mut count) }, + HRESULT(0) + ); + assert_eq!(count, 3); + assert_eq!( + unsafe { std::slice::from_raw_parts(bytes, count as usize) }, + [4, 5, 6] + ); + unsafe { windows::Win32::System::Com::CoTaskMemFree(Some(bytes.cast())) }; } - .ok() - .map_err(result::Error::WindowsError) -} -pub fn get_error_info() -> result::Result> { - windows_link::link!("oleaut32.dll" "system" fn GetErrorInfo( - reserved: u32, - error_info: *mut *mut c_void - ) -> windows_core::HRESULT); - - let mut error_info = std::ptr::null_mut(); - let status = unsafe { GetErrorInfo(0, &mut error_info) }; - if status == windows_core::HRESULT(1) { - return Ok(None); - } - status.ok().map_err(result::Error::WindowsError)?; - if error_info.is_null() { - return Err(invalid_argument( - "GetErrorInfo succeeded without returning an interface", - )); + #[test] + fn dynamic_com_sink_libffi_rolls_back_owned_outputs_before_commit() { + let table = MetadataTable::new(); + let source = create_sink( + &sink_interface(IID_TEST_SINK, &[StaticCallbackShape::InterfaceIn1]), + Arc::new(|_, _, _, _| SinkCallbackResult::hresult(HRESULT(0))), + ) + .unwrap(); + let probe_ref_count = |value: &IUnknown| { + let vtable = unsafe { *(value.as_raw() as *const *const windows_core::IUnknown_Vtbl) }; + let added = unsafe { ((*vtable).AddRef)(value.as_raw()) }; + let restored = unsafe { ((*vtable).Release)(value.as_raw()) }; + assert_eq!(added, restored + 1); + restored + }; + let baseline_refs = probe_ref_count(&source); + let source_raw = source.as_raw() as usize; + let signature = MethodSignature::new(&table) + .add_out(Type::winrt(table.interface(IUnknown::IID))) + .add_callee_allocated_buffer( + Type::winrt(table.u8_type()), + 2, + BufferCountUnit::Elements, + BufferAllocator::CoTaskMem, + ) + .unwrap() + .add_out(Type::winrt(table.u32_type())); + let interface = register_interface( + &table, + "Test.ITransactionalOutputSink", + GUID::from_u128(0xd76be352_ef91_4202_85cb_672e2dbf3474), + InterfaceBase::IUnknown, + ) + .add_method("Invoke", signature); + let sink = create_sink( + &interface, + Arc::new(move |_, _, _, output| { + assert_eq!(output, CallbackContract::hresult(2)); + let raw = source_raw as *mut c_void; + let object = unsafe { IUnknown::from_raw_borrowed(&raw) } + .expect("retained callback output object") + .clone(); + SinkCallbackResult::with_outputs( + HRESULT(0), + vec![ + Value::WinRt(WinRTValue::Object(object)), + Value::Buffer(ComBufferValue::from_owned_bytes(vec![1, 2, 3], 3)), + ], + ) + }), + ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn( + *mut c_void, + *mut *mut c_void, + *mut *mut u8, + *mut u32, + ) -> HRESULT = unsafe { std::mem::transmute(*vtable.add(3)) }; + let mut object = std::ptr::dangling_mut(); + let mut bytes = std::ptr::dangling_mut(); + let mut count = u32::MAX; + FAIL_NEXT_CALLBACK_COTASKMEM_ALLOC.with(|fail| fail.set(true)); + assert_eq!( + unsafe { invoke(sink.as_raw(), &mut object, &mut bytes, &mut count) }, + SINK_E_OUTOFMEMORY + ); + let refs_after_failure = probe_ref_count(&source); + let leaked_object = object; + let leaked_bytes = bytes; + if !leaked_object.is_null() { + unsafe { drop(IUnknown::from_raw(leaked_object)) }; + } + if !leaked_bytes.is_null() { + unsafe { windows::Win32::System::Com::CoTaskMemFree(Some(leaked_bytes.cast())) }; + } + assert!(leaked_object.is_null()); + assert!(leaked_bytes.is_null()); + assert_eq!(count, 0); + assert_eq!(refs_after_failure, baseline_refs); } - Ok(Some(unsafe { adopt_com_pointer(error_info) })) -} -/// 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) }) + #[test] + fn dynamic_com_sink_libffi_round_trips_handle_values() { + let table = MetadataTable::new(); + let interface = register_interface( + &table, + "Test.ILibffiHandleSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method( + "Invoke", + MethodSignature::new(&table) + .add_in(Type::pointer()) + .add_out(Type::pointer()), + ); + let expected_bits = 0x1234usize; + let expected = expected_bits as *mut c_void; + let sink = create_sink( + &interface, + Arc::new(move |_, _, values, output| { + let expected = expected_bits as *mut c_void; + assert_eq!(output, CallbackContract::hresult(1)); + assert!(matches!( + values, + [Value::WinRt(WinRTValue::RawPtr(value))] if *value == expected + )); + SinkCallbackResult::with_output( + HRESULT(0), + Value::WinRt(WinRTValue::RawPtr(expected)), + ) + }), + ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn( + *mut c_void, + *mut c_void, + *mut *mut c_void, + ) -> HRESULT = unsafe { std::mem::transmute(*vtable.add(3)) }; + let mut output = std::ptr::null_mut(); + assert_eq!( + unsafe { invoke(sink.as_raw(), expected, &mut output) }, + HRESULT(0) + ); + assert_eq!(output, expected); } -} - -/// Project a managed COM object as a typed WinRT async operation. -/// -/// The input remains owned by the caller. The returned `Async` value holds a -/// separate `IAsyncInfo` reference and can be awaited independently. -pub fn project_winrt_async( - value: &WinRTValue, - async_type: TypeHandle, -) -> result::Result { - let async_type = async_type.normalized_async_type()?; - let object = value - .as_object() - .ok_or_else(|| invalid_argument("project_winrt_async requires a COM object"))?; - let iid = async_type - .iid() - .ok_or_else(|| invalid_argument("project_winrt_async requires a closed async IID"))?; - let mut concrete_ptr = std::ptr::null_mut(); - unsafe { object.query(&iid, &mut concrete_ptr) } - .ok() - .map_err(result::Error::WindowsError)?; - let concrete = unsafe { IUnknown::from_raw(concrete_ptr) }; - let info: windows_future::IAsyncInfo = concrete.cast().map_err(result::Error::WindowsError)?; - Ok(WinRTValue::Async(crate::value::AsyncInfo { - info, - async_type, - })) -} -#[cfg(test)] -fn call_method( - vtable_index: usize, - obj: *mut c_void, - signature: MethodSignature, - args: &[WinRTValue], -) -> result::Result> { - signature.build(vtable_index)?.plan.invoke(obj, args) -} + #[test] + fn dynamic_com_sink_libffi_addrefs_interface_outputs() { + let table = MetadataTable::new(); + let source = create_sink( + &sink_interface(IID_TEST_SINK, &[StaticCallbackShape::InterfaceIn1]), + Arc::new(|_, _, _, _| SinkCallbackResult::hresult(HRESULT(0))), + ) + .unwrap(); + let retained = source.as_raw() as usize; + let interface = register_interface( + &table, + "Test.ILibffiInterfaceOutputSink", + GUID::from_u128(0xfdd1f02a_03b6_4b49_a3f6_8bba9b013779), + InterfaceBase::IUnknown, + ) + .add_method( + "Invoke", + MethodSignature::new(&table).add_out(Type::winrt(table.interface(IUnknown::IID))), + ); + let sink = create_sink( + &interface, + Arc::new(move |_, _, values, output| { + assert!(values.is_empty()); + assert_eq!(output, CallbackContract::hresult(1)); + let raw = retained as *mut c_void; + let retained = unsafe { IUnknown::from_raw_borrowed(&raw) } + .expect("retained test COM object") + .clone(); + SinkCallbackResult::with_output( + HRESULT(0), + Value::WinRt(WinRTValue::Object(retained)), + ) + }), + ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + let mut output = std::ptr::null_mut(); + assert_eq!(unsafe { invoke(sink.as_raw(), &mut output) }, HRESULT(0)); + let mut expected = std::ptr::null_mut(); + unsafe { source.query(&IUnknown::IID, &mut expected) } + .ok() + .unwrap(); + assert_eq!(output, expected); + unsafe { drop(IUnknown::from_raw(expected)) }; + unsafe { drop(IUnknown::from_raw(output)) }; + } -#[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) -} + #[test] + fn dynamic_com_sink_libffi_queries_typed_interface_outputs() { + const IID_EXPECTED: GUID = GUID::from_u128(0x3381cf13_03ba_4a87_94d3_d684a34e50f7); + const IID_RETURNED: GUID = GUID::from_u128(0x124f66d5_7a1c_4d4a_b4c5_a347e8c8980d); + let table = MetadataTable::new(); + let expected = register_interface( + &table, + "Test.IExpectedCallbackOutput", + IID_EXPECTED, + InterfaceBase::IUnknown, + ) + .add_method("Invoke", MethodSignature::new(&table)); + let returned = register_interface( + &table, + "Test.IReturnedCallbackOutput", + IID_RETURNED, + InterfaceBase::IUnknown, + ) + .add_method("Invoke", MethodSignature::new(&table)); + let source = create_object( + &[expected, returned], + Arc::new(|_, _, _, _| SinkCallbackResult::hresult(HRESULT(0))), + ) + .unwrap(); + let query = |iid: &GUID| { + let mut value = std::ptr::null_mut(); + unsafe { source.query(iid, &mut value) }.ok().unwrap(); + unsafe { IUnknown::from_raw(value) } + }; + let expected_view = query(&IID_EXPECTED); + let returned_view = query(&IID_RETURNED); + assert_ne!(expected_view.as_raw(), returned_view.as_raw()); -#[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) -} + let returned_raw = returned_view.as_raw() as usize; + let output_interface = register_interface( + &table, + "Test.ITypedCallbackOutputSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ) + .add_method( + "Invoke", + MethodSignature::new(&table).add_out(Type::winrt(table.interface(IID_EXPECTED))), + ); + let sink = create_sink( + &output_interface, + Arc::new(move |_, _, _, _| { + let raw = returned_raw as *mut c_void; + let returned = unsafe { IUnknown::from_raw_borrowed(&raw) } + .expect("retained returned interface view") + .clone(); + SinkCallbackResult::with_output( + HRESULT(0), + Value::WinRt(WinRTValue::Object(returned)), + ) + }), + ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + let mut output = std::ptr::null_mut(); + assert_eq!(unsafe { invoke(sink.as_raw(), &mut output) }, HRESULT(0)); + assert_eq!(output, expected_view.as_raw()); + assert_ne!(output, returned_view.as_raw()); + unsafe { drop(IUnknown::from_raw(output)) }; + + let foreign = create_object( + &[register_interface( + &table, + "Test.IForeignCallbackOutput", + IID_RETURNED, + InterfaceBase::IUnknown, + ) + .add_method("Invoke", MethodSignature::new(&table))], + Arc::new(|_, _, _, _| SinkCallbackResult::hresult(HRESULT(0))), + ) + .unwrap(); + let mut foreign_view = std::ptr::null_mut(); + unsafe { foreign.query(&IID_RETURNED, &mut foreign_view) } + .ok() + .unwrap(); + let foreign_view = unsafe { IUnknown::from_raw(foreign_view) }; + let foreign_raw = foreign_view.as_raw() as usize; + let foreign_sink = create_sink( + &output_interface, + Arc::new(move |_, _, _, _| { + let raw = foreign_raw as *mut c_void; + let foreign = unsafe { IUnknown::from_raw_borrowed(&raw) } + .expect("retained foreign interface view") + .clone(); + SinkCallbackResult::with_output( + HRESULT(0), + Value::WinRt(WinRTValue::Object(foreign)), + ) + }), + ) + .unwrap(); + let vtable = unsafe { *(foreign_sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + output = std::ptr::dangling_mut(); + assert_eq!( + unsafe { invoke(foreign_sink.as_raw(), &mut output) }, + SINK_E_NOINTERFACE + ); + assert!(output.is_null()); -#[cfg(test)] -fn wide_null(text: &str) -> Vec { - text.encode_utf16().chain(std::iter::once(0)).collect() -} + let null_sink = create_sink( + &output_interface, + Arc::new(|_, _, _, _| { + SinkCallbackResult::with_output(HRESULT(0), Value::WinRt(WinRTValue::Null)) + }), + ) + .unwrap(); + let vtable = unsafe { *(null_sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(3)) }; + output = std::ptr::dangling_mut(); + assert_eq!( + unsafe { invoke(null_sink.as_raw(), &mut output) }, + SINK_E_FAIL + ); + assert!(output.is_null()); + } -#[cfg(test)] -fn wide_buffer(characters: usize) -> Vec { - vec![0; characters] -} + #[test] + fn dynamic_com_sink_libffi_supports_vtables_larger_than_static_fast_path() { + let table = MetadataTable::new(); + let mut interface = register_interface( + &table, + "Test.ILargeLibffiSink", + IID_TEST_SINK, + InterfaceBase::IUnknown, + ); + for index in 0..17 { + interface = interface.add_method( + &format!("Invoke{index}"), + MethodSignature::new(&table).add_in(Type::winrt(table.i32_type())), + ); + } + let sink = create_sink( + &interface, + Arc::new(|_, slot, values, contract| { + assert_eq!(slot, 19); + assert_eq!(values.len(), 1); + assert_eq!(contract, CallbackContract::hresult(0)); + SinkCallbackResult::hresult(HRESULT(17)) + }), + ) + .unwrap(); + let vtable = unsafe { *(sink.as_raw() as *const *const *const c_void) }; + let invoke: unsafe extern "system" fn(*mut c_void, i32) -> HRESULT = + unsafe { std::mem::transmute(*vtable.add(19)) }; + assert_eq!(unsafe { invoke(sink.as_raw(), 1) }, HRESULT(17)); + } -#[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]) -} + #[test] + fn dynamic_com_object_exposes_multiple_interfaces_with_canonical_identity() { + const IID_FIRST: GUID = GUID::from_u128(0x8aab87f3_1b12_4494_a664_15157b113f93); + const IID_SECOND: GUID = GUID::from_u128(0x506d20b3_30e8_4d7f_94bc_29a0c1a6b5ca); + const IID_FIRST_BASE: GUID = GUID::from_u128(0x4f023f97_329f_4d66_969d_0d85629a863d); + let table = MetadataTable::new(); + let first = register_interface(&table, "Test.IFirst", IID_FIRST, InterfaceBase::IUnknown) + .add_method( + "Invoke", + MethodSignature::new(&table).add_in(Type::winrt(table.i32_type())), + ) + .add_base_interface(IID_FIRST_BASE) + .unwrap(); + let second = + register_interface(&table, "Test.ISecond", IID_SECOND, InterfaceBase::IUnknown) + .add_method("Invoke", MethodSignature::new(&table)); + let calls = Arc::new(Mutex::new(Vec::new())); + let retained = calls.clone(); + let object = create_object( + &[first, second], + Arc::new(move |iid, slot, values, output| { + assert_eq!(slot, 3); + assert_eq!(output, CallbackContract::hresult(0)); + retained.lock().unwrap().push((iid, values.len())); + SinkCallbackResult::hresult(HRESULT(0)) + }), + ) + .unwrap(); -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - MetadataTable, com_helpers::E_NOINTERFACE, ro_get_activation_factory_2, - roapi::query_interface, - }; - use std::sync::atomic::{AtomicU32, Ordering}; - use windows::{ - ApplicationModel::DataTransfer::DataTransferManager, - System::Threading::{ThreadPool, WorkItemHandler}, - Win32::{ - System::Com::{CoGetMalloc, CreateBindCtx, IBindCtx, IMalloc, IPersistFile, IStream}, - System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize}, - UI::Shell::{IDataTransferManagerInterop, SHCreateMemStream}, - UI::WindowsAndMessaging::{ - CreateWindowExW, DestroyWindow, WINDOW_EX_STYLE, WS_OVERLAPPED, - }, - }, - }; + let query = |iid: &GUID| { + let mut value = std::ptr::null_mut(); + unsafe { object.query(iid, &mut value) }.ok().unwrap(); + unsafe { IUnknown::from_raw(value) } + }; + let first = query(&IID_FIRST); + let first_base = query(&IID_FIRST_BASE); + let second = query(&IID_SECOND); + assert_eq!(first.as_raw(), first_base.as_raw()); + assert_ne!(first.as_raw(), second.as_raw()); + + let first_vtable = unsafe { *(first.as_raw() as *const *const *const c_void) }; + let first_invoke: unsafe extern "system" fn(*mut c_void, i32) -> HRESULT = + unsafe { std::mem::transmute(*first_vtable.add(3)) }; + assert_eq!(unsafe { first_invoke(first.as_raw(), 5) }, HRESULT(0)); + + let second_vtable = unsafe { *(second.as_raw() as *const *const *const c_void) }; + let second_invoke: unsafe extern "system" fn(*mut c_void) -> HRESULT = + unsafe { std::mem::transmute(*second_vtable.add(3)) }; + assert_eq!(unsafe { second_invoke(second.as_raw()) }, HRESULT(0)); + + let mut first_identity = std::ptr::null_mut(); + let mut second_identity = std::ptr::null_mut(); + unsafe { first.query(&IUnknown::IID, &mut first_identity) } + .ok() + .unwrap(); + unsafe { second.query(&IUnknown::IID, &mut second_identity) } + .ok() + .unwrap(); + assert_eq!(first_identity, object.as_raw()); + assert_eq!(second_identity, object.as_raw()); + unsafe { drop(IUnknown::from_raw(first_identity)) }; + unsafe { drop(IUnknown::from_raw(second_identity)) }; + assert_eq!( + *calls.lock().unwrap(), + vec![(IID_FIRST, 1), (IID_SECOND, 0)] + ); + } use windows_core::{HSTRING, w}; #[repr(C)] diff --git a/crates/dynwinrt/src/lib.rs b/crates/dynwinrt/src/lib.rs index 884f7e7f..9ad14cec 100644 --- a/crates/dynwinrt/src/lib.rs +++ b/crates/dynwinrt/src/lib.rs @@ -9,6 +9,7 @@ pub mod com; mod composition; mod interfaces; mod native_call; +mod native_callback; mod result; mod roapi; mod signature; diff --git a/crates/dynwinrt/src/native_callback.rs b/crates/dynwinrt/src/native_callback.rs new file mode 100644 index 00000000..199d12bb --- /dev/null +++ b/crates/dynwinrt/src/native_callback.rs @@ -0,0 +1,313 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use core::{ffi::c_void, mem::size_of}; +use std::{ + collections::HashMap, + hash::{Hash, Hasher}, + panic::{AssertUnwindSafe, catch_unwind}, + sync::{Arc, LazyLock, Mutex}, +}; + +use libffi::{low, middle::Type}; +use windows_core::HRESULT; + +use crate::native_call::system_cif; + +const E_FAIL: HRESULT = HRESULT(0x80004005u32 as i32); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) enum CallbackAbiType { + I8, + U8, + I16, + U16, + I32, + U32, + I64, + U64, + F32, + F64, + Pointer, + Guid, + NativeStruct(String, usize), +} + +impl CallbackAbiType { + pub(crate) fn size(&self) -> usize { + match self { + Self::I8 | Self::U8 => 1, + Self::I16 | Self::U16 => 2, + Self::I32 | Self::U32 | Self::F32 => 4, + Self::I64 | Self::U64 | Self::F64 => 8, + Self::Pointer => size_of::<*mut c_void>(), + Self::Guid => 16, + Self::NativeStruct(_, size) => *size, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(crate) enum CallbackReturnAbi { + HResult, + Void, + Value(CallbackAbiType), +} + +#[derive(Debug, Clone)] +pub(crate) struct CallbackSignature { + parameters: Vec, + libffi_parameters: Vec, + return_abi: CallbackReturnAbi, + libffi_return: Type, +} + +// libffi Type graphs are immutable after construction. CallbackSignature owns +// every graph and only shares it through immutable references while preparing +// cached CIFs. +unsafe impl Send for CallbackSignature {} +unsafe impl Sync for CallbackSignature {} + +impl PartialEq for CallbackSignature { + fn eq(&self, other: &Self) -> bool { + self.parameters == other.parameters && self.return_abi == other.return_abi + } +} + +impl Eq for CallbackSignature {} + +impl Hash for CallbackSignature { + fn hash(&self, state: &mut H) { + self.parameters.hash(state); + self.return_abi.hash(state); + } +} + +impl CallbackSignature { + pub(crate) fn hresult(parameters: Vec<(CallbackAbiType, Type)>) -> Self { + let (parameters, libffi_parameters) = parameters.into_iter().unzip(); + Self { + parameters, + libffi_parameters, + return_abi: CallbackReturnAbi::HResult, + libffi_return: Type::i32(), + } + } + + pub(crate) fn void(parameters: Vec<(CallbackAbiType, Type)>) -> Self { + let (parameters, libffi_parameters) = parameters.into_iter().unzip(); + Self { + parameters, + libffi_parameters, + return_abi: CallbackReturnAbi::Void, + libffi_return: Type::void(), + } + } + + pub(crate) fn direct( + parameters: Vec<(CallbackAbiType, Type)>, + result: (CallbackAbiType, Type), + ) -> Self { + let (parameters, libffi_parameters) = parameters.into_iter().unzip(); + Self { + parameters, + libffi_parameters, + return_abi: CallbackReturnAbi::Value(result.0), + libffi_return: result.1, + } + } + + pub(crate) fn parameters(&self) -> &[CallbackAbiType] { + &self.parameters + } + + pub(crate) fn return_abi(&self) -> &CallbackReturnAbi { + &self.return_abi + } + + pub(crate) unsafe fn initialize_failure_result(&self, result: *mut c_void, error: HRESULT) { + if result.is_null() { + return; + } + unsafe { + match self.return_abi() { + CallbackReturnAbi::HResult => result.cast::().write(error.0), + CallbackReturnAbi::Void => {} + CallbackReturnAbi::Value(value) if value.size() != 0 => { + std::ptr::write_bytes(result, 0, value.size()) + } + CallbackReturnAbi::Value(_) => {} + } + } + } + + fn argument_types(&self) -> Vec { + let mut types = vec![Type::pointer()]; + types.extend(self.libffi_parameters.iter().cloned()); + types + } + + fn result_type(&self) -> Type { + self.libffi_return.clone() + } +} + +pub(crate) type CallbackDispatch = unsafe fn( + slot: usize, + signature: &CallbackSignature, + args: *const *const c_void, + result: *mut c_void, +); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +struct ClosureKey { + slot: usize, + signature: CallbackSignature, + dispatch: usize, +} + +struct CallbackContext { + slot: usize, + signature: CallbackSignature, + dispatch: CallbackDispatch, +} + +struct OwnedCallbackClosure { + _cif: Box, + closure: *mut low::ffi_closure, + code: *const c_void, + _context: Box, +} + +// The CIF, context, and executable closure are fully initialized before they +// enter the global cache and remain immutable for the process lifetime. +unsafe impl Send for OwnedCallbackClosure {} +unsafe impl Sync for OwnedCallbackClosure {} + +impl OwnedCallbackClosure { + fn new( + slot: usize, + signature: CallbackSignature, + dispatch: CallbackDispatch, + ) -> Result { + let cif = Box::new(system_cif( + signature.argument_types(), + signature.result_type(), + )); + let context = Box::new(CallbackContext { + slot, + signature, + dispatch, + }); + let (closure, code) = low::closure_alloc(); + if closure.is_null() || code.as_ptr().is_null() { + if !closure.is_null() { + unsafe { low::closure_free(closure) }; + } + return Err("libffi could not allocate executable callback memory".into()); + } + let status = unsafe { + libffi_sys::ffi_prep_closure_loc( + closure, + cif.as_raw_ptr(), + Some(invoke_callback), + context.as_ref() as *const CallbackContext as *mut c_void, + code.as_mut_ptr(), + ) + }; + if status != libffi_sys::ffi_status_FFI_OK { + unsafe { low::closure_free(closure) }; + return Err(format!( + "libffi could not prepare callback closure: {status}" + )); + } + Ok(Self { + _cif: cif, + closure, + code: code.as_ptr(), + _context: context, + }) + } +} + +impl Drop for OwnedCallbackClosure { + fn drop(&mut self) { + if !self.closure.is_null() { + unsafe { low::closure_free(self.closure) }; + self.closure = std::ptr::null_mut(); + } + } +} + +unsafe extern "C" fn invoke_callback( + _cif: *mut libffi_sys::ffi_cif, + result: *mut c_void, + args: *mut *mut c_void, + userdata: *mut c_void, +) { + let context = unsafe { &*userdata.cast::() }; + let dispatch = catch_unwind(AssertUnwindSafe(|| unsafe { + (context.dispatch)( + context.slot, + &context.signature, + args.cast_const().cast(), + result, + ) + })); + if dispatch.is_err() { + unsafe { context.signature.initialize_failure_result(result, E_FAIL) }; + } +} + +static CLOSURES: LazyLock>>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + +pub(crate) fn callback_code( + slot: usize, + signature: CallbackSignature, + dispatch: CallbackDispatch, +) -> Result<*const c_void, String> { + let key = ClosureKey { + slot, + signature: signature.clone(), + dispatch: dispatch as usize, + }; + let mut closures = CLOSURES + .lock() + .map_err(|_| "libffi callback cache is poisoned".to_string())?; + if let Some(closure) = closures.get(&key) { + return Ok(closure.code); + } + let closure = Arc::new(OwnedCallbackClosure::new(slot, signature, dispatch)?); + let code = closure.code; + closures.insert(key, closure); + Ok(code) +} + +#[cfg(test)] +mod tests { + use super::*; + + unsafe fn test_dispatch( + slot: usize, + signature: &CallbackSignature, + args: *const *const c_void, + result: *mut c_void, + ) { + assert_eq!(slot, 3); + assert_eq!(signature.parameters(), &[CallbackAbiType::I32]); + let value = unsafe { *(*args.add(1)).cast::() }; + unsafe { *result.cast::() = value }; + } + + #[test] + fn libffi_callback_closures_are_callable_and_cached() { + let signature = CallbackSignature::hresult(vec![(CallbackAbiType::I32, Type::i32())]); + let first = callback_code(3, signature.clone(), test_dispatch).unwrap(); + let second = callback_code(3, signature, test_dispatch).unwrap(); + assert_eq!(first, second); + let callback: unsafe extern "system" fn(*mut c_void, i32) -> HRESULT = + unsafe { std::mem::transmute(first) }; + assert_eq!(unsafe { callback(std::ptr::null_mut(), 27) }, HRESULT(27)); + } +} diff --git a/docs/architecture/classic-com-support.md b/docs/architecture/classic-com-support.md index 784f0584..2785b57e 100644 --- a/docs/architecture/classic-com-support.md +++ b/docs/architecture/classic-com-support.md @@ -110,6 +110,28 @@ the count and output storage, and returns a numeric/enum array. Parameter direction, return convention, result ownership, cleanup, buffer relationships, activation, and dynamic-IID behavior are encoded in the projected IR. +An optional implementation plan is encoded only for an `IUnknown`-rooted +interface whose complete contiguous vtable maps to the validated callback +subset. That subset includes scalar/enum/GUID/handle/interface values, +HRESULT/void/direct-scalar returns, BSTR/HSTRING and borrowed string pointers, +POD values/pointers, basic InOut, and authoritative plain counted-buffer +contracts. Each registered method owns both its outbound `ComCallPlan` and +full inbound `CallbackMethodPlan`. The runtime chooses a static thunk for a +common signature or a cached libffi closure for every other supported +signature; the renderer never serializes backend shapes. + +Implemented objects may expose multiple independently generated interface +views. QueryInterface routes each derived and base IID to its frozen view, +every view shares one reference count, and QueryInterface for `IUnknown` +always returns the canonical identity. Generated implementation descriptors +compose these views without exposing handwritten signatures. + +libffi allocates executable closure memory. A process mitigation such as +`ProhibitDynamicCode` can therefore reject a signature that has no static fast +path; object creation reports that failure instead of publishing a partial +vtable. Prepared closures are cached for the process lifetime so a callback +that performs the final reentrant `Release` cannot free the machine-code page +currently executing. Production projection reads those decisions only from the validated semantic contracts; the shared compatibility metadata supplies names, documentation, and enum member values, not ABI meaning. @@ -351,6 +373,12 @@ Required support: - callback threading/apartment dispatch; and - conversion of callback failures to HRESULT. +The dynamic implementation backend now provides generated vtables, canonical +multi-interface identity, static fast paths plus libffi closures, owner-thread +dispatch, and fail-closed output validation. Interface InOut replacement +remains unsupported because its old/new reference ownership is not encoded +strongly enough. + ### 8. Semantic HRESULT values **Problem:** Most HRESULTs are throw-or-success, but methods such as @@ -397,7 +425,8 @@ conventions, `GetLastError`, callbacks, and handle cleanup. 3. Explicit allocator/ownership metadata. 4. VARIANT/PROPVARIANT and semantic HRESULT handling. 5. SAFEARRAY. -6. Arbitrary COM sink/interface implementation. +6. Broaden generated COM sink/interface implementation beyond the initial + same-thread interface-input subset. 7. Apartment-aware marshaling. 8. Separate flat-Win32 acquisition/invocation layer. @@ -434,6 +463,7 @@ not be described as solving every problem in the map above. | 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 `initializeCom()`. Generated implementation files use the isolated unsafe runtime internally. | +| Dynamic JavaScript COM implementations | Any `IUnknown`-rooted interface whose complete contiguous vtable maps to the validated callback subset receives `static implement()` and `static implementation()`. Static fast-path thunks cover common signatures; cached libffi closures cover arbitrary supported parameter counts, scalar widths, POD layouts, outputs, and native return conventions using the platform COM calling convention. Objects support derived/base IID aliases, multiple interface views, canonical IUnknown identity, shared atomic AddRef/Release, synchronous owner-thread JS dispatch, required Out initialization/validation, allocator-correct transfer, and panic/exception containment. Count/capacity values are read according to their In/InOut ABI direction, fixed outputs without an actual-length slot require exact size, and typed interface outputs are queried to the declared IID. QI references, BSTR/HSTRING values, and CoTaskMem buffers remain RAII-owned until every output is prepared; only then are all native output slots committed, so preparation failure leaves owned outputs null and releases every temporary owner. Wrong-thread HRESULT methods return `RPC_E_WRONG_THREAD`; direct returns are zeroed and void methods do nothing because those native ABIs have no error channel. `IFileDialogEvents` is live-tested with `Advise`/`Unadvise`; `IDropTarget` exercises libffi, POD/InOut, generated multi-interface composition, and QueryInterface. | | Fail-closed generation | Unknown/unsafe layouts, untagged/by-value/output unions, bitfields, flexible arrays, nested owned fields, unsupported VARTYPE/BYREF/SAFEARRAY/PROPVARIANT combinations, unsupported arrays, pointer outputs, ownership, and in/out shapes stop generation with a targeted error. | | Consumable output | Classic COM files live under `com/`, with `./com` and `./com/*` package exports. Mixed and incremental generation preserve the WinRT-only root barrel; COM-only output retains its legacy root entrypoint. | | Explicit vtable registration | Every generated method is registered with `.addMethodAt(vtableIndex, name, signature)`, keyed by its actual metadata-derived vtable slot. Methods are never deduplicated by name, so same-name overloads at different slots both register correctly. | @@ -575,8 +605,8 @@ and `@microsoft/dynwinrt/com`. | SAFEARRAY | Rank 1–8, signed bounds, typed scalar/bool/BSTR/interface/VARIANT elements, SafeArray API validation and cleanup | Unsupported element VARTYPEs, rank > 8, untyped arrays whose VARTYPE cannot be proven, and Automation InOut replacement | | PROPVARIANT | Scalar numeric/bool, LPWSTR, CLSID, FILETIME, blob, and supported vectors with PropVariantClear | Nested VARIANT vectors, streams/interfaces, arrays, clipboard/storage types, BYREF, and unknown VARTYPEs | | Allocator ownership | COM Release, BSTR output/replacement and array elements, VARIANT clear, CoTaskMem buffers/PWSTR elements, 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 | +| Interface pointers | Typed input/output interfaces, QueryInterface, dynamic IID output, and generated multi-interface callback objects with inherited IID aliases | Interface in/out replacement, aggregation, and `IInspectable` implementation | +| Apartments | Explicit initialization, non-agile owner-thread implementations, synchronous same-thread callbacks, and rejection before entering JS on a foreign thread | Cross-apartment marshaling, GIT/agility handling, and callback dispatch | | Activation | In-process `CoCreateInstance` and `CoGetClassObject` | Aggregation, arbitrary CLSCTX, and other non-CoCreate factory functions | | Direct pointer returns | Runtime signature plus exact `IMalloc` codegen | Other direct pointer returns remain fail-closed without exact ownership and cleanup evidence | @@ -592,7 +622,8 @@ and `@microsoft/dynwinrt/com`. - BSTR pointer nesting, scalar input `BSTR*`, callee-allocated outer arrays without exact allocators, and unknown/custom BSTR allocation contracts; - FORMATETC and STGMEDIUM; -- arbitrary COM event/callback sink generation; +- callback methods containing unmodeled ownership, Automation, union, array, + or interface-replacement contracts; - cross-thread/apartment marshaling; and - the general flat-Win32 DLL-export and handle-cleanup layer. @@ -673,7 +704,7 @@ of every type in the 24 MB metadata file. | Private-data bytes or interface pointer | `IDXGIObject`, `ID3D10DeviceChild`, `ID3D10Device`, `ID3D11DeviceChild`, `ID3D11Device`, `ID3D12Object`, and `IDMLObject` `GetPrivateData` | The same GUID-keyed method may return ordinary bytes or an AddRef'd interface pointer. A `Buffer` projection would lose the interface ownership transfer; Direct3D 10 NULL calls are destructive, and DXGI's data parameter is required. | Exact Win32 winmd identities + Microsoft method documentation | | Untyped output pointers without allocator/ownership | `IAudioClient::IsFormatSupported` and unrelated `void*` outputs | 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 | +| Callback methods outside the validated implementation subset | Automation providers, custom marshaling, and resource-owning callbacks | The dynamic backend supports broad scalar/string/POD/buffer ABI shapes and multi-interface inheritance, but VARIANT/SAFEARRAY/PROPVARIANT callbacks, untagged unions, unknown pointers/allocators, interface replacement, and custom marshal contracts still fail the whole interface closed. | Runtime/codegen validation 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 | The unsafe runtime's `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 | @@ -1238,10 +1269,10 @@ against the resolved namespace. | 5 | `IClassFactory` | 6,712 | 70 | Yes | Generates completely and is live-tested through `CoGetClassObject` | | 6 | `IDispatch` via `IID_IDispatch` | 6,408 | 46 | Yes | Complete inherited interface generates; `Invoke` uses dedicated DISPPARAMS/EXCEPINFO and explicit optional-output requests | | 7 | `IPersistFile` | 5,996 | 97 | Yes | Generates and live-tested | -| 8 | `IConnectionPoint` | 5,832 | 51 | Yes | Generates; implementing event sinks is not supported | +| 8 | `IConnectionPoint` | 5,832 | 51 | Yes | Generates; callback objects passed to `Advise` must satisfy the complete validated same-thread implementation subset | | 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 | Generates with by-value POD `POINTL`; implementing a drop target is not supported | +| 11 | `IDropTarget` | 4,368 | 57 | Yes | Generates for client calls and dynamic JavaScript implementation; live E2E covers by-value `POINTL`, scalar/InOut callback ABI, libffi dispatch, and multi-interface QueryInterface | | 12 | `IShellFolder` | 4,056 | 33 | Yes | Fail closed: untyped PIDL output ownership (and later `STRRET` union ABI) | | 13 | `IFileDialog` | 4,048 | 98 | Yes | Generates; inherited methods tested through `IFileOpenDialog` | | 14 | `IXMLDOMDocument` | 3,784 | 46 | Yes | Fail closed: inherited unsupported Automation shapes beyond scalar VARIANT | @@ -1323,7 +1354,7 @@ hardware, and whether it adds a distinct ABI shape. Classic COM interfaces are exercised across core and generated Node coverage. Core live tests are in -[`crates/dynwinrt/src/com.rs`](../../crates/dynwinrt/src/com.rs). The sixteen Node +[`crates/dynwinrt/src/com.rs`](../../crates/dynwinrt/src/com.rs). The seventeen Node runners are in [`tests/e2e/runners/com`](../../tests/e2e/runners/com) and are generated and executed by [`tests/e2e/e2e_test.ps1`](../../tests/e2e/e2e_test.ps1). @@ -1371,7 +1402,8 @@ meaningful `argErr`, and generate an `Error` with `hresult` plus optional | `IBindCtx` | Core + Node | Exact multi-architecture `BIND_OPTS` layout, automatic `cbStruct`, pre-dispatch validation, and live `CreateBindCtx` round trip. | | `TaskbarList` / `ITaskbarList3` | Node E2E | Coclass construction, inherited slots, runtime QI views, HWND, BOOL, enum, and `u64`. | | `FileOperation` | Node E2E | Coclass construction, unsigned flags, and state query. | -| `FileOpenDialog` | Node E2E | STA coclass construction and get/set options without user interaction. | +| `FileOpenDialog` / `IFileDialogEvents` | Core + Node E2E | STA coclass construction, generated synchronous JS implementation, self-vtable callback dispatch, public native-value bridge, and real `Advise`/`Unadvise` without showing UI. | +| `IDropTarget` | Core + Node E2E | Dynamic libffi callbacks, interface/scalar/POD/InOut parameters, generated multi-interface composition, and QueryInterface to the additional view. | | `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. | @@ -1380,10 +1412,13 @@ Additional regression tests cover: - a test-only windows-rs ABI oracle for selected stable interfaces and native layouts: interface IIDs, host-target size/alignment, and field offsets for - `RECT`, `THUMBBUTTON`, `WIN32_FIND_DATAW`, `DISPPARAMS`, and `EXCEPINFO`; + `RECT`, `THUMBBUTTON`, `WIN32_FIND_DATAW`, `DISPPARAMS`, `EXCEPINFO`, and + the complete `IFileDialogEvents` callback vtable; windows-rs is not a production dispatch backend and does not replace semantic validation; - rejection of duplicate ownership through exported pointer bits; +- generated COM sink Worker teardown, wrong-thread late invocation, + JavaScript exception-to-HRESULT behavior, and callback-resource cleanup; - detached TypedArray backing storage; - BSTR exact-length allocation, replacement, null/failure cleanup, and `CoTaskMem` cleanup; diff --git a/docs/guides/windows/classic-com-usage.md b/docs/guides/windows/classic-com-usage.md index 6ebcc29f..dc0b0ee5 100644 --- a/docs/guides/windows/classic-com-usage.md +++ b/docs/guides/windows/classic-com-usage.md @@ -224,6 +224,93 @@ try { } ``` +### 5.4 Implement generated COM event sinks + +Codegen emits `static implement()` only when an interface is +`IUnknown`-rooted and every method in its complete inherited vtable maps to +the validated callback ABI subset. For example, `IFileDialogEvents` can be +implemented by synchronous JavaScript handlers: + +```js +import { initializeCom } from "@microsoft/dynwinrt/com"; +import { + FDE_OVERWRITE_RESPONSE, + FileOpenDialog, + IFileDialogEvents, + FDE_SHAREVIOLATION_RESPONSE, +} from "./generated/com/index.js"; + +initializeCom(0); // File dialogs and their event sink use this STA thread. + +const dialog = new FileOpenDialog(); +const events = IFileDialogEvents.implement({ + onFileOk(fileDialog) { + console.log("File accepted", fileDialog.isNull()); + return 0; // S_OK; another successful HRESULT such as S_FALSE is also allowed. + }, + onFolderChanging() {}, + onFolderChange() {}, + onSelectionChange() {}, + onShareViolation(_fileDialog, _item) { + return FDE_SHAREVIOLATION_RESPONSE.FDESVR_REFUSE; + // To return an explicit HRESULT too: return [hresult, response]. + }, + onTypeChange() {}, + onOverwrite() { + return FDE_OVERWRITE_RESPONSE.FDEOR_DEFAULT; + }, +}); + +const cookie = dialog.advise(events.nativeValue); +try { + dialog.show(hwnd); +} finally { + dialog.unadvise(cookie); + events.release(); + dialog.release(); +} +``` + +`nativeValue` is a borrowed bridge for generated COM input parameters. Do not +release it separately; release the generated sink wrapper after every source +has been unadvised. + +Multiple generated interfaces can share one COM identity. Use +`implementation()` for every additional interface and call `as()` to obtain a +QueryInterface view: + +```js +const object = IPrimary.implement( + primaryHandlers, + ISecondary.implementation(secondaryHandlers), +); +const secondary = object.as(ISecondary); + +secondary.release(); +object.release(); +``` + +The generated implementation boundary is: + +- one or more generated `IUnknown`-rooted interfaces, including validated + single-inheritance/base-IID chains; +- every generated handler must be provided; dynwinrt never invents semantic + callback defaults; +- HRESULT, semantic HRESULT, `void`, and direct scalar returns; +- scalar, enum, GUID/REFGUID, handle, COM-interface In/Out, BSTR/HSTRING, + borrowed NUL-terminated strings, POD value/pointer, basic InOut, and plain + counted-buffer contracts with known capacity/count/allocator semantics; +- static thunks for common signatures and dynamic libffi closures for the + remaining validated signatures; +- synchronous handlers on the creating thread only; Promises are unsupported; +- a wrong-thread HRESULT method returns `RPC_E_WRONG_THREAD` without entering + JS; direct-return methods return a zero value and `void` methods do nothing. + +If any method contains an unmodeled Automation/union/ownership/allocator +contract, codegen omits `implement()` for the entire interface. Cross-apartment +dispatch, connection-point helpers, COM aggregation, custom marshaling, and +COM server registration remain unsupported. + ## 6. JavaScript projections of common native types | Native semantics | JavaScript/TypeScript | diff --git a/tests/e2e/e2e_test.ps1 b/tests/e2e/e2e_test.ps1 index 4edf738a..cc4876d8 100644 --- a/tests/e2e/e2e_test.ps1 +++ b/tests/e2e/e2e_test.ps1 @@ -225,7 +225,7 @@ if ("com" -in $Lang) { & cargo run -p dynwinrt-codegen @cargoProfileArgs @cargoTargetArgs --quiet -- generate ` --winmd $win32Winmd ` --namespace Windows.Win32.UI.Shell ` - --class-name "TaskbarList,IShellLinkW,IDataTransferManagerInterop,FileOperation,FileOpenDialog" ` + --class-name "TaskbarList,IShellLinkW,IDataTransferManagerInterop,FileOperation,FileOpenDialog,IFileDialogEvents" ` --output $comShellDir ` --import-name $comRuntimeImport if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM Shell generation failed"; exit 1 } @@ -293,6 +293,14 @@ if ("com" -in $Lang) { --import-name $comRuntimeImport if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM error-info generation failed"; exit 1 } + & cargo run -p dynwinrt-codegen @cargoProfileArgs @cargoTargetArgs --quiet -- generate ` + --winmd $win32Winmd ` + --namespace Windows.Win32.System.Ole ` + --class-name IDropTarget ` + --output $comShellDir ` + --import-name $comRuntimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM callback generation failed"; exit 1 } + & cargo run -p dynwinrt-codegen @cargoProfileArgs @cargoTargetArgs --quiet -- generate ` --namespace Windows.Media ` --class-name SystemMediaTransportControls ` @@ -358,6 +366,7 @@ if ("com" -in $Lang) { "shell-link-pod.mjs", "file-operation.mjs", "file-open-dialog.mjs", + "drop-target.mjs", "wic-imaging-factory.mjs", "sequential-stream-buffer.mjs", "automation-values.mjs", diff --git a/tests/e2e/runners/com/drop-target.mjs b/tests/e2e/runners/com/drop-target.mjs new file mode 100644 index 00000000..c0350487 --- /dev/null +++ b/tests/e2e/runners/com/drop-target.mjs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "node:assert/strict"; +import { Buffer } from "node:buffer"; +import { DynCom } from "../../../../bindings/js/dist/com-unsafe.js"; +import { + createPOINTL, + IDropTarget, +} from "../../e2e_generated/com/shell/com/IDropTarget.js"; +import { IFileDialogEvents } from "../../e2e_generated/com/shell/com/IFileDialogEvents.js"; + +DynCom.initialize(0); + +const calls = []; +const handlers = { + dragEnter(dataObject, keyState, point, effect) { + assert.equal(this, handlers); + assert.equal(typeof dataObject.isNull, "function"); + assert.equal(typeof keyState, "number"); + assert.equal(point.bytes.length, 8); + return effect; + }, + dragOver(keyState, point, effect) { + assert.equal(keyState, 8); + assert.equal(point.bytes.readInt32LE(0), 10); + assert.equal(point.bytes.readInt32LE(4), 20); + assert.equal(effect, 2); + calls.push("over"); + return 3; + }, + dragLeave() { + calls.push("leave"); + }, + drop(dataObject, keyState, point, effect) { + assert.equal(typeof dataObject.isNull, "function"); + assert.equal(typeof keyState, "number"); + assert.equal(point.bytes.length, 8); + return effect; + }, +}; + +const events = IFileDialogEvents.implementation({ + onFileOk() {}, + onFolderChanging() {}, + onFolderChange() {}, + onSelectionChange() {}, + onShareViolation() { + return 0; + }, + onTypeChange() {}, + onOverwrite() { + return 0; + }, +}); +const target = IDropTarget.implement(handlers, events); +const eventsView = target.as(IFileDialogEvents); +try { + const bytes = Buffer.alloc(8); + bytes.writeInt32LE(10, 0); + bytes.writeInt32LE(20, 4); + const point = createPOINTL(bytes); + + assert.equal(target.dragOver(8, point, 2), 3); + target.dragLeave(); + assert.deepEqual(calls, ["over", "leave"]); +} finally { + eventsView.release(); + target.release(); +} + +console.log("drop-target multi-interface dynamic sink ok"); diff --git a/tests/e2e/runners/com/file-open-dialog.mjs b/tests/e2e/runners/com/file-open-dialog.mjs index 4b1be0c0..c4266885 100644 --- a/tests/e2e/runners/com/file-open-dialog.mjs +++ b/tests/e2e/runners/com/file-open-dialog.mjs @@ -3,14 +3,53 @@ import assert from 'node:assert/strict'; import { DynCom } from '../../../../bindings/js/dist/com-unsafe.js'; +import { FDE_OVERWRITE_RESPONSE } from '../../e2e_generated/com/shell/com/FDE_OVERWRITE_RESPONSE.js'; +import { FDE_SHAREVIOLATION_RESPONSE } from '../../e2e_generated/com/shell/com/FDE_SHAREVIOLATION_RESPONSE.js'; import { FileOpenDialog } from '../../e2e_generated/com/shell/com/FileOpenDialog.js'; +import { IFileDialogEvents } from '../../e2e_generated/com/shell/com/IFileDialogEvents.js'; DynCom.initialize(0); const dialog = new FileOpenDialog(); -const options = dialog.getOptions(); -dialog.setOptions(options); -assert.equal(dialog.getOptions(), options); -dialog.release(); +const calls = []; +assert.throws( + () => IFileDialogEvents.implement({}), + /onFileOk must be a function/, +); +const eventsImplementation = { + onFileOk(value) { + assert.equal(this, eventsImplementation); + assert.equal(value.isNull(), false); + calls.push('fileOk'); + return 1; + }, + onFolderChanging() {}, + onFolderChange() {}, + onSelectionChange() {}, + onShareViolation() { + return FDE_SHAREVIOLATION_RESPONSE.FDESVR_DEFAULT; + }, + onTypeChange() {}, + onOverwrite() { + return FDE_OVERWRITE_RESPONSE.FDEOR_DEFAULT; + }, +}; +const events = IFileDialogEvents.implement(eventsImplementation); -console.log('file-open-dialog ok'); +try { + const options = dialog.getOptions(); + dialog.setOptions(options); + assert.equal(dialog.getOptions(), options); + + events.onFileOk(dialog._obj); + assert.deepEqual(calls, ['fileOk']); + events.onSelectionChange(dialog._obj); + + const cookie = dialog.advise(events.nativeValue); + dialog.unadvise(cookie); +} finally { + events.release(); + dialog.release(); +} + +console.log('file-open-dialog and events sink ok'); diff --git a/tools/dynwinrt-codegen/src/codegen/com/ir.rs b/tools/dynwinrt-codegen/src/codegen/com/ir.rs index 40ae59e9..7008a0f2 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/ir.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/ir.rs @@ -489,6 +489,27 @@ pub(super) struct ProjectedComMethod { pub(super) overload: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct ProjectedComSinkMethod { + pub(super) vtable_index: usize, + pub(super) handler_name: String, + pub(super) return_convention: ComSinkReturnConvention, + pub(super) output_count: usize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ComSinkReturnConvention { + HResult, + SemanticHResult, + Void, + Direct, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct ComSinkPlan { + pub(super) methods: Vec, +} + /// Classifies the JS runtime shape a validated `ComType` presents as, for /// overload-dispatch purposes. Returns `None` for any type whose JS /// representation is ambiguous or overlaps another candidate shape (pointer @@ -590,10 +611,12 @@ pub(super) struct ProjectedComInterface { pub(super) name: String, pub(super) namespace: String, pub(super) iid: String, + pub(super) base_iids: Vec, pub(super) is_iunknown_rooted: bool, pub(super) methods: Vec, pub(super) activation: ActivationPlan, pub(super) referenced_enums: Vec, + pub(super) sink: Option, } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs index 27886f28..1f440df6 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs @@ -9,11 +9,11 @@ use std::collections::BTreeMap; use super::super::ir::ProjectedComEnumMember; use super::super::ir::{ ActivationPlan, BufferCountUnit, ComEnumUnderlying, ComParamDirection, ComPrimitive, - ComReturnConvention, ComType, DispatchShape, OverloadDispatch, PointerAliasKind, - ProjectedComCoclass, ProjectedComEnum, ProjectedComInterface, ProjectedComMethod, - ProjectedComMethodKind, ProjectedComParam, ProjectedComResult, ProjectedEnumValue, - ProjectedInterfaceRef, ResultConversion, ResultSource, SharedCountPlan, StringEncoding, - TypedBufferPlan, TypedBufferRelation, TypedBufferSizing, + ComReturnConvention, ComSinkReturnConvention, ComType, DispatchShape, OverloadDispatch, + PointerAliasKind, ProjectedComCoclass, ProjectedComEnum, ProjectedComInterface, + ProjectedComMethod, ProjectedComMethodKind, ProjectedComParam, ProjectedComResult, + ProjectedEnumValue, ProjectedInterfaceRef, ResultConversion, ResultSource, SharedCountPlan, + StringEncoding, TypedBufferPlan, TypedBufferRelation, TypedBufferSizing, }; use super::naming::js_param_name; #[cfg(test)] @@ -21,7 +21,7 @@ use super::types::type_dts; use super::types::{ abi_type_js, input_type_dts, native_pod_descriptor_js, native_pod_layout_js, native_union_descriptor_js, native_union_layout_js, result_type_dts, safe_array_abi_type_js, - scalar_type_dts, unwrap_result_js, wrap_arg_js, + scalar_type_dts, unwrap_callback_arg_js, unwrap_result_js, wrap_arg_js, }; use crate::codegen::winrt::javascript::render::javascript::commonjs::convert_to_cjs_with_eager; @@ -246,24 +246,44 @@ fn render_js(meta: &ProjectedComInterface) -> String { }; 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", + let get_iface_var = format!("_get{}", meta.name); + let method_lines = meta + .methods + .iter() + .map(|method| { + format!( + " .addMethodAt({}, '{}', {})\n", + method.vtable_index, + method.name, + build_method_sig_js(method) + ) + }) + .collect::(); + let mut registration = format!( + "DynCom.{register_fn}('{}.{}', IID_{})", meta.namespace, meta.name, meta.name - )); - for method in &meta.methods { + ); + if meta.sink.is_some() { + for base_iid in &meta.base_iids { + registration.push_str(&format!( + "\n .addBaseInterface(WinGuid.parse('{base_iid}'))" + )); + } + } + if !method_lines.is_empty() { + registration.push('\n'); + registration.push_str(method_lines.trim_end()); + } + out.push_str(&format!("let {cache_var};\n")); + if meta.sink.is_some() { out.push_str(&format!( - " .addMethodAt({}, '{}', {})\n", - method.vtable_index, - method.name, - build_method_sig_js(method) + "const {get_iface_var} = () => {{\n {cache_var} ??= {registration};\n return {cache_var};\n}};\nconst {iface_var} = new Proxy({{}}, {{\n get(_target, prop) {{\n const iface = {get_iface_var}();\n const value = iface[prop];\n return typeof value === 'function' ? value.bind(iface) : value;\n }},\n}});\n\n" + )); + } else { + out.push_str(&format!( + "const {iface_var} = new Proxy({{}}, {{\n get(_target, prop) {{\n {cache_var} ??= {registration};\n const value = {cache_var}[prop];\n return typeof value === 'function' ? value.bind({cache_var}) : value;\n }},\n}});\n\n" )); } - 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(&format!(" static IID = IID_{};\n", meta.name)); let wrap_owned = format!("_wrap{}Owned", meta.name); @@ -275,6 +295,13 @@ fn render_js(meta: &ProjectedComInterface) -> String { " static _fromNative(obj) {{ return {wrap_owned}(obj.cast(IID_{})); }}\n", meta.name, )); + if meta.sink.is_some() { + out.push_str(" /** Borrowed native value for passing this implementation to generated COM methods. Do not release it separately. */\n get nativeValue() { return this._obj; }\n"); + out.push_str( + " /** Query another generated interface implemented by the same COM identity. */\n as(InterfaceClass) { return InterfaceClass._fromNative(this._obj); }\n", + ); + render_sink_implementation_js(&mut out, meta, &wrap_owned, &get_iface_var); + } out.push_str(" /** Release the underlying native COM reference. Safe to call more than once. */\n release() {\n this._obj.release();\n }\n"); match &meta.activation { ActivationPlan::None => {} @@ -364,6 +391,166 @@ fn render_js(meta: &ProjectedComInterface) -> String { out } +fn render_sink_implementation_js( + out: &mut String, + meta: &ProjectedComInterface, + wrap_owned: &str, + get_iface_var: &str, +) { + let sink = meta.sink.as_ref().expect("sink plan"); + let handler_names = sink + .methods + .iter() + .map(|sink_method| sink_method.handler_name.as_str()) + .collect::>(); + out.push_str(&format!( + " /** Describe an apartment-bound COM interface implementation for composition with other generated interfaces. */\n static implementation(handlers) {{\n if (handlers === null || typeof handlers !== 'object' || Array.isArray(handlers)) throw new TypeError('{} implementation handlers must be an object');\n", + meta.name + )); + out.push_str(&format!( + " for (const name of [{}]) {{\n if (typeof handlers[name] !== 'function') throw new TypeError(`${{name}} must be a function`);\n }}\n", + handler_names + .iter() + .map(|name| format!("'{name}'")) + .collect::>() + .join(", ") + )); + out.push_str(" const dispatch = (vtableIndex, ...args) => {\n switch (vtableIndex) {\n"); + for sink_method in &sink.methods { + let method = meta + .methods + .iter() + .find(|method| method.vtable_index == sink_method.vtable_index) + .expect("validated sink method"); + out.push_str(&format!( + " case {}: {{\n const callback = handlers.{};\n", + sink_method.vtable_index, sink_method.handler_name + )); + let callback_args = method + .params + .iter() + .filter(|param| param.surface_input) + .enumerate() + .map(|(index, param)| unwrap_callback_arg_js(¶m.typ, &format!("args[{index}]"))) + .collect::>() + .join(", "); + out.push_str(&format!( + " const result = callback.call(handlers{}{});\n if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously');\n", + if callback_args.is_empty() { "" } else { ", " }, + callback_args + )); + let output_results = method + .results + .iter() + .filter(|result| matches!(result.source, ResultSource::Param(_))) + .collect::>(); + match sink_method.return_convention { + ComSinkReturnConvention::HResult => match sink_method.output_count { + 0 => { + out.push_str(" return result === undefined ? 0 : result;\n") + } + 1 => { + let wrapped = wrap_callback_result_js(method, output_results[0], "value"); + out.push_str(&format!( + " const explicit = Array.isArray(result);\n const hresult = explicit ? result[0] : 0;\n const value = explicit ? result[1] : result;\n return [hresult, {wrapped}];\n" + )); + } + output_count => { + let wrapped = output_results + .iter() + .enumerate() + .map(|(index, result)| { + wrap_callback_result_js(method, result, &format!("values[{index}]")) + }) + .collect::>() + .join(", "); + out.push_str(&format!( + " const explicit = result !== null && typeof result === 'object' && !Array.isArray(result) && 'hresult' in result;\n const hresult = explicit ? result.hresult : 0;\n const values = explicit ? result.values : result;\n if (!Array.isArray(values) || values.length !== {output_count}) throw new TypeError('COM sink handler must return {output_count} output values');\n return [hresult, {wrapped}];\n" + )); + } + }, + ComSinkReturnConvention::SemanticHResult => { + if sink_method.output_count == 0 { + out.push_str(" return result;\n"); + } else { + let wrapped = output_results + .iter() + .enumerate() + .map(|(index, result)| { + wrap_callback_result_js( + method, + result, + &format!("result[{}]", index + 1), + ) + }) + .collect::>() + .join(", "); + let expected = sink_method.output_count + 1; + out.push_str(&format!( + " if (!Array.isArray(result) || result.length !== {expected}) throw new TypeError('COM sink semantic HRESULT handler must return {expected} values');\n return [result[0], {wrapped}];\n" + )); + } + } + ComSinkReturnConvention::Void => match sink_method.output_count { + 0 => out.push_str(" return undefined;\n"), + 1 => { + let wrapped = wrap_callback_result_js(method, output_results[0], "result"); + out.push_str(&format!(" return [{wrapped}];\n")); + } + output_count => { + let wrapped = output_results + .iter() + .enumerate() + .map(|(index, result)| { + wrap_callback_result_js(method, result, &format!("result[{index}]")) + }) + .collect::>() + .join(", "); + out.push_str(&format!( + " if (!Array.isArray(result) || result.length !== {output_count}) throw new TypeError('COM sink void handler must return {output_count} output values');\n return [{wrapped}];\n" + )); + } + }, + ComSinkReturnConvention::Direct => { + let direct = method + .results + .iter() + .find(|result| matches!(result.source, ResultSource::DirectReturn)) + .expect("validated direct sink return"); + if sink_method.output_count == 0 { + let wrapped = wrap_arg_js(&direct.typ, "result"); + out.push_str(&format!(" return {wrapped};\n")); + } else { + let wrapped = method + .results + .iter() + .enumerate() + .map(|(index, result)| { + wrap_callback_result_js(method, result, &format!("result[{index}]")) + }) + .collect::>() + .join(", "); + let expected = sink_method.output_count + 1; + out.push_str(&format!( + " if (!Array.isArray(result) || result.length !== {expected}) throw new TypeError('COM sink direct-return handler must return {expected} values');\n return [{wrapped}];\n" + )); + } + } + } + out.push_str(" }\n"); + } + out.push_str(" default: throw new RangeError(`Unexpected COM sink vtable index ${vtableIndex}`);\n }\n };\n"); + out.push_str(&format!( + " return Object.freeze({{ interfaceType: {}(), iid: '{}', dispatch }});\n }}\n", + get_iface_var, + meta.iid.to_ascii_lowercase() + )); + out.push_str(&format!( + " /** Create an apartment-bound COM object, optionally implementing additional generated interfaces. */\n static implement(handlers, ...additional) {{\n const primary = {}.implementation(handlers);\n if (additional.length === 0) return {}(DynCom.createIUnknownSink(primary.interfaceType, primary.dispatch));\n const implementations = [primary, ...additional];\n const byIid = new Map();\n for (const implementation of implementations) {{\n if (implementation === null || typeof implementation !== 'object' || implementation.interfaceType == null || typeof implementation.iid !== 'string' || typeof implementation.dispatch !== 'function') throw new TypeError('Invalid generated COM implementation descriptor');\n const iid = implementation.iid.toLowerCase();\n if (byIid.has(iid)) throw new TypeError(`Duplicate COM implementation IID ${{implementation.iid}}`);\n byIid.set(iid, implementation);\n }}\n const identity = DynCom.createComObject(implementations.map(implementation => implementation.interfaceType), (iid, vtableIndex, ...args) => {{\n const implementation = byIid.get(iid.toLowerCase());\n if (implementation === undefined) throw new RangeError(`Unexpected COM implementation IID ${{iid}}`);\n return implementation.dispatch(vtableIndex, ...args);\n }});\n try {{\n return {}(identity.cast(IID_{}));\n }} finally {{\n identity.release();\n }}\n }}\n", + meta.name, wrap_owned, wrap_owned, meta.name + )); +} + fn build_method_sig_js(method: &ProjectedComMethod) -> String { let mut parts = Vec::new(); for (index, param) in method.params.iter().enumerate() { @@ -565,6 +752,17 @@ fn wrap_param_arg_js(param: &ProjectedComParam, variable: &str) -> String { wrapped } +fn wrap_callback_result_js( + method: &ProjectedComMethod, + result: &ProjectedComResult, + variable: &str, +) -> String { + match result.source { + ResultSource::Param(index) => wrap_param_arg_js(&method.params[index], variable), + ResultSource::DirectReturn => wrap_arg_js(&result.typ, variable), + } +} + fn param_input_type_dts(param: &ProjectedComParam) -> String { let typ = input_type_dts(¶m.typ); if param.nullable { @@ -1607,8 +1805,13 @@ fn unwrap_dynamic_iid_result_js( fn render_dts(meta: &ProjectedComInterface) -> String { let mut out = String::new(); out.push_str("// Generated by dynwinrt-codegen — do not edit\n"); + let public_types = if meta.sink.is_some() { + "DynComImplementation, WinGuid" + } else { + "WinGuid" + }; out.push_str(&format!( - "import type {{ WinGuid }} from '{}';\n", + "import type {{ {public_types} }} from '{}';\n", com_public_import_name() )); for en in &meta.referenced_enums { @@ -1625,7 +1828,7 @@ fn render_dts(meta: &ProjectedComInterface) -> String { )); } } - if needs_bridge_import(meta) { + if meta.sink.is_some() || needs_bridge_import(meta) { out.push_str(&format!( "import type {{ DynWinRtValue }} from '{}';\n", com_public_import_name() @@ -1714,6 +1917,42 @@ fn render_dts(meta: &ProjectedComInterface) -> String { if !collect_native_unions(meta).is_empty() { out.push('\n'); } + if let Some(sink) = &meta.sink { + out.push_str(&format!( + "export interface {}Implementation {{\n", + meta.name + )); + for sink_method in &sink.methods { + let method = meta + .methods + .iter() + .find(|method| method.vtable_index == sink_method.vtable_index) + .expect("validated sink method"); + let return_type = match sink_method.return_convention { + ComSinkReturnConvention::HResult => match sink_method.output_count { + 0 => "void | number".into(), + 1 => { + let result = dts_return_type(method); + format!("{result} | readonly [hresult: number, value: {result}]") + } + _ => { + let result = dts_return_type(method); + format!("{result} | {{ hresult: number; values: {result} }}") + } + }, + ComSinkReturnConvention::SemanticHResult + | ComSinkReturnConvention::Void + | ComSinkReturnConvention::Direct => dts_return_type(method), + }; + out.push_str(&format!( + " {}: ({}) => {};\n", + sink_method.handler_name, + dts_params(method).join(", "), + return_type + )); + } + out.push_str("}\n\n"); + } out.push_str(&format!( "export declare const IID_{}: WinGuid;\n\nexport declare class {} {{\n", meta.name, meta.name @@ -1732,6 +1971,18 @@ fn render_dts(meta: &ProjectedComInterface) -> String { )), } out.push_str(&format!(" /** Wrap an existing native COM pointer (for QueryInterface bridging). */\n static _fromNative(obj: unknown): {};\n", meta.name)); + if meta.sink.is_some() { + out.push_str(" /** Borrowed native value for passing this implementation to generated COM methods. Do not release it separately. */\n readonly nativeValue: DynWinRtValue;\n"); + out.push_str(" /** Query another generated interface implemented by the same COM identity. */\n as(InterfaceClass: { readonly IID: unknown; _fromNative(obj: unknown): T }): T;\n"); + out.push_str(&format!( + " /** Describe this interface implementation for composition with other generated interfaces. */\n static implementation(handlers: {}Implementation): DynComImplementation;\n", + meta.name + )); + out.push_str(&format!( + " /** Create an apartment-bound COM object, optionally implementing additional generated interfaces. */\n static implement(handlers: {}Implementation, ...additional: DynComImplementation[]): {};\n", + meta.name, meta.name + )); + } out.push_str(" /** Release the underlying native COM reference. Safe to call more than once. */\n release(): void;\n"); let mut emitted_groups = std::collections::HashSet::new(); for method in &meta.methods { diff --git a/tools/dynwinrt-codegen/src/codegen/com/javascript/render_tests.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/render_tests.rs index 4ec90f69..a5122a33 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/javascript/render_tests.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/render_tests.rs @@ -2,7 +2,9 @@ // Licensed under the MIT License. use super::*; -use crate::codegen::com::ir::{ComPrimitive, ComScalarRepr}; +use crate::codegen::com::ir::{ + ComPrimitive, ComScalarRepr, ComSinkPlan, ComSinkReturnConvention, ProjectedComSinkMethod, +}; use crate::codegen::com::javascript::naming::{camel_case, strip_hungarian}; use crate::codegen::com::project::{ project_com_interface_for_test as project_com_interface, project_type_for_test as project_type, @@ -28,10 +30,12 @@ fn renderer_api_accepts_only_projected_ir() { name: "ITest".into(), namespace: "Tests".into(), iid: "00000000-0000-0000-0000-000000000001".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: Vec::new(), activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }; let output = render_com_interface(&projected); assert!(output.js.contains("registerIUnknownInterface")); @@ -56,6 +60,267 @@ fn renderer_api_accepts_only_projected_ir() { assert!(output.js.contains("DynCom.bindComObject")); } +#[test] +fn renderer_serializes_validated_com_sink_plan() { + let interface_param = |name: &str| ProjectedComParam { + name: name.into(), + typ: ComType::ManagedInterface { + iid: "00000000-0000-0000-c000-000000000046".into(), + }, + direction: ComParamDirection::In, + surface_input: true, + surface_result: false, + nullable: false, + }; + let methods = vec![ + ProjectedComMethod { + name: "OnChanged".into(), + camel_name: "onChanged".into(), + vtable_index: 3, + params: vec![interface_param("sender")], + return_convention: ComReturnConvention::HResult, + results: Vec::new(), + string_buffer: None, + typed_buffers: Vec::new(), + shared_counts: Vec::new(), + kind: ProjectedComMethodKind::Normal, + doc: None, + overload: None, + }, + ProjectedComMethod { + name: "OnDecision".into(), + camel_name: "onDecision".into(), + vtable_index: 4, + params: vec![ + interface_param("sender"), + interface_param("item"), + ProjectedComParam { + name: "decision".into(), + typ: ComType::Enum { + namespace: "Tests".into(), + name: "DECISION".into(), + underlying: ComEnumUnderlying::I32, + }, + direction: ComParamDirection::Out, + surface_input: false, + surface_result: true, + nullable: false, + }, + ], + return_convention: ComReturnConvention::HResult, + results: vec![ProjectedComResult { + typ: ComType::Enum { + namespace: "Tests".into(), + name: "DECISION".into(), + underlying: ComEnumUnderlying::I32, + }, + source: ResultSource::Param(2), + conversion: ResultConversion::Value, + }], + string_buffer: None, + typed_buffers: Vec::new(), + shared_counts: Vec::new(), + kind: ProjectedComMethodKind::Normal, + doc: None, + overload: None, + }, + ]; + let projected = ProjectedComInterface { + name: "ITestSink".into(), + namespace: "Tests".into(), + iid: "00000000-0000-0000-0000-000000000001".into(), + base_iids: Vec::new(), + is_iunknown_rooted: true, + methods, + activation: ActivationPlan::None, + referenced_enums: vec![ProjectedComEnum { + namespace: "Tests".into(), + name: "DECISION".into(), + underlying: ComEnumUnderlying::I32, + members: Vec::new(), + }], + sink: Some(ComSinkPlan { + methods: vec![ + ProjectedComSinkMethod { + vtable_index: 3, + handler_name: "onChanged".into(), + return_convention: ComSinkReturnConvention::HResult, + output_count: 0, + }, + ProjectedComSinkMethod { + vtable_index: 4, + handler_name: "onDecision".into(), + return_convention: ComSinkReturnConvention::HResult, + output_count: 1, + }, + ], + }), + }; + + let output = render_com_interface(&projected); + assert!(output.js.contains("static implementation(handlers)")); + assert!( + output + .js + .contains("static implement(handlers, ...additional)") + ); + assert!( + output + .js + .contains("get nativeValue() { return this._obj; }") + ); + assert!( + output + .js + .contains("DynCom.createIUnknownSink(primary.interfaceType, primary.dispatch)") + ); + assert!( + output + .dts + .contains("export interface ITestSinkImplementation") + ); + assert!(output.dts.contains("readonly nativeValue: DynWinRtValue;")); + assert!( + output + .dts + .contains("onChanged: (sender: DynWinRtValue) => void | number;") + ); + assert!(output.dts.contains( + "onDecision: (sender: DynWinRtValue, item: DynWinRtValue) => DECISION | readonly [hresult: number, value: DECISION];" + )); + assert!(output.dts.contains( + "static implementation(handlers: ITestSinkImplementation): DynComImplementation;" + )); + assert!( + output.dts.contains( + "static implement(handlers: ITestSinkImplementation, ...additional: DynComImplementation[]): ITestSink;" + ) + ); +} + +#[test] +fn renderer_serializes_direct_and_void_com_sink_returns() { + let methods = vec![ + ProjectedComMethod { + name: "GetValue".into(), + camel_name: "getValue".into(), + vtable_index: 3, + params: Vec::new(), + return_convention: ComReturnConvention::Direct(ComType::Primitive(ComPrimitive::I32)), + results: vec![ProjectedComResult { + typ: ComType::Primitive(ComPrimitive::I32), + source: ResultSource::DirectReturn, + conversion: ResultConversion::Value, + }], + string_buffer: None, + typed_buffers: Vec::new(), + shared_counts: Vec::new(), + kind: ProjectedComMethodKind::Normal, + doc: None, + overload: None, + }, + ProjectedComMethod { + name: "Notify".into(), + camel_name: "notify".into(), + vtable_index: 4, + params: Vec::new(), + return_convention: ComReturnConvention::Void, + results: Vec::new(), + string_buffer: None, + typed_buffers: Vec::new(), + shared_counts: Vec::new(), + kind: ProjectedComMethodKind::Normal, + doc: None, + overload: None, + }, + ]; + let projected = ProjectedComInterface { + name: "INativeReturnSink".into(), + namespace: "Tests".into(), + iid: "00000000-0000-0000-0000-000000000002".into(), + base_iids: Vec::new(), + is_iunknown_rooted: true, + methods, + activation: ActivationPlan::None, + referenced_enums: Vec::new(), + sink: Some(ComSinkPlan { + methods: vec![ + ProjectedComSinkMethod { + vtable_index: 3, + handler_name: "getValue".into(), + return_convention: ComSinkReturnConvention::Direct, + output_count: 0, + }, + ProjectedComSinkMethod { + vtable_index: 4, + handler_name: "notify".into(), + return_convention: ComSinkReturnConvention::Void, + output_count: 0, + }, + ], + }), + }; + + let output = render_com_interface(&projected); + assert!(output.js.contains("return DynCom.i32(result);")); + assert!(output.js.contains("return undefined;")); + assert!( + output + .dts + .contains("import type { DynWinRtValue } from '@microsoft/dynwinrt/com';") + ); + assert!(output.dts.contains("getValue: () => number;")); + assert!(output.dts.contains("notify: () => void;")); +} + +#[test] +fn callback_results_use_nullable_abi_wrappers() { + assert_eq!( + unwrap_callback_arg_js(&ComType::Bstr, "value"), + "DynCom.copyCallbackBstr(value)" + ); + let method = |typ: ComType, conversion: ResultConversion| ProjectedComMethod { + name: "GetOptional".into(), + camel_name: "getOptional".into(), + vtable_index: 3, + params: vec![ProjectedComParam { + name: "value".into(), + typ: typ.clone(), + direction: ComParamDirection::Out, + surface_input: false, + surface_result: true, + nullable: true, + }], + return_convention: ComReturnConvention::HResult, + results: vec![ProjectedComResult { + typ, + source: ResultSource::Param(0), + conversion, + }], + string_buffer: None, + typed_buffers: Vec::new(), + shared_counts: Vec::new(), + kind: ProjectedComMethodKind::Normal, + doc: None, + overload: None, + }; + let bstr = method(ComType::Bstr, ResultConversion::Bstr); + assert_eq!( + wrap_callback_result_js(&bstr, &bstr.results[0], "value"), + "value === null ? DynCom.nullBstr() : DynCom.bstr(value)" + ); + let interface = method( + ComType::ManagedInterface { + iid: "00000000-0000-0000-c000-000000000046".into(), + }, + ResultConversion::ManagedCom, + ); + assert_eq!( + wrap_callback_result_js(&interface, &interface.results[0], "value"), + "value === null ? DynCom.nullComValue() : value" + ); +} + #[test] fn renderer_projects_borrowed_hwnd_output_as_numeric_handle() { let hwnd = ComType::PointerAlias { @@ -67,6 +332,7 @@ fn renderer_projects_borrowed_hwnd_output_as_numeric_handle() { name: "IWindowOwner".into(), namespace: "Tests".into(), iid: "00000000-0000-0000-0000-000000000001".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: vec![ProjectedComMethod { name: "GetWindow".into(), @@ -95,6 +361,7 @@ fn renderer_projects_borrowed_hwnd_output_as_numeric_handle() { }], activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }; let output = render_com_interface(&projected); @@ -137,6 +404,7 @@ fn canonical_iunknown_arrays_use_managed_values_without_nominal_wrappers() { name: "IUnknownArray".into(), namespace: "Tests".into(), iid: "00000000-0000-0000-0000-000000000001".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: vec![ProjectedComMethod { name: "GetValues".into(), @@ -161,6 +429,7 @@ fn canonical_iunknown_arrays_use_managed_values_without_nominal_wrappers() { }], activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }; let output = render_com_interface(&projected); assert!( @@ -176,6 +445,7 @@ fn renderer_projects_bstr_replacement_as_a_string_roundtrip() { name: "IReplaceText".into(), namespace: "Tests".into(), iid: "00000000-0000-0000-0000-000000000001".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: vec![ProjectedComMethod { name: "Replace".into(), @@ -204,6 +474,7 @@ fn renderer_projects_bstr_replacement_as_a_string_roundtrip() { }], activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }; let output = render_com_interface(&projected); @@ -225,6 +496,7 @@ fn renderer_allows_null_only_for_nullable_bstr_inputs() { name: "IOptionalText".into(), namespace: "Tests".into(), iid: "00000000-0000-0000-0000-000000000001".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: vec![ProjectedComMethod { name: "SetOptional".into(), @@ -249,6 +521,7 @@ fn renderer_allows_null_only_for_nullable_bstr_inputs() { }], activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }; let output = render_com_interface(&projected); @@ -362,10 +635,12 @@ fn renderer_keeps_dynamic_iid_native_order_and_all_results() { name: "IResolve".into(), namespace: "Tests".into(), iid: "00000000-0000-0000-0000-000000000010".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: vec![method], activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }); assert!(output.js.contains( @@ -394,6 +669,7 @@ fn renderer_emits_distinct_by_value_variant_inputs() { name: "IVariantInput".into(), namespace: "Tests".into(), iid: "00000000-0000-0000-0000-000000000002".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: vec![ProjectedComMethod { name: "UseVariant".into(), @@ -418,6 +694,7 @@ fn renderer_emits_distinct_by_value_variant_inputs() { }], activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }; let output = render_com_interface(&projected); @@ -444,6 +721,7 @@ fn typed_buffer_scalar_aliases_are_collected_for_declarations() { name: "ITest".into(), namespace: "Tests".into(), iid: "00000000-0000-0000-0000-000000000001".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: vec![ProjectedComMethod { name: "Resolve".into(), @@ -470,6 +748,7 @@ fn typed_buffer_scalar_aliases_are_collected_for_declarations() { }], activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }; assert_eq!( @@ -487,6 +766,7 @@ fn renderer_serializes_fixed_capacity_bytes_from_projected_ir() { name: "ITestBlob".into(), namespace: "Tests".into(), iid: "00000000-0000-0000-0000-000000000001".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: vec![ProjectedComMethod { name: "GetBlob".into(), @@ -552,6 +832,7 @@ fn renderer_serializes_fixed_capacity_bytes_from_projected_ir() { }], activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }; let output = render_com_interface(&projected); @@ -579,6 +860,7 @@ fn parallel_arrays_use_semantic_element_counts_and_guid_conversion() { name: "IParallel".into(), namespace: "Tests".into(), iid: "00000000-0000-0000-0000-000000000010".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: vec![ProjectedComMethod { name: "Copy".into(), @@ -655,6 +937,7 @@ fn parallel_arrays_use_semantic_element_counts_and_guid_conversion() { }], activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }; let output = render_com_interface(&projected); @@ -751,6 +1034,7 @@ fn renderer_emits_tagged_unions_and_automation_runtime_transfers() { name: "IAutomationTest".into(), namespace: "Tests".into(), iid: "00000000-0000-0000-0000-000000000009".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: vec![ method( @@ -781,6 +1065,7 @@ fn renderer_emits_tagged_unions_and_automation_runtime_transfers() { ], activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }; let output = render_com_interface(&projected); @@ -855,10 +1140,12 @@ fn renderer_emits_explicit_idispatch_invoke_options_and_compound_types() { name: "IDispatch".into(), namespace: "Windows.Win32.System.Com".into(), iid: "00020400-0000-0000-c000-000000000046".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: vec![method], activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }); assert!(output.js.contains(".addIn(DynCom.dispatchParamsType())")); @@ -903,10 +1190,12 @@ fn coclass_renderer_uses_new_and_runtime_query_interface_views() { name: "ITest4".into(), namespace: "Tests".into(), iid: "00000000-0000-0000-0000-000000000004".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: Vec::new(), activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }; let coclass = ProjectedComCoclass { name: "Test".into(), @@ -1602,6 +1891,7 @@ fn interop_generation_fails_when_target_iid_unresolvable() { base_offset: 3, is_iunknown_rooted: true, base_chain: vec!["IUnknown".into()], + base_iids: Vec::new(), coclass_clsid: None, coclass_name: None, own_methods_start: 3, @@ -1656,6 +1946,7 @@ fn non_interop_iunknown_interface_still_generates_without_winmd_lookup() { base_offset: 3, is_iunknown_rooted: true, base_chain: vec!["IUnknown".into()], + base_iids: Vec::new(), coclass_clsid: None, coclass_name: None, own_methods_start: 3, @@ -1688,6 +1979,7 @@ fn plain_iface_with_method(m: MethodMeta) -> crate::com_metadata::ComInterfaceMe base_offset: 3, is_iunknown_rooted: true, base_chain: vec!["IUnknown".into()], + base_iids: Vec::new(), coclass_clsid: None, coclass_name: None, own_methods_start: 3, diff --git a/tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs index 013adc10..ac631d77 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs @@ -376,6 +376,41 @@ pub(super) fn unwrap_result_js(result: &ProjectedComResult, expression: &str) -> } } +pub(super) fn unwrap_callback_arg_js(typ: &ComType, expression: &str) -> String { + match typ { + ComType::Bstr => format!("DynCom.copyCallbackBstr({expression})"), + ComType::GuidPointer => format!("DynCom.copyCallbackGuid({expression})"), + ComType::NativePod { layout } => format!( + "create{}(DynCom.nativeStructBytes({}, {expression}).bytes)", + layout.name, + native_pod_layout_js(layout) + ), + ComType::NativePodPointer { layout } => format!( + "{expression}.isNull() ? null : create{}(DynCom.nativeStructBytes({}, {expression}).bytes)", + layout.name, + native_pod_layout_js(layout) + ), + ComType::TypedBuffer { element } => match element.as_ref() { + ComType::NativePod { layout } => { + format!( + "create{}Array(DynCom.takeBuffer({expression}))", + layout.name + ) + } + _ => format!("DynCom.takeBuffer({expression})"), + }, + ComType::PointerAlias { + kind: PointerAliasKind::StringPointer(StringEncoding::Wide), + .. + } => format!("DynCom.copyCallbackWideString({expression})"), + ComType::PointerAlias { + kind: PointerAliasKind::StringPointer(StringEncoding::Ansi), + .. + } => format!("DynCom.copyCallbackAnsiString({expression})"), + _ => unwrap_value_js(typ, expression), + } +} + fn unwrap_value_js(typ: &ComType, expression: &str) -> String { match typ { ComType::Primitive(primitive) => match primitive { diff --git a/tools/dynwinrt-codegen/src/codegen/com/project/legacy_diagnostics.rs b/tools/dynwinrt-codegen/src/codegen/com/project/legacy_diagnostics.rs index 2a1aec58..50eefbb0 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/project/legacy_diagnostics.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/project/legacy_diagnostics.rs @@ -78,10 +78,12 @@ fn project_legacy_interface( name: meta.interface.name.clone(), namespace: meta.interface.namespace.clone(), iid: meta.interface.iid.clone(), + base_iids: meta.base_iids.clone(), is_iunknown_rooted: meta.is_iunknown_rooted, methods, activation, referenced_enums, + sink: None, }; Ok(projected) } @@ -851,6 +853,7 @@ mod tests { base_offset: 3, is_iunknown_rooted: true, base_chain: vec!["IUnknown".into()], + base_iids: Vec::new(), coclass_clsid: None, coclass_name: None, own_methods_start: 3, diff --git a/tools/dynwinrt-codegen/src/codegen/com/project/mod.rs b/tools/dynwinrt-codegen/src/codegen/com/project/mod.rs index b5614efa..763b567f 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/project/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/project/mod.rs @@ -9,15 +9,16 @@ use crate::com_metadata::{ComCoclassMeta, ComInterfaceMeta}; use super::ir::{ ActivationPlan, BufferCountUnit as ProjectedBufferCountUnit, ComEnumUnderlying, - ComParamDirection, ComPrimitive, ComReturnConvention, ComScalarRepr, ComType, DispatchShape, - NativePodArchitectureLayout, NativePodField, NativePodFieldType, NativePodInitializer, - NativePodLayout, NativePodScalar, NativeUnionArchitectureLayout, NativeUnionField, - NativeUnionFieldType, NativeUnionLayout, OverloadDispatch, OverloadInfo, PointerAliasKind, - ProjectedComCoclass, ProjectedComEnum, ProjectedComEnumMember, ProjectedComInterface, - ProjectedComMethod, ProjectedComMethodKind, ProjectedComParam, ProjectedComResult, - ProjectedEnumValue, ProjectedInterfaceRef, ResultConversion, ResultSource, SafeArrayElement, - SharedCountPlan, StringBufferPlan, StringEncoding, TypedBufferPlan, TypedBufferRelation, - TypedBufferSizing, dispatch_shape, + ComParamDirection, ComPrimitive, ComReturnConvention, ComScalarRepr, ComSinkPlan, + ComSinkReturnConvention, ComType, DispatchShape, NativePodArchitectureLayout, NativePodField, + NativePodFieldType, NativePodInitializer, NativePodLayout, NativePodScalar, + NativeUnionArchitectureLayout, NativeUnionField, NativeUnionFieldType, NativeUnionLayout, + OverloadDispatch, OverloadInfo, PointerAliasKind, ProjectedComCoclass, ProjectedComEnum, + ProjectedComEnumMember, ProjectedComInterface, ProjectedComMethod, ProjectedComMethodKind, + ProjectedComParam, ProjectedComResult, ProjectedComSinkMethod, ProjectedEnumValue, + ProjectedInterfaceRef, ResultConversion, ResultSource, SafeArrayElement, SharedCountPlan, + StringBufferPlan, StringEncoding, TypedBufferPlan, TypedBufferRelation, TypedBufferSizing, + dispatch_shape, }; use super::javascript::naming::camel_case; use super::model::ValidatedComInterface; @@ -62,10 +63,12 @@ pub(super) fn project_com_reference_interface( name: meta.interface.name.clone(), namespace: meta.interface.namespace.clone(), iid: meta.interface.iid.clone(), + base_iids: meta.base_iids.clone(), is_iunknown_rooted: meta.is_iunknown_rooted, methods: Vec::new(), activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }; validate_projected_surface_names(&projected)?; Ok(projected) @@ -137,20 +140,251 @@ fn project_validated_interface( }) }) .collect::, String>>()?; + let sink = project_sink_plan(meta, &methods); let projected = ProjectedComInterface { name: meta.interface.name.clone(), namespace: meta.interface.namespace.clone(), iid: format_guid(semantic.iid().as_bytes()), + base_iids: meta.base_iids.clone(), is_iunknown_rooted: semantic.is_iunknown_rooted(), methods, activation, referenced_enums, + sink, }; validate_projected_surface_names(&projected)?; Ok(projected) } +fn project_sink_plan( + meta: &ComInterfaceMeta, + methods: &[ProjectedComMethod], +) -> Option { + let expected_base_iids = meta + .base_chain + .iter() + .filter(|name| name.as_str() != "IUnknown" && name.as_str() != "IInspectable") + .count(); + if !meta.is_iunknown_rooted || meta.base_iids.len() != expected_base_iids || methods.is_empty() + { + return None; + } + + let methods = methods + .iter() + .enumerate() + .map(|(index, method)| { + if method.vtable_index != index + 3 + || method.kind != ProjectedComMethodKind::Normal + || method.string_buffer.is_some() + || method + .typed_buffers + .iter() + .any(|buffer| !callback_buffer_supported(buffer)) + || !method.shared_counts.is_empty() + { + return None; + } + let return_convention = match &method.return_convention { + ComReturnConvention::HResult => ComSinkReturnConvention::HResult, + ComReturnConvention::SemanticHResult => ComSinkReturnConvention::SemanticHResult, + ComReturnConvention::Void => ComSinkReturnConvention::Void, + ComReturnConvention::Direct(typ) if callback_output_type_supported(typ) => { + ComSinkReturnConvention::Direct + } + ComReturnConvention::Direct(_) | ComReturnConvention::DispatchInvokeHResult => { + return None; + } + }; + let hidden_params = method + .typed_buffers + .iter() + .flat_map(|buffer| match buffer.relation { + TypedBufferRelation::Input { + count_param_index, + actual_length_param_index, + .. + } => [Some(count_param_index), actual_length_param_index], + TypedBufferRelation::CallerOutput { + actual_length_param_index, + .. + } => [actual_length_param_index, None], + TypedBufferRelation::EnumeratorNext { + fetched_param_index, + .. + } => [Some(fetched_param_index), None], + TypedBufferRelation::CalleeAllocated { + count_param_index, .. + } => [Some(count_param_index), None], + }) + .flatten() + .collect::>(); + if method.params.iter().enumerate().any(|(index, param)| { + if hidden_params.contains(&index) && !param.surface_input && !param.surface_result { + return false; + } + match param.direction { + ComParamDirection::In => { + !param.surface_input + || param.surface_result + || !callback_input_type_supported(¶m.typ) + } + ComParamDirection::Out => { + param.surface_input + || !param.surface_result + || !callback_output_type_supported(¶m.typ) + } + ComParamDirection::InOut => { + !param.surface_input + || !param.surface_result + || !callback_input_type_supported(¶m.typ) + || !callback_inout_type_supported(¶m.typ) + } + ComParamDirection::InputBuffer => { + !param.surface_input + || param.surface_result + || !matches!(param.typ, ComType::TypedBuffer { .. }) + } + ComParamDirection::CallerOutputBuffer => { + param.surface_input + || !param.surface_result + || !matches!(param.typ, ComType::TypedBuffer { .. }) + } + ComParamDirection::CalleeAllocatedBuffer => { + param.surface_input + || !param.surface_result + || !matches!(param.typ, ComType::TypedBuffer { .. }) + } + _ => true, + } + }) || method + .results + .iter() + .any(|result| !callback_result_supported(result)) + { + return None; + } + + fn callback_buffer_supported(buffer: &TypedBufferPlan) -> bool { + matches!( + buffer.relation, + TypedBufferRelation::Input { + actual_length_param_index: None, + .. + } | TypedBufferRelation::CallerOutput { + sizing: TypedBufferSizing::FixedCapacity, + .. + } | TypedBufferRelation::CalleeAllocated { .. } + ) && matches!( + &buffer.element, + ComType::Primitive(_) + | ComType::Guid + | ComType::Enum { .. } + | ComType::ScalarAlias { .. } + | ComType::NativePod { .. } + ) + } + Some(ProjectedComSinkMethod { + vtable_index: method.vtable_index, + handler_name: method.overload.as_ref().map_or_else( + || method.camel_name.clone(), + |overload| overload.impl_name.trim_start_matches('_').to_string(), + ), + return_convention, + output_count: method + .results + .iter() + .filter(|result| matches!(result.source, ResultSource::Param(_))) + .count(), + }) + }) + .collect::>>()?; + Some(ComSinkPlan { methods }) +} + +fn callback_input_type_supported(typ: &ComType) -> bool { + matches!( + typ, + ComType::Primitive(_) + | ComType::NativeIsize + | ComType::NativeUsize + | ComType::Win32Bool + | ComType::HResult + | ComType::Guid + | ComType::GuidPointer + | ComType::HString + | ComType::Enum { .. } + | ComType::ScalarAlias { .. } + | ComType::Bstr + | ComType::ManagedInterface { .. } + | ComType::NativePod { .. } + | ComType::NativePodPointer { .. } + | ComType::PointerAlias { + kind: PointerAliasKind::HandleValue | PointerAliasKind::StringPointer(_), + .. + } + ) +} + +fn callback_output_type_supported(typ: &ComType) -> bool { + matches!( + typ, + ComType::Primitive(_) + | ComType::NativeIsize + | ComType::NativeUsize + | ComType::Win32Bool + | ComType::HResult + | ComType::Guid + | ComType::HString + | ComType::Enum { .. } + | ComType::ScalarAlias { .. } + | ComType::Bstr + | ComType::ManagedInterface { .. } + | ComType::NativePod { .. } + | ComType::PointerAlias { + kind: PointerAliasKind::HandleValue, + .. + } + ) +} + +fn callback_inout_type_supported(typ: &ComType) -> bool { + matches!( + typ, + ComType::Primitive(_) + | ComType::NativeIsize + | ComType::NativeUsize + | ComType::Win32Bool + | ComType::HResult + | ComType::Guid + | ComType::Enum { .. } + | ComType::ScalarAlias { .. } + | ComType::Bstr + | ComType::NativePod { .. } + | ComType::PointerAlias { + kind: PointerAliasKind::HandleValue, + .. + } + ) +} + +fn callback_result_supported(result: &ProjectedComResult) -> bool { + (callback_output_type_supported(&result.typ) + && matches!( + result.conversion, + ResultConversion::Value + | ResultConversion::BorrowedHandle + | ResultConversion::ManagedCom + | ResultConversion::Bstr + )) + || (matches!(result.typ, ComType::TypedBuffer { .. }) + && matches!( + result.conversion, + ResultConversion::Buffer | ResultConversion::PlainArray + )) +} + fn validate_projected_surface_names(meta: &ProjectedComInterface) -> Result<(), String> { let mut names = std::collections::BTreeMap::::new(); insert_surface_name( @@ -158,6 +392,13 @@ fn validate_projected_surface_names(meta: &ProjectedComInterface) -> Result<(), &meta.name, format!("interface {}.{}", meta.namespace, meta.name), )?; + if meta.sink.is_some() { + insert_surface_name( + &mut names, + &format!("{}Implementation", meta.name), + format!("implementation {}.{}", meta.namespace, meta.name), + )?; + } for definition in &meta.referenced_enums { insert_surface_name( &mut names, @@ -2739,6 +2980,141 @@ mod tests { ); } + #[test] + fn sink_plan_accepts_refguid_handle_and_callee_allocated_buffer_contracts() { + let meta = ComInterfaceMeta { + interface: crate::com_metadata::InterfaceMeta::default(), + base_offset: 3, + is_iunknown_rooted: true, + base_chain: vec!["IUnknown".into()], + base_iids: Vec::new(), + coclass_clsid: None, + coclass_name: None, + own_methods_start: 3, + referenced_enums: Vec::new(), + raw_referenced_enums: None, + raw_methods: None, + }; + let handle = ComType::PointerAlias { + namespace: "Windows.Win32.Foundation".into(), + name: "HANDLE".into(), + kind: PointerAliasKind::HandleValue, + }; + let buffer = ComType::TypedBuffer { + element: Box::new(ComType::Primitive(ComPrimitive::U8)), + }; + let method = ProjectedComMethod { + name: "Invoke".into(), + camel_name: "invoke".into(), + vtable_index: 3, + params: vec![ + ProjectedComParam { + name: "riid".into(), + typ: ComType::GuidPointer, + direction: ComParamDirection::In, + surface_input: true, + surface_result: false, + nullable: false, + }, + ProjectedComParam { + name: "handle".into(), + typ: handle.clone(), + direction: ComParamDirection::Out, + surface_input: false, + surface_result: true, + nullable: false, + }, + ProjectedComParam { + name: "buffer".into(), + typ: buffer.clone(), + direction: ComParamDirection::CalleeAllocatedBuffer, + surface_input: false, + surface_result: true, + nullable: false, + }, + ProjectedComParam { + name: "count".into(), + typ: ComType::Primitive(ComPrimitive::U32), + direction: ComParamDirection::Out, + surface_input: false, + surface_result: false, + nullable: false, + }, + ], + return_convention: ComReturnConvention::HResult, + results: vec![ + ProjectedComResult { + typ: handle, + source: ResultSource::Param(1), + conversion: ResultConversion::BorrowedHandle, + }, + ProjectedComResult { + typ: buffer, + source: ResultSource::Param(2), + conversion: ResultConversion::Buffer, + }, + ], + string_buffer: None, + typed_buffers: vec![TypedBufferPlan { + buffer_param_index: 2, + element: ComType::Primitive(ComPrimitive::U8), + relation: TypedBufferRelation::CalleeAllocated { + count_param_index: 3, + unit: ProjectedBufferCountUnit::Elements, + }, + }], + shared_counts: Vec::new(), + kind: ProjectedComMethodKind::Normal, + doc: None, + overload: Some(OverloadInfo { + public_name: "invoke".into(), + impl_name: "_invoke_3".into(), + dispatch: OverloadDispatch::Arity, + }), + }; + + let plan = project_sink_plan(&meta, std::slice::from_ref(&method)) + .expect("supported callback sink"); + assert_eq!(plan.methods[0].output_count, 2); + assert_eq!(plan.methods[0].handler_name, "invoke_3"); + + let mut incomplete_base = meta; + incomplete_base.base_chain = vec!["IExternalBase".into(), "IUnknown".into()]; + assert!(project_sink_plan(&incomplete_base, &[method]).is_none()); + } + + #[test] + fn real_metadata_derived_sink_registers_every_base_iid() { + let Some(winmd) = std::env::var("DYNWINRT_WIN32_WINMD") + .ok() + .filter(|path| std::path::Path::new(path).exists()) + else { + return; + }; + let interfaces = + crate::com_metadata::parse_all_com_interfaces(&winmd).expect("parse Win32 metadata"); + let (meta, output) = interfaces + .iter() + .filter(|meta| meta.is_iunknown_rooted && !meta.base_iids.is_empty()) + .find_map(|meta| { + crate::codegen::com::generate_com_interface_files(meta, &winmd) + .ok() + .filter(|output| output.js.contains("static implementation(handlers)")) + .map(|output| (meta, output)) + }) + .expect("Win32 metadata should contain a supported derived callback interface"); + for iid in &meta.base_iids { + assert!( + output + .js + .contains(&format!(".addBaseInterface(WinGuid.parse('{iid}'))")), + "{}.{} omitted base IID {iid}", + meta.interface.namespace, + meta.interface.name + ); + } + } + #[test] fn validated_semantic_projection_only_diverges_for_pod_upgrades() { let Some(winmd) = std::env::var("DYNWINRT_WIN32_WINMD") @@ -2802,6 +3178,7 @@ mod tests { "SetThumbnailClip" | "ThumbBarAddButtons" | "ThumbBarUpdateButtons" ) }); + semantic.sink = None; assert_eq!(semantic, legacy); } @@ -3018,6 +3395,7 @@ mod tests { name: "ITest".into(), namespace: "Tests".into(), iid: "00000000-0000-0000-c000-000000000046".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: vec![ method("first", pod("Contoso.One"), 3), @@ -3025,6 +3403,7 @@ mod tests { ], activation: ActivationPlan::None, referenced_enums: Vec::new(), + sink: None, }; let error = validate_projected_surface_names(&interface).unwrap_err(); @@ -3150,6 +3529,7 @@ mod tests { name: "ITest".into(), namespace: "Tests".into(), iid: "00000000-0000-0000-c000-000000000046".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: Vec::new(), activation: ActivationPlan::None, @@ -3167,6 +3547,7 @@ mod tests { members: Vec::new(), }, ], + sink: None, }; assert!(validate_projected_surface_names(&interface).is_err()); @@ -3205,6 +3586,7 @@ mod tests { name: name.into(), namespace: "Tests".into(), iid: "00000000-0000-0000-c000-000000000046".into(), + base_iids: Vec::new(), is_iunknown_rooted: true, methods: Vec::new(), activation: ActivationPlan::None, @@ -3214,6 +3596,7 @@ mod tests { underlying: ComEnumUnderlying::I32, members: Vec::new(), }], + sink: None, }; assert!( validate_coclass_enum_files( diff --git a/tools/dynwinrt-codegen/src/com_metadata.rs b/tools/dynwinrt-codegen/src/com_metadata.rs index b117f62e..ae6f48a4 100644 --- a/tools/dynwinrt-codegen/src/com_metadata.rs +++ b/tools/dynwinrt-codegen/src/com_metadata.rs @@ -355,6 +355,7 @@ pub struct ComInterfaceMeta { pub base_offset: usize, pub is_iunknown_rooted: bool, pub base_chain: Vec, + pub base_iids: Vec, pub coclass_clsid: Option, pub coclass_name: Option, pub own_methods_start: usize, @@ -495,6 +496,15 @@ fn parse_com_interface_from_index( .filter(|(_, name, _)| name != "IUnknown" && name != "IInspectable") .map(|(_, _, count)| count) .sum::(); + let base_iids = base_chain + .iter() + .filter(|(_, name, _)| name != "IUnknown" && name != "IInspectable") + .filter_map(|(namespace, name, _)| { + let definition = index.get(namespace, name).next()?; + let iid = crate::meta::extract_iid(&definition); + (!iid.is_empty()).then_some(iid) + }) + .collect::>(); let mut methods = Vec::new(); let mut raw_methods = Vec::new(); @@ -538,6 +548,7 @@ fn parse_com_interface_from_index( base_offset: root_offset, is_iunknown_rooted, base_chain: base_chain.into_iter().map(|(_, name, _)| name).collect(), + base_iids, coclass_clsid, coclass_name, own_methods_start, @@ -3078,6 +3089,7 @@ mod tests { base_offset: 3, is_iunknown_rooted: true, base_chain: base_chain.iter().map(|name| (*name).into()).collect(), + base_iids: Vec::new(), coclass_clsid: None, coclass_name: None, own_methods_start: 3, diff --git a/tools/dynwinrt-codegen/tests/com_sink_tsc_check_test.rs b/tools/dynwinrt-codegen/tests/com_sink_tsc_check_test.rs new file mode 100644 index 00000000..7e90f36b --- /dev/null +++ b/tools/dynwinrt-codegen/tests/com_sink_tsc_check_test.rs @@ -0,0 +1,212 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::fs; +use std::path::Path; +use std::process::Command; + +#[test] +fn generated_com_sink_dts_passes_tsc_no_emit() { + let winmd = std::env::var("DYNWINRT_WIN32_WINMD") + .unwrap_or_else(|_| r"C:\s\win32metadata\Windows.Win32.winmd".into()); + if !Path::new(&winmd).exists() { + eprintln!("Skipping: Windows.Win32.winmd not found"); + return; + } + let tsc = + Path::new(env!("CARGO_MANIFEST_DIR")).join(r"..\..\bindings\js\node_modules\.bin\tsc.cmd"); + let tsc_check = Command::new("cmd") + .arg("/c") + .arg(&tsc) + .arg("--version") + .output(); + if !matches!(tsc_check, Ok(output) if output.status.success()) { + eprintln!("Skipping: repository TypeScript compiler not available"); + return; + } + + let exe = env!("CARGO_BIN_EXE_dynwinrt-codegen"); + let tmp = std::env::temp_dir().join(format!( + "dynwinrt-codegen-com-sink-tsc-{}", + std::process::id() + )); + let _ = fs::remove_dir_all(&tmp); + fs::create_dir_all(&tmp).expect("create COM sink temp dir"); + let status = Command::new(exe) + .args([ + "generate", + "--winmd", + &winmd, + "--namespace", + "Windows.Win32.UI.Shell", + "--class-name", + "FileOpenDialog,IFileDialogEvents,TaskbarList", + "--output", + ]) + .arg(&tmp) + .status() + .expect("spawn COM sink codegen"); + assert!(status.success(), "COM sink codegen failed: {status:?}"); + let status = Command::new(exe) + .args([ + "generate", + "--winmd", + &winmd, + "--class-name", + "Windows.Win32.System.Ole.IDropTarget", + "--output", + ]) + .arg(&tmp) + .status() + .expect("spawn IDropTarget codegen"); + assert!( + status.success(), + "IDropTarget sink codegen failed: {status:?}" + ); + + let com_dir = tmp.join("com"); + fs::write( + com_dir.join("sink-usage.ts"), + r#"import { FileOpenDialog } from "./FileOpenDialog.js"; +import { + DROPEFFECT, + FDE_OVERWRITE_RESPONSE, + FDE_SHAREVIOLATION_RESPONSE, + IFileDialogEvents, + MODIFIERKEYS_FLAGS, +} from "./index.js"; +import type { IFileDialogEventsImplementation } from "./IFileDialogEvents.js"; +import { + createPOINTL, + IDropTarget, + type IDropTargetImplementation, + type POINTL, +} from "./IDropTarget.js"; + +const dialog = new FileOpenDialog(); +const eventHandlers: IFileDialogEventsImplementation = { + onFileOk(fileDialog) { + fileDialog.isNull(); + return 0; + }, + onFolderChanging() {}, + onFolderChange() {}, + onSelectionChange() {}, + onShareViolation(fileDialog, item) { + fileDialog.isNull(); + item.isNull(); + return FDE_SHAREVIOLATION_RESPONSE.FDESVR_REFUSE; + }, + onTypeChange() {}, + onOverwrite() { + return FDE_OVERWRITE_RESPONSE.FDEOR_DEFAULT; + }, +}; +const events = IFileDialogEvents.implement(eventHandlers); +const cookie = dialog.advise(events.nativeValue); +dialog.unadvise(cookie); +events.release(); +dialog.release(); + +const dropHandlers: IDropTargetImplementation = { + dragEnter(dataObject, keyState, point, effect) { + dataObject.isNull(); + const values: [MODIFIERKEYS_FLAGS, POINTL, DROPEFFECT] = [keyState, point, effect]; + return values[2]; + }, + dragOver(keyState, point, effect) { + const values: [MODIFIERKEYS_FLAGS, POINTL, DROPEFFECT] = [keyState, point, effect]; + return values[2]; + }, + dragLeave() {}, + drop(dataObject, keyState, point, effect) { + dataObject.isNull(); + const values: [MODIFIERKEYS_FLAGS, POINTL, DROPEFFECT] = [keyState, point, effect]; + return values[2]; + }, +}; +const dropTarget = IDropTarget.implement(dropHandlers); +declare const point: ReturnType; +const effect = dropTarget.dragOver( + MODIFIERKEYS_FLAGS.MK_CONTROL, + point, + DROPEFFECT.DROPEFFECT_COPY, +); +effect satisfies DROPEFFECT; +dropTarget.release(); +const composed = IDropTarget.implement( + dropHandlers, + IFileDialogEvents.implementation(eventHandlers), +); +const composedEvents = composed.as(IFileDialogEvents); +composedEvents.release(); +composed.release(); +"#, + ) + .expect("write COM sink usage"); + fs::write( + tmp.join("globals.d.ts"), + "declare class Buffer extends Uint8Array {}\n", + ) + .expect("write Buffer stub"); + fs::write( + tmp.join("tsconfig.json"), + r#"{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "strict": true, + "noEmit": true, + "skipLibCheck": false, + "types": [] + }, + "include": ["globals.d.ts", "com/*.d.ts", "com/*.ts"] +}"#, + ) + .expect("write COM sink tsconfig"); + + let package = tmp.join("node_modules").join("@microsoft").join("dynwinrt"); + fs::create_dir_all(&package).expect("create COM runtime stub"); + fs::write( + package.join("package.json"), + r#"{ + "name": "@microsoft/dynwinrt", + "version": "0.0.0", + "exports": { + "./com": { + "types": "./com.d.ts" + } + } +}"#, + ) + .expect("write COM runtime package stub"); + fs::write( + package.join("com.d.ts"), + r#"export declare class WinGuid {} +export interface DynComImplementation {} +export declare class DynWinRtValue { + isNull(): boolean; +} +export declare class DynComNativeStruct {} +export declare class DynComNativeStructArray {} +"#, + ) + .expect("write COM runtime declarations"); + + let output = Command::new("cmd") + .arg("/c") + .arg(&tsc) + .args(["--noEmit", "-p"]) + .arg(tmp.join("tsconfig.json")) + .current_dir(&tmp) + .output() + .expect("spawn COM sink tsc"); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let _ = fs::remove_dir_all(&tmp); + assert!( + output.status.success(), + "tsc --noEmit failed on generated COM sink declarations!\nstdout:\n{stdout}\nstderr:\n{stderr}" + ); +} diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList.d.ts b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList.d.ts index 1270d6de..fa5af054 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList.d.ts +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList.d.ts @@ -1,9 +1,18 @@ // Generated by dynwinrt-codegen — do not edit -import type { WinGuid } from '@microsoft/dynwinrt/com'; +import type { DynComImplementation, WinGuid } from '@microsoft/dynwinrt/com'; +import type { DynWinRtValue } from '@microsoft/dynwinrt/com'; /** Opaque Win32 handle value. Pass a raw pointer value as a `bigint` (full pointer width) or `number` (safe integer). */ export type HWND = bigint | number; +export interface ITaskbarListImplementation { + hrInit: () => void | number; + addTab: (hwnd: HWND | Buffer | Uint8Array) => void | number; + deleteTab: (hwnd: HWND | Buffer | Uint8Array) => void | number; + activateTab: (hwnd: HWND | Buffer | Uint8Array) => void | number; + setActiveAlt: (hwnd: HWND | Buffer | Uint8Array) => void | number; +} + export declare const IID_ITaskbarList: WinGuid; export declare class ITaskbarList { @@ -11,6 +20,14 @@ export declare class ITaskbarList { protected constructor(obj: unknown); /** Wrap an existing native COM pointer (for QueryInterface bridging). */ static _fromNative(obj: unknown): ITaskbarList; + /** Borrowed native value for passing this implementation to generated COM methods. Do not release it separately. */ + readonly nativeValue: DynWinRtValue; + /** Query another generated interface implemented by the same COM identity. */ + as(InterfaceClass: { readonly IID: unknown; _fromNative(obj: unknown): T }): T; + /** Describe this interface implementation for composition with other generated interfaces. */ + static implementation(handlers: ITaskbarListImplementation): DynComImplementation; + /** Create an apartment-bound COM object, optionally implementing additional generated interfaces. */ + static implement(handlers: ITaskbarListImplementation, ...additional: DynComImplementation[]): ITaskbarList; /** Release the underlying native COM reference. Safe to call more than once. */ release(): void; /** @see {@link https://learn.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist-hrinit} */ diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList.js b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList.js index 590c8245..dbb7bf72 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList.js +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList.js @@ -4,16 +4,20 @@ const { DynCom, DynComMethodSig, WinGuid } = require('@microsoft/dynwinrt/com/un const IID_ITaskbarList = WinGuid.parse('56fdf342-fd6d-11d0-958a-006097c9a090'); let _ITaskbarListCache; -const _ITaskbarList = new Proxy({}, { - get(_target, prop) { - _ITaskbarListCache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.ITaskbarList', IID_ITaskbarList) +const _getITaskbarList = () => { + _ITaskbarListCache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.ITaskbarList', IID_ITaskbarList) .addMethodAt(3, 'HrInit', new DynComMethodSig()) .addMethodAt(4, 'AddTab', new DynComMethodSig().addIn(DynCom.pointerType())) .addMethodAt(5, 'DeleteTab', new DynComMethodSig().addIn(DynCom.pointerType())) .addMethodAt(6, 'ActivateTab', new DynComMethodSig().addIn(DynCom.pointerType())) .addMethodAt(7, 'SetActiveAlt', new DynComMethodSig().addIn(DynCom.pointerType())); - const value = _ITaskbarListCache[prop]; - return typeof value === 'function' ? value.bind(_ITaskbarListCache) : value; + return _ITaskbarListCache; +}; +const _ITaskbarList = new Proxy({}, { + get(_target, prop) { + const iface = _getITaskbarList(); + const value = iface[prop]; + return typeof value === 'function' ? value.bind(iface) : value; }, }); @@ -26,6 +30,76 @@ class ITaskbarList { this._obj = cast; } static _fromNative(obj) { return _wrapITaskbarListOwned(obj.cast(IID_ITaskbarList)); } + /** Borrowed native value for passing this implementation to generated COM methods. Do not release it separately. */ + get nativeValue() { return this._obj; } + /** Query another generated interface implemented by the same COM identity. */ + as(InterfaceClass) { return InterfaceClass._fromNative(this._obj); } + /** Describe an apartment-bound COM interface implementation for composition with other generated interfaces. */ + static implementation(handlers) { + if (handlers === null || typeof handlers !== 'object' || Array.isArray(handlers)) throw new TypeError('ITaskbarList implementation handlers must be an object'); + for (const name of ['hrInit', 'addTab', 'deleteTab', 'activateTab', 'setActiveAlt']) { + if (typeof handlers[name] !== 'function') throw new TypeError(`${name} must be a function`); + } + const dispatch = (vtableIndex, ...args) => { + switch (vtableIndex) { + case 3: { + const callback = handlers.hrInit; + const result = callback.call(handlers); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 4: { + const callback = handlers.addTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 5: { + const callback = handlers.deleteTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 6: { + const callback = handlers.activateTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 7: { + const callback = handlers.setActiveAlt; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + default: throw new RangeError(`Unexpected COM sink vtable index ${vtableIndex}`); + } + }; + return Object.freeze({ interfaceType: _getITaskbarList(), iid: '56fdf342-fd6d-11d0-958a-006097c9a090', dispatch }); + } + /** Create an apartment-bound COM object, optionally implementing additional generated interfaces. */ + static implement(handlers, ...additional) { + const primary = ITaskbarList.implementation(handlers); + if (additional.length === 0) return _wrapITaskbarListOwned(DynCom.createIUnknownSink(primary.interfaceType, primary.dispatch)); + const implementations = [primary, ...additional]; + const byIid = new Map(); + for (const implementation of implementations) { + if (implementation === null || typeof implementation !== 'object' || implementation.interfaceType == null || typeof implementation.iid !== 'string' || typeof implementation.dispatch !== 'function') throw new TypeError('Invalid generated COM implementation descriptor'); + const iid = implementation.iid.toLowerCase(); + if (byIid.has(iid)) throw new TypeError(`Duplicate COM implementation IID ${implementation.iid}`); + byIid.set(iid, implementation); + } + const identity = DynCom.createComObject(implementations.map(implementation => implementation.interfaceType), (iid, vtableIndex, ...args) => { + const implementation = byIid.get(iid.toLowerCase()); + if (implementation === undefined) throw new RangeError(`Unexpected COM implementation IID ${iid}`); + return implementation.dispatch(vtableIndex, ...args); + }); + try { + return _wrapITaskbarListOwned(identity.cast(IID_ITaskbarList)); + } finally { + identity.release(); + } + } /** Release the underlying native COM reference. Safe to call more than once. */ release() { this._obj.release(); diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList2.d.ts b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList2.d.ts index 9d9d9901..c8ad4b23 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList2.d.ts +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList2.d.ts @@ -1,9 +1,19 @@ // Generated by dynwinrt-codegen — do not edit -import type { WinGuid } from '@microsoft/dynwinrt/com'; +import type { DynComImplementation, WinGuid } from '@microsoft/dynwinrt/com'; +import type { DynWinRtValue } from '@microsoft/dynwinrt/com'; /** Opaque Win32 handle value. Pass a raw pointer value as a `bigint` (full pointer width) or `number` (safe integer). */ export type HWND = bigint | number; +export interface ITaskbarList2Implementation { + hrInit: () => void | number; + addTab: (hwnd: HWND | Buffer | Uint8Array) => void | number; + deleteTab: (hwnd: HWND | Buffer | Uint8Array) => void | number; + activateTab: (hwnd: HWND | Buffer | Uint8Array) => void | number; + setActiveAlt: (hwnd: HWND | Buffer | Uint8Array) => void | number; + markFullscreenWindow: (hwnd: HWND | Buffer | Uint8Array, fFullscreen: boolean) => void | number; +} + export declare const IID_ITaskbarList2: WinGuid; export declare class ITaskbarList2 { @@ -11,6 +21,14 @@ export declare class ITaskbarList2 { protected constructor(obj: unknown); /** Wrap an existing native COM pointer (for QueryInterface bridging). */ static _fromNative(obj: unknown): ITaskbarList2; + /** Borrowed native value for passing this implementation to generated COM methods. Do not release it separately. */ + readonly nativeValue: DynWinRtValue; + /** Query another generated interface implemented by the same COM identity. */ + as(InterfaceClass: { readonly IID: unknown; _fromNative(obj: unknown): T }): T; + /** Describe this interface implementation for composition with other generated interfaces. */ + static implementation(handlers: ITaskbarList2Implementation): DynComImplementation; + /** Create an apartment-bound COM object, optionally implementing additional generated interfaces. */ + static implement(handlers: ITaskbarList2Implementation, ...additional: DynComImplementation[]): ITaskbarList2; /** Release the underlying native COM reference. Safe to call more than once. */ release(): void; /** @see {@link https://learn.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist-hrinit} */ diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList2.js b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList2.js index 51e3b6bb..acaee1ff 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList2.js +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList2.js @@ -4,17 +4,22 @@ const { DynCom, DynComMethodSig, WinGuid } = require('@microsoft/dynwinrt/com/un const IID_ITaskbarList2 = WinGuid.parse('602d4995-b13a-429b-a66e-1935e44f4317'); let _ITaskbarList2Cache; -const _ITaskbarList2 = new Proxy({}, { - get(_target, prop) { - _ITaskbarList2Cache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.ITaskbarList2', IID_ITaskbarList2) +const _getITaskbarList2 = () => { + _ITaskbarList2Cache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.ITaskbarList2', IID_ITaskbarList2) + .addBaseInterface(WinGuid.parse('56fdf342-fd6d-11d0-958a-006097c9a090')) .addMethodAt(3, 'HrInit', new DynComMethodSig()) .addMethodAt(4, 'AddTab', new DynComMethodSig().addIn(DynCom.pointerType())) .addMethodAt(5, 'DeleteTab', new DynComMethodSig().addIn(DynCom.pointerType())) .addMethodAt(6, 'ActivateTab', new DynComMethodSig().addIn(DynCom.pointerType())) .addMethodAt(7, 'SetActiveAlt', new DynComMethodSig().addIn(DynCom.pointerType())) .addMethodAt(8, 'MarkFullscreenWindow', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())); - const value = _ITaskbarList2Cache[prop]; - return typeof value === 'function' ? value.bind(_ITaskbarList2Cache) : value; + return _ITaskbarList2Cache; +}; +const _ITaskbarList2 = new Proxy({}, { + get(_target, prop) { + const iface = _getITaskbarList2(); + const value = iface[prop]; + return typeof value === 'function' ? value.bind(iface) : value; }, }); @@ -27,6 +32,82 @@ class ITaskbarList2 { this._obj = cast; } static _fromNative(obj) { return _wrapITaskbarList2Owned(obj.cast(IID_ITaskbarList2)); } + /** Borrowed native value for passing this implementation to generated COM methods. Do not release it separately. */ + get nativeValue() { return this._obj; } + /** Query another generated interface implemented by the same COM identity. */ + as(InterfaceClass) { return InterfaceClass._fromNative(this._obj); } + /** Describe an apartment-bound COM interface implementation for composition with other generated interfaces. */ + static implementation(handlers) { + if (handlers === null || typeof handlers !== 'object' || Array.isArray(handlers)) throw new TypeError('ITaskbarList2 implementation handlers must be an object'); + for (const name of ['hrInit', 'addTab', 'deleteTab', 'activateTab', 'setActiveAlt', 'markFullscreenWindow']) { + if (typeof handlers[name] !== 'function') throw new TypeError(`${name} must be a function`); + } + const dispatch = (vtableIndex, ...args) => { + switch (vtableIndex) { + case 3: { + const callback = handlers.hrInit; + const result = callback.call(handlers); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 4: { + const callback = handlers.addTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 5: { + const callback = handlers.deleteTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 6: { + const callback = handlers.activateTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 7: { + const callback = handlers.setActiveAlt; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 8: { + const callback = handlers.markFullscreenWindow; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), (DynCom.toNumber(args[1]) !== 0)); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + default: throw new RangeError(`Unexpected COM sink vtable index ${vtableIndex}`); + } + }; + return Object.freeze({ interfaceType: _getITaskbarList2(), iid: '602d4995-b13a-429b-a66e-1935e44f4317', dispatch }); + } + /** Create an apartment-bound COM object, optionally implementing additional generated interfaces. */ + static implement(handlers, ...additional) { + const primary = ITaskbarList2.implementation(handlers); + if (additional.length === 0) return _wrapITaskbarList2Owned(DynCom.createIUnknownSink(primary.interfaceType, primary.dispatch)); + const implementations = [primary, ...additional]; + const byIid = new Map(); + for (const implementation of implementations) { + if (implementation === null || typeof implementation !== 'object' || implementation.interfaceType == null || typeof implementation.iid !== 'string' || typeof implementation.dispatch !== 'function') throw new TypeError('Invalid generated COM implementation descriptor'); + const iid = implementation.iid.toLowerCase(); + if (byIid.has(iid)) throw new TypeError(`Duplicate COM implementation IID ${implementation.iid}`); + byIid.set(iid, implementation); + } + const identity = DynCom.createComObject(implementations.map(implementation => implementation.interfaceType), (iid, vtableIndex, ...args) => { + const implementation = byIid.get(iid.toLowerCase()); + if (implementation === undefined) throw new RangeError(`Unexpected COM implementation IID ${iid}`); + return implementation.dispatch(vtableIndex, ...args); + }); + try { + return _wrapITaskbarList2Owned(identity.cast(IID_ITaskbarList2)); + } finally { + identity.release(); + } + } /** Release the underlying native COM reference. Safe to call more than once. */ release() { this._obj.release(); diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts index 54ee4e2f..5163789e 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts @@ -1,6 +1,7 @@ // Generated by dynwinrt-codegen — do not edit -import type { WinGuid } from '@microsoft/dynwinrt/com'; +import type { DynComImplementation, WinGuid } from '@microsoft/dynwinrt/com'; import { TBPFLAG } from './TBPFLAG.js'; +import type { DynWinRtValue } from '@microsoft/dynwinrt/com'; import type { DynComNativeStruct, DynComNativeStructArray } from '@microsoft/dynwinrt/com'; /** Opaque Win32 handle value. Pass a raw pointer value as a `bigint` (full pointer width) or `number` (safe integer). */ @@ -21,6 +22,27 @@ export declare function createTHUMBBUTTON(bytes?: Buffer): THUMBBUTTON; export type THUMBBUTTONArray = DynComNativeStructArray & { readonly __dynComNativeStructArrayLayout: 'Windows.Win32.UI.Shell.THUMBBUTTON' }; export declare function createTHUMBBUTTONArray(bytes: Buffer): THUMBBUTTONArray; +export interface ITaskbarList3Implementation { + hrInit: () => void | number; + addTab: (hwnd: HWND | Buffer | Uint8Array) => void | number; + deleteTab: (hwnd: HWND | Buffer | Uint8Array) => void | number; + activateTab: (hwnd: HWND | Buffer | Uint8Array) => void | number; + setActiveAlt: (hwnd: HWND | Buffer | Uint8Array) => void | number; + markFullscreenWindow: (hwnd: HWND | Buffer | Uint8Array, fFullscreen: boolean) => void | number; + setProgressValue: (hwnd: HWND | Buffer | Uint8Array, ullCompleted: bigint, ullTotal: bigint) => void | number; + setProgressState: (hwnd: HWND | Buffer | Uint8Array, tbpFlags: TBPFLAG) => void | number; + registerTab: (tab: HWND | Buffer | Uint8Array, mdi: HWND | Buffer | Uint8Array) => void | number; + unregisterTab: (tab: HWND | Buffer | Uint8Array) => void | number; + setTabOrder: (tab: HWND | Buffer | Uint8Array, insertBefore: HWND | Buffer | Uint8Array) => void | number; + setTabActive: (tab: HWND | Buffer | Uint8Array, mdi: HWND | Buffer | Uint8Array, reserved: number) => void | number; + thumbBarAddButtons: (hwnd: HWND | Buffer | Uint8Array, pButton: THUMBBUTTONArray) => void | number; + thumbBarUpdateButtons: (hwnd: HWND | Buffer | Uint8Array, pButton: THUMBBUTTONArray) => void | number; + thumbBarSetImageList: (hwnd: HWND | Buffer | Uint8Array, himl: HIMAGELIST) => void | number; + setOverlayIcon: (hwnd: HWND | Buffer | Uint8Array, hIcon: HICON, description: PWSTR) => void | number; + setThumbnailTooltip: (hwnd: HWND | Buffer | Uint8Array, tip: PWSTR) => void | number; + setThumbnailClip: (hwnd: HWND | Buffer | Uint8Array, prcClip: RECT | null) => void | number; +} + export declare const IID_ITaskbarList3: WinGuid; export declare class ITaskbarList3 { @@ -28,6 +50,14 @@ export declare class ITaskbarList3 { protected constructor(obj: unknown); /** Wrap an existing native COM pointer (for QueryInterface bridging). */ static _fromNative(obj: unknown): ITaskbarList3; + /** Borrowed native value for passing this implementation to generated COM methods. Do not release it separately. */ + readonly nativeValue: DynWinRtValue; + /** Query another generated interface implemented by the same COM identity. */ + as(InterfaceClass: { readonly IID: unknown; _fromNative(obj: unknown): T }): T; + /** Describe this interface implementation for composition with other generated interfaces. */ + static implementation(handlers: ITaskbarList3Implementation): DynComImplementation; + /** Create an apartment-bound COM object, optionally implementing additional generated interfaces. */ + static implement(handlers: ITaskbarList3Implementation, ...additional: DynComImplementation[]): ITaskbarList3; /** Release the underlying native COM reference. Safe to call more than once. */ release(): void; /** @see {@link https://learn.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist-hrinit} */ diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js index 63a32ac9..861c8950 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js @@ -11,9 +11,10 @@ function createTHUMBBUTTONArray(bytes) { return DynCom.createNativeStructArray(_ 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) +const _getITaskbarList3 = () => { + _ITaskbarList3Cache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.ITaskbarList3', IID_ITaskbarList3) + .addBaseInterface(WinGuid.parse('602d4995-b13a-429b-a66e-1935e44f4317')) + .addBaseInterface(WinGuid.parse('56fdf342-fd6d-11d0-958a-006097c9a090')) .addMethodAt(3, 'HrInit', new DynComMethodSig()) .addMethodAt(4, 'AddTab', new DynComMethodSig().addIn(DynCom.pointerType())) .addMethodAt(5, 'DeleteTab', new DynComMethodSig().addIn(DynCom.pointerType())) @@ -32,8 +33,13 @@ const _ITaskbarList3 = new Proxy({}, { .addMethodAt(18, 'SetOverlayIcon', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) .addMethodAt(19, 'SetThumbnailTooltip', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) .addMethodAt(20, 'SetThumbnailClip', new DynComMethodSig().addIn(DynCom.pointerType()).addNullableIn(DynCom.nativeStructPointerType(_nativeLayout_RECT, true))); - const value = _ITaskbarList3Cache[prop]; - return typeof value === 'function' ? value.bind(_ITaskbarList3Cache) : value; + return _ITaskbarList3Cache; +}; +const _ITaskbarList3 = new Proxy({}, { + get(_target, prop) { + const iface = _getITaskbarList3(); + const value = iface[prop]; + return typeof value === 'function' ? value.bind(iface) : value; }, }); @@ -46,6 +52,154 @@ class ITaskbarList3 { this._obj = cast; } static _fromNative(obj) { return _wrapITaskbarList3Owned(obj.cast(IID_ITaskbarList3)); } + /** Borrowed native value for passing this implementation to generated COM methods. Do not release it separately. */ + get nativeValue() { return this._obj; } + /** Query another generated interface implemented by the same COM identity. */ + as(InterfaceClass) { return InterfaceClass._fromNative(this._obj); } + /** Describe an apartment-bound COM interface implementation for composition with other generated interfaces. */ + static implementation(handlers) { + if (handlers === null || typeof handlers !== 'object' || Array.isArray(handlers)) throw new TypeError('ITaskbarList3 implementation handlers must be an object'); + for (const name of ['hrInit', 'addTab', 'deleteTab', 'activateTab', 'setActiveAlt', 'markFullscreenWindow', 'setProgressValue', 'setProgressState', 'registerTab', 'unregisterTab', 'setTabOrder', 'setTabActive', 'thumbBarAddButtons', 'thumbBarUpdateButtons', 'thumbBarSetImageList', 'setOverlayIcon', 'setThumbnailTooltip', 'setThumbnailClip']) { + if (typeof handlers[name] !== 'function') throw new TypeError(`${name} must be a function`); + } + const dispatch = (vtableIndex, ...args) => { + switch (vtableIndex) { + case 3: { + const callback = handlers.hrInit; + const result = callback.call(handlers); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 4: { + const callback = handlers.addTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 5: { + const callback = handlers.deleteTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 6: { + const callback = handlers.activateTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 7: { + const callback = handlers.setActiveAlt; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 8: { + const callback = handlers.markFullscreenWindow; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), (DynCom.toNumber(args[1]) !== 0)); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 9: { + const callback = handlers.setProgressValue; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.toU64Bigint(args[1]), DynCom.toU64Bigint(args[2])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 10: { + const callback = handlers.setProgressState; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.toNumber(args[1])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 11: { + const callback = handlers.registerTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.asPointerBigint(args[1])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 12: { + const callback = handlers.unregisterTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 13: { + const callback = handlers.setTabOrder; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.asPointerBigint(args[1])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 14: { + const callback = handlers.setTabActive; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.asPointerBigint(args[1]), DynCom.toU32(args[2])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 15: { + const callback = handlers.thumbBarAddButtons; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), createTHUMBBUTTONArray(DynCom.takeBuffer(args[1]))); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 16: { + const callback = handlers.thumbBarUpdateButtons; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), createTHUMBBUTTONArray(DynCom.takeBuffer(args[1]))); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 17: { + const callback = handlers.thumbBarSetImageList; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.asPointerBigint(args[1])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 18: { + const callback = handlers.setOverlayIcon; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.asPointerBigint(args[1]), DynCom.copyCallbackWideString(args[2])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 19: { + const callback = handlers.setThumbnailTooltip; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.copyCallbackWideString(args[1])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 20: { + const callback = handlers.setThumbnailClip; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), args[1].isNull() ? null : createRECT(DynCom.nativeStructBytes(_nativeLayout_RECT, args[1]).bytes)); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + default: throw new RangeError(`Unexpected COM sink vtable index ${vtableIndex}`); + } + }; + return Object.freeze({ interfaceType: _getITaskbarList3(), iid: 'ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf', dispatch }); + } + /** Create an apartment-bound COM object, optionally implementing additional generated interfaces. */ + static implement(handlers, ...additional) { + const primary = ITaskbarList3.implementation(handlers); + if (additional.length === 0) return _wrapITaskbarList3Owned(DynCom.createIUnknownSink(primary.interfaceType, primary.dispatch)); + const implementations = [primary, ...additional]; + const byIid = new Map(); + for (const implementation of implementations) { + if (implementation === null || typeof implementation !== 'object' || implementation.interfaceType == null || typeof implementation.iid !== 'string' || typeof implementation.dispatch !== 'function') throw new TypeError('Invalid generated COM implementation descriptor'); + const iid = implementation.iid.toLowerCase(); + if (byIid.has(iid)) throw new TypeError(`Duplicate COM implementation IID ${implementation.iid}`); + byIid.set(iid, implementation); + } + const identity = DynCom.createComObject(implementations.map(implementation => implementation.interfaceType), (iid, vtableIndex, ...args) => { + const implementation = byIid.get(iid.toLowerCase()); + if (implementation === undefined) throw new RangeError(`Unexpected COM implementation IID ${iid}`); + return implementation.dispatch(vtableIndex, ...args); + }); + try { + return _wrapITaskbarList3Owned(identity.cast(IID_ITaskbarList3)); + } finally { + identity.release(); + } + } /** Release the underlying native COM reference. Safe to call more than once. */ release() { this._obj.release(); diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList4.d.ts b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList4.d.ts index a26e2cab..582961a8 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList4.d.ts +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList4.d.ts @@ -1,7 +1,8 @@ // Generated by dynwinrt-codegen — do not edit -import type { WinGuid } from '@microsoft/dynwinrt/com'; +import type { DynComImplementation, WinGuid } from '@microsoft/dynwinrt/com'; import { TBPFLAG } from './TBPFLAG.js'; import { STPFLAG } from './STPFLAG.js'; +import type { DynWinRtValue } from '@microsoft/dynwinrt/com'; import type { DynComNativeStruct, DynComNativeStructArray } from '@microsoft/dynwinrt/com'; /** Opaque Win32 handle value. Pass a raw pointer value as a `bigint` (full pointer width) or `number` (safe integer). */ @@ -22,6 +23,28 @@ export declare function createTHUMBBUTTON(bytes?: Buffer): THUMBBUTTON; export type THUMBBUTTONArray = DynComNativeStructArray & { readonly __dynComNativeStructArrayLayout: 'Windows.Win32.UI.Shell.THUMBBUTTON' }; export declare function createTHUMBBUTTONArray(bytes: Buffer): THUMBBUTTONArray; +export interface ITaskbarList4Implementation { + hrInit: () => void | number; + addTab: (hwnd: HWND | Buffer | Uint8Array) => void | number; + deleteTab: (hwnd: HWND | Buffer | Uint8Array) => void | number; + activateTab: (hwnd: HWND | Buffer | Uint8Array) => void | number; + setActiveAlt: (hwnd: HWND | Buffer | Uint8Array) => void | number; + markFullscreenWindow: (hwnd: HWND | Buffer | Uint8Array, fFullscreen: boolean) => void | number; + setProgressValue: (hwnd: HWND | Buffer | Uint8Array, ullCompleted: bigint, ullTotal: bigint) => void | number; + setProgressState: (hwnd: HWND | Buffer | Uint8Array, tbpFlags: TBPFLAG) => void | number; + registerTab: (tab: HWND | Buffer | Uint8Array, mdi: HWND | Buffer | Uint8Array) => void | number; + unregisterTab: (tab: HWND | Buffer | Uint8Array) => void | number; + setTabOrder: (tab: HWND | Buffer | Uint8Array, insertBefore: HWND | Buffer | Uint8Array) => void | number; + setTabActive: (tab: HWND | Buffer | Uint8Array, mdi: HWND | Buffer | Uint8Array, reserved: number) => void | number; + thumbBarAddButtons: (hwnd: HWND | Buffer | Uint8Array, pButton: THUMBBUTTONArray) => void | number; + thumbBarUpdateButtons: (hwnd: HWND | Buffer | Uint8Array, pButton: THUMBBUTTONArray) => void | number; + thumbBarSetImageList: (hwnd: HWND | Buffer | Uint8Array, himl: HIMAGELIST) => void | number; + setOverlayIcon: (hwnd: HWND | Buffer | Uint8Array, hIcon: HICON, description: PWSTR) => void | number; + setThumbnailTooltip: (hwnd: HWND | Buffer | Uint8Array, tip: PWSTR) => void | number; + setThumbnailClip: (hwnd: HWND | Buffer | Uint8Array, prcClip: RECT | null) => void | number; + setTabProperties: (tab: HWND | Buffer | Uint8Array, stpFlags: STPFLAG) => void | number; +} + export declare const IID_ITaskbarList4: WinGuid; export declare class ITaskbarList4 { @@ -29,6 +52,14 @@ export declare class ITaskbarList4 { protected constructor(obj: unknown); /** Wrap an existing native COM pointer (for QueryInterface bridging). */ static _fromNative(obj: unknown): ITaskbarList4; + /** Borrowed native value for passing this implementation to generated COM methods. Do not release it separately. */ + readonly nativeValue: DynWinRtValue; + /** Query another generated interface implemented by the same COM identity. */ + as(InterfaceClass: { readonly IID: unknown; _fromNative(obj: unknown): T }): T; + /** Describe this interface implementation for composition with other generated interfaces. */ + static implementation(handlers: ITaskbarList4Implementation): DynComImplementation; + /** Create an apartment-bound COM object, optionally implementing additional generated interfaces. */ + static implement(handlers: ITaskbarList4Implementation, ...additional: DynComImplementation[]): ITaskbarList4; /** Release the underlying native COM reference. Safe to call more than once. */ release(): void; /** @see {@link https://learn.microsoft.com/windows/win32/api/shobjidl_core/nf-shobjidl_core-itaskbarlist-hrinit} */ diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList4.js b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList4.js index 97bfcbc4..97b671cc 100644 --- a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList4.js +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList4.js @@ -12,9 +12,11 @@ function createTHUMBBUTTONArray(bytes) { return DynCom.createNativeStructArray(_ const IID_ITaskbarList4 = WinGuid.parse('c43dc798-95d1-4bea-9030-bb99e2983a1a'); let _ITaskbarList4Cache; -const _ITaskbarList4 = new Proxy({}, { - get(_target, prop) { - _ITaskbarList4Cache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.ITaskbarList4', IID_ITaskbarList4) +const _getITaskbarList4 = () => { + _ITaskbarList4Cache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.ITaskbarList4', IID_ITaskbarList4) + .addBaseInterface(WinGuid.parse('ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf')) + .addBaseInterface(WinGuid.parse('602d4995-b13a-429b-a66e-1935e44f4317')) + .addBaseInterface(WinGuid.parse('56fdf342-fd6d-11d0-958a-006097c9a090')) .addMethodAt(3, 'HrInit', new DynComMethodSig()) .addMethodAt(4, 'AddTab', new DynComMethodSig().addIn(DynCom.pointerType())) .addMethodAt(5, 'DeleteTab', new DynComMethodSig().addIn(DynCom.pointerType())) @@ -34,8 +36,13 @@ const _ITaskbarList4 = new Proxy({}, { .addMethodAt(19, 'SetThumbnailTooltip', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) .addMethodAt(20, 'SetThumbnailClip', new DynComMethodSig().addIn(DynCom.pointerType()).addNullableIn(DynCom.nativeStructPointerType(_nativeLayout_RECT, true))) .addMethodAt(21, 'SetTabProperties', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())); - const value = _ITaskbarList4Cache[prop]; - return typeof value === 'function' ? value.bind(_ITaskbarList4Cache) : value; + return _ITaskbarList4Cache; +}; +const _ITaskbarList4 = new Proxy({}, { + get(_target, prop) { + const iface = _getITaskbarList4(); + const value = iface[prop]; + return typeof value === 'function' ? value.bind(iface) : value; }, }); @@ -48,6 +55,160 @@ class ITaskbarList4 { this._obj = cast; } static _fromNative(obj) { return _wrapITaskbarList4Owned(obj.cast(IID_ITaskbarList4)); } + /** Borrowed native value for passing this implementation to generated COM methods. Do not release it separately. */ + get nativeValue() { return this._obj; } + /** Query another generated interface implemented by the same COM identity. */ + as(InterfaceClass) { return InterfaceClass._fromNative(this._obj); } + /** Describe an apartment-bound COM interface implementation for composition with other generated interfaces. */ + static implementation(handlers) { + if (handlers === null || typeof handlers !== 'object' || Array.isArray(handlers)) throw new TypeError('ITaskbarList4 implementation handlers must be an object'); + for (const name of ['hrInit', 'addTab', 'deleteTab', 'activateTab', 'setActiveAlt', 'markFullscreenWindow', 'setProgressValue', 'setProgressState', 'registerTab', 'unregisterTab', 'setTabOrder', 'setTabActive', 'thumbBarAddButtons', 'thumbBarUpdateButtons', 'thumbBarSetImageList', 'setOverlayIcon', 'setThumbnailTooltip', 'setThumbnailClip', 'setTabProperties']) { + if (typeof handlers[name] !== 'function') throw new TypeError(`${name} must be a function`); + } + const dispatch = (vtableIndex, ...args) => { + switch (vtableIndex) { + case 3: { + const callback = handlers.hrInit; + const result = callback.call(handlers); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 4: { + const callback = handlers.addTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 5: { + const callback = handlers.deleteTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 6: { + const callback = handlers.activateTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 7: { + const callback = handlers.setActiveAlt; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 8: { + const callback = handlers.markFullscreenWindow; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), (DynCom.toNumber(args[1]) !== 0)); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 9: { + const callback = handlers.setProgressValue; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.toU64Bigint(args[1]), DynCom.toU64Bigint(args[2])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 10: { + const callback = handlers.setProgressState; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.toNumber(args[1])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 11: { + const callback = handlers.registerTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.asPointerBigint(args[1])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 12: { + const callback = handlers.unregisterTab; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 13: { + const callback = handlers.setTabOrder; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.asPointerBigint(args[1])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 14: { + const callback = handlers.setTabActive; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.asPointerBigint(args[1]), DynCom.toU32(args[2])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 15: { + const callback = handlers.thumbBarAddButtons; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), createTHUMBBUTTONArray(DynCom.takeBuffer(args[1]))); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 16: { + const callback = handlers.thumbBarUpdateButtons; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), createTHUMBBUTTONArray(DynCom.takeBuffer(args[1]))); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 17: { + const callback = handlers.thumbBarSetImageList; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.asPointerBigint(args[1])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 18: { + const callback = handlers.setOverlayIcon; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.asPointerBigint(args[1]), DynCom.copyCallbackWideString(args[2])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 19: { + const callback = handlers.setThumbnailTooltip; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.copyCallbackWideString(args[1])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 20: { + const callback = handlers.setThumbnailClip; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), args[1].isNull() ? null : createRECT(DynCom.nativeStructBytes(_nativeLayout_RECT, args[1]).bytes)); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + case 21: { + const callback = handlers.setTabProperties; + const result = callback.call(handlers, DynCom.asPointerBigint(args[0]), DynCom.toNumber(args[1])); + if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') throw new TypeError('COM implementation handlers must return synchronously'); + return result === undefined ? 0 : result; + } + default: throw new RangeError(`Unexpected COM sink vtable index ${vtableIndex}`); + } + }; + return Object.freeze({ interfaceType: _getITaskbarList4(), iid: 'c43dc798-95d1-4bea-9030-bb99e2983a1a', dispatch }); + } + /** Create an apartment-bound COM object, optionally implementing additional generated interfaces. */ + static implement(handlers, ...additional) { + const primary = ITaskbarList4.implementation(handlers); + if (additional.length === 0) return _wrapITaskbarList4Owned(DynCom.createIUnknownSink(primary.interfaceType, primary.dispatch)); + const implementations = [primary, ...additional]; + const byIid = new Map(); + for (const implementation of implementations) { + if (implementation === null || typeof implementation !== 'object' || implementation.interfaceType == null || typeof implementation.iid !== 'string' || typeof implementation.dispatch !== 'function') throw new TypeError('Invalid generated COM implementation descriptor'); + const iid = implementation.iid.toLowerCase(); + if (byIid.has(iid)) throw new TypeError(`Duplicate COM implementation IID ${implementation.iid}`); + byIid.set(iid, implementation); + } + const identity = DynCom.createComObject(implementations.map(implementation => implementation.interfaceType), (iid, vtableIndex, ...args) => { + const implementation = byIid.get(iid.toLowerCase()); + if (implementation === undefined) throw new RangeError(`Unexpected COM implementation IID ${iid}`); + return implementation.dispatch(vtableIndex, ...args); + }); + try { + return _wrapITaskbarList4Owned(identity.cast(IID_ITaskbarList4)); + } finally { + identity.release(); + } + } /** Release the underlying native COM reference. Safe to call more than once. */ release() { this._obj.release(); diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs index 929b1a51..cc8f9525 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -83,6 +83,60 @@ fn required_win32_metadata_is_present() { } } +#[test] +fn file_dialog_events_and_drop_target_project_safe_javascript_sinks() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let events = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IFileDialogEvents", + ) + .expect("IFileDialogEvents must exist"); + assert_eq!(events.base_chain, ["IUnknown"]); + assert_eq!(events.own_methods_start, 3); + let output = com::generate_com_interface_files(&events, &win32_winmd()).unwrap(); + assert!(output.js.contains("static implementation(handlers)")); + assert!( + output + .js + .contains("DynCom.createIUnknownSink(primary.interfaceType, primary.dispatch)") + ); + assert!(!output.js.contains("interface_in1")); + assert!( + output + .dts + .contains("export interface IFileDialogEventsImplementation") + ); + assert!(output.dts.contains( + "static implement(handlers: IFileDialogEventsImplementation, ...additional: DynComImplementation[]): IFileDialogEvents;" + )); + + let dialog = + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "IFileDialog") + .expect("IFileDialog must exist"); + assert_ne!(dialog.base_chain, ["IUnknown"]); + let output = com::generate_com_interface_files(&dialog, &win32_winmd()).unwrap(); + assert!(!output.js.contains("static implementation(handlers)")); + assert!(!output.dts.contains("IFileDialogImplementation")); + + let drop_target = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.Ole", + "IDropTarget", + ) + .expect("IDropTarget must exist"); + assert_eq!(drop_target.base_chain, ["IUnknown"]); + let output = com::generate_com_interface_files(&drop_target, &win32_winmd()).unwrap(); + assert!( + output.js.contains("static implementation(handlers)"), + "validated POD/scalar/InOut callback shapes should use the dynamic sink backend" + ); +} + #[test] fn real_metadata_preserves_automation_pointer_contracts_and_fails_closed() { if !win32_available() {