diff --git a/.github/agents/e2e-test.md b/.github/agents/e2e-test.md index 73a45eb5..c0a982d5 100644 --- a/.github/agents/e2e-test.md +++ b/.github/agents/e2e-test.md @@ -1,6 +1,6 @@ --- name: e2e-test -description: Run end-to-end tests for dynwinrt code generation and WinRT API invocation +description: Run end-to-end tests for dynwinrt code generation and WinRT/Classic COM API invocation tools: - powershell - view @@ -21,6 +21,9 @@ You run and manage the dynwinrt end-to-end test suite. # Python only .\tests\e2e_test.ps1 -SkipBuild -Lang py +# Classic COM only (requires Windows.Win32.winmd) +.\tests\e2e_test.ps1 -SkipBuild -Lang com + # Full build + test .\tests\e2e_test.ps1 ``` @@ -40,5 +43,5 @@ Avoid APIs that need WinAppSDK, network, or user interaction. ## Diagnosing failures 1. Check `tests/e2e_generated/results_py.json` or `results_ts.json` for structured failure details -2. Inspect generated code in `tests/e2e_generated/python_bindings/` or `ts/` +2. Inspect generated code in `tests/e2e_generated/python_bindings/`, `ts/`, or `com/` 3. Common issues: circular imports in codegen, naming mismatch (Python snake_case vs TS camelCase) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 3242f6b8..20048eb5 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -33,7 +33,7 @@ python -m pytest tests/ -v # JS binding (requires Node.js 18+) cd bindings/js npm install -npx napi build --no-const-enum --platform --release -o dist +npm run build # Code generation (JS + .d.ts is the default; --lang py for Python) cargo run -p dynwinrt-codegen -- generate --namespace Windows.Foundation --class-name Uri --output ./generated @@ -53,9 +53,9 @@ The E2E test framework validates the full pipeline: reading .winmd metadata → - `instantiate`: how to create an instance (`activate`, `static_factory`, or `none`) - `checks`: array of assertions (`property_equals`, `property_exists`, `method_equals`, `method_result_contains`, `static_equals`, `static_not_null`) -2. **Runners** (`tests/runners/py_runner.py`, `tests/runners/ts_runner.ts`) read the specs and execute them, outputting `results.json`. +2. **Runners** (`tests/runners/py_runner.py`, `tests/runners/ts_runner.ts`, and `tests/runners/com/*.mjs`) execute generated WinRT and Classic COM bindings. -3. **Orchestrator** (`tests/e2e_test.ps1`) handles build, code generation, and runner invocation. +3. **Orchestrator** (`tests/e2e_test.ps1`) handles build, temporary code generation, and runner invocation. Use `-Lang com` for the Classic COM suite; it requires `DYNWINRT_WIN32_WINMD` or an installed `Microsoft.Windows.SDK.Win32Metadata` package. 4. **Adding new test cases**: Add entries to `e2e_specs.json`: ```json @@ -95,6 +95,17 @@ These APIs are available on any Windows 10/11 machine without WinAppSDK: - **Method invocation** returns a single `WinRTValue` (not a list) in Python binding - **Generated code** uses relative imports (`from .module import Class`) — must be in a Python package +### Classic COM implementation rule + +For Classic COM, Windows.Win32 metadata, pointer, handle, ownership, or native +ABI changes, follow +[`classic-com-abi`](skills/classic-com-abi/SKILL.md). Model native type plus +parameter contract before language projection, keep COM separate from WinRT, +and fail closed when layout or ownership is incomplete. JavaScript ergonomics +belong to the codegen projection layer; the renderer must not infer ABI +semantics. Classic COM changes must preserve existing WinRT models, generated +output, runtime behavior, and the `@microsoft/dynwinrt` root API. + ### Code Generator (dynwinrt-codegen) - `src/codegen/project.rs` + `src/codegen/projected.rs` — Build the language-neutral `ProjectedFile` IR from parsed metadata - `src/codegen/render_js.rs` + `src/codegen/render_dts.rs` — Render IR to `.js` and `.d.ts` diff --git a/.github/skills/classic-com-abi/SKILL.md b/.github/skills/classic-com-abi/SKILL.md new file mode 100644 index 00000000..5a33489a --- /dev/null +++ b/.github/skills/classic-com-abi/SKILL.md @@ -0,0 +1,307 @@ +--- +name: classic-com-abi +description: Use when implementing or reviewing Classic COM, Windows.Win32.winmd, native ABI, pointer, handle, ownership, libffi, or COM codegen changes in dynwinrt. +--- + +# Classic COM ABI development + +Use this skill for changes under: + +- `crates/dynwinrt/src/com.rs`, `signature.rs`, `native_call.rs`, or `call.rs`; +- `bindings/js/src/com.rs`; +- `tools/dynwinrt-codegen/src/com_metadata.rs`; +- `tools/dynwinrt-codegen/src/codegen/com/`; or +- Classic COM runners in `tests/runners/com/`. + +Read [`docs/classic-com-support.md`](../../../docs/classic-com-support.md) +before changing supported types or claiming support for an interface. + +## Core principle + +Start from the native ABI type **and parameter contract**, never from the +desired JavaScript/Python representation. + +```text +Windows.Win32.winmd facts + -> COM-local semantic ABI model + -> validation and ownership plan + -> libffi call plan + -> language projection +``` + +`Buffer`, `bigint`, `string`, and generated wrappers are projection choices. +They must not determine native semantics. + +## Required semantic model + +Preserve these facts before rendering: + +- native type name and underlying type; +- pointer depth; +- const/mutability; +- `In`, `Out`, or `InOut`; +- nullable/required state; +- struct/union size, alignment, packing, and fields; +- count/capacity/actual-length parameter relationships; +- ownership transfer; +- allocator or cleanup function; +- interface IID and reference ownership; and +- return convention: HRESULT, semantic HRESULT, direct value, pointer, or + `void`. + +Do not erase these facts into a generic `Object` or pointer before validation. + +## Semantic categories + +Model at least these categories explicitly: + +```text +Scalar +Enum +NativeStruct +NativeUnion +HandleValue +DataPointer +StringPointer +Bstr +ComInterface +CountedBuffer +SafeArray +Variant +FunctionPointer +Unknown +``` + +Unknown or incomplete categories must fail closed. + +## Layer boundaries + +1. Keep Classic COM metadata and projected types COM-local. +2. Do not add Classic COM concepts to the existing WinRT metadata model or + `DynWinRt*` public surface. +3. Sharing private libffi storage and vtable dispatch is allowed. +4. Keep the npm root WinRT-only; generated COM bindings import + `@microsoft/dynwinrt/com`. +5. Renderers consume validated semantic IR. They must not infer ABI semantics + from names, JavaScript values, or struct shape. + +### Required runtime architecture + +```text +WinRT metadata -> signature.rs (WinRT planner) --------\ + -> native_call.rs -> call.rs -> native method +COM metadata -> com.rs (COM planner and method table) / +``` + +Keep these source-level responsibilities distinct: + +| Component | Required responsibility | +|---|---| +| `signature.rs` | WinRT-only signature facade preserving existing `In`, `Out`, fill-array, HRESULT, and out-value behavior. | +| `com.rs` | COM-local `Type`, `MethodSignature`, `Interface`, `MethodHandle`, interface roots, method registry, pointer/InOut semantics, and native return conventions. | +| `native_call.rs` | Private lowering backend for completed signatures: parameter/output indexing, value validation and coercion, array ABI expansion, fast-path selection, libffi CIF preparation, and result coordination. | +| `call.rs` | Private executor: vtable lookup, stable ABI storage, libffi argument construction and invocation, and decoding raw output slots according to the plan. | + +Apply these rules: + +- WinRT methods stay in the WinRT `MetadataTable`; COM methods stay in the + COM-local registry. +- Only the WinRT planner may define WinRT signature behavior. Do not add raw + pointers, `InOut`, direct native returns, or `void` returns to its public + model. +- Only the COM metadata/projection and planner layers may interpret pointer + categories, parameter direction, return convention, and ownership. +- A by-value GUID is not REFIID. Dynamic-IID output adoption requires + pointer-shaped metadata plus an explicit `iid`/`riid` semantic parameter. +- `native_call.rs` may validate and lower an already-described call, but must + not infer metadata semantics, allocator ownership, or language projection. +- `call.rs` must execute the completed plan without inferring metadata, + ownership, or projection contracts from the caller or language-level value. +- Native methods published through a shared registry must be fully constructed + and immutable. Any manual `Send`/`Sync` implementation requires a documented + libffi read-only safety argument and compile-time trait tests. +- Exact identity checks are required for structs. Preserve established + ABI-compatible WinRT projection aliases such as Char16/U16 and enum/I32 + arrays. + +### Required codegen architecture + +```text +ComInterfaceMeta + -> codegen/com/project + -> validated ComType / ProjectedComMethod + -> codegen/com/javascript renderer +``` + +- Keep WinRT generators under `codegen/winrt`; do not import COM semantic IR + into them. +- Convert shared `TypeMeta` values to the closed COM-local `ComType` set before + rendering. +- Encode parameter direction, native return convention, result ordering, + ownership, cleanup, string-buffer relationships, activation, and + dynamic-IID behavior in `ProjectedComMethod`. +- Production renderers may consume only projected COM IR. They must not import + `TypeMeta`, `MethodMeta`, `ParamMeta`, metadata attributes, or infer + ownership. +- Every renderer match over `ComType` must be exhaustive. Never use a wildcard + branch that emits `pointerType`, `Buffer`, bigint, or a raw value. +- Transparent scalar typedefs preserve their underlying scalar ABI. A + one-field Win32 struct is not automatically a handle. +- Pointer aliases require explicit `HandleValue`, `DataPointer`, or + `StringPointer` classification. Unknown aliases fail closed. +- Cleanup identifiers must match a single known allocator exactly. Do not use + substring matching. + +## Projection responsibility + +Keep these responsibilities separate: + +| Layer | Responsibility | +|---|---| +| Runtime / ABI | Faithfully and safely execute a fully described native call: storage, libffi types, vtable dispatch, HRESULT, ownership, and cleanup. | +| Codegen semantic projection | Turn validated COM semantics into an idiomatic language API: Buffer/string/bigint choices, camelCase, overloads, optional arguments, hidden ABI parameters, and projected return values. | +| Renderer | Serialize the projection decision into JavaScript and declarations. It must not discover or guess native semantics. | + +Electron/Node conveniences belong in the JavaScript projection. The runtime may +provide a small, centralized safety primitive such as `handleValue()`, but it +must not decide that an arbitrary Buffer represents a handle. + +## WinRT compatibility invariant + +Classic COM work must not change existing WinRT semantics. + +- Do not add COM-only types, directions, ownership, pointers, or return + conventions to the public WinRT model. +- Do not change existing `DynWinRt*` behavior or the + `@microsoft/dynwinrt` root surface. +- Do not change generated WinRT constructors, method signatures, imports, + naming, ownership, or output files as a side effect of COM support. +- Shared ABI/libffi helpers must remain private and behavior-neutral for WinRT. +- Route Classic COM through COM-local metadata and projection before any + language renderer. +- Require WinRT snapshot, package, runtime, and live E2E regression coverage + for every shared-infrastructure change. + +## Pointer and Buffer rules + +A Node Buffer can have different native meanings: + +| Semantic type | Buffer meaning | Projection | +|---|---|---| +| Handle value | Pointer-width bytes containing a numeric handle | Explicit `DynCom.handleValue()` | +| Data pointer | Native data stored in the Buffer | `DynCom.pointer(buffer)` passes and retains its address | +| String pointer | Encoded, terminated string bytes | Pass the backing address with encoding validation | +| BSTR | Length-prefixed Automation allocation | Dedicated BSTR allocation/conversion | +| COM interface | Reference-counted interface pointer | Managed COM wrapper, never a Buffer | + +Never apply one Buffer interpretation to every pointer-shaped typedef. + +For Electron HWND input: + +- accept Buffer/Uint8Array only for a confirmed `HWND` input; +- require exactly `size_of::()` bytes; +- decode little-endian handle bits in the centralized runtime helper; +- keep HWND output aliases numeric; and +- keep PSID, security descriptors, structs, and strings on address semantics. + +Do not infer `HandleValue` merely because a Win32 struct has one `Value` +pointer field. Use metadata attributes and an explicit conservative mapping. +Examples: + +- `HANDLE`: `RAIIFree(CloseHandle)`; +- `HKEY`: `RAIIFree(RegCloseKey)`; +- `HICON`: `RAIIFree(DestroyIcon)`; +- `HWND`: `AlsoUsableFor(HANDLE)`; +- `BSTR`: `RAIIFree(SysFreeString)`; +- `PSID`: data pointer, not a handle value. + +## Ownership rules + +- `CoCreateInstance`, QueryInterface, and typed interface out-parameters return + owned `+1` references. +- Managed COM values release automatically; explicit `release()` is only + deterministic early release. +- Interface inputs are borrowed unless the callee AddRefs them for retention. +- `adoptComPointer()` accepts only a native output known to transfer `+1`. +- Numeric and Buffer-backed pointers are borrowed and cannot be adopted. +- Pair BSTR with `SysFreeString`. +- Pair HSTRING ownership with `WindowsDeleteString`; never project HSTRING as a + numeric pointer. +- Pair CoTaskMem allocations with `CoTaskMemFree`. +- Win32 handles are not COM references; cleanup is resource-specific. +- Unknown allocator or ownership contracts fail closed. + +## Metadata evidence + +Before supporting an interface: + +1. Parse the actual configured `Windows.Win32.winmd`. +2. Walk its full interface inheritance chain. +3. Inspect every method, not only the method intended for a sample. +4. Record `NativeArrayInfo`, `FreeWith`, `Const`, parameter direction, and + pointer depth. +5. Record `CanReturnMultipleSuccessValuesAttribute` before deciding whether an + HRESULT is throw-or-void or a semantic result. +6. Resolve every referenced interface IID from the loaded metadata. Require + callers to provide external definitions through `--ref`. +7. Check Microsoft API documentation for ownership that metadata does not + encode. +8. Generate with `--dry-run` and verify unsupported methods stop the whole + unsafe interface projection. + +Do not claim general interface support when only a manually described runtime +subset works. + +## Fail-closed requirements + +Reject generation when any required fact is unknown, including: + +- native struct/union layout; +- writable caller-sized buffers without a modeled count relationship; +- untyped output pointers without ownership; +- unsupported interface in/out replacement; +- BSTR arrays or unknown string allocation; +- VARIANT, PROPVARIANT, SAFEARRAY, FORMATETC, or STGMEDIUM without dedicated + models; +- unsupported direct native returns; or +- interface parameters whose IID or PIID cannot be resolved from loaded + metadata; +- parameterized or async interfaces without a computed closed IID; +- delegates without a managed callback projection; +- native arrays without explicit count and element-ownership contracts; +- incomplete inherited vtable layout. + +An error during generation is safer than plausible generated code with the +wrong ABI. + +## Validation + +Every new semantic type or ownership rule needs: + +1. a pure unit test for mapping and rendering; +2. a real `Windows.Win32.winmd` regression test; +3. a runtime test covering storage and cleanup; +4. a fail-before/fail-closed test for the nearest unsupported shape; +5. x64 and i686 compile validation for pointer-sized ABI; +6. WinRT regression coverage proving the existing generator and runtime did + not change; and +7. a live stock-Windows E2E when the API is deterministic and requires no + optional software, network, or user interaction. + +Prefer tests that add a new ABI shape. Do not add many interfaces that only +repeat activation. + +## Review checklist + +- Does the change start from metadata facts rather than JS convenience? +- Is the semantic type explicit? +- Are pointer depth and direction preserved? +- Is storage correctly sized before native invocation? +- Is ownership explicit on success and failure? +- Are x86 and x64 widths correct? +- Can a borrowed pointer become a second owner? +- Can Buffer contents be confused with Buffer address? +- Does an InOut path use the same conversion and helper availability as In? +- Does the renderer contain ABI heuristics that belong in projection? +- Does unsupported metadata fail during generation? +- Did any WinRT model, output, or root API change? diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1d844ae5..5a0b9950 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,10 +17,31 @@ jobs: steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + with: + targets: i686-pc-windows-msvc + - uses: NuGet/setup-nuget@v2 + - name: Install Win32 metadata + shell: pwsh + run: | + $root = Join-Path $env:RUNNER_TEMP 'win32metadata' + nuget install Microsoft.Windows.SDK.Win32Metadata ` + -Version 69.0.7-preview ` + -OutputDirectory $root ` + -DirectDownload ` + -NonInteractive + $winmd = Get-ChildItem $root -Filter Windows.Win32.winmd -File -Recurse | + Select-Object -First 1 + if (-not $winmd) { + throw 'Microsoft.Windows.SDK.Win32Metadata did not contain Windows.Win32.winmd' + } + "DYNWINRT_WIN32_WINMD=$($winmd.FullName)" >> $env:GITHUB_ENV + "DYNWINRT_REQUIRE_WIN32_METADATA=1" >> $env:GITHUB_ENV - name: Test core library run: cargo test -p dynwinrt - name: Test dynwinrt-codegen run: cargo test -p dynwinrt-codegen + - name: Check x86 Classic COM runtime + run: cargo check -p jswinrt_rs --target i686-pc-windows-msvc # E2E tests: winmd → generate → call real WinRT APIs e2e: @@ -35,6 +56,23 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.12' + - uses: NuGet/setup-nuget@v2 + - name: Install Win32 metadata + shell: pwsh + run: | + $root = Join-Path $env:RUNNER_TEMP 'win32metadata' + nuget install Microsoft.Windows.SDK.Win32Metadata ` + -Version 69.0.7-preview ` + -OutputDirectory $root ` + -DirectDownload ` + -NonInteractive + $winmd = Get-ChildItem $root -Filter Windows.Win32.winmd -File -Recurse | + Select-Object -First 1 + if (-not $winmd) { + throw 'Microsoft.Windows.SDK.Win32Metadata did not contain Windows.Win32.winmd' + } + "DYNWINRT_WIN32_WINMD=$($winmd.FullName)" >> $env:GITHUB_ENV + "DYNWINRT_REQUIRE_WIN32_METADATA=1" >> $env:GITHUB_ENV - name: Build dynwinrt-codegen run: cargo build -p dynwinrt-codegen --release - name: Build JS binding @@ -42,6 +80,8 @@ jobs: run: | npm install npx napi build --no-const-enum --platform --release -o dist + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build:entrypoints --silent npm install --no-save tsx - name: Build Python binding run: | @@ -99,10 +139,16 @@ jobs: run: npm install - name: Build x64 working-directory: bindings/js - run: npx napi build --no-const-enum --platform --release -o dist + run: | + npx napi build --no-const-enum --platform --release -o dist + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build:entrypoints --silent - name: Build arm64 working-directory: bindings/js - run: npx napi build --no-const-enum --platform --release --target aarch64-pc-windows-msvc -o dist + run: | + npx napi build --no-const-enum --platform --release --target aarch64-pc-windows-msvc -o dist + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build:entrypoints --silent - uses: actions/upload-artifact@v4 with: name: dynwinrt @@ -111,3 +157,7 @@ jobs: bindings/js/dist/dynwinrt.win32-arm64-msvc.node bindings/js/dist/index.js bindings/js/dist/index.d.ts + bindings/js/dist/winrt.js + bindings/js/dist/winrt.d.ts + bindings/js/dist/com.js + bindings/js/dist/com.d.ts diff --git a/.gitignore b/.gitignore index 06356cd9..04c6e8b5 100644 --- a/.gitignore +++ b/.gitignore @@ -437,4 +437,6 @@ bench-electron/out/ **/build # Claude Code local settings -**/settings.local.json \ No newline at end of file +**/settings.local.json +# Generated E2E projections are recreated and removed by tests/e2e_test.ps1. +tests/e2e_generated/ diff --git a/.pipelines/ci.yml b/.pipelines/ci.yml index afdc94fa..71da2f28 100644 --- a/.pipelines/ci.yml +++ b/.pipelines/ci.yml @@ -99,6 +99,24 @@ extends: env: SYSTEM_ACCESSTOKEN: $(System.AccessToken) + - task: PowerShell@2 + displayName: Install Win32 metadata + inputs: + targetType: inline + script: | + $root = Join-Path "$(Agent.TempDirectory)" "win32metadata" + nuget install Microsoft.Windows.SDK.Win32Metadata ` + -Version 69.0.7-preview ` + -OutputDirectory $root ` + -DirectDownload ` + -NonInteractive + if ($LASTEXITCODE -ne 0) { Write-Error "Win32 metadata install failed"; exit 1 } + $winmd = Get-ChildItem $root -Filter Windows.Win32.winmd -File -Recurse | + Select-Object -First 1 + if (-not $winmd) { Write-Error "Windows.Win32.winmd not found"; exit 1 } + Write-Host "##vso[task.setvariable variable=DYNWINRT_WIN32_WINMD]$($winmd.FullName)" + Write-Host "##vso[task.setvariable variable=DYNWINRT_REQUIRE_WIN32_METADATA]1" + # Core library tests - task: PowerShell@2 displayName: Test core library @@ -156,14 +174,20 @@ extends: inputs: targetType: inline workingDirectory: bindings/js - script: npx napi build --no-const-enum --platform --release -o dist + script: | + npx napi build --no-const-enum --platform --release -o dist + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build:entrypoints --silent - task: PowerShell@2 displayName: Build dynwinrt (arm64) inputs: targetType: inline workingDirectory: bindings/js - script: npx napi build --no-const-enum --platform --release --target aarch64-pc-windows-msvc -o dist + script: | + npx napi build --no-const-enum --platform --release --target aarch64-pc-windows-msvc -o dist + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build:entrypoints --silent # Install tsx for E2E TS runner - task: PowerShell@2 @@ -219,9 +243,9 @@ extends: .\bindings\py\.venv\Scripts\python.exe -m mypy.stubtest dynwinrt_py --allowlist bindings\py\stubtest_allowlist.txt --ignore-disjoint-bases if ($LASTEXITCODE -ne 0) { Write-Error "Python runtime stub validation failed"; exit 1 } - # E2E tests: winmd → generate → type-check → call real WinRT APIs + # E2E tests: winmd → generate → type-check → call real WinRT and COM APIs - task: PowerShell@2 - displayName: Run E2E tests (40 tests) + displayName: Run E2E tests inputs: targetType: inline script: | diff --git a/.pipelines/release.yml b/.pipelines/release.yml index 671651e1..4e8c5218 100644 --- a/.pipelines/release.yml +++ b/.pipelines/release.yml @@ -164,14 +164,20 @@ extends: inputs: targetType: inline workingDirectory: bindings/js - script: npx napi build --no-const-enum --platform --release -o dist + script: | + npx napi build --no-const-enum --platform --release -o dist + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build:entrypoints --silent - task: PowerShell@2 displayName: Build dynwinrt (arm64) inputs: targetType: inline workingDirectory: bindings/js - script: npx napi build --no-const-enum --platform --release --target aarch64-pc-windows-msvc -o dist + script: | + npx napi build --no-const-enum --platform --release --target aarch64-pc-windows-msvc -o dist + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + npm run build:entrypoints --silent # Set version from tag - task: PowerShell@2 @@ -341,4 +347,3 @@ extends: mainpublisher: 'ESRPRELPACMAN' domaintenantid: ${{ parameters.signingIdentity.tenantId }} - diff --git a/CLAUDE.md b/CLAUDE.md index 64c204ef..87295f99 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,7 @@ cargo test -p dynwinrt cargo test -p dynwinrt-codegen # Build JS bindings -cd bindings/js && npm install && npx napi build --no-const-enum --platform --release -o dist +cd bindings/js && npm install && npm run build # Build Python bindings cd bindings/py && maturin develop @@ -195,4 +195,3 @@ The library uses `windows-core::IUnknown` smart pointers which automatically han ### Parameterized IID Computation Generic interfaces (IVector\, IMap\, IAsyncOperation\) have IIDs computed at runtime using the WinRT parameterized interface algorithm (SHA-1 hash of the PIID + type argument signatures). This is implemented in `metadata_table/iid.rs`. - diff --git a/README.md b/README.md index 6751c1b8..516ee180 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,12 @@ const uri = new Uri('https://example.com/path?q=1'); console.log(uri.host); // "example.com" ``` +Classic COM bindings import their runtime API from the separate +`@microsoft/dynwinrt/com` subpath. It is part of the same npm package; the +package root remains the WinRT-only API. See +[Classic COM support](docs/classic-com-support.md) for the supported ABI, +common-interface test matrix, unsupported native types, and ownership rules. + Generated bindings project unambiguous public WinRT activation metadata as JavaScript constructors, including overloads such as `new Uri(base, relative)`. Existing static factory methods remain available. Classes that can only be returned by @@ -119,7 +125,7 @@ cargo build -p dynwinrt cargo test -p dynwinrt # JS bindings (napi-rs) -cd bindings/js && npm install && npx napi build --no-const-enum --platform --release -o dist +cd bindings/js && npm install && npm run build # Python bindings (PyO3 + maturin) — experimental, not published to PyPI cd bindings/py && maturin develop && pytest @@ -153,7 +159,7 @@ For each WinRT class the codegen emits a typed wrapper, factory, interface regis Generated files import from `'@microsoft/dynwinrt'`. When iterating against a locally-built runtime, rewrite imports to the relative path: ```bash -find generated -name "*.js" -exec sed -i "s|from '@microsoft/dynwinrt'|from '../../dist/index.js'|g" {} + +find generated -name "*.js" -exec sed -i "s|from '@microsoft/dynwinrt'|from '../../dist/winrt.js'|g" {} + ``` ## Troubleshooting diff --git a/bench-electron/electron.vite.config.ts b/bench-electron/electron.vite.config.ts index dacc4ba8..d8ca927d 100644 --- a/bench-electron/electron.vite.config.ts +++ b/bench-electron/electron.vite.config.ts @@ -10,7 +10,7 @@ export default defineConfig({ plugins: [externalizeDepsPlugin()], build: { rollupOptions: { - external: ['@microsoft/dynwinrt', /\.node$/] + external: [/^@microsoft\/dynwinrt(?:\/com)?$/, /\.node$/] } } }, diff --git a/bindings/js/Cargo.toml b/bindings/js/Cargo.toml index d0a465f5..d48da037 100644 --- a/bindings/js/Cargo.toml +++ b/bindings/js/Cargo.toml @@ -12,7 +12,7 @@ repository = "https://github.com/microsoft/dynwinrt" crate-type = ["cdylib"] [dependencies] -napi = { version = "3", features = ["napi6"] } +napi = { version = "3", features = ["napi7"] } napi-derive = "3.0.0" dynwinrt = { path = "../../crates/dynwinrt" } windows-future = "0.3.2" diff --git a/bindings/js/README.md b/bindings/js/README.md index 45babc4a..59e22783 100644 --- a/bindings/js/README.md +++ b/bindings/js/README.md @@ -41,6 +41,20 @@ console.log(uri.host); // "example.com" console.log(uri.port); // 443 ``` +Classic COM uses a separate subpath from the same package, keeping the WinRT +root API unchanged: + +```js +const { DynCom } = require('@microsoft/dynwinrt/com'); +``` + +COM interface values returned by activation, `QueryInterface`, or typed +interface out-parameters own one reference and release it when their +`DynWinRtValue` is released or collected. `adoptComPointer()` is only for a +native output that transfers an existing `+1` reference; numeric pointers and +typed-array pointers are borrowed and cannot be adopted. Win32 handles are not +COM references and require their own type-specific cleanup function. + Unambiguous public WinRT activation metadata is projected as JavaScript constructors. Parameterized and composable activations support idiomatic forms such as `new Uri(base, relative)` and `new StackPanel()`. The generated static factory diff --git a/bindings/js/__test__/async-promise-child.mjs b/bindings/js/__test__/async-promise-child.mjs index 4f782ad2..72fbf8fe 100644 --- a/bindings/js/__test__/async-promise-child.mjs +++ b/bindings/js/__test__/async-promise-child.mjs @@ -7,7 +7,7 @@ import { createRequire } from 'node:module' const require = createRequire(import.meta.url) const { DynWinRtMethodSig, DynWinRtType, DynWinRtValue, WinGuid, roInitialize } = require( - process.env.DYNWINRT_TEST_RUNTIME ?? '../dist/index.js', + process.env.DYNWINRT_TEST_RUNTIME ?? '../dist/winrt.js', ) roInitialize(1) diff --git a/bindings/js/__test__/dispatcher-queue-winui-child.cjs b/bindings/js/__test__/dispatcher-queue-winui-child.cjs index bebafb61..d8229ce1 100644 --- a/bindings/js/__test__/dispatcher-queue-winui-child.cjs +++ b/bindings/js/__test__/dispatcher-queue-winui-child.cjs @@ -11,7 +11,7 @@ const { spawn } = require('node:child_process') const applicationModule = process.argv[2] const bootstrapDll = process.argv[3] -const runtimeModule = process.argv[4] ?? path.resolve(__dirname, '../dist/index.js') +const runtimeModule = process.argv[4] ?? path.resolve(__dirname, '../dist/winrt.js') const startMode = process.argv[5] ?? 'direct' if (!applicationModule || !bootstrapDll) { throw new Error( diff --git a/bindings/js/__test__/dispatcher-shutdown-child.mjs b/bindings/js/__test__/dispatcher-shutdown-child.mjs index d89ba55d..33f92dc3 100644 --- a/bindings/js/__test__/dispatcher-shutdown-child.mjs +++ b/bindings/js/__test__/dispatcher-shutdown-child.mjs @@ -14,7 +14,7 @@ const { registerWinuiDispatcherQueue, roInitialize, unregisterWinuiDispatcherQueue, -} = require(process.env.DYNWINRT_TEST_RUNTIME ?? '../dist/index.js') +} = require(process.env.DYNWINRT_TEST_RUNTIME ?? '../dist/winrt.js') roInitialize(0) diff --git a/bindings/js/__test__/env-cleanup-child.mjs b/bindings/js/__test__/env-cleanup-child.mjs index a1a4b6bc..0bd62d46 100644 --- a/bindings/js/__test__/env-cleanup-child.mjs +++ b/bindings/js/__test__/env-cleanup-child.mjs @@ -6,7 +6,7 @@ import { createRequire } from 'node:module' import { fileURLToPath } from 'node:url' import { Worker } from 'node:worker_threads' -const runtime = process.env.DYNWINRT_TEST_RUNTIME ?? fileURLToPath(new URL('../dist/index.js', import.meta.url)) +const runtime = process.env.DYNWINRT_TEST_RUNTIME ?? fileURLToPath(new URL('../dist/winrt.js', import.meta.url)) createRequire(import.meta.url)(runtime) const worker = new Worker(new URL('./env-cleanup-worker.mjs', import.meta.url), { workerData: { runtime }, diff --git a/bindings/js/__test__/index.spec.ts b/bindings/js/__test__/index.spec.ts index 96c0ee18..fbb7a73f 100644 --- a/bindings/js/__test__/index.spec.ts +++ b/bindings/js/__test__/index.spec.ts @@ -2,7 +2,7 @@ // Licensed under the MIT License. import test from 'ava' -import { spawn } from 'node:child_process' +import { spawn, spawnSync } from 'node:child_process' import { existsSync } from 'node:fs' import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' @@ -17,7 +17,109 @@ import { getWindowsDirectory, hasPackageIdentity, roInitialize, -} from '../dist/index.js' +} from '../dist/winrt.js' +import * as winrtRuntime from '../dist/winrt.js' +import { DynCom, DynComMethodSig } from '../dist/com.js' + +test('Classic COM is isolated from the WinRT root entrypoint', (t) => { + t.false(Object.prototype.hasOwnProperty.call(winrtRuntime, 'DynCom')) + t.truthy(DynCom) + + const assertion = + "const assert = require('node:assert/strict');" + + "const winrt = require('@microsoft/dynwinrt');" + + "const com = require('@microsoft/dynwinrt/com');" + + "assert.equal(Object.prototype.hasOwnProperty.call(winrt, 'DynCom'), false);" + + "assert.equal(typeof winrt.DynWinRtType, 'function');" + + "assert.equal(typeof com.DynCom, 'function');" + + "console.log('runtime-entrypoints-ok')" + const cjs = spawnSync(process.execPath, ['--eval', assertion], { + cwd: resolve(process.cwd()), + encoding: 'utf8', + windowsHide: true, + }) + t.is(cjs.status, 0, cjs.stderr) + t.regex(cjs.stdout, /runtime-entrypoints-ok/) + + const esmAssertion = + "import assert from 'node:assert/strict';" + + "import * as winrt from '@microsoft/dynwinrt';" + + "import * as com from '@microsoft/dynwinrt/com';" + + "assert.equal(Object.prototype.hasOwnProperty.call(winrt, 'DynCom'), false);" + + "assert.equal(typeof winrt.DynWinRtType, 'function');" + + "assert.equal(typeof com.DynCom, 'function');" + + "console.log('runtime-entrypoints-ok')" + const esm = spawnSync(process.execPath, ['--input-type=module', '--eval', esmAssertion], { + cwd: resolve(process.cwd()), + encoding: 'utf8', + windowsHide: true, + }) + t.is(esm.status, 0, esm.stderr) + t.regex(esm.stdout, /runtime-entrypoints-ok/) +}) + +test('DynCom rejects pointers after their TypedArray backing store is detached', (t) => { + const bytes = new Uint8Array(16) + const pointer = DynCom.pointer(bytes) + + structuredClone(bytes.buffer, { transfer: [bytes.buffer] }) + + t.is(bytes.byteLength, 0) + const error = t.throws(() => DynCom.asPointerBigint(pointer)) + t.regex(error.message, /backing ArrayBuffer is detached/) +}) + +test('DynCom does not adopt borrowed raw pointer bits as owned COM references', (t) => { + const borrowed = DynCom.pointer(0n) + const error = t.throws(() => DynCom.adoptComPointer(borrowed)) + t.regex(error.message, /only owned native outputs may be consumed/) +}) + +test('DynCom exposes HSTRING and semantic HRESULT primitives', (t) => { + t.is(DynCom.hstring('dynwinrt').toString(), 'dynwinrt') + t.truthy(DynCom.hstringType()) + t.truthy(new DynComMethodSig().preserveHresult()) +}) + +test('DynCom distinguishes handle-value bytes from data-pointer storage', (t) => { + const width = process.arch === 'ia32' ? 4 : 8 + const expected = 0x12345678n + const handle = Buffer.alloc(width) + if (width === 8) { + handle.writeBigUInt64LE(expected) + } else { + handle.writeUInt32LE(Number(expected)) + } + + t.is(DynCom.handleValue(handle), expected) + t.is(DynCom.handleValue(expected), expected) + t.throws(() => DynCom.handleValue(Buffer.alloc(width - 1)), { + message: /must contain exactly/, + }) + t.throws(() => DynCom.handleValue(Buffer.alloc(width + 1)), { + message: /must contain exactly/, + }) + const wrongTypedArray = new Uint16Array(width / 2) as unknown as Uint8Array + t.throws(() => DynCom.handleValue(wrongTypedArray), { + message: /expected bigint, number, Buffer, or Uint8Array/, + }) + t.throws(() => DynCom.pointer(wrongTypedArray), { + message: /expected bigint, number, Buffer, Uint8Array/, + }) + + const sid = Buffer.alloc(width) + sid.set(width === 8 ? [1, 2, 0, 0, 0, 0, 0, 5] : [1, 2, 0, 5]) + const sidPointer = DynCom.pointer(sid) + t.not(DynCom.asPointerBigint(sidPointer), DynCom.handleValue(sid)) +}) + +test('DynCom rejects a detached handle-value buffer', (t) => { + const bytes = new Uint8Array(process.arch === 'ia32' ? 4 : 8) + structuredClone(bytes.buffer, { transfer: [bytes.buffer] }) + + const error = t.throws(() => DynCom.handleValue(bytes)) + t.regex(error.message, /detached Buffer/) +}) test('getComputerName', (t) => { const name = getComputerName() @@ -221,7 +323,7 @@ if (missingWinuiFixtures.length > 0) { test.skip('WinUI scheduled start drains Promise reactions inside Application.Start', () => {}) } else { test('WinUI scheduled start drains Promise reactions inside Application.Start', async (t) => { - const runtimeModule = fileURLToPath(new URL('../dist/index.js', import.meta.url)) + const runtimeModule = fileURLToPath(new URL('../dist/winrt.js', import.meta.url)) const child = spawn( process.execPath, [ diff --git a/bindings/js/__test__/progress-exit-child.mjs b/bindings/js/__test__/progress-exit-child.mjs index bda3701b..c213fa12 100644 --- a/bindings/js/__test__/progress-exit-child.mjs +++ b/bindings/js/__test__/progress-exit-child.mjs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { DynWinRtMethodSig, DynWinRtType, DynWinRtValue, WinGuid, roInitialize } from '../dist/index.js' +import { DynWinRtMethodSig, DynWinRtType, DynWinRtValue, WinGuid, roInitialize } from '../dist/winrt.js' roInitialize(1) diff --git a/bindings/js/__test__/sta-async-child.mjs b/bindings/js/__test__/sta-async-child.mjs index 66241a4f..11ef4cd1 100644 --- a/bindings/js/__test__/sta-async-child.mjs +++ b/bindings/js/__test__/sta-async-child.mjs @@ -5,7 +5,7 @@ import { createRequire } from 'node:module' const require = createRequire(import.meta.url) const { DynWinRtMethodSig, DynWinRtType, DynWinRtValue, WinGuid, roInitialize } = require( - process.env.DYNWINRT_TEST_RUNTIME ?? '../dist/index.js', + process.env.DYNWINRT_TEST_RUNTIME ?? '../dist/winrt.js', ) roInitialize(0) diff --git a/bindings/js/package.json b/bindings/js/package.json index e4f2d15f..779bdace 100644 --- a/bindings/js/package.json +++ b/bindings/js/package.json @@ -2,8 +2,30 @@ "name": "@microsoft/dynwinrt", "version": "0.1.0", "description": "Dynamic WinRT bindings for Node.js — call any Windows Runtime API without native code generation", - "main": "dist/index.js", - "types": "dist/index.d.ts", + "main": "dist/winrt.js", + "types": "dist/winrt.d.ts", + "exports": { + ".": { + "types": "./dist/winrt.d.ts", + "import": "./dist/winrt.js", + "require": "./dist/winrt.js", + "default": "./dist/winrt.js" + }, + "./com": { + "types": "./dist/com.d.ts", + "import": "./dist/com.js", + "require": "./dist/com.js", + "default": "./dist/com.js" + }, + "./package.json": "./package.json" + }, + "typesVersions": { + "*": { + "com": [ + "dist/com.d.ts" + ] + } + }, "repository": { "type": "git", "url": "https://github.com/microsoft/dynwinrt" @@ -41,8 +63,9 @@ "scripts": { "artifacts": "napi artifacts", "bench": "node --import @oxc-node/core/register benchmark/bench.ts", - "build": "napi build --no-const-enum --platform --release -o dist", - "build:debug": "napi --no-const-enum build --platform -o dist", + "build": "napi build --no-const-enum --platform --release -o dist && npm run build:entrypoints", + "build:debug": "napi --no-const-enum build --platform -o dist && npm run build:entrypoints", + "build:entrypoints": "node scripts/generate-entrypoints.mjs", "format": "run-p format:prettier format:rs format:toml", "format:prettier": "prettier . -w", "format:toml": "taplo format", diff --git a/bindings/js/samples/array_struct.ts b/bindings/js/samples/array_struct.ts index 61d0d042..a553897a 100644 --- a/bindings/js/samples/array_struct.ts +++ b/bindings/js/samples/array_struct.ts @@ -18,7 +18,7 @@ import { DynWinRtStruct, WinGuid, roInitialize, -} from '../dist/index.js' +} from '../dist/winrt.js' // Initialize WinRT (MTA) roInitialize(1) diff --git a/bindings/js/samples/bench_3way.ts b/bindings/js/samples/bench_3way.ts index a17a15ca..25b3cca4 100644 --- a/bindings/js/samples/bench_3way.ts +++ b/bindings/js/samples/bench_3way.ts @@ -13,7 +13,7 @@ import { DynWinRtValue, DynWinRtType, DynWinRtMethodSig, DynWinRtStruct, WinGuid, roInitialize, RustStaticBench, rawGetString, rawGetI32, -} from '../dist/index.js' +} from '../dist/winrt.js' import { createRequire } from 'node:module' const require = createRequire(import.meta.url) diff --git a/bindings/js/samples/benchmark.ts b/bindings/js/samples/benchmark.ts index 44d8eccb..ad87132c 100644 --- a/bindings/js/samples/benchmark.ts +++ b/bindings/js/samples/benchmark.ts @@ -25,7 +25,7 @@ import { DynWinRtStruct, WinGuid, roInitialize, -} from '../dist/index.js' +} from '../dist/winrt.js' import { createRequire } from 'node:module' const require = createRequire(import.meta.url) diff --git a/bindings/js/samples/ocr.ts b/bindings/js/samples/ocr.ts index 67e95e44..8cf0a8e5 100644 --- a/bindings/js/samples/ocr.ts +++ b/bindings/js/samples/ocr.ts @@ -14,7 +14,7 @@ import { WinGuid, hasPackageIdentity, initWinappsdk, -} from '../dist/index.js' +} from '../dist/winrt.js' // ====================================================================== // IIDs diff --git a/bindings/js/samples/picker.ts b/bindings/js/samples/picker.ts index 7d4c4f6e..577b792e 100644 --- a/bindings/js/samples/picker.ts +++ b/bindings/js/samples/picker.ts @@ -8,7 +8,7 @@ import { DynWinRtType, DynWinRtMethodSig, WinGuid, -} from '../dist/index.js' +} from '../dist/winrt.js' // ====================================================================== // Register interfaces (once) diff --git a/bindings/js/samples/test_progress.ts b/bindings/js/samples/test_progress.ts index 794c9c35..67f95f7c 100644 --- a/bindings/js/samples/test_progress.ts +++ b/bindings/js/samples/test_progress.ts @@ -12,7 +12,7 @@ import { DynWinRtMethodSig, WinGuid, roInitialize, -} from '../dist/index.js' +} from '../dist/winrt.js' roInitialize(1) diff --git a/bindings/js/samples/test_register_interface.ts b/bindings/js/samples/test_register_interface.ts index 8b2e9719..16bdd83e 100644 --- a/bindings/js/samples/test_register_interface.ts +++ b/bindings/js/samples/test_register_interface.ts @@ -11,7 +11,7 @@ import { DynWinRtMethodSig, WinGuid, roInitialize, -} from '../dist/index.js' +} from '../dist/winrt.js' roInitialize(1) diff --git a/bindings/js/scripts/generate-entrypoints.mjs b/bindings/js/scripts/generate-entrypoints.mjs new file mode 100644 index 00000000..2fa73d4a --- /dev/null +++ b/bindings/js/scripts/generate-entrypoints.mjs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { readFileSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const packageDir = fileURLToPath(new URL('..', import.meta.url)) +const distDir = join(packageDir, 'dist') +const loader = readFileSync(join(distDir, 'index.js'), 'utf8') +const nativeExports = [ + ...loader.matchAll(/^module\.exports\.([A-Za-z_$][\w$]*) = nativeBinding\.\1$/gm), +].map((match) => match[1]) + +if (nativeExports.length === 0) { + throw new Error('No N-API exports found in dist/index.js') +} + +const comExports = new Set([ + 'DynCom', + 'DynComInterface', + 'DynComMethodHandle', + 'DynComMethodSig', + 'DynComType', + 'DynWinRtValue', + 'DynWinRTValue', + 'WinGuid', + 'WinGUID', +]) + +writeFacade( + 'winrt', + nativeExports.filter((name) => !name.startsWith('DynCom')), +) +writeFacade( + 'com', + nativeExports.filter((name) => comExports.has(name)), +) + +function writeFacade(name, exports) { + const missing = name === 'com' ? [...comExports].filter((value) => !exports.includes(value)) : [] + if (missing.length > 0) { + throw new Error(`Missing required ${name} exports: ${missing.join(', ')}`) + } + + const js = [ + '// Generated by scripts/generate-entrypoints.mjs - do not edit', + "'use strict'", + "const native = require('./index.js')", + ...exports.map((value) => `module.exports.${value} = native.${value}`), + '', + ].join('\n') + const dts = [ + '// Generated by scripts/generate-entrypoints.mjs - do not edit', + `export { ${exports.join(', ')} } from './index.js'`, + '', + ].join('\n') + + writeFileSync(join(distDir, `${name}.js`), js) + writeFileSync(join(distDir, `${name}.d.ts`), dts) +} diff --git a/bindings/js/src/async_promise.rs b/bindings/js/src/async_promise.rs index 124d6060..0d83787a 100644 --- a/bindings/js/src/async_promise.rs +++ b/bindings/js/src/async_promise.rs @@ -465,7 +465,7 @@ impl AsyncPromiseState { let result = result .and_then(StoredWinRTValue::resolve) .and_then(|value| { - unsafe { DynWinRTValue::to_napi_value(env, DynWinRTValue(value)) } + unsafe { DynWinRTValue::to_napi_value(env, DynWinRTValue::new(value)) } .map_err(|error| format!("Async result conversion failed: {error}")) }); diff --git a/bindings/js/src/com.rs b/bindings/js/src/com.rs new file mode 100644 index 00000000..9f3882d9 --- /dev/null +++ b/bindings/js/src/com.rs @@ -0,0 +1,1145 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use napi::JsValue; +use napi::bindgen_prelude::{BigInt, FromNapiValue, ToNapiValue, Unknown}; +use napi_derive::napi; +use windows::core::{GUID, Interface as _}; + +use super::{DynWinRTValue, TABLE, WinGUID}; + +#[allow(dead_code)] +pub(super) enum NativePointerOwner { + Uint8Array { + value: std::sync::Mutex, + env: napi::sys::napi_env, + pointer: usize, + length: usize, + }, + CoTaskMem(*mut std::ffi::c_void), + Guid(*mut GUID), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum PointerProvenance { + None, + Borrowed, + NativeOutput, +} + +impl NativePointerOwner { + fn validate(&self) -> napi::Result<()> { + let Self::Uint8Array { + value, + env, + pointer, + length, + } = self + else { + return Ok(()); + }; + let mut value = value + .lock() + .map_err(|_| napi::Error::from_reason("TypedArray pointer owner lock is poisoned"))?; + let raw = unsafe { + <&mut napi::bindgen_prelude::Uint8Array as ToNapiValue>::to_napi_value(*env, &mut *value) + }?; + let mut typed_array_type = 0; + let mut current_length = 0usize; + let mut current_pointer = std::ptr::null_mut(); + let mut array_buffer = std::ptr::null_mut(); + let mut byte_offset = 0usize; + napi::check_status!( + unsafe { + napi::sys::napi_get_typedarray_info( + *env, + raw, + &mut typed_array_type, + &mut current_length, + &mut current_pointer, + &mut array_buffer, + &mut byte_offset, + ) + }, + "Failed to revalidate TypedArray backing storage" + )?; + let mut detached = false; + napi::check_status!( + unsafe { napi::sys::napi_is_detached_arraybuffer(*env, array_buffer, &mut detached) }, + "Failed to inspect TypedArray backing storage" + )?; + if detached { + return Err(napi::Error::from_reason( + "Cannot use a pointer whose TypedArray backing ArrayBuffer is detached", + )); + } + let current_pointer = if current_length == 0 { + 0 + } else { + current_pointer as usize + }; + if current_length != *length || current_pointer != *pointer { + return Err(napi::Error::from_reason( + "Cannot use a pointer whose TypedArray backing storage changed", + )); + } + Ok(()) + } +} + +impl Drop for NativePointerOwner { + fn drop(&mut self) { + match self { + Self::CoTaskMem(ptr) => { + if !ptr.is_null() { + unsafe { windows::Win32::System::Com::CoTaskMemFree(Some(*ptr)) }; + *ptr = std::ptr::null_mut(); + } + } + Self::Guid(ptr) => { + if !ptr.is_null() { + drop(unsafe { Box::from_raw(*ptr) }); + *ptr = std::ptr::null_mut(); + } + } + _ => {} + } + } +} + +fn co_create_instance(clsid: String, iid: &WinGUID) -> napi::Result { + let parsed = windows::core::GUID::try_from(clsid.as_str()) + .map_err(|_| napi::Error::from_reason(format!("Invalid CLSID: '{clsid}'")))?; + dynwinrt::com::co_create_instance(parsed, iid.0) + .map(DynWinRTValue::new) + .map_err(|error| napi::Error::from_reason(error.message())) +} + +fn create_test_hwnd() -> napi::Result { + use std::sync::atomic::{AtomicUsize, Ordering}; + use windows::Win32::UI::WindowsAndMessaging::{CreateWindowExW, WINDOW_EX_STYLE, WS_POPUP}; + + static CACHED_HWND: AtomicUsize = AtomicUsize::new(0); + let cached = CACHED_HWND.load(Ordering::Acquire); + if cached != 0 { + return Ok(BigInt::from(cached as u64)); + } + let class_name: Vec = "STATIC".encode_utf16().chain(Some(0)).collect(); + let title: Vec = "dynwinrt-test-hwnd\0".encode_utf16().collect(); + let hwnd = unsafe { + CreateWindowExW( + WINDOW_EX_STYLE(0), + windows::core::PCWSTR(class_name.as_ptr()), + windows::core::PCWSTR(title.as_ptr()), + WS_POPUP, + 0, + 0, + 1, + 1, + None, + None, + None, + None, + ) + } + .map_err(|error| napi::Error::from_reason(format!("CreateWindowExW: {error}")))?; + let bits = hwnd.0 as usize; + CACHED_HWND.store(bits, Ordering::Release); + Ok(BigInt::from(bits as u64)) +} + +struct Uint8ArrayInfo { + data: *const u8, + length: usize, +} + +fn uint8_array_info( + env: napi::sys::napi_env, + raw: napi::sys::napi_value, +) -> napi::Result> { + let mut is_typed_array = false; + napi::check_status!( + unsafe { napi::sys::napi_is_typedarray(env, raw, &mut is_typed_array) }, + "Failed to inspect TypedArray value" + )?; + if !is_typed_array { + return Ok(None); + } + + let mut typed_array_type = 0; + let mut length = 0usize; + let mut data = std::ptr::null_mut(); + let mut array_buffer = std::ptr::null_mut(); + let mut byte_offset = 0usize; + napi::check_status!( + unsafe { + napi::sys::napi_get_typedarray_info( + env, + raw, + &mut typed_array_type, + &mut length, + &mut data, + &mut array_buffer, + &mut byte_offset, + ) + }, + "Failed to inspect TypedArray backing storage" + )?; + if typed_array_type != napi::sys::TypedarrayType::uint8_array as i32 { + return Ok(None); + } + + let mut detached = false; + napi::check_status!( + unsafe { napi::sys::napi_is_detached_arraybuffer(env, array_buffer, &mut detached) }, + "Failed to inspect TypedArray backing storage" + )?; + if detached { + return Err(napi::Error::from_reason( + "Cannot use a detached Buffer/Uint8Array", + )); + } + + Ok(Some(Uint8ArrayInfo { + data: data.cast(), + length, + })) +} + +fn pointer(value: Unknown) -> napi::Result { + use napi::sys; + + let env = value.value().env; + let raw = value.value().value; + let mut value_type = sys::ValueType::napi_undefined; + unsafe { sys::napi_typeof(env, raw, &mut value_type) }; + if matches!( + value_type, + sys::ValueType::napi_null | sys::ValueType::napi_undefined + ) { + return Ok(DynWinRTValue::with_borrowed_pointer( + dynwinrt::WinRTValue::RawPtr(std::ptr::null_mut()), + )); + } + if value_type == sys::ValueType::napi_bigint { + let bigint = unsafe { BigInt::from_napi_value(env, raw) }?; + let (negative, bits, lossless) = bigint.get_u64(); + if negative || !lossless || bits as usize as u64 != bits { + return Err(napi::Error::from_reason( + "pointer(): bigint must fit in an unsigned pointer", + )); + } + return Ok(DynWinRTValue::with_borrowed_pointer( + dynwinrt::WinRTValue::RawPtr(bits as usize as *mut std::ffi::c_void), + )); + } + if value_type == sys::ValueType::napi_number { + let mut number = 0.0; + unsafe { sys::napi_get_value_double(env, raw, &mut number) }; + if !number.is_finite() + || number < 0.0 + || number.fract() != 0.0 + || number > 9_007_199_254_740_991.0 + || number as u64 as usize as u64 != number as u64 + { + return Err(napi::Error::from_reason( + "pointer(): number must be a non-negative safe integer that fits in a pointer", + )); + } + return Ok(DynWinRTValue::with_borrowed_pointer( + dynwinrt::WinRTValue::RawPtr(number as usize as *mut std::ffi::c_void), + )); + } + if uint8_array_info(env, raw)?.is_some() { + let array = unsafe { napi::bindgen_prelude::Uint8Array::from_napi_value(env, raw) }?; + let length = array.len(); + let pointer = if length == 0 { + 0 + } else { + array.as_ref().as_ptr() as usize + }; + return Ok(DynWinRTValue::with_pointer_owner( + dynwinrt::WinRTValue::RawPtr(pointer as *mut std::ffi::c_void), + NativePointerOwner::Uint8Array { + value: std::sync::Mutex::new(array), + env, + pointer, + length, + }, + )); + } + // Reject existing DynWinRtValue inputs. Borrowing an Object's raw COM pointer + // here would make it indistinguishable from an owned raw pointer to + // adoptComPointer(), which can double-release the original wrapper's COM + // object. Callers that already have raw pointer bits should pass those bits. + if unsafe { <&DynWinRTValue>::from_napi_value(env, raw) }.is_ok() { + return Err(napi::Error::from_reason( + "pointer(): DynWinRtValue inputs are not accepted; pass raw pointer bits, Buffer/Uint8Array, or null instead", + )); + } + Err(napi::Error::from_reason( + "pointer(): expected bigint, number, Buffer, Uint8Array, null, or undefined", + )) +} + +fn handle_value(value: Unknown) -> napi::Result { + use napi::sys; + + let env = value.value().env; + let raw = value.value().value; + let mut value_type = sys::ValueType::napi_undefined; + unsafe { sys::napi_typeof(env, raw, &mut value_type) }; + if value_type == sys::ValueType::napi_bigint { + let bigint = unsafe { BigInt::from_napi_value(env, raw) }?; + let (negative, bits, lossless) = bigint.get_u64(); + if negative || !lossless || bits as usize as u64 != bits { + return Err(napi::Error::from_reason( + "handleValue(): bigint must fit in an unsigned pointer", + )); + } + return Ok(BigInt::from(bits)); + } + if value_type == sys::ValueType::napi_number { + let mut number = 0.0; + unsafe { sys::napi_get_value_double(env, raw, &mut number) }; + if !number.is_finite() + || number < 0.0 + || number.fract() != 0.0 + || number > 9_007_199_254_740_991.0 + || number as u64 as usize as u64 != number as u64 + { + return Err(napi::Error::from_reason( + "handleValue(): number must be a non-negative safe integer that fits in a pointer", + )); + } + return Ok(BigInt::from(number as u64)); + } + if let Some(array) = uint8_array_info(env, raw)? { + let expected = std::mem::size_of::(); + if array.length != expected { + return Err(napi::Error::from_reason(format!( + "handleValue(): Buffer/Uint8Array must contain exactly {expected} bytes on this target", + ))); + } + if array.data.is_null() { + return Err(napi::Error::from_reason( + "handleValue(): Buffer/Uint8Array backing storage is null", + )); + } + let bytes = unsafe { std::slice::from_raw_parts(array.data, array.length) }; + #[cfg(target_pointer_width = "64")] + let bits = u64::from_le_bytes(bytes.try_into().expect("validated handle byte length")); + #[cfg(target_pointer_width = "32")] + let bits = u32::from_le_bytes(bytes.try_into().expect("validated handle byte length")) as u64; + return Ok(BigInt::from(bits)); + } + Err(napi::Error::from_reason( + "handleValue(): expected bigint, number, Buffer, or Uint8Array", + )) +} + +fn adopt_com_pointer( + value: &mut DynWinRTValue, + iid: Option<&WinGUID>, +) -> napi::Result { + let ptr = take_native_output_pointer(value, "COM interface")?; + let adopted = unsafe { dynwinrt::com::adopt_com_pointer(ptr) }; + match iid { + Some(iid) => adopted + .cast(&iid.0) + .map(DynWinRTValue::new) + .map_err(|error| napi::Error::from_reason(error.message())), + None => Ok(DynWinRTValue::new(adopted)), + } +} + +fn adopt_co_task_mem_pointer(value: &mut DynWinRTValue) -> napi::Result { + let ptr = take_native_output_pointer(value, "CoTaskMem allocation")?; + if ptr.is_null() { + return Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Null)); + } + Ok(DynWinRTValue::with_pointer_owner( + dynwinrt::WinRTValue::RawPtr(ptr), + NativePointerOwner::CoTaskMem(ptr), + )) +} + +fn as_pointer_bigint(value: &DynWinRTValue) -> napi::Result { + validate_pointer_owner(value)?; + let bits = match &value.0 { + dynwinrt::WinRTValue::Object(_) => { + return Err(napi::Error::from_reason( + "Managed COM objects cannot be exported as raw pointer addresses", + )); + } + dynwinrt::WinRTValue::RawPtr(ptr) => *ptr as usize, + dynwinrt::WinRTValue::Null => 0, + _ => { + return Err(napi::Error::from_reason( + "Value is not a pointer or COM object", + )); + } + }; + Ok(BigInt::from(bits as u64)) +} + +fn take_co_task_mem_wide_string(value: &mut DynWinRTValue) -> napi::Result { + let ptr = take_native_output_pointer(value, "wide-string")?; + if ptr.is_null() { + return Ok(String::new()); + } + let result = unsafe { windows::core::PCWSTR(ptr.cast()).to_string() } + .map_err(|error| napi::Error::from_reason(error.to_string())); + unsafe { windows::Win32::System::Com::CoTaskMemFree(Some(ptr)) }; + result +} + +fn take_co_task_mem_ansi_string(value: &mut DynWinRTValue) -> napi::Result { + let ptr = take_native_output_pointer(value, "ANSI-string")?; + if ptr.is_null() { + return Ok(String::new()); + } + let result = unsafe { windows::core::PCSTR(ptr.cast()).to_string() } + .map_err(|error| napi::Error::from_reason(error.to_string())); + unsafe { windows::Win32::System::Com::CoTaskMemFree(Some(ptr)) }; + result +} + +fn take_bstr(value: &mut DynWinRTValue) -> napi::Result { + let ptr = take_native_output_pointer(value, "BSTR")?; + if ptr.is_null() { + return Ok(String::new()); + } + let value = unsafe { windows::core::BSTR::from_raw(ptr.cast()) }; + String::try_from(&value).map_err(|error| napi::Error::from_reason(error.to_string())) +} + +fn validate_pointer_owner(value: &DynWinRTValue) -> napi::Result<()> { + if let Some(owner) = &value.1 { + owner.validate()?; + } + Ok(()) +} + +fn take_native_output_pointer( + value: &mut DynWinRTValue, + description: &str, +) -> napi::Result<*mut std::ffi::c_void> { + if value.1.is_some() { + return Err(napi::Error::from_reason(format!( + "Cannot consume an owner-backed {description} pointer" + ))); + } + if value.2 != PointerProvenance::NativeOutput { + return Err(napi::Error::from_reason(format!( + "Cannot adopt a borrowed {description} pointer; only owned native outputs may be consumed" + ))); + } + match std::mem::replace(&mut value.0, dynwinrt::WinRTValue::Null) { + dynwinrt::WinRTValue::RawPtr(ptr) => { + value.2 = PointerProvenance::None; + Ok(ptr) + } + dynwinrt::WinRTValue::Null => { + value.2 = PointerProvenance::None; + Ok(std::ptr::null_mut()) + } + other => { + value.0 = other; + Err(napi::Error::from_reason(format!( + "Expected a {description} raw pointer" + ))) + } + } +} + +fn iid_pointer(value: &WinGUID) -> DynWinRTValue { + // Owner-backed: the boxed GUID is freed when the returned DynWinRtValue is + // dropped / GC'd, instead of being leaked into a process-lifetime cache. The + // REFIID is only read during the synchronous COM call the value is passed to, + // and the JS temporary holding it outlives that call, so this is safe. + let ptr = Box::into_raw(Box::new(value.0)); + DynWinRTValue::with_pointer_owner( + dynwinrt::WinRTValue::RawPtr(ptr as *mut std::ffi::c_void), + NativePointerOwner::Guid(ptr), + ) +} + +#[napi] +pub struct DynComType(dynwinrt::com::Type); + +#[napi] +pub struct DynComMethodSig(dynwinrt::com::MethodSignature); + +#[napi] +impl DynComMethodSig { + #[napi(constructor)] + pub fn new() -> Self { + Self(dynwinrt::com::MethodSignature::new(&TABLE)) + } + + #[napi] + pub fn add_in(&self, typ: &DynComType) -> Self { + Self(self.0.clone().add_in(typ.0.clone())) + } + + #[napi] + pub fn add_out(&self, typ: &DynComType) -> Self { + Self(self.0.clone().add_out(typ.0.clone())) + } + + #[napi] + pub fn add_in_out(&self, typ: &DynComType) -> Self { + Self(self.0.clone().add_in_out(typ.0.clone())) + } + + #[napi] + pub fn add_out_fill(&self, typ: &DynComType) -> Self { + Self(self.0.clone().add_out_fill(typ.0.clone())) + } + + #[napi] + pub fn returns(&self, typ: &DynComType) -> Self { + Self(self.0.clone().returns(typ.0.clone())) + } + + #[napi] + pub fn returns_void(&self) -> Self { + Self(self.0.clone().returns_void()) + } + + #[napi] + pub fn preserve_hresult(&self) -> Self { + Self(self.0.clone().preserve_hresult()) + } +} + +#[napi] +pub struct DynComInterface(dynwinrt::com::Interface); + +#[napi] +impl DynComInterface { + #[napi] + pub fn add_method(&self, name: String, signature: &DynComMethodSig) -> Self { + Self(self.0.clone().add_method(&name, signature.0.clone())) + } + + #[napi] + pub fn method(&self, vtable_index: i32) -> napi::Result { + self + .0 + .method(vtable_index as usize) + .map(DynComMethodHandle) + .ok_or_else(|| { + napi::Error::from_reason(format!("No COM method at vtable index {vtable_index}")) + }) + } +} + +#[napi] +pub struct DynComMethodHandle(dynwinrt::com::MethodHandle); + +#[napi] +impl DynComMethodHandle { + #[napi] + pub fn get_string(&self, obj: &DynWinRTValue) -> napi::Result { + let raw = obj + .0 + .as_object() + .ok_or_else(|| napi::Error::from_reason("getString() requires a COM object"))? + .as_raw(); + self + .0 + .call_getter_hstring(raw) + .map(|value| value.to_string()) + .map_err(|error| napi::Error::from_reason(error.message())) + } + + #[napi] + pub fn invoke( + &self, + obj: &DynWinRTValue, + args: Vec<&DynWinRTValue>, + ) -> napi::Result { + let raw = obj + .0 + .as_object() + .ok_or_else(|| napi::Error::from_reason("invoke() requires a COM object"))? + .as_raw(); + for arg in &args { + validate_pointer_owner(arg)?; + } + let args = args.iter().map(|arg| arg.0.clone()).collect::>(); + let results = self + .0 + .invoke(raw, &args) + .map_err(|error| napi::Error::from_reason(error.message()))?; + Ok(DynWinRTValue::from_com_result( + results + .into_iter() + .next() + .unwrap_or(dynwinrt::WinRTValue::I32(0)), + )) + } + + #[napi] + pub fn invoke_all( + &self, + obj: &DynWinRTValue, + args: Vec<&DynWinRTValue>, + ) -> napi::Result> { + let raw = obj + .0 + .as_object() + .ok_or_else(|| napi::Error::from_reason("invokeAll() requires a COM object"))? + .as_raw(); + for arg in &args { + validate_pointer_owner(arg)?; + } + let args = args.iter().map(|arg| arg.0.clone()).collect::>(); + self + .0 + .invoke(raw, &args) + .map(|results| { + results + .into_iter() + .map(DynWinRTValue::from_com_result) + .collect() + }) + .map_err(|error| napi::Error::from_reason(error.message())) + } +} + +#[napi] +pub struct DynCom; + +#[napi] +impl DynCom { + #[napi] + pub fn initialize(apartment_type: Option) -> napi::Result<()> { + let apartment_type = match apartment_type.unwrap_or(1) { + 0 => dynwinrt::com::ApartmentType::SingleThreaded, + _ => dynwinrt::com::ApartmentType::MultiThreaded, + }; + dynwinrt::com::initialize_apartment(apartment_type) + .map_err(|error| napi::Error::from_reason(error.message())) + } + + #[napi(js_name = "registerIUnknownInterface")] + pub fn register_iunknown_interface(name: String, iid: &WinGUID) -> DynComInterface { + DynComInterface(dynwinrt::com::register_interface( + &TABLE, + &name, + iid.0, + dynwinrt::com::InterfaceBase::IUnknown, + )) + } + + #[napi(js_name = "registerIInspectableInterface")] + pub fn register_iinspectable_interface(name: String, iid: &WinGUID) -> DynComInterface { + DynComInterface(dynwinrt::com::register_interface( + &TABLE, + &name, + iid.0, + dynwinrt::com::InterfaceBase::IInspectable, + )) + } + + #[napi] + pub fn bool_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.bool_type())) + } + + #[napi] + pub fn i8_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.i8_type())) + } + + #[napi] + pub fn u8_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.u8_type())) + } + + #[napi] + pub fn i16_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.i16_type())) + } + + #[napi] + pub fn u16_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.u16_type())) + } + + #[napi] + pub fn i32_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.i32_type())) + } + + #[napi] + pub fn u32_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.u32_type())) + } + + #[napi] + pub fn i64_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.i64_type())) + } + + #[napi] + pub fn u64_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.u64_type())) + } + + #[napi] + pub fn isize_type() -> DynComType { + #[cfg(target_pointer_width = "64")] + { + DynComType(dynwinrt::com::Type::winrt(TABLE.i64_type())) + } + #[cfg(target_pointer_width = "32")] + { + DynComType(dynwinrt::com::Type::winrt(TABLE.i32_type())) + } + } + + #[napi] + pub fn usize_type() -> DynComType { + #[cfg(target_pointer_width = "64")] + { + DynComType(dynwinrt::com::Type::winrt(TABLE.u64_type())) + } + #[cfg(target_pointer_width = "32")] + { + DynComType(dynwinrt::com::Type::winrt(TABLE.u32_type())) + } + } + + #[napi] + pub fn f32_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.f32_type())) + } + + #[napi] + pub fn f64_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.f64_type())) + } + + #[napi] + pub fn char16_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.char16_type())) + } + + #[napi] + pub fn guid_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.guid_type())) + } + + #[napi] + pub fn hstring_type() -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.hstring())) + } + + #[napi] + pub fn hstring(value: String) -> DynWinRTValue { + DynWinRTValue::new(dynwinrt::WinRTValue::HString(windows::core::HSTRING::from( + value, + ))) + } + + #[napi] + pub fn pointer_type() -> DynComType { + DynComType(dynwinrt::com::Type::pointer()) + } + + #[napi] + pub fn interface_type(iid: &WinGUID) -> DynComType { + DynComType(dynwinrt::com::Type::winrt(TABLE.interface(iid.0))) + } + + #[napi] + pub fn bool_value(value: bool) -> DynWinRTValue { + DynWinRTValue::bool_value(value) + } + + #[napi] + pub fn i8_value(value: i32) -> DynWinRTValue { + DynWinRTValue::i8_value(value) + } + + #[napi] + pub fn u8_value(value: u32) -> DynWinRTValue { + DynWinRTValue::u8_value(value) + } + + #[napi] + pub fn i16(value: i32) -> DynWinRTValue { + DynWinRTValue::i16(value) + } + + #[napi] + pub fn u16(value: u32) -> DynWinRTValue { + DynWinRTValue::u16(value) + } + + #[napi] + pub fn i32(value: i32) -> DynWinRTValue { + DynWinRTValue::i32(value) + } + + #[napi] + pub fn u32(value: u32) -> DynWinRTValue { + DynWinRTValue::u32(value) + } + + #[napi] + pub fn i64(value: BigInt) -> napi::Result { + let (value, lossless) = value.get_i64(); + if !lossless { + return Err(napi::Error::from_reason( + "DynCom.i64(): value must fit in a signed 64-bit integer", + )); + } + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::I64(value))) + } + + #[napi] + pub fn u64(value: BigInt) -> napi::Result { + let (negative, value, lossless) = value.get_u64(); + if negative || !lossless { + return Err(napi::Error::from_reason( + "DynCom.u64(): value must fit in an unsigned 64-bit integer", + )); + } + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::U64(value))) + } + + #[napi] + pub fn isize(value: BigInt) -> napi::Result { + let (value, lossless) = value.get_i64(); + if !lossless { + return Err(napi::Error::from_reason( + "DynCom.isize(): value must fit in a pointer-sized signed integer", + )); + } + #[cfg(target_pointer_width = "64")] + { + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::I64(value))) + } + #[cfg(target_pointer_width = "32")] + { + let value = i32::try_from(value).map_err(|_| { + napi::Error::from_reason("DynCom.isize(): value must fit in a pointer-sized signed integer") + })?; + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::I32(value))) + } + } + + #[napi] + pub fn usize(value: BigInt) -> napi::Result { + let (negative, value, lossless) = value.get_u64(); + if negative || !lossless { + return Err(napi::Error::from_reason( + "DynCom.usize(): value must fit in a pointer-sized unsigned integer", + )); + } + #[cfg(target_pointer_width = "64")] + { + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::U64(value))) + } + #[cfg(target_pointer_width = "32")] + { + let value = u32::try_from(value).map_err(|_| { + napi::Error::from_reason( + "DynCom.usize(): value must fit in a pointer-sized unsigned integer", + ) + })?; + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::U32(value))) + } + } + + #[napi] + pub fn f32(value: f64) -> DynWinRTValue { + DynWinRTValue::f32(value) + } + + #[napi] + pub fn f64(value: f64) -> DynWinRTValue { + DynWinRTValue::f64(value) + } + + #[napi] + pub fn char16(value: u32) -> DynWinRTValue { + DynWinRTValue::new(dynwinrt::WinRTValue::U16(value as u16)) + } + + #[napi] + pub fn guid(value: &WinGUID) -> DynWinRTValue { + DynWinRTValue::guid(value) + } + + #[napi] + pub fn co_create_instance(clsid: String, iid: &WinGUID) -> napi::Result { + self::co_create_instance(clsid, iid) + } + + #[napi] + pub fn pointer( + #[napi(ts_arg_type = "bigint | number | Buffer | Uint8Array | null | undefined")] + value: Unknown, + ) -> napi::Result { + self::pointer(value) + } + + #[napi] + pub fn handle_value( + #[napi(ts_arg_type = "bigint | number | Buffer | Uint8Array")] value: Unknown, + ) -> napi::Result { + self::handle_value(value) + } + + #[napi] + pub fn iid_pointer(value: &WinGUID) -> DynWinRTValue { + self::iid_pointer(value) + } + + #[napi] + pub fn adopt_com_pointer( + value: &mut DynWinRTValue, + iid: Option<&WinGUID>, + ) -> napi::Result { + self::adopt_com_pointer(value, iid) + } + + #[napi] + pub fn adopt_co_task_mem_pointer(value: &mut DynWinRTValue) -> napi::Result { + self::adopt_co_task_mem_pointer(value) + } + + #[napi] + pub fn as_pointer_bigint(value: &DynWinRTValue) -> napi::Result { + self::as_pointer_bigint(value) + } + + #[napi] + pub fn to_number(value: &DynWinRTValue) -> i32 { + value.to_number() + } + + #[napi] + pub fn to_bool(value: &DynWinRTValue) -> bool { + value.to_bool() + } + + #[napi] + pub fn to_f64(value: &DynWinRTValue) -> f64 { + value.to_f64() + } + + #[napi] + pub fn to_guid_string(value: &DynWinRTValue) -> napi::Result { + value.to_guid().map(|guid| guid.to_string()) + } + + #[napi] + pub fn take_co_task_mem_wide_string(value: &mut DynWinRTValue) -> napi::Result { + self::take_co_task_mem_wide_string(value) + } + + #[napi] + pub fn take_co_task_mem_ansi_string(value: &mut DynWinRTValue) -> napi::Result { + self::take_co_task_mem_ansi_string(value) + } + + #[napi] + pub fn take_bstr(value: &mut DynWinRTValue) -> napi::Result { + self::take_bstr(value) + } + + #[napi] + pub fn to_u32(value: &DynWinRTValue) -> napi::Result { + match &value.0 { + dynwinrt::WinRTValue::U32(value) => Ok(*value), + _ => Err(napi::Error::from_reason("Value is not a u32")), + } + } + + #[napi] + pub fn to_i64_bigint(value: &DynWinRTValue) -> napi::Result { + match &value.0 { + dynwinrt::WinRTValue::I64(value) => Ok(BigInt::from(*value)), + _ => Err(napi::Error::from_reason("Value is not an i64")), + } + } + + #[napi] + pub fn to_u64_bigint(value: &DynWinRTValue) -> napi::Result { + match &value.0 { + dynwinrt::WinRTValue::U64(value) => Ok(BigInt::from(*value)), + _ => Err(napi::Error::from_reason("Value is not a u64")), + } + } + + #[napi] + pub fn to_isize_bigint(value: &DynWinRTValue) -> napi::Result { + #[cfg(target_pointer_width = "64")] + let result = match &value.0 { + dynwinrt::WinRTValue::I64(value) => Some(BigInt::from(*value)), + _ => None, + }; + #[cfg(target_pointer_width = "32")] + let result = match &value.0 { + dynwinrt::WinRTValue::I32(value) => Some(BigInt::from(i64::from(*value))), + _ => None, + }; + result.ok_or_else(|| napi::Error::from_reason("Value is not a pointer-sized signed integer")) + } + + #[napi] + pub fn to_usize_bigint(value: &DynWinRTValue) -> napi::Result { + #[cfg(target_pointer_width = "64")] + let result = match &value.0 { + dynwinrt::WinRTValue::U64(value) => Some(BigInt::from(*value)), + _ => None, + }; + #[cfg(target_pointer_width = "32")] + let result = match &value.0 { + dynwinrt::WinRTValue::U32(value) => Some(BigInt::from(u64::from(*value))), + _ => None, + }; + result.ok_or_else(|| napi::Error::from_reason("Value is not a pointer-sized unsigned integer")) + } + + #[napi] + pub fn create_test_hwnd() -> napi::Result { + self::create_test_hwnd() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn takes_and_clears_cotaskmem_wide_string() { + let text = "dynwinrt"; + let wide = text.encode_utf16().chain(Some(0)).collect::>(); + let bytes = wide.len() * std::mem::size_of::(); + let ptr = unsafe { windows::Win32::System::Com::CoTaskMemAlloc(bytes) }; + assert!(!ptr.is_null()); + unsafe { + std::ptr::copy_nonoverlapping(wide.as_ptr(), ptr.cast::(), wide.len()); + } + let mut value = DynWinRTValue::from_com_result(dynwinrt::WinRTValue::RawPtr(ptr)); + + assert_eq!(take_co_task_mem_wide_string(&mut value).unwrap(), text); + assert!(matches!(value.0, dynwinrt::WinRTValue::Null)); + } + + #[test] + fn consuming_native_output_pointer_clears_source_value() { + let ptr = 0x1234usize as *mut std::ffi::c_void; + let mut value = DynWinRTValue::from_com_result(dynwinrt::WinRTValue::RawPtr(ptr)); + + assert_eq!(take_native_output_pointer(&mut value, "test").unwrap(), ptr); + assert!(matches!(value.0, dynwinrt::WinRTValue::Null)); + assert!(take_native_output_pointer(&mut value, "test").is_err()); + } + + #[test] + fn borrowed_pointer_cannot_be_adopted() { + let ptr = 0x1234usize as *mut std::ffi::c_void; + let mut value = DynWinRTValue::with_borrowed_pointer(dynwinrt::WinRTValue::RawPtr(ptr)); + + let error = take_native_output_pointer(&mut value, "COM interface").unwrap_err(); + assert!( + error + .reason + .contains("Cannot adopt a borrowed COM interface") + ); + assert!(matches!(value.0, dynwinrt::WinRTValue::RawPtr(raw) if raw == ptr)); + } + + #[test] + fn takes_and_frees_bstr() { + let raw = windows::core::BSTR::from("dynwinrt").into_raw(); + let mut value = + DynWinRTValue::from_com_result(dynwinrt::WinRTValue::RawPtr(raw as *mut std::ffi::c_void)); + + assert_eq!(take_bstr(&mut value).unwrap(), "dynwinrt"); + assert!(matches!(value.0, dynwinrt::WinRTValue::Null)); + } + + #[test] + fn managed_com_object_address_is_not_exported() { + dynwinrt::com::initialize_apartment(dynwinrt::com::ApartmentType::MultiThreaded).unwrap(); + let iid = WinGUID(GUID::from_u128(0x000214f9_0000_0000_c000_000000000046)); + let value = co_create_instance("00021401-0000-0000-c000-000000000046".into(), &iid).unwrap(); + + let error = as_pointer_bigint(&value).unwrap_err(); + assert!( + error + .reason + .contains("Managed COM objects cannot be exported") + ); + } + + #[test] + fn pointer_sized_values_use_the_current_target_width() { + let signed = DynCom::isize(BigInt::from(-1i64)).unwrap(); + let unsigned = DynCom::usize(BigInt::from(1u64)).unwrap(); + #[cfg(target_pointer_width = "64")] + { + assert!(matches!(signed.0, dynwinrt::WinRTValue::I64(-1))); + assert!(matches!(unsigned.0, dynwinrt::WinRTValue::U64(1))); + } + #[cfg(target_pointer_width = "32")] + { + assert!(matches!(signed.0, dynwinrt::WinRTValue::I32(-1))); + assert!(matches!(unsigned.0, dynwinrt::WinRTValue::U32(1))); + } + } + + #[test] + fn iid_pointer_is_owner_backed_and_holds_the_guid() { + // Regression (#4): iid_pointer must return an OWNER-BACKED value so the + // boxed GUID is freed on drop/GC — not leak one Box per distinct GUID + // into a process-lifetime static cache. The pre-fix version returned an + // unowned RawPtr (`.1 == None`) into a static cache (stable address per + // GUID), so both assertions below fail against it. + let guid = GUID::from_u128(0xa5caee9b_8708_49d1_8d36_67d25a8da00c); + + let value = iid_pointer(&WinGUID(guid)); + assert!( + value.1.is_some(), + "iid_pointer must be owner-backed (NativePointerOwner::Guid) so it frees on drop" + ); + match value.0 { + dynwinrt::WinRTValue::RawPtr(ptr) => { + assert!(!ptr.is_null()); + let read = unsafe { *(ptr as *const GUID) }; + assert_eq!( + read, guid, + "REFIID pointer must hold the correct GUID bytes" + ); + } + _ => panic!("iid_pointer must return a RawPtr"), + } + + // Two concurrently-live calls for the SAME GUID must allocate distinct + // boxes (distinct addresses) — proving there is no shared static cache. + let a = iid_pointer(&WinGUID(guid)); + let b = iid_pointer(&WinGUID(guid)); + let pa = match a.0 { + dynwinrt::WinRTValue::RawPtr(p) => p as usize, + _ => 0, + }; + let pb = match b.0 { + dynwinrt::WinRTValue::RawPtr(p) => p as usize, + _ => 0, + }; + assert_ne!( + pa, pb, + "each iid_pointer call must own its own boxed GUID, not share a static one" + ); + } +} diff --git a/bindings/js/src/lib.rs b/bindings/js/src/lib.rs index 02d8f8d2..bbdb7686 100644 --- a/bindings/js/src/lib.rs +++ b/bindings/js/src/lib.rs @@ -11,12 +11,14 @@ use std::{ }; use dynwinrt; +use napi::Env; use napi::bindgen_prelude::{BigInt, PromiseRaw}; use napi::threadsafe_function::ThreadsafeFunctionCallMode; -use napi::Env; use napi_derive::napi; -use windows::core::{IUnknown, Interface, HSTRING}; +use windows::core::{HSTRING, IUnknown, Interface}; +mod com; +pub use com::{DynCom, DynComInterface, DynComMethodHandle, DynComMethodSig, DynComType}; mod async_promise; mod scheduled_start; @@ -103,7 +105,7 @@ pub fn get_winappsdk_resource_pri_path() -> napi::Result { #[napi] pub fn ro_initialize(apartment_type: Option) { use windows::Win32::System::WinRT::{ - RoInitialize, RO_INIT_MULTITHREADED, RO_INIT_SINGLETHREADED, + RO_INIT_MULTITHREADED, RO_INIT_SINGLETHREADED, RoInitialize, }; let init_type = match apartment_type.unwrap_or(1) { 0 => RO_INIT_SINGLETHREADED, @@ -415,7 +417,7 @@ impl DynWinRTMethodHandle { _ => { return Err(napi::Error::from_reason( "invoke() requires an Object value", - )) + )); } }; let wrt_args: Vec = args.iter().map(|a| a.0.clone()).collect(); @@ -424,9 +426,9 @@ impl DynWinRTMethodHandle { .invoke(raw, &wrt_args) .map_err(|e| napi::Error::from_reason(e.message()))?; if results.is_empty() { - Ok(DynWinRTValue(dynwinrt::WinRTValue::I32(0))) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::I32(0))) } else { - Ok(DynWinRTValue(results.into_iter().next().ok_or_else( + Ok(DynWinRTValue::new(results.into_iter().next().ok_or_else( || napi::Error::from_reason("invoke: method returned no results"), )?)) } @@ -462,7 +464,7 @@ impl DynWinRTMethodHandle { _ => { return Err(napi::Error::from_reason( "invoke_all() requires an Object value", - )) + )); } }; let wrt_args: Vec = args.iter().map(|a| a.0.clone()).collect(); @@ -470,7 +472,7 @@ impl DynWinRTMethodHandle { .0 .invoke(raw, &wrt_args) .map_err(|e| napi::Error::from_reason(e.message()))?; - Ok(results.into_iter().map(DynWinRTValue).collect()) + Ok(results.into_iter().map(DynWinRTValue::new).collect()) } // --- Fast paths: skip Vec alloc + skip DynWinRTValue wrapping for result --- @@ -529,7 +531,7 @@ impl DynWinRTMethodHandle { self .0 .call_getter_object(raw) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(|e| napi::Error::from_reason(e.message())) } @@ -545,7 +547,7 @@ impl DynWinRTMethodHandle { .0 .invoke(raw, &[dynwinrt::WinRTValue::HString(HSTRING::from(arg))]) .map_err(|e| napi::Error::from_reason(e.message()))?; - Ok(DynWinRTValue(results.into_iter().next().ok_or_else( + Ok(DynWinRTValue::new(results.into_iter().next().ok_or_else( || napi::Error::from_reason("invoke_hstring: no result"), )?)) } @@ -562,7 +564,7 @@ impl DynWinRTMethodHandle { .0 .invoke(raw, &[dynwinrt::WinRTValue::I32(arg)]) .map_err(|e| napi::Error::from_reason(e.message()))?; - Ok(DynWinRTValue(results.into_iter().next().ok_or_else( + Ok(DynWinRTValue::new(results.into_iter().next().ok_or_else( || napi::Error::from_reason("invoke_i32: no result"), )?)) } @@ -573,10 +575,37 @@ impl DynWinRTMethodHandle { // ====================================================================== #[napi] -pub struct DynWinRTValue(dynwinrt::WinRTValue); +pub struct DynWinRTValue( + dynwinrt::WinRTValue, + Option, + com::PointerProvenance, +); unsafe impl Send for DynWinRTValue {} unsafe impl Sync for DynWinRTValue {} +impl DynWinRTValue { + fn new(value: dynwinrt::WinRTValue) -> Self { + Self(value, None, com::PointerProvenance::None) + } + + fn with_pointer_owner(value: dynwinrt::WinRTValue, owner: com::NativePointerOwner) -> Self { + Self(value, Some(owner), com::PointerProvenance::Borrowed) + } + + fn with_borrowed_pointer(value: dynwinrt::WinRTValue) -> Self { + Self(value, None, com::PointerProvenance::Borrowed) + } + + fn from_com_result(value: dynwinrt::WinRTValue) -> Self { + let provenance = if matches!(value, dynwinrt::WinRTValue::RawPtr(_)) { + com::PointerProvenance::NativeOutput + } else { + com::PointerProvenance::None + }; + Self(value, None, provenance) + } +} + impl Drop for DynWinRTValue { fn drop(&mut self) { // After Application.Start returns, XAML has already torn down its thread @@ -595,6 +624,8 @@ impl DynWinRTValue { #[napi] pub fn release(&mut self) { self.0 = dynwinrt::WinRTValue::Null; + self.1 = None; + self.2 = com::PointerProvenance::None; } #[napi] @@ -602,7 +633,7 @@ impl DynWinRTValue { let factory = dynwinrt::ro_get_activation_factory_2(&HSTRING::from(&name)).map_err(|e| { napi::Error::from_reason(format!("ActivationFactory '{}': {}", name, e.message())) })?; - Ok(DynWinRTValue(factory)) + Ok(DynWinRTValue::new(factory)) } /// Create a composed WinUI Application that forwards IXamlMetadataProvider @@ -623,7 +654,7 @@ impl DynWinRTValue { }) .transpose()?; dynwinrt::create_xaml_application(&provider, callback.as_ref()) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(|e| { napi::Error::from_reason(format!("createXamlApplication failed: {}", e.message())) }) @@ -631,52 +662,52 @@ impl DynWinRTValue { #[napi] pub fn bool_value(value: bool) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Bool(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::Bool(value)) } #[napi] pub fn i8_value(value: i32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::I8(value as i8)) + DynWinRTValue::new(dynwinrt::WinRTValue::I8(value as i8)) } #[napi] pub fn u8_value(value: u32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::U8(value as u8)) + DynWinRTValue::new(dynwinrt::WinRTValue::U8(value as u8)) } #[napi] pub fn i16(value: i32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::I16(value as i16)) + DynWinRTValue::new(dynwinrt::WinRTValue::I16(value as i16)) } #[napi] pub fn u16(value: u32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::U16(value as u16)) + DynWinRTValue::new(dynwinrt::WinRTValue::U16(value as u16)) } #[napi] pub fn i32(value: i32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::I32(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::I32(value)) } #[napi] pub fn u32(value: u32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::U32(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::U32(value)) } #[napi] pub fn i64(value: i64) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::I64(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::I64(value)) } #[napi] pub fn u64(value: i64) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::U64(value as u64)) + DynWinRTValue::new(dynwinrt::WinRTValue::U64(value as u64)) } #[napi] pub fn f32(value: f64) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::F32(value as f32)) + DynWinRTValue::new(dynwinrt::WinRTValue::F32(value as f32)) } #[napi] pub fn f64(value: f64) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::F64(value)) + DynWinRTValue::new(dynwinrt::WinRTValue::F64(value)) } /// Create an enum value from an i32. The type_handle must be an enum type. #[napi] pub fn enum_value(enum_type: &DynWinRTType, value: i32) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Enum { + DynWinRTValue::new(dynwinrt::WinRTValue::Enum { value, type_handle: enum_type.0.clone(), }) @@ -688,7 +719,7 @@ impl DynWinRTValue { value_type: &DynWinRTType, ) -> napi::Result { dynwinrt::box_ireference(value.0.clone(), value_type.0.clone()) - .map(DynWinRTValue) + .map(DynWinRTValue::new) .map_err(|e| napi::Error::from_reason(e.message())) } @@ -712,15 +743,15 @@ impl DynWinRTValue { #[napi] pub fn hstring(value: String) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::HString(HSTRING::from(value))) + DynWinRTValue::new(dynwinrt::WinRTValue::HString(HSTRING::from(value))) } #[napi] pub fn guid(value: &WinGUID) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Guid(value.0)) + DynWinRTValue::new(dynwinrt::WinRTValue::Guid(value.0)) } #[napi] pub fn null_value() -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Null) + DynWinRTValue::new(dynwinrt::WinRTValue::Null) } /// Create an IVector from items. The element_type is used for IID computation. @@ -734,7 +765,7 @@ impl DynWinRTValue { let wrt_items: Vec = items.iter().map(|i| i.0.clone()).collect(); let vector = dynwinrt::vector::create_vector_from_values(&wrt_items, &element_type.0, iids) .map_err(|error| napi::Error::from_reason(error.message()))?; - Ok(DynWinRTValue(dynwinrt::WinRTValue::Object(vector))) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Object(vector))) } /// Create an IMap from parallel key/value arrays. @@ -759,7 +790,7 @@ impl DynWinRTValue { .collect(); let map = dynwinrt::map::create_map_from_values(&entries, &key_type.0, &value_type.0, iids) .map_err(|error| napi::Error::from_reason(error.message()))?; - Ok(DynWinRTValue(dynwinrt::WinRTValue::Object(map))) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Object(map))) } #[napi] @@ -811,7 +842,10 @@ impl DynWinRTValue { .weak::() .build()?; let progress_cb: dynwinrt::ProgressCallback = Box::new(move |val: dynwinrt::WinRTValue| { - tsfn.call(DynWinRTValue(val), ThreadsafeFunctionCallMode::NonBlocking); + tsfn.call( + DynWinRTValue::new(val), + ThreadsafeFunctionCallMode::NonBlocking, + ); }); let handler = dynwinrt::create_progress_handler(handler_iid, progress_type, progress_cb); @@ -853,7 +887,7 @@ impl DynWinRTValue { .0 .cast(&iid.0) .map_err(|e| napi::Error::from_reason(format!("QueryInterface failed: {}", e.message())))?; - Ok(DynWinRTValue(result)) + Ok(DynWinRTValue::new(result)) } #[napi] @@ -986,14 +1020,14 @@ impl DynWinRTArray { /// Per-element access (works for all element types). #[napi] pub fn get(&self, index: u32) -> DynWinRTValue { - DynWinRTValue(self.0.get(index as usize)) + DynWinRTValue::new(self.0.get(index as usize)) } /// Convert all elements to DynWinRTValue array. #[napi] pub fn to_values(&self) -> Vec { (0..self.0.len()) - .map(|i| DynWinRTValue(self.0.get(i))) + .map(|i| DynWinRTValue::new(self.0.get(i))) .collect() } @@ -1225,7 +1259,7 @@ impl DynWinRTArray { /// Wrap as DynWinRTValue::Array for passing to call(). #[napi] pub fn to_value(&self) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Array(self.0.clone())) + DynWinRTValue::new(dynwinrt::WinRTValue::Array(self.0.clone())) } } @@ -1399,12 +1433,12 @@ impl DynWinRTStruct { let inner = self.0.get_field_struct(index as usize); let raw = unsafe { *(inner.as_ptr() as *const *mut std::ffi::c_void) }; if raw.is_null() { - Ok(DynWinRTValue(dynwinrt::WinRTValue::Null)) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Null)) } else { let obj = unsafe { IUnknown::from_raw_borrowed(&raw) } .ok_or_else(|| napi::Error::from_reason("null COM pointer"))? .clone(); - Ok(DynWinRTValue(dynwinrt::WinRTValue::Object(obj))) + Ok(DynWinRTValue::new(dynwinrt::WinRTValue::Object(obj))) } } @@ -1436,7 +1470,7 @@ impl DynWinRTStruct { /// Wrap as DynWinRTValue::Struct for passing to call(). #[napi] pub fn to_value(&self) -> DynWinRTValue { - DynWinRTValue(dynwinrt::WinRTValue::Struct(self.0.clone())) + DynWinRTValue::new(dynwinrt::WinRTValue::Struct(self.0.clone())) } } @@ -1457,8 +1491,8 @@ pub fn has_package_identity() -> bool { pub fn get_computer_name() -> napi::Result { #[cfg(target_os = "windows")] { - use windows::core::PWSTR; use windows::Win32::System::WindowsProgramming::GetComputerNameW; + use windows::core::PWSTR; let mut buffer = [0u16; 256]; let mut size = buffer.len() as u32; @@ -1661,8 +1695,8 @@ impl DynWinRtDelegate { #[napi(ts_arg_type = "(...args: DynWinRTValue[]) => void")] callback: napi::bindgen_prelude::Function<'static, Vec, ()>, ) -> napi::Result { - use napi::bindgen_prelude::ToNapiValue; use napi::JsValue; + use napi::bindgen_prelude::ToNapiValue; use windows::Win32::System::Threading::GetCurrentThreadId; // Track the thread we were registered on. WinRT delegate callbacks that @@ -1707,7 +1741,8 @@ impl DynWinRtDelegate { const E_UNEXPECTED: windows::core::HRESULT = windows::core::HRESULT(0x8000FFFFu32 as i32); let current_tid = unsafe { GetCurrentThreadId() }; - let js_args: Vec = args.iter().map(|a| DynWinRTValue(a.clone())).collect(); + let js_args: Vec = + args.iter().map(|a| DynWinRTValue::new(a.clone())).collect(); if current_tid == register_tid { // Same-thread synchronous direct invocation. Bypass the TSFN because @@ -1808,7 +1843,7 @@ impl DynWinRtDelegate { /// Get the delegate as a DynWinRtValue for passing to WinRT methods. #[napi] pub fn to_value(&self) -> DynWinRTValue { - DynWinRTValue(self.0.clone()) + DynWinRTValue::new(self.0.clone()) } } @@ -1880,8 +1915,8 @@ impl DynWinRtElementFactory { #[napi(ts_arg_type = "(args: DynWinRtValue) => void")] recycle_element: ElementFactoryRecycleFunction, ) -> napi::Result { - use napi::bindgen_prelude::{FromNapiValue, ToNapiValue}; use napi::JsValue; + use napi::bindgen_prelude::{FromNapiValue, ToNapiValue}; use windows::Win32::System::Threading::GetCurrentThreadId; const E_FAIL: windows::core::HRESULT = windows::core::HRESULT(0x80004005u32 as i32); @@ -1918,7 +1953,7 @@ impl DynWinRtElementFactory { Err(_) => return Err(E_FAIL), }; let raw_env = get_env.0; - let js_arg = DynWinRTValue(args.clone()); + let js_arg = DynWinRTValue::new(args.clone()); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe( || -> napi::Result { unsafe { @@ -1995,7 +2030,7 @@ impl DynWinRtElementFactory { Err(_) => return E_FAIL, }; let raw_env = recycle_env.0; - let js_arg = DynWinRTValue(args.clone()); + let js_arg = DynWinRTValue::new(args.clone()); let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| -> napi::Result<()> { unsafe { @@ -2059,7 +2094,7 @@ impl DynWinRtElementFactory { #[napi] pub fn to_value(&self) -> DynWinRTValue { - DynWinRTValue(self.value.clone()) + DynWinRTValue::new(self.value.clone()) } #[napi] diff --git a/crates/dynwinrt/Cargo.toml b/crates/dynwinrt/Cargo.toml index 5da7bc62..7a352f36 100644 --- a/crates/dynwinrt/Cargo.toml +++ b/crates/dynwinrt/Cargo.toml @@ -20,6 +20,7 @@ windows-metadata = "0.59.0" version = ">=0.59, <=0.62" features = [ "ApplicationModel", + "ApplicationModel_DataTransfer", "Data_Xml_Dom", "Foundation_Collections", "Devices_Geolocation", @@ -31,6 +32,8 @@ features = [ "Win32_System_LibraryLoader", "Win32_System_Threading", "Win32_System_WinRT", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging", "Win32_Storage_Packaging_Appx", "Win32_UI_HiDpi", "Management_Deployment", diff --git a/crates/dynwinrt/src/call.rs b/crates/dynwinrt/src/call.rs index 829fce31..cabf9d40 100644 --- a/crates/dynwinrt/src/call.rs +++ b/crates/dynwinrt/src/call.rs @@ -5,7 +5,11 @@ use core::ffi::c_void; use libffi::middle::{Arg, arg}; use windows_core::{HRESULT, Interface}; -use crate::{abi::AbiValue, signature::Parameter, value::WinRTValue}; +use crate::{ + abi::{AbiType, AbiValue}, + native_call::{MethodReturn, Parameter}, + value::WinRTValue, +}; pub(crate) trait ArgumentList { fn get_value(&self, index: usize) -> &WinRTValue; @@ -74,6 +78,7 @@ macro_rules! dispatch_scalar { WinRTValue::F64(v) => $call(*v), WinRTValue::Object(o) => $call(o.as_raw()), WinRTValue::Null => $call(std::ptr::null_mut::()), + WinRTValue::RawPtr(p) => $call(*p), WinRTValue::Guid(g) => $call(*g), _ => panic!("dispatch_scalar: unsupported type {:?}", $in_val), } @@ -116,7 +121,7 @@ pub fn call_fill_array_1in( }) } -use crate::metadata_table::{TypeHandle, TypeKind}; +use crate::metadata_table::TypeHandle; /// Stable heap storage for array in-param data. /// Owns the serialized byte buffer so it stays alive for the FFI call. @@ -179,12 +184,40 @@ impl Drop for FillArraySlot { } } -pub fn call_winrt_method_dynamic( +fn input_abi_value(value: &WinRTValue) -> windows_core::Result { + let value = match value { + WinRTValue::Bool(value) => AbiValue::Bool(u8::from(*value)), + WinRTValue::I8(value) => AbiValue::I8(*value), + WinRTValue::U8(value) => AbiValue::U8(*value), + WinRTValue::I16(value) => AbiValue::I16(*value), + WinRTValue::U16(value) => AbiValue::U16(*value), + WinRTValue::I32(value) => AbiValue::I32(*value), + WinRTValue::U32(value) => AbiValue::U32(*value), + WinRTValue::I64(value) => AbiValue::I64(*value), + WinRTValue::U64(value) => AbiValue::U64(*value), + WinRTValue::F32(value) => AbiValue::F32(*value), + WinRTValue::F64(value) => AbiValue::F64(*value), + WinRTValue::HResult(value) => AbiValue::I32(value.0), + WinRTValue::Enum { value, .. } => AbiValue::I32(*value), + WinRTValue::RawPtr(value) => AbiValue::Pointer(*value), + WinRTValue::Null => AbiValue::Pointer(std::ptr::null_mut()), + _ => { + return Err(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + "unsupported in/out argument value", + )); + } + }; + Ok(value) +} + +pub fn call_method_dynamic( vtable_index: usize, obj: *mut c_void, parameters: &[Parameter], args: &A, out_count: usize, + return_kind: &MethodReturn, cif: &libffi::middle::Cif, ) -> windows_core::Result> { use crate::metadata_table::ValueTypeData; @@ -195,6 +228,7 @@ pub fn call_winrt_method_dynamic( let mut out_values: Vec = Vec::with_capacity(out_count); let mut out_ptrs: Vec<*const std::ffi::c_void> = Vec::with_capacity(out_count); let mut struct_out_values: Vec> = Vec::with_capacity(out_count); + let mut guid_out_values: Vec>> = Vec::with_capacity(out_count); // Array storage: Box'd for pointer stability (addresses don't change after creation) let mut array_out_slots: Vec> = Vec::new(); @@ -241,6 +275,7 @@ pub fn call_winrt_method_dynamic( out_values.push(AbiValue::Pointer(std::ptr::null_mut())); out_ptrs.push(std::ptr::null()); struct_out_values.push(None); + guid_out_values.push(None); array_out_map.push(None); } else if p.typ.is_array() { let slot = Box::new(ArrayOutSlot { @@ -257,18 +292,46 @@ pub fn call_winrt_method_dynamic( out_values.push(AbiValue::Pointer(std::ptr::null_mut())); out_ptrs.push(std::ptr::null()); struct_out_values.push(None); + guid_out_values.push(None); fill_array_map.push(None); - } else if matches!(p.typ.kind(), TypeKind::Struct(_)) { - let val = p.typ.default_value(); + } else if p.typ.is_guid() { + let value = Box::new(windows_core::GUID::zeroed()); + out_ptrs.push((&*value as *const windows_core::GUID).cast()); + out_values.push(AbiValue::Pointer(std::ptr::null_mut())); + struct_out_values.push(None); + guid_out_values.push(Some(value)); + array_out_map.push(None); + fill_array_map.push(None); + } else if p.typ.is_struct() { + let val = if p.is_in_out() { + args.get_value(p.input_index.expect("in/out input index")) + .as_struct() + .ok_or_else(|| { + windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + "expected struct value for in/out parameter", + ) + })? + .clone() + } else { + p.typ.default_struct_value() + }; out_ptrs.push(val.as_ptr() as *const std::ffi::c_void); out_values.push(AbiValue::Pointer(std::ptr::null_mut())); struct_out_values.push(Some(val)); + guid_out_values.push(None); array_out_map.push(None); fill_array_map.push(None); } else { - out_values.push(p.typ.abi_type().default_value()); + let value = if p.is_in_out() { + input_abi_value(args.get_value(p.input_index.expect("in/out input index")))? + } else { + p.typ.abi_type().default_value() + }; + out_values.push(value); out_ptrs.push(out_values.last().unwrap().as_out_ptr()); struct_out_values.push(None); + guid_out_values.push(None); array_out_map.push(None); fill_array_map.push(None); } @@ -277,7 +340,7 @@ pub fn call_winrt_method_dynamic( // Phase 1b: Pre-compute all array in-param data (must happen before Phase 2) for p in parameters { - if !p.is_out() && p.typ.is_array() { + if p.is_input() && !p.is_out() && p.typ.is_array() { let array_data = args .get_value(p.value_index) .as_array() @@ -322,8 +385,47 @@ pub fn call_winrt_method_dynamic( } // Phase 3: Call - let hr: windows_core::HRESULT = unsafe { cif.call(CodePtr(fptr), &ffi_args) }; - hr.ok()?; + let return_value = unsafe { + match return_kind { + MethodReturn::HResult => { + let hr: windows_core::HRESULT = cif.call(CodePtr(fptr), &ffi_args); + hr.ok()?; + None + } + MethodReturn::SemanticHResult => { + let hr: windows_core::HRESULT = cif.call(CodePtr(fptr), &ffi_args); + hr.ok()?; + Some(WinRTValue::HResult(hr)) + } + MethodReturn::Void => { + cif.call::<()>(CodePtr(fptr), &ffi_args); + None + } + MethodReturn::Value(typ) => { + let value = match typ.abi_type() { + AbiType::Bool => AbiValue::Bool(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::I8 => AbiValue::I8(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::U8 => AbiValue::U8(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::I16 => AbiValue::I16(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::U16 => AbiValue::U16(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::I32 => AbiValue::I32(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::U32 => AbiValue::U32(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::I64 => AbiValue::I64(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::U64 => AbiValue::U64(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::F32 => AbiValue::F32(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::F64 => AbiValue::F64(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::Guid => AbiValue::Guid(cif.call(CodePtr(fptr), &ffi_args)), + AbiType::Ptr => AbiValue::Pointer(cif.call(CodePtr(fptr), &ffi_args)), + }; + Some(typ.from_out_value(&value).map_err(|error| { + windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &error.message(), + ) + })?) + } + } + }; // Counted FillArray methods (for example GetMany) carry the actual count // as their UInt32 retval. Other FillArray methods write the full capacity. @@ -334,7 +436,7 @@ pub fn call_winrt_method_dynamic( .iter() .rev() .find(|param| param.is_out() && !param.is_fill_array()) - .filter(|param| matches!(param.typ.kind(), TypeKind::U32)) + .filter(|param| param.typ.is_u32()) .and_then(|param| match out_values[param.value_index] { AbiValue::U32(value) => Some(value), _ => None, @@ -342,7 +444,11 @@ pub fn call_winrt_method_dynamic( }; // Phase 4: Extract results - let mut result_values: Vec = Vec::with_capacity(out_count); + let mut result_values: Vec = + Vec::with_capacity(out_count + usize::from(return_value.is_some())); + if let Some(value) = return_value { + result_values.push(value); + } for p in parameters { if p.is_out() { if let Some(slot_idx) = fill_array_map[p.value_index] { @@ -382,6 +488,8 @@ pub fn call_winrt_method_dynamic( ) }; result_values.push(WinRTValue::Array(array_value)); + } else if let Some(guid) = guid_out_values[p.value_index].take() { + result_values.push(WinRTValue::Guid(*guid)); } else if let Some(struct_val) = struct_out_values[p.value_index].take() { result_values.push(WinRTValue::Struct(struct_val)); } else { diff --git a/crates/dynwinrt/src/com.rs b/crates/dynwinrt/src/com.rs new file mode 100644 index 00000000..118bd258 --- /dev/null +++ b/crates/dynwinrt/src/com.rs @@ -0,0 +1,1025 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use core::ffi::c_void; +use std::{ + cell::RefCell, + sync::{Arc, RwLock}, +}; + +use windows::Win32::System::Com::{ + CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, COINIT_MULTITHREADED, CoCreateInstance, + CoInitializeEx, CoUninitialize, +}; +use windows_core::{GUID, IUnknown, Interface as WindowsInterface}; + +use crate::{ + MetadataTable, TypeHandle, WinRTValue, + native_call::{AbiMethodSignature, Method as NativeMethod, ParameterType}, + result, +}; + +const RPC_E_CHANGED_MODE: windows_core::HRESULT = windows_core::HRESULT(0x80010106u32 as i32); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InterfaceBase { + IUnknown, + IInspectable, +} + +impl InterfaceBase { + pub const fn first_method_slot(self) -> usize { + match self { + Self::IUnknown => 3, + Self::IInspectable => 6, + } + } +} + +#[derive(Debug, Clone)] +pub struct Type(ParameterType); + +impl Type { + pub fn winrt(typ: TypeHandle) -> Self { + Self(ParameterType::winrt(typ)) + } + + pub fn pointer() -> Self { + Self(ParameterType::pointer()) + } +} + +#[derive(Debug, Clone)] +pub struct MethodSignature(AbiMethodSignature); + +impl MethodSignature { + pub fn new(table: &std::sync::Arc) -> Self { + Self(AbiMethodSignature::new(table)) + } + + pub fn add_in(self, typ: Type) -> Self { + Self(self.0.add_in_type(typ.0)) + } + + pub fn add_out(self, typ: Type) -> Self { + Self(self.0.add_out_type(typ.0)) + } + + pub fn add_in_out(self, typ: Type) -> Self { + Self(self.0.add_in_out_type(typ.0)) + } + + pub fn add_out_fill(self, typ: Type) -> Self { + Self(self.0.add_out_fill_type(typ.0)) + } + + pub fn returns(self, typ: Type) -> Self { + Self(self.0.returns_type(typ.0)) + } + + pub fn returns_void(self) -> Self { + Self(self.0.returns_void()) + } + + pub fn preserve_hresult(self) -> Self { + Self(self.0.preserve_hresult()) + } +} + +#[derive(Debug)] +struct RegisteredMethod(NativeMethod); + +// Safety: a RegisteredMethod is fully built before publication and remains +// immutable. NativeMethod invokes libffi's CIF only through shared references; +// ffi_call treats the prepared CIF and its type graph as read-only. +unsafe impl Send for RegisteredMethod {} +unsafe impl Sync for RegisteredMethod {} + +#[derive(Debug, Clone)] +pub struct Interface { + name: String, + iid: GUID, + base_slot: usize, + methods: Arc)>>>, +} + +impl Interface { + pub fn name(&self) -> &str { + &self.name + } + + pub fn iid(&self) -> GUID { + self.iid + } + + pub fn add_method(self, name: &str, signature: MethodSignature) -> Self { + let mut methods = self.methods.write().unwrap(); + if methods.iter().any(|(existing, _)| existing == name) { + drop(methods); + return self; + } + let vtable_index = self.base_slot + methods.len(); + methods.push(( + name.to_string(), + Arc::new(RegisteredMethod(signature.0.build(vtable_index))), + )); + drop(methods); + self + } + + pub fn method(&self, vtable_index: usize) -> Option { + let local_index = vtable_index.checked_sub(self.base_slot)?; + self.methods + .read() + .unwrap() + .get(local_index) + .map(|(_, method)| MethodHandle(Arc::clone(method))) + } +} + +#[derive(Clone)] +pub struct MethodHandle(Arc); + +impl std::fmt::Debug for MethodHandle { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MethodHandle").finish_non_exhaustive() + } +} + +impl MethodHandle { + pub fn invoke(&self, obj: *mut c_void, args: &[WinRTValue]) -> result::Result> { + self.0 + .0 + .call_dynamic(obj, args) + .map_err(result::Error::WindowsError) + } + + pub fn call_getter_hstring(&self, obj: *mut c_void) -> result::Result { + self.0 + .0 + .call_getter_hstring(obj) + .map_err(result::Error::WindowsError) + } +} + +pub fn register_interface( + _table: &std::sync::Arc, + name: &str, + iid: GUID, + base: InterfaceBase, +) -> Interface { + Interface { + name: name.to_string(), + iid, + base_slot: base.first_method_slot(), + methods: Arc::new(RwLock::new(Vec::new())), + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ApartmentType { + SingleThreaded, + MultiThreaded, +} + +impl ApartmentType { + fn as_flag(self) -> windows::Win32::System::Com::COINIT { + match self { + Self::SingleThreaded => COINIT_APARTMENTTHREADED, + Self::MultiThreaded => COINIT_MULTITHREADED, + } + } +} + +struct ComApartment { + apartment_type: ApartmentType, +} + +impl Drop for ComApartment { + fn drop(&mut self) { + unsafe { CoUninitialize() }; + } +} + +enum ComInitialization { + Uninitialized, + Owned(ComApartment), +} + +thread_local! { + static COM_INITIALIZATION: RefCell = + const { RefCell::new(ComInitialization::Uninitialized) }; +} + +pub fn initialize_apartment(apartment_type: ApartmentType) -> result::Result<()> { + COM_INITIALIZATION.with(|state| { + if let ComInitialization::Owned(existing) = &*state.borrow() { + return if existing.apartment_type == apartment_type { + Ok(()) + } else { + Err(result::Error::WindowsError( + windows_core::Error::from_hresult(RPC_E_CHANGED_MODE), + )) + }; + } + + let hr = unsafe { CoInitializeEx(None, apartment_type.as_flag()) }; + if hr.is_ok() { + *state.borrow_mut() = ComInitialization::Owned(ComApartment { apartment_type }); + Ok(()) + } else { + Err(result::Error::WindowsError( + windows_core::Error::from_hresult(hr), + )) + } + }) +} + +pub fn co_create_instance(clsid: GUID, iid: GUID) -> result::Result { + let unknown: IUnknown = unsafe { CoCreateInstance(&clsid, None, CLSCTX_INPROC_SERVER) } + .map_err(result::Error::WindowsError)?; + let mut result = std::ptr::null_mut(); + unsafe { unknown.query(&iid, &mut result) } + .ok() + .map_err(result::Error::WindowsError)?; + Ok(WinRTValue::Object(unsafe { IUnknown::from_raw(result) })) +} + +/// Adopt an AddRef-owned COM interface pointer into a managed Object value. +/// +/// The pointer must represent a caller-owned COM reference (+1). This function +/// takes ownership with `IUnknown::from_raw` and must not be used for borrowed +/// pointers. +pub unsafe fn adopt_com_pointer(ptr: *mut c_void) -> WinRTValue { + if ptr.is_null() { + WinRTValue::Null + } else { + WinRTValue::Object(unsafe { IUnknown::from_raw(ptr) }) + } +} + +pub fn call_method( + vtable_index: usize, + obj: *mut c_void, + signature: MethodSignature, + args: &[WinRTValue], +) -> result::Result> { + signature + .0 + .build(vtable_index) + .call_dynamic(obj, args) + .map_err(result::Error::WindowsError) +} + +#[cfg(test)] +fn call_method_1_ptr( + vtable_index: usize, + obj: *mut c_void, + ptr: *const c_void, +) -> result::Result<()> { + crate::call::call_winrt_method_1(vtable_index, obj, ptr) + .ok() + .map_err(result::Error::WindowsError) +} + +#[cfg(test)] +fn call_method_2_ptr_i32( + vtable_index: usize, + obj: *mut c_void, + ptr: *mut c_void, + value: i32, +) -> result::Result<()> { + crate::call::call_winrt_method_2(vtable_index, obj, ptr, value) + .ok() + .map_err(result::Error::WindowsError) +} + +#[cfg(test)] +fn wide_null(text: &str) -> Vec { + text.encode_utf16().chain(std::iter::once(0)).collect() +} + +#[cfg(test)] +fn wide_buffer(characters: usize) -> Vec { + vec![0; characters] +} + +#[cfg(test)] +fn wide_to_string(buffer: &[u16]) -> String { + let end = buffer + .iter() + .position(|ch| *ch == 0) + .unwrap_or(buffer.len()); + String::from_utf16_lossy(&buffer[..end]) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + MetadataTable, com_helpers::E_NOINTERFACE, ro_get_activation_factory_2, + roapi::query_interface, + }; + use std::sync::atomic::{AtomicU32, Ordering}; + use windows::{ + ApplicationModel::DataTransfer::DataTransferManager, + Win32::{ + System::Com::{CoGetMalloc, IMalloc, IPersistFile, IStream}, + UI::Shell::{IDataTransferManagerInterop, SHCreateMemStream}, + UI::WindowsAndMessaging::{ + CreateWindowExW, DestroyWindow, WINDOW_EX_STYLE, WS_OVERLAPPED, + }, + }, + }; + use windows_core::{HSTRING, w}; + + #[repr(C)] + struct FakeComObject { + vtable: *const *mut c_void, + } + + #[test] + fn interface_and_method_handle_are_send_and_sync() { + fn assert_send_sync() {} + + assert_send_sync::(); + assert_send_sync::(); + } + + unsafe extern "system" fn return_u32(_this: *mut c_void) -> u32 { + u32::MAX + } + + unsafe extern "system" fn return_s_false(_this: *mut c_void) -> windows_core::HRESULT { + windows_core::HRESULT(1) + } + + unsafe extern "system" fn return_failure(_this: *mut c_void) -> windows_core::HRESULT { + windows_core::HRESULT(0x80004005u32 as i32) + } + + unsafe extern "system" fn return_hstring( + _this: *mut c_void, + value: *mut *mut c_void, + ) -> windows_core::HRESULT { + unsafe { + *value = std::mem::transmute(HSTRING::from("dynwinrt HSTRING")); + } + windows_core::HRESULT(0) + } + + static VOID_CALLS: AtomicU32 = AtomicU32::new(0); + + unsafe extern "system" fn return_void(_this: *mut c_void) { + VOID_CALLS.fetch_add(1, Ordering::Relaxed); + } + + unsafe extern "system" fn increment_i32( + _this: *mut c_void, + value: *mut i32, + ) -> windows_core::HRESULT { + unsafe { *value += 1 }; + windows_core::HRESULT(0) + } + + unsafe extern "system" fn write_native_pointer( + _this: *mut c_void, + value: *mut *mut c_void, + ) -> windows_core::HRESULT { + unsafe { *value = 0x1234usize as *mut c_void }; + windows_core::HRESULT(0) + } + + unsafe extern "system" fn write_guid_and_i32( + _this: *mut c_void, + guid: *mut GUID, + value: *mut i32, + ) -> windows_core::HRESULT { + unsafe { + *guid = GUID::from_u128(0x11111111_2222_3333_4444_555555555555); + *value = 42; + } + windows_core::HRESULT(0) + } + + #[test] + fn direct_native_return_is_not_interpreted_as_hresult() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table).returns(Type::winrt(table.u32_type())); + let vtable = [return_u32 as *mut c_void]; + let mut object = FakeComObject { + vtable: vtable.as_ptr(), + }; + + let values = call_method( + 0, + (&mut object as *mut FakeComObject).cast(), + signature, + &[], + ) + .expect("u32::MAX is a value, not a failed HRESULT"); + + assert!(matches!(values.as_slice(), [WinRTValue::U32(u32::MAX)])); + } + + #[test] + fn native_void_return_does_not_read_hresult_register() { + VOID_CALLS.store(0, Ordering::Relaxed); + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table).returns_void(); + let vtable = [return_void as *mut c_void]; + let mut object = FakeComObject { + vtable: vtable.as_ptr(), + }; + + let values = call_method( + 0, + (&mut object as *mut FakeComObject).cast(), + signature, + &[], + ) + .unwrap(); + + assert!(values.is_empty()); + assert_eq!(VOID_CALLS.load(Ordering::Relaxed), 1); + } + + #[test] + fn semantic_hresult_preserves_success_codes_and_throws_failures() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table).preserve_hresult(); + let success_vtable = [return_s_false as *mut c_void]; + let mut success = FakeComObject { + vtable: success_vtable.as_ptr(), + }; + + let result = call_method( + 0, + (&mut success as *mut FakeComObject).cast(), + signature.clone(), + &[], + ) + .unwrap(); + assert!(matches!( + result.as_slice(), + [WinRTValue::HResult(value)] if value.0 == 1 + )); + + let failure_vtable = [return_failure as *mut c_void]; + let mut failure = FakeComObject { + vtable: failure_vtable.as_ptr(), + }; + let error = call_method( + 0, + (&mut failure as *mut FakeComObject).cast(), + signature, + &[], + ) + .unwrap_err(); + match error { + result::Error::WindowsError(error) => { + assert_eq!(error.code(), windows_core::HRESULT(0x80004005u32 as i32)); + } + other => panic!("expected Windows error, got {other:?}"), + } + } + + #[test] + fn classic_com_hstring_output_is_owned_and_decoded() { + let table = MetadataTable::new(); + let vtable = [return_hstring as *mut c_void]; + let mut object = FakeComObject { + vtable: vtable.as_ptr(), + }; + + let result = call_method( + 0, + (&mut object as *mut FakeComObject).cast(), + MethodSignature::new(&table).add_out(Type::winrt(table.hstring())), + &[], + ) + .unwrap(); + + assert!(matches!( + result.as_slice(), + [WinRTValue::HString(value)] if value == "dynwinrt HSTRING" + )); + } + + #[test] + fn in_out_parameter_preserves_input_and_returns_updated_value() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table).add_in_out(Type::winrt(table.i32_type())); + let vtable = [increment_i32 as *mut c_void]; + let mut object = FakeComObject { + vtable: vtable.as_ptr(), + }; + + let values = call_method( + 0, + (&mut object as *mut FakeComObject).cast(), + signature, + &[WinRTValue::I32(41)], + ) + .unwrap(); + + assert!(matches!(values.as_slice(), [WinRTValue::I32(42)])); + } + + #[test] + fn native_pointer_out_is_not_adopted_as_com_object() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table).add_out(Type::pointer()); + let vtable = [write_native_pointer as *mut c_void]; + let mut object = FakeComObject { + vtable: vtable.as_ptr(), + }; + + let values = call_method( + 0, + (&mut object as *mut FakeComObject).cast(), + signature, + &[], + ) + .unwrap(); + + assert!(matches!( + values.as_slice(), + [WinRTValue::RawPtr(ptr)] if *ptr == 0x1234usize as *mut c_void + )); + } + + #[test] + fn multi_output_guid_uses_full_sized_storage() { + let table = MetadataTable::new(); + let signature = MethodSignature::new(&table) + .add_out(Type::winrt(table.guid_type())) + .add_out(Type::winrt(table.i32_type())); + let vtable = [write_guid_and_i32 as *mut c_void]; + let mut object = FakeComObject { + vtable: vtable.as_ptr(), + }; + + let values = call_method( + 0, + (&mut object as *mut FakeComObject).cast(), + signature, + &[], + ) + .unwrap(); + + assert!(matches!( + values.as_slice(), + [WinRTValue::Guid(guid), WinRTValue::I32(42)] + if *guid == GUID::from_u128(0x11111111_2222_3333_4444_555555555555) + )); + } + + const CLSID_SHELL_LINK: GUID = GUID::from_u128(0x00021401_0000_0000_c000_000000000046); + const IID_ISHELL_LINK_W: GUID = GUID::from_u128(0x000214f9_0000_0000_c000_000000000046); + const REGDB_E_CLASSNOTREG: windows_core::HRESULT = windows_core::HRESULT(0x80040154u32 as i32); + + fn shell_link() -> result::Result { + initialize_apartment(ApartmentType::MultiThreaded)?; + co_create_instance(CLSID_SHELL_LINK, IID_ISHELL_LINK_W) + } + + #[test] + fn interfaces_with_the_same_name_do_not_alias() { + let table = MetadataTable::new(); + let first = register_interface( + &table, + "Windows.Win32.Example.IThing", + GUID::from_u128(1), + InterfaceBase::IUnknown, + ); + let second = register_interface( + &table, + "Windows.Win32.Example.IThing", + GUID::from_u128(2), + InterfaceBase::IUnknown, + ); + + assert_eq!(first.name(), second.name()); + assert_ne!(first.iid(), second.iid()); + assert!(!Arc::ptr_eq(&first.methods, &second.methods)); + } + + fn shell_link_interface(table: &std::sync::Arc) -> Interface { + register_interface( + table, + "Windows.Win32.UI.Shell.IShellLinkW", + IID_ISHELL_LINK_W, + InterfaceBase::IUnknown, + ) + .add_method("GetPath", MethodSignature::new(table)) + .add_method("GetIDList", MethodSignature::new(table)) + .add_method("SetIDList", MethodSignature::new(table)) + .add_method("GetDescription", MethodSignature::new(table)) + .add_method("SetDescription", MethodSignature::new(table)) + .add_method("GetWorkingDirectory", MethodSignature::new(table)) + .add_method("SetWorkingDirectory", MethodSignature::new(table)) + .add_method("GetArguments", MethodSignature::new(table)) + .add_method("SetArguments", MethodSignature::new(table)) + .add_method( + "GetHotkey", + MethodSignature::new(table).add_out(Type::winrt(table.u16_type())), + ) + .add_method( + "SetHotkey", + MethodSignature::new(table).add_in(Type::winrt(table.u16_type())), + ) + .add_method( + "GetShowCmd", + MethodSignature::new(table).add_out(Type::winrt(table.i32_type())), + ) + .add_method( + "SetShowCmd", + MethodSignature::new(table).add_in(Type::winrt(table.i32_type())), + ) + } + + fn native_usize_type(table: &std::sync::Arc) -> Type { + #[cfg(target_pointer_width = "64")] + { + Type::winrt(table.u64_type()) + } + #[cfg(target_pointer_width = "32")] + { + Type::winrt(table.u32_type()) + } + } + + fn native_usize_value(value: usize) -> WinRTValue { + #[cfg(target_pointer_width = "64")] + { + WinRTValue::U64(value as u64) + } + #[cfg(target_pointer_width = "32")] + { + WinRTValue::U32(value as u32) + } + } + + fn read_native_usize(value: &WinRTValue) -> usize { + #[cfg(target_pointer_width = "64")] + { + match value { + WinRTValue::U64(value) => *value as usize, + value => panic!("expected native u64, got {value:?}"), + } + } + #[cfg(target_pointer_width = "32")] + { + match value { + WinRTValue::U32(value) => *value as usize, + value => panic!("expected native u32, got {value:?}"), + } + } + } + + #[test] + fn shell_link_set_get_show_cmd_round_trips_via_classic_com_vtable() -> result::Result<()> { + let shell_link = shell_link()?.as_object().unwrap(); + let table = MetadataTable::new(); + let iface = shell_link_interface(&table); + + iface + .method(15) + .unwrap() + .invoke(shell_link.as_raw(), &[WinRTValue::I32(3)])?; + let result = iface.method(14).unwrap().invoke(shell_link.as_raw(), &[])?; + + assert_eq!(result[0].as_i32().unwrap(), 3); + Ok(()) + } + + #[test] + fn shell_link_set_get_hotkey_round_trips_u16() -> result::Result<()> { + let shell_link = shell_link()?.as_object().unwrap(); + let table = MetadataTable::new(); + let iface = shell_link_interface(&table); + + iface + .method(13) + .unwrap() + .invoke(shell_link.as_raw(), &[WinRTValue::U16(0x0141)])?; + let result = iface.method(12).unwrap().invoke(shell_link.as_raw(), &[])?; + + assert_eq!(result[0].as_i32().unwrap() as u16, 0x0141); + Ok(()) + } + + #[test] + fn shell_link_set_get_description_round_trips_wide_string() -> result::Result<()> { + let shell_link = shell_link()?.as_object().unwrap(); + let expected = "dynwinrt classic COM"; + let wide = wide_null(expected); + + call_method_1_ptr(7, shell_link.as_raw(), wide.as_ptr() as *const c_void)?; + + let mut buffer = wide_buffer(128); + call_method_2_ptr_i32( + 6, + shell_link.as_raw(), + buffer.as_mut_ptr() as *mut c_void, + buffer.len() as i32, + )?; + + assert_eq!(wide_to_string(&buffer), expected); + Ok(()) + } + + #[test] + fn shell_link_query_interface_returns_owned_ipersistfile() -> result::Result<()> { + let shell_link = shell_link()?; + let shell_link_object = shell_link + .as_object() + .expect("IShellLinkW must be non-null"); + let description = wide_null("semantic HRESULT"); + call_method_1_ptr(7, shell_link_object.as_raw(), description.as_ptr().cast())?; + let persist = shell_link.cast(&IPersistFile::IID)?; + let persist = persist.as_object().expect("IPersistFile must be non-null"); + let table = MetadataTable::new(); + let result = call_method( + 3, + persist.as_raw(), + MethodSignature::new(&table).add_out(Type::winrt(table.guid_type())), + &[], + )?; + + assert!(matches!( + result.as_slice(), + [WinRTValue::Guid(clsid)] if *clsid == CLSID_SHELL_LINK + )); + let dirty = call_method( + 4, + persist.as_raw(), + MethodSignature::new(&table).preserve_hresult(), + &[], + )?; + assert!(matches!( + dirty.as_slice(), + [WinRTValue::HResult(value)] if value.0 == 0 + )); + Ok(()) + } + + #[test] + fn malloc_exercises_pointer_sized_and_non_hresult_abi() -> result::Result<()> { + initialize_apartment(ApartmentType::MultiThreaded)?; + let allocator = unsafe { CoGetMalloc(1) }.map_err(result::Error::WindowsError)?; + let table = MetadataTable::new(); + let requested = 64usize; + let allocated = call_method( + 3, + allocator.as_raw(), + MethodSignature::new(&table) + .add_in(native_usize_type(&table)) + .returns(Type::pointer()), + &[native_usize_value(requested)], + )?; + let WinRTValue::RawPtr(ptr) = allocated[0] else { + panic!("IMalloc::Alloc must return a native pointer"); + }; + assert!(!ptr.is_null()); + + struct AllocationGuard { + allocator: IMalloc, + ptr: *mut c_void, + } + impl Drop for AllocationGuard { + fn drop(&mut self) { + if !self.ptr.is_null() { + unsafe { self.allocator.Free(Some(self.ptr)) }; + } + } + } + let mut allocation = AllocationGuard { + allocator: allocator.clone(), + ptr, + }; + + let size = call_method( + 6, + allocator.as_raw(), + MethodSignature::new(&table) + .add_in(Type::pointer()) + .returns(native_usize_type(&table)), + &[WinRTValue::RawPtr(ptr)], + )?; + assert!(read_native_usize(&size[0]) >= requested); + + let owned = call_method( + 7, + allocator.as_raw(), + MethodSignature::new(&table) + .add_in(Type::pointer()) + .returns(Type::winrt(table.i32_type())), + &[WinRTValue::RawPtr(ptr)], + )?; + assert!(matches!(owned.as_slice(), [WinRTValue::I32(value)] if *value != 0)); + + let freed = call_method( + 5, + allocator.as_raw(), + MethodSignature::new(&table) + .add_in(Type::pointer()) + .returns_void(), + &[WinRTValue::RawPtr(ptr)], + )?; + allocation.ptr = std::ptr::null_mut(); + assert!(freed.is_empty()); + + let minimized = call_method( + 8, + allocator.as_raw(), + MethodSignature::new(&table).returns_void(), + &[], + )?; + assert!(minimized.is_empty()); + Ok(()) + } + + #[test] + fn memory_stream_exercises_counted_buffers_seek_and_interface_out() -> result::Result<()> { + initialize_apartment(ApartmentType::MultiThreaded)?; + let expected = b"dynwinrt"; + let stream = unsafe { SHCreateMemStream(Some(expected)) } + .expect("SHCreateMemStream must return an IStream"); + let table = MetadataTable::new(); + let mut buffer = vec![0u8; expected.len()]; + + let read = call_method( + 3, + stream.as_raw(), + MethodSignature::new(&table) + .add_in(Type::pointer()) + .add_in(Type::winrt(table.u32_type())) + .add_out(Type::winrt(table.u32_type())), + &[ + WinRTValue::RawPtr(buffer.as_mut_ptr().cast()), + WinRTValue::U32(buffer.len() as u32), + ], + )?; + assert!( + matches!(read.as_slice(), [WinRTValue::U32(count)] if *count == expected.len() as u32) + ); + assert_eq!(buffer, expected); + + let position = call_method( + 5, + stream.as_raw(), + MethodSignature::new(&table) + .add_in(Type::winrt(table.i64_type())) + .add_in(Type::winrt(table.u32_type())) + .add_out(Type::winrt(table.u64_type())), + &[WinRTValue::I64(0), WinRTValue::U32(0)], + )?; + assert!(matches!(position.as_slice(), [WinRTValue::U64(0)])); + + let cloned = call_method( + 13, + stream.as_raw(), + MethodSignature::new(&table).add_out(Type::winrt(table.object())), + &[], + )?; + let clone = cloned[0].as_object().expect("IStream::Clone returned null"); + let _: IStream = clone.cast().map_err(result::Error::WindowsError)?; + Ok(()) + } + + #[test] + fn adopt_com_pointer_accepts_addref_owned_pointer() -> result::Result<()> { + let shell_link = shell_link()?.as_object().unwrap(); + let shell_link_raw = shell_link.as_raw(); + let borrowed = unsafe { IUnknown::from_raw_borrowed(&shell_link_raw) }.unwrap(); + let addref_owned = borrowed.clone(); + let raw = addref_owned.as_raw(); + std::mem::forget(addref_owned); + + let adopted = unsafe { adopt_com_pointer(raw) }; + let adopted = adopted.as_object().expect("adopted value must be Object"); + let table = MetadataTable::new(); + let iface = shell_link_interface(&table); + + iface + .method(15) + .unwrap() + .invoke(adopted.as_raw(), &[WinRTValue::I32(7)])?; + let result = iface.method(14).unwrap().invoke(adopted.as_raw(), &[])?; + + assert_eq!(result[0].as_i32().unwrap(), 7); + Ok(()) + } + + #[test] + fn co_create_instance_with_bogus_clsid_returns_error() -> result::Result<()> { + initialize_apartment(ApartmentType::MultiThreaded)?; + let bogus = GUID::from_u128(0xaaaaaaaa_bbbb_cccc_dddd_eeeeeeeeeeee); + + let err = co_create_instance(bogus, IID_ISHELL_LINK_W).unwrap_err(); + match err { + result::Error::WindowsError(err) => assert_eq!(err.code(), REGDB_E_CLASSNOTREG), + err => panic!("expected REGDB_E_CLASSNOTREG, got {err:?}"), + } + Ok(()) + } + + #[test] + fn co_create_instance_does_not_choose_an_apartment_implicitly() { + let remains_uninitialized = std::thread::spawn(|| { + let _ = co_create_instance( + GUID::from_u128(0xaaaaaaaa_bbbb_cccc_dddd_eeeeeeeeeeee), + IID_ISHELL_LINK_W, + ); + COM_INITIALIZATION + .with(|state| matches!(*state.borrow(), ComInitialization::Uninitialized)) + }) + .join() + .unwrap(); + + assert!(remains_uninitialized); + } + + #[test] + fn query_interface_with_unsupported_iid_returns_error() -> result::Result<()> { + let shell_link = shell_link()?; + let bogus = GUID::from_u128(0xbbbbbbbb_cccc_dddd_eeee_ffffffffffff); + + let err = shell_link.cast(&bogus).unwrap_err(); + match err { + result::Error::WindowsError(err) => assert_eq!(err.code(), E_NOINTERFACE), + err => panic!("expected E_NOINTERFACE, got {err:?}"), + } + Ok(()) + } + + #[test] + fn data_transfer_manager_interop_get_for_window_returns_winrt_object_via_dynamic_iunknown_vtable() + -> result::Result<()> { + initialize_apartment(ApartmentType::MultiThreaded)?; + + let hwnd = unsafe { + CreateWindowExW( + WINDOW_EX_STYLE(0), + w!("STATIC"), + w!("dynwinrt data transfer interop test"), + WS_OVERLAPPED, + 0, + 0, + 1, + 1, + None, + None, + None, + None, + ) + } + .map_err(result::Error::WindowsError)?; + struct WindowGuard(windows::Win32::Foundation::HWND); + impl Drop for WindowGuard { + fn drop(&mut self) { + let _ = unsafe { DestroyWindow(self.0) }; + } + } + let _window = WindowGuard(hwnd); + + let factory = ro_get_activation_factory_2(&HSTRING::from( + "Windows.ApplicationModel.DataTransfer.DataTransferManager", + ))?; + let interop = query_interface(factory, &IDataTransferManagerInterop::IID) + .map_err(result::Error::WindowsError)? + .as_object() + .unwrap(); + + let table = MetadataTable::new(); + let iface = register_interface( + &table, + "IDataTransferManagerInterop", + IDataTransferManagerInterop::IID, + InterfaceBase::IUnknown, + ) + .add_method( + "GetForWindow", + MethodSignature::new(&table) + .add_in(Type::pointer()) + .add_in(Type::pointer()) + .add_out(Type::winrt(table.object())), + ); + + let target_iid = DataTransferManager::IID; + let result = iface.method(3).unwrap().invoke( + interop.as_raw(), + &[ + WinRTValue::RawPtr(hwnd.0 as *mut c_void), + WinRTValue::RawPtr(&target_iid as *const GUID as *mut c_void), + ], + )?; + + let manager = result[0].as_object().expect("GetForWindow returned null"); + assert!(!manager.as_raw().is_null()); + let _typed: DataTransferManager = manager.cast().map_err(result::Error::WindowsError)?; + Ok(()) + } +} diff --git a/crates/dynwinrt/src/lib.rs b/crates/dynwinrt/src/lib.rs index 54679203..0f900016 100644 --- a/crates/dynwinrt/src/lib.rs +++ b/crates/dynwinrt/src/lib.rs @@ -5,7 +5,9 @@ use windows::core::*; mod abi; mod call; +pub mod com; mod interfaces; +mod native_call; mod result; mod roapi; mod signature; diff --git a/crates/dynwinrt/src/metadata_table/type_handle.rs b/crates/dynwinrt/src/metadata_table/type_handle.rs index 0e55c918..8f9eb5c0 100644 --- a/crates/dynwinrt/src/metadata_table/type_handle.rs +++ b/crates/dynwinrt/src/metadata_table/type_handle.rs @@ -322,7 +322,13 @@ impl TypeHandle { TypeKind::Object | TypeKind::Interface(_) | TypeKind::Delegate(_) - | TypeKind::RuntimeClass(_) => Ok(WinRTValue::Object(IUnknown::from_raw(ptr))), + | TypeKind::RuntimeClass(_) => { + if ptr.is_null() { + Ok(WinRTValue::Null) + } else { + Ok(WinRTValue::Object(IUnknown::from_raw(ptr))) + } + } TypeKind::HString => Ok(WinRTValue::HString(std::mem::transmute(ptr))), @@ -331,6 +337,9 @@ impl TypeHandle { ))), TypeKind::Parameterized(idx) => { + if ptr.is_null() { + return Ok(WinRTValue::Null); + } let (generic_def, args) = self.table.get_parameterized(idx); if is_async_piid(generic_def) { let raw = IUnknown::from_raw(ptr); @@ -345,6 +354,9 @@ impl TypeHandle { | TypeKind::IAsyncActionWithProgress(_) | TypeKind::IAsyncOperation(_) | TypeKind::IAsyncOperationWithProgress(_) => { + if ptr.is_null() { + return Ok(WinRTValue::Null); + } let raw = IUnknown::from_raw(ptr); let info: windows_future::IAsyncInfo = raw .cast() @@ -389,7 +401,13 @@ impl TypeHandle { | TypeKind::Delegate(_) | TypeKind::RuntimeClass(_), AbiValue::Pointer(p), - ) => Ok(WinRTValue::Object(unsafe { IUnknown::from_raw(*p) })), + ) => { + if p.is_null() { + Ok(WinRTValue::Null) + } else { + Ok(WinRTValue::Object(unsafe { IUnknown::from_raw(*p) })) + } + } (TypeKind::HString, AbiValue::Pointer(p)) => { Ok(WinRTValue::HString(unsafe { core::mem::transmute(*p) })) @@ -400,6 +418,9 @@ impl TypeHandle { } (TypeKind::Parameterized(idx), AbiValue::Pointer(p)) => { + if p.is_null() { + return Ok(WinRTValue::Null); + } let (generic_def, args) = self.table.get_parameterized(idx); if is_async_piid(generic_def) { let raw = unsafe { IUnknown::from_raw(*p) }; diff --git a/crates/dynwinrt/src/metadata_table/type_kind.rs b/crates/dynwinrt/src/metadata_table/type_kind.rs index 8d5a488e..4b0f0e12 100644 --- a/crates/dynwinrt/src/metadata_table/type_kind.rs +++ b/crates/dynwinrt/src/metadata_table/type_kind.rs @@ -18,7 +18,7 @@ pub const IVECTOR_VIEW: GUID = GUID::from_u128(0xbbe1fa4c_b0e3_4583_baef_1f1b2e4 pub const IITERABLE: GUID = GUID::from_u128(0xfaa585ea_6214_4217_afda_7f46de5869b3); pub const IITERATOR: GUID = GUID::from_u128(0x6a79e863_4300_459a_9966_cbb660963ee1); pub const IMAP: GUID = GUID::from_u128(0x3c2925fe_8519_45c1_aa79_197b6718c1c1); -pub const IMAP_VIEW: GUID = GUID::from_u128(0xe9bdaaf0_cbf6_4c39_de49_316b34326a17); +pub const IMAP_VIEW: GUID = GUID::from_u128(0xe480ce40_a338_4ada_adcf_272272e48cb9); pub const IKEY_VALUE_PAIR: GUID = GUID::from_u128(0x02b51929_c1c4_4a7e_8940_0312b5c18500); pub const IOBSERVABLE_VECTOR: GUID = GUID::from_u128(0x5917eb53_50b4_4a0d_b309_65862b3f1dbc); pub const VECTOR_CHANGED_EVENT_HANDLER: GUID = @@ -235,3 +235,22 @@ pub(crate) fn pinterface_signature_from_strings(piid_sig: &str, arg_sigs: &[Stri s.push(')'); s } + +#[cfg(test)] +mod tests { + use super::*; + + /// The WinRT `IMapView`2` parameterized-interface IID. This MUST equal the + /// value the code generator uses (`IMAP_VIEW_PIID = + /// "e480ce40-a338-4ada-adcf-272272e48cb9"`), or `MetadataTable::map_iids()` + /// computes wrong `IMapView` IIDs at runtime and map-view projections + /// fail to QueryInterface. Regression guard against the prior mismatched + /// constant. + #[test] + fn imap_view_piid_is_canonical() { + assert_eq!( + IMAP_VIEW, + GUID::from_u128(0xe480ce40_a338_4ada_adcf_272272e48cb9) + ); + } +} diff --git a/crates/dynwinrt/src/native_call.rs b/crates/dynwinrt/src/native_call.rs new file mode 100644 index 00000000..9e69d6ec --- /dev/null +++ b/crates/dynwinrt/src/native_call.rs @@ -0,0 +1,1388 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Private native call planning and execution shared by WinRT and Classic COM. + +use libffi::middle::Cif; +use std::sync::Arc; +use windows::core::{GUID, IInspectable, Interface}; + +use crate::{ + abi::{AbiType, AbiValue}, + call, + call::ArgumentList, + metadata_table::{MetadataTable, TypeHandle, TypeKind}, + value::WinRTValue, +}; + +#[derive(Debug, Clone)] +pub(crate) enum ParameterType { + WinRT(TypeHandle), + Pointer, +} + +impl ParameterType { + pub(crate) fn winrt(typ: TypeHandle) -> Self { + Self::WinRT(typ) + } + + pub(crate) fn pointer() -> Self { + Self::Pointer + } + + pub(crate) fn as_winrt(&self) -> Option<&TypeHandle> { + match self { + Self::WinRT(typ) => Some(typ), + Self::Pointer => None, + } + } + + pub(crate) fn is_array(&self) -> bool { + self.as_winrt().is_some_and(TypeHandle::is_array) + } + + pub(crate) fn is_struct(&self) -> bool { + matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::Struct(_))) + } + + pub(crate) fn is_hstring(&self) -> bool { + matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::HString)) + } + + pub(crate) fn is_u32(&self) -> bool { + matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::U32)) + } + + pub(crate) fn is_guid(&self) -> bool { + matches!(self, Self::WinRT(typ) if matches!(typ.kind(), TypeKind::Guid)) + } + + pub(crate) fn supports_in_out(&self) -> bool { + matches!(self, Self::Pointer) + || matches!( + self, + Self::WinRT(typ) + if matches!( + typ.kind(), + TypeKind::Bool + | TypeKind::I8 + | TypeKind::U8 + | TypeKind::I16 + | TypeKind::U16 + | TypeKind::Char16 + | TypeKind::I32 + | TypeKind::U32 + | TypeKind::I64 + | TypeKind::U64 + | TypeKind::F32 + | TypeKind::F64 + | TypeKind::HResult + | TypeKind::Enum(_) + | TypeKind::Struct(_) + ) + ) + } + + pub(crate) fn supports_direct_return(&self) -> bool { + matches!(self, Self::Pointer) + || matches!( + self, + Self::WinRT(typ) + if matches!( + typ.kind(), + TypeKind::Bool + | TypeKind::I8 + | TypeKind::U8 + | TypeKind::I16 + | TypeKind::U16 + | TypeKind::Char16 + | TypeKind::I32 + | TypeKind::U32 + | TypeKind::I64 + | TypeKind::U64 + | TypeKind::F32 + | TypeKind::F64 + | TypeKind::HResult + | TypeKind::Enum(_) + ) + ) + } + + pub(crate) fn abi_type(&self) -> AbiType { + match self { + Self::WinRT(typ) => typ.abi_type(), + Self::Pointer => AbiType::Ptr, + } + } + + pub(crate) fn libffi_type(&self) -> libffi::middle::Type { + match self { + Self::WinRT(typ) => typ.libffi_type(), + Self::Pointer => libffi::middle::Type::pointer(), + } + } + + pub(crate) fn array_element_type(&self) -> TypeHandle { + self.as_winrt() + .expect("native pointer is not an array") + .array_element_type() + } + + pub(crate) fn default_struct_value(&self) -> crate::metadata_table::ValueTypeData { + self.as_winrt() + .expect("native pointer is not a struct") + .default_value() + } + + pub(crate) fn default_value(&self) -> WinRTValue { + match self { + Self::WinRT(typ) => typ.default_winrt_value(), + Self::Pointer => WinRTValue::RawPtr(std::ptr::null_mut()), + } + } + + pub(crate) fn from_out(&self, ptr: *mut std::ffi::c_void) -> crate::result::Result { + match self { + Self::WinRT(typ) => typ.from_out(ptr), + Self::Pointer => Ok(WinRTValue::RawPtr(ptr)), + } + } + + pub(crate) fn from_out_value(&self, value: &AbiValue) -> crate::result::Result { + match (self, value) { + (Self::WinRT(typ), value) => typ.from_out_value(value), + (Self::Pointer, AbiValue::Pointer(ptr)) => Ok(WinRTValue::RawPtr(*ptr)), + (Self::Pointer, value) => Err(crate::result::Error::InvalidTypeAbiToWinRT( + TypeKind::Object, + value.abi_type(), + )), + } + } +} + +/// How a parameter is passed at the ABI level. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ParamKind { + In, + Out, + InOut, + /// FillArray: caller allocates buffer, callee fills it. + /// ABI expands to 2 params: (u32 capacity, T* items). + OutFillArray, +} + +#[derive(Debug, Clone)] +pub struct Parameter { + pub(crate) typ: ParameterType, + /// Index in the method result vector for out and FillArray parameters. + pub value_index: usize, + /// Index in the caller-provided argument slice. FillArray parameters have + /// both an input index (capacity buffer) and an output index (filled data). + pub input_index: Option, + pub kind: ParamKind, +} + +impl Parameter { + pub fn is_input(&self) -> bool { + matches!(self.kind, ParamKind::In | ParamKind::InOut) + } + + pub fn is_out(&self) -> bool { + matches!( + self.kind, + ParamKind::Out | ParamKind::InOut | ParamKind::OutFillArray + ) + } + + pub fn is_in_out(&self) -> bool { + self.kind == ParamKind::InOut + } + + pub fn is_fill_array(&self) -> bool { + self.kind == ParamKind::OutFillArray + } +} + +#[derive(Debug, Clone)] +pub(crate) struct AbiMethodSignature { + out_count: usize, + input_count: usize, + parameters: Vec, + return_kind: MethodReturn, + #[allow(dead_code)] + is_opaque: bool, + #[allow(dead_code)] + table: Arc, +} + +#[derive(Debug, Clone)] +pub(crate) enum MethodReturn { + HResult, + SemanticHResult, + Void, + Value(ParameterType), +} + +impl MethodReturn { + fn libffi_type(&self) -> libffi::middle::Type { + match self { + Self::HResult | Self::SemanticHResult => libffi::middle::Type::i32(), + Self::Void => libffi::middle::Type::void(), + Self::Value(typ) => typ.libffi_type(), + } + } +} + +impl AbiMethodSignature { + pub(crate) fn new(table: &Arc) -> Self { + AbiMethodSignature { + out_count: 0, + input_count: 0, + parameters: Vec::new(), + return_kind: MethodReturn::HResult, + is_opaque: false, + table: Arc::clone(table), + } + } + + pub(crate) fn add_in_type(mut self, typ: ParameterType) -> Self { + let input_index = self.input_count; + self.input_count += 1; + self.parameters.push(Parameter { + kind: ParamKind::In, + typ, + value_index: input_index, + input_index: Some(input_index), + }); + self + } + + pub(crate) fn add_out_type(mut self, typ: ParameterType) -> Self { + self.parameters.push(Parameter { + kind: ParamKind::Out, + typ, + value_index: self.out_count, + input_index: None, + }); + self.out_count += 1; + self + } + + pub(crate) fn add_in_out_type(mut self, typ: ParameterType) -> Self { + assert!( + typ.supports_in_out(), + "in/out currently supports native scalars, pointers, enums, and structs" + ); + let input_index = self.input_count; + self.input_count += 1; + self.parameters.push(Parameter { + kind: ParamKind::InOut, + typ, + value_index: self.out_count, + input_index: Some(input_index), + }); + self.out_count += 1; + self + } + + pub(crate) fn add_out_fill_type(mut self, typ: ParameterType) -> Self { + let input_index = self.input_count; + self.input_count += 1; + self.parameters.push(Parameter { + kind: ParamKind::OutFillArray, + typ, + value_index: self.out_count, + input_index: Some(input_index), + }); + self.out_count += 1; + self + } + + pub(crate) fn returns_type(mut self, typ: ParameterType) -> Self { + assert!( + typ.supports_direct_return(), + "direct native returns currently support scalars, enums, and pointers" + ); + self.return_kind = MethodReturn::Value(typ); + self + } + + pub(crate) fn returns_void(mut self) -> Self { + self.return_kind = MethodReturn::Void; + self + } + + pub(crate) fn preserve_hresult(mut self) -> Self { + self.return_kind = MethodReturn::SemanticHResult; + self + } + + pub(crate) fn build(self, index: usize) -> Method { + use libffi::middle::Type; + let mut types: Vec = Vec::with_capacity(self.parameters.len() + 1); + types.push(Type::pointer()); // com object's this pointer + for param in &self.parameters { + if param.is_fill_array() { + // FillArray: UINT32 capacity, T* items + types.push(Type::u32()); + types.push(Type::pointer()); + } else if param.typ.is_array() { + if param.is_out() { + // ReceiveArray: UINT32* out_length, T** out_data + types.push(Type::pointer()); + types.push(Type::pointer()); + } else { + // PassArray: UINT32 length, T* data + types.push(Type::u32()); + types.push(Type::pointer()); + } + } else if param.is_out() { + types.push(Type::pointer()); + } else { + types.push(param.typ.libffi_type()); + } + } + let in_count = self.parameters.iter().filter(|p| p.is_input()).count(); + let has_complex_param = self + .parameters + .iter() + .any(|p| p.typ.is_array() || p.is_fill_array() || p.is_in_out() || p.typ.is_struct()); + + // Check if the single in-param (if any) is a simple non-HString, non-Struct type + let simple_in = !has_complex_param && in_count == 1 && { + let in_param = self.parameters.iter().find(|p| p.is_input()).unwrap(); + !in_param.typ.is_hstring() + }; + + // Classify array parameters + let array_in_count = self + .parameters + .iter() + .filter(|p| p.is_input() && p.typ.is_array()) + .count(); + let fill_out_count = self.parameters.iter().filter(|p| p.is_fill_array()).count(); + let array_out_count = self + .parameters + .iter() + .filter(|p| p.is_out() && p.typ.is_array() && !p.is_fill_array()) + .count(); + let scalar_in_count = in_count - array_in_count; + let scalar_out_count = self.out_count - fill_out_count - array_out_count; + + let returns_hresult = matches!(self.return_kind, MethodReturn::HResult); + let strategy = if returns_hresult + && !has_complex_param + && in_count == 0 + && self.out_count == 1 + { + CallStrategy::Direct0In1Out + } else if returns_hresult && !has_complex_param && in_count == 0 && self.out_count == 0 { + CallStrategy::Direct0In0Out + } else if returns_hresult && simple_in && self.out_count == 0 { + CallStrategy::Direct1In0Out + } else if returns_hresult && simple_in && self.out_count == 1 { + CallStrategy::Direct1In1Out + // ReceiveArray only: fn(this, *mut u32, *mut *mut c_void) -> HRESULT + } else if returns_hresult + && scalar_in_count == 0 + && array_in_count == 0 + && array_out_count == 1 + && fill_out_count == 0 + && scalar_out_count == 0 + { + CallStrategy::DirectReceiveArray + // PassArray + 1 out: fn(this, u32, *const u8, out) -> HRESULT + } else if returns_hresult + && scalar_in_count == 0 + && array_in_count == 1 + && array_out_count == 0 + && fill_out_count == 0 + && scalar_out_count == 1 + { + CallStrategy::DirectPassArray1Out + // FillArray only: fn(this, u32, *mut u8, *mut u32) -> HRESULT + } else if returns_hresult + && scalar_in_count == 0 + && array_in_count == 0 + && fill_out_count == 1 + && array_out_count == 0 + && scalar_out_count == 0 + { + CallStrategy::DirectFillArray + // 1 scalar in + FillArray: fn(this, val, u32, *mut u8, *mut u32) -> HRESULT + } else if returns_hresult + && scalar_in_count == 1 + && array_in_count == 0 + && fill_out_count == 1 + && array_out_count == 0 + && scalar_out_count == 0 + { + let in_param = self + .parameters + .iter() + .find(|p| p.is_input() && !p.typ.is_array()) + .unwrap(); + if !in_param.typ.is_hstring() && !in_param.typ.is_struct() { + CallStrategy::Direct1InFillArray + } else { + CallStrategy::Libffi(Cif::new(types.into_iter(), self.return_kind.libffi_type())) + } + } else { + CallStrategy::Libffi(Cif::new(types.into_iter(), self.return_kind.libffi_type())) + }; + + Method { + info: MethodInfo { + index, + parameters: self.parameters, + out_count: self.out_count, + return_kind: self.return_kind, + }, + strategy, + } + } +} + +#[derive(Debug)] +pub struct MethodInfo { + pub index: usize, + pub parameters: Vec, + pub out_count: usize, + pub(crate) return_kind: MethodReturn, +} + +/// How a Method should be invoked — decided once at build time. +#[derive(Debug)] +enum CallStrategy { + /// 0 in + 0 out: fn(this) -> HRESULT. + Direct0In0Out, + /// 0 in + 1 out (getter): fn(this, out) -> HRESULT. + Direct0In1Out, + /// 1 in + 0 out (setter, non-HString): fn(this, val) -> HRESULT. + Direct1In0Out, + /// 1 in + 1 out (factory/query, non-HString in): fn(this, val, out) -> HRESULT. + Direct1In1Out, + /// ReceiveArray: fn(this, *mut u32, *mut *mut c_void) -> HRESULT. + DirectReceiveArray, + /// PassArray + 1 out: fn(this, u32, *const u8, out) -> HRESULT. + DirectPassArray1Out, + /// FillArray only: fn(this, u32, *mut u8) -> HRESULT. + DirectFillArray, + /// 1 scalar in + FillArray: fn(this, val, u32, *mut u8) -> HRESULT. + Direct1InFillArray, + /// General case → libffi via cached Cif. + Libffi(Cif), +} + +#[derive(Debug)] +pub struct Method { + info: MethodInfo, + strategy: CallStrategy, +} + +fn expected_object_iid(typ: &TypeHandle) -> Option { + match typ.kind() { + TypeKind::Object => Some(IInspectable::IID), + TypeKind::Interface(_) + | TypeKind::Delegate(_) + | TypeKind::RuntimeClass(_) + | TypeKind::Parameterized(_) + | TypeKind::IAsyncAction + | TypeKind::IAsyncActionWithProgress(_) + | TypeKind::IAsyncOperation(_) + | TypeKind::IAsyncOperationWithProgress(_) => typ.iid(), + _ => None, + } +} + +fn coerce_input_object( + expected: &TypeHandle, + value: &WinRTValue, +) -> windows_core::Result> { + let Some(iid) = expected_object_iid(expected) else { + return Ok(None); + }; + // Null objects are always allowed (`WinRTValue::Null` and the + // Object-typed null variant both project as "no coercion needed" — the + // ABI receives a null pointer directly). + if value.is_null_object() { + return Ok(None); + } + // Raw pointers never satisfy a WinRT object parameter. Otherwise arbitrary + // pointer bits could reach a typed COM slot without QueryInterface validation. + if matches!(value, WinRTValue::RawPtr(_)) { + return Err(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Refusing to pass a raw pointer as a typed COM parameter ({}). \ + Use a Pointer signature for native pointers and handles; for \ + COM parameters pass a real object (or one obtained via `.cast(IID)`).", + expected.signature_string(), + ), + )); + } + + let object = value.as_object().ok_or_else(|| { + windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Expected object argument for {}, found {:?}", + expected.signature_string(), + value.get_type_kind() + ), + ) + })?; + + WinRTValue::Object(object) + .cast(&iid) + .map(Some) + .map_err(|error| match error { + crate::result::Error::WindowsError(error) => error, + other => windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &other.message(), + ), + }) +} + +fn coerce_input_array( + expected: &TypeHandle, + value: &WinRTValue, +) -> windows_core::Result> { + if !expected.is_array() { + return Ok(None); + } + + let element_type = expected.array_element_type(); + let array = value.as_array().ok_or_else(|| { + windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Expected array argument for {}, found {:?}", + expected.signature_string(), + value.get_type_kind() + ), + ) + })?; + let is_object_array = expected_object_iid(&element_type).is_some(); + let element_type_matches = match (element_type.kind(), array.element_type.kind()) { + (TypeKind::Struct(_), TypeKind::Struct(_)) => array.element_type == element_type, + (TypeKind::Enum(_), TypeKind::Enum(_)) => array.element_type == element_type, + (TypeKind::Enum(_), TypeKind::I32) + | (TypeKind::Char16, TypeKind::U16) + | (TypeKind::U16, TypeKind::Char16) => true, + (expected, actual) => expected == actual, + }; + if !is_object_array && !element_type_matches { + return Err(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Array element type mismatch: expected {}, received {}", + element_type.signature_string(), + array.element_type.signature_string(), + ), + )); + } + + let mut values = Vec::with_capacity(array.len()); + let mut changed = false; + for index in 0..array.len() { + let value = array.get(index); + validate_array_element(&element_type, &value, index)?; + if is_object_array && let Some(coerced) = coerce_input_object(&element_type, &value)? { + values.push(coerced); + changed = true; + } else { + values.push(value); + } + } + + Ok(changed + .then(|| WinRTValue::Array(crate::array::ArrayData::from_values(element_type, &values)))) +} + +fn validate_input_struct(expected: &TypeHandle, value: &WinRTValue) -> windows_core::Result<()> { + if !matches!(expected.kind(), TypeKind::Struct(_)) { + return Ok(()); + } + + let actual = value.as_struct().ok_or_else(|| { + windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Expected struct argument for {}, found {:?}", + expected.signature_string(), + value.get_type_kind() + ), + ) + })?; + if actual.type_handle() != expected { + return Err(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Struct type mismatch: expected {} ({} bytes, align {}), \ + received {} ({} bytes, align {})", + expected.signature_string(), + expected.size_of(), + expected.align_of(), + actual.type_handle().signature_string(), + actual.type_handle().size_of(), + actual.type_handle().align_of(), + ), + )); + } + + Ok(()) +} + +fn validate_array_element( + expected: &TypeHandle, + value: &WinRTValue, + index: usize, +) -> windows_core::Result<()> { + if expected_object_iid(expected).is_some() { + return Ok(()); + } + if matches!(expected.kind(), TypeKind::Struct(_)) { + return validate_input_struct(expected, value).map_err(|error| { + windows_core::Error::new( + error.code(), + &format!("Array element {index}: {}", error.message()), + ) + }); + } + if let TypeKind::Enum(_) = expected.kind() { + let matches = matches!(value, WinRTValue::I32(_)) + || matches!( + value, + WinRTValue::Enum { type_handle, .. } if type_handle == expected + ); + if !matches { + return Err(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Array element {index} type mismatch: expected {}, found {:?}", + expected.signature_string(), + value.get_type_kind() + ), + )); + } + return Ok(()); + } + if matches!(expected.kind(), TypeKind::Char16) && matches!(value, WinRTValue::U16(_)) { + return Ok(()); + } + if value.get_type_kind() != expected.kind() { + return Err(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!( + "Array element {index} type mismatch: expected {}, found {:?}", + expected.signature_string(), + value.get_type_kind() + ), + )); + } + + Ok(()) +} + +struct InvocationArgs<'a> { + original: &'a [WinRTValue], + replacements: Option>>, +} + +impl<'a> InvocationArgs<'a> { + fn new(original: &'a [WinRTValue]) -> Self { + Self { + original, + replacements: None, + } + } + + fn replace(&mut self, index: usize, value: WinRTValue) { + self.replacements.get_or_insert_with(|| { + std::iter::repeat_with(|| None) + .take(self.original.len()) + .collect() + })[index] = Some(value); + } +} + +impl call::ArgumentList for InvocationArgs<'_> { + fn get_value(&self, index: usize) -> &WinRTValue { + self.replacements + .as_ref() + .and_then(|values| values[index].as_ref()) + .unwrap_or(&self.original[index]) + } +} + +impl Method { + // --- Fast getter paths: zero Vec/WinRTValue allocation --- + + /// Getter → i32 (0 in, 1 out). Writes directly to stack i32. + pub fn call_getter_i32(&self, obj: *mut std::ffi::c_void) -> windows_core::Result { + let mut out: i32 = 0; + let hr = call::call_winrt_method_1( + self.info.index, + obj, + &mut out as *mut i32 as *mut std::ffi::c_void, + ); + hr.ok()?; + Ok(out) + } + + /// Getter → bool (0 in, 1 out). Writes directly to stack bool. + pub fn call_getter_bool(&self, obj: *mut std::ffi::c_void) -> windows_core::Result { + let mut out: i32 = 0; // WinRT bool is i32 on ABI + let hr = call::call_winrt_method_1( + self.info.index, + obj, + &mut out as *mut i32 as *mut std::ffi::c_void, + ); + hr.ok()?; + Ok(out != 0) + } + + /// Getter → HSTRING (0 in, 1 out). Writes directly to stack HSTRING ptr. + pub fn call_getter_hstring( + &self, + obj: *mut std::ffi::c_void, + ) -> windows_core::Result { + // HSTRING is a pointer-sized handle on ABI. Let WinRT write it directly. + let mut out = windows_core::HSTRING::new(); + let hr = call::call_winrt_method_1( + self.info.index, + obj, + &mut out as *mut windows_core::HSTRING as *mut std::ffi::c_void, + ); + hr.ok()?; + Ok(out) + } + + /// Getter → COM object (0 in, 1 out). Writes directly to stack pointer. + pub fn call_getter_object( + &self, + obj: *mut std::ffi::c_void, + ) -> windows_core::Result { + let mut out: *mut std::ffi::c_void = std::ptr::null_mut(); + let hr = call::call_winrt_method_1( + self.info.index, + obj, + &mut out as *mut _ as *mut std::ffi::c_void, + ); + hr.ok()?; + if out.is_null() { + Ok(WinRTValue::Null) + } else { + Ok(WinRTValue::Object(unsafe { + windows_core::IUnknown::from_raw(out) + })) + } + } + + pub fn call_dynamic( + &self, + obj: *mut std::ffi::c_void, + args: &[WinRTValue], + ) -> windows_core::Result> { + let mut args = InvocationArgs::new(args); + for parameter in self.info.parameters.iter().filter(|p| p.is_input()) { + let input_index = parameter.input_index.expect("input parameter index"); + let value = args.get_value(input_index); + let coerced = if let Some(typ) = parameter.typ.as_winrt() { + validate_input_struct(typ, value)?; + if typ.is_array() { + coerce_input_array(typ, value)? + } else { + coerce_input_object(typ, value)? + } + } else { + None + }; + if let Some(value) = coerced { + args.replace(input_index, value); + } + } + + match &self.strategy { + CallStrategy::Direct0In0Out => { + // 0 in + 0 out: fn(this) -> HRESULT + let hr = call::call_winrt_method_0(self.info.index, obj); + hr.ok()?; + Ok(vec![]) + } + CallStrategy::Direct0In1Out => { + // 0 in + 1 out: fn(this, out) -> HRESULT + let param = &self.info.parameters[0]; + let mut out = param.typ.default_value(); + let hr = call::call_winrt_method_1(self.info.index, obj, out.out_ptr()); + hr.ok()?; + // COM pointer types use RawPtr(null) as buffer to avoid IUnknown::from_raw(null) UB. + // After COM writes the pointer, convert via from_out. + if let WinRTValue::RawPtr(raw_ptr) = out { + out = param.typ.from_out(raw_ptr).map_err(|e| { + windows_core::Error::new(windows_core::HRESULT(-1), &format!("{:?}", e)) + })?; + } + out.sanitize_null_object(); + Ok(vec![out]) + } + CallStrategy::Direct1In0Out => { + // 1 in + 0 out: fn(this, val) -> HRESULT + let hr = call::call_1in(self.info.index, obj, args.get_value(0)); + hr.ok()?; + Ok(vec![]) + } + CallStrategy::Direct1In1Out => { + // 1 in + 1 out: fn(this, val, out) -> HRESULT + let out_param = self.info.parameters.iter().find(|p| p.is_out()).unwrap(); + let mut out = out_param.typ.default_value(); + let hr = + call::call_1in_1out(self.info.index, obj, args.get_value(0), out.out_ptr()); + hr.ok()?; + if let WinRTValue::RawPtr(raw_ptr) = out { + out = out_param.typ.from_out(raw_ptr).map_err(|e| { + windows_core::Error::new(windows_core::HRESULT(-1), &format!("{:?}", e)) + })?; + } + out.sanitize_null_object(); + Ok(vec![out]) + } + CallStrategy::DirectReceiveArray => { + // fn(this, *mut u32, *mut *mut c_void) -> HRESULT + let param = &self.info.parameters[0]; + let elem_type = param.typ.array_element_type(); + let mut length: u32 = 0; + let mut data_ptr: *mut std::ffi::c_void = std::ptr::null_mut(); + let fptr = call::get_vtable_function_ptr(obj, self.info.index); + let hr: windows_core::HRESULT = unsafe { + let method: unsafe extern "system" fn( + *mut std::ffi::c_void, + *mut u32, + *mut *mut std::ffi::c_void, + ) + -> windows_core::HRESULT = std::mem::transmute(fptr); + method(obj, &mut length, &mut data_ptr) + }; + if hr.is_err() { + // Callee may have allocated a buffer before returning failure. + // Wrap in ArrayData to release elements + CoTaskMemFree. + if !data_ptr.is_null() { + let _ = crate::array::ArrayData::from_cotaskmem( + elem_type.clone(), + data_ptr, + length as usize, + ); + } + hr.ok()?; + } + let array = if data_ptr.is_null() || length == 0 { + if !data_ptr.is_null() { + unsafe { + windows::Win32::System::Com::CoTaskMemFree(Some(data_ptr)); + } + } + + crate::array::ArrayData::empty(elem_type) + } else { + crate::array::ArrayData::from_cotaskmem(elem_type, data_ptr, length as usize) + }; + Ok(vec![WinRTValue::Array(array)]) + } + CallStrategy::DirectPassArray1Out => { + // fn(this, u32, *const u8, out) -> HRESULT + let in_param = self.info.parameters.iter().find(|p| p.is_input()).unwrap(); + let out_param = self.info.parameters.iter().find(|p| p.is_out()).unwrap(); + let array_data = args.get_value(in_param.value_index).as_array().unwrap(); + let buffer = array_data.serialize_for_abi(); + let mut out = out_param.typ.default_value(); + let fptr = call::get_vtable_function_ptr(obj, self.info.index); + let hr: windows_core::HRESULT = unsafe { + let method: unsafe extern "system" fn( + *mut std::ffi::c_void, + u32, + *const u8, + *mut std::ffi::c_void, + ) + -> windows_core::HRESULT = std::mem::transmute(fptr); + method(obj, array_data.len() as u32, buffer.as_ptr(), out.out_ptr()) + }; + hr.ok()?; + if let WinRTValue::RawPtr(raw_ptr) = out { + out = out_param.typ.from_out(raw_ptr).map_err(|e| { + windows_core::Error::new(windows_core::HRESULT(-1), &format!("{:?}", e)) + })?; + } + out.sanitize_null_object(); + Ok(vec![out]) + } + CallStrategy::DirectFillArray => { + // fn(this, u32, *mut u8) -> HRESULT + // FillArray: caller provides buffer of known capacity, callee fills it. + let param = &self.info.parameters[0]; + let elem_type = param.typ.array_element_type(); + let fptr = call::get_vtable_function_ptr(obj, self.info.index); + + assert!( + param + .input_index + .is_some_and(|index| { args.get_value(index).as_array().is_some() }), + "DirectFillArray requires a pre-allocated array argument with the desired capacity. \ + Pass an ArrayData with the expected number of elements." + ); + let array_data = args + .get_value(param.input_index.unwrap()) + .as_array() + .unwrap(); + let capacity = array_data.len() as u32; + let total_bytes = capacity as usize * elem_type.element_size(); + let buffer_ptr = + unsafe { windows::Win32::System::Com::CoTaskMemAlloc(total_bytes) as *mut u8 }; + assert!(!buffer_ptr.is_null(), "CoTaskMemAlloc failed for FillArray"); + unsafe { std::ptr::write_bytes(buffer_ptr, 0, total_bytes) }; + let hr: windows_core::HRESULT = unsafe { + let method: unsafe extern "system" fn( + *mut std::ffi::c_void, + u32, + *mut u8, + ) + -> windows_core::HRESULT = std::mem::transmute(fptr); + method(obj, capacity, buffer_ptr) + }; + if hr.is_err() { + // Callee may have written elements before failing. + // Buffer was zero-initialized, so null slots are safe to release. + // Use capacity as cleanup length — ArrayData::Drop skips null elements. + let _ = crate::array::ArrayData::from_cotaskmem( + elem_type.clone(), + buffer_ptr as _, + capacity as usize, + ); + hr.ok()?; + } + let array = crate::array::ArrayData::from_cotaskmem( + elem_type, + buffer_ptr as _, + capacity as usize, + ); + Ok(vec![WinRTValue::Array(array)]) + } + CallStrategy::Direct1InFillArray => { + // fn(this, val, u32, *mut u8) -> HRESULT + let in_param = self.info.parameters.iter().find(|p| p.is_input()).unwrap(); + let fill_param = self + .info + .parameters + .iter() + .find(|p| p.is_fill_array()) + .unwrap(); + let array_data = args + .get_value(fill_param.input_index.unwrap()) + .as_array() + .unwrap(); + let elem_type = fill_param.typ.array_element_type(); + let capacity = array_data.len() as u32; + let total_bytes = capacity as usize * elem_type.element_size(); + let buffer_ptr = + unsafe { windows::Win32::System::Com::CoTaskMemAlloc(total_bytes) as *mut u8 }; + assert!(!buffer_ptr.is_null(), "CoTaskMemAlloc failed for FillArray"); + unsafe { std::ptr::write_bytes(buffer_ptr, 0, total_bytes) }; + let fptr = call::get_vtable_function_ptr(obj, self.info.index); + let hr = call::call_fill_array_1in( + fptr, + obj, + args.get_value(in_param.value_index), + capacity, + buffer_ptr, + ); + if hr.is_err() { + // Buffer was zero-initialized; use capacity for cleanup. + let _ = crate::array::ArrayData::from_cotaskmem( + elem_type.clone(), + buffer_ptr as _, + capacity as usize, + ); + hr.ok()?; + } + let array = crate::array::ArrayData::from_cotaskmem( + elem_type, + buffer_ptr as _, + capacity as usize, + ); + Ok(vec![WinRTValue::Array(array)]) + } + CallStrategy::Libffi(cif) => call::call_method_dynamic( + self.info.index, + obj, + &self.info.parameters, + &args, + self.info.out_count, + &self.info.return_kind, + cif, + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::atomic::{AtomicU32, Ordering}; + use windows::Foundation::{IStringable, IUriRuntimeClass, Uri}; + use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize}; + use windows_core::{IInspectable, Interface, h}; + + #[repr(C)] + struct FakeComObject { + vtable: *const *mut std::ffi::c_void, + calls: AtomicU32, + } + + unsafe extern "system" fn increment_struct_first_field( + this: *mut std::ffi::c_void, + value: *mut i32, + ) -> windows_core::HRESULT { + let object = unsafe { &*(this as *const FakeComObject) }; + object.calls.fetch_add(1, Ordering::Relaxed); + unsafe { *value += 1 }; + windows_core::HRESULT(0) + } + + fn struct_in_out_method( + table: &Arc, + expected: TypeHandle, + ) -> (Method, FakeComObject, Box<[*mut std::ffi::c_void; 1]>) { + let method = AbiMethodSignature::new(table) + .add_in_out_type(ParameterType::winrt(expected)) + .build(0); + let vtable = Box::new([increment_struct_first_field as *mut std::ffi::c_void]); + let object = FakeComObject { + vtable: vtable.as_ptr(), + calls: AtomicU32::new(0), + }; + (method, object, vtable) + } + + #[test] + fn fill_array_tracks_distinct_input_and_output_indices() { + let table = MetadataTable::new(); + let method = AbiMethodSignature::new(&table) + .add_in_type(ParameterType::winrt(table.u32_type())) + .add_out_fill_type(ParameterType::winrt(table.array(&table.hstring()))) + .add_out_type(ParameterType::winrt(table.u32_type())) + .build(6); + + assert_eq!(method.info.parameters[0].value_index, 0); + assert_eq!(method.info.parameters[0].input_index, Some(0)); + assert_eq!(method.info.parameters[1].value_index, 0); + assert_eq!(method.info.parameters[1].input_index, Some(1)); + assert_eq!(method.info.parameters[2].value_index, 1); + assert_eq!(method.info.parameters[2].input_index, None); + } + + #[test] + fn coerces_object_inputs_to_the_expected_interface() -> windows_core::Result<()> { + let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; + let uri = Uri::CreateUri(h!("https://example.com"))?; + let default_interface: IUriRuntimeClass = uri.cast()?; + let expected_interface: IStringable = uri.cast()?; + assert_ne!( + default_interface.as_raw(), + expected_interface.as_raw(), + "test requires distinct default and requested interface pointers" + ); + + let table = MetadataTable::new(); + let expected_type = table.interface(IStringable::IID); + let value = WinRTValue::Object(default_interface.cast()?); + let coerced = coerce_input_object(&expected_type, &value)? + .expect("interface parameters must be coerced"); + assert_eq!( + coerced.as_object().unwrap().as_raw(), + expected_interface.as_raw() + ); + + let inspectable: IInspectable = uri.cast()?; + let coerced_object = coerce_input_object(&table.object(), &value)? + .expect("Object parameters must be coerced to IInspectable"); + assert_eq!( + coerced_object.as_object().unwrap().as_raw(), + inspectable.as_raw() + ); + Ok(()) + } + + #[test] + fn raw_pointer_is_rejected_for_winrt_object_params() { + let table = MetadataTable::new(); + let bogus = WinRTValue::RawPtr(0xDEADBEEF as *mut std::ffi::c_void); + + let object_ty = table.object(); + let object_err = coerce_input_object(&object_ty, &bogus) + .expect_err("RawPtr into Object must be rejected"); + assert_eq!(object_err.code().0, 0x80070057u32 as i32); + + let iface_ty = table.interface(IStringable::IID); + let err = coerce_input_object(&iface_ty, &bogus) + .expect_err("RawPtr into a typed interface must be rejected"); + assert_eq!( + err.code().0, + 0x80070057u32 as i32, + "typed-interface RawPtr rejection must use E_INVALIDARG (got {:?})", + err + ); + let msg = err.message(); + assert!( + msg.contains("raw pointer") && msg.contains("typed"), + "rejection error must explain the constraint, got: {}", + msg + ); + + let null_object = WinRTValue::Null; + assert!( + coerce_input_object(&object_ty, &null_object) + .expect("null into TypeKind::Object must be allowed") + .is_none(), + ); + assert!( + coerce_input_object(&iface_ty, &null_object) + .expect("null into typed interface must be allowed") + .is_none(), + ); + } + + #[test] + fn struct_in_out_rejects_different_sized_type_before_native_call() { + let table = MetadataTable::new(); + let expected = + table.struct_type("Test.ExpectedLarge", &[table.i64_type(), table.i64_type()]); + let actual = table.struct_type("Test.ActualSmall", &[table.i32_type()]); + let (method, mut object, _vtable) = struct_in_out_method(&table, expected); + + let error = method + .call_dynamic( + (&mut object as *mut FakeComObject).cast(), + &[WinRTValue::Struct(actual.default_value())], + ) + .expect_err("different-sized struct must be rejected"); + + assert_eq!(error.code().0, 0x80070057u32 as i32); + assert!(error.message().contains("Struct type mismatch")); + assert_eq!(object.calls.load(Ordering::Relaxed), 0); + } + + #[test] + fn struct_in_out_rejects_same_sized_different_type() { + let table = MetadataTable::new(); + let expected = table.struct_type("Test.ExpectedI64", &[table.i64_type()]); + let actual = table.struct_type("Test.ActualF64", &[table.f64_type()]); + assert_eq!(expected.layout(), actual.layout()); + let (method, mut object, _vtable) = struct_in_out_method(&table, expected); + + let error = method + .call_dynamic( + (&mut object as *mut FakeComObject).cast(), + &[WinRTValue::Struct(actual.default_value())], + ) + .expect_err("same-sized different struct must be rejected"); + + assert_eq!(error.code().0, 0x80070057u32 as i32); + assert!(error.message().contains("Struct type mismatch")); + assert_eq!(object.calls.load(Ordering::Relaxed), 0); + } + + #[test] + fn struct_in_out_accepts_exact_type_and_returns_updated_value() -> windows_core::Result<()> { + let table = MetadataTable::new(); + let expected = table.struct_type("Test.Counter", &[table.i32_type()]); + let mut actual = expected.default_value(); + actual.set_field(0, 41i32); + let (method, mut object, _vtable) = struct_in_out_method(&table, expected); + + let result = method.call_dynamic( + (&mut object as *mut FakeComObject).cast(), + &[WinRTValue::Struct(actual)], + )?; + let WinRTValue::Struct(value) = &result[0] else { + panic!("expected struct result"); + }; + + assert_eq!(value.get_field::(0), 42); + assert_eq!(object.calls.load(Ordering::Relaxed), 1); + Ok(()) + } + + #[test] + fn struct_array_rejects_different_element_type() { + let table = MetadataTable::new(); + let expected_element = table.struct_type( + "Test.ExpectedArrayElement", + &[table.i64_type(), table.i64_type()], + ); + let actual_element = table.struct_type("Test.ActualArrayElement", &[table.i32_type()]); + let expected_array = table.array(&expected_element); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + actual_element.clone(), + &[WinRTValue::Struct(actual_element.default_value())], + )); + + let error = coerce_input_array(&expected_array, &value) + .expect_err("different struct array element type must be rejected"); + + assert_eq!(error.code().0, 0x80070057u32 as i32); + assert!(error.message().contains("Array element type mismatch")); + } + + #[test] + fn struct_array_rejects_value_that_lies_about_declared_element_type() { + let table = MetadataTable::new(); + let expected_element = table.struct_type( + "Test.ExpectedDeclaredElement", + &[table.i64_type(), table.i64_type()], + ); + let actual_element = table.struct_type("Test.ActualStoredElement", &[table.i32_type()]); + let expected_array = table.array(&expected_element); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + expected_element.clone(), + &[WinRTValue::Struct(actual_element.default_value())], + )); + + let error = coerce_input_array(&expected_array, &value) + .expect_err("mismatched stored struct value must be rejected"); + + assert_eq!(error.code().0, 0x80070057u32 as i32); + assert!(error.message().contains("Array element 0")); + assert!(error.message().contains("Struct type mismatch")); + } + + #[test] + fn struct_array_accepts_exact_element_type() -> windows_core::Result<()> { + let table = MetadataTable::new(); + let element = table.struct_type("Test.ValidArrayElement", &[table.i32_type()]); + let expected_array = table.array(&element); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + element.clone(), + &[WinRTValue::Struct(element.default_value())], + )); + + assert!(coerce_input_array(&expected_array, &value)?.is_none()); + Ok(()) + } + + #[test] + fn primitive_array_accepts_equivalent_type_from_another_table() -> windows_core::Result<()> { + let signature_table = MetadataTable::new(); + let value_table = MetadataTable::new(); + let expected_array = signature_table.array(&signature_table.i32_type()); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + value_table.i32_type(), + &[WinRTValue::I32(42)], + )); + + assert!(coerce_input_array(&expected_array, &value)?.is_none()); + Ok(()) + } + + #[test] + fn char16_array_accepts_u16_projection() -> windows_core::Result<()> { + let table = MetadataTable::new(); + let expected_array = table.array(&table.char16_type()); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + table.u16_type(), + &[WinRTValue::U16('x' as u16)], + )); + + assert!(coerce_input_array(&expected_array, &value)?.is_none()); + Ok(()) + } + + #[test] + fn enum_array_accepts_i32_projection() -> windows_core::Result<()> { + let table = MetadataTable::new(); + let enum_type = table.enum_type("Test.ProjectedEnum", vec![("Value".to_string(), 7)]); + let expected_array = table.array(&enum_type); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + table.i32_type(), + &[WinRTValue::I32(7)], + )); + + assert!(coerce_input_array(&expected_array, &value)?.is_none()); + Ok(()) + } + + #[test] + fn enum_array_rejects_a_different_named_enum() { + let table = MetadataTable::new(); + let expected = table.enum_type("Test.ExpectedEnum", Vec::new()); + let actual = table.enum_type("Test.ActualEnum", Vec::new()); + let expected_array = table.array(&expected); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + actual.clone(), + &[WinRTValue::Enum { + value: 0, + type_handle: actual, + }], + )); + + let error = coerce_input_array(&expected_array, &value) + .expect_err("different named enum array must be rejected"); + + assert_eq!(error.code().0, 0x80070057u32 as i32); + assert!(error.message().contains("Array element type mismatch")); + } + + #[test] + fn struct_array_rejects_equivalent_layout_from_another_table() { + let signature_table = MetadataTable::new(); + let value_table = MetadataTable::new(); + let expected_element = + signature_table.struct_type("Test.CrossTable", &[signature_table.i32_type()]); + let actual_element = value_table.struct_type("Test.CrossTable", &[value_table.i32_type()]); + assert_eq!( + expected_element.signature_string(), + actual_element.signature_string() + ); + assert_eq!(expected_element.layout(), actual_element.layout()); + let expected_array = signature_table.array(&expected_element); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + actual_element.clone(), + &[WinRTValue::Struct(actual_element.default_value())], + )); + + let error = coerce_input_array(&expected_array, &value) + .expect_err("struct identity from another table must be rejected"); + + assert_eq!(error.code().0, 0x80070057u32 as i32); + assert!(error.message().contains("Array element type mismatch")); + } + + #[test] + fn coerces_object_array_elements_to_the_expected_interface() -> windows_core::Result<()> { + let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; + let uri = Uri::CreateUri(h!("https://example.com"))?; + let default_interface: IUriRuntimeClass = uri.cast()?; + let expected_interface: IStringable = uri.cast()?; + + let table = MetadataTable::new(); + let element_type = table.interface(IStringable::IID); + let array_type = table.array(&element_type); + let value = WinRTValue::Array(crate::array::ArrayData::from_values( + element_type, + &[WinRTValue::Object(default_interface.cast()?)], + )); + let coerced = coerce_input_array(&array_type, &value)? + .expect("object array elements must be coerced"); + assert_eq!( + coerced + .as_array() + .unwrap() + .get(0) + .as_object() + .unwrap() + .as_raw(), + expected_interface.as_raw() + ); + Ok(()) + } +} diff --git a/crates/dynwinrt/src/signature.rs b/crates/dynwinrt/src/signature.rs index 105a1b24..edb885a8 100644 --- a/crates/dynwinrt/src/signature.rs +++ b/crates/dynwinrt/src/signature.rs @@ -1,452 +1,75 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use libffi::middle::Cif; +//! WinRT method planning. +//! +//! This public surface intentionally exposes only WinRT's HRESULT plus +//! input/output conventions. Classic COM lowers through its own planner in +//! `com`, while both planners share the private `native_call` executor. + use std::sync::Arc; -use windows::core::{GUID, HSTRING, IInspectable, Interface}; + +use windows::core::{GUID, HSTRING}; use crate::{ - call, - call::ArgumentList, - metadata_table::{MetadataTable, TypeHandle, TypeKind}, + metadata_table::{MetadataTable, TypeHandle}, + native_call::{AbiMethodSignature, Method as NativeMethod, ParameterType}, value::WinRTValue, }; -/// How a parameter is passed at the ABI level. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ParamKind { - In, - Out, - /// FillArray: caller allocates buffer, callee fills it. - /// ABI expands to 2 params: (u32 capacity, T* items). - OutFillArray, -} - #[derive(Debug, Clone)] -pub struct Parameter { - pub typ: TypeHandle, - /// Index in the method result vector for out and FillArray parameters. - pub value_index: usize, - /// Index in the caller-provided argument slice. FillArray parameters have - /// both an input index (capacity buffer) and an output index (filled data). - pub input_index: Option, - pub kind: ParamKind, -} - -impl Parameter { - pub fn is_out(&self) -> bool { - matches!(self.kind, ParamKind::Out | ParamKind::OutFillArray) - } - - pub fn is_fill_array(&self) -> bool { - self.kind == ParamKind::OutFillArray - } -} - -#[derive(Debug, Clone)] -pub struct MethodSignature { - out_count: usize, - input_count: usize, - parameters: Vec, - return_type: TypeHandle, - #[allow(dead_code)] - is_opaque: bool, - #[allow(dead_code)] - table: Arc, -} +pub struct MethodSignature(AbiMethodSignature); impl MethodSignature { pub fn new(table: &Arc) -> Self { - MethodSignature { - out_count: 0, - input_count: 0, - parameters: Vec::new(), - return_type: table.hresult(), - is_opaque: false, - table: Arc::clone(table), - } + Self(AbiMethodSignature::new(table)) } pub fn new_with_registry(table: &Arc) -> Self { Self::new(table) } - pub fn add_in(mut self, typ: TypeHandle) -> Self { - let input_index = self.input_count; - self.input_count += 1; - self.parameters.push(Parameter { - kind: ParamKind::In, - typ, - value_index: input_index, - input_index: Some(input_index), - }); - self + pub fn add_in(self, typ: TypeHandle) -> Self { + Self(self.0.add_in_type(ParameterType::winrt(typ))) } - pub fn add_out(mut self, typ: TypeHandle) -> Self { - self.parameters.push(Parameter { - kind: ParamKind::Out, - typ, - value_index: self.out_count, - input_index: None, - }); - self.out_count += 1; - self + pub fn add_out(self, typ: TypeHandle) -> Self { + Self(self.0.add_out_type(ParameterType::winrt(typ))) } - /// Add a FillArray out parameter: caller allocates buffer, callee fills it. - /// ABI expands to (u32 capacity, T* items). - pub fn add_out_fill(mut self, typ: TypeHandle) -> Self { - let input_index = self.input_count; - self.input_count += 1; - self.parameters.push(Parameter { - kind: ParamKind::OutFillArray, - typ, - value_index: self.out_count, - input_index: Some(input_index), - }); - self.out_count += 1; - self + pub fn add_out_fill(self, typ: TypeHandle) -> Self { + Self(self.0.add_out_fill_type(ParameterType::winrt(typ))) } pub fn build(self, index: usize) -> Method { - use libffi::middle::Type; - let mut types: Vec = Vec::with_capacity(self.parameters.len() + 1); - types.push(Type::pointer()); // com object's this pointer - for param in &self.parameters { - if param.is_fill_array() { - // FillArray: UINT32 capacity, T* items - types.push(Type::u32()); - types.push(Type::pointer()); - } else if param.typ.is_array() { - if param.is_out() { - // ReceiveArray: UINT32* out_length, T** out_data - types.push(Type::pointer()); - types.push(Type::pointer()); - } else { - // PassArray: UINT32 length, T* data - types.push(Type::u32()); - types.push(Type::pointer()); - } - } else if param.is_out() { - types.push(Type::pointer()); - } else { - types.push(param.typ.libffi_type()); - } - } - let in_count = self.parameters.len() - self.out_count; - let has_complex_param = self.parameters.iter().any(|p| { - p.typ.is_array() || p.is_fill_array() || matches!(p.typ.kind(), TypeKind::Struct(_)) - }); - - // Check if the single in-param (if any) is a simple non-HString, non-Struct type - let simple_in = !has_complex_param && in_count == 1 && { - let in_param = self.parameters.iter().find(|p| !p.is_out()).unwrap(); - !matches!(in_param.typ.kind(), TypeKind::HString) - }; - - // Classify array parameters - let array_in_count = self - .parameters - .iter() - .filter(|p| !p.is_out() && p.typ.is_array()) - .count(); - let fill_out_count = self.parameters.iter().filter(|p| p.is_fill_array()).count(); - let array_out_count = self - .parameters - .iter() - .filter(|p| p.is_out() && p.typ.is_array() && !p.is_fill_array()) - .count(); - let scalar_in_count = in_count - array_in_count; - let scalar_out_count = self.out_count - fill_out_count - array_out_count; - - let strategy = if !has_complex_param && in_count == 0 && self.out_count == 1 { - CallStrategy::Direct0In1Out - } else if !has_complex_param && in_count == 0 && self.out_count == 0 { - CallStrategy::Direct0In0Out - } else if simple_in && self.out_count == 0 { - CallStrategy::Direct1In0Out - } else if simple_in && self.out_count == 1 { - CallStrategy::Direct1In1Out - // ReceiveArray only: fn(this, *mut u32, *mut *mut c_void) -> HRESULT - } else if scalar_in_count == 0 - && array_in_count == 0 - && array_out_count == 1 - && fill_out_count == 0 - && scalar_out_count == 0 - { - CallStrategy::DirectReceiveArray - // PassArray + 1 out: fn(this, u32, *const u8, out) -> HRESULT - } else if scalar_in_count == 0 - && array_in_count == 1 - && array_out_count == 0 - && fill_out_count == 0 - && scalar_out_count == 1 - { - CallStrategy::DirectPassArray1Out - // FillArray only: fn(this, u32, *mut u8, *mut u32) -> HRESULT - } else if scalar_in_count == 0 - && array_in_count == 0 - && fill_out_count == 1 - && array_out_count == 0 - && scalar_out_count == 0 - { - CallStrategy::DirectFillArray - // 1 scalar in + FillArray: fn(this, val, u32, *mut u8, *mut u32) -> HRESULT - } else if scalar_in_count == 1 - && array_in_count == 0 - && fill_out_count == 1 - && array_out_count == 0 - && scalar_out_count == 0 - { - let in_param = self - .parameters - .iter() - .find(|p| !p.is_out() && !p.typ.is_array()) - .unwrap(); - if !matches!(in_param.typ.kind(), TypeKind::HString | TypeKind::Struct(_)) { - CallStrategy::Direct1InFillArray - } else { - CallStrategy::Libffi(Cif::new( - types.into_iter(), - self.return_type.abi_type().libffi_type(), - )) - } - } else { - CallStrategy::Libffi(Cif::new( - types.into_iter(), - self.return_type.abi_type().libffi_type(), - )) - }; - - Method { - info: MethodInfo { - index, - parameters: self.parameters, - out_count: self.out_count, - }, - strategy, - } + Method(self.0.build(index)) } } #[derive(Debug)] -pub struct MethodInfo { - pub index: usize, - pub parameters: Vec, - pub out_count: usize, -} - -/// How a Method should be invoked — decided once at build time. -#[derive(Debug)] -enum CallStrategy { - /// 0 in + 0 out: fn(this) -> HRESULT. - Direct0In0Out, - /// 0 in + 1 out (getter): fn(this, out) -> HRESULT. - Direct0In1Out, - /// 1 in + 0 out (setter, non-HString): fn(this, val) -> HRESULT. - Direct1In0Out, - /// 1 in + 1 out (factory/query, non-HString in): fn(this, val, out) -> HRESULT. - Direct1In1Out, - /// ReceiveArray: fn(this, *mut u32, *mut *mut c_void) -> HRESULT. - DirectReceiveArray, - /// PassArray + 1 out: fn(this, u32, *const u8, out) -> HRESULT. - DirectPassArray1Out, - /// FillArray only: fn(this, u32, *mut u8) -> HRESULT. - DirectFillArray, - /// 1 scalar in + FillArray: fn(this, val, u32, *mut u8) -> HRESULT. - Direct1InFillArray, - /// General case → libffi via cached Cif. - Libffi(Cif), -} - -#[derive(Debug)] -pub struct Method { - info: MethodInfo, - strategy: CallStrategy, -} - -fn expected_object_iid(typ: &TypeHandle) -> Option { - match typ.kind() { - TypeKind::Object => Some(IInspectable::IID), - TypeKind::Interface(_) - | TypeKind::Delegate(_) - | TypeKind::RuntimeClass(_) - | TypeKind::Parameterized(_) - | TypeKind::IAsyncAction - | TypeKind::IAsyncActionWithProgress(_) - | TypeKind::IAsyncOperation(_) - | TypeKind::IAsyncOperationWithProgress(_) => typ.iid(), - _ => None, - } -} - -fn coerce_input_object( - expected: &TypeHandle, - value: &WinRTValue, -) -> windows_core::Result> { - let Some(iid) = expected_object_iid(expected) else { - return Ok(None); - }; - if value.is_null_object() { - return Ok(None); - } - - let object = value.as_object().ok_or_else(|| { - windows_core::Error::new( - windows_core::HRESULT(0x80070057u32 as i32), - &format!( - "Expected object argument for {}, found {:?}", - expected.signature_string(), - value.get_type_kind() - ), - ) - })?; - - WinRTValue::Object(object) - .cast(&iid) - .map(Some) - .map_err(|error| match error { - crate::result::Error::WindowsError(error) => error, - other => windows_core::Error::new( - windows_core::HRESULT(0x80070057u32 as i32), - &other.message(), - ), - }) -} - -fn coerce_input_array( - expected: &TypeHandle, - value: &WinRTValue, -) -> windows_core::Result> { - if !expected.is_array() { - return Ok(None); - } - - let element_type = expected.array_element_type(); - if expected_object_iid(&element_type).is_none() { - return Ok(None); - } - - let array = value.as_array().ok_or_else(|| { - windows_core::Error::new( - windows_core::HRESULT(0x80070057u32 as i32), - &format!( - "Expected array argument for {}, found {:?}", - expected.signature_string(), - value.get_type_kind() - ), - ) - })?; - let mut values = Vec::with_capacity(array.len()); - let mut changed = false; - for index in 0..array.len() { - let value = array.get(index); - if let Some(coerced) = coerce_input_object(&element_type, &value)? { - values.push(coerced); - changed = true; - } else { - values.push(value); - } - } - - Ok(changed - .then(|| WinRTValue::Array(crate::array::ArrayData::from_values(element_type, &values)))) -} - -struct InvocationArgs<'a> { - original: &'a [WinRTValue], - replacements: Option>>, -} - -impl<'a> InvocationArgs<'a> { - fn new(original: &'a [WinRTValue]) -> Self { - Self { - original, - replacements: None, - } - } - - fn replace(&mut self, index: usize, value: WinRTValue) { - self.replacements.get_or_insert_with(|| { - std::iter::repeat_with(|| None) - .take(self.original.len()) - .collect() - })[index] = Some(value); - } -} - -impl call::ArgumentList for InvocationArgs<'_> { - fn get_value(&self, index: usize) -> &WinRTValue { - self.replacements - .as_ref() - .and_then(|values| values[index].as_ref()) - .unwrap_or(&self.original[index]) - } -} +pub struct Method(NativeMethod); impl Method { - // --- Fast getter paths: zero Vec/WinRTValue allocation --- - - /// Getter → i32 (0 in, 1 out). Writes directly to stack i32. pub fn call_getter_i32(&self, obj: *mut std::ffi::c_void) -> windows_core::Result { - let mut out: i32 = 0; - let hr = call::call_winrt_method_1( - self.info.index, - obj, - &mut out as *mut i32 as *mut std::ffi::c_void, - ); - hr.ok()?; - Ok(out) + self.0.call_getter_i32(obj) } - /// Getter → bool (0 in, 1 out). Writes directly to stack bool. pub fn call_getter_bool(&self, obj: *mut std::ffi::c_void) -> windows_core::Result { - let mut out: i32 = 0; // WinRT bool is i32 on ABI - let hr = call::call_winrt_method_1( - self.info.index, - obj, - &mut out as *mut i32 as *mut std::ffi::c_void, - ); - hr.ok()?; - Ok(out != 0) + self.0.call_getter_bool(obj) } - /// Getter → HSTRING (0 in, 1 out). Writes directly to stack HSTRING ptr. pub fn call_getter_hstring( &self, obj: *mut std::ffi::c_void, ) -> windows_core::Result { - // HSTRING is a pointer-sized handle on ABI. Let WinRT write it directly. - let mut out = windows_core::HSTRING::new(); - let hr = call::call_winrt_method_1( - self.info.index, - obj, - &mut out as *mut windows_core::HSTRING as *mut std::ffi::c_void, - ); - hr.ok()?; - Ok(out) + self.0.call_getter_hstring(obj) } - /// Getter → COM object (0 in, 1 out). Writes directly to stack pointer. pub fn call_getter_object( &self, obj: *mut std::ffi::c_void, ) -> windows_core::Result { - let mut out: *mut std::ffi::c_void = std::ptr::null_mut(); - let hr = call::call_winrt_method_1( - self.info.index, - obj, - &mut out as *mut _ as *mut std::ffi::c_void, - ); - hr.ok()?; - if out.is_null() { - Ok(WinRTValue::Null) - } else { - Ok(WinRTValue::Object(unsafe { - windows_core::IUnknown::from_raw(out) - })) - } + self.0.call_getter_object(obj) } pub fn call_dynamic( @@ -454,254 +77,21 @@ impl Method { obj: *mut std::ffi::c_void, args: &[WinRTValue], ) -> windows_core::Result> { - let mut args = InvocationArgs::new(args); - for parameter in self.info.parameters.iter().filter(|p| !p.is_out()) { - let value = args.get_value(parameter.value_index); - let coerced = if parameter.typ.is_array() { - coerce_input_array(¶meter.typ, value)? - } else { - coerce_input_object(¶meter.typ, value)? - }; - if let Some(value) = coerced { - args.replace(parameter.value_index, value); - } - } - - match &self.strategy { - CallStrategy::Direct0In0Out => { - // 0 in + 0 out: fn(this) -> HRESULT - let hr = call::call_winrt_method_0(self.info.index, obj); - hr.ok()?; - Ok(vec![]) - } - CallStrategy::Direct0In1Out => { - // 0 in + 1 out: fn(this, out) -> HRESULT - let param = &self.info.parameters[0]; - let mut out = param.typ.default_winrt_value(); - let hr = call::call_winrt_method_1(self.info.index, obj, out.out_ptr()); - hr.ok()?; - // COM pointer types use RawPtr(null) as buffer to avoid IUnknown::from_raw(null) UB. - // After COM writes the pointer, convert via from_out. - if let WinRTValue::RawPtr(raw_ptr) = out { - out = param.typ.from_out(raw_ptr).map_err(|e| { - windows_core::Error::new(windows_core::HRESULT(-1), &format!("{:?}", e)) - })?; - } - out.sanitize_null_object(); - Ok(vec![out]) - } - CallStrategy::Direct1In0Out => { - // 1 in + 0 out: fn(this, val) -> HRESULT - let hr = call::call_1in(self.info.index, obj, args.get_value(0)); - hr.ok()?; - Ok(vec![]) - } - CallStrategy::Direct1In1Out => { - // 1 in + 1 out: fn(this, val, out) -> HRESULT - let out_param = self.info.parameters.iter().find(|p| p.is_out()).unwrap(); - let mut out = out_param.typ.default_winrt_value(); - let hr = - call::call_1in_1out(self.info.index, obj, args.get_value(0), out.out_ptr()); - hr.ok()?; - if let WinRTValue::RawPtr(raw_ptr) = out { - out = out_param.typ.from_out(raw_ptr).map_err(|e| { - windows_core::Error::new(windows_core::HRESULT(-1), &format!("{:?}", e)) - })?; - } - out.sanitize_null_object(); - Ok(vec![out]) - } - CallStrategy::DirectReceiveArray => { - // fn(this, *mut u32, *mut *mut c_void) -> HRESULT - let param = &self.info.parameters[0]; - let elem_type = param.typ.array_element_type(); - let mut length: u32 = 0; - let mut data_ptr: *mut std::ffi::c_void = std::ptr::null_mut(); - let fptr = call::get_vtable_function_ptr(obj, self.info.index); - let hr: windows_core::HRESULT = unsafe { - let method: unsafe extern "system" fn( - *mut std::ffi::c_void, - *mut u32, - *mut *mut std::ffi::c_void, - ) - -> windows_core::HRESULT = std::mem::transmute(fptr); - method(obj, &mut length, &mut data_ptr) - }; - if hr.is_err() { - // Callee may have allocated a buffer before returning failure. - // Wrap in ArrayData to release elements + CoTaskMemFree. - if !data_ptr.is_null() { - let _ = crate::array::ArrayData::from_cotaskmem( - elem_type.clone(), - data_ptr, - length as usize, - ); - } - hr.ok()?; - } - let array = if data_ptr.is_null() || length == 0 { - if !data_ptr.is_null() { - unsafe { - windows::Win32::System::Com::CoTaskMemFree(Some(data_ptr)); - } - } - - crate::array::ArrayData::empty(elem_type) - } else { - crate::array::ArrayData::from_cotaskmem(elem_type, data_ptr, length as usize) - }; - Ok(vec![WinRTValue::Array(array)]) - } - CallStrategy::DirectPassArray1Out => { - // fn(this, u32, *const u8, out) -> HRESULT - let in_param = self.info.parameters.iter().find(|p| !p.is_out()).unwrap(); - let out_param = self.info.parameters.iter().find(|p| p.is_out()).unwrap(); - let array_data = args.get_value(in_param.value_index).as_array().unwrap(); - let buffer = array_data.serialize_for_abi(); - let mut out = out_param.typ.default_winrt_value(); - let fptr = call::get_vtable_function_ptr(obj, self.info.index); - let hr: windows_core::HRESULT = unsafe { - let method: unsafe extern "system" fn( - *mut std::ffi::c_void, - u32, - *const u8, - *mut std::ffi::c_void, - ) - -> windows_core::HRESULT = std::mem::transmute(fptr); - method(obj, array_data.len() as u32, buffer.as_ptr(), out.out_ptr()) - }; - hr.ok()?; - if let WinRTValue::RawPtr(raw_ptr) = out { - out = out_param.typ.from_out(raw_ptr).map_err(|e| { - windows_core::Error::new(windows_core::HRESULT(-1), &format!("{:?}", e)) - })?; - } - out.sanitize_null_object(); - Ok(vec![out]) - } - CallStrategy::DirectFillArray => { - // fn(this, u32, *mut u8) -> HRESULT - // FillArray: caller provides buffer of known capacity, callee fills it. - let param = &self.info.parameters[0]; - let elem_type = param.typ.array_element_type(); - let fptr = call::get_vtable_function_ptr(obj, self.info.index); - - assert!( - param - .input_index - .is_some_and(|index| { args.get_value(index).as_array().is_some() }), - "DirectFillArray requires a pre-allocated array argument with the desired capacity. \ - Pass an ArrayData with the expected number of elements." - ); - let array_data = args - .get_value(param.input_index.unwrap()) - .as_array() - .unwrap(); - let capacity = array_data.len() as u32; - let total_bytes = capacity as usize * elem_type.element_size(); - let buffer_ptr = - unsafe { windows::Win32::System::Com::CoTaskMemAlloc(total_bytes) as *mut u8 }; - assert!(!buffer_ptr.is_null(), "CoTaskMemAlloc failed for FillArray"); - unsafe { std::ptr::write_bytes(buffer_ptr, 0, total_bytes) }; - let hr: windows_core::HRESULT = unsafe { - let method: unsafe extern "system" fn( - *mut std::ffi::c_void, - u32, - *mut u8, - ) - -> windows_core::HRESULT = std::mem::transmute(fptr); - method(obj, capacity, buffer_ptr) - }; - if hr.is_err() { - // Callee may have written elements before failing. - // Buffer was zero-initialized, so null slots are safe to release. - // Use capacity as cleanup length — ArrayData::Drop skips null elements. - let _ = crate::array::ArrayData::from_cotaskmem( - elem_type.clone(), - buffer_ptr as _, - capacity as usize, - ); - hr.ok()?; - } - let array = crate::array::ArrayData::from_cotaskmem( - elem_type, - buffer_ptr as _, - capacity as usize, - ); - Ok(vec![WinRTValue::Array(array)]) - } - CallStrategy::Direct1InFillArray => { - // fn(this, val, u32, *mut u8) -> HRESULT - let in_param = self.info.parameters.iter().find(|p| !p.is_out()).unwrap(); - let fill_param = self - .info - .parameters - .iter() - .find(|p| p.is_fill_array()) - .unwrap(); - let array_data = args - .get_value(fill_param.input_index.unwrap()) - .as_array() - .unwrap(); - let elem_type = fill_param.typ.array_element_type(); - let capacity = array_data.len() as u32; - let total_bytes = capacity as usize * elem_type.element_size(); - let buffer_ptr = - unsafe { windows::Win32::System::Com::CoTaskMemAlloc(total_bytes) as *mut u8 }; - assert!(!buffer_ptr.is_null(), "CoTaskMemAlloc failed for FillArray"); - unsafe { std::ptr::write_bytes(buffer_ptr, 0, total_bytes) }; - let fptr = call::get_vtable_function_ptr(obj, self.info.index); - let hr = call::call_fill_array_1in( - fptr, - obj, - args.get_value(in_param.value_index), - capacity, - buffer_ptr, - ); - if hr.is_err() { - // Buffer was zero-initialized; use capacity for cleanup. - let _ = crate::array::ArrayData::from_cotaskmem( - elem_type.clone(), - buffer_ptr as _, - capacity as usize, - ); - hr.ok()?; - } - let array = crate::array::ArrayData::from_cotaskmem( - elem_type, - buffer_ptr as _, - capacity as usize, - ); - Ok(vec![WinRTValue::Array(array)]) - } - CallStrategy::Libffi(cif) => call::call_winrt_method_dynamic( - self.info.index, - obj, - &self.info.parameters, - &args, - self.info.out_count, - cif, - ), - } + self.0.call_dynamic(obj, args) } } -#[derive(Debug)] pub struct InterfaceSignature { pub name: String, - pub iid: windows_core::GUID, + pub iid: GUID, pub methods: Vec, #[allow(dead_code)] table: Arc, } impl InterfaceSignature { - pub fn define_interface( - name: String, - iid: windows_core::GUID, - table: &Arc, - ) -> Self { - InterfaceSignature { + pub fn define_interface(name: String, iid: GUID, table: &Arc) -> Self { + Self { name, iid, methods: Vec::new(), @@ -710,19 +100,21 @@ impl InterfaceSignature { } pub fn define_from_iunknown(name: &str, iid: GUID, table: &Arc) -> Self { - let mut t = InterfaceSignature::define_interface(name.to_owned(), iid, table); - t.add_method(MethodSignature::new(table)) // 0 QueryInterface - .add_method(MethodSignature::new(table)) // 1 AddRef - .add_method(MethodSignature::new(table)); // 2 Release - t + let mut result = Self::define_interface(name.to_owned(), iid, table); + result + .add_method(MethodSignature::new(table)) + .add_method(MethodSignature::new(table)) + .add_method(MethodSignature::new(table)); + result } pub fn define_from_iinspectable(name: &str, iid: GUID, table: &Arc) -> Self { - let mut t = Self::define_from_iunknown(name, iid, table); - t.add_method(MethodSignature::new(table)) // 3 GetIids - .add_method(MethodSignature::new(table).add_out(table.hstring())) // 4 GetRuntimeClassName - .add_method(MethodSignature::new(table)); // 5 GetTrustLevel - t + let mut result = Self::define_from_iunknown(name, iid, table); + result + .add_method(MethodSignature::new(table)) + .add_method(MethodSignature::new(table).add_out(table.hstring())) + .add_method(MethodSignature::new(table)); + result } pub fn add_method(&mut self, signature: MethodSignature) -> &mut Self { @@ -742,85 +134,15 @@ pub struct RuntimeClassSignature { #[cfg(test)] mod tests { use super::*; - use windows::Foundation::{IStringable, IUriRuntimeClass, Uri}; - use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize}; - use windows_core::{IInspectable, Interface, h}; - - #[test] - fn fill_array_tracks_distinct_input_and_output_indices() { - let table = MetadataTable::new(); - let method = MethodSignature::new(&table) - .add_in(table.u32_type()) - .add_out_fill(table.array(&table.hstring())) - .add_out(table.u32_type()) - .build(6); - - assert_eq!(method.info.parameters[0].value_index, 0); - assert_eq!(method.info.parameters[0].input_index, Some(0)); - assert_eq!(method.info.parameters[1].value_index, 0); - assert_eq!(method.info.parameters[1].input_index, Some(1)); - assert_eq!(method.info.parameters[2].value_index, 1); - assert_eq!(method.info.parameters[2].input_index, None); - } #[test] - fn coerces_object_inputs_to_the_expected_interface() -> windows_core::Result<()> { - let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; - let uri = Uri::CreateUri(h!("https://example.com"))?; - let default_interface: IUriRuntimeClass = uri.cast()?; - let expected_interface: IStringable = uri.cast()?; - assert_ne!( - default_interface.as_raw(), - expected_interface.as_raw(), - "test requires distinct default and requested interface pointers" - ); - + fn winrt_signature_exposes_only_winrt_parameter_contracts() { let table = MetadataTable::new(); - let expected_type = table.interface(IStringable::IID); - let value = WinRTValue::Object(default_interface.cast()?); - let coerced = coerce_input_object(&expected_type, &value)? - .expect("interface parameters must be coerced"); - assert_eq!( - coerced.as_object().unwrap().as_raw(), - expected_interface.as_raw() - ); + let signature = MethodSignature::new(&table) + .add_in(table.i32_type()) + .add_out(table.hstring()) + .add_out_fill(table.array(&table.object())); - let inspectable: IInspectable = uri.cast()?; - let coerced_object = coerce_input_object(&table.object(), &value)? - .expect("Object parameters must be coerced to IInspectable"); - assert_eq!( - coerced_object.as_object().unwrap().as_raw(), - inspectable.as_raw() - ); - Ok(()) - } - - #[test] - fn coerces_object_array_elements_to_the_expected_interface() -> windows_core::Result<()> { - let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; - let uri = Uri::CreateUri(h!("https://example.com"))?; - let default_interface: IUriRuntimeClass = uri.cast()?; - let expected_interface: IStringable = uri.cast()?; - - let table = MetadataTable::new(); - let element_type = table.interface(IStringable::IID); - let array_type = table.array(&element_type); - let value = WinRTValue::Array(crate::array::ArrayData::from_values( - element_type, - &[WinRTValue::Object(default_interface.cast()?)], - )); - let coerced = coerce_input_array(&array_type, &value)? - .expect("object array elements must be coerced"); - assert_eq!( - coerced - .as_array() - .unwrap() - .get(0) - .as_object() - .unwrap() - .as_raw(), - expected_interface.as_raw() - ); - Ok(()) + let _ = signature.build(6); } } diff --git a/crates/dynwinrt/tests/winrt_regression.rs b/crates/dynwinrt/tests/winrt_regression.rs new file mode 100644 index 00000000..9619f49f --- /dev/null +++ b/crates/dynwinrt/tests/winrt_regression.rs @@ -0,0 +1,404 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use dynwinrt::{InterfaceSignature, MetadataTable, MethodSignature, WinRTValue}; +use windows::Devices::Geolocation::{BasicGeoposition, Geopoint, IGeopoint, IGeopointFactory}; +use windows::Foundation::{IPropertyValue, IUriRuntimeClass, IUriRuntimeClassFactory}; +use windows::Win32::System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize}; +use windows_core::{GUID, HRESULT, HSTRING, Interface}; + +fn init_winrt() { + unsafe { RoInitialize(RO_INIT_MULTITHREADED) }.expect("RoInitialize should succeed"); +} + +fn assert_hstring(value: &WinRTValue, expected: &str) { + assert_eq!(value.as_hstring().expect("expected HSTRING"), expected); +} + +fn assert_bool(value: &WinRTValue, expected: bool) { + match value { + WinRTValue::Bool(actual) => assert_eq!(*actual, expected), + other => panic!("expected Bool({expected}), got {other:?}"), + } +} + +fn uri_runtime_class_signature(reg: &std::sync::Arc) -> InterfaceSignature { + let mut iface = InterfaceSignature::define_from_iinspectable( + "IUriRuntimeClass", + IUriRuntimeClass::IID, + reg, + ); + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 6 AbsoluteUri + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 7 DisplayUri + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 8 Domain + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 9 Extension + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 10 Fragment + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 11 Host + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 12 Password + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 13 Path + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 14 Query + iface.add_method(MethodSignature::new(reg).add_out(reg.object())); // 15 QueryParsed + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 16 RawUri + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 17 SchemeName + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 18 UserName + iface.add_method(MethodSignature::new(reg).add_out(reg.i32_type())); // 19 Port + iface.add_method(MethodSignature::new(reg)); // 20 Suspicious (unused) + iface +} + +fn create_uri_dynamic( + reg: &std::sync::Arc, + raw: &str, +) -> windows_core::Result { + let factory = WinRTValue::from_activation_factory(&HSTRING::from("Windows.Foundation.Uri")) + .expect("Windows.Foundation.Uri activation factory"); + let uri_factory = factory + .cast(&IUriRuntimeClassFactory::IID) + .expect("IUriRuntimeClassFactory"); + let mut iface = InterfaceSignature::define_from_iinspectable( + "IUriRuntimeClassFactory", + IUriRuntimeClassFactory::IID, + reg, + ); + iface.add_method( + MethodSignature::new(reg) + .add_in(reg.hstring()) + .add_out(reg.object()), + ); + let uri_factory_obj = uri_factory.as_object().expect("factory object"); + let result = iface.methods[6].call_dynamic( + uri_factory_obj.as_raw(), + &[WinRTValue::HString(HSTRING::from(raw))], + )?; + Ok(result[0].clone()) +} + +fn property_value_statics_signature(reg: &std::sync::Arc) -> InterfaceSignature { + let statics_iid = GUID::from_u128(0x629BDBC8_D932_4FF4_96B9_8D96C5C1E858); + let mut iface = + InterfaceSignature::define_from_iinspectable("IPropertyValueStatics", statics_iid, reg); + for _ in 0..4 { + iface.add_method(MethodSignature::new(reg)); // 6 CreateEmpty through 9 CreateUInt16 + } + iface.add_method( + MethodSignature::new(reg) + .add_in(reg.i32_type()) + .add_out(reg.object()), + ); // 10 CreateInt32 + for _ in 0..6 { + iface.add_method(MethodSignature::new(reg)); // 11 CreateUInt32 through 16 CreateChar16 + } + iface.add_method( + MethodSignature::new(reg) + .add_in(reg.bool_type()) + .add_out(reg.object()), + ); // 17 CreateBoolean + iface.add_method( + MethodSignature::new(reg) + .add_in(reg.hstring()) + .add_out(reg.object()), + ); // 18 CreateString + iface +} + +fn property_value_signature(reg: &std::sync::Arc) -> InterfaceSignature { + let ipv_iid = GUID::from_u128(0x4BD682DD_7554_40E9_9A9B_82654EDE7E62); + let mut iface = InterfaceSignature::define_from_iinspectable("IPropertyValue", ipv_iid, reg); + iface.add_method(MethodSignature::new(reg).add_out(reg.i32_type())); // 6 get_Type + iface.add_method(MethodSignature::new(reg).add_out(reg.bool_type())); // 7 get_IsNumericScalar + for _ in 0..3 { + iface.add_method(MethodSignature::new(reg)); // 8 GetUInt8 through 10 GetUInt16 + } + iface.add_method(MethodSignature::new(reg).add_out(reg.i32_type())); // 11 GetInt32 + for _ in 0..6 { + iface.add_method(MethodSignature::new(reg)); // 12 GetUInt32 through 17 GetChar16 + } + iface.add_method(MethodSignature::new(reg).add_out(reg.bool_type())); // 18 GetBoolean + iface.add_method(MethodSignature::new(reg).add_out(reg.hstring())); // 19 GetString + iface +} + +fn create_property_value( + statics: &WinRTValue, + iface: &InterfaceSignature, + vtable_index: usize, + arg: WinRTValue, +) -> windows_core::Result { + let statics_obj = statics.as_object().expect("statics object"); + Ok(iface.methods[vtable_index].call_dynamic(statics_obj.as_raw(), &[arg])?[0].clone()) +} + +fn as_property_value(value: &WinRTValue) -> WinRTValue { + value + .cast(&IPropertyValue::IID) + .expect("IPropertyValue interface") +} + +fn check_winrt_uri_factory_dynamic_properties_are_golden() -> windows_core::Result<()> { + let reg = MetadataTable::new(); + let uri = create_uri_dynamic(®, "https://www.example.com/a/b?q=2#frag")?; + let uri_obj = uri.as_object().expect("uri object"); + let iface = uri_runtime_class_signature(®); + + assert_hstring( + &iface.methods[6].call_dynamic(uri_obj.as_raw(), &[])?[0], + "https://www.example.com/a/b?q=2#frag", + ); + assert_hstring( + &iface.methods[8].call_dynamic(uri_obj.as_raw(), &[])?[0], + "example.com", + ); + assert_hstring( + &iface.methods[10].call_dynamic(uri_obj.as_raw(), &[])?[0], + "#frag", + ); + assert_hstring( + &iface.methods[11].call_dynamic(uri_obj.as_raw(), &[])?[0], + "www.example.com", + ); + assert_hstring( + &iface.methods[13].call_dynamic(uri_obj.as_raw(), &[])?[0], + "/a/b", + ); + assert_hstring( + &iface.methods[14].call_dynamic(uri_obj.as_raw(), &[])?[0], + "?q=2", + ); + assert_hstring( + &iface.methods[16].call_dynamic(uri_obj.as_raw(), &[])?[0], + "https://www.example.com/a/b?q=2#frag", + ); + assert_hstring( + &iface.methods[17].call_dynamic(uri_obj.as_raw(), &[])?[0], + "https", + ); + assert_eq!( + iface.methods[19].call_dynamic(uri_obj.as_raw(), &[])?[0] + .as_i32() + .unwrap(), + 443 + ); + + Ok(()) +} + +fn check_winrt_uri_empty_path_is_golden() -> windows_core::Result<()> { + let reg = MetadataTable::new(); + let uri = create_uri_dynamic(®, "https://www.example.com")?; + let uri_obj = uri.as_object().expect("uri object"); + let iface = uri_runtime_class_signature(®); + + assert_hstring( + &iface.methods[6].call_dynamic(uri_obj.as_raw(), &[])?[0], + "https://www.example.com/", + ); + assert_hstring( + &iface.methods[13].call_dynamic(uri_obj.as_raw(), &[])?[0], + "/", + ); + assert_hstring( + &iface.methods[14].call_dynamic(uri_obj.as_raw(), &[])?[0], + "", + ); + assert_hstring( + &iface.methods[17].call_dynamic(uri_obj.as_raw(), &[])?[0], + "https", + ); + assert_hstring( + &iface.methods[18].call_dynamic(uri_obj.as_raw(), &[])?[0], + "", + ); + assert_eq!( + iface.methods[19].call_dynamic(uri_obj.as_raw(), &[])?[0] + .as_i32() + .unwrap(), + 443 + ); + + Ok(()) +} + +fn check_property_value_dynamic_scalar_round_trips_are_golden() -> windows_core::Result<()> { + let reg = MetadataTable::new(); + let statics = + WinRTValue::from_activation_factory(&HSTRING::from("Windows.Foundation.PropertyValue")) + .expect("PropertyValue activation factory") + .cast(&GUID::from_u128(0x629BDBC8_D932_4FF4_96B9_8D96C5C1E858)) + .expect("IPropertyValueStatics"); + let statics_iface = property_value_statics_signature(®); + let value_iface = property_value_signature(®); + + let int_value = as_property_value(&create_property_value( + &statics, + &statics_iface, + 10, + WinRTValue::I32(-12345), + )?); + let int_obj = int_value.as_object().expect("IPropertyValue int object"); + assert_eq!( + value_iface.methods[6].call_dynamic(int_obj.as_raw(), &[])?[0] + .as_i32() + .unwrap(), + 4 + ); + assert_bool( + &value_iface.methods[7].call_dynamic(int_obj.as_raw(), &[])?[0], + false, + ); + assert_eq!( + value_iface.methods[11].call_dynamic(int_obj.as_raw(), &[])?[0] + .as_i32() + .unwrap(), + -12345 + ); + + let bool_value = as_property_value(&create_property_value( + &statics, + &statics_iface, + 17, + WinRTValue::Bool(true), + )?); + let bool_obj = bool_value.as_object().expect("IPropertyValue bool object"); + assert_eq!( + value_iface.methods[6].call_dynamic(bool_obj.as_raw(), &[])?[0] + .as_i32() + .unwrap(), + 11 + ); + assert_bool( + &value_iface.methods[7].call_dynamic(bool_obj.as_raw(), &[])?[0], + false, + ); + assert_bool( + &value_iface.methods[18].call_dynamic(bool_obj.as_raw(), &[])?[0], + true, + ); + + let string_value = as_property_value(&create_property_value( + &statics, + &statics_iface, + 18, + WinRTValue::HString(HSTRING::from("dynwinrt regression")), + )?); + let string_obj = string_value + .as_object() + .expect("IPropertyValue string object"); + assert_eq!( + value_iface.methods[6].call_dynamic(string_obj.as_raw(), &[])?[0] + .as_i32() + .unwrap(), + 12 + ); + assert_bool( + &value_iface.methods[7].call_dynamic(string_obj.as_raw(), &[])?[0], + false, + ); + assert_hstring( + &value_iface.methods[19].call_dynamic(string_obj.as_raw(), &[])?[0], + "dynwinrt regression", + ); + + Ok(()) +} + +fn check_property_value_dynamic_type_mismatch_returns_golden_error() -> windows_core::Result<()> { + let reg = MetadataTable::new(); + let statics = + WinRTValue::from_activation_factory(&HSTRING::from("Windows.Foundation.PropertyValue")) + .expect("PropertyValue activation factory") + .cast(&GUID::from_u128(0x629BDBC8_D932_4FF4_96B9_8D96C5C1E858)) + .expect("IPropertyValueStatics"); + let statics_iface = property_value_statics_signature(®); + let value_iface = property_value_signature(®); + let int_value = as_property_value(&create_property_value( + &statics, + &statics_iface, + 10, + WinRTValue::I32(7), + )?); + let int_obj = int_value.as_object().expect("IPropertyValue int object"); + + let err = value_iface.methods[19] + .call_dynamic(int_obj.as_raw(), &[]) + .expect_err("GetString on an Int32 PropertyValue should fail"); + assert_eq!(err.code(), HRESULT(0x80028CA0u32 as i32)); + + Ok(()) +} + +fn check_geopoint_struct_layout_and_dynamic_position_round_trip_are_golden() +-> windows_core::Result<()> { + let reg = MetadataTable::new(); + let f64_type = reg.f64_type(); + let geo_type = reg.struct_type( + "Windows.Devices.Geolocation.BasicGeoposition", + &[f64_type.clone(), f64_type.clone(), f64_type], + ); + assert_eq!(geo_type.size_of(), 24); + assert_eq!(geo_type.align_of(), 8); + assert_eq!(geo_type.field_offset(0), 0); + assert_eq!(geo_type.field_offset(1), 8); + assert_eq!(geo_type.field_offset(2), 16); + + let mut geo_value = geo_type.default_value(); + assert_eq!(geo_value.get_field::(0), 0.0); + assert_eq!(geo_value.get_field::(1), 0.0); + assert_eq!(geo_value.get_field::(2), 0.0); + geo_value.set_field(0, 47.643); + geo_value.set_field(1, -122.131); + geo_value.set_field(2, 100.5); + + let projected = Geopoint::Create(BasicGeoposition { + Latitude: 47.643, + Longitude: -122.131, + Altitude: 100.5, + })?; + let projected_position = projected.Position()?; + assert!((projected_position.Latitude - 47.643).abs() < 1e-6); + assert!((projected_position.Longitude + 122.131).abs() < 1e-6); + assert!((projected_position.Altitude - 100.5).abs() < 1e-6); + + let factory = + WinRTValue::from_activation_factory(&HSTRING::from("Windows.Devices.Geolocation.Geopoint")) + .expect("Geopoint activation factory") + .cast(&IGeopointFactory::IID) + .expect("IGeopointFactory"); + let mut factory_iface = InterfaceSignature::define_from_iinspectable( + "IGeopointFactory", + IGeopointFactory::IID, + ®, + ); + factory_iface.add_method( + MethodSignature::new(®) + .add_in(geo_type.clone()) + .add_out(reg.object()), + ); + let factory_obj = factory.as_object().expect("factory object"); + let created = factory_iface.methods[6] + .call_dynamic(factory_obj.as_raw(), &[WinRTValue::Struct(geo_value)])?; + let geopoint: IGeopoint = created[0].as_object().expect("Geopoint object").cast()?; + + let mut geopoint_iface = + InterfaceSignature::define_from_iinspectable("IGeopoint", IGeopoint::IID, ®); + geopoint_iface.add_method(MethodSignature::new(®).add_out(geo_type)); + let position = geopoint_iface.methods[6].call_dynamic(geopoint.as_raw(), &[])?; + let data = position[0].as_struct().expect("BasicGeoposition struct"); + assert!((data.get_field::(0) - 47.643).abs() < 1e-6); + assert!((data.get_field::(1) + 122.131).abs() < 1e-6); + assert!((data.get_field::(2) - 100.5).abs() < 1e-6); + + Ok(()) +} + +#[test] +fn winrt_regression_harness_golden_behaviors() -> windows_core::Result<()> { + init_winrt(); + + check_winrt_uri_factory_dynamic_properties_are_golden()?; + check_winrt_uri_empty_path_is_golden()?; + check_property_value_dynamic_scalar_round_trips_are_golden()?; + check_property_value_dynamic_type_mismatch_returns_golden_error()?; + check_geopoint_struct_layout_and_dynamic_position_round_trip_are_golden()?; + + Ok(()) +} diff --git a/docs/classic-com-support.md b/docs/classic-com-support.md new file mode 100644 index 00000000..2889a1bf --- /dev/null +++ b/docs/classic-com-support.md @@ -0,0 +1,688 @@ +# Classic COM support + +`dynwinrt` supports a deliberately limited subset of Classic COM. It is not a +general Automation or native Win32 projection. + +The design keeps the existing WinRT API separate: + +```js +import { DynWinRtType, DynWinRtValue } from '@microsoft/dynwinrt'; +import { DynCom, DynComMethodSig } from '@microsoft/dynwinrt/com'; +``` + +Both entrypoints use the same native N-API binary and private libffi call +machinery. Classic COM metadata, generated wrappers, ownership rules, and +public APIs remain separate from the WinRT projection. + +Language ergonomics belong to codegen projection, after native semantics have +been validated. The runtime executes a faithful ABI plan; the JavaScript +projection chooses Buffer/string/bigint, naming, hidden ABI parameters, and +return shapes; the renderer only serializes those decisions. Classic COM work +must not change existing WinRT metadata, generated output, ownership, runtime +behavior, or the `@microsoft/dynwinrt` root API. + +## Runtime call architecture + +WinRT and Classic COM have separate semantic planners. They share only the +private native-call backend and executor: + +```text +WinRT metadata -> signature.rs (WinRT planner) --------\ + -> native_call.rs -> call.rs -> native method +COM metadata -> com.rs (COM planner and method table) / +``` + +| Layer | Responsibility | +|---|---| +| `signature.rs` | WinRT-only signature facade. It preserves the existing `In`, `Out`, fill-array, HRESULT, and out-value conventions. It must not expose raw pointers, `InOut`, native direct returns, or other Classic COM semantics. | +| `com.rs` | Classic COM types, method signatures, interface roots, vtable slot numbering, method registry, and method handles. It owns raw-pointer, `InOut`, direct-return, and `void` call semantics without registering methods in the WinRT `MetadataTable`. | +| `native_call.rs` | Private lowering backend. It converts a completed WinRT or COM signature into parameter/output slots, validates input values, expands array ABI parameters, chooses a fast path or prepares a libffi CIF, and coordinates result conversion. It does not own metadata, language projection, or public interface registries. | +| `call.rs` | Private native executor. It reads the vtable function pointer, creates stable ABI storage and libffi arguments, performs the call, and decodes raw output storage according to the completed plan. It must not infer WinRT, Classic COM, ownership, or JavaScript semantics. | + +This separation is semantic, not a duplication of the native executor. WinRT +and Classic COM may both lower primitive and struct layout information through +the same private backend, but only their respective semantic layers may decide +what a type, parameter direction, return convention, or ownership contract +means. + +In particular: + +- WinRT interface methods remain in the WinRT `MetadataTable` and begin at + `IInspectable` slot 6. +- Classic COM maintains its own interface method table and selects slot 3 or 6 + from its `IUnknown` or `IInspectable` root. +- shared native methods are fully built before publication and are immutable + during concurrent invocation; +- exact struct identity is validated before native dispatch, while established + WinRT ABI aliases such as Char16/U16 and enum/I32 arrays remain compatible; + and +- language-friendly choices remain a codegen responsibility after the COM + planner has validated the native contract. + +## Code generation architecture + +Code generation is organized by semantic domain before target language: + +```text +codegen/ +├── winrt/ +│ ├── shared/ +│ ├── javascript/ +│ └── python/ +└── com/ + ├── ir.rs + ├── project/ + │ ├── types.rs + │ └── interop.rs + └── javascript/ + ├── types.rs + └── render.rs +``` + +The Classic COM flow is: + +```text +ComInterfaceMeta + -> COM type projection + -> validated ComType / ProjectedComMethod + -> JavaScript and declaration renderer +``` + +`ComType` is a closed set of supported ABI semantics: primitives, transparent +scalar typedefs, pointer-sized scalars, BOOL/HRESULT, GUID, HSTRING, enums, +explicitly classified handle/data/string pointers, BSTR, raw input pointers, +and managed interfaces with resolved IIDs. Parameter direction, return +convention, result ownership, cleanup, string-buffer relationships, +activation, and dynamic-IID behavior are encoded in the projected IR. + +Arrays, parameterized and async interfaces, delegates, unknown layouts, +unclassified pointer typedefs, unresolved IIDs, unknown allocators, and +unsupported ownership transfers fail during projection. The renderer cannot +see `TypeMeta` or metadata attributes and has no default pointer/Buffer +fallback; it only serializes the validated projected IR with exhaustive type +matches. + +## Size of Windows.Win32.winmd + +The counts below are exact for +`Microsoft.Windows.SDK.Win32Metadata` **69.0.7-preview** +`Windows.Win32.winmd`, read with `windows-metadata` 0.59.0. `` is +excluded. + +There is no single canonical definition of an "API" in ECMA-335 metadata. For +callable entries, the most useful count is: + +```text +17,760 flat P/Invoke functions ++46,233 declared interface methods +=63,993 callable entries +``` + +| Metadata entity | Count | +|---|---:| +| Namespaces | 324 | +| Type definitions | 35,055 | +| Flat P/Invoke functions | 17,760 | +| Interfaces | 7,971 | +| `IUnknown`-rooted interfaces | 7,878 | +| `IInspectable`-rooted interfaces | 43 | +| Other/no-root interfaces | 50 | +| Declared interface methods | 46,233 | +| Structs | 15,944 | +| Enums | 7,784 | +| Enum members | 67,587 | +| Delegates | 3,002 | +| Classes/API containers | 316 | +| Metadata attributes | 38 | +| Non-enum literal constants | 88,931 | + +These numbers describe the metadata, not dynwinrt support: + +- The current Classic COM work targets interface methods. It does **not** + project the 17,760 flat DLL exports. +- An interface declaration may describe a caller-implemented callback rather + than an OS object that can be activated and called. +- The interface count includes graphics, media, WMI, Automation, Shell, and + other families whose native types are not all supported. +- Methods inherited by a derived interface are counted once where they are + declared, not repeated for every derived interface. + +The largest flat-function modules in this metadata version include +`KERNEL32.dll` (1,407), `USER32.dll` (767), `gdiplus.dll` (629), +`ADVAPI32.dll` (619), `GDI32.dll` (431), `OLEAUT32.dll` (405), +`OLE32.dll` (273), and `SHELL32.dll` (244). + +## Type-system problem map + +The following counts come from all 46,233 declared interface methods, not only +the 30-interface frequency sample. Nested pointee types are included in type +occurrence counts. + +| Signature characteristic | Count | +|---|---:| +| Parameters | 79,181 | +| Input parameters | 47,289 | +| Output parameters | 28,058 | +| In/out parameters | 3,834 | +| Optional parameters | 5,362 | +| `HRESULT` returns | 44,309 | +| Direct `void` returns | 1,018 | +| Direct value returns | 906 | +| Mutable pointer occurrences, depth 1 | 36,321 | +| Mutable pointer occurrences, depth 2 | 1,492 | +| Mutable pointer occurrences, depth 3 | 8 | +| Parameters with `NativeArrayInfo` | 2,973 | +| Parameters with `FreeWith` metadata | **13** | +| Unique referenced interfaces | 2,875 | +| Unique referenced structs | 1,491 | +| Unique referenced enums | 1,739 | +| Unique referenced delegates | 71 | +| BSTR occurrences | 7,697 | +| VARIANT-family occurrences | 3,586 | +| SAFEARRAY occurrences | 238 | +| PROPVARIANT occurrences | 156 | +| PROPERTYKEY occurrences | 138 | +| Representative audio-format struct occurrences | 69 | +| FORMATETC/STGMEDIUM occurrences | 27 | + +The implementation should therefore be planned around the following problems, +not around one-off interface fixes. + +### 1. Native layout engine + +**Problem:** A named native type is not enough to call a method. The ABI needs +its exact size, alignment, packing, field offsets, nested layout, architecture +variation, and whether it is a struct or union. + +This is the largest general blocker: 1,491 distinct structs appear in interface +signatures. It affects Direct3D, DXGI, Shell, drag-and-drop, streams, WMI, +audio, and the Property System. + +Required model: + +- sequential and explicit layout; +- nested structs and unions; +- fixed arrays and bitfields; +- x86/x64/ARM64 size and alignment; +- by-value, pointer-to, out, and in/out forms; and +- safe construction and field access in each language binding. + +### 2. Pointer depth and pointee semantics + +**Problem:** `T*`, `T**`, and `T***` are not interchangeable. A pointer may +mean a borrowed object, optional value, caller storage, callee allocation, +array, null-terminated string, interface reference, or opaque token. + +The metadata contains 37,821 pointer occurrences, including 1,500 with depth +greater than one. + +Required model: + +- pointee type and pointer depth; +- const versus writable storage; +- nullable versus required; +- interface pointer versus data pointer; +- input, output, and replacement/in-out semantics; and +- storage size before a native call is allowed. + +### 3. Counted buffers and native arrays + +**Problem:** A pointer plus count is one logical value. Allocating one scalar +for a writable `BYTE*` is a memory overwrite. + +There are 2,973 `NativeArrayInfo` parameters. The projection needs: + +- which parameter supplies the count; +- whether the count is bytes or elements; +- capacity versus actual returned length; +- caller-allocated, callee-allocated, and two-call sizing patterns; +- string termination and encoding; and +- partial writes and failure cleanup. + +Recognized UTF-16 output-buffer shapes are supported today. General writable +native arrays remain fail closed. + +### 4. Ownership and allocator contracts + +**Problem:** The type and pointer depth do not identify who owns memory or how +to release it. + +Only 13 parameters in this metadata carry `FreeWith`, despite thousands of +owned-output contracts. Metadata alone is therefore insufficient. + +The ABI/projection needs explicit ownership such as: + +- borrowed; +- COM `AddRef`/`Release`; +- BSTR / `SysFreeString`; +- `CoTaskMemFree`; +- `LocalFree`; +- allocator/interface-specific release; +- Win32 resource-specific cleanup; and +- custom or unknown ownership, which must fail closed. + +### 5. Discriminated unions: Automation and Property System + +**Problem:** `VARIANT` and `PROPVARIANT` combine a type tag, a union payload, +and nested ownership. Treating either as an opaque pointer is not a complete or +safe projection. + +Required support: + +- scalar and interface alternatives; +- BSTR and other owned strings; +- nested VARIANT values; +- SAFEARRAY and vector alternatives; +- `VariantInit`/`VariantClear` and `PropVariantClear`; +- language conversion and range checking; and +- DISPPARAMS argument order, named arguments, and EXCEPINFO. + +This unlocks `IDispatch`, XML Automation, Task Scheduler, `IPropertyStore`, and +many scripting/management APIs. + +### 6. SAFEARRAY + +**Problem:** SAFEARRAY is a descriptor, not a pointer to a flat JavaScript +array. It carries rank, bounds, element type, locks, ownership, and potentially +non-blittable elements. + +Required support includes multidimensional bounds, lower bounds, element +cleanup, interface/BSTR/VARIANT elements, and safe lock/unlock behavior. + +### 7. Interface in/out and callback implementations + +**Problem:** Replacing `IFoo*` through `IFoo**` requires precise release and +AddRef behavior. Event APIs additionally require dynwinrt to implement an +arbitrary caller-defined COM interface, not merely invoke one. + +Required support: + +- release of the old in/out reference when the contract requires it; +- ownership of the replacement reference; +- generated sink vtables; +- QueryInterface identity and reference counting for implemented objects; +- callback threading/apartment dispatch; and +- conversion of callback failures to HRESULT. + +### 8. Semantic HRESULT values + +**Problem:** Most HRESULTs are throw-or-success, but methods such as +`IPersistFile::IsDirty` use `S_OK` versus `S_FALSE` as their actual result. +Discarding every successful HRESULT loses information. + +Windows.Win32 metadata marks these methods with +`CanReturnMultipleSuccessValuesAttribute`. The COM projection preserves the +numeric successful HRESULT for marked methods while still throwing failed +HRESULTs. Exact documented exceptions such as `IPersistFile::GetCurFile`, +whose metadata omits the marker, are classified explicitly. Other unmarked +HRESULT methods retain the normal throw-or-`void` behavior. + +### 9. Apartment affinity and marshaling + +**Problem:** A valid COM reference is not necessarily callable from every +thread. STA objects require the owning apartment or a marshaled proxy. + +Required support includes: + +- tracking the apartment where a value was acquired; +- preventing unsafe cross-thread calls; +- agile-object detection; +- Global Interface Table or COM marshaling integration; and +- deterministic callback dispatch to the correct apartment. + +### 10. Acquisition and flat-function boundary + +**Problem:** many common interfaces are not created with `CoCreateInstance`. +Examples include `CoGetMalloc`, `CreateBindCtx`, `D2D1CreateFactory`, +`DWriteCreateFactory`, `D3D11CreateDevice`, and shell helper functions. + +The current Classic COM layer can invoke an acquired interface, but a separate +flat-Win32 layer is needed for the 17,760 DLL exports, their calling +conventions, `GetLastError`, callbacks, and handle cleanup. + +### Recommended implementation order + +1. General native struct/union layout. +2. Pointer-depth plus counted-buffer contracts. +3. Explicit allocator/ownership metadata. +4. VARIANT/PROPVARIANT and semantic HRESULT handling. +5. SAFEARRAY. +6. Arbitrary COM sink/interface implementation. +7. Apartment-aware marshaling. +8. Separate flat-Win32 acquisition/invocation layer. + +## What the current PR handles + +The PR establishes a safe Classic COM subset and rejects the rest. It should +not be described as solving every problem in the map above. + +### Implemented + +| Problem | Current implementation | +|---|---| +| WinRT/Classic COM separation | Separate COM metadata/codegen path and `@microsoft/dynwinrt/com` public entrypoint. The WinRT generator and root runtime API remain unchanged. | +| Interface root and vtable layout | Distinguishes `IUnknown` slot 3 from `IInspectable` slot 6 and walks inherited Classic COM interfaces before assigning slots. | +| Method return conventions | Supports normal HRESULT methods, semantic HRESULT values marked with `CanReturnMultipleSuccessValuesAttribute`, native direct scalar, direct pointer at the runtime layer, and direct `void` returns. | +| Basic parameter direction | Supports input, output, and scalar in/out parameters without reducing in/out to out-only. | +| Primitive ABI types | Signed/unsigned integers, floats, BOOL, HRESULT, GUID, enums, and `char16`. | +| Pointer-sized values | `ISize`/`USize` select the correct x86/x64 ABI width and JavaScript uses `bigint`. | +| GUID ABI | Full 16-byte GUID output storage plus GUID value and REFIID/REFGUID pointer patterns. | +| Unsigned enum values | COM-local enum metadata preserves unsigned values, including 32-bit high-bit flags and 64-bit `bigint` literals. | +| Standard COM references | `CoCreateInstance`, QueryInterface, and typed interface outputs carry an owned `+1` reference and release automatically. | +| Ownership provenance | Borrowed numeric/TypedArray pointers cannot be re-adopted as a second COM owner. Native owned outputs are consumed once. | +| Backing-storage lifetime | Buffer/TypedArray owners are retained and detached ArrayBuffers are rejected before native use. | +| Common string ownership | Scalar BSTR output uses `SysFreeString`; supported `PWSTR`/`PSTR` allocations use `CoTaskMemFree`. | +| HSTRING ownership | Classic COM methods that explicitly use HSTRING project strings through owning HSTRING values; outputs release with `WindowsDeleteString`. | +| External interface metadata | Interface parameters require a resolvable IID. Missing referenced metadata fails generation with a `--ref` diagnostic instead of degrading an owned interface to a raw pointer. | +| WinRT runtime-class references | A resolved runtime class lowers through its default interface IID and remains a managed COM value. Missing defaults fail closed. | +| Common interop pattern | Supports HWND + REFIID + `void**` bridges and adopts the returned interface reference. | +| Explicit COM initialization | Activation no longer silently chooses MTA; callers select STA or MTA with `DynCom.initialize()`. | +| Fail-closed generation | Unsupported structs, arrays, pointer outputs, ownership, and in/out shapes stop generation with a targeted error. | +| Consumable output | COM-only generation emits index declarations and package exports and preserves them across incremental generation. | + +### Partially implemented + +| Problem family | Supported subset | Remaining gap | +|---|---|---| +| Native pointers | Pointer width, depth preservation, borrowed pointers, handles, REFIID, and known interface outputs | General nullable/required semantics, arbitrary pointee storage, and all allocator contracts | +| Counted buffers | Recognized caller-owned UTF-16 output buffers and input Buffer pointers | General byte/element output arrays, two-call sizing, actual-length returns, ANSI output decoding | +| Native layout | Primitives, GUID, enum, handle-shaped typedefs, and manually described runtime structs | General metadata-driven struct/union/packing/bitfield layout | +| Allocator ownership | COM Release, BSTR, CoTaskMem, boxed GUID, retained JS buffers | LocalFree, custom allocators, allocator interfaces, unknown ownership | +| Interface pointers | Typed input/output interfaces, QueryInterface, dynamic IID output | Interface in/out replacement and arbitrary implemented sink interfaces | +| Apartments | Explicit initialization and same-thread invocation | Cross-apartment marshaling, GIT/agility handling, callback dispatch | +| Activation | In-process `CoCreateInstance` | `CoGetClassObject`, aggregation, arbitrary CLSCTX, and non-CoCreate factory functions | +| Direct pointer returns | Runtime signature supports them | Metadata codegen does not yet preserve raw-pointer direct-return semantics, so `IMalloc` generation fails closed | + +### Not implemented + +- general struct/union native layout; +- VARIANT, VARIANTARG, DISPPARAMS, and EXCEPINFO; +- PROPVARIANT and the Property System value model; +- SAFEARRAY; +- FORMATETC and STGMEDIUM; +- arbitrary COM event/callback sink generation; +- cross-thread/apartment marshaling; and +- the general flat-Win32 DLL-export and handle-cleanup layer. + +## Supported ABI surface + +| Capability | Status | Notes | +|---|---|---| +| `IUnknown` and `IInspectable` roots | Supported | User methods begin at vtable slot 3 or 6 respectively. Full inherited Classic COM slot numbering is preserved. | +| `HRESULT` methods | Supported | Failed HRESULTs become errors. | +| Semantic `HRESULT` methods | Supported | `CanReturnMultipleSuccessValuesAttribute` preserves successful values such as `S_OK` and `S_FALSE`; failed values still become errors. | +| Native `void` returns | Supported | Used by interfaces such as `IMalloc`. | +| Direct scalar returns | Supported | Includes signed/unsigned integers, floating point values, and enums. | +| Direct pointer returns | Runtime supported; codegen partial | The runtime can describe a pointer return explicitly. Metadata codegen currently fails closed for interfaces such as `IMalloc` because it does not preserve the raw-pointer return kind. | +| `[in]`, `[out]`, and scalar `[in, out]` parameters | Supported | Unsupported composite in/out types fail generation. | +| Primitive integer and floating-point types | Supported | `i8` through `u64`, `f32`, `f64`, `BOOL`, and `HRESULT`. | +| `ISize` / `USize` | Supported | Projected with the target pointer width; verified by an i686 compile check. | +| GUID values and `REFIID`/`REFGUID` pointers | Supported | GUID out storage uses the full 16-byte layout. | +| Signed and unsigned enums/flags | Supported | Values up to unsigned 64-bit are preserved; 64-bit JavaScript values use `bigint`. | +| Typed interface parameters and outputs | Supported | Interface outputs carry an owned COM reference. | +| Opaque pointers and handle-shaped typedefs | Supported with limits | They are pointer values, not COM objects. Cleanup remains type-specific. | +| NUL-terminated string pointer inputs | Supported | Callers pass a NUL-terminated `Buffer` or a borrowed numeric pointer. | +| Caller-owned UTF-16 output buffers | Supported for recognized shapes | The generator allocates and decodes the buffer when metadata identifies the count parameter. | +| Callee-allocated `PWSTR` / `PSTR` outputs | Supported | Generated code decodes and frees `CoTaskMem` storage. | +| Scalar `[out] BSTR*` | Supported | Generated code converts the BSTR and releases it with `SysFreeString`. | +| HSTRING inputs and scalar outputs | Supported | JavaScript strings are converted to owning HSTRING values; returned HSTRING values are decoded and released automatically. | +| Referenced interface types | Supported when IID metadata is loaded | Missing external definitions fail closed and direct callers to pass the defining winmd with `--ref`. | +| Dynamic-IID `void**` outputs | Supported for explicit REFIID shapes | The IID argument must be a pointer-shaped `iid`/`riid` parameter. A GUID passed by value is not REFIID and cannot trigger interface adoption. | +| Explicit apartment initialization | Supported | `DynCom.initialize()` never silently chooses an apartment for the caller. | + +The runtime can manually describe some ABI shapes that the generator rejects. +For example, a carefully defined native struct can be called from Rust, but the +generator does not emit a struct until its native layout is known to be +correct. + +Parameterized and async interfaces, delegates, and native arrays remain +fail-closed until the COM projection can compute their complete IID, callback, +count, and element-ownership contracts. They must never fall back to +`bigint | Buffer`. + +## Unsupported types and shapes + +The generator fails closed for unsupported signatures instead of emitting a +plausible but memory-unsafe binding. + +The native type rows below come from real signatures in +`Windows.Win32.winmd`, including the 30-interface survey, plus the exact +fail-closed diagnostics produced by the current generator. The policy rows +describe known runtime/public-API boundaries. This is not an exhaustive scan +of every type in the 24 MB metadata file. + +| Type or shape | Affected common APIs | Why it is unsupported | Basis | +|---|---|---|---| +| `VARIANT` / `VARIANTARG` | `IDispatch::Invoke`, Automation APIs | Requires a discriminated union with ownership rules for BSTR, interfaces, arrays, decimals, and nested values. | Win32 winmd signature | +| `DISPPARAMS` / `EXCEPINFO` | `IDispatch::Invoke` | Contains VARIANT arrays, BSTR fields, and nested pointer ownership. | Win32 winmd signature | +| `PROPVARIANT` | `IPropertyStore`, Windows Property System | Larger discriminated union with vector, string, stream, and interface ownership. | Win32 winmd signature | +| `PROPERTYKEY` and arbitrary native structs | `IPropertyStore::GetAt` | Native struct layout, alignment, and architecture must be modeled explicitly. | Win32 winmd + codegen diagnostic | +| `SAFEARRAY` | Automation and Office-style COM APIs | Requires rank, bounds, element type, locking, and element cleanup semantics. | Win32 winmd Automation signatures | +| `FORMATETC` / `STGMEDIUM` | `IDataObject`, clipboard, drag-and-drop | `STGMEDIUM` is a union of handles and interfaces with type-specific release behavior. | Win32 winmd + codegen diagnostic | +| Arbitrary unions, bitfields, and nested pointer-rich structs | `D3D11_COUNTER_INFO`, `STATSTG`, `STRRET`, `POINTL`, `BIND_OPTS`, audio/media formats | The current generator has no general native C layout engine. | Win32 winmd + codegen diagnostics | +| Writable caller-sized native arrays | `IDispatch::GetIDsOfNames`, counted byte/element output buffers | A scalar pointee is not sufficient storage. These are rejected unless a supported string-buffer projection applies. | Win32 winmd `NativeArrayInfo` + codegen diagnostic | +| `BSTR**` arrays and BSTR in/out arrays | Automation collection APIs | Each element has independent allocation and release semantics. | Win32 winmd signature + ownership analysis | +| Caller-owned ANSI output buffers | `PSTR` output-buffer APIs | Safe sizing and decoding are not yet projected. | Win32 winmd signature + projection limitation | +| Untyped output pointers without allocator/ownership | `IDXGIFactory::GetPrivateData`, `IAudioClient::IsFormatSupported` | The runtime cannot infer whether the result is borrowed, COM-owned, `CoTaskMem`, or another allocator. | Win32 winmd + codegen diagnostics | +| Interface `[in, out]` ownership | `IWbemServices::OpenNamespace` | Replacing an existing interface pointer requires explicit release/AddRef transfer semantics. | Win32 winmd + codegen diagnostic | +| Arbitrary COM sink/interface implementation | Connection points and event sinks | `Advise` requires implementing a caller-defined COM interface, not only invoking one. | Runtime/public-API boundary | +| COM aggregation | `IClassFactory::CreateInstance` with `pUnkOuter` | The public activation helper always creates a non-aggregated in-process object. | Runtime/public-API boundary | +| General out-of-process activation controls | Custom `CLSCTX` scenarios | `DynCom.coCreateInstance()` currently uses `CLSCTX_INPROC_SERVER`. | Runtime/public-API boundary | +| Flat Win32 DLL exports | `CreateFile`, registry functions, GDI, etc. | These are not COM interfaces and need a separate DLL-export/handle model. | Architecture boundary | + +Consequently, `IDispatch`, `IPropertyStore`, and `IDataObject` are important +and widely encountered interfaces, but they are not currently supported as +complete generated bindings. + +## Public-code frequency snapshot + +There is no authoritative Microsoft ranking of COM interface usage. The table +below is a reproducible demand proxy based on public GitHub code, not runtime +telemetry. + +The snapshot was collected on **2026-07-29** with GitHub code search: + +```text + extension:cpp + NOT path:test + NOT path:tests + NOT path:third_party + NOT path:vendor + NOT path:external + NOT path:generated +``` + +The survey selected 30 representative desktop COM interfaces across COM +infrastructure, Shell, OLE, graphics, audio, WMI, XML, and WebView2. +`IID_IDispatch` and `IID_IStream` were searched instead of their bare names to +reduce collisions with unrelated classes and C++ `std::istream`. + +Two metrics are reported: + +- **`.cpp` hits** is GitHub's total matching-file count after the best-effort + path exclusions above. +- **Repos / first 100** is the number of distinct repositories represented in + the first 100 matching files. It prevents one large repository from being + mistaken for broad adoption, but it is not a count of every matching + repository. + +Vendored code can still appear under other directory names, search ranking and +repository contents change over time, and interfaces used through wrappers may +not mention the native symbol. Treat the numbers as relative prevalence only. + +Each candidate was then checked against +`Microsoft.Windows.SDK.Win32Metadata` **69.0.7-preview** +`Windows.Win32.winmd`, and the current generator was run with `--dry-run` +against the resolved namespace. + +| Rank | Interface/search token | `.cpp` hits | Repos / first 100 | In Win32 winmd | Current codegen | +|---:|---|---:|---:|---|---| +| 1 | `ID3D11Device` | 27,552 | 87 | Yes | Fail closed: native `D3D11_COUNTER_INFO` layout | +| 2 | `IDXGIFactory` | 17,432 | 83 | Yes | Fail closed: untyped output ownership | +| 3 | `IDataObject` | 10,648 | 44 | Yes | Fail closed: `STGMEDIUM`/union layout | +| 4 | `IMalloc` | 10,624 | 56 | Yes | Fail closed: direct raw-pointer return mapping; runtime tested | +| 5 | `IClassFactory` | 6,712 | 70 | Yes | Generates; acquisition helper and live test still needed | +| 6 | `IDispatch` via `IID_IDispatch` | 6,408 | 46 | Yes | Fail closed: counted arrays, VARIANT-family ABI | +| 7 | `IPersistFile` | 5,996 | 97 | Yes | Generates and live-tested | +| 8 | `IConnectionPoint` | 5,832 | 51 | Yes | Generates; implementing event sinks is not supported | +| 9 | `IWbemServices` | 5,680 | 76 | Yes | Fail closed: interface in/out ownership | +| 10 | `IWICImagingFactory` | 4,536 | 83 | Yes | Generates and live-tested | +| 11 | `IDropTarget` | 4,368 | 57 | Yes | Fail closed: native `POINTL` layout | +| 12 | `IShellFolder` | 4,056 | 33 | Yes | Fail closed: native `STRRET` union layout | +| 13 | `IFileDialog` | 4,048 | 98 | Yes | Generates; inherited methods tested through `IFileOpenDialog` | +| 14 | `IXMLDOMDocument` | 3,784 | 46 | Yes | Fail closed: inherits Automation/VARIANT ABI | +| 15 | `ID2D1Factory` | 3,752 | 92 | Yes | Generates; requires flat factory acquisition and native input structs | +| 16 | `IDWriteFactory` | 3,712 | 76 | Yes | Generates; requires flat factory acquisition | +| 17 | `IStream` via `IID_IStream` | 3,560 | 41 | Yes | Fail closed on `STATSTG`; safe runtime subset is live-tested | +| 18 | `IPropertyStore` | 3,400 | 77 | Yes | Fail closed: `PROPERTYKEY` and `PROPVARIANT` | +| 19 | `IShellItem` | 3,028 | 76 | Yes | Generates; acquisition/live test still needed | +| 20 | `IMMDeviceEnumerator` | 2,932 | 83 | Yes | Generates; live result depends on audio services/devices | +| 21 | `IBindCtx` | 2,660 | 42 | Yes | Fail closed: native `BIND_OPTS` layout | +| 22 | `IFileOpenDialog` | 2,536 | 92 | Yes | Generates and live-tested without showing UI | +| 23 | `IRunningObjectTable` | 2,532 | 50 | Yes | Fail closed: native `FILETIME` layout | +| 24 | `IAudioClient` | 2,500 | 82 | Yes | Fail closed: format pointer/output ownership | +| 25 | `IShellLinkW` | 2,128 | 67 | Yes | Generates and live-tested | +| 26 | `ITaskbarList3` | 1,672 | 87 | Yes | Generates and live-tested | +| 27 | `ICoreWebView2` | 1,608 | 35 | **No** | Defined in WebView2 metadata, not Windows.Win32.winmd | +| 28 | `IFileSaveDialog` | 1,188 | 96 | Yes | Generates; live test still needed | +| 29 | `IFileOperation` | 768 | 79 | Yes | Generates and live-tested | +| 30 | `ITaskService` | 461 | 72 | Yes | Fail closed: inherits Automation/VARIANT ABI | + +### What the snapshot shows + +- **29 of 30** candidates are defined as `IUnknown`-rooted interfaces in + Windows.Win32.winmd. `ICoreWebView2` is the only external-metadata case. +- **14 of 29** Win32-metadata candidates pass complete codegen validation. + **15 of 29** fail closed on an unsupported ABI or ownership shape. +- Among the **top 10** by `.cpp` hits, only `IClassFactory`, + `IPersistFile`, `IConnectionPoint`, and `IWICImagingFactory` pass complete + codegen. `IMalloc` has a tested runtime path but not a complete generated + interface. +- The largest unsupported demand clusters are: + - native structs/unions and layout (`D3D11`, `IDataObject`, Shell, streams); + - Automation types (`IDispatch`, XML, Task Scheduler); + - explicit output ownership (`DXGI`, audio); + - interface in/out semantics (WMI); and + - Property System types (`PROPERTYKEY`, `PROPVARIANT`). +- Seven frequency-survey candidates have generated live coverage: + `IPersistFile`, `IWICImagingFactory`, `IFileDialog` through + `IFileOpenDialog`, `IFileOpenDialog`, `IShellLinkW`, `ITaskbarList3`, and + `IFileOperation`. `IMalloc` and `IStream` add runtime-only live coverage. + +This means the current ten-interface suite provides useful ABI breadth, but it +does **not** cover every high-frequency interface. In particular, +`IDataObject`, `IDispatch`, `IPropertyStore`, graphics interfaces, WMI, and +audio remain material gaps. + +## Engineering priority map + +The frequency snapshot is only one input. Test priority also considers stock +Windows availability, deterministic behavior, whether an API requires UI or +hardware, and whether it adds a distinct ABI shape. + +| Interface | Typical use | Current status | +|---|---|---| +| `IStream` | OLE streams, imaging, shell, serialization | Core live test covers counted buffers, seek, and interface output. | +| `IMalloc` | COM task allocator | Core live test covers direct pointer, pointer-sized, scalar, and void returns. | +| `IPersistFile` | Loading and saving persistent COM objects | Core and Node tests query it from `IShellLinkW` and verify `GetClassID`. | +| `IShellLinkW` | Shortcut creation and inspection | Core and Node tests cover strings, `u16`, enums, and scalar outputs. | +| `IFileOpenDialog` | Desktop file selection | Node test covers activation and option round-trip without showing UI. | +| `IFileOperation` | Shell copy/move/delete operations | Node test covers activation, unsigned flags, and state without modifying files. | +| `IWICImagingFactory` | Windows Imaging Component | Node test activates WIC and creates an interface-valued stream. | +| `ITaskbarList3` | Taskbar progress and window state | Node test covers inherited vtable slots, HWND values, BOOL, enums, and `u64`. | +| `IDataTransferManagerInterop` | HWND-to-WinRT data-transfer bridge | Core and Node tests cover `IUnknown`-rooted interop and interface output. | +| `ISystemMediaTransportControlsInterop` | HWND-to-WinRT media controls | Node test covers `IInspectable`-rooted interop and use of the returned WinRT object. | +| `IClassFactory` | Low-level COM activation | High-value next test; needs a public `CoGetClassObject` acquisition path. | +| `IBindCtx` / `IRunningObjectTable` | Monikers and object binding | High-value next test; needs acquisition helpers and validated native structs. | +| `ICreateErrorInfo` / `IErrorInfo` | COM rich error information | Good next test for GUID, wide strings, BSTR, and thread-local error state. | +| `IMMDeviceEnumerator` | Audio endpoint discovery | Generates today, but live behavior depends on available audio endpoints. | +| `IAudioClient` | Low-level audio streaming | Fails closed because its format and output-pointer shapes are not fully modeled. | +| `IDispatch` | Automation and scripting | Unsupported until VARIANT-family marshaling exists. | +| `IPropertyStore` | Shell/property metadata | Unsupported until PROPERTYKEY and PROPVARIANT are modeled. | +| `IDataObject` | Clipboard and drag-and-drop | Unsupported until FORMATETC and STGMEDIUM are modeled. | + +## Automated coverage + +Ten unique Classic COM interfaces are currently exercised. +Core live tests are in +[`crates/dynwinrt/src/com.rs`](../crates/dynwinrt/src/com.rs). The nine Node +runners are in [`tests/runners/com`](../tests/runners/com) and are generated +and executed by [`tests/e2e_test.ps1`](../tests/e2e_test.ps1). + +| Interface | Test layer | Representative coverage | +|---|---|---| +| `IShellLinkW` | Core + Node E2E | Activation, wide strings, hotkeys, show command, and deterministic release. | +| `IPersistFile` | Core + Node E2E | `QueryInterface`, owned returned reference, and GUID output. | +| `IMalloc` | Core | Direct pointer return, `usize` return, direct `i32`, direct `void`, allocation cleanup. | +| `IStream` | Core | Counted byte buffer, `u32` output, `i64` seek, `u64` output, and `IStream**` clone. | +| `ITaskbarList3` | Node E2E | Inherited slots, HWND, BOOL, enum, and `u64`. | +| `IFileOperation` | Node E2E | Coclass activation, unsigned flags, and state query. | +| `IFileOpenDialog` | Node E2E | STA activation and get/set options without user interaction. | +| `IWICImagingFactory` | Node E2E | Explicit CLSID activation and typed interface output. | +| `IDataTransferManagerInterop` | Core + Node E2E | `IUnknown` base, HWND, REFIID, and WinRT interface output. | +| `ISystemMediaTransportControlsInterop` | Node E2E | `IInspectable` base and meaningful use of the returned WinRT projection. | + +Additional regression tests cover: + +- rejection of duplicate ownership through exported pointer bits; +- detached TypedArray backing storage; +- BSTR and `CoTaskMem` cleanup; +- x86 pointer width; +- unsupported native arrays and native struct layouts; +- required parameter preservation; +- unsigned enum values; +- COM-only package generation; and +- separation of `@microsoft/dynwinrt` from `@microsoft/dynwinrt/com`. + +Run the live Classic COM suite with: + +```powershell +$env:DYNWINRT_WIN32_WINMD = "C:\path\to\Windows.Win32.winmd" +.\tests\e2e_test.ps1 -SkipBuild -Lang com +``` + +## Reference counting and ownership + +COM interface references and Win32 handles must not be treated the same. + +| Value source | Ownership in dynwinrt | Cleanup | +|---|---|---| +| `CoCreateInstance` result | Owned `+1` COM reference | Automatic `Release` on `DynWinRtValue` drop/GC, or explicit `release()`. | +| `QueryInterface` / `cast()` result | Owned `+1` COM reference | Automatic `Release`, independently of the source wrapper. | +| Typed interface out parameter | Owned `+1` COM reference from the callee | Automatic `Release`. | +| Interface passed as `[in]` | Borrowed for the duration of the call | No ownership transfer unless the callee explicitly retains it with `AddRef`. | +| Numeric raw pointer | Borrowed | Never automatically released or freed. | +| Buffer/TypedArray pointer | Borrowed and owner-backed | Backing storage is retained and revalidated; it cannot be adopted as a COM owner. | +| `adoptComPointer()` input | Must be a native output carrying an existing `+1` reference | Ownership transfers to the returned wrapper. | +| Callee-allocated `CoTaskMem` string | Owned allocation | Generated conversion frees it with `CoTaskMemFree`. | +| Scalar BSTR output | Owned allocation | Generated conversion frees it with `SysFreeString`. | +| `HANDLE`, `HWND`, `HBITMAP`, etc. | Win32 resource value, not a COM reference | Use the resource-specific API such as `CloseHandle`, `DestroyWindow`, or `DeleteObject` when required. | + +The JavaScript ownership provenance checks intentionally prevent turning a +borrowed numeric or TypedArray pointer into a second owner. This avoids two +wrappers releasing the same COM reference. + +## Test selection guidance + +Prefer new CI tests that: + +1. use stock Windows components; +2. require no network, optional software, or user input; +3. avoid persistent filesystem or system-state changes; +4. assert meaningful results rather than activation alone; +5. add a distinct ABI or ownership shape; and +6. clean up every COM reference, native allocation, and Win32 resource. + +Interfaces that require Office, deprecated Internet Explorer automation, +active drag-and-drop, a populated clipboard, audio hardware, or an Explorer +desktop should remain optional or local-only tests. + +## Related Microsoft documentation + +- [Rules for managing COM reference counts](https://learn.microsoft.com/windows/win32/com/rules-for-managing-reference-counts) +- [IUnknown::QueryInterface](https://learn.microsoft.com/windows/win32/api/unknwn/nf-unknwn-iunknown-queryinterface(q)) +- [IMalloc](https://learn.microsoft.com/windows/win32/api/objidl/nn-objidl-imalloc) +- [IStream](https://learn.microsoft.com/windows/win32/api/objidl/nn-objidl-istream) +- [IPersistFile](https://learn.microsoft.com/windows/win32/api/objidl/nn-objidl-ipersistfile) +- [IFileOperation](https://learn.microsoft.com/windows/win32/api/shobjidl_core/nn-shobjidl_core-ifileoperation) +- [Windows Imaging Component overview](https://learn.microsoft.com/windows/win32/wic/-wic-about-windows-imaging-codec) diff --git a/tests/e2e_test.ps1 b/tests/e2e_test.ps1 index 0ebf947a..ffaf7f47 100644 --- a/tests/e2e_test.ps1 +++ b/tests/e2e_test.ps1 @@ -3,25 +3,33 @@ # Licensed under the MIT License. # # E2E test orchestrator: build, generate, run language-specific runners, collect results. -# All test logic lives in runners/py_runner.py and runners/ts_runner.ts. +# Test logic lives in runners/py_runner.py, runners/ts_runner.ts, and runners/com/*.mjs. # # Usage: # .\tests\e2e_test.ps1 # Full (build + generate + test) # .\tests\e2e_test.ps1 -SkipBuild # Skip build step # .\tests\e2e_test.ps1 -Lang py # Python only # .\tests\e2e_test.ps1 -Lang ts # TypeScript only +# .\tests\e2e_test.ps1 -Lang com # Classic COM only param( [switch]$SkipBuild, - [string[]]$Lang = @("py", "ts") + [ValidateSet("py", "ts", "com")] + [string[]]$Lang = @("py", "ts", "com") ) $ErrorActionPreference = "Stop" +$langWasExplicit = $PSBoundParameters.ContainsKey("Lang") $root = Split-Path $PSScriptRoot -Parent $specsFile = Join-Path $PSScriptRoot "e2e_specs.json" $e2eDir = Join-Path $root "tests\e2e_generated" $runnersDir = Join-Path $root "tests\runners" $pyBindingsDir = Join-Path $e2eDir "python_bindings" +$comBindingsDir = Join-Path $e2eDir "com" +$comShellDir = Join-Path $comBindingsDir "shell" +$comInteropDir = Join-Path $comBindingsDir "interop" +$comWicDir = Join-Path $comBindingsDir "wic" +$comSmtcDir = Join-Path $comBindingsDir "smtc" $env:PATH = "$env:USERPROFILE\.cargo\bin;$env:PATH" @@ -37,10 +45,45 @@ if ("py" -in $Lang -and -not $hasPython) { Write-Host " SKIP Python (not installed)" -ForegroundColor DarkYellow $Lang = $Lang | Where-Object { $_ -ne "py" } } -if ("ts" -in $Lang -and -not $hasNode) { - Write-Host " SKIP TypeScript (Node.js not installed)" -ForegroundColor DarkYellow - $Lang = $Lang | Where-Object { $_ -ne "ts" } +if (("ts" -in $Lang -or "com" -in $Lang) -and -not $hasNode) { + Write-Host " SKIP JavaScript E2E (Node.js not installed)" -ForegroundColor DarkYellow + $Lang = @($Lang | Where-Object { $_ -notin @("ts", "com") }) } + +function Find-Win32Winmd { + if ($env:DYNWINRT_WIN32_WINMD -and (Test-Path $env:DYNWINRT_WIN32_WINMD)) { + return (Resolve-Path $env:DYNWINRT_WIN32_WINMD).Path + } + + $packageRoot = Join-Path $env:USERPROFILE ".nuget\packages\microsoft.windows.sdk.win32metadata" + if (Test-Path $packageRoot) { + $candidate = Get-ChildItem $packageRoot -Filter Windows.Win32.winmd -File -Recurse | + Sort-Object LastWriteTime -Descending | + Select-Object -First 1 + if ($candidate) { return $candidate.FullName } + } + + $legacyPath = "C:\s\win32metadata\Windows.Win32.winmd" + if (Test-Path $legacyPath) { return $legacyPath } + return $null +} + +$win32Winmd = $null +if ("com" -in $Lang) { + $win32Winmd = Find-Win32Winmd + if (-not $win32Winmd) { + if ($langWasExplicit -or $env:DYNWINRT_REQUIRE_WIN32_METADATA -eq "1") { + Write-Error "Classic COM E2E requires Windows.Win32.winmd. Set DYNWINRT_WIN32_WINMD or install Microsoft.Windows.SDK.Win32Metadata." + exit 1 + } + Write-Host " SKIP Classic COM (Windows.Win32.winmd not found)" -ForegroundColor DarkYellow + $Lang = @($Lang | Where-Object { $_ -ne "com" }) + } else { + $env:DYNWINRT_WIN32_WINMD = $win32Winmd + Write-Host " Win32 metadata: $win32Winmd" + } +} + if ($Lang.Count -eq 0) { Write-Error "No languages available"; exit 1 } # -------------------------------------------------------------------------- @@ -71,12 +114,14 @@ if (-not $SkipBuild) { Pop-Location } - if ("ts" -in $Lang) { + if ("ts" -in $Lang -or "com" -in $Lang) { Push-Location (Join-Path $root "bindings\js") npm install --quiet 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { Write-Error "npm install failed"; exit 1 } npx napi build --no-const-enum --platform --release -o dist 2>&1 | Out-Null if ($LASTEXITCODE -ne 0) { Write-Error "napi build failed"; exit 1 } + npm run build:entrypoints --silent + if ($LASTEXITCODE -ne 0) { Write-Error "runtime entrypoint generation failed"; exit 1 } Pop-Location } } else { @@ -123,12 +168,57 @@ function Generate($lang, $outDir) { } } -foreach ($l in $Lang) { +foreach ($l in @($Lang | Where-Object { $_ -in @("py", "ts") })) { Write-Host "`n--- Generate ($l) ---" -ForegroundColor Yellow $outDir = if ($l -eq "py") { $pyBindingsDir } else { Join-Path $e2eDir $l } Generate $l $outDir } +if ("com" -in $Lang) { + Write-Host "`n--- Generate (Classic COM) ---" -ForegroundColor Yellow + $comRuntimeImport = "../../../../bindings/js/dist/com.js" + $winrtRuntimeImport = "../../../../bindings/js/dist/winrt.js" + + & cargo run -p dynwinrt-codegen --release --quiet -- generate ` + --winmd $win32Winmd ` + --namespace Windows.Win32.UI.Shell ` + --class-name "ITaskbarList3,IDataTransferManagerInterop,IShellLinkW,IFileOperation,IFileOpenDialog" ` + --output $comShellDir ` + --import-name $comRuntimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM Shell generation failed"; exit 1 } + + & cargo run -p dynwinrt-codegen --release --quiet -- generate ` + --winmd $win32Winmd ` + --namespace Windows.Win32.System.Com ` + --class-name IPersistFile ` + --output $comShellDir ` + --import-name $comRuntimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM persistence generation failed"; exit 1 } + + & cargo run -p dynwinrt-codegen --release --quiet -- generate ` + --winmd $win32Winmd ` + --namespace Windows.Win32.System.WinRT ` + --class-name ISystemMediaTransportControlsInterop ` + --output $comInteropDir ` + --import-name $comRuntimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM interop generation failed"; exit 1 } + + & cargo run -p dynwinrt-codegen --release --quiet -- generate ` + --winmd $win32Winmd ` + --namespace Windows.Win32.Graphics.Imaging ` + --class-name IWICImagingFactory ` + --output $comWicDir ` + --import-name $comRuntimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM WIC generation failed"; exit 1 } + + & cargo run -p dynwinrt-codegen --release --quiet -- generate ` + --namespace Windows.Media ` + --class-name SystemMediaTransportControls ` + --output $comSmtcDir ` + --import-name $winrtRuntimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "SMTC WinRT generation failed"; exit 1 } +} + # -------------------------------------------------------------------------- # Run language-specific runners # -------------------------------------------------------------------------- @@ -168,12 +258,44 @@ if ("ts" -in $Lang) { & $tsx (Join-Path $runnersDir "ts_runner.ts") ` --specs $specsFile ` --generated (Join-Path $e2eDir "ts") ` - --runtime (Join-Path $root "bindings\js\dist\index.js") ` + --runtime (Join-Path $root "bindings\js\dist\winrt.js") ` --output $tsResult if ($LASTEXITCODE -ne 0) { $totalFail++ } else { $totalPass++ } if (Test-Path $tsResult) { $allResults += (Get-Content $tsResult -Raw | ConvertFrom-Json) } } +if ("com" -in $Lang) { + Write-Host "`n--- Classic COM E2E ---" -ForegroundColor Yellow + $comRunners = @( + "pointer-reject-object.mjs", + "taskbarlist.mjs", + "electron-hwnd-buffer.mjs", + "shelllink-buffer.mjs", + "file-operation.mjs", + "file-open-dialog.mjs", + "wic-imaging-factory.mjs", + "dtm.mjs", + "smtc.mjs" + ) + $comPassed = 0 + $comFailed = 0 + foreach ($runner in $comRunners) { + Write-Host " $runner" + & node (Join-Path $runnersDir "com\$runner") + if ($LASTEXITCODE -eq 0) { + $comPassed++ + } else { + $comFailed++ + } + } + if ($comFailed -eq 0) { $totalPass++ } else { $totalFail++ } + $allResults += [pscustomobject]@{ + language = "com" + passed = $comPassed + total = $comRunners.Count + } +} + # -------------------------------------------------------------------------- # Summary # -------------------------------------------------------------------------- diff --git a/tests/runners/com/dtm.mjs b/tests/runners/com/dtm.mjs new file mode 100644 index 00000000..1c8d980b --- /dev/null +++ b/tests/runners/com/dtm.mjs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// E2E: real Node.js proof that IDataTransferManagerInterop returns a live +// WinRT object through the HWND interop pattern: +// IDataTransferManagerInterop::GetForWindow(HWND, REFIID, void**) +// Run: .\tests\e2e_test.ps1 -SkipBuild -Lang com + +import { DynCom, DynComMethodSig, WinGuid } from '../../../bindings/js/dist/com.js'; +import { IDataTransferManagerInterop } from '../../e2e_generated/com/shell/IDataTransferManagerInterop.js'; +import { acquireHwndBigInt } from './hwnd.mjs'; + +function fail(msg) { + console.error(`[e2e] FAIL: ${msg}`); + process.exit(1); +} + +console.log('[e2e] step 1: acquiring a process-owned HWND via napi createTestHwnd()'); +// The classic-vertical does not bundle flat-Win32, so we obtain a +// process-owned HWND via a small napi helper (`createTestHwnd`) instead of +// `flatInvoke(user32!CreateWindowExW, ...)`. This keeps the E2E +// self-contained with respect to the classic vertical's surface area. +const hwndBig = acquireHwndBigInt(); +console.log(`[e2e] HWND → 0x${hwndBig.toString(16)}`); +if (hwndBig === 0n) fail('acquireHwndBigInt returned NULL'); + +console.log('[e2e] step 2: IDataTransferManagerInterop.getForWindow(hwnd)'); +let dtm; +try { + dtm = IDataTransferManagerInterop.create().getForWindow(hwndBig); +} catch (e) { + fail(`getForWindow threw: ${e && e.message ? e.message : e}`); +} + +if (dtm == null) fail('getForWindow returned null'); +console.log(`[e2e] got DataTransferManager instance = ${dtm}`); + +console.log('[e2e] step 3: MEANINGFUL — read live member `runtimeClassName` (via IInspectable::GetRuntimeClassName)'); +const inspectable = DynCom.registerIUnknownInterface( + 'IInspectable_e2e', + WinGuid.parse('af86e2e0-b12d-4c6a-9c5a-d7aa65101e90'), +) + .addMethod('GetIids', new DynComMethodSig().addOut(DynCom.pointerType()).addOut(DynCom.pointerType())) + .addMethod('GetRuntimeClassName', new DynComMethodSig().addOut(DynCom.hstringType())) + .addMethod('GetTrustLevel', new DynComMethodSig().addOut(DynCom.i32Type())); +let name; +try { + name = inspectable.method(4).getString(dtm); +} catch (e) { + fail(`GetRuntimeClassName threw: ${e && e.message ? e.message : e}`); +} +console.log(`[e2e] runtimeClassName = ${JSON.stringify(name)}`); + +const expected = 'Windows.ApplicationModel.DataTransfer.DataTransferManager'; +if (name !== expected) fail(`expected runtimeClassName='${expected}', got '${name}'`); + +console.log('PASS'); +process.exit(0); diff --git a/tests/runners/com/electron-hwnd-buffer.mjs b/tests/runners/com/electron-hwnd-buffer.mjs new file mode 100644 index 00000000..6c70f5c0 --- /dev/null +++ b/tests/runners/com/electron-hwnd-buffer.mjs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// Regression for Electron's BrowserWindow.getNativeWindowHandle() shape: +// generated HWND inputs accept an exact pointer-width Buffer and decode its +// contents as the handle value. Other Buffer-backed pointers retain address +// semantics. + +import { ITaskbarList3 } from '../../e2e_generated/com/shell/ITaskbarList3.js'; +import { TBPFLAG } from '../../e2e_generated/com/shell/TBPFLAG.js'; +import { acquireHwndBigInt } from './hwnd.mjs'; + +function fail(msg) { + console.error(`[e2e] FAIL: ${msg}`); + process.exit(1); +} + +console.log('[e2e] step 1: acquiring a process-owned HWND'); +const hwnd = acquireHwndBigInt(); +console.log(`[e2e] HWND → 0x${hwnd.toString(16)}`); + +console.log('[e2e] step 2: simulating Electron getNativeWindowHandle() Buffer'); +const pointerWidth = process.arch === 'ia32' ? 4 : 8; +const electronHandleBuffer = Buffer.alloc(pointerWidth); +if (pointerWidth === 8) { + electronHandleBuffer.writeBigUInt64LE(hwnd, 0); +} else { + electronHandleBuffer.writeUInt32LE(Number(hwnd), 0); +} + +console.log('[e2e] step 3: creating ITaskbarList3'); +let taskbar; +try { + taskbar = ITaskbarList3.create(); + taskbar.hrInit(); +} catch (e) { + fail(`ITaskbarList3 activation/HrInit threw: ${e && e.message ? e.message : e}`); +} + +console.log('[e2e] step 4: passing Electron-style HWND Buffer directly'); +try { + taskbar.setProgressState(electronHandleBuffer, TBPFLAG.TBPF_NORMAL); + taskbar.markFullscreenWindow(electronHandleBuffer, false); + taskbar.setProgressState(electronHandleBuffer, TBPFLAG.TBPF_NOPROGRESS); +} catch (e) { + fail(`Electron HWND Buffer pattern threw: ${e && e.message ? e.message : e}`); +} + +console.log('PASS'); +process.exit(0); diff --git a/tests/runners/com/file-open-dialog.mjs b/tests/runners/com/file-open-dialog.mjs new file mode 100644 index 00000000..7fdefde4 --- /dev/null +++ b/tests/runners/com/file-open-dialog.mjs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from 'node:assert/strict'; +import { DynCom } from '../../../bindings/js/dist/com.js'; +import { IFileOpenDialog } from '../../e2e_generated/com/shell/IFileOpenDialog.js'; + +DynCom.initialize(0); + +const dialog = IFileOpenDialog.create(); +const options = dialog.getOptions(); +dialog.setOptions(options); +assert.equal(dialog.getOptions(), options); +dialog._obj.release(); + +console.log('file-open-dialog ok'); diff --git a/tests/runners/com/file-operation.mjs b/tests/runners/com/file-operation.mjs new file mode 100644 index 00000000..09cd9297 --- /dev/null +++ b/tests/runners/com/file-operation.mjs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from 'node:assert/strict'; +import { DynCom } from '../../../bindings/js/dist/com.js'; +import { FILEOPERATION_FLAGS } from '../../e2e_generated/com/shell/FILEOPERATION_FLAGS.js'; +import { IFileOperation } from '../../e2e_generated/com/shell/IFileOperation.js'; + +DynCom.initialize(1); + +const operation = IFileOperation.create(); +const flags = + FILEOPERATION_FLAGS.FOF_NO_UI + + FILEOPERATION_FLAGS.FOFX_DONTDISPLAYLOCATIONS; + +assert.equal(flags, 2147485204); +operation.setOperationFlags(flags); +assert.equal(operation.getAnyOperationsAborted(), false); +operation._obj.release(); + +console.log('file-operation ok'); diff --git a/tests/runners/com/hwnd.mjs b/tests/runners/com/hwnd.mjs new file mode 100644 index 00000000..4afa0c1f --- /dev/null +++ b/tests/runners/com/hwnd.mjs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// Classic-COM/interop E2E helper to obtain a process-owned Win32 HWND +// without relying on flat-Win32 codegen. +// +// Interop APIs like `IDataTransferManagerInterop::GetForWindow` and +// `ISystemMediaTransportControlsInterop::GetForWindow` require an HWND +// that is OWNED BY THE CALLING PROCESS (they return E_ACCESSDENIED for +// desktop / shell / cross-process HWNDs). This helper delegates to the +// napi `createTestHwnd()` export, which creates a hidden `WS_POPUP` +// window in the Node process using the pre-registered `STATIC` class. + +import { DynCom } from '../../../bindings/js/dist/com.js'; +import { roInitialize } from '../../../bindings/js/dist/winrt.js'; + +roInitialize(1); + +/** + * Return a valid Win32 HWND owned by the current process, as a bigint. + * Throws if window creation fails. + */ +export function acquireHwndBigInt() { + const hwnd = DynCom.createTestHwnd(); + // napi BigInt → JS bigint. + const n = typeof hwnd === 'bigint' ? hwnd : BigInt(hwnd); + if (n === 0n) { + throw new Error('acquireHwndBigInt: createTestHwnd returned 0'); + } + return n; +} diff --git a/tests/runners/com/pointer-reject-object.mjs b/tests/runners/com/pointer-reject-object.mjs new file mode 100644 index 00000000..30be9322 --- /dev/null +++ b/tests/runners/com/pointer-reject-object.mjs @@ -0,0 +1,21 @@ +// Regression for memory-safety fix #2: DynCom.pointer() must REJECT +// DynWinRtValue inputs. Borrowing an owned COM object's raw pointer here would +// make it indistinguishable from an owned raw pointer to adoptComPointer(), +// which can double-release the original wrapper's COM object. +import { DynCom, WinGuid } from '../../../bindings/js/dist/com.js'; + +// iidPointer() returns a DynWinRtValue — a representative value input. +const someValue = DynCom.iidPointer(WinGuid.parse('a5caee9b-8708-49d1-8d36-67d25a8da00c')); + +let rejected = false; +try { + DynCom.pointer(someValue); +} catch (e) { + rejected = String(e).includes('not accepted'); +} + +if (!rejected) { + console.log('FAIL: DynCom.pointer() accepted a DynWinRtValue input (double-release hazard)'); + process.exit(1); +} +console.log('PASS: DynCom.pointer() rejects DynWinRtValue inputs'); diff --git a/tests/runners/com/shelllink-buffer.mjs b/tests/runners/com/shelllink-buffer.mjs new file mode 100644 index 00000000..87f882b4 --- /dev/null +++ b/tests/runners/com/shelllink-buffer.mjs @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict'; +import { DynCom } from '../../../bindings/js/dist/com.js'; +import { IShellLinkW, IID_IShellLinkW } from '../../e2e_generated/com/shell/IShellLinkW.js'; +import { IPersistFile, IID_IPersistFile } from '../../e2e_generated/com/shell/IPersistFile.js'; +import { SHOW_WINDOW_CMD } from '../../e2e_generated/com/shell/SHOW_WINDOW_CMD.js'; + +const CLSID_SHELL_LINK = '00021401-0000-0000-c000-000000000046'; +DynCom.initialize(1); + +function wide(text) { + return Buffer.from(`${text}\0`, 'utf16le'); +} + +const link = IShellLinkW._fromNative( + DynCom.coCreateInstance(CLSID_SHELL_LINK, IID_IShellLinkW), +); + +const expectedPath = 'C:\\Windows\\explorer.exe'; +link.setPath(wide(expectedPath)); +assert.equal(link.getPath(260, 0n, 0).toLowerCase(), expectedPath.toLowerCase()); +const pidl = link.getIDList(); +assert.equal(pidl.isNull(), false); +pidl.release(); + +const expectedDescription = 'dynwinrt shelllink buffer'; +link.setDescription(wide(expectedDescription)); +assert.equal(link.getDescription(), expectedDescription); + +// Proves the u16 arg-wrapper codegen fix: setHotkey takes a [in] u16 (WORD). +// Before the fix, codegen emitted the non-existent DynWinRtValue.u16Value(...) +// and this call threw a TypeError. It must now complete without throwing. +const expectedHotkey = 0x0341; // Ctrl+Alt+'A' +assert.doesNotThrow(() => link.setHotkey(expectedHotkey)); +assert.equal(link.getHotkey(), expectedHotkey); + +link.setShowCmd(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED); +assert.equal(link.getShowCmd(), SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED); + +const persist = IPersistFile._fromNative(link._obj.cast(IID_IPersistFile)); +assert.equal(persist.getClassID().toLowerCase(), CLSID_SHELL_LINK); +persist._obj.release(); +link._obj.release(); + +console.log('shelllink-buffer ok'); diff --git a/tests/runners/com/smtc.mjs b/tests/runners/com/smtc.mjs new file mode 100644 index 00000000..3c95e8cf --- /dev/null +++ b/tests/runners/com/smtc.mjs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// E2E: real Node.js proof that the generated natural SystemMediaTransportControls +// wrapper drives live WinRT via the IInspectable-rooted (+6) *Interop* HWND +// pattern: +// ISystemMediaTransportControlsInterop::GetForWindow(HWND, REFIID, void**) +// The test uses ONLY high-level generated wrappers — no low-level +// `registerInterface` / `coCreateInstance` / QI plumbing in the test. +// +// This is the companion of dtm.mjs. DTM proves the IUnknown-rooted (+3) +// interop; this proves the IInspectable-rooted (+6) interop AND exercises a +// real SMTC member (isPlayEnabled) to prove the returned object is a live, +// usable SystemMediaTransportControls, not just a valid IInspectable pointer. +// +// Run: .\tests\e2e_test.ps1 -SkipBuild -Lang com + +import { DynCom, DynComMethodSig, WinGuid } from '../../../bindings/js/dist/com.js'; +// Classic-COM interop wrapper: gets the SMTC pointer from an HWND. +import { ISystemMediaTransportControlsInterop } from '../../e2e_generated/com/interop/ISystemMediaTransportControlsInterop.js'; +import { acquireHwndBigInt } from './hwnd.mjs'; + +// Full WinRT natural projection generated by the unified E2E orchestrator. +const { SystemMediaTransportControls: SmtcProjected } = + await import('../../e2e_generated/com/smtc/SystemMediaTransportControls.js'); +const { MediaPlaybackStatus } = + await import('../../e2e_generated/com/smtc/MediaPlaybackStatus.js'); + +function fail(msg) { + console.error(`[e2e] FAIL: ${msg}`); + process.exit(1); +} + +// SMTC (unlike DTM) is documented as requiring a real top-level window that +// owns a media session. The classic-vertical `createTestHwnd()` helper +// creates a hidden `WS_POPUP` window owned by this process; that has been +// sufficient for interop dispatch on tested Windows builds. + +console.log('[e2e] step 1: acquiring a process-owned HWND via napi createTestHwnd()'); +const hwndBig = acquireHwndBigInt(); +console.log(`[e2e] HWND → 0x${hwndBig.toString(16)}`); +if (hwndBig === 0n) fail('acquireHwndBigInt returned NULL'); + +console.log('[e2e] step 2: ISystemMediaTransportControlsInterop.getForWindow(hwnd) [HIGH-LEVEL WRAPPER, IInspectable-rooted +6]'); +let smtcRaw; +try { + const interop = ISystemMediaTransportControlsInterop.create(); + smtcRaw = interop.getForWindow(hwndBig); +} catch (e) { + fail(`ISystemMediaTransportControlsInterop.getForWindow threw: ${e && e.message ? e.message : e}`); +} +if (smtcRaw == null) fail('getForWindow returned null'); +console.log(`[e2e] got SystemMediaTransportControls pointer = ${smtcRaw}`); + +console.log('[e2e] step 3: prove liveness via IInspectable::GetRuntimeClassName'); +const inspectable = DynCom.registerIUnknownInterface( + 'IInspectable_smtc_e2e', + WinGuid.parse('af86e2e0-b12d-4c6a-9c5a-d7aa65101e90'), +) + .addMethod('GetIids', new DynComMethodSig().addOut(DynCom.pointerType()).addOut(DynCom.pointerType())) + .addMethod('GetRuntimeClassName', new DynComMethodSig().addOut(DynCom.hstringType())) + .addMethod('GetTrustLevel', new DynComMethodSig().addOut(DynCom.i32Type())); +let name; +try { + name = inspectable.method(4).getString(smtcRaw); +} catch (e) { + fail(`GetRuntimeClassName threw: ${e && e.message ? e.message : e}`); +} +console.log(`[e2e] runtimeClassName = ${JSON.stringify(name)}`); +const expected = 'Windows.Media.SystemMediaTransportControls'; +if (name !== expected) fail(`expected runtimeClassName='${expected}', got '${name}'`); + +console.log('[e2e] step 4: MEANINGFUL — exercise real SMTC members through the natural WinRT wrapper'); +// Re-wrap the SAME native pointer with the full WinRT projection. +// This is still 100% "generated wrapper" code — no manual registerInterface. +const smtc = SmtcProjected._fromNative(smtcRaw); + +// (a) round-trip a boolean property. +console.log('[e2e] set isPlayEnabled = true'); +try { + smtc.isPlayEnabled = true; +} catch (e) { + fail(`smtc.isPlayEnabled = true threw: ${e && e.message ? e.message : e}`); +} +let readBack; +try { + readBack = smtc.isPlayEnabled; +} catch (e) { + fail(`get smtc.isPlayEnabled threw: ${e && e.message ? e.message : e}`); +} +console.log(`[e2e] get isPlayEnabled → ${readBack}`); +if (readBack !== true) fail(`isPlayEnabled round-trip: expected true, got ${readBack}`); + +// Flip it back to prove get/set really goes to the COM object. +console.log('[e2e] set isPlayEnabled = false'); +smtc.isPlayEnabled = false; +const readBack2 = smtc.isPlayEnabled; +console.log(`[e2e] get isPlayEnabled → ${readBack2}`); +if (readBack2 !== false) fail(`isPlayEnabled round-trip #2: expected false, got ${readBack2}`); + +// (b) round-trip an enum property. +console.log('[e2e] set playbackStatus = MediaPlaybackStatus.Playing (3)'); +try { + smtc.playbackStatus = MediaPlaybackStatus.Playing; +} catch (e) { + fail(`set playbackStatus threw: ${e && e.message ? e.message : e}`); +} +let status; +try { + status = smtc.playbackStatus; +} catch (e) { + fail(`get playbackStatus threw: ${e && e.message ? e.message : e}`); +} +console.log(`[e2e] get playbackStatus → ${status} (${status === MediaPlaybackStatus.Playing ? 'Playing' : 'other'})`); +if (status !== MediaPlaybackStatus.Playing) fail(`playbackStatus round-trip: expected ${MediaPlaybackStatus.Playing} (Playing), got ${status}`); + +// (c) reach a nested COM object. +console.log('[e2e] get displayUpdater (IInspectable child object)'); +let updater; +try { + updater = smtc.displayUpdater; +} catch (e) { + fail(`get displayUpdater threw: ${e && e.message ? e.message : e}`); +} +if (updater == null || !updater._obj) fail('displayUpdater returned null / no _obj'); +console.log(`[e2e] displayUpdater is a live SystemMediaTransportControlsDisplayUpdater`); + +console.log('PASS'); +process.exit(0); diff --git a/tests/runners/com/taskbarlist.mjs b/tests/runners/com/taskbarlist.mjs new file mode 100644 index 00000000..d5459b53 --- /dev/null +++ b/tests/runners/com/taskbarlist.mjs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +// +// Phase 2 E2E: real Node.js proof that the generated natural ITaskbarList3 +// wrapper drives live Windows classic COM (ITaskbarList3) via CoCreateInstance. +// +// Run: .\tests\e2e_test.ps1 -SkipBuild -Lang com + +import { ITaskbarList3 } from '../../e2e_generated/com/shell/ITaskbarList3.js'; +import { TBPFLAG } from '../../e2e_generated/com/shell/TBPFLAG.js'; +import { acquireHwndBigInt } from './hwnd.mjs'; + +function fail(msg) { + console.error(`[e2e] FAIL: ${msg}`); + process.exit(1); +} + +console.log('[e2e] step 1: acquiring a process-owned HWND via napi createTestHwnd()'); +// The classic-vertical does not bundle flat-Win32, so we obtain a +// process-owned HWND via a small napi helper (`createTestHwnd`) instead of +// `flatInvoke(user32!CreateWindowExW, ...)`. This keeps the E2E +// self-contained with respect to the classic vertical's surface area. +const hwndBig = acquireHwndBigInt(); +console.log(`[e2e] HWND → 0x${hwndBig.toString(16)}`); +if (hwndBig === 0n) fail('acquireHwndBigInt returned NULL'); + +console.log('[e2e] step 2: CoCreateInstance(CLSID_TaskbarList, IID_ITaskbarList3)'); + +let t; +try { + t = ITaskbarList3.create(); +} catch (e) { + fail(`ITaskbarList3.create() threw: ${e && e.message ? e.message : e}`); +} +console.log(`[e2e] ITaskbarList3 = ${t}`); + +console.log('[e2e] step 3: HrInit() (vtable slot 3)'); +try { + t.hrInit(); +} catch (e) { + fail(`HrInit() threw: ${e && e.message ? e.message : e}`); +} + +// ITaskbarList3 accepts arbitrary HWNDs — SetProgressState / SetProgressValue on +// non-owned windows do not fail; they simply have no visible effect if the +// window is not a top-level shell window. What we need is: the call returns +// without an HRESULT error being thrown. + +console.log('[e2e] step 4: SetProgressState(hwnd, TBPF_NORMAL) (vtable slot 10)'); +try { + t.setProgressState(hwndBig, TBPFLAG.TBPF_NORMAL); +} catch (e) { + fail(`SetProgressState(TBPF_NORMAL) threw: ${e && e.message ? e.message : e}`); +} + +console.log('[e2e] step 5: SetProgressValue(hwnd, 30n, 100n) (vtable slot 9, u64 args)'); +try { + t.setProgressValue(hwndBig, 30n, 100n); +} catch (e) { + fail(`SetProgressValue(30, 100) threw: ${e && e.message ? e.message : e}`); +} + +console.log('[e2e] step 6: SetProgressState(hwnd, TBPF_NOPROGRESS)'); +try { + t.setProgressState(hwndBig, TBPFLAG.TBPF_NOPROGRESS); +} catch (e) { + fail(`SetProgressState(TBPF_NOPROGRESS) threw: ${e && e.message ? e.message : e}`); +} + +// Prove the BOOL → i32 codegen fix: markFullscreenWindow historically emitted +// `DynCom.pointer(fFullscreen)` and typed `fFullscreen: BOOL = bigint | Buffer`, +// so passing a plain `false` threw at napi. After the fix, BOOL projects as an +// i32 with a `boolean` surface, and this natural-JS call round-trips. +console.log('[e2e] step 7: MarkFullscreenWindow(hwnd, false) — proves BOOL→i32 codegen fix'); +try { + t.markFullscreenWindow(hwndBig, false); +} catch (e) { + fail(`MarkFullscreenWindow(hwnd, false) threw: ${e && e.message ? e.message : e}`); +} + +console.log('PASS'); +process.exit(0); diff --git a/tests/runners/com/wic-imaging-factory.mjs b/tests/runners/com/wic-imaging-factory.mjs new file mode 100644 index 00000000..4270b881 --- /dev/null +++ b/tests/runners/com/wic-imaging-factory.mjs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from 'node:assert/strict'; +import { DynCom } from '../../../bindings/js/dist/com.js'; +import { + IID_IWICImagingFactory, + IWICImagingFactory, +} from '../../e2e_generated/com/wic/IWICImagingFactory.js'; + +const CLSID_WIC_IMAGING_FACTORY = 'cacaf262-9370-4615-a13b-9f5539da4c0a'; + +DynCom.initialize(1); + +const factory = IWICImagingFactory._fromNative( + DynCom.coCreateInstance(CLSID_WIC_IMAGING_FACTORY, IID_IWICImagingFactory), +); +const stream = factory.createStream(); +assert.equal(stream.isNull(), false); +stream.release(); +factory._obj.release(); + +console.log('wic-imaging-factory ok'); diff --git a/tests/runners/ts_runner.ts b/tests/runners/ts_runner.ts index 37772f37..7ec1bd2e 100644 --- a/tests/runners/ts_runner.ts +++ b/tests/runners/ts_runner.ts @@ -8,7 +8,7 @@ * and executes checks against real WinRT APIs. * * Usage: - * npx tsx tests/runners/ts_runner.ts --specs tests/e2e_specs.json --generated tests/e2e_generated/ts --runtime bindings/js/dist/index.js [--output results.json] + * npx tsx tests/runners/ts_runner.ts --specs tests/e2e_specs.json --generated tests/e2e_generated/ts --runtime bindings/js/dist/winrt.js [--output results.json] */ import { strict as assert } from 'node:assert'; diff --git a/tools/dynwinrt-codegen/E2E_TEST.md b/tools/dynwinrt-codegen/E2E_TEST.md index 22abc429..0b1e7877 100644 --- a/tools/dynwinrt-codegen/E2E_TEST.md +++ b/tools/dynwinrt-codegen/E2E_TEST.md @@ -29,7 +29,7 @@ cargo build -p dynwinrt-codegen --release # Build the JS native binding cd bindings/js -npx napi build --no-const-enum --platform --release -o dist +npm run build cd ../.. ``` diff --git a/tools/dynwinrt-codegen/src/codegen/com/ir.rs b/tools/dynwinrt-codegen/src/codegen/com/ir.rs new file mode 100644 index 00000000..cf8129c0 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/ir.rs @@ -0,0 +1,229 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Validated Classic-COM semantic IR. +//! +//! Nothing in this module depends on the shared WinRT metadata model. A value +//! can enter this IR only after its ABI shape, ownership, and projection have +//! been validated by `project`. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ComPrimitive { + Bool, + I8, + U8, + I16, + U16, + I32, + U32, + I64, + U64, + F32, + F64, + Char16, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ComEnumUnderlying { + I8, + U8, + I16, + U16, + I32, + U32, + I64, + U64, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ComScalarRepr { + Primitive(ComPrimitive), + NativeIsize, + NativeUsize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub(super) enum PointerAliasKind { + HandleValue, + DataPointer, + StringPointer, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum ComType { + Primitive(ComPrimitive), + NativeIsize, + NativeUsize, + Win32Bool, + HResult, + Guid, + HString, + Enum { + name: String, + underlying: ComEnumUnderlying, + }, + ScalarAlias { + name: String, + underlying: ComScalarRepr, + }, + RawPointer, + PointerAlias { + name: String, + kind: PointerAliasKind, + }, + Bstr, + ManagedInterface { + iid: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum UnsupportedComType { + Array, + ParameterizedInterface { namespace: String, name: String }, + AsyncInterface, + Delegate { namespace: String, name: String }, + NativeStructLayout { namespace: String, name: String }, + UnknownPointerAlias { namespace: String, name: String }, + UnresolvedInterface { namespace: String, name: String }, + UnresolvedRuntimeClass { namespace: String, name: String }, + UnknownOwnership { type_name: String }, + UnsupportedDirectReturn { type_name: String }, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ComParamDirection { + In, + Out, + InOut, + OutStringBuffer, +} + +impl ComParamDirection { + pub(super) fn is_input(self) -> bool { + matches!(self, Self::In | Self::InOut) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct ProjectedComParam { + pub(super) name: String, + pub(super) typ: ComType, + pub(super) direction: ComParamDirection, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum ComReturnConvention { + HResult, + SemanticHResult, + Void, + Direct(ComType), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum StringEncoding { + Wide, + Ansi, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum ResultConversion { + Value, + ManagedCom, + Bstr, + CoTaskMemString(StringEncoding), + CoTaskMemData, + HString, + DynamicIidAdoption, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum ResultSource { + DirectReturn, + Param(usize), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct ProjectedComResult { + pub(super) typ: ComType, + pub(super) source: ResultSource, + pub(super) conversion: ResultConversion, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct StringBufferPlan { + pub(super) buffer_param_index: usize, + pub(super) count_param_index: usize, + pub(super) encoding: StringEncoding, + pub(super) optional_param_indices: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum ProjectedComMethodKind { + Normal, + CallerSuppliedDynamicIid { + natural_param_count: usize, + }, + SynthesizedGetForWindow { + natural_param_count: usize, + target_iid: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct ProjectedComMethod { + pub(super) name: String, + pub(super) camel_name: String, + pub(super) vtable_index: usize, + pub(super) params: Vec, + pub(super) return_convention: ComReturnConvention, + pub(super) results: Vec, + pub(super) string_buffer: Option, + pub(super) kind: ProjectedComMethodKind, + pub(super) doc: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum ActivationPlan { + None, + Coclass { + clsid: String, + coclass_name: String, + }, + WinRtFactory { + class_name: String, + class_namespace: String, + target_iid: String, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum ProjectedEnumValue { + Signed(i64), + Unsigned(u64), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct ProjectedComEnumMember { + pub(super) name: String, + pub(super) value: ProjectedEnumValue, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct ProjectedComEnum { + pub(super) name: String, + pub(super) underlying: ComEnumUnderlying, + pub(super) members: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) struct ProjectedComInterface { + pub(super) name: String, + pub(super) namespace: String, + pub(super) iid: String, + pub(super) is_iunknown_rooted: bool, + pub(super) methods: Vec, + pub(super) activation: ActivationPlan, + pub(super) referenced_enums: Vec, +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/javascript/mod.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/mod.rs new file mode 100644 index 00000000..20afccfd --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/mod.rs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub(super) mod naming; +pub(super) mod render; +mod types; diff --git a/tools/dynwinrt-codegen/src/codegen/com/javascript/naming.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/naming.rs new file mode 100644 index 00000000..4ab4c002 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/naming.rs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub(in crate::codegen::com) fn camel_case(name: &str) -> String { + if name.is_empty() { + return String::new(); + } + let chars: Vec = name.chars().collect(); + let mut run = 0usize; + while run < chars.len() && chars[run].is_ascii_uppercase() { + run += 1; + } + let mut result = String::with_capacity(name.len()); + if run == 0 { + return name.to_string(); + } + if run == chars.len() { + for c in &chars { + result.push(c.to_ascii_lowercase()); + } + return result; + } + if run == 1 { + result.push(chars[0].to_ascii_lowercase()); + for c in &chars[1..] { + result.push(*c); + } + return result; + } + for c in &chars[..run - 1] { + result.push(c.to_ascii_lowercase()); + } + for c in &chars[run - 1..] { + result.push(*c); + } + result +} + +pub(super) fn js_param_name(raw: &str, index: usize) -> String { + let base = if raw.is_empty() { + format!("arg{}", index) + } else { + raw.to_string() + }; + let stripped = strip_hungarian(&base); + let mut out = String::with_capacity(stripped.len()); + let mut chars = stripped.chars(); + if let Some(first) = chars.next() { + out.push(first.to_ascii_lowercase()); + } + for c in chars { + out.push(c); + } + match out.as_str() { + "class" | "return" | "function" | "default" | "this" | "new" | "delete" | "let" + | "const" | "var" | "if" | "else" | "for" | "while" | "do" | "switch" | "case" + | "break" | "continue" | "true" | "false" | "null" | "undefined" | "in" | "of" + | "typeof" | "instanceof" | "throw" | "try" | "catch" | "finally" | "yield" | "async" + | "await" | "with" | "void" | "public" | "private" | "protected" | "package" | "static" + | "import" | "export" | "extends" | "super" | "arguments" => { + format!("{}_", out) + } + _ => out, + } +} + +pub(super) fn strip_hungarian(s: &str) -> &str { + let prefixes = [ + "lpwsz", "pwsz", "lpsz", "psz", "lpsz", "pwstr", "pcwstr", "hwnd", "dw", "sz", "cb", "cx", + "cy", "cw", "ch", "cn", "cc", "lp", "np", "ph", "pd", "pf", "pv", "ppv", "pp", "wsz", + ]; + for p in prefixes { + if let Some(rest) = s.strip_prefix(p) { + if rest + .chars() + .next() + .map(|c| c.is_ascii_uppercase()) + .unwrap_or(false) + { + return rest; + } + } + } + s +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs new file mode 100644 index 00000000..be9dc88b --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs @@ -0,0 +1,717 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Pure JavaScript and declaration rendering for validated Classic-COM IR. + +use std::collections::BTreeMap; + +#[cfg(test)] +use super::super::ir::ProjectedComEnumMember; +use super::super::ir::{ + ActivationPlan, ComEnumUnderlying, ComParamDirection, ComReturnConvention, ComType, + PointerAliasKind, ProjectedComEnum, ProjectedComInterface, ProjectedComMethod, + ProjectedComMethodKind, ProjectedComParam, ProjectedEnumValue, ResultConversion, + StringEncoding, +}; +use super::naming::js_param_name; +use super::types::{ + abi_type_js, input_type_dts, result_type_dts, scalar_type_dts, unwrap_result_js, wrap_arg_js, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ComGeneratedOutput { + pub js: String, + pub dts: String, + pub extra_files: Vec<(String, String)>, +} + +pub(in crate::codegen::com) fn render_com_interface( + meta: &ProjectedComInterface, +) -> ComGeneratedOutput { + let js = render_js(meta); + let dts = render_dts(meta); + let mut extra_files = Vec::new(); + for en in &meta.referenced_enums { + let (enum_js, enum_dts) = render_enum_files(en); + extra_files.push((format!("{}.js", en.name), enum_js)); + extra_files.push((format!("{}.d.ts", en.name), enum_dts)); + } + extra_files.sort_by(|a, b| a.0.cmp(&b.0)); + ComGeneratedOutput { + js, + dts, + extra_files, + } +} + +fn render_js(meta: &ProjectedComInterface) -> String { + let mut out = String::new(); + out.push_str("// Generated by dynwinrt-codegen — do not edit\n"); + let runtime_imports = if matches!(meta.activation, ActivationPlan::WinRtFactory { .. }) { + "DynCom, DynComMethodSig, DynWinRtValue, WinGuid" + } else { + "DynCom, DynComMethodSig, WinGuid" + }; + out.push_str(&format!( + "import {{ {runtime_imports} }} from '{}';\n", + com_runtime_import_name() + )); + for en in &meta.referenced_enums { + out.push_str(&format!( + "import {{ {} }} from './{}.js';\n", + en.name, en.name + )); + } + out.push('\n'); + if meta + .methods + .iter() + .any(|method| method.string_buffer.is_some()) + { + out.push_str("function _normalizeStringBufferCount(value, name) {\n"); + out.push_str(" if (!Number.isInteger(value) || value <= 0) throw new TypeError(`${name} must be a positive integer`);\n"); + out.push_str(" return value;\n}\n"); + out.push_str("function _decodeWideString(buffer) {\n let end = 0;\n"); + out.push_str( + " while (end + 1 < buffer.length && buffer.readUInt16LE(end) !== 0) end += 2;\n", + ); + out.push_str(" return buffer.subarray(0, end).toString('utf16le');\n}\n\n"); + } + out.push_str(&format!( + "export const IID_{} = WinGuid.parse('{}');\n", + meta.name, meta.iid + )); + if let ActivationPlan::WinRtFactory { + class_name, + target_iid, + .. + } = &meta.activation + { + out.push_str(&format!( + "const IID_{class_name}_default = WinGuid.parse('{target_iid}');\n" + )); + } + out.push('\n'); + let register_fn = if meta.is_iunknown_rooted { + "registerIUnknownInterface" + } else { + "registerIInspectableInterface" + }; + let cache_var = format!("_{}Cache", meta.name); + let iface_var = format!("_{}", meta.name); + out.push_str(&format!("let {cache_var};\n")); + out.push_str(&format!( + "const {iface_var} = new Proxy({{}}, {{\n get(_target, prop) {{\n {cache_var} ??= DynCom.{register_fn}('{}.{}', IID_{})\n", + meta.namespace, meta.name, meta.name + )); + for method in &meta.methods { + out.push_str(&format!( + " .addMethod('{}', {})\n", + method.name, + build_method_sig_js(method) + )); + } + if out.ends_with('\n') { + out.pop(); + } + out.push_str(";\n"); + out.push_str(&format!(" const value = {cache_var}[prop];\n return typeof value === 'function' ? value.bind({cache_var}) : value;\n }},\n}});\n\n")); + out.push_str(&format!("export class {} {{\n", meta.name)); + out.push_str(" _obj;\n constructor(obj) { this._obj = obj; }\n"); + out.push_str(&format!( + " static _fromNative(obj) {{ return new {}(obj); }}\n", + meta.name + )); + match &meta.activation { + ActivationPlan::None => {} + ActivationPlan::Coclass { + clsid, + coclass_name, + } => { + out.push_str(&format!( + " /** Create a new `{}` via `CoCreateInstance` on `CLSID_{coclass_name}`. */\n", + meta.name + )); + out.push_str(&format!(" static create() {{\n const _obj = DynCom.coCreateInstance('{clsid}', IID_{});\n return new {}(_obj);\n }}\n", meta.name, meta.name)); + } + ActivationPlan::WinRtFactory { + class_name, + class_namespace, + .. + } => { + let full = format!("{class_namespace}.{class_name}"); + out.push_str(&format!(" /** Create a new `{}` by activating the `{full}` factory and QI'ing to the interop. */\n", meta.name)); + out.push_str(&format!(" static create() {{\n const factory = DynWinRtValue.activationFactory('{full}');\n const _obj = factory.cast(IID_{});\n return new {}(_obj);\n }}\n", meta.name, meta.name)); + } + } + for method in &meta.methods { + match method.kind { + ProjectedComMethodKind::Normal => emit_method_js(&mut out, method, &iface_var), + ProjectedComMethodKind::CallerSuppliedDynamicIid { + natural_param_count, + } => emit_dynamic_iid_method_js(&mut out, method, natural_param_count, &iface_var), + ProjectedComMethodKind::SynthesizedGetForWindow { + natural_param_count, + ref target_iid, + } => emit_synthesized_interop_method_js( + &mut out, + method, + natural_param_count, + target_iid, + &iface_var, + meta, + ), + } + } + out.push_str("}\n"); + out +} + +fn build_method_sig_js(method: &ProjectedComMethod) -> String { + let mut parts = Vec::new(); + for (index, param) in method.params.iter().enumerate() { + match param.direction { + ComParamDirection::In => parts.push(format!(".addIn({})", abi_type_js(¶m.typ))), + ComParamDirection::InOut => { + parts.push(format!(".addInOut({})", abi_type_js(¶m.typ))) + } + ComParamDirection::OutStringBuffer => parts.push(".addIn(DynCom.pointerType())".into()), + ComParamDirection::Out => { + if method.string_buffer.as_ref().is_some_and(|plan| { + index > plan.count_param_index && plan.optional_param_indices.contains(&index) + }) { + parts.push(".addIn(DynCom.pointerType())".into()); + } else { + parts.push(format!(".addOut({})", abi_type_js(¶m.typ))); + } + } + } + } + match &method.return_convention { + ComReturnConvention::HResult => {} + ComReturnConvention::SemanticHResult => parts.push(".preserveHresult()".into()), + ComReturnConvention::Void => parts.push(".returnsVoid()".into()), + ComReturnConvention::Direct(typ) => parts.push(format!(".returns({})", abi_type_js(typ))), + } + if parts.is_empty() { + "new DynComMethodSig()".into() + } else { + format!("new DynComMethodSig(){}", parts.join("")) + } +} + +fn input_params(method: &ProjectedComMethod) -> Vec<(usize, &ProjectedComParam)> { + method + .params + .iter() + .enumerate() + .filter(|(_, param)| param.direction.is_input()) + .collect() +} + +fn emit_method_js(out: &mut String, method: &ProjectedComMethod, iface_var: &str) { + let inputs = input_params(method); + let params = inputs + .iter() + .enumerate() + .map(|(surface, (index, param))| { + let name = js_param_name(¶m.name, surface); + if let Some(plan) = &method.string_buffer { + if plan.optional_param_indices.contains(index) { + return if *index == plan.count_param_index { + format!("{name} = 260") + } else { + format!("{name} = 0") + }; + } + } + name + }) + .collect::>(); + out.push_str(&format!( + " {}({}) {{\n", + method.camel_name, + params.join(", ") + )); + if let Some(plan) = &method.string_buffer { + emit_string_buffer_method_body(out, method, plan, &inputs, iface_var); + out.push_str(" }\n"); + return; + } + let args = inputs + .iter() + .enumerate() + .map(|(surface, (_, param))| wrap_arg_js(¶m.typ, &js_param_name(¶m.name, surface))) + .collect::>(); + emit_invocation_and_results(out, method, iface_var, &args); + out.push_str(" }\n"); +} + +fn emit_string_buffer_method_body( + out: &mut String, + method: &ProjectedComMethod, + plan: &super::super::ir::StringBufferPlan, + inputs: &[(usize, &ProjectedComParam)], + iface_var: &str, +) { + let count_surface = inputs + .iter() + .position(|(index, _)| *index == plan.count_param_index) + .expect("validated count input"); + let count_name = js_param_name(&method.params[plan.count_param_index].name, count_surface); + if plan.encoding == StringEncoding::Ansi { + out.push_str(" throw new Error('PSTR out buffers are not yet decoded safely');\n"); + return; + } + let args = method + .params + .iter() + .enumerate() + .filter_map(|(index, param)| { + if index == plan.buffer_param_index { + Some("DynCom.pointer(_buffer)".into()) + } else if param.direction.is_input() { + let surface = inputs + .iter() + .position(|(input_index, _)| *input_index == index) + .expect("validated input"); + Some(wrap_arg_js( + ¶m.typ, + &js_param_name(¶m.name, surface), + )) + } else if index > plan.count_param_index && plan.optional_param_indices.contains(&index) + { + Some("DynCom.pointer(0n)".into()) + } else { + None + } + }) + .collect::>(); + out.push_str(&format!( + " {count_name} = _normalizeStringBufferCount({count_name}, '{count_name}');\n" + )); + out.push_str(&format!( + " const _buffer = Buffer.alloc({count_name} * 2);\n" + )); + match method.results.len() { + 0 => out.push_str(&format!( + " {iface_var}.method({}).invoke(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )), + 1 => out.push_str(&format!( + " const _out = {iface_var}.method({}).invoke(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )), + _ => out.push_str(&format!( + " const _out = {iface_var}.method({}).invokeAll(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )), + } + out.push_str(" const _text = _decodeWideString(_buffer);\n"); + match method.results.len() { + 0 => out.push_str(" return _text;\n"), + 1 => out.push_str(&format!( + " return [_text, {}];\n", + unwrap_result_js(&method.results[0], "_out") + )), + _ => { + let values = method + .results + .iter() + .enumerate() + .map(|(index, result)| unwrap_result_js(result, &format!("_out[{index}]"))) + .collect::>(); + out.push_str(&format!(" return [_text, {}];\n", values.join(", "))); + } + } +} + +fn emit_invocation_and_results( + out: &mut String, + method: &ProjectedComMethod, + iface_var: &str, + args: &[String], +) { + match method.results.len() { + 0 => out.push_str(&format!( + " {iface_var}.method({}).invoke(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )), + 1 => { + out.push_str(&format!( + " const _out = {iface_var}.method({}).invoke(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )); + out.push_str(&format!( + " return {};\n", + unwrap_result_js(&method.results[0], "_out") + )); + } + _ => { + out.push_str(&format!( + " const _r = {iface_var}.method({}).invokeAll(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )); + let values = method + .results + .iter() + .enumerate() + .map(|(index, result)| unwrap_result_js(result, &format!("_r[{index}]"))) + .collect::>(); + out.push_str(&format!(" return [{}];\n", values.join(", "))); + } + } +} + +fn emit_dynamic_iid_method_js( + out: &mut String, + method: &ProjectedComMethod, + natural_count: usize, + iface_var: &str, +) { + let natural = &method.params[..natural_count]; + let mut surface = natural + .iter() + .enumerate() + .map(|(index, param)| js_param_name(¶m.name, index)) + .collect::>(); + surface.push("iid".into()); + let mut args = natural + .iter() + .enumerate() + .map(|(index, param)| wrap_arg_js(¶m.typ, &js_param_name(¶m.name, index))) + .collect::>(); + args.push("DynCom.iidPointer(_iid)".into()); + out.push_str(&format!( + " {}({}) {{\n const _iid = WinGuid.parse(iid);\n", + method.camel_name, + surface.join(", ") + )); + out.push_str(&format!( + " const _raw = {iface_var}.method({}).invoke(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )); + out.push_str(" return DynCom.adoptComPointer(_raw, _iid);\n }\n"); +} + +fn emit_synthesized_interop_method_js( + out: &mut String, + method: &ProjectedComMethod, + natural_count: usize, + _target_iid: &str, + iface_var: &str, + meta: &ProjectedComInterface, +) { + let natural = &method.params[..natural_count]; + let params = natural + .iter() + .enumerate() + .map(|(index, param)| js_param_name(¶m.name, index)) + .collect::>(); + let mut args = natural + .iter() + .enumerate() + .map(|(index, param)| wrap_arg_js(¶m.typ, &js_param_name(¶m.name, index))) + .collect::>(); + let class_name = match &meta.activation { + ActivationPlan::WinRtFactory { class_name, .. } => class_name, + _ => unreachable!("validated interop activation"), + }; + args.push(format!("DynCom.iidPointer(IID_{class_name}_default)")); + out.push_str(&format!( + " {}({}) {{\n", + method.camel_name, + params.join(", ") + )); + out.push_str(&format!( + " const _raw = {iface_var}.method({}).invoke(this._obj, [{}]);\n", + method.vtable_index, + args.join(", ") + )); + out.push_str(&format!(" const _out = DynCom.adoptComPointer(_raw, IID_{class_name}_default);\n return _out;\n }}\n")); +} + +fn render_dts(meta: &ProjectedComInterface) -> String { + let mut out = String::new(); + out.push_str("// Generated by dynwinrt-codegen — do not edit\n"); + for en in &meta.referenced_enums { + out.push_str(&format!( + "import {{ {} }} from './{}.js';\n", + en.name, en.name + )); + } + if needs_bridge_import(meta) { + out.push_str(&format!( + "import type {{ DynWinRtValue }} from '{}';\n", + com_runtime_import_name() + )); + } + out.push('\n'); + for (name, underlying) in collect_scalar_aliases(meta) { + out.push_str(&format!( + "/** Transparent Win32 scalar typedef. */\nexport type {name} = {};\n", + scalar_type_dts(underlying) + )); + } + if !collect_scalar_aliases(meta).is_empty() { + out.push('\n'); + } + for (name, kind) in collect_pointer_aliases(meta) { + match kind { + PointerAliasKind::HandleValue => out.push_str(&format!("/** Opaque Win32 handle value. Pass a raw pointer value as a `bigint` (full pointer width) or `number` (safe integer). */\nexport type {name} = bigint | number;\n")), + PointerAliasKind::DataPointer => out.push_str(&format!("/** Opaque native data address. Inputs may also use a `Buffer`/`Uint8Array`, whose backing-store address is passed and retained for the call. */\nexport type {name} = bigint | number;\n")), + PointerAliasKind::StringPointer => out.push_str(&format!("/** Win32 NUL-terminated string pointer. Pass a `Buffer` holding the string bytes (including the NUL terminator), or pass a raw pointer as `bigint`. */\nexport type {name} = bigint | Buffer;\n")), + } + } + if !collect_pointer_aliases(meta).is_empty() { + out.push('\n'); + } + out.push_str(&format!( + "export declare const IID_{}: unknown;\n\nexport declare class {} {{\n", + meta.name, meta.name + )); + match &meta.activation { + ActivationPlan::None => {} + ActivationPlan::Coclass { .. } => out.push_str(&format!( + " /** Create a new instance via the coclass activation path. */\n static create(): {};\n", + meta.name + )), + ActivationPlan::WinRtFactory { .. } => out.push_str(&format!( + " /** Activate the projected WinRT class and QI to the interop. */\n static create(): {};\n", + meta.name + )), + } + out.push_str(&format!(" /** Wrap an existing native COM pointer (for QueryInterface bridging). */\n static _fromNative(obj: unknown): {};\n", meta.name)); + for method in &meta.methods { + let (params, ret) = match method.kind { + ProjectedComMethodKind::Normal => (dts_params(method), dts_return_type(method)), + ProjectedComMethodKind::CallerSuppliedDynamicIid { + natural_param_count, + } => { + let mut params = dts_natural_params(method, natural_param_count); + params.push("iid: string".into()); + (params, "DynWinRtValue".into()) + } + ProjectedComMethodKind::SynthesizedGetForWindow { + natural_param_count, + .. + } => ( + dts_natural_params(method, natural_param_count), + "DynWinRtValue".into(), + ), + }; + out.push_str(&format!( + " {}({}): {};\n", + method.camel_name, + params.join(", "), + ret + )); + } + out.push_str("}\n"); + out +} + +fn dts_natural_params(method: &ProjectedComMethod, count: usize) -> Vec { + method.params[..count] + .iter() + .enumerate() + .map(|(index, param)| { + format!( + "{}: {}", + js_param_name(¶m.name, index), + input_type_dts(¶m.typ) + ) + }) + .collect() +} + +fn dts_params(method: &ProjectedComMethod) -> Vec { + method + .params + .iter() + .enumerate() + .filter(|(_, param)| param.direction.is_input()) + .enumerate() + .map(|(surface, (index, param))| { + let mut name = js_param_name(¶m.name, surface); + if method + .string_buffer + .as_ref() + .is_some_and(|plan| plan.optional_param_indices.contains(&index)) + { + name.push('?'); + } + format!("{name}: {}", input_type_dts(¶m.typ)) + }) + .collect() +} + +fn dts_return_type(method: &ProjectedComMethod) -> String { + if method.string_buffer.is_some() { + return if method.results.is_empty() { + "string".into() + } else { + format!( + "[string, {}]", + method + .results + .iter() + .map(result_type_dts) + .collect::>() + .join(", ") + ) + }; + } + match method.results.len() { + 0 => "void".into(), + 1 => result_type_dts(&method.results[0]), + _ => format!( + "[{}]", + method + .results + .iter() + .map(result_type_dts) + .collect::>() + .join(", ") + ), + } +} + +fn collect_pointer_aliases(meta: &ProjectedComInterface) -> Vec<(String, PointerAliasKind)> { + let mut aliases = BTreeMap::new(); + for method in &meta.methods { + for typ in + method + .params + .iter() + .map(|param| ¶m.typ) + .chain(match &method.return_convention { + ComReturnConvention::Direct(typ) => Some(typ), + _ => None, + }) + { + match typ { + ComType::PointerAlias { name, kind } => { + aliases.insert(name.clone(), *kind); + } + ComType::Bstr => { + aliases.insert("BSTR".into(), PointerAliasKind::HandleValue); + } + ComType::Primitive(_) + | ComType::NativeIsize + | ComType::NativeUsize + | ComType::Win32Bool + | ComType::HResult + | ComType::Guid + | ComType::HString + | ComType::Enum { .. } + | ComType::ScalarAlias { .. } + | ComType::RawPointer + | ComType::ManagedInterface { .. } => {} + } + } + } + aliases.into_iter().collect() +} + +fn collect_scalar_aliases( + meta: &ProjectedComInterface, +) -> Vec<(String, super::super::ir::ComScalarRepr)> { + let mut aliases = BTreeMap::new(); + for method in &meta.methods { + for typ in + method + .params + .iter() + .map(|param| ¶m.typ) + .chain(match &method.return_convention { + ComReturnConvention::Direct(typ) => Some(typ), + _ => None, + }) + { + if let ComType::ScalarAlias { name, underlying } = typ { + aliases.insert(name.clone(), *underlying); + } + } + } + aliases.into_iter().collect() +} + +fn needs_bridge_import(meta: &ProjectedComInterface) -> bool { + matches!(meta.activation, ActivationPlan::WinRtFactory { .. }) + || meta.methods.iter().any(|method| { + !matches!(method.kind, ProjectedComMethodKind::Normal) + || method + .params + .iter() + .any(|param| matches!(param.typ, ComType::ManagedInterface { .. })) + || method.results.iter().any(|result| { + matches!( + result.conversion, + ResultConversion::ManagedCom + | ResultConversion::CoTaskMemData + | ResultConversion::DynamicIidAdoption + ) + }) + }) +} + +fn com_runtime_import_name() -> String { + let import_name = crate::codegen::project::get_import_name(); + if import_name == "@microsoft/dynwinrt" { + format!("{import_name}/com") + } else { + import_name + } +} + +fn render_enum_files(en: &ProjectedComEnum) -> (String, String) { + let mut js = String::from("// Generated by dynwinrt-codegen — do not edit\n"); + js.push_str(&format!("export const {} = Object.freeze({{\n", en.name)); + for member in &en.members { + js.push_str(&format!( + " {}: {},\n", + member.name, + render_enum_value(&member.value, en.underlying) + )); + } + js.push_str("});\n"); + let mut dts = String::from("// Generated by dynwinrt-codegen — do not edit\n"); + dts.push_str(&format!( + "export type {} = (typeof {})[keyof typeof {}];\n", + en.name, en.name, en.name + )); + dts.push_str(&format!("export declare const {}: {{\n", en.name)); + for member in &en.members { + dts.push_str(&format!( + " readonly {}: {};\n", + member.name, + render_enum_value(&member.value, en.underlying) + )); + } + dts.push_str("};\n"); + (js, dts) +} + +fn render_enum_value(value: &ProjectedEnumValue, underlying: ComEnumUnderlying) -> String { + let suffix = if matches!(underlying, ComEnumUnderlying::I64 | ComEnumUnderlying::U64) { + "n" + } else { + "" + }; + match value { + ProjectedEnumValue::Signed(value) => format!("{value}{suffix}"), + ProjectedEnumValue::Unsigned(value) => format!("{value}{suffix}"), + } +} + +#[cfg(test)] +#[path = "render_tests.rs"] +mod tests; diff --git a/tools/dynwinrt-codegen/src/codegen/com/javascript/render_tests.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/render_tests.rs new file mode 100644 index 00000000..09bff2d6 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/render_tests.rs @@ -0,0 +1,1909 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::*; +use crate::codegen::com::generate_com_interface_files; +use crate::codegen::com::javascript::naming::{camel_case, strip_hungarian}; +use crate::codegen::com::project::project_com_interface; +use crate::codegen::com::project::types::project_type; +use crate::com_metadata::{ + ComEnumMeta, ComEnumValue, ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta, +}; +use crate::types::TypeMeta; + +type HandleAliasKind = PointerAliasKind; + +#[test] +fn renderer_api_accepts_only_projected_ir() { + let projected = ProjectedComInterface { + name: "ITest".into(), + namespace: "Tests".into(), + iid: "00000000-0000-0000-0000-000000000001".into(), + is_iunknown_rooted: true, + methods: Vec::new(), + activation: ActivationPlan::None, + referenced_enums: Vec::new(), + }; + let output = render_com_interface(&projected); + assert!(output.js.contains("registerIUnknownInterface")); + assert!(output.dts.contains("export declare class ITest")); +} + +#[test] +fn allocator_contract_rejects_trailing_separator_and_whitespace() { + for free_with in ["CoTaskMemFree:", " CoTaskMemFree", "CoTaskMemFree "] { + let method = MethodMeta { + name: "GetData".into(), + params: vec![ParamMeta { + name: "data".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 0, + free_with: free_with.into(), + }], + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("malformed allocator names must fail closed"); + assert!(error.contains("unsupported output cleanup contract")); + } +} + +#[test] +fn cotaskmem_handle_and_inout_ownership_fail_closed() { + let handle = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + for (typ, direction, expected) in [ + ( + handle, + ParamDirection::Out, + "requires an Out data or string pointer", + ), + ( + TypeMeta::Object, + ParamDirection::InOut, + "allocator ownership transfer for [in, out]", + ), + ] { + let method = MethodMeta { + name: "GetData".into(), + params: vec![ParamMeta { + name: "data".into(), + typ, + direction, + }], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 0, + free_with: "CoTaskMemFree".into(), + }], + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("CoTaskMem ownership must not apply to handles or InOut"); + assert!(error.contains(expected), "{error}"); + } +} + +#[test] +fn dynamic_iid_output_rejects_cleanup_contract() { + let method = MethodMeta { + name: "GetThing".into(), + params: vec![ + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "result".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 1, + free_with: "CoTaskMemFree".into(), + }], + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("dynamic COM outputs cannot carry allocator cleanup"); + + assert!(error.contains("dynamic-IID interface output cannot declare an allocator")); +} + +fn handle_type_name(typ: &TypeMeta) -> Option { + match project_type(typ).ok()? { + ComType::PointerAlias { name, .. } => Some(name), + _ => None, + } +} + +fn handle_alias_kind(typ: &TypeMeta) -> Option { + match project_type(typ).ok()? { + ComType::PointerAlias { kind, .. } => Some(kind), + _ => None, + } +} + +fn is_hresult(typ: &TypeMeta) -> bool { + matches!(project_type(typ), Ok(ComType::HResult)) +} + +fn ts_type_expr_dts(typ: &TypeMeta) -> String { + super::super::types::type_dts(&project_type(typ).unwrap()) +} + +fn ts_type_expr_js(typ: &TypeMeta) -> String { + abi_type_js(&project_type(typ).unwrap()) +} + +fn wrap_arg_js(typ: &TypeMeta, variable: &str) -> String { + super::super::types::wrap_arg_js(&project_type(typ).unwrap(), variable) +} + +fn render_js(meta: &ComInterfaceMeta, _interop: Option<()>) -> String { + let projected = project_com_interface(meta, "").unwrap(); + super::render_js(&projected) +} + +fn render_dts(meta: &ComInterfaceMeta, _interop: Option<()>) -> String { + let projected = project_com_interface(meta, "").unwrap(); + super::render_dts(&projected) +} + +fn build_method_sig_js(method: &MethodMeta) -> String { + let projected = project_com_interface(&plain_iface_with_method(method.clone()), "").unwrap(); + super::build_method_sig_js(&projected.methods[0]) +} + +fn render_enum_files(en: &ComEnumMeta) -> (String, String) { + let underlying = match en.underlying { + TypeMeta::I8 => ComEnumUnderlying::I8, + TypeMeta::U8 => ComEnumUnderlying::U8, + TypeMeta::I16 => ComEnumUnderlying::I16, + TypeMeta::U16 => ComEnumUnderlying::U16, + TypeMeta::I32 => ComEnumUnderlying::I32, + TypeMeta::U32 => ComEnumUnderlying::U32, + TypeMeta::I64 => ComEnumUnderlying::I64, + TypeMeta::U64 => ComEnumUnderlying::U64, + _ => panic!("unsupported test enum underlying type"), + }; + let projected = ProjectedComEnum { + name: en.name.clone(), + underlying, + members: en + .members + .iter() + .map(|member| ProjectedComEnumMember { + name: member.name.clone(), + value: match member.value { + ComEnumValue::Signed(value) => ProjectedEnumValue::Signed(value), + ComEnumValue::Unsigned(value) => ProjectedEnumValue::Unsigned(value), + }, + }) + .collect(), + }; + super::render_enum_files(&projected) +} + +fn method_is_interop_shape(method: &MethodMeta) -> Option> { + if !method + .return_type + .as_ref() + .is_some_and(|typ| matches!(project_type(typ), Ok(ComType::HResult))) + || method.params.len() < 2 + { + return None; + } + let output = method.params.last()?; + let iid = &method.params[method.params.len() - 2]; + let iid_name = iid.name.to_ascii_lowercase(); + if output.direction != ParamDirection::Out + || output.typ != TypeMeta::Object + || iid.direction != ParamDirection::In + || iid.typ != TypeMeta::Object + || !matches!(iid_name.as_str(), "iid" | "riid") + || method.params[..method.params.len() - 2] + .iter() + .any(|param| param.direction != ParamDirection::In) + { + return None; + } + Some(method.params[..method.params.len() - 2].to_vec()) +} + +#[test] +fn camel_case_basic() { + assert_eq!(camel_case("HrInit"), "hrInit"); + assert_eq!(camel_case("SetProgressValue"), "setProgressValue"); + assert_eq!(camel_case("AddTab"), "addTab"); + assert_eq!(camel_case("URL"), "url"); + assert_eq!(camel_case("IOHandle"), "ioHandle"); +} + +#[test] +fn default_runtime_import_uses_com_subpath() { + let previous = crate::codegen::project::get_import_name(); + crate::codegen::project::set_import_name("@microsoft/dynwinrt"); + assert_eq!(com_runtime_import_name(), "@microsoft/dynwinrt/com"); + crate::codegen::project::set_import_name(&previous); +} + +#[test] +fn strip_hungarian_only_at_word_boundary() { + assert_eq!(strip_hungarian("dwReserved"), "Reserved"); + assert_eq!(strip_hungarian("hwndTab"), "Tab"); + // "hwnd" alone must NOT be stripped (no uppercase follow-up). + assert_eq!(strip_hungarian("hwnd"), "hwnd"); +} + +#[test] +fn handle_type_name_recognizes_hwnd_shape() { + let hwnd = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + assert_eq!(handle_type_name(&hwnd).as_deref(), Some("HWND")); +} + +#[test] +fn handle_alias_kind_distinguishes_handle_values_from_string_pointers() { + let hwnd = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + assert_eq!(handle_alias_kind(&hwnd), Some(HandleAliasKind::HandleValue)); + assert_eq!( + handle_alias_kind(&pwstr_struct()), + Some(HandleAliasKind::StringPointer) + ); + let psid = TypeMeta::Struct { + namespace: "Windows.Win32.Security".into(), + name: "PSID".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + assert_eq!(handle_alias_kind(&psid), Some(HandleAliasKind::DataPointer)); +} + +#[test] +fn hresult_is_not_a_handle() { + let hr = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HRESULT".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::I32, + }], + }; + assert!(handle_type_name(&hr).is_none()); + assert!(is_hresult(&hr)); +} + +#[test] +fn non_win32_struct_is_not_a_handle() { + let rect = TypeMeta::Struct { + namespace: "Windows.Foundation".into(), + name: "Rect".into(), + fields: vec![ + crate::types::FieldMeta { + name: "X".into(), + typ: TypeMeta::F32, + }, + crate::types::FieldMeta { + name: "Y".into(), + typ: TypeMeta::F32, + }, + crate::types::FieldMeta { + name: "Width".into(), + typ: TypeMeta::F32, + }, + crate::types::FieldMeta { + name: "Height".into(), + typ: TypeMeta::F32, + }, + ], + }; + assert!(handle_type_name(&rect).is_none()); +} + +// ---- Fix 2 (BOOL → boolean/i32) ---- + +fn win32_bool_struct() -> TypeMeta { + TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "BOOL".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::I32, + }], + } +} + +#[test] +fn win32_bool_is_not_a_handle() { + let b = win32_bool_struct(); + // Sanity: it's the exact shape of a handle (single Value: I32) — the + // special-case must WIN over the generic handle heuristic. + assert!( + handle_type_name(&b).is_none(), + "BOOL must not be emitted as an opaque handle typedef" + ); +} + +#[test] +fn win32_bool_projects_as_boolean_and_i32() { + let b = win32_bool_struct(); + // .d.ts surface: boolean (not `BOOL` or `bigint | Buffer`) + assert_eq!(ts_type_expr_dts(&b), "boolean"); + // .js registration: i32 type (not pointer) + assert_eq!(ts_type_expr_js(&b), "DynCom.i32Type()"); + // .js argument marshalling: truthy→1, falsy→0 as an i32 (not pointer) + assert_eq!( + wrap_arg_js(&b, "fFullscreen"), + "DynCom.i32(fFullscreen ? 1 : 0)" + ); +} + +#[test] +fn hresult_input_projects_as_number_and_i32_value() { + let hr = make_hresult(); + assert_eq!(ts_type_expr_dts(&hr), "number"); + assert_eq!(ts_type_expr_js(&hr), "DynCom.i32Type()"); + assert_eq!(wrap_arg_js(&hr, "hr"), "DynCom.i32(hr)"); + + let m = MethodMeta { + name: "Close".into(), + vtable_index: 4, + params: vec![ParamMeta { + name: "hr".into(), + typ: hr, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains(".addMethod('Close', new DynComMethodSig().addIn(DynCom.i32Type()))"), + ".js must register HRESULT in-param as i32:\n{}", + js + ); + assert!( + js.contains("DynCom.i32(hr)"), + ".js must pass HRESULT by value as i32:\n{}", + js + ); + assert!( + !js.contains("DynCom.pointer(hr)"), + ".js must not pass HRESULT as a pointer:\n{}", + js + ); + assert!( + dts.contains("close(hr: number): void;"), + ".d.ts must type HRESULT in-param as number:\n{}", + dts + ); + assert!( + !dts.contains("HRESULT"), + ".d.ts must not expose an undefined HRESULT alias:\n{}", + dts + ); +} + +// ---- Fix 3 (REFIID-guarded interop heuristic) ---- + +/// Helper: construct a MethodMeta with HRESULT return type. +fn make_hresult() -> TypeMeta { + TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HRESULT".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::I32, + }], + } +} + +#[test] +fn interop_shape_accepts_riid_named_object_trailing_in() { + // Real Windows.Win32 shape: `HRESULT GetForWindow(HWND appWindow, REFIID riid, out void** ppv)`. + // REFIID typically projects to TypeMeta::Object with name "riid". + let m = MethodMeta { + name: "GetForWindow".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "appWindow".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "ppv".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let natural = method_is_interop_shape(&m) + .expect("REFIID-shaped trailing in-param named `riid` must be recognised as interop"); + // Natural in-params = every in EXCEPT the trailing REFIID. + assert_eq!(natural.len(), 1); + assert_eq!(natural[0].name, "appWindow"); +} + +#[test] +fn interop_shape_rejects_guid_passed_by_value() { + let m = MethodMeta { + name: "GetSomething".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "target".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Guid, + direction: ParamDirection::In, + }, + ParamMeta { + name: "out".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + assert!( + method_is_interop_shape(&m).is_none(), + "a by-value GUID must not be passed as a REFIID pointer" + ); +} + +/// FIX 3 REGRESSION: a method returning HRESULT with an [out] Object and a +/// trailing In-Object whose name is NOT `riid`/`iid` (e.g. a real application +/// COM interface pointer like `original`) must NOT be mis-classified as +/// interop-shape. Otherwise the codegen would silently drop the caller's +/// meaningful argument. +#[test] +fn interop_shape_rejects_non_refiid_trailing_object() { + let m = MethodMeta { + name: "CloneWithOriginal".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "context".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + // NOT `riid`/`iid`, NOT Guid — a real COM pointer in-param. + name: "original".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "cloned".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + assert!( + method_is_interop_shape(&m).is_none(), + "trailing in-param `original` is a real Object argument, NOT a REFIID — \ + it must not be dropped by the interop heuristic" + ); +} + +#[test] +fn interop_shape_rejects_iid_named_non_object_param() { + // A parameter named `riid` but typed as a plain I32 is not a REFIID — + // reject rather than silently drop. + let m = MethodMeta { + name: "Weird".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "hwnd".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ParamMeta { + name: "out".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + assert!( + method_is_interop_shape(&m).is_none(), + "an I32 named `riid` is not a REFIID — must be rejected" + ); +} + +// ---- Fix 1 (winmd-derived interop IID, fail-loud on unresolved) ---- + +/// Build a fully synthetic ComInterfaceMeta for an `IFooInterop`-style +/// interface whose derived projected class name (`Foo`) does NOT exist +/// anywhere reachable. The generator must FAIL LOUDLY rather than emit +/// a NULL riid. +#[test] +fn interop_generation_fails_when_target_iid_unresolvable() { + use crate::com_metadata::{ComInterfaceMeta, InterfaceMeta}; + + let iface = InterfaceMeta { + name: "IThisRuntimeClassDoesNotExist_DynWinrtInterop".into(), + namespace: "Windows.Win32.System.WinRT".into(), + iid: "00000000-0000-0000-0000-000000000000".into(), + methods: vec![MethodMeta { + name: "GetForWindow".into(), + vtable_index: 3, + params: vec![ + ParamMeta { + name: "appWindow".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "ppv".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }], + generic_piid: None, + generic_args: Vec::new(), + doc: None, + deprecated: None, + }; + let com = ComInterfaceMeta { + interface: iface, + base_offset: 3, + is_iunknown_rooted: true, + base_chain: vec!["IUnknown".into()], + coclass_clsid: None, + coclass_name: None, + own_methods_start: 3, + referenced_enums: Vec::new(), + }; + // Pass empty winmd_paths — even with the newest-SDK fallback, the + // synthetic class name won't be found anywhere. + let result = generate_com_interface_files(&com, ""); + assert!( + result.is_err(), + "generator must fail loudly when the projected runtime-class IID \ + cannot be resolved; got Ok(_)" + ); + let err = result.unwrap_err(); + assert!( + err.contains("ThisRuntimeClassDoesNotExist_Dynwinrt") + || err.contains("ThisRuntimeClassDoesNotExist_DynWinrt"), + "error must name the class it failed to resolve: {}", + err + ); + assert!( + !err.is_empty(), + "error message must be non-empty (fail-loud contract)" + ); +} + +#[test] +fn non_interop_iunknown_interface_still_generates_without_winmd_lookup() { + // A vanilla IUnknown-rooted interface with no coclass and no + // interop shape must succeed even when we pass empty winmd paths. + use crate::com_metadata::{ComInterfaceMeta, InterfaceMeta}; + let iface = InterfaceMeta { + name: "IMyPlainClassicCom".into(), + namespace: "Windows.Win32.System.Com".into(), + iid: "11111111-2222-3333-4444-555555555555".into(), + methods: vec![MethodMeta { + name: "DoStuff".into(), + vtable_index: 3, + params: vec![], + return_type: Some(make_hresult()), + ..Default::default() + }], + generic_piid: None, + generic_args: Vec::new(), + doc: None, + deprecated: None, + }; + let com = ComInterfaceMeta { + interface: iface, + base_offset: 3, + is_iunknown_rooted: true, + base_chain: vec!["IUnknown".into()], + coclass_clsid: None, + coclass_name: None, + own_methods_start: 3, + referenced_enums: Vec::new(), + }; + let out = generate_com_interface_files(&com, "") + .expect("plain classic-COM codegen must succeed with no winmds"); + assert!(out.js.contains("DynCom.registerIUnknownInterface")); + assert!(out.js.contains("method(3)")); +} + +// ---- Fix 4 (classic-COM plain `[out]` param → return-value projection) ---- + +fn plain_iface_with_method(m: MethodMeta) -> crate::com_metadata::ComInterfaceMeta { + use crate::com_metadata::{ComInterfaceMeta, InterfaceMeta}; + let iface = InterfaceMeta { + name: "IHasOut".into(), + namespace: "Windows.Win32.System.Com".into(), + iid: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee".into(), + methods: vec![m], + generic_piid: None, + generic_args: Vec::new(), + doc: None, + deprecated: None, + }; + ComInterfaceMeta { + interface: iface, + base_offset: 3, + is_iunknown_rooted: true, + base_chain: vec!["IUnknown".into()], + coclass_clsid: None, + coclass_name: None, + own_methods_start: 3, + referenced_enums: Vec::new(), + } +} + +#[test] +fn unsupported_struct_in_out_fails_closed() { + let method = MethodMeta { + name: "Read".into(), + params: vec![ParamMeta { + name: "value".into(), + typ: TypeMeta::Struct { + namespace: "Windows.Win32.System.Com".into(), + name: "VARIANT".into(), + fields: vec![ + crate::types::FieldMeta { + name: "vt".into(), + typ: TypeMeta::U16, + }, + crate::types::FieldMeta { + name: "data".into(), + typ: TypeMeta::U64, + }, + ], + }, + direction: ParamDirection::InOut, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("unsupported struct in/out must not emit a wrong T** ABI"); + assert!(error.contains("requires native layout projection")); +} + +#[test] +fn unsupported_by_value_struct_fails_closed() { + let method = MethodMeta { + name: "DragEnter".into(), + params: vec![ParamMeta { + name: "point".into(), + typ: TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "POINTL".into(), + fields: vec![ + crate::types::FieldMeta { + name: "x".into(), + typ: TypeMeta::I32, + }, + crate::types::FieldMeta { + name: "y".into(), + typ: TypeMeta::I32, + }, + ], + }, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("struct layout must fail closed"); + assert!(error.contains("requires native layout projection")); +} + +#[test] +fn unsupported_struct_direct_return_fails_closed() { + let method = MethodMeta { + name: "GetPoint".into(), + return_type: Some(TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "POINT".into(), + fields: vec![ + crate::types::FieldMeta { + name: "x".into(), + typ: TypeMeta::I32, + }, + crate::types::FieldMeta { + name: "y".into(), + typ: TypeMeta::I32, + }, + ], + }), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("unsupported struct return must not panic at invocation time"); + assert!(error.contains("unsupported direct native return")); +} + +#[test] +fn plain_method_single_out_scalar_projects_as_return() { + // Model: `HRESULT GetShowCmd([out] int* pcmd)` — the classic single-out + // int shape. The out-int must become the method's return value. + let m = MethodMeta { + name: "GetShowCmd".into(), + vtable_index: 8, + params: vec![ParamMeta { + name: "pcmd".into(), + typ: TypeMeta::I32, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + // .js: must capture `_out` and return it as a JS number. + assert!( + js.contains("const _out = _IHasOut.method(8).invoke(this._obj, [])"), + ".js must capture invoke() result into _out:\n{}", + js + ); + assert!( + js.contains("return DynCom.toNumber(_out);"), + ".js must unwrap the I32 out:\n{}", + js + ); + // .d.ts: return type must be `number`, not `void`. + assert!( + dts.contains("getShowCmd(): number;"), + ".d.ts must project single-out I32 as `number`:\n{}", + dts + ); +} + +#[test] +fn plain_method_single_out_guid_projects_as_string() { + // Model: `HRESULT GetClassID([out] GUID* pClassID)` (IPersist shape). + let m = MethodMeta { + name: "GetClassID".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "pClassID".into(), + typ: TypeMeta::Guid, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains("const _out = _IHasOut.method(3).invoke(this._obj, [])"), + ".js must capture invoke() result into _out:\n{}", + js + ); + assert!( + js.contains("return DynCom.toGuidString(_out);"), + ".js must unwrap GUID out:\n{}", + js + ); + assert!( + dts.contains("getClassID(): string;"), + ".d.ts must project single-out GUID as `string`:\n{}", + dts + ); +} + +#[test] +fn plain_method_single_out_enum_projects_as_underlying() { + // Model: `HRESULT GetKind([out] MyKind* pk)` where MyKind is an I32 + // enum. Underlying-scalar unwrap → `.toNumber()`; .d.ts uses the enum + // type name. + let m = MethodMeta { + name: "GetKind".into(), + vtable_index: 5, + params: vec![ParamMeta { + name: "pk".into(), + typ: TypeMeta::Enum { + namespace: "Windows.Win32.System.Com".into(), + name: "MyKind".into(), + underlying: Box::new(TypeMeta::I32), + members: Vec::new(), + is_flags: false, + doc: None, + deprecated: None, + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains("return DynCom.toNumber(_out);"), + ".js must unwrap enum out via its underlying scalar:\n{}", + js + ); + assert!( + dts.contains("getKind(): MyKind;"), + ".d.ts must project enum out under the enum's declared name:\n{}", + dts + ); +} + +#[test] +fn plain_method_multi_out_uses_invoke_all_and_tuple_return() { + // Model: `HRESULT Q([out] uint32_t* a, [out] BOOL* found)` — two + // trailing out params must flip to `.invokeAll()` and a tuple return. + let m = MethodMeta { + name: "Q".into(), + vtable_index: 6, + params: vec![ + ParamMeta { + name: "a".into(), + typ: TypeMeta::U32, + direction: ParamDirection::Out, + }, + ParamMeta { + name: "found".into(), + typ: TypeMeta::Bool, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains("const _r = _IHasOut.method(6).invokeAll(this._obj, [])"), + ".js multi-out must use .invokeAll():\n{}", + js + ); + assert!( + js.contains("return [DynCom.toU32(_r[0]), DynCom.toBool(_r[1])];"), + ".js multi-out must return a tuple with each out unwrapped:\n{}", + js + ); + assert!( + dts.contains("q(): [number, boolean];"), + ".d.ts multi-out must project a tuple type:\n{}", + dts + ); +} + +#[test] +fn plain_method_zero_out_still_discards_result() { + // No out params: existing behavior — invoke and discard. + let m = MethodMeta { + name: "DoIt".into(), + vtable_index: 4, + params: vec![ParamMeta { + name: "arg".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + !js.contains("const _out ="), + ".js zero-out must not capture invoke() result:\n{}", + js + ); + assert!( + !js.contains("invokeAll"), + ".js zero-out must not use .invokeAll():\n{}", + js + ); + assert!( + js.contains("_IHasOut.method(4).invoke(this._obj,"), + ".js zero-out must call plain .invoke():\n{}", + js + ); + assert!( + dts.contains("doIt(arg: number): void;"), + ".d.ts zero-out must still be `void`:\n{}", + dts + ); +} + +#[test] +fn direct_native_return_uses_return_abi_instead_of_synthetic_out_param() { + let method = MethodMeta { + name: "RetryRejectedCall".into(), + vtable_index: 5, + return_type: Some(TypeMeta::U32), + ..Default::default() + }; + let signature = build_method_sig_js(&method); + assert_eq!(signature, "new DynComMethodSig().returns(DynCom.u32Type())"); + + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!(js.contains("const _out = _IHasOut.method(5).invoke(this._obj, [])")); + assert!(js.contains("return DynCom.toU32(_out);")); + assert!(dts.contains("retryRejectedCall(): number;")); +} + +#[test] +fn native_void_return_is_declared_explicitly() { + let method = MethodMeta { + name: "OnClose".into(), + vtable_index: 8, + return_type: None, + ..Default::default() + }; + assert_eq!( + build_method_sig_js(&method), + "new DynComMethodSig().returnsVoid()" + ); + + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + assert!(js.contains("_IHasOut.method(8).invoke(this._obj, [])")); + assert!(!js.contains("const _out =")); +} + +#[test] +fn direct_64_bit_returns_use_bigint_accessors() { + let i64_method = MethodMeta { + name: "GetSigned".into(), + return_type: Some(TypeMeta::I64), + ..Default::default() + }; + let u64_method = MethodMeta { + name: "GetUnsigned".into(), + return_type: Some(TypeMeta::U64), + ..Default::default() + }; + + let i64_js = render_js(&plain_iface_with_method(i64_method), None); + let u64_js = render_js(&plain_iface_with_method(u64_method), None); + assert!(i64_js.contains("return DynCom.toI64Bigint(_out);")); + assert!(u64_js.contains("return DynCom.toU64Bigint(_out);")); +} + +#[test] +fn return_only_handle_declares_its_alias() { + let hwnd = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + let method = MethodMeta { + name: "GetWindow".into(), + return_type: Some(hwnd), + ..Default::default() + }; + let com = plain_iface_with_method(method); + let dts = render_dts(&com, None); + assert!(dts.contains("export type HWND = bigint | number;")); + assert!(dts.contains("getWindow(): HWND;")); +} + +#[test] +fn handle_value_arg_accepts_buffer_and_string_pointer_keeps_buffer() { + let hwnd = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + let method = MethodMeta { + name: "SetOverlayIcon".into(), + params: vec![ + ParamMeta { + name: "hwnd".into(), + typ: hwnd, + direction: ParamDirection::In, + }, + ParamMeta { + name: "description".into(), + typ: pwstr_struct(), + direction: ParamDirection::In, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let iface = plain_iface_with_method(method); + let dts = render_dts(&iface, None); + let js = render_js(&iface, None); + + // HWND inputs accept Electron's pointer-width Buffer, but the HWND + // output alias remains a numeric handle value. + assert!(dts.contains("export type HWND = bigint | number;")); + assert!(dts.contains("export type PWSTR = bigint | Buffer;")); + assert!(dts.contains("Pass a `Buffer` holding the string bytes")); + assert!( + dts.contains("setOverlayIcon(hwnd: HWND | Buffer | Uint8Array, description: PWSTR): void;") + ); + + // Handle-value conversion is centralized in the runtime; string + // pointers continue to pass their backing-store address. + assert!( + js.contains("DynCom.pointer(DynCom.handleValue(hwnd))"), + "HWND arg must use DynCom.handleValue:\n{js}" + ); + assert!(!js.contains("function _handleArg(")); + assert!(!js.contains("handleValue(description)")); +} + +#[test] +fn data_pointer_alias_does_not_read_buffer_contents_as_a_handle() { + let psid = TypeMeta::Struct { + namespace: "Windows.Win32.Security".into(), + name: "PSID".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + let method = MethodMeta { + name: "AddUserSid".into(), + params: vec![ParamMeta { + name: "userSid".into(), + typ: psid, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let iface = plain_iface_with_method(method); + let js = render_js(&iface, None); + let dts = render_dts(&iface, None); + + assert!(dts.contains("export type PSID = bigint | number;")); + assert!(dts.contains("addUserSid(userSid: PSID | Buffer | Uint8Array): void;")); + assert!(js.contains("DynCom.pointer(userSid)")); + assert!(!js.contains("handleValue(userSid)")); +} + +#[test] +fn hwnd_in_out_uses_runtime_handle_conversion_without_inline_helper() { + let hwnd = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + let method = MethodMeta { + name: "Create".into(), + params: vec![ParamMeta { + name: "window".into(), + typ: hwnd, + direction: ParamDirection::InOut, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let iface = plain_iface_with_method(method); + let js = render_js(&iface, None); + + assert!(js.contains("DynCom.pointer(DynCom.handleValue(window))")); + assert!(!js.contains("function _handleArg(")); +} + +#[test] +fn return_only_enum_emits_import_and_sibling_files() { + let kind = TypeMeta::Enum { + namespace: "Windows.Win32.Example".into(), + name: "THING_KIND".into(), + underlying: Box::new(TypeMeta::I32), + members: Vec::new(), + is_flags: false, + doc: None, + deprecated: None, + }; + let method = MethodMeta { + name: "GetKind".into(), + return_type: Some(kind.clone()), + ..Default::default() + }; + let mut com = plain_iface_with_method(method); + com.referenced_enums.push(ComEnumMeta { + namespace: "Windows.Win32.Example".into(), + name: "THING_KIND".into(), + underlying: TypeMeta::I32, + members: Vec::new(), + is_flags: false, + }); + + let output = generate_com_interface_files(&com, "").unwrap(); + assert!( + output + .dts + .contains("import { THING_KIND } from './THING_KIND.js';") + ); + assert!( + output + .extra_files + .iter() + .any(|(name, _)| name == "THING_KIND.d.ts") + ); +} + +#[test] +fn unsigned_enum_literals_preserve_u32_and_u64_values() { + let u32_enum = ComEnumMeta { + namespace: "Windows.Win32.Example".into(), + name: "U32_FLAGS".into(), + underlying: TypeMeta::U32, + members: vec![crate::com_metadata::ComEnumMember { + name: "HIGH_BIT".into(), + value: ComEnumValue::Unsigned(2_147_483_648), + }], + is_flags: true, + }; + let u64_enum = ComEnumMeta { + namespace: "Windows.Win32.Example".into(), + name: "U64_FLAGS".into(), + underlying: TypeMeta::U64, + members: vec![crate::com_metadata::ComEnumMember { + name: "HIGH_BIT".into(), + value: ComEnumValue::Unsigned(9_223_372_036_854_775_808), + }], + is_flags: true, + }; + + let (u32_js, u32_dts) = render_enum_files(&u32_enum); + assert!(u32_js.contains("HIGH_BIT: 2147483648")); + assert!(u32_dts.contains("readonly HIGH_BIT: 2147483648;")); + let (u64_js, u64_dts) = render_enum_files(&u64_enum); + assert!(u64_js.contains("HIGH_BIT: 9223372036854775808n")); + assert!(u64_dts.contains("readonly HIGH_BIT: 9223372036854775808n;")); +} + +#[test] +fn in_out_parameter_is_both_argument_and_result() { + let method = MethodMeta { + name: "Adjust".into(), + vtable_index: 4, + params: vec![ParamMeta { + name: "value".into(), + typ: TypeMeta::I32, + direction: ParamDirection::InOut, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + assert_eq!( + build_method_sig_js(&method), + "new DynComMethodSig().addInOut(DynCom.i32Type())" + ); + + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!(js.contains("adjust(value)")); + assert!(js.contains("const _out = _IHasOut.method(4).invoke(this._obj, [DynCom.i32(value)])")); + assert!(js.contains("return DynCom.toNumber(_out);")); + assert!(dts.contains("adjust(value: number): number;")); +} + +#[test] +fn unsupported_outfill_fails_closed() { + let m = MethodMeta { + name: "GetPath".into(), + vtable_index: 2, + params: vec![ + ParamMeta { + name: "pszFile".into(), + typ: TypeMeta::String, // PWSTR buffer, caller-allocated + direction: ParamDirection::OutFill, + }, + ParamMeta { + name: "cch".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let error = generate_com_interface_files(&com, "") + .expect_err("unsupported caller-allocated arrays must fail closed"); + assert!(error.contains("caller-allocated array outputs are not supported")); +} + +fn pwstr_struct() -> TypeMeta { + TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "PWSTR".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + } +} + +#[test] +fn out_string_buffer_allocates_decodes_and_returns_string() { + let m = MethodMeta { + name: "GetDescription".into(), + vtable_index: 6, + params: vec![ + ParamMeta { + name: "pszName".into(), + typ: pwstr_struct(), + direction: ParamDirection::OutStringBuffer { + count_param_index: 1, + }, + }, + ParamMeta { + name: "cch".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + js.contains("function _normalizeStringBufferCount"), + ".js must emit string buffer validation helper:\n{}", + js + ); + assert!( + js.contains(".addMethod('GetDescription', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type()))"), + ".js must register string buffer as an input pointer:\n{}", + js + ); + assert!( + js.contains("getDescription(cch = 260)") && js.contains("Buffer.alloc(cch * 2)"), + ".js must default cch and allocate a UTF-16 buffer:\n{}", + js + ); + assert!( + js.contains("const _text = _decodeWideString(_buffer);") && js.contains("return _text;"), + ".js must return the decoded wide string:\n{}", + js + ); + assert!( + dts.contains("getDescription(cch?: number): string;"), + ".d.ts must expose optional count and string return:\n{}", + dts + ); +} + +#[test] +fn callee_allocated_pwstr_is_decoded_and_freed() { + let method = MethodMeta { + name: "GetDisplayName".into(), + vtable_index: 5, + params: vec![ParamMeta { + name: "name".into(), + typ: pwstr_struct(), + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 0, + free_with: "CoTaskMemFree".into(), + }], + ..Default::default() + }; + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + + assert!(js.contains("return DynCom.takeCoTaskMemWideString(_out);")); + assert!(dts.contains("getDisplayName(): string;")); +} + +#[test] +fn string_pointer_output_without_allocator_fails_closed() { + let method = MethodMeta { + name: "GetDisplayName".into(), + params: vec![ParamMeta { + name: "name".into(), + typ: pwstr_struct(), + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("string pointer outputs require an allocator contract"); + + assert!(error.contains("string pointer output")); + assert!(error.contains("no ownership projection")); +} + +#[test] +fn unknown_output_cleanup_contract_fails_closed() { + let method = MethodMeta { + name: "GetData".into(), + params: vec![ParamMeta { + name: "data".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 0, + free_with: "LocalFree".into(), + }], + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("unknown allocators must fail before rendering"); + + assert!(error.contains("unsupported output cleanup contract")); + assert!(error.contains("LocalFree")); +} + +#[test] +fn allocator_name_must_match_exactly() { + let method = MethodMeta { + name: "GetData".into(), + params: vec![ParamMeta { + name: "data".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 0, + free_with: "CoTaskMemFreeEx".into(), + }], + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("allocator prefixes must not be accepted"); + + assert!(error.contains("unsupported output cleanup contract")); + assert!(error.contains("CoTaskMemFreeEx")); +} + +#[test] +fn multiple_string_buffers_fail_closed_before_rendering() { + let method = MethodMeta { + name: "GetNames".into(), + params: vec![ + ParamMeta { + name: "first".into(), + typ: pwstr_struct(), + direction: ParamDirection::OutStringBuffer { + count_param_index: 1, + }, + }, + ParamMeta { + name: "firstCount".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ParamMeta { + name: "second".into(), + typ: pwstr_struct(), + direction: ParamDirection::OutStringBuffer { + count_param_index: 3, + }, + }, + ParamMeta { + name: "secondCount".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("only one validated string buffer can be rendered"); + + assert!(error.contains("multiple caller-owned string buffers")); +} + +#[test] +fn semantic_hresult_dynamic_iid_fails_closed() { + let method = MethodMeta { + name: "GetThing".into(), + params: vec![ + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "result".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + preserve_hresult: true, + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("dynamic IID rendering cannot discard semantic HRESULT"); + + assert!(error.contains("semantic HRESULT dynamic-IID methods are not supported")); +} + +#[test] +fn managed_interface_input_imports_the_bridge_type() { + let method = MethodMeta { + name: "SetThing".into(), + params: vec![ParamMeta { + name: "thing".into(), + typ: TypeMeta::Interface { + namespace: "Tests".into(), + name: "IThing".into(), + iid: "11111111-2222-3333-4444-555555555555".into(), + }, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); + + assert!(output.dts.contains("import type { DynWinRtValue }")); + assert!(output.dts.contains("setThing(thing: DynWinRtValue): void;")); +} + +#[test] +fn untyped_sysfree_output_fails_closed() { + let method = MethodMeta { + name: "GetAllFileTypes".into(), + vtable_index: 4, + params: vec![ParamMeta { + name: "types".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + owned_outputs: vec![crate::com_metadata::OwnedOutput { + param_index: 0, + free_with: "SysFreeString".into(), + }], + ..Default::default() + }; + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("BSTR**-style untyped outputs must fail closed"); + assert!(error.contains("SysFreeString ownership requires a scalar Out BSTR")); +} + +#[test] +fn string_buffer_preserves_additional_outputs() { + let method = MethodMeta { + name: "GetIconLocation".into(), + vtable_index: 16, + params: vec![ + ParamMeta { + name: "path".into(), + typ: pwstr_struct(), + direction: ParamDirection::OutStringBuffer { + count_param_index: 1, + }, + }, + ParamMeta { + name: "cch".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ParamMeta { + name: "icon".into(), + typ: TypeMeta::I32, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(method); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + + assert!(js.contains("const _out = _IHasOut.method(16).invoke")); + assert!(js.contains("return [_text, DynCom.toNumber(_out)];")); + assert!(dts.contains("getIconLocation(cch?: number): [string, number];")); +} + +#[test] +fn interface_out_param_projects_as_explicit_bridge_value() { + let m = MethodMeta { + name: "GetThing".into(), + vtable_index: 7, + params: vec![ParamMeta { + name: "thing".into(), + typ: TypeMeta::Interface { + namespace: "Windows.Win32.System.Com".into(), + name: "IThing".into(), + iid: "11111111-2222-3333-4444-555555555555".into(), + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(m); + let js = render_js(&com, None); + let dts = render_dts(&com, None); + assert!( + !js.contains("from './IThing.js'"), + ".js must not depend on an ungenerated wrapper:\n{}", + js + ); + assert!(js.contains( + ".addOut(DynCom.interfaceType(WinGuid.parse('11111111-2222-3333-4444-555555555555')))" + )); + assert!( + js.contains("return _out;"), + ".js must return the managed bridge value:\n{}", + js + ); + assert!( + dts.contains("import type { DynWinRtValue }"), + ".d.ts must import the bridge type:\n{}", + dts + ); + assert!( + dts.contains("getThing(): DynWinRtValue;"), + ".d.ts must return the explicit bridge value:\n{}", + dts + ); +} + +#[test] +fn caller_supplied_riid_output_is_adopted() { + let method = MethodMeta { + name: "BindToHandler".into(), + vtable_index: 4, + params: vec![ + ParamMeta { + name: "pbc".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Object, + direction: ParamDirection::In, + }, + ParamMeta { + name: "ppv".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(make_hresult()), + ..Default::default() + }; + let com = plain_iface_with_method(method); + let output = generate_com_interface_files(&com, "").unwrap(); + + assert!(output.js.contains("bindToHandler(pbc, iid)")); + assert!(output.js.contains("DynCom.adoptComPointer(_raw, _iid)")); + assert!( + output + .dts + .contains("bindToHandler(pbc: bigint | Buffer, iid: string): DynWinRtValue;") + ); +} + +#[test] +fn hstring_output_uses_owned_hstring_projection() { + let method = MethodMeta { + name: "get_CorrelationVector".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "cv".into(), + typ: TypeMeta::String, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); + + assert!(output.js.contains(".addOut(DynCom.hstringType())")); + assert!(output.js.contains("return _out.toString();")); + assert!(output.dts.contains("get_CorrelationVector(): string;")); +} + +#[test] +fn unresolved_interface_iid_fails_closed() { + let method = MethodMeta { + name: "CreateSurface".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::Interface { + namespace: "Windows.UI.Composition".into(), + name: "ICompositionSurface".into(), + iid: String::new(), + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("an unresolved interface must not degrade to a raw pointer"); + + assert!(error.contains("ICompositionSurface")); + assert!(error.contains("no resolvable IID")); + assert!(error.contains("--ref")); +} + +#[test] +fn parameterized_interface_fails_closed_even_with_a_piid() { + let method = MethodMeta { + name: "GetItems".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IVectorView`1".into(), + piid: "bbe1fa4c-b0e3-4583-baef-1f1b2e483e56".into(), + args: vec![TypeMeta::String], + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("a PIID alone is not a closed interface IID"); + + assert!(error.contains("computed closed IID")); + assert!(error.contains("raw-pointer fallback is not allowed")); +} + +#[test] +fn async_interface_fails_closed_without_a_closed_iid() { + let method = MethodMeta { + name: "OpenAsync".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::AsyncOperation(Box::new(TypeMeta::String)), + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("async interfaces must not degrade to raw pointers"); + + assert!(error.contains("async interface requires a computed closed IID")); + assert!(error.contains("raw-pointer fallback is not allowed")); +} + +#[test] +fn native_array_fails_closed_without_count_and_ownership() { + let method = MethodMeta { + name: "GetItems".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::Array(Box::new(TypeMeta::Interface { + namespace: "Contoso".into(), + name: "IItem".into(), + iid: "11111111-2222-3333-4444-555555555555".into(), + })), + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("native arrays must not degrade to raw pointers"); + + assert!(error.contains("explicit count and element-ownership projection")); + assert!(error.contains("raw-pointer fallback is not allowed")); +} + +#[test] +fn delegate_fails_closed_without_a_callback_projection() { + let method = MethodMeta { + name: "SetHandler".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "handler".into(), + typ: TypeMeta::Delegate { + namespace: "Contoso".into(), + name: "Handler".into(), + iid: "11111111-2222-3333-4444-555555555555".into(), + }, + direction: ParamDirection::In, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("delegates require an explicit managed projection"); + + assert!(error.contains("managed callback projection")); + assert!(error.contains("raw-pointer fallback is not allowed")); +} + +#[test] +fn runtime_class_uses_its_resolved_default_interface() { + let method = MethodMeta { + name: "CreateDevice".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::RuntimeClass { + namespace: "Windows.UI.Composition".into(), + name: "CompositionGraphicsDevice".into(), + default_interface: Some(Box::new(TypeMeta::Interface { + namespace: "Windows.UI.Composition".into(), + name: "ICompositionGraphicsDevice".into(), + iid: "a329b321-0d69-4b89-9951-28de94dc998d".into(), + })), + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); + + assert!(output.js.contains( + ".addOut(DynCom.interfaceType(WinGuid.parse('a329b321-0d69-4b89-9951-28de94dc998d')))" + )); + assert!(output.js.contains("return _out;")); + assert!(output.dts.contains("createDevice(): DynWinRtValue;")); +} + +#[test] +fn runtime_class_without_a_default_interface_fails_closed() { + let method = MethodMeta { + name: "CreateDevice".into(), + vtable_index: 3, + params: vec![ParamMeta { + name: "result".into(), + typ: TypeMeta::RuntimeClass { + namespace: "Windows.UI.Composition".into(), + name: "CompositionGraphicsDevice".into(), + default_interface: None, + }, + direction: ParamDirection::Out, + }], + return_type: Some(make_hresult()), + ..Default::default() + }; + + let error = generate_com_interface_files(&plain_iface_with_method(method), "") + .expect_err("runtime classes require a resolved default interface"); + + assert!(error.contains("no resolvable default interface")); + assert!(error.contains("--ref")); +} + +#[test] +fn semantic_hresult_is_preserved_as_a_number() { + let method = MethodMeta { + name: "IsDirty".into(), + vtable_index: 4, + return_type: Some(make_hresult()), + preserve_hresult: true, + ..Default::default() + }; + + let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); + + assert!(output.js.contains(".preserveHresult()")); + assert!(output.js.contains("return DynCom.toNumber(_out);")); + assert!(output.dts.contains("isDirty(): number;")); +} + +#[test] +fn ordinary_hresult_remains_throw_or_void() { + let method = MethodMeta { + name: "Load".into(), + vtable_index: 5, + return_type: Some(make_hresult()), + ..Default::default() + }; + + let output = generate_com_interface_files(&plain_iface_with_method(method), "").unwrap(); + + assert!(!output.js.contains(".preserveHresult()")); + assert!(output.dts.contains("load(): void;")); +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs new file mode 100644 index 00000000..a3f6c2a8 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs @@ -0,0 +1,346 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::super::ir::{ + ComEnumUnderlying, ComPrimitive, ComScalarRepr, ComType, PointerAliasKind, ProjectedComResult, + ResultConversion, StringEncoding, +}; + +pub(super) fn abi_type_js(typ: &ComType) -> String { + match typ { + ComType::Primitive(primitive) => match primitive { + ComPrimitive::Bool => "DynCom.boolType()", + ComPrimitive::I8 => "DynCom.i8Type()", + ComPrimitive::U8 => "DynCom.u8Type()", + ComPrimitive::I16 => "DynCom.i16Type()", + ComPrimitive::U16 => "DynCom.u16Type()", + ComPrimitive::I32 => "DynCom.i32Type()", + ComPrimitive::U32 => "DynCom.u32Type()", + ComPrimitive::I64 => "DynCom.i64Type()", + ComPrimitive::U64 => "DynCom.u64Type()", + ComPrimitive::F32 => "DynCom.f32Type()", + ComPrimitive::F64 => "DynCom.f64Type()", + ComPrimitive::Char16 => "DynCom.char16Type()", + } + .into(), + ComType::NativeIsize => "DynCom.isizeType()".into(), + ComType::NativeUsize => "DynCom.usizeType()".into(), + ComType::Win32Bool | ComType::HResult => "DynCom.i32Type()".into(), + ComType::Guid => "DynCom.guidType()".into(), + ComType::HString => "DynCom.hstringType()".into(), + ComType::Enum { underlying, .. } => enum_abi_type_js(*underlying).into(), + ComType::ScalarAlias { underlying, .. } => scalar_abi_type_js(*underlying).into(), + ComType::RawPointer | ComType::PointerAlias { .. } | ComType::Bstr => { + "DynCom.pointerType()".into() + } + ComType::ManagedInterface { iid } => { + format!("DynCom.interfaceType(WinGuid.parse('{iid}'))") + } + } +} + +pub(super) fn input_type_dts(typ: &ComType) -> String { + match typ { + ComType::PointerAlias { + name, + kind: PointerAliasKind::HandleValue, + } if name == "HWND" => format!("{name} | Buffer | Uint8Array"), + ComType::PointerAlias { + name, + kind: PointerAliasKind::DataPointer, + } => format!("{name} | Buffer | Uint8Array"), + _ => type_dts(typ), + } +} + +pub(super) fn type_dts(typ: &ComType) -> String { + match typ { + ComType::Primitive(primitive) => match primitive { + ComPrimitive::Bool => "boolean", + ComPrimitive::I8 + | ComPrimitive::U8 + | ComPrimitive::I16 + | ComPrimitive::U16 + | ComPrimitive::I32 + | ComPrimitive::U32 + | ComPrimitive::F32 + | ComPrimitive::F64 + | ComPrimitive::Char16 => "number", + ComPrimitive::I64 | ComPrimitive::U64 => "bigint", + } + .into(), + ComType::NativeIsize | ComType::NativeUsize => "bigint".into(), + ComType::Win32Bool => "boolean".into(), + ComType::HResult => "number".into(), + ComType::Guid => "string".into(), + ComType::HString => "string".into(), + ComType::Enum { name, .. } => name.clone(), + ComType::ScalarAlias { name, .. } => name.clone(), + ComType::RawPointer => "bigint | Buffer".into(), + ComType::PointerAlias { name, .. } => name.clone(), + ComType::Bstr => "BSTR".into(), + ComType::ManagedInterface { .. } => "DynWinRtValue".into(), + } +} + +pub(super) fn result_type_dts(result: &ProjectedComResult) -> String { + match result.conversion { + ResultConversion::Bstr | ResultConversion::CoTaskMemString(_) => "string".into(), + ResultConversion::CoTaskMemData + | ResultConversion::ManagedCom + | ResultConversion::DynamicIidAdoption => "DynWinRtValue".into(), + ResultConversion::Value | ResultConversion::HString => type_dts(&result.typ), + } +} + +pub(super) fn wrap_arg_js(typ: &ComType, variable: &str) -> String { + match typ { + ComType::Primitive(primitive) => match primitive { + ComPrimitive::Bool => format!("DynCom.boolValue({variable})"), + ComPrimitive::I8 => format!("DynCom.i8Value({variable})"), + ComPrimitive::U8 => format!("DynCom.u8Value({variable})"), + ComPrimitive::I16 => format!("DynCom.i16({variable})"), + ComPrimitive::U16 => format!("DynCom.u16({variable})"), + ComPrimitive::I32 => format!("DynCom.i32({variable})"), + ComPrimitive::U32 => format!("DynCom.u32({variable})"), + ComPrimitive::I64 => format!("DynCom.i64(BigInt({variable}))"), + ComPrimitive::U64 => format!("DynCom.u64(BigInt({variable}))"), + ComPrimitive::F32 => format!("DynCom.f32({variable})"), + ComPrimitive::F64 => format!("DynCom.f64({variable})"), + ComPrimitive::Char16 => format!("DynCom.char16({variable})"), + }, + ComType::NativeIsize => format!("DynCom.isize(BigInt({variable}))"), + ComType::NativeUsize => format!("DynCom.usize(BigInt({variable}))"), + ComType::Win32Bool => format!("DynCom.i32({variable} ? 1 : 0)"), + ComType::HResult => format!("DynCom.i32({variable})"), + ComType::Guid => format!("DynCom.guid(WinGuid.parse({variable}))"), + ComType::HString => format!("DynCom.hstring({variable})"), + ComType::Enum { underlying, .. } => wrap_enum_arg_js(*underlying, variable), + ComType::ScalarAlias { underlying, .. } => wrap_scalar_arg_js(*underlying, variable), + ComType::RawPointer | ComType::Bstr => format!("DynCom.pointer({variable})"), + ComType::PointerAlias { + name, + kind: PointerAliasKind::HandleValue, + } if name == "HWND" => { + format!("DynCom.pointer(DynCom.handleValue({variable}))") + } + ComType::PointerAlias { .. } => format!("DynCom.pointer({variable})"), + ComType::ManagedInterface { .. } => variable.to_string(), + } +} + +pub(super) fn unwrap_result_js(result: &ProjectedComResult, expression: &str) -> String { + match result.conversion { + ResultConversion::Bstr => format!("DynCom.takeBstr({expression})"), + ResultConversion::CoTaskMemString(StringEncoding::Wide) => { + format!("DynCom.takeCoTaskMemWideString({expression})") + } + ResultConversion::CoTaskMemString(StringEncoding::Ansi) => { + format!("DynCom.takeCoTaskMemAnsiString({expression})") + } + ResultConversion::CoTaskMemData => { + format!("DynCom.adoptCoTaskMemPointer({expression})") + } + ResultConversion::ManagedCom | ResultConversion::DynamicIidAdoption => { + expression.to_string() + } + ResultConversion::HString => format!("{expression}.toString()"), + ResultConversion::Value => unwrap_value_js(&result.typ, expression), + } +} + +fn unwrap_value_js(typ: &ComType, expression: &str) -> String { + match typ { + ComType::Primitive(primitive) => match primitive { + ComPrimitive::Bool => format!("DynCom.toBool({expression})"), + ComPrimitive::I8 + | ComPrimitive::U8 + | ComPrimitive::I16 + | ComPrimitive::U16 + | ComPrimitive::I32 + | ComPrimitive::Char16 => format!("DynCom.toNumber({expression})"), + ComPrimitive::U32 => format!("DynCom.toU32({expression})"), + ComPrimitive::I64 => format!("DynCom.toI64Bigint({expression})"), + ComPrimitive::U64 => format!("DynCom.toU64Bigint({expression})"), + ComPrimitive::F32 | ComPrimitive::F64 => { + format!("DynCom.toF64({expression})") + } + }, + ComType::NativeIsize => format!("DynCom.toIsizeBigint({expression})"), + ComType::NativeUsize => format!("DynCom.toUsizeBigint({expression})"), + ComType::Win32Bool => format!("(DynCom.toNumber({expression}) !== 0)"), + ComType::HResult => format!("DynCom.toNumber({expression})"), + ComType::Guid => format!("DynCom.toGuidString({expression})"), + ComType::HString => format!("{expression}.toString()"), + ComType::Enum { underlying, .. } => unwrap_enum_js(*underlying, expression), + ComType::ScalarAlias { underlying, .. } => unwrap_scalar_js(*underlying, expression), + ComType::RawPointer | ComType::PointerAlias { .. } | ComType::Bstr => { + format!("DynCom.asPointerBigint({expression})") + } + ComType::ManagedInterface { .. } => expression.to_string(), + } +} + +fn enum_abi_type_js(underlying: ComEnumUnderlying) -> &'static str { + match underlying { + ComEnumUnderlying::I8 => "DynCom.i8Type()", + ComEnumUnderlying::U8 => "DynCom.u8Type()", + ComEnumUnderlying::I16 => "DynCom.i16Type()", + ComEnumUnderlying::U16 => "DynCom.u16Type()", + ComEnumUnderlying::I32 => "DynCom.i32Type()", + ComEnumUnderlying::U32 => "DynCom.u32Type()", + ComEnumUnderlying::I64 => "DynCom.i64Type()", + ComEnumUnderlying::U64 => "DynCom.u64Type()", + } +} + +pub(super) fn scalar_type_dts(underlying: ComScalarRepr) -> &'static str { + match underlying { + ComScalarRepr::Primitive(ComPrimitive::Bool) => "boolean", + ComScalarRepr::Primitive(ComPrimitive::I64 | ComPrimitive::U64) + | ComScalarRepr::NativeIsize + | ComScalarRepr::NativeUsize => "bigint", + ComScalarRepr::Primitive(_) => "number", + } +} + +fn scalar_abi_type_js(underlying: ComScalarRepr) -> &'static str { + match underlying { + ComScalarRepr::Primitive(primitive) => match primitive { + ComPrimitive::Bool => "DynCom.boolType()", + ComPrimitive::I8 => "DynCom.i8Type()", + ComPrimitive::U8 => "DynCom.u8Type()", + ComPrimitive::I16 => "DynCom.i16Type()", + ComPrimitive::U16 => "DynCom.u16Type()", + ComPrimitive::I32 => "DynCom.i32Type()", + ComPrimitive::U32 => "DynCom.u32Type()", + ComPrimitive::I64 => "DynCom.i64Type()", + ComPrimitive::U64 => "DynCom.u64Type()", + ComPrimitive::F32 => "DynCom.f32Type()", + ComPrimitive::F64 => "DynCom.f64Type()", + ComPrimitive::Char16 => "DynCom.char16Type()", + }, + ComScalarRepr::NativeIsize => "DynCom.isizeType()", + ComScalarRepr::NativeUsize => "DynCom.usizeType()", + } +} + +fn wrap_scalar_arg_js(underlying: ComScalarRepr, variable: &str) -> String { + match underlying { + ComScalarRepr::Primitive(primitive) => match primitive { + ComPrimitive::Bool => format!("DynCom.boolValue({variable})"), + ComPrimitive::I8 => format!("DynCom.i8Value({variable})"), + ComPrimitive::U8 => format!("DynCom.u8Value({variable})"), + ComPrimitive::I16 => format!("DynCom.i16({variable})"), + ComPrimitive::U16 => format!("DynCom.u16({variable})"), + ComPrimitive::I32 => format!("DynCom.i32({variable})"), + ComPrimitive::U32 => format!("DynCom.u32({variable})"), + ComPrimitive::I64 => format!("DynCom.i64(BigInt({variable}))"), + ComPrimitive::U64 => format!("DynCom.u64(BigInt({variable}))"), + ComPrimitive::F32 => format!("DynCom.f32({variable})"), + ComPrimitive::F64 => format!("DynCom.f64({variable})"), + ComPrimitive::Char16 => format!("DynCom.char16({variable})"), + }, + ComScalarRepr::NativeIsize => format!("DynCom.isize(BigInt({variable}))"), + ComScalarRepr::NativeUsize => format!("DynCom.usize(BigInt({variable}))"), + } +} + +fn unwrap_scalar_js(underlying: ComScalarRepr, expression: &str) -> String { + match underlying { + ComScalarRepr::Primitive(primitive) => match primitive { + ComPrimitive::Bool => format!("DynCom.toBool({expression})"), + ComPrimitive::I8 + | ComPrimitive::U8 + | ComPrimitive::I16 + | ComPrimitive::U16 + | ComPrimitive::I32 + | ComPrimitive::Char16 => format!("DynCom.toNumber({expression})"), + ComPrimitive::U32 => format!("DynCom.toU32({expression})"), + ComPrimitive::I64 => format!("DynCom.toI64Bigint({expression})"), + ComPrimitive::U64 => format!("DynCom.toU64Bigint({expression})"), + ComPrimitive::F32 | ComPrimitive::F64 => { + format!("DynCom.toF64({expression})") + } + }, + ComScalarRepr::NativeIsize => format!("DynCom.toIsizeBigint({expression})"), + ComScalarRepr::NativeUsize => format!("DynCom.toUsizeBigint({expression})"), + } +} + +fn wrap_enum_arg_js(underlying: ComEnumUnderlying, variable: &str) -> String { + match underlying { + ComEnumUnderlying::I8 => format!("DynCom.i8Value({variable})"), + ComEnumUnderlying::U8 => format!("DynCom.u8Value({variable})"), + ComEnumUnderlying::I16 => format!("DynCom.i16({variable})"), + ComEnumUnderlying::U16 => format!("DynCom.u16({variable})"), + ComEnumUnderlying::I32 => format!("DynCom.i32({variable})"), + ComEnumUnderlying::U32 => format!("DynCom.u32({variable})"), + ComEnumUnderlying::I64 => format!("DynCom.i64(BigInt({variable}))"), + ComEnumUnderlying::U64 => format!("DynCom.u64(BigInt({variable}))"), + } +} + +fn unwrap_enum_js(underlying: ComEnumUnderlying, expression: &str) -> String { + match underlying { + ComEnumUnderlying::I8 + | ComEnumUnderlying::U8 + | ComEnumUnderlying::I16 + | ComEnumUnderlying::U16 + | ComEnumUnderlying::I32 => format!("DynCom.toNumber({expression})"), + ComEnumUnderlying::U32 => format!("DynCom.toU32({expression})"), + ComEnumUnderlying::I64 => format!("DynCom.toI64Bigint({expression})"), + ComEnumUnderlying::U64 => format!("DynCom.toU64Bigint({expression})"), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn mappings_cover_every_supported_com_type() { + let types = vec![ + ComType::Primitive(ComPrimitive::Bool), + ComType::Primitive(ComPrimitive::I8), + ComType::Primitive(ComPrimitive::U8), + ComType::Primitive(ComPrimitive::I16), + ComType::Primitive(ComPrimitive::U16), + ComType::Primitive(ComPrimitive::I32), + ComType::Primitive(ComPrimitive::U32), + ComType::Primitive(ComPrimitive::I64), + ComType::Primitive(ComPrimitive::U64), + ComType::Primitive(ComPrimitive::F32), + ComType::Primitive(ComPrimitive::F64), + ComType::Primitive(ComPrimitive::Char16), + ComType::NativeIsize, + ComType::NativeUsize, + ComType::Win32Bool, + ComType::HResult, + ComType::Guid, + ComType::HString, + ComType::Enum { + name: "E".into(), + underlying: ComEnumUnderlying::U32, + }, + ComType::ScalarAlias { + name: "COLORREF".into(), + underlying: ComScalarRepr::Primitive(ComPrimitive::U32), + }, + ComType::RawPointer, + ComType::PointerAlias { + name: "HWND".into(), + kind: PointerAliasKind::HandleValue, + }, + ComType::Bstr, + ComType::ManagedInterface { + iid: "00000000-0000-0000-0000-000000000000".into(), + }, + ]; + for typ in types { + assert!(!abi_type_js(&typ).is_empty()); + assert!(!type_dts(&typ).is_empty()); + assert!(!wrap_arg_js(&typ, "value").is_empty()); + } + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/mod.rs b/tools/dynwinrt-codegen/src/codegen/com/mod.rs new file mode 100644 index 00000000..67d0f914 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/mod.rs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Classic-COM metadata projection and JavaScript generation. + +mod ir; +mod javascript; +mod project; + +use crate::com_metadata::ComInterfaceMeta; + +pub use javascript::render::ComGeneratedOutput; + +pub fn generate_com_interface_files( + meta: &ComInterfaceMeta, + winmd_paths: &str, +) -> Result { + let projected = project::project_com_interface(meta, winmd_paths)?; + Ok(javascript::render::render_com_interface(&projected)) +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/project/interop.rs b/tools/dynwinrt-codegen/src/codegen/com/project/interop.rs new file mode 100644 index 00000000..5a87fc9a --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/project/interop.rs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub(super) fn resolve_projected_default_iid( + winmd_paths: &str, + simple_class_name: &str, +) -> Option<(String, String, String)> { + if !winmd_paths.is_empty() { + if let Some(result) = + crate::com_metadata::find_runtime_class_default_iid(winmd_paths, simple_class_name) + { + return Some(result); + } + } + let sdk_winmd = crate::com_metadata::discover_newest_windows_winmd()?; + if winmd_paths + .split(';') + .any(|path| path.eq_ignore_ascii_case(&sdk_winmd)) + { + return None; + } + crate::com_metadata::find_runtime_class_default_iid(&sdk_winmd, simple_class_name) +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/project/mod.rs b/tools/dynwinrt-codegen/src/codegen/com/project/mod.rs new file mode 100644 index 00000000..81a852d2 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/project/mod.rs @@ -0,0 +1,762 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod interop; +pub(super) mod types; + +use crate::com_metadata::{ComEnumValue, ComInterfaceMeta, MethodMeta, ParamDirection, ParamMeta}; +use crate::types::TypeMeta; + +use super::ir::{ + ActivationPlan, ComParamDirection, ComReturnConvention, ComType, ProjectedComEnum, + ProjectedComEnumMember, ProjectedComInterface, ProjectedComMethod, ProjectedComMethodKind, + ProjectedComParam, ProjectedComResult, ProjectedEnumValue, ResultConversion, ResultSource, + StringBufferPlan, StringEncoding, UnsupportedComType, +}; +use super::javascript::naming::camel_case; +use interop::resolve_projected_default_iid; +use types::{is_scalar_in_out, is_supported_direct_return, project_enum_underlying, project_type}; + +pub(super) fn project_com_interface( + meta: &ComInterfaceMeta, + winmd_paths: &str, +) -> Result { + let interop_target = detect_interop_target(meta, winmd_paths)?; + let methods = meta + .interface + .methods + .iter() + .map(|method| project_method(meta, method, interop_target.as_ref())) + .collect::, _>>()?; + let activation = if let Some((class_name, class_namespace, target_iid)) = interop_target { + ActivationPlan::WinRtFactory { + class_name, + class_namespace, + target_iid, + } + } else if let Some(clsid) = &meta.coclass_clsid { + ActivationPlan::Coclass { + clsid: clsid.clone(), + coclass_name: meta + .coclass_name + .clone() + .unwrap_or_else(|| "Coclass".into()), + } + } else { + ActivationPlan::None + }; + let referenced_enums = meta + .referenced_enums + .iter() + .map(|en| { + Ok(ProjectedComEnum { + name: en.name.clone(), + underlying: project_enum_underlying(&en.underlying).map_err(|unsupported| { + unsupported_error(unsupported, "enum underlying type") + })?, + members: en + .members + .iter() + .map(|member| ProjectedComEnumMember { + name: member.name.clone(), + value: match member.value { + ComEnumValue::Signed(value) => ProjectedEnumValue::Signed(value), + ComEnumValue::Unsigned(value) => ProjectedEnumValue::Unsigned(value), + }, + }) + .collect(), + }) + }) + .collect::, String>>()?; + Ok(ProjectedComInterface { + name: meta.interface.name.clone(), + namespace: meta.interface.namespace.clone(), + iid: meta.interface.iid.clone(), + is_iunknown_rooted: meta.is_iunknown_rooted, + methods, + activation, + referenced_enums, + }) +} + +fn project_method( + interface: &ComInterfaceMeta, + method: &MethodMeta, + interop_target: Option<&(String, String, String)>, +) -> Result { + let context = || format!("{}.{}", interface.interface.name, method.name); + let dynamic_natural_count = dynamic_iid_natural_param_count(method); + if dynamic_natural_count.is_some() && method.preserve_hresult { + return Err(format!( + "{}: semantic HRESULT dynamic-IID methods are not supported", + context() + )); + } + let kind = match (interop_target, method.name.as_str(), dynamic_natural_count) { + (Some((_, _, target_iid)), "GetForWindow", Some(natural_param_count)) => { + ProjectedComMethodKind::SynthesizedGetForWindow { + natural_param_count, + target_iid: target_iid.clone(), + } + } + (_, _, Some(natural_param_count)) => ProjectedComMethodKind::CallerSuppliedDynamicIid { + natural_param_count, + }, + _ => ProjectedComMethodKind::Normal, + }; + + let mut params = Vec::with_capacity(method.params.len()); + for (index, param) in method.params.iter().enumerate() { + match param.direction { + ParamDirection::UnsupportedNativeArray { count_param_index } => { + let count = count_param_index + .map(|index| format!("parameter index {index}")) + .unwrap_or_else(|| "metadata-defined size".into()); + return Err(format!( + "{}: caller-sized native buffers are not supported (`{}` uses {count})", + context(), + param.name + )); + } + ParamDirection::OutFill => { + return Err(format!( + "{}: caller-allocated array outputs are not supported", + context() + )); + } + _ => {} + } + let typ = project_type(¶m.typ).map_err(|unsupported| { + unsupported_error( + unsupported, + &format!("{} parameter `{}`", context(), param.name), + ) + })?; + let cleanup = cleanup_for_param(method, index, &context())?; + if cleanup.is_some() && dynamic_natural_count.is_some() && index == method.params.len() - 1 + { + return Err(format!( + "{}: dynamic-IID interface output cannot declare an allocator cleanup contract", + context() + )); + } + if cleanup.is_some() && param.direction == ParamDirection::InOut { + return Err(format!( + "{}: allocator ownership transfer for [in, out] parameter `{}` is not supported", + context(), + param.name + )); + } + if param.direction == ParamDirection::InOut && !is_scalar_in_out(&typ) { + return Err(format!( + "{}: unsupported [in, out] parameter `{}` of type {:?}", + context(), + param.name, + param.typ + )); + } + if param.direction == ParamDirection::Out + && typ == ComType::RawPointer + && dynamic_natural_count.is_none() + && cleanup != Some(CleanupKind::CoTaskMemFree) + && cleanup.is_none() + { + return Err(unsupported_error( + UnsupportedComType::UnknownOwnership { + type_name: "untyped pointer output".into(), + }, + &context(), + )); + } + if param.direction == ParamDirection::Out + && typ == ComType::Bstr + && cleanup != Some(CleanupKind::SysFreeString) + && cleanup.is_none() + { + return Err(unsupported_error( + UnsupportedComType::UnknownOwnership { + type_name: "BSTR output".into(), + }, + &context(), + )); + } + if matches!(param.direction, ParamDirection::Out | ParamDirection::InOut) + && matches!( + typ, + ComType::PointerAlias { + kind: super::ir::PointerAliasKind::StringPointer, + .. + } + ) + && cleanup != Some(CleanupKind::CoTaskMemFree) + && cleanup.is_none() + { + return Err(unsupported_error( + UnsupportedComType::UnknownOwnership { + type_name: format!("string pointer output `{}`", param.name), + }, + &context(), + )); + } + params.push(ProjectedComParam { + name: param.name.clone(), + typ, + direction: match param.direction { + ParamDirection::In => ComParamDirection::In, + ParamDirection::Out => ComParamDirection::Out, + ParamDirection::InOut => ComParamDirection::InOut, + ParamDirection::OutStringBuffer { .. } => ComParamDirection::OutStringBuffer, + ParamDirection::OutFill | ParamDirection::UnsupportedNativeArray { .. } => { + unreachable!("unsupported directions returned above") + } + }, + }); + } + validate_owned_outputs(method, ¶ms, &context())?; + + let return_convention = match &method.return_type { + None => ComReturnConvention::Void, + Some(typ) => { + let projected = project_type(typ).map_err(|unsupported| match unsupported { + UnsupportedComType::NativeStructLayout { .. } | UnsupportedComType::Unknown => { + unsupported_error( + UnsupportedComType::UnsupportedDirectReturn { + type_name: format!("{typ:?}"), + }, + &context(), + ) + } + unsupported => { + unsupported_error(unsupported, &format!("{} return value", context())) + } + })?; + if projected == ComType::HResult { + if method.preserve_hresult { + ComReturnConvention::SemanticHResult + } else { + ComReturnConvention::HResult + } + } else if is_supported_direct_return(&projected) { + ComReturnConvention::Direct(projected) + } else { + return Err(unsupported_error( + UnsupportedComType::UnsupportedDirectReturn { + type_name: format!("{typ:?}"), + }, + &context(), + )); + } + } + }; + if method.preserve_hresult && !matches!(return_convention, ComReturnConvention::SemanticHResult) + { + return Err(format!( + "{}: semantic HRESULT metadata requires an HRESULT return", + context() + )); + } + + let string_buffer = project_string_buffer(method)?; + if params + .iter() + .any(|param| param.direction == ComParamDirection::OutStringBuffer) + && string_buffer.is_none() + { + return Err(format!( + "{}: unsupported string-buffer encoding or count relationship", + context() + )); + } + let mut results = Vec::new(); + if let ComReturnConvention::SemanticHResult | ComReturnConvention::Direct(_) = + &return_convention + { + let typ = match &return_convention { + ComReturnConvention::SemanticHResult => ComType::HResult, + ComReturnConvention::Direct(typ) => typ.clone(), + ComReturnConvention::HResult | ComReturnConvention::Void => unreachable!(), + }; + results.push(ProjectedComResult { + conversion: result_conversion(&typ, method, None, &kind), + typ, + source: ResultSource::DirectReturn, + }); + } + for (index, param) in params.iter().enumerate() { + if matches!( + param.direction, + ComParamDirection::Out | ComParamDirection::InOut + ) { + results.push(ProjectedComResult { + typ: param.typ.clone(), + source: ResultSource::Param(index), + conversion: result_conversion(¶m.typ, method, Some(index), &kind), + }); + } + } + + Ok(ProjectedComMethod { + name: method.name.clone(), + camel_name: camel_case(&method.name), + vtable_index: method.vtable_index, + params, + return_convention, + results, + string_buffer, + kind, + doc: method.doc.clone(), + }) +} + +fn result_conversion( + typ: &ComType, + method: &MethodMeta, + param_index: Option, + kind: &ProjectedComMethodKind, +) -> ResultConversion { + if matches!( + kind, + ProjectedComMethodKind::CallerSuppliedDynamicIid { .. } + | ProjectedComMethodKind::SynthesizedGetForWindow { .. } + ) && param_index == Some(method.params.len() - 1) + { + return ResultConversion::DynamicIidAdoption; + } + if let Some(index) = param_index { + let cleanup = cleanup_for_param(method, index, &method.name) + .expect("cleanup contract was validated during projection"); + if cleanup == Some(CleanupKind::SysFreeString) && matches!(typ, ComType::Bstr) { + return ResultConversion::Bstr; + } + if cleanup == Some(CleanupKind::CoTaskMemFree) { + return match typ { + ComType::PointerAlias { name, .. } if name == "PWSTR" => { + ResultConversion::CoTaskMemString(StringEncoding::Wide) + } + ComType::PointerAlias { name, .. } if name == "PSTR" => { + ResultConversion::CoTaskMemString(StringEncoding::Ansi) + } + _ => ResultConversion::CoTaskMemData, + }; + } + } + match typ { + ComType::ManagedInterface { .. } => ResultConversion::ManagedCom, + ComType::HString => ResultConversion::HString, + _ => ResultConversion::Value, + } +} + +fn validate_owned_outputs( + method: &MethodMeta, + params: &[ProjectedComParam], + context: &str, +) -> Result<(), String> { + for (index, param) in params.iter().enumerate() { + let Some(cleanup) = cleanup_for_param(method, index, context)? else { + continue; + }; + if !matches!( + param.direction, + ComParamDirection::Out | ComParamDirection::InOut + ) { + return Err(format!( + "{context}: ownership metadata applies to non-output parameter `{}`", + param.name + )); + } + if cleanup == CleanupKind::SysFreeString { + if param.direction != ComParamDirection::Out || param.typ != ComType::Bstr { + return Err(format!( + "{context}: SysFreeString ownership requires a scalar Out BSTR" + )); + } + } else if cleanup == CleanupKind::CoTaskMemFree { + if param.direction != ComParamDirection::Out + || !matches!( + param.typ, + ComType::RawPointer + | ComType::PointerAlias { + kind: super::ir::PointerAliasKind::DataPointer + | super::ir::PointerAliasKind::StringPointer, + .. + } + ) + { + return Err(format!( + "{context}: CoTaskMemFree ownership requires an Out data or string pointer" + )); + } + } + } + for owned in &method.owned_outputs { + if owned.param_index >= params.len() { + return Err(format!( + "{context}: ownership metadata references missing parameter index {}", + owned.param_index + )); + } + } + Ok(()) +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum CleanupKind { + SysFreeString, + CoTaskMemFree, +} + +fn cleanup_for_param( + method: &MethodMeta, + param_index: usize, + context: &str, +) -> Result, String> { + let contracts = method + .owned_outputs + .iter() + .filter(|owned| owned.param_index == param_index) + .map(|owned| owned.free_with.as_str()) + .collect::>(); + match contracts.as_slice() { + [] => Ok(None), + [contract] => parse_cleanup_contract(contract) + .map(Some) + .ok_or_else(|| format!("{context}: unsupported output cleanup contract `{contract}`")), + _ => Err(format!( + "{context}: output parameter index {param_index} has multiple cleanup contracts" + )), + } +} + +fn parse_cleanup_contract(contract: &str) -> Option { + if contract.is_empty() + || contract != contract.trim() + || contract + .chars() + .any(|character| character.is_whitespace() || matches!(character, ',' | ';')) + { + return None; + } + let identifier = contract + .rsplit_once(|character: char| matches!(character, '.' | '!' | ':')) + .map_or(contract, |(_, identifier)| identifier); + if identifier.is_empty() { + return None; + } + match identifier { + "SysFreeString" => Some(CleanupKind::SysFreeString), + "CoTaskMemFree" => Some(CleanupKind::CoTaskMemFree), + _ => None, + } +} + +fn project_string_buffer(method: &MethodMeta) -> Result, String> { + let buffers = method + .params + .iter() + .enumerate() + .filter_map(|(buffer_param_index, param)| { + let ParamDirection::OutStringBuffer { count_param_index } = param.direction else { + return None; + }; + Some((buffer_param_index, count_param_index, param)) + }) + .collect::>(); + if buffers.len() > 1 { + return Err(format!( + "{}: multiple caller-owned string buffers are not supported", + method.name + )); + } + let plan = + buffers + .into_iter() + .next() + .and_then(|(buffer_param_index, count_param_index, param)| { + let encoding = match pointer_alias_name(¶m.typ) { + Some("PWSTR") => StringEncoding::Wide, + Some("PSTR") => StringEncoding::Ansi, + _ => return None, + }; + method + .params + .get(count_param_index) + .filter(|count| count.direction == ParamDirection::In)?; + let optional_param_indices = (count_param_index..method.params.len()) + .filter(|index| string_buffer_param_is_optional(method, *index)) + .collect(); + Some(StringBufferPlan { + buffer_param_index, + count_param_index, + encoding, + optional_param_indices, + }) + }); + if plan + .as_ref() + .is_some_and(|plan| plan.encoding == StringEncoding::Ansi) + { + return Err(format!( + "{}: caller-owned ANSI output buffers are not yet decoded safely", + method.name + )); + } + Ok(plan) +} + +fn string_buffer_param_is_optional(method: &MethodMeta, param_index: usize) -> bool { + let Some((_, count_index)) = + method + .params + .iter() + .enumerate() + .find_map(|(buffer_index, param)| match param.direction { + ParamDirection::OutStringBuffer { count_param_index } => { + Some((buffer_index, count_param_index)) + } + _ => None, + }) + else { + return false; + }; + let Some(param) = method.params.get(param_index) else { + return false; + }; + let optional_shape = + param_index == count_index || (param_index > count_index && is_optional_find_data(param)); + optional_shape + && method + .params + .iter() + .skip(param_index + 1) + .filter(|param| param.direction.is_input()) + .all(is_optional_find_data) +} + +fn is_optional_find_data(param: &ParamMeta) -> bool { + if !matches!(param.direction, ParamDirection::In | ParamDirection::Out) { + return false; + } + let name = param.name.to_ascii_lowercase(); + name == "pfd" + || name.contains("finddata") + || name.contains("find_data") + || matches!( + ¶m.typ, + TypeMeta::Struct { name, .. } + if name == "WIN32_FIND_DATAW" || name == "WIN32_FIND_DATAA" + ) +} + +fn pointer_alias_name(typ: &TypeMeta) -> Option<&str> { + match typ { + TypeMeta::Struct { name, .. } => Some(name), + _ => None, + } +} + +fn dynamic_iid_natural_param_count(method: &MethodMeta) -> Option { + if !method + .return_type + .as_ref() + .is_some_and(|typ| matches!(project_type(typ), Ok(ComType::HResult))) + || method.params.len() < 2 + { + return None; + } + let output = method.params.last()?; + if output.direction != ParamDirection::Out || output.typ != TypeMeta::Object { + return None; + } + let iid = &method.params[method.params.len() - 2]; + let iid_name = iid.name.to_ascii_lowercase(); + if iid.direction != ParamDirection::In + || iid.typ != TypeMeta::Object + || !matches!(iid_name.as_str(), "iid" | "riid") + || method.params[..method.params.len() - 2] + .iter() + .any(|param| param.direction != ParamDirection::In) + { + return None; + } + Some(method.params.len() - 2) +} + +fn detect_interop_target( + meta: &ComInterfaceMeta, + winmd_paths: &str, +) -> Result, String> { + if !meta.interface.name.ends_with("Interop") + || !meta.interface.methods.iter().any(|method| { + method.name == "GetForWindow" && dynamic_iid_natural_param_count(method).is_some() + }) + { + return Ok(None); + } + let stripped_i = meta + .interface + .name + .strip_prefix('I') + .unwrap_or(&meta.interface.name); + let class_name = stripped_i + .strip_suffix("Interop") + .unwrap_or(stripped_i) + .to_string(); + let Some((namespace, _, iid)) = resolve_projected_default_iid(winmd_paths, &class_name) else { + return Err(format!( + "Classic-COM interop generator: cannot resolve default IID for the projected \ + WinRT runtime class `{class_name}` (derived from `{}`). \ + Neither the winmds passed to the generator ({winmd_paths:?}) nor the newest installed \ + `C:\\Program Files (x86)\\Windows Kits\\10\\UnionMetadata\\\\Windows.winmd` \ + contains a WinRT runtime class of that name with a resolvable default interface. \ + Pass the correct Windows.winmd via --ref or install a recent Windows SDK.", + meta.interface.name + )); + }; + Ok(Some((class_name, namespace, iid))) +} + +fn unsupported_error(unsupported: UnsupportedComType, context: &str) -> String { + match unsupported { + UnsupportedComType::Array => format!( + "{context}: native arrays require an explicit count and element-ownership projection; \ + raw-pointer fallback is not allowed" + ), + UnsupportedComType::ParameterizedInterface { namespace, name } => format!( + "{context}: parameterized interface `{namespace}.{name}` requires a computed closed IID \ + and managed ownership projection; raw-pointer fallback is not allowed" + ), + UnsupportedComType::AsyncInterface => format!( + "{context}: async interface requires a computed closed IID and managed ownership \ + projection; raw-pointer fallback is not allowed" + ), + UnsupportedComType::Delegate { namespace, name } => format!( + "{context}: delegate `{namespace}.{name}` requires a managed callback projection; \ + raw-pointer fallback is not allowed" + ), + UnsupportedComType::NativeStructLayout { namespace, name } => { + format!("{context}: struct `{namespace}.{name}` requires native layout projection") + } + UnsupportedComType::UnknownPointerAlias { namespace, name } => format!( + "{context}: pointer-shaped typedef `{namespace}.{name}` has no explicit semantic \ + classification; raw-pointer fallback is not allowed" + ), + UnsupportedComType::UnresolvedInterface { namespace, name } => format!( + "{context}: interface `{namespace}.{name}` has no resolvable IID; \ + pass the metadata that defines it via --ref instead of projecting it as a raw pointer" + ), + UnsupportedComType::UnresolvedRuntimeClass { namespace, name } => format!( + "{context}: runtime class `{namespace}.{name}` has no resolvable default interface; \ + pass the metadata that defines it via --ref" + ), + UnsupportedComType::UnknownOwnership { type_name } => { + format!("{context}: {type_name} has no ownership projection") + } + UnsupportedComType::UnsupportedDirectReturn { type_name } => { + format!("{context}: unsupported direct native return type {type_name}") + } + UnsupportedComType::Unknown => { + format!("{context}: unsupported Classic-COM type") + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::com_metadata::{InterfaceMeta, MethodMeta, ParamMeta}; + + fn interface(method: MethodMeta) -> ComInterfaceMeta { + ComInterfaceMeta { + interface: InterfaceMeta { + name: "ITest".into(), + namespace: "Tests".into(), + iid: "00000000-0000-0000-0000-000000000001".into(), + methods: vec![method], + ..Default::default() + }, + base_offset: 3, + is_iunknown_rooted: true, + base_chain: vec!["IUnknown".into()], + coclass_clsid: None, + coclass_name: None, + own_methods_start: 3, + referenced_enums: Vec::new(), + } + } + + #[test] + fn unsupported_type_fails_during_projection() { + let method = MethodMeta { + name: "Bad".into(), + params: vec![ParamMeta { + name: "values".into(), + typ: TypeMeta::Array(Box::new(TypeMeta::I32)), + direction: ParamDirection::In, + }], + ..Default::default() + }; + assert!( + project_com_interface(&interface(method), "") + .unwrap_err() + .contains("raw-pointer fallback is not allowed") + ); + } + + #[test] + fn by_value_guid_is_not_dynamic_iid() { + let method = MethodMeta { + name: "Get".into(), + params: vec![ + ParamMeta { + name: "riid".into(), + typ: TypeMeta::Guid, + direction: ParamDirection::In, + }, + ParamMeta { + name: "result".into(), + typ: TypeMeta::Object, + direction: ParamDirection::Out, + }, + ], + return_type: Some(TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HRESULT".into(), + fields: Vec::new(), + }), + ..Default::default() + }; + assert!( + project_com_interface(&interface(method), "") + .unwrap_err() + .contains("untyped pointer output") + ); + } + + #[test] + fn owned_output_without_a_known_cleanup_fails_before_rendering() { + let method = MethodMeta { + name: "GetName".into(), + params: vec![ParamMeta { + name: "name".into(), + typ: TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "BSTR".into(), + fields: Vec::new(), + }, + direction: ParamDirection::Out, + }], + return_type: Some(TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "HRESULT".into(), + fields: Vec::new(), + }), + ..Default::default() + }; + assert!( + project_com_interface(&interface(method), "") + .unwrap_err() + .contains("BSTR output has no ownership projection") + ); + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/com/project/types.rs b/tools/dynwinrt-codegen/src/codegen/com/project/types.rs new file mode 100644 index 00000000..45d27a77 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/com/project/types.rs @@ -0,0 +1,359 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::com_metadata::{is_native_isize, is_native_usize}; +use crate::types::TypeMeta; + +use super::super::ir::{ + ComEnumUnderlying, ComPrimitive, ComScalarRepr, ComType, PointerAliasKind, UnsupportedComType, +}; + +pub(in crate::codegen::com) fn project_type(typ: &TypeMeta) -> Result { + if is_native_isize(typ) { + return Ok(ComType::NativeIsize); + } + if is_native_usize(typ) { + return Ok(ComType::NativeUsize); + } + match typ { + TypeMeta::Bool => Ok(ComType::Primitive(ComPrimitive::Bool)), + TypeMeta::I8 => Ok(ComType::Primitive(ComPrimitive::I8)), + TypeMeta::U8 => Ok(ComType::Primitive(ComPrimitive::U8)), + TypeMeta::I16 => Ok(ComType::Primitive(ComPrimitive::I16)), + TypeMeta::U16 => Ok(ComType::Primitive(ComPrimitive::U16)), + TypeMeta::I32 => Ok(ComType::Primitive(ComPrimitive::I32)), + TypeMeta::U32 => Ok(ComType::Primitive(ComPrimitive::U32)), + TypeMeta::I64 => Ok(ComType::Primitive(ComPrimitive::I64)), + TypeMeta::U64 => Ok(ComType::Primitive(ComPrimitive::U64)), + TypeMeta::F32 => Ok(ComType::Primitive(ComPrimitive::F32)), + TypeMeta::F64 => Ok(ComType::Primitive(ComPrimitive::F64)), + TypeMeta::Char16 => Ok(ComType::Primitive(ComPrimitive::Char16)), + TypeMeta::String => Ok(ComType::HString), + TypeMeta::Guid => Ok(ComType::Guid), + TypeMeta::Object => Ok(ComType::RawPointer), + TypeMeta::Interface { + namespace, + name, + iid, + } if iid.is_empty() => Err(UnsupportedComType::UnresolvedInterface { + namespace: namespace.clone(), + name: name.clone(), + }), + TypeMeta::Interface { iid, .. } => Ok(ComType::ManagedInterface { iid: iid.clone() }), + TypeMeta::RuntimeClass { + default_interface: Some(default_interface), + .. + } => project_type(default_interface), + TypeMeta::RuntimeClass { + namespace, + name, + default_interface: None, + } => Err(UnsupportedComType::UnresolvedRuntimeClass { + namespace: namespace.clone(), + name: name.clone(), + }), + TypeMeta::Delegate { + namespace, name, .. + } => Err(UnsupportedComType::Delegate { + namespace: namespace.clone(), + name: name.clone(), + }), + TypeMeta::Parameterized { + namespace, name, .. + } => Err(UnsupportedComType::ParameterizedInterface { + namespace: namespace.clone(), + name: name.clone(), + }), + TypeMeta::AsyncAction + | TypeMeta::AsyncActionWithProgress(_) + | TypeMeta::AsyncOperation(_) + | TypeMeta::AsyncOperationWithProgress(_, _) => Err(UnsupportedComType::AsyncInterface), + TypeMeta::Array(_) => Err(UnsupportedComType::Array), + TypeMeta::Enum { + name, underlying, .. + } => Ok(ComType::Enum { + name: name.clone(), + underlying: project_enum_underlying(underlying)?, + }), + TypeMeta::Struct { + namespace, name, .. + } if namespace == "Windows.Win32.Foundation" && name == "BOOL" => Ok(ComType::Win32Bool), + TypeMeta::Struct { + namespace, name, .. + } if namespace == "Windows.Win32.Foundation" && name == "HRESULT" => Ok(ComType::HResult), + TypeMeta::Struct { + namespace, name, .. + } if namespace == "Windows.Win32.Foundation" && name == "BSTR" => Ok(ComType::Bstr), + TypeMeta::Struct { + namespace, + name, + fields, + } if namespace.starts_with("Windows.Win32.") + && fields.len() == 1 + && fields[0].name == "Value" => + { + if let Some(underlying) = scalar_alias_underlying(name, &fields[0].typ) { + Ok(ComType::ScalarAlias { + name: name.clone(), + underlying, + }) + } else if matches!(fields[0].typ, TypeMeta::Object) { + classify_pointer_alias(name) + .map(|kind| ComType::PointerAlias { + name: name.clone(), + kind, + }) + .ok_or_else(|| UnsupportedComType::UnknownPointerAlias { + namespace: namespace.clone(), + name: name.clone(), + }) + } else { + Err(UnsupportedComType::NativeStructLayout { + namespace: namespace.clone(), + name: name.clone(), + }) + } + } + TypeMeta::Struct { + namespace, name, .. + } => Err(UnsupportedComType::NativeStructLayout { + namespace: namespace.clone(), + name: name.clone(), + }), + } +} + +pub(super) fn project_enum_underlying( + typ: &TypeMeta, +) -> Result { + match typ { + TypeMeta::I8 => Ok(ComEnumUnderlying::I8), + TypeMeta::U8 => Ok(ComEnumUnderlying::U8), + TypeMeta::I16 => Ok(ComEnumUnderlying::I16), + TypeMeta::U16 => Ok(ComEnumUnderlying::U16), + TypeMeta::I32 => Ok(ComEnumUnderlying::I32), + TypeMeta::U32 => Ok(ComEnumUnderlying::U32), + TypeMeta::I64 => Ok(ComEnumUnderlying::I64), + TypeMeta::U64 => Ok(ComEnumUnderlying::U64), + _ => Err(UnsupportedComType::Unknown), + } +} + +fn scalar_alias_underlying(name: &str, typ: &TypeMeta) -> Option { + match name { + "LPARAM" | "LRESULT" => return Some(ComScalarRepr::NativeIsize), + "WPARAM" => return Some(ComScalarRepr::NativeUsize), + _ => {} + } + let primitive = match typ { + TypeMeta::Bool => ComPrimitive::Bool, + TypeMeta::I8 => ComPrimitive::I8, + TypeMeta::U8 => ComPrimitive::U8, + TypeMeta::I16 => ComPrimitive::I16, + TypeMeta::U16 => ComPrimitive::U16, + TypeMeta::I32 => ComPrimitive::I32, + TypeMeta::U32 => ComPrimitive::U32, + TypeMeta::I64 => ComPrimitive::I64, + TypeMeta::U64 => ComPrimitive::U64, + TypeMeta::F32 => ComPrimitive::F32, + TypeMeta::F64 => ComPrimitive::F64, + TypeMeta::Char16 => ComPrimitive::Char16, + _ => return None, + }; + Some(ComScalarRepr::Primitive(primitive)) +} + +fn classify_pointer_alias(name: &str) -> Option { + if matches!( + name, + "PWSTR" + | "PCWSTR" + | "PSTR" + | "PCSTR" + | "LPWSTR" + | "LPCWSTR" + | "LPSTR" + | "LPCSTR" + | "PWCHAR" + | "PCWCHAR" + | "LPWCH" + | "LPCWCH" + | "LPCH" + | "LPCCH" + ) { + Some(PointerAliasKind::StringPointer) + } else if matches!( + name, + "PSID" + | "PSECURITY_DESCRIPTOR" + | "MEMORY_MAPPED_VIEW_ADDRESS" + | "LPPROC_THREAD_ATTRIBUTE_LIST" + | "PVOID" + | "PCVOID" + | "LPVOID" + | "LPCVOID" + ) { + Some(PointerAliasKind::DataPointer) + } else if is_known_handle_alias(name) { + Some(PointerAliasKind::HandleValue) + } else { + None + } +} + +fn is_known_handle_alias(name: &str) -> bool { + matches!( + name, + "HANDLE" + | "HWND" + | "HACCEL" + | "HBITMAP" + | "HBRUSH" + | "HCURSOR" + | "HDC" + | "HDESK" + | "HDWP" + | "HENHMETAFILE" + | "HFILE" + | "HFONT" + | "HGDIOBJ" + | "HGLOBAL" + | "HHOOK" + | "HICON" + | "HIMAGELIST" + | "HINSTANCE" + | "HKEY" + | "HKL" + | "HLOCAL" + | "HMENU" + | "HMETAFILE" + | "HMODULE" + | "HMONITOR" + | "HPALETTE" + | "HPEN" + | "HRAWINPUT" + | "HRGN" + | "HRSRC" + | "HTHEME" + | "HWINSTA" + | "SC_HANDLE" + | "SERVICE_STATUS_HANDLE" + | "DPI_AWARENESS_CONTEXT" + ) +} + +pub(super) fn is_scalar_in_out(typ: &ComType) -> bool { + matches!( + typ, + ComType::Primitive(_) + | ComType::NativeIsize + | ComType::NativeUsize + | ComType::Win32Bool + | ComType::HResult + | ComType::Enum { .. } + | ComType::ScalarAlias { .. } + | ComType::PointerAlias { .. } + ) +} + +pub(super) fn is_supported_direct_return(typ: &ComType) -> bool { + is_scalar_in_out(typ) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn every_supported_type_category_projects_without_fallback() { + let types = [ + TypeMeta::Bool, + TypeMeta::I8, + TypeMeta::U8, + TypeMeta::I16, + TypeMeta::U16, + TypeMeta::I32, + TypeMeta::U32, + TypeMeta::I64, + TypeMeta::U64, + TypeMeta::F32, + TypeMeta::F64, + TypeMeta::Char16, + TypeMeta::String, + TypeMeta::Guid, + TypeMeta::Object, + ]; + for typ in types { + assert!(project_type(&typ).is_ok(), "{typ:?}"); + } + } + + #[test] + fn reference_like_types_never_degrade_to_raw_pointer() { + let parameterized = TypeMeta::Parameterized { + namespace: "Windows.Foundation.Collections".into(), + name: "IVector".into(), + piid: String::new(), + args: vec![TypeMeta::I32], + }; + assert!(matches!( + project_type(¶meterized), + Err(UnsupportedComType::ParameterizedInterface { .. }) + )); + assert!(matches!( + project_type(&TypeMeta::AsyncAction), + Err(UnsupportedComType::AsyncInterface) + )); + } + + #[test] + fn transparent_scalar_typedefs_preserve_scalar_abi() { + let colorref = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "COLORREF".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::U32, + }], + }; + assert_eq!( + project_type(&colorref), + Ok(ComType::ScalarAlias { + name: "COLORREF".into(), + underlying: ComScalarRepr::Primitive(ComPrimitive::U32), + }) + ); + + let lparam = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "LPARAM".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + assert_eq!( + project_type(&lparam), + Ok(ComType::ScalarAlias { + name: "LPARAM".into(), + underlying: ComScalarRepr::NativeIsize, + }) + ); + } + + #[test] + fn unknown_pointer_shaped_typedef_fails_closed() { + let unknown = TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "MYSTERY_POINTER".into(), + fields: vec![crate::types::FieldMeta { + name: "Value".into(), + typ: TypeMeta::Object, + }], + }; + assert!(matches!( + project_type(&unknown), + Err(UnsupportedComType::UnknownPointerAlias { .. }) + )); + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/common.rs b/tools/dynwinrt-codegen/src/codegen/common.rs index 5fccc4e2..0aba4e6b 100644 --- a/tools/dynwinrt-codegen/src/codegen/common.rs +++ b/tools/dynwinrt-codegen/src/codegen/common.rs @@ -10,13 +10,13 @@ pub use super::python::naming::to_snake_case_filename; #[cfg(test)] mod tests { - use crate::codegen::javascript::naming::*; - use crate::codegen::javascript::signature::*; - use crate::codegen::javascript::structs::*; - use crate::codegen::python::naming::*; - use crate::codegen::python::signature::*; - use crate::codegen::python::structs::*; - use crate::codegen::shared::imports::*; + use crate::codegen::winrt::javascript::naming::*; + use crate::codegen::winrt::javascript::signature::*; + use crate::codegen::winrt::javascript::structs::*; + use crate::codegen::winrt::python::naming::*; + use crate::codegen::winrt::python::signature::*; + use crate::codegen::winrt::python::structs::*; + use crate::codegen::winrt::shared::imports::*; use crate::meta::{MethodMeta, ParamDirection, ParamMeta}; use crate::types::TypeMeta; use std::collections::HashSet; diff --git a/tools/dynwinrt-codegen/src/codegen/mod.rs b/tools/dynwinrt-codegen/src/codegen/mod.rs index b70c5a6a..3a0893e4 100644 --- a/tools/dynwinrt-codegen/src/codegen/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/mod.rs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +pub mod com; pub mod common; -pub mod javascript; -pub mod python; -pub(crate) mod shared; +pub mod winrt; + +// Preserve the existing public module paths while callers migrate to +// `codegen::winrt::{javascript, python}`. +pub use winrt::{javascript, python}; // Preserve the existing public API while the implementations live under // language-specific modules. diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/docs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/docs.rs similarity index 98% rename from tools/dynwinrt-codegen/src/codegen/javascript/docs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/docs.rs index 02873d28..1aec2e17 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/docs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/docs.rs @@ -3,7 +3,7 @@ //! JSDoc rendering for generated JavaScript declarations. -use crate::codegen::shared::docs::DocText; +use crate::codegen::winrt::shared::docs::DocText; /// Escape `*/` sequences so a JSDoc block comment cannot terminate early. fn escape_jsdoc(s: &str) -> String { diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/generator.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/generator.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/generator.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/generator.rs index 240ec4b9..86012582 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/generator.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/generator.rs @@ -6,7 +6,7 @@ use std::collections::HashSet; use crate::meta::{ClassMeta, InterfaceMeta}; use crate::types::TypeMeta; -use crate::codegen::shared::structs::{ +use crate::codegen::winrt::shared::structs::{ collect_used_structs_from_class, collect_used_structs_from_iface, }; diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/ir.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/ir.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/ir.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/ir.rs diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/method.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/method.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/method.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/method.rs index 55dc2b08..78fa48d1 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/method.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/method.rs @@ -3,7 +3,7 @@ use std::collections::HashSet; -use crate::codegen::shared::imports::ireference_inner_type; +use crate::codegen::winrt::shared::imports::ireference_inner_type; use crate::types::TypeMeta; // ====================================================================== diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/mod.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/mod.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/mod.rs diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/naming.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/naming.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/naming.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/naming.rs diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/project/collections.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/collections.rs similarity index 98% rename from tools/dynwinrt-codegen/src/codegen/javascript/project/collections.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/collections.rs index 9a97ac61..08dcb567 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/project/collections.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/collections.rs @@ -621,7 +621,7 @@ pub(super) fn project_collection_create( is_static: true, invoke_expr: String::new(), sync_return_expr: Some(format!( - "(() => {{ const value = DynWinRtValue.createVector(items.map(i => _unwrap(i)), {elem_type}); const observable = new {observable}(value); const vector = new ((__load_{vector}()).{vector})(value); Object.defineProperties(vector, {{ onVectorChanged: {{ value: observable.onVectorChanged.bind(observable) }}, onceVectorChanged: {{ value: observable.onceVectorChanged.bind(observable) }}, offVectorChanged: {{ value: observable.offVectorChanged.bind(observable) }} }}); return vector; }})()", + "(() => {{ const value = DynWinRtValue.createVector(items.map(i => _unwrap(i)), {elem_type}); const observable = new {observable}(value); const vector = new ((__load_{vector}()).{vector})(value); Object.defineProperties(vector, {{ asVector: {{ value: observable.asVector.bind(observable) }}, onVectorChanged: {{ value: observable.onVectorChanged.bind(observable) }}, onceVectorChanged: {{ value: observable.onceVectorChanged.bind(observable) }}, offVectorChanged: {{ value: observable.offVectorChanged.bind(observable) }} }}); return vector; }})()", observable = iface.name, vector = vector_name, )), diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/project/constructors.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/constructors.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/project/constructors.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/constructors.rs index bf211f71..ceeb28bd 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/project/constructors.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/constructors.rs @@ -9,7 +9,7 @@ use crate::meta::{ConstructorKind, ConstructorMeta, InterfaceMeta, MethodMeta, P use crate::types::TypeMeta; use super::*; -use crate::codegen::javascript::signature::ref_marker; +use crate::codegen::winrt::javascript::signature::ref_marker; struct ConstructorCandidate { params: Vec, diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/project/methods.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/methods.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/project/methods.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/methods.rs diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/project/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/mod.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/project/mod.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/mod.rs index 03868b19..95a18bee 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/project/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/mod.rs @@ -34,12 +34,12 @@ pub fn get_import_name() -> String { RUNTIME_IMPORT_NAME.with(|n| n.borrow().clone()) } -use crate::codegen::shared::imports::{ +use crate::codegen::winrt::shared::imports::{ NO_DEFERRED, collect_iface_type_imports, collect_type_imports, collect_used_generics_from_class, collect_used_generics_from_methods, fill_array_output_index, fill_array_uses_retval_count, get_in_params, ireference_inner_type, method_abi_output_count, }; -use crate::codegen::shared::structs::{ +use crate::codegen::winrt::shared::structs::{ collect_used_structs_from_class, collect_used_structs_from_iface, }; @@ -1531,7 +1531,7 @@ fn build_method_doc(method: &MethodMeta, in_params: &[&crate::meta::ParamMeta]) let params_display: Vec<(String, String)> = in_params .iter() .filter_map(|p| { - crate::codegen::shared::docs::find_param_doc(&method.param_docs, &p.name) + crate::codegen::winrt::shared::docs::find_param_doc(&method.param_docs, &p.name) .map(|d| (to_camel_case(&p.name), d.to_string())) }) .collect(); diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/project/structs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/structs.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/project/structs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/project/structs.rs diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/render/declarations.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/declarations.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/render/declarations.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/declarations.rs index 9c49225b..27edbcee 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/render/declarations.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/declarations.rs @@ -11,7 +11,7 @@ //! - Emits TSDoc comments //! - Enums as `export enum` (not `Object.freeze`) -use crate::codegen::javascript::ir::*; +use crate::codegen::winrt::javascript::ir::*; /// Render a projected file as a `.d.ts` declaration. pub fn render(file: &ProjectedFile) -> String { @@ -577,7 +577,7 @@ fn render_enum_dts(out: &mut String, en: &ProjectedEnum) { // ====================================================================== fn render_tsdoc(doc: &DocInfo, indent: &str) -> String { - let doc_text = crate::codegen::shared::docs::DocText { + let doc_text = crate::codegen::winrt::shared::docs::DocText { summary: doc.summary.as_deref(), deprecated: doc.deprecated.as_deref(), returns: doc.returns.as_deref(), @@ -587,7 +587,7 @@ fn render_tsdoc(doc: &DocInfo, indent: &str) -> String { .map(|(n, d)| (n.as_str(), d.as_str())) .collect(), }; - crate::codegen::javascript::docs::format_jsdoc(&doc_text, indent) + crate::codegen::winrt::javascript::docs::format_jsdoc(&doc_text, indent) } /// Check if any method in the file uses AsyncWithProgress. diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/commonjs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/commonjs.rs similarity index 98% rename from tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/commonjs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/commonjs.rs index 11d30d78..44750772 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/commonjs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/commonjs.rs @@ -374,15 +374,15 @@ fn extract_export_decl_name(line: &str) -> Option { /// import classification of the surrounding file. fn resolve_ref_markers(body: &str, lazy_symbols: &std::collections::HashSet) -> String { // Fast path: no markers present. - if !body.contains(crate::codegen::javascript::signature::REF_MARKER_PREFIX) { + if !body.contains(crate::codegen::winrt::javascript::signature::REF_MARKER_PREFIX) { return body.to_string(); } let mut out = String::with_capacity(body.len()); let mut cursor = 0usize; let bytes = body.as_bytes(); - let prefix = crate::codegen::javascript::signature::REF_MARKER_PREFIX; - let suffix = crate::codegen::javascript::signature::REF_MARKER_SUFFIX; + let prefix = crate::codegen::winrt::javascript::signature::REF_MARKER_PREFIX; + let suffix = crate::codegen::winrt::javascript::signature::REF_MARKER_SUFFIX; while let Some(rel_start) = body[cursor..].find(prefix) { let start = cursor + rel_start; diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/helpers.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/helpers.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/helpers.rs index 9b0c8d3e..349a0313 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/helpers.rs @@ -3,7 +3,7 @@ //! Member, overload, and asynchronous JavaScript emitters. -use crate::codegen::javascript::ir::*; +use crate::codegen::winrt::javascript::ir::*; // ====================================================================== // Async scaffolding @@ -84,7 +84,7 @@ pub(super) fn emit_with_progress_body( // ====================================================================== pub(super) fn render_jsdoc(doc: &DocInfo, indent: &str) -> String { - let doc_text = crate::codegen::shared::docs::DocText { + let doc_text = crate::codegen::winrt::shared::docs::DocText { summary: doc.summary.as_deref(), deprecated: doc.deprecated.as_deref(), returns: doc.returns.as_deref(), @@ -94,7 +94,7 @@ pub(super) fn render_jsdoc(doc: &DocInfo, indent: &str) -> String { .map(|(n, d)| (n.as_str(), d.as_str())) .collect(), }; - crate::codegen::javascript::docs::format_jsdoc(&doc_text, indent) + crate::codegen::winrt::javascript::docs::format_jsdoc(&doc_text, indent) } pub(super) fn inject_unwrap(code: String) -> String { diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/mod.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/mod.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/mod.rs index 81ebf20a..d14e0a1f 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/render/javascript/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/javascript/mod.rs @@ -10,8 +10,8 @@ mod commonjs; mod helpers; -use crate::codegen::javascript::ir::*; -use crate::codegen::javascript::signature::ref_marker; +use crate::codegen::winrt::javascript::ir::*; +use crate::codegen::winrt::javascript::signature::ref_marker; use commonjs::convert_to_cjs_with_lazy; use helpers::{ @@ -222,7 +222,7 @@ fn render_class_js(out: &mut String, class: &ProjectedClass) { .filter_map(|m| match m { ProjectedMember::Method(method) => Some((method.name.clone(), method.params.len())), ProjectedMember::Symbol(s) => Some(( - crate::codegen::javascript::project::symbol_dedup_key(&s.kind), + crate::codegen::winrt::javascript::project::symbol_dedup_key(&s.kind), 0, )), ProjectedMember::Close => Some(("close".into(), 0)), diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/render/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/mod.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/render/mod.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/mod.rs diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/render/package_json.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/package_json.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/render/package_json.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/render/package_json.rs diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/signature.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/signature.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/javascript/signature.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/signature.rs index e18be78a..7cf80e92 100644 --- a/tools/dynwinrt-codegen/src/codegen/javascript/signature.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/signature.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; -use crate::codegen::shared::imports::ireference_inner_type; +use crate::codegen::winrt::shared::imports::ireference_inner_type; use crate::meta::{InterfaceMeta, MethodMeta, ParamDirection}; use crate::types::TypeMeta; diff --git a/tools/dynwinrt-codegen/src/codegen/javascript/structs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/javascript/structs.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/javascript/structs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/javascript/structs.rs diff --git a/tools/dynwinrt-codegen/src/codegen/winrt/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/mod.rs new file mode 100644 index 00000000..91de89cb --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/winrt/mod.rs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Windows Runtime code generation. + +pub mod javascript; +pub mod python; +pub(crate) mod shared; diff --git a/tools/dynwinrt-codegen/src/codegen/python/collections.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/collections.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/collections.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/collections.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/docs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/docs.rs similarity index 98% rename from tools/dynwinrt-codegen/src/codegen/python/docs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/docs.rs index ce75610e..fcc2bb06 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/docs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/docs.rs @@ -3,7 +3,7 @@ //! Docstring rendering for generated Python bindings. -use crate::codegen::shared::docs::DocText; +use crate::codegen::winrt::shared::docs::DocText; /// Escape `"""` so a Python triple-quoted string cannot terminate early. fn escape_pydoc(s: &str) -> String { diff --git a/tools/dynwinrt-codegen/src/codegen/python/generator/class.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs similarity index 95% rename from tools/dynwinrt-codegen/src/codegen/python/generator/class.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs index 8cb7f2b9..5c9245c8 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/generator/class.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/class.rs @@ -6,7 +6,7 @@ use super::imports::{emit_type_checking_imports, format_py_type_import}; use super::structs::generate_struct_helpers; use super::*; -use crate::codegen::python::collections::{ +use crate::codegen::winrt::python::collections::{ CollectionKind, class_interface, interface_kind, map_iterable_name, runtime_mixin, }; @@ -222,12 +222,14 @@ pub fn generate_class( out.push_str(&format!("\nclass {}:\n", class.name)); } { - let doc = crate::codegen::shared::docs::DocText { + let doc = crate::codegen::winrt::shared::docs::DocText { summary: class.doc.as_deref(), deprecated: class.deprecated.as_deref(), ..Default::default() }; - out.push_str(&crate::codegen::python::docs::format_pydoc(&doc, " ")); + out.push_str(&crate::codegen::winrt::python::docs::format_pydoc( + &doc, " ", + )); } out.push_str(&generate_python_constructor( @@ -303,7 +305,7 @@ pub fn generate_class( ) .collect::>(); let static_method_names = - crate::codegen::python::overloads::method_names(static_methods.iter().copied()); + crate::codegen::winrt::python::overloads::method_names(static_methods.iter().copied()); let mut static_groups: Vec<(String, Vec>)> = Vec::new(); for (kind, interfaces) in [ (StaticOverloadKind::Factory, &class.factory_interfaces), @@ -311,7 +313,7 @@ pub fn generate_class( ] { for iface in interfaces { for method in &iface.methods { - let mut key = crate::codegen::python::overloads::method_group_key( + let mut key = crate::codegen::winrt::python::overloads::method_group_key( method, &static_method_names, ); @@ -355,7 +357,7 @@ pub fn generate_class( .chain(class.required_interfaces.iter()) .filter(|iface| iface.iid != "30d5a829-7fa4-4026-83bb-d75bae4ea99e") .collect::>(); - let instance_method_names = crate::codegen::python::overloads::method_names( + let instance_method_names = crate::codegen::winrt::python::overloads::method_names( instance_ifaces .iter() .flat_map(|iface| iface.methods.iter()), @@ -378,8 +380,10 @@ pub fn generate_class( obj_expr.to_string() }; for method in reorder_getters_before_setters(&iface.methods) { - let key = - crate::codegen::python::overloads::method_group_key(method, &instance_method_names); + let key = crate::codegen::winrt::python::overloads::method_group_key( + method, + &instance_method_names, + ); let overload = InstanceOverload { iface_var: format!("_{}", iface.name), obj_expr: obj_expr.clone(), @@ -570,7 +574,7 @@ fn generate_python_constructor( .flat_map(|iface| iface.methods.iter()) .collect::>(); let factory_names = - crate::codegen::python::overloads::method_names(factory_methods.iter().copied()); + crate::codegen::winrt::python::overloads::method_names(factory_methods.iter().copied()); let has_create_factory = factory_methods.iter().any(|method| { let name = to_snake_case(&method.name); name == "create" || name.starts_with("create") @@ -585,7 +589,7 @@ fn generate_python_constructor( )); } for method in factory_methods { - let in_params = crate::codegen::shared::imports::get_in_params(method); + let in_params = crate::codegen::winrt::shared::imports::get_in_params(method); let parameter_names = in_params .iter() .map(|param| format!("'{}'", to_snake_case(¶m.name))) @@ -597,7 +601,7 @@ fn generate_python_constructor( format!("({parameter_names},)") }; let public_name = - crate::codegen::python::overloads::method_group_key(method, &factory_names); + crate::codegen::winrt::python::overloads::method_group_key(method, &factory_names); let overload_count = factory_methods_for_name(class, &factory_names, &public_name); let call_name = if overload_count > 1 { format!("_{public_name}_{}", method.vtable_index) @@ -647,7 +651,7 @@ fn factory_methods_for_name( .iter() .flat_map(|iface| iface.methods.iter()) .filter(|method| { - crate::codegen::python::overloads::method_group_key(method, names) == public_name + crate::codegen::winrt::python::overloads::method_group_key(method, names) == public_name }) .count() } diff --git a/tools/dynwinrt-codegen/src/codegen/python/generator/imports.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/imports.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/generator/imports.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/generator/imports.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/generator/index.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/index.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/generator/index.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/generator/index.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/generator/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs similarity index 97% rename from tools/dynwinrt-codegen/src/codegen/python/generator/mod.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs index ef09aef5..0649a78a 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/generator/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/mod.rs @@ -12,11 +12,11 @@ use std::collections::HashSet; use crate::meta::{ClassMeta, InterfaceMeta, MethodMeta, ParamDirection}; use crate::types::{TypeKind, TypeMeta}; -use crate::codegen::shared::imports::{ +use crate::codegen::winrt::shared::imports::{ collect_iface_type_imports, collect_type_imports, collect_used_generics_from_class, collect_used_generics_from_methods, ireference_inner_type, }; -use crate::codegen::shared::structs::{ +use crate::codegen::winrt::shared::structs::{ collect_used_structs_from_class, collect_used_structs_from_iface, }; diff --git a/tools/dynwinrt-codegen/src/codegen/python/generator/structs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs similarity index 98% rename from tools/dynwinrt-codegen/src/codegen/python/generator/structs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs index b00cecca..ac9792f3 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/generator/structs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/structs.rs @@ -4,7 +4,7 @@ //! Python struct projection helpers. use super::*; -use crate::codegen::python::native_types::{FoundationType, foundation_type}; +use crate::codegen::winrt::python::native_types::{FoundationType, foundation_type}; // ====================================================================== // Struct helpers: Python dataclass-style + _unpack/_pack functions diff --git a/tools/dynwinrt-codegen/src/codegen/python/generator/types.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs similarity index 94% rename from tools/dynwinrt-codegen/src/codegen/python/generator/types.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs index 8cbb41b7..5a28c1bf 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/generator/types.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/generator/types.rs @@ -6,7 +6,7 @@ use super::imports::{emit_type_checking_imports, format_py_type_import}; use super::structs::generate_struct_helpers; use super::*; -use crate::codegen::python::collections::{ +use crate::codegen::winrt::python::collections::{ CollectionKind, interface_kind, map_iterable_name, runtime_mixin, }; @@ -35,13 +35,13 @@ pub fn generate_enum(en: &TypeMeta) -> Option { let enum_base = if is_flags { "IntFlag" } else { "IntEnum" }; out.push_str(&format!("from enum import {enum_base}\n\n\n")); out.push_str(&format!("class {}({enum_base}):\n", name)); - let type_doc = crate::codegen::shared::docs::DocText { + let type_doc = crate::codegen::winrt::shared::docs::DocText { summary: enum_doc, deprecated: enum_dep, returns: None, params: Vec::new(), }; - let type_ds = crate::codegen::python::docs::format_pydoc(&type_doc, " "); + let type_ds = crate::codegen::winrt::python::docs::format_pydoc(&type_doc, " "); if !type_ds.is_empty() { out.push_str(&type_ds); out.push('\n'); @@ -190,12 +190,14 @@ pub fn generate_interface( out.push_str(&format!("\nclass {}:\n", iface.name)); } { - let doc = crate::codegen::shared::docs::DocText { + let doc = crate::codegen::winrt::shared::docs::DocText { summary: iface.doc.as_deref(), deprecated: iface.deprecated.as_deref(), ..Default::default() }; - out.push_str(&crate::codegen::python::docs::format_pydoc(&doc, " ")); + out.push_str(&crate::codegen::winrt::python::docs::format_pydoc( + &doc, " ", + )); } out.push_str(" def __init__(self, obj: DynWinRTValue):\n"); if iface.generic_piid.is_some() { @@ -226,7 +228,7 @@ pub fn generate_interface( if let Some(ref piid) = iface.generic_piid { if piid == "913337e9-11a1-4345-a3a2-4e7f956e222d" && iface.generic_args.len() == 1 { let elem_type = py_dynwinrt_type(&iface.generic_args[0]); - let elem_annotation = crate::codegen::python::type_helpers::py_return_type_safe( + let elem_annotation = crate::codegen::winrt::python::type_helpers::py_return_type_safe( Some(&iface.generic_args[0]), known_types, ); @@ -244,11 +246,11 @@ pub fn generate_interface( } else if piid == "3c2925fe-8519-45c1-aa79-197b6718c1c1" && iface.generic_args.len() == 2 { let key_type = py_dynwinrt_type(&iface.generic_args[0]); let val_type = py_dynwinrt_type(&iface.generic_args[1]); - let key_annotation = crate::codegen::python::type_helpers::py_return_type_safe( + let key_annotation = crate::codegen::winrt::python::type_helpers::py_return_type_safe( Some(&iface.generic_args[0]), known_types, ); - let val_annotation = crate::codegen::python::type_helpers::py_return_type_safe( + let val_annotation = crate::codegen::winrt::python::type_helpers::py_return_type_safe( Some(&iface.generic_args[1]), known_types, ); @@ -281,7 +283,7 @@ pub fn generate_interface( // Instance methods (reorder so @property comes before @x.setter) let iface_var = format!("_{}", iface.name); - for methods in crate::codegen::python::overloads::grouped_methods( + for methods in crate::codegen::winrt::python::overloads::grouped_methods( reorder_getters_before_setters(&iface.methods), ) { out.push('\n'); diff --git a/tools/dynwinrt-codegen/src/codegen/python/method.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/python/method.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs index 70ce49b1..4c7936cd 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/method.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/method.rs @@ -6,7 +6,7 @@ use std::collections::HashSet; use crate::meta::{ClassMeta, InterfaceMeta, MethodMeta}; use crate::types::TypeMeta; -use crate::codegen::shared::imports::{ +use crate::codegen::winrt::shared::imports::{ fill_array_output_index, fill_array_uses_retval_count, get_in_params, }; diff --git a/tools/dynwinrt-codegen/src/codegen/python/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/mod.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/mod.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/naming.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/naming.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/naming.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/native_types.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/native_types.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/native_types.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/native_types.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/overloads.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/overloads.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/overloads.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/overloads.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/shared.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/shared.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/shared.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/shared.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/signature.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/python/signature.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs index 97a080d6..e7405d11 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/signature.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/signature.rs @@ -9,9 +9,9 @@ use crate::meta::{InterfaceMeta, MethodMeta, ParamDirection}; use crate::types::TypeMeta; use super::naming::{to_snake_case, to_snake_case_filename}; -use crate::codegen::python::collections::{CollectionKind, is_mapping_input, type_kind}; -use crate::codegen::python::native_types::{FoundationType, foundation_type}; -use crate::codegen::shared::imports::ireference_inner_type; +use crate::codegen::winrt::python::collections::{CollectionKind, is_mapping_input, type_kind}; +use crate::codegen::winrt::python::native_types::{FoundationType, foundation_type}; +use crate::codegen::winrt::shared::imports::ireference_inner_type; pub(crate) fn py_runtime_symbol(type_name: &str, symbol_name: &str) -> String { format!( diff --git a/tools/dynwinrt-codegen/src/codegen/python/structs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/structs.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/python/structs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/structs.rs diff --git a/tools/dynwinrt-codegen/src/codegen/python/stub_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/python/stub_helpers.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs index 03dc721b..062c59be 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/stub_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stub_helpers.rs @@ -5,7 +5,7 @@ use std::collections::HashSet; -use crate::codegen::shared::imports::get_in_params; +use crate::codegen::winrt::shared::imports::get_in_params; use crate::meta::MethodMeta; use crate::types::{TypeKind, TypeMeta}; diff --git a/tools/dynwinrt-codegen/src/codegen/python/stubs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs similarity index 99% rename from tools/dynwinrt-codegen/src/codegen/python/stubs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs index a9d8676c..b705c8ba 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/stubs.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/stubs.rs @@ -11,11 +11,11 @@ use std::collections::HashSet; use crate::meta::{ClassMeta, InterfaceMeta, MethodMeta}; use crate::types::{TypeKind, TypeMeta}; -use crate::codegen::shared::imports::{ +use crate::codegen::winrt::shared::imports::{ collect_iface_type_imports, collect_type_imports, collect_used_generics_from_class, collect_used_generics_from_methods, }; -use crate::codegen::shared::structs::{ +use crate::codegen::winrt::shared::structs::{ collect_used_structs_from_class, collect_used_structs_from_iface, }; @@ -611,7 +611,7 @@ fn emit_constructor_stubs( if count > 1 { out.push_str(" @overload\n"); } - let in_params = crate::codegen::shared::imports::get_in_params(method); + let in_params = crate::codegen::winrt::shared::imports::get_in_params(method); let params = super::type_helpers::py_param_list(&in_params, known_types, delegate_type_names); if params.is_empty() { diff --git a/tools/dynwinrt-codegen/src/codegen/python/type_helpers.rs b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs similarity index 98% rename from tools/dynwinrt-codegen/src/codegen/python/type_helpers.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs index ffedcd14..8b6c5f56 100644 --- a/tools/dynwinrt-codegen/src/codegen/python/type_helpers.rs +++ b/tools/dynwinrt-codegen/src/codegen/winrt/python/type_helpers.rs @@ -5,8 +5,8 @@ use std::collections::HashSet; -use crate::codegen::shared::docs::{DocText, find_param_doc}; -use crate::codegen::shared::imports::{ +use crate::codegen::winrt::shared::docs::{DocText, find_param_doc}; +use crate::codegen::winrt::shared::imports::{ fill_array_uses_retval_count, ireference_inner_type, method_abi_output_count, }; use crate::meta::MethodMeta; @@ -525,11 +525,11 @@ mod tests { "list[str]" ); assert_eq!( - crate::codegen::python::signature::py_build_method_sig(&method), + crate::codegen::winrt::python::signature::py_build_method_sig(&method), "DynWinRTMethodSig().add_in(DynWinRTType.u32_type()).add_out_fill(DynWinRTType.array_type(DynWinRTType.hstring())).add_out(DynWinRTType.u32_type())" ); assert_eq!( - crate::codegen::javascript::signature::build_method_sig(&method), + crate::codegen::winrt::javascript::signature::build_method_sig(&method), "new DynWinRtMethodSig().addIn(DynWinRtType.u32()).addOutFill(DynWinRtType.arrayType(DynWinRtType.hstring())).addOut(DynWinRtType.u32())" ); } diff --git a/tools/dynwinrt-codegen/src/codegen/shared/docs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/shared/docs.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/shared/docs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/shared/docs.rs diff --git a/tools/dynwinrt-codegen/src/codegen/shared/imports.rs b/tools/dynwinrt-codegen/src/codegen/winrt/shared/imports.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/shared/imports.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/shared/imports.rs diff --git a/tools/dynwinrt-codegen/src/codegen/shared/mod.rs b/tools/dynwinrt-codegen/src/codegen/winrt/shared/mod.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/shared/mod.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/shared/mod.rs diff --git a/tools/dynwinrt-codegen/src/codegen/shared/structs.rs b/tools/dynwinrt-codegen/src/codegen/winrt/shared/structs.rs similarity index 100% rename from tools/dynwinrt-codegen/src/codegen/shared/structs.rs rename to tools/dynwinrt-codegen/src/codegen/winrt/shared/structs.rs diff --git a/tools/dynwinrt-codegen/src/com_metadata.rs b/tools/dynwinrt-codegen/src/com_metadata.rs new file mode 100644 index 00000000..93d3fad2 --- /dev/null +++ b/tools/dynwinrt-codegen/src/com_metadata.rs @@ -0,0 +1,930 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::HashSet; + +use windows_metadata::{HasAttributes, reader}; + +use crate::types::TypeMeta; + +#[derive(Debug, Clone, PartialEq)] +pub enum ParamDirection { + In, + Out, + InOut, + OutFill, + OutStringBuffer { count_param_index: usize }, + UnsupportedNativeArray { count_param_index: Option }, +} + +impl ParamDirection { + pub fn is_input(&self) -> bool { + matches!( + self, + Self::In | Self::InOut | Self::UnsupportedNativeArray { .. } + ) + } + + pub fn is_output(&self) -> bool { + matches!( + self, + Self::Out + | Self::InOut + | Self::OutFill + | Self::OutStringBuffer { .. } + | Self::UnsupportedNativeArray { .. } + ) + } +} + +#[derive(Debug, Clone)] +pub struct ParamMeta { + pub name: String, + pub typ: TypeMeta, + pub direction: ParamDirection, +} + +#[derive(Debug, Clone, Default)] +pub struct MethodMeta { + pub name: String, + pub vtable_index: usize, + pub params: Vec, + pub return_type: Option, + pub preserve_hresult: bool, + pub doc: Option, + pub owned_outputs: Vec, +} + +#[derive(Debug, Clone)] +pub struct OwnedOutput { + pub param_index: usize, + pub free_with: String, +} + +#[derive(Debug, Clone, Default)] +pub struct InterfaceMeta { + pub name: String, + pub namespace: String, + pub iid: String, + pub methods: Vec, + pub generic_piid: Option, + pub generic_args: Vec, + pub doc: Option, + pub deprecated: Option, +} + +#[derive(Debug, Clone)] +pub struct ComInterfaceMeta { + pub interface: InterfaceMeta, + pub base_offset: usize, + pub is_iunknown_rooted: bool, + pub base_chain: Vec, + pub coclass_clsid: Option, + pub coclass_name: Option, + pub own_methods_start: usize, + pub referenced_enums: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ComEnumValue { + Signed(i64), + Unsigned(u64), +} + +#[derive(Debug, Clone)] +pub struct ComEnumMember { + pub name: String, + pub value: ComEnumValue, +} + +#[derive(Debug, Clone)] +pub struct ComEnumMeta { + pub namespace: String, + pub name: String, + pub underlying: TypeMeta, + pub members: Vec, + pub is_flags: bool, +} + +pub fn parse_com_interface( + winmd_paths: &str, + namespace: &str, + name: &str, +) -> Option { + let index = crate::meta::load_index(winmd_paths)?; + parse_com_interface_from_index(&index, namespace, name) +} + +pub fn parse_com_enum(winmd_paths: &str, namespace: &str, name: &str) -> Option { + let index = crate::meta::load_index(winmd_paths)?; + let def = index.get(namespace, name).next()?; + parse_com_enum_def(&def) +} + +pub fn first_classic_com_interface_in_namespace( + winmd_paths: &str, + namespace: &str, +) -> Option { + let index = crate::meta::load_index(winmd_paths)?; + let names = index + .all() + .filter(|def| { + def.namespace() == namespace + && def + .flags() + .contains(windows_metadata::TypeAttributes::Interface) + }) + .map(|def| def.name().to_string()) + .collect::>(); + names.into_iter().find(|name| { + parse_com_interface_from_index(&index, namespace, name).is_some_and(|interface| { + interface.is_iunknown_rooted || interface.interface.name.ends_with("Interop") + }) + }) +} + +fn parse_com_interface_from_index( + index: &reader::Index, + namespace: &str, + name: &str, +) -> Option { + let def = index.get(namespace, name).next()?; + if !def + .flags() + .contains(windows_metadata::TypeAttributes::Interface) + { + return None; + } + + let mut base_chain = Vec::new(); + let mut current = (namespace.to_string(), name.to_string()); + let mut root = None; + for _ in 0..32 { + let current_def = index.get(¤t.0, ¤t.1).next()?; + let base = match current_def.interface_impls().next()?.interface(&[]) { + windows_metadata::Type::Name(name) => (name.namespace, name.name), + _ => return None, + }; + match base.1.as_str() { + "IUnknown" => { + root = Some((true, 3)); + base_chain.push(( + "Windows.Win32.System.Com".to_string(), + "IUnknown".to_string(), + 0, + )); + break; + } + "IInspectable" => { + root = Some((false, 6)); + base_chain.push(( + "Windows.Foundation".to_string(), + "IInspectable".to_string(), + 0, + )); + break; + } + _ => { + let base_def = index.get(&base.0, &base.1).next()?; + let count = base_def.methods().count(); + base_chain.push((base.0.clone(), base.1.clone(), count)); + current = base; + } + } + } + let (is_iunknown_rooted, root_offset) = root?; + let own_methods_start = root_offset + + base_chain + .iter() + .filter(|(_, name, _)| name != "IUnknown" && name != "IInspectable") + .map(|(_, _, count)| count) + .sum::(); + + let mut methods = Vec::new(); + let mut slot = root_offset; + for (base_namespace, base_name, _) in base_chain + .iter() + .rev() + .filter(|(_, name, _)| name != "IUnknown" && name != "IInspectable") + { + let base_def = index.get(base_namespace, base_name).next()?; + let mut base_methods = parse_methods(index, &base_def, slot); + slot += base_methods.len(); + methods.append(&mut base_methods); + } + if slot != own_methods_start { + return None; + } + methods.extend(parse_methods(index, &def, slot)); + + let iid = crate::meta::extract_iid(&def); + let interface = InterfaceMeta { + name: name.to_string(), + namespace: namespace.to_string(), + iid, + methods, + generic_piid: None, + generic_args: Vec::new(), + doc: None, + deprecated: None, + }; + let (coclass_name, coclass_clsid) = find_coclass(index, namespace, name); + let referenced_enums = collect_referenced_enums(index, &interface); + + Some(ComInterfaceMeta { + interface, + base_offset: root_offset, + is_iunknown_rooted, + base_chain: base_chain.into_iter().map(|(_, name, _)| name).collect(), + coclass_clsid, + coclass_name, + own_methods_start, + referenced_enums, + }) +} + +fn parse_methods( + index: &reader::Index, + def: &reader::TypeDef, + base_offset: usize, +) -> Vec { + def.methods() + .enumerate() + .map(|(index_in_interface, method)| { + let signature = method.signature(&[]); + let raw_name = method.name().to_string(); + let name = method + .find_attribute("OverloadAttribute") + .and_then(|attribute| { + attribute + .value() + .into_iter() + .next() + .and_then(|(_, value)| match value { + windows_metadata::Value::Utf8(value) => Some(value), + _ => None, + }) + }) + .unwrap_or(raw_name); + let mut params = Vec::new(); + let mut owned_outputs = Vec::new(); + for (param_index, (param, typ)) in method + .params() + .filter(|param| param.sequence() > 0) + .zip(signature.types.iter()) + .enumerate() + { + let mut direction = classify_direction( + param.flags(), + matches!(typ, windows_metadata::Type::Array(_)), + ); + let mapped_type = map_parameter_type(typ, &direction, index); + if let Some(count_param_index) = native_array_count_param(¶m) { + if direction.is_output() && !is_string_buffer(&mapped_type) { + direction = ParamDirection::UnsupportedNativeArray { count_param_index }; + } + } + let free_with = param + .find_attribute("FreeWithAttribute") + .and_then(|attribute| { + attribute + .value() + .into_iter() + .next() + .and_then(|(_, value)| match value { + windows_metadata::Value::Utf8(value) => Some(value), + _ => None, + }) + }) + .or_else(|| { + known_free_with(def.namespace(), def.name(), method.name(), typ, &direction) + }); + if let Some(free_with) = free_with { + owned_outputs.push(OwnedOutput { + param_index, + free_with, + }); + } + params.push(ParamMeta { + name: param.name().to_string(), + typ: mapped_type, + direction, + }); + } + mark_caller_owned_string_buffers(&mut params); + let return_type = (signature.return_type != windows_metadata::Type::Void) + .then(|| map_return_type(&signature.return_type, index)); + let preserve_hresult = method.has_attribute("CanReturnMultipleSuccessValuesAttribute") + || is_known_semantic_hresult(def.namespace(), def.name(), method.name()); + MethodMeta { + name, + vtable_index: base_offset + index_in_interface, + params, + return_type, + preserve_hresult, + doc: None, + owned_outputs, + } + }) + .collect() +} + +fn known_free_with( + interface_namespace: &str, + interface_name: &str, + method_name: &str, + typ: &windows_metadata::Type, + direction: &ParamDirection, +) -> Option { + let (windows_metadata::Type::PtrMut(inner, depth) + | windows_metadata::Type::PtrConst(inner, depth)) = typ + else { + return None; + }; + if !matches!(direction, ParamDirection::Out | ParamDirection::InOut) { + return None; + } + if *depth == 1 + && matches!( + inner.as_ref(), + windows_metadata::Type::Name(name) + if name.namespace == "Windows.Win32.Foundation" && name.name == "BSTR" + ) + { + return Some("SysFreeString".into()); + } + let is_known_cotaskmem_wide_string = matches!( + (interface_namespace, interface_name, method_name), + ("Windows.Win32.UI.Shell", "IShellItem", "GetDisplayName") + | ("Windows.Win32.UI.Shell", "IFileDialog", "GetFileName") + | ("Windows.Win32.System.Com", "IPersistFile", "GetCurFile") + ); + if *depth == 1 + && is_known_cotaskmem_wide_string + && matches!( + inner.as_ref(), + windows_metadata::Type::Name(name) + if name.namespace == "Windows.Win32.Foundation" && name.name == "PWSTR" + ) + { + return Some("CoTaskMemFree".into()); + } + // Windows.Win32.winmd omits FreeWith on IShellLink::GetIDList. + if *depth < 2 { + return None; + } + match inner.as_ref() { + windows_metadata::Type::Name(name) + if name.namespace == "Windows.Win32.UI.Shell.Common" && name.name == "ITEMIDLIST" => + { + Some("CoTaskMemFree".into()) + } + _ => None, + } +} + +fn is_known_semantic_hresult( + interface_namespace: &str, + interface_name: &str, + method_name: &str, +) -> bool { + matches!( + (interface_namespace, interface_name, method_name), + ("Windows.Win32.System.Com", "IPersistFile", "GetCurFile") + ) +} + +fn map_parameter_type( + typ: &windows_metadata::Type, + direction: &ParamDirection, + index: &reader::Index, +) -> TypeMeta { + use windows_metadata::Type; + + match typ { + Type::PtrMut(inner, depth) | Type::PtrConst(inner, depth) => { + if matches!(direction, ParamDirection::Out | ParamDirection::InOut) && *depth == 1 { + map_com_type(inner, index) + } else { + TypeMeta::Object + } + } + Type::ConstRef(inner) + if matches!(direction, ParamDirection::Out | ParamDirection::InOut) => + { + map_com_type(inner, index) + } + Type::ConstRef(_) => TypeMeta::Object, + _ => map_com_type(typ, index), + } +} + +fn map_return_type(typ: &windows_metadata::Type, index: &reader::Index) -> TypeMeta { + use windows_metadata::Type; + + match typ { + Type::PtrMut(_, _) | Type::PtrConst(_, _) | Type::ConstRef(_) => TypeMeta::Object, + _ => map_com_type(typ, index), + } +} + +fn map_com_type(typ: &windows_metadata::Type, index: &reader::Index) -> TypeMeta { + match typ { + windows_metadata::Type::ISize => native_isize_type(), + windows_metadata::Type::USize => native_usize_type(), + windows_metadata::Type::Name(name) + if is_canonical_hstring_name(&name.namespace, &name.name) => + { + TypeMeta::String + } + windows_metadata::Type::Name(name) => { + if let Some(def) = index.get(&name.namespace, &name.name).next() { + if let Some(enum_meta) = parse_com_enum_def(&def) { + return enum_meta.as_type_meta(); + } + if let Some(delegate) = parse_com_delegate_def(&def) { + return delegate; + } + } + crate::meta::map_winmd_type_with_generics(typ, index, &[]) + } + _ => crate::meta::map_winmd_type_with_generics(typ, index, &[]), + } +} + +fn is_canonical_hstring_name(namespace: &str, name: &str) -> bool { + namespace == "Windows.Win32.System.WinRT" && name == "HSTRING" +} + +fn parse_com_delegate_def(def: &reader::TypeDef) -> Option { + let extends = def.extends()?; + if !matches!( + (extends.namespace(), extends.name()), + ("System", "Delegate") | ("System", "MulticastDelegate") + ) { + return None; + } + Some(TypeMeta::Delegate { + namespace: def.namespace().to_string(), + name: def.name().to_string(), + iid: crate::meta::extract_iid(def), + }) +} + +impl ComEnumMeta { + fn as_type_meta(&self) -> TypeMeta { + TypeMeta::Enum { + namespace: self.namespace.clone(), + name: self.name.clone(), + underlying: Box::new(self.underlying.clone()), + members: Vec::new(), + is_flags: self.is_flags, + doc: None, + deprecated: None, + } + } +} + +fn parse_com_enum_def(def: &reader::TypeDef) -> Option { + let mut fields = def.fields(); + let underlying = fields + .find(|field| field.name() == "value__") + .and_then(|field| map_com_enum_underlying(&field.ty()))?; + let members = def + .fields() + .filter(|field| field.name() != "value__") + .filter_map(|field| { + let value = match field.constant()?.value() { + windows_metadata::Value::I8(value) => ComEnumValue::Signed(i64::from(value)), + windows_metadata::Value::U8(value) => ComEnumValue::Unsigned(u64::from(value)), + windows_metadata::Value::I16(value) => ComEnumValue::Signed(i64::from(value)), + windows_metadata::Value::U16(value) => ComEnumValue::Unsigned(u64::from(value)), + windows_metadata::Value::I32(value) => ComEnumValue::Signed(i64::from(value)), + windows_metadata::Value::U32(value) => ComEnumValue::Unsigned(u64::from(value)), + windows_metadata::Value::I64(value) => ComEnumValue::Signed(value), + windows_metadata::Value::U64(value) => ComEnumValue::Unsigned(value), + _ => return None, + }; + Some(ComEnumMember { + name: field.name().to_string(), + value, + }) + }) + .collect(); + Some(ComEnumMeta { + namespace: def.namespace().to_string(), + name: def.name().to_string(), + underlying, + members, + is_flags: def.has_attribute("FlagsAttribute"), + }) +} + +fn map_com_enum_underlying(typ: &windows_metadata::Type) -> Option { + match typ { + windows_metadata::Type::I8 => Some(TypeMeta::I8), + windows_metadata::Type::U8 => Some(TypeMeta::U8), + windows_metadata::Type::I16 => Some(TypeMeta::I16), + windows_metadata::Type::U16 => Some(TypeMeta::U16), + windows_metadata::Type::I32 => Some(TypeMeta::I32), + windows_metadata::Type::U32 => Some(TypeMeta::U32), + windows_metadata::Type::I64 => Some(TypeMeta::I64), + windows_metadata::Type::U64 => Some(TypeMeta::U64), + _ => None, + } +} + +pub fn native_isize_type() -> TypeMeta { + TypeMeta::Struct { + namespace: "System".into(), + name: "IntPtr".into(), + fields: Vec::new(), + } +} + +pub fn native_usize_type() -> TypeMeta { + TypeMeta::Struct { + namespace: "System".into(), + name: "UIntPtr".into(), + fields: Vec::new(), + } +} + +pub fn is_native_isize(typ: &TypeMeta) -> bool { + matches!( + typ, + TypeMeta::Struct { + namespace, + name, + .. + } if namespace == "System" && name == "IntPtr" + ) +} + +pub fn is_native_usize(typ: &TypeMeta) -> bool { + matches!( + typ, + TypeMeta::Struct { + namespace, + name, + .. + } if namespace == "System" && name == "UIntPtr" + ) +} + +fn native_array_count_param(param: &reader::MethodParam) -> Option> { + let attribute = param.find_attribute("NativeArrayInfoAttribute")?; + let count = attribute + .value() + .into_iter() + .find(|(name, _)| name == "CountParamIndex") + .and_then(|(_, value)| match value { + windows_metadata::Value::I16(value) if value >= 0 => Some(value as usize), + windows_metadata::Value::U16(value) => Some(value as usize), + windows_metadata::Value::I32(value) if value >= 0 => Some(value as usize), + windows_metadata::Value::U32(value) => usize::try_from(value).ok(), + _ => None, + }); + Some(count) +} + +fn classify_direction(flags: windows_metadata::ParamAttributes, is_array: bool) -> ParamDirection { + let is_in = flags.contains(windows_metadata::ParamAttributes::In); + let is_out = flags.contains(windows_metadata::ParamAttributes::Out); + match (is_in, is_out, is_array) { + (true, true, _) => ParamDirection::InOut, + (_, true, true) => ParamDirection::OutFill, + (_, true, false) => ParamDirection::Out, + _ => ParamDirection::In, + } +} + +fn find_coclass( + index: &reader::Index, + namespace: &str, + interface_name: &str, +) -> (Option, Option) { + let Some(stripped) = interface_name.strip_prefix('I') else { + return (None, None); + }; + let mut candidates = vec![stripped.to_string()]; + let without_version = stripped + .trim_end_matches(|character: char| character.is_ascii_digit()) + .to_string(); + if without_version != stripped { + candidates.push(without_version); + } + for candidate in candidates { + let Some(def) = index.get(namespace, &candidate).next() else { + continue; + }; + let is_coclass = matches!( + def.extends() + .map(|base| (base.namespace().to_string(), base.name().to_string())), + Some((namespace, name)) if namespace == "System" && name == "ValueType" + ); + if is_coclass { + let clsid = crate::meta::extract_iid(&def); + if !clsid.is_empty() { + return (Some(candidate), Some(clsid)); + } + } + } + (None, None) +} + +fn collect_referenced_enums(index: &reader::Index, interface: &InterfaceMeta) -> Vec { + let mut names = HashSet::new(); + let mut result = Vec::new(); + for method in &interface.methods { + for typ in method + .params + .iter() + .map(|param| ¶m.typ) + .chain(method.return_type.iter()) + { + if let TypeMeta::Enum { + namespace, name, .. + } = typ + { + let full_name = format!("{namespace}.{name}"); + if names.insert(full_name) + && let Some(enum_meta) = index + .get(namespace, name) + .next() + .and_then(|def| parse_com_enum_def(&def)) + { + result.push(enum_meta); + } + } + } + } + result +} + +fn mark_caller_owned_string_buffers(params: &mut [ParamMeta]) { + for index in 0..params.len().saturating_sub(1) { + if params[index].direction == ParamDirection::Out + && is_string_buffer(¶ms[index].typ) + && params[index + 1].direction == ParamDirection::In + && is_string_buffer_count(¶ms[index].typ, ¶ms[index + 1]) + { + params[index].direction = ParamDirection::OutStringBuffer { + count_param_index: index + 1, + }; + } + } + let count_index = params.iter().find_map(|param| match param.direction { + ParamDirection::OutStringBuffer { count_param_index } => Some(count_param_index), + _ => None, + }); + if let Some(count_index) = count_index { + for param in params.iter_mut().skip(count_index + 1) { + let name = param.name.to_ascii_lowercase(); + let is_find_data = name == "pfd" + || name.contains("finddata") + || matches!( + ¶m.typ, + TypeMeta::Struct { name, .. } + if name == "WIN32_FIND_DATAW" || name == "WIN32_FIND_DATAA" + ); + if is_find_data + && matches!(param.direction, ParamDirection::Out | ParamDirection::InOut) + { + param.direction = ParamDirection::In; + param.typ = TypeMeta::Object; + } + } + } +} + +fn is_string_buffer(typ: &TypeMeta) -> bool { + matches!( + typ, + TypeMeta::Struct { namespace, name, .. } + if namespace == "Windows.Win32.Foundation" && (name == "PWSTR" || name == "PSTR") + ) +} + +fn is_string_buffer_count(buffer_type: &TypeMeta, param: &ParamMeta) -> bool { + let name = param.name.to_ascii_lowercase(); + let is_wide = matches!( + buffer_type, + TypeMeta::Struct { namespace, name, .. } + if namespace == "Windows.Win32.Foundation" && name == "PWSTR" + ); + matches!(param.typ, TypeMeta::I32 | TypeMeta::U32) + && (name.starts_with("cch") + || (!is_wide && name.starts_with("cb")) + || matches!(name.as_str(), "len" | "length" | "size" | "max" | "count") + || name.starts_with("max") + || name.starts_with("size")) +} + +pub fn find_runtime_class_default_iid( + winmd_paths: &str, + simple_name: &str, +) -> Option<(String, String, String)> { + let index = crate::meta::load_index(winmd_paths)?; + let mut found = None; + let mut collision = false; + for def in index.all() { + if def.name() != simple_name + || !def + .flags() + .contains(windows_metadata::TypeAttributes::WindowsRuntime) + || def + .flags() + .contains(windows_metadata::TypeAttributes::Interface) + { + continue; + } + for implementation in def.interface_impls() { + if !implementation.has_attribute("DefaultAttribute") { + continue; + } + let windows_metadata::Type::Name(name) = implementation.interface(&[]) else { + continue; + }; + if !name.generics.is_empty() { + continue; + } + let interface = index.get(&name.namespace, &name.name).next()?; + let iid = crate::meta::extract_iid(&interface); + if iid.is_empty() { + continue; + } + let candidate = (def.namespace().to_string(), name.name, iid); + match &found { + None => found = Some(candidate), + Some(existing) if existing == &candidate => {} + Some(_) => collision = true, + } + break; + } + } + (!collision).then_some(found).flatten() +} + +pub fn discover_newest_windows_winmd() -> Option { + let base = std::path::Path::new(r"C:\Program Files (x86)\Windows Kits\10\UnionMetadata"); + let mut versions = std::fs::read_dir(base) + .ok()? + .filter_map(Result::ok) + .filter(|entry| entry.path().is_dir()) + .map(|entry| entry.file_name().to_string_lossy().to_string()) + .filter(|name| name.starts_with("10.")) + .collect::>(); + versions.sort_by_key(|version| { + version + .split('.') + .filter_map(|part| part.parse::().ok()) + .collect::>() + }); + versions.into_iter().rev().find_map(|version| { + let path = base.join(version).join("Windows.winmd"); + path.exists().then(|| path.to_string_lossy().to_string()) + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn in_out_is_com_only() { + use windows_metadata::ParamAttributes; + + assert_eq!( + classify_direction(ParamAttributes::In | ParamAttributes::Out, false), + ParamDirection::InOut + ); + } + + #[test] + fn hstring_mapping_requires_the_canonical_namespace() { + assert!(is_canonical_hstring_name( + "Windows.Win32.System.WinRT", + "HSTRING" + )); + assert!(!is_canonical_hstring_name("Contoso.Interop", "HSTRING")); + } + + #[test] + fn find_data_after_string_buffer_is_caller_owned_pointer() { + let mut params = vec![ + ParamMeta { + name: "pszFile".into(), + typ: TypeMeta::Struct { + namespace: "Windows.Win32.Foundation".into(), + name: "PWSTR".into(), + fields: Vec::new(), + }, + direction: ParamDirection::Out, + }, + ParamMeta { + name: "cch".into(), + typ: TypeMeta::I32, + direction: ParamDirection::In, + }, + ParamMeta { + name: "pfd".into(), + typ: TypeMeta::Object, + direction: ParamDirection::InOut, + }, + ]; + + mark_caller_owned_string_buffers(&mut params); + + assert_eq!( + params[0].direction, + ParamDirection::OutStringBuffer { + count_param_index: 1 + } + ); + assert_eq!(params[2].direction, ParamDirection::In); + assert!(matches!(params[2].typ, TypeMeta::Object)); + } + + #[test] + fn item_id_list_double_pointer_uses_cotaskmem_ownership() { + let typ = windows_metadata::Type::PtrMut( + Box::new(windows_metadata::Type::named( + "Windows.Win32.UI.Shell.Common", + "ITEMIDLIST", + )), + 2, + ); + assert_eq!( + known_free_with("", "", "", &typ, &ParamDirection::Out).as_deref(), + Some("CoTaskMemFree") + ); + } + + #[test] + fn bstr_array_does_not_claim_scalar_sysfree_ownership() { + let typ = windows_metadata::Type::PtrMut( + Box::new(windows_metadata::Type::named( + "Windows.Win32.Foundation", + "BSTR", + )), + 2, + ); + assert_eq!( + known_free_with("", "", "", &typ, &ParamDirection::Out), + None + ); + } + + #[test] + fn documented_shell_wide_string_outputs_use_cotaskmem() { + let typ = windows_metadata::Type::PtrMut( + Box::new(windows_metadata::Type::named( + "Windows.Win32.Foundation", + "PWSTR", + )), + 1, + ); + for (interface, method) in [ + ("IShellItem", "GetDisplayName"), + ("IFileDialog", "GetFileName"), + ] { + assert_eq!( + known_free_with( + "Windows.Win32.UI.Shell", + interface, + method, + &typ, + &ParamDirection::Out + ) + .as_deref(), + Some("CoTaskMemFree") + ); + } + assert_eq!( + known_free_with( + "Windows.Win32.System.Com", + "IPersistFile", + "GetCurFile", + &typ, + &ParamDirection::Out + ) + .as_deref(), + Some("CoTaskMemFree") + ); + } + + #[test] + fn documented_get_cur_file_hresult_is_semantic() { + assert!(is_known_semantic_hresult( + "Windows.Win32.System.Com", + "IPersistFile", + "GetCurFile" + )); + assert!(!is_known_semantic_hresult( + "Windows.Win32.System.Com", + "IPersistFile", + "Load" + )); + } +} diff --git a/tools/dynwinrt-codegen/src/lib.rs b/tools/dynwinrt-codegen/src/lib.rs index bd1e1af3..f3e29f4a 100644 --- a/tools/dynwinrt-codegen/src/lib.rs +++ b/tools/dynwinrt-codegen/src/lib.rs @@ -2,6 +2,7 @@ // Licensed under the MIT License. pub mod codegen; +pub mod com_metadata; pub mod meta; pub mod types; pub mod xml_doc; diff --git a/tools/dynwinrt-codegen/src/main.rs b/tools/dynwinrt-codegen/src/main.rs index f8c3403b..80e7f8c8 100644 --- a/tools/dynwinrt-codegen/src/main.rs +++ b/tools/dynwinrt-codegen/src/main.rs @@ -1,16 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::collections::{BTreeSet, HashMap, HashSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use std::fs; use std::path::Path; use clap::{Parser, Subcommand}; +use dynwinrt_codegen::codegen::com; use dynwinrt_codegen::codegen::python; use dynwinrt_codegen::codegen::render_package_json; use dynwinrt_codegen::codegen::typescript; use dynwinrt_codegen::codegen::{project, render_dts, render_js}; +use dynwinrt_codegen::com_metadata; use dynwinrt_codegen::meta; use dynwinrt_codegen::types::TypeMeta; use dynwinrt_codegen::xml_doc::DocTable; @@ -268,16 +270,126 @@ fn run() -> Result<(), String> { .map(|s| s.trim()) .filter(|s| !s.is_empty()) .collect(); + + // First: partition into WinRT classes and classic-COM interfaces. let mut classes = Vec::new(); + let mut com_interfaces: Vec = Vec::new(); for cls in &class_names { + if let Some(com_iface) = com_metadata::parse_com_interface(&winmd, ns, cls) { + // Route through classic-COM path when: + // 1) The interface is IUnknown-rooted (base +3), OR + // 2) It is a `*Interop` bridge (name ends with "Interop") — even + // if IInspectable-rooted (base +6), because the emitter + // handles that via `registerInterface`. + if com_iface.is_iunknown_rooted || cls.ends_with("Interop") { + com_interfaces.push(com_iface); + continue; + } + // The type exists as an interface but is IInspectable-rooted and + // not `*Interop` — it's a plain WinRT interface. Those still need + // to go through the WinRT projection pipeline via `parse_class`, + // which will find it if it's the projected surface of a runtime + // class. If not, give a targeted error rather than the misleading + // "Class not found". + if meta::parse_class(&winmd, ns, cls).is_none() { + return Err(format!( + "{}.{} is an IInspectable-rooted WinRT interface, not a runtime class \ + or classic-COM interface. `--class-name` expects a WinRT runtime class, \ + an IUnknown-rooted classic COM interface, or a `*Interop` bridge. \ + If you meant to project a WinRT interface directly, use the full \ + namespace-projection mode (no `--class-name`).", + ns, cls + )); + } + } match meta::parse_class(&winmd, ns, cls) { Some(mut c) => { doc_table.apply_to_class(&mut c); classes.push(c); } - None => return Err(format!("Class {}.{} not found in {}", ns, cls, winmd)), + None => { + return Err(format!("Class {}.{} not found in {}", ns, cls, winmd)); + } } } + + // Fail loud: classic-COM codegen only emits `.js` + `.d.ts` + // today. If the user asked for a different language + // (e.g. `--lang py`) but any of the requested `--class-name` + // inputs resolved to a classic-COM interface, silently writing + // JS files into a Python output directory would produce the + // wrong artifact types with no diagnostic. Reject the + // combination up front. + if lang != "js" && !com_interfaces.is_empty() { + let mut offenders: Vec = Vec::new(); + for ci in &com_interfaces { + offenders.push(format!( + "{}.{} (classic-COM interface)", + ci.interface.namespace, ci.interface.name + )); + } + return Err(format!( + "`--lang {}` is not supported for classic-COM interfaces \ + (they emit only `.js` + `.d.ts` today). \ + Offending inputs: {}. Re-run with `--lang js`, or split the \ + invocation so the WinRT classes are generated with `--lang {}` and \ + the COM classes with `--lang js`.", + lang, + offenders.join(", "), + lang + )); + } + + if !com_interfaces.is_empty() && !classes.is_empty() { + return Err( + "Classic-COM and WinRT class generation cannot share one output package yet. \ + Run separate `generate` commands with separate output directories." + .into(), + ); + } + + // Emit classic-COM interfaces. Mixed WinRT/COM packages were + // rejected above; COM-only output is finalized below. + if !com_interfaces.is_empty() { + for com_iface in &com_interfaces { + let out = + com::generate_com_interface_files(com_iface, &winmd).map_err(|e| { + format!( + "Classic-COM codegen for {} failed: {}", + com_iface.interface.name, e + ) + })?; + let js_name = format!("{}.js", com_iface.interface.name); + let dts_name = format!("{}.d.ts", com_iface.interface.name); + if !dry_run { + fs::write(output_dir.join(&js_name), &out.js) + .map_err(|e| format!("Failed to write {}: {}", js_name, e))?; + fs::write(output_dir.join(&dts_name), &out.dts) + .map_err(|e| format!("Failed to write {}: {}", dts_name, e))?; + for (name, content) in &out.extra_files { + fs::write(output_dir.join(name), content) + .map_err(|e| format!("Failed to write {}: {}", name, e))?; + } + println!( + "Generated {} ({} .js/.d.ts + {} extras)", + com_iface.interface.name, + 2, + out.extra_files.len() + ); + } else { + println!("[dry-run] Would generate {}", com_iface.interface.name); + } + } + // If we only had classic-COM interfaces requested, return early — + // no WinRT index/barrel work to do. + if classes.is_empty() { + if !dry_run { + write_com_js_barrel_and_manifest(output_dir)?; + } + return Ok(()); + } + } + add_implicit_js_types(&winmd, &lang, &mut classes); generate_for_types( &winmd, @@ -417,6 +529,16 @@ fn run() -> Result<(), String> { let mut total_enums = 0usize; for ns in &namespaces { + if let Some(interface) = + com_metadata::first_classic_com_interface_in_namespace(&winmd, ns) + { + return Err(format!( + "classic-COM namespace projection is not supported because `{ns}` \ + contains `{interface}`. Use `--class-name {interface}` (or a \ + comma-separated class list) so each interface is validated by the \ + Classic-COM ABI pipeline." + )); + } let mut classes = meta::parse_namespace(&winmd, ns); let mut interfaces = meta::parse_interfaces(&winmd, ns); let mut enums = meta::parse_enums(&winmd, ns); @@ -899,6 +1021,7 @@ fn write_js_barrel_and_manifest(output_dir: &Path, index_content: &str) -> Resul if stale.exists() { let _ = fs::remove_file(&stale); } + // Remove the previous opt-in getter barrel name if it exists from older // generated output. `index.js` is now the getter barrel and // `index.proxy.js` is the explicit compatibility path. @@ -948,6 +1071,84 @@ fn write_js_barrel_and_manifest(output_dir: &Path, index_content: &str) -> Resul Ok(()) } +fn write_com_js_barrel_and_manifest(output_dir: &Path) -> Result<(), String> { + let mut modules: BTreeMap> = BTreeMap::new(); + let entries = fs::read_dir(output_dir).map_err(|error| { + format!( + "Failed to read COM output directory {}: {error}", + output_dir.display() + ) + })?; + for entry in entries.flatten() { + let path = entry.path(); + let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else { + continue; + }; + let Some(module) = file_name.strip_suffix(".js") else { + continue; + }; + if module == "index" { + continue; + } + let content = fs::read_to_string(&path) + .map_err(|error| format!("Failed to read {}: {error}", path.display()))?; + let exports = collect_com_esm_exports(&content); + if !exports.is_empty() { + modules.insert(module.to_string(), exports); + } + } + + let mut index = String::from("// Generated by dynwinrt-codegen - do not edit\n"); + for (module, exports) in &modules { + index.push_str(&format!( + "export {{ {} }} from './{module}.js';\n", + exports.iter().cloned().collect::>().join(", ") + )); + } + fs::write(output_dir.join("index.js"), &index) + .map_err(|error| format!("Failed to write COM index.js: {error}"))?; + fs::write(output_dir.join("index.d.ts"), &index) + .map_err(|error| format!("Failed to write COM index.d.ts: {error}"))?; + + let mut package = String::from( + "{\n \"name\": \"@winapp/bindings\",\n \"type\": \"module\",\n \ + \"sideEffects\": false,\n \"main\": \"./index.js\",\n \ + \"types\": \"./index.d.ts\",\n \"exports\": {\n \".\": {\n \ + \"types\": \"./index.d.ts\",\n \"import\": \"./index.js\",\n \ + \"default\": \"./index.js\"\n }", + ); + for module in modules.keys() { + package.push_str(&format!( + ",\n \"./{module}\": {{\n \"types\": \"./{module}.d.ts\",\n \ + \"import\": \"./{module}.js\",\n \"default\": \"./{module}.js\"\n }}" + )); + } + package.push_str("\n }\n}\n"); + fs::write(output_dir.join("package.json"), package) + .map_err(|error| format!("Failed to write COM package.json: {error}"))?; + Ok(()) +} + +fn collect_com_esm_exports(content: &str) -> BTreeSet { + const PREFIXES: &[&str] = &["export const ", "export class ", "export function "]; + content + .lines() + .filter_map(|line| { + let line = line.trim_start(); + let rest = PREFIXES + .iter() + .find_map(|prefix| line.strip_prefix(prefix))?; + let name = rest + .chars() + .take_while(|character| { + character.is_ascii_alphanumeric() || *character == '_' || *character == '$' + }) + .collect::(); + (!name.is_empty()).then_some(name) + }) + .collect() +} + fn write_lifetime_module(output_dir: &Path) -> Result<(), String> { let js = "'use strict';\n\ let activeScope = null;\n\ diff --git a/tools/dynwinrt-codegen/src/meta.rs b/tools/dynwinrt-codegen/src/meta.rs index aa187f06..d4417047 100644 --- a/tools/dynwinrt-codegen/src/meta.rs +++ b/tools/dynwinrt-codegen/src/meta.rs @@ -695,7 +695,7 @@ pub fn expand_winmd_paths(winmd_paths: &str) -> String { all_paths.join(";") } -fn load_index(winmd_paths: &str) -> Option { +pub(crate) fn load_index(winmd_paths: &str) -> Option { let paths: Vec<&str> = winmd_paths.split(';').filter(|s| !s.is_empty()).collect(); if paths.is_empty() { eprintln!("warning: no winmd paths provided"); @@ -1210,7 +1210,7 @@ fn type_meta_to_winmd_type(typ: &TypeMeta) -> windows_metadata::Type { } } -fn extract_iid(def: &reader::TypeDef) -> String { +pub(crate) fn extract_iid(def: &reader::TypeDef) -> String { if let Some(attr) = def.find_attribute("GuidAttribute") { let args: Vec<(String, windows_metadata::Value)> = attr.value(); if args.len() >= 11 { @@ -1310,7 +1310,7 @@ fn map_winmd_type(ty: &windows_metadata::Type, index: &reader::Index) -> TypeMet map_winmd_type_with_generics(ty, index, &[]) } -fn map_winmd_type_with_generics( +pub(crate) fn map_winmd_type_with_generics( ty: &windows_metadata::Type, index: &reader::Index, generic_args: &[TypeMeta], diff --git a/tools/dynwinrt-codegen/tests/observable_vector_test.rs b/tools/dynwinrt-codegen/tests/observable_vector_test.rs index 5d87fbe2..512f1758 100644 --- a/tools/dynwinrt-codegen/tests/observable_vector_test.rs +++ b/tools/dynwinrt-codegen/tests/observable_vector_test.rs @@ -34,6 +34,10 @@ fn observable_vector_projects_mutable_create_helper() { "{js}", ); assert!(js.contains("onVectorChanged")); + assert!( + js.contains("asVector: { value: observable.asVector.bind(observable) }"), + "{js}", + ); assert!(js.contains( "asVector() {\n return new ((__load_IVector_Object()).IVector_Object)(this._obj);", )); diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts new file mode 100644 index 00000000..ecf25267 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.d.ts @@ -0,0 +1,16 @@ +// Generated by dynwinrt-codegen — do not edit +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 declare const IID_IDataTransferManagerInterop: unknown; + +export declare class IDataTransferManagerInterop { + /** Activate the projected WinRT class and QI to the interop. */ + static create(): IDataTransferManagerInterop; + /** Wrap an existing native COM pointer (for QueryInterface bridging). */ + static _fromNative(obj: unknown): IDataTransferManagerInterop; + getForWindow(appWindow: HWND | Buffer | Uint8Array): DynWinRtValue; + showShareUIForWindow(appWindow: HWND | Buffer | Uint8Array): void; +} diff --git a/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js new file mode 100644 index 00000000..c33d6ba3 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/idatatransfermanagerinterop/IDataTransferManagerInterop.js @@ -0,0 +1,36 @@ +// Generated by dynwinrt-codegen — do not edit +import { DynCom, DynComMethodSig, DynWinRtValue, WinGuid } from '@microsoft/dynwinrt/com'; + +export const IID_IDataTransferManagerInterop = WinGuid.parse('3a3dcd6c-3eab-43dc-bcde-45671ce800c8'); +const IID_DataTransferManager_default = WinGuid.parse('a5caee9b-8708-49d1-8d36-67d25a8da00c'); + +let _IDataTransferManagerInteropCache; +const _IDataTransferManagerInterop = new Proxy({}, { + get(_target, prop) { + _IDataTransferManagerInteropCache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.IDataTransferManagerInterop', IID_IDataTransferManagerInterop) + .addMethod('GetForWindow', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addOut(DynCom.pointerType())) + .addMethod('ShowShareUIForWindow', new DynComMethodSig().addIn(DynCom.pointerType())); + const value = _IDataTransferManagerInteropCache[prop]; + return typeof value === 'function' ? value.bind(_IDataTransferManagerInteropCache) : value; + }, +}); + +export class IDataTransferManagerInterop { + _obj; + constructor(obj) { this._obj = obj; } + static _fromNative(obj) { return new IDataTransferManagerInterop(obj); } + /** Create a new `IDataTransferManagerInterop` by activating the `Windows.ApplicationModel.DataTransfer.DataTransferManager` factory and QI'ing to the interop. */ + static create() { + const factory = DynWinRtValue.activationFactory('Windows.ApplicationModel.DataTransfer.DataTransferManager'); + const _obj = factory.cast(IID_IDataTransferManagerInterop); + return new IDataTransferManagerInterop(_obj); + } + getForWindow(appWindow) { + const _raw = _IDataTransferManagerInterop.method(3).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(appWindow)), DynCom.iidPointer(IID_DataTransferManager_default)]); + const _out = DynCom.adoptComPointer(_raw, IID_DataTransferManager_default); + return _out; + } + showShareUIForWindow(appWindow) { + _IDataTransferManagerInterop.method(4).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(appWindow))]); + } +} diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts new file mode 100644 index 00000000..67b26bbd --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.d.ts @@ -0,0 +1,38 @@ +// Generated by dynwinrt-codegen — do not edit +import { TBPFLAG } from './TBPFLAG.js'; + +/** Opaque Win32 handle value. Pass a raw pointer value as a `bigint` (full pointer width) or `number` (safe integer). */ +export type HICON = bigint | number; +/** Opaque Win32 handle value. Pass a raw pointer value as a `bigint` (full pointer width) or `number` (safe integer). */ +export type HIMAGELIST = bigint | number; +/** Opaque Win32 handle value. Pass a raw pointer value as a `bigint` (full pointer width) or `number` (safe integer). */ +export type HWND = bigint | number; +/** Win32 NUL-terminated string pointer. Pass a `Buffer` holding the string bytes (including the NUL terminator), or pass a raw pointer as `bigint`. */ +export type PWSTR = bigint | Buffer; + +export declare const IID_ITaskbarList3: unknown; + +export declare class ITaskbarList3 { + /** Create a new instance via the coclass activation path. */ + static create(): ITaskbarList3; + /** Wrap an existing native COM pointer (for QueryInterface bridging). */ + static _fromNative(obj: unknown): ITaskbarList3; + hrInit(): void; + addTab(hwnd: HWND | Buffer | Uint8Array): void; + deleteTab(hwnd: HWND | Buffer | Uint8Array): void; + activateTab(hwnd: HWND | Buffer | Uint8Array): void; + setActiveAlt(hwnd: HWND | Buffer | Uint8Array): void; + markFullscreenWindow(hwnd: HWND | Buffer | Uint8Array, fFullscreen: boolean): void; + setProgressValue(hwnd: HWND | Buffer | Uint8Array, ullCompleted: bigint, ullTotal: bigint): void; + setProgressState(hwnd: HWND | Buffer | Uint8Array, tbpFlags: TBPFLAG): void; + registerTab(tab: HWND | Buffer | Uint8Array, mDI: HWND | Buffer | Uint8Array): void; + unregisterTab(tab: HWND | Buffer | Uint8Array): void; + setTabOrder(tab: HWND | Buffer | Uint8Array, insertBefore: HWND | Buffer | Uint8Array): void; + setTabActive(tab: HWND | Buffer | Uint8Array, mDI: HWND | Buffer | Uint8Array, reserved: number): void; + thumbBarAddButtons(hwnd: HWND | Buffer | Uint8Array, cButtons: number, pButton: bigint | Buffer): void; + thumbBarUpdateButtons(hwnd: HWND | Buffer | Uint8Array, cButtons: number, pButton: bigint | Buffer): void; + thumbBarSetImageList(hwnd: HWND | Buffer | Uint8Array, himl: HIMAGELIST): void; + setOverlayIcon(hwnd: HWND | Buffer | Uint8Array, hIcon: HICON, description: PWSTR): void; + setThumbnailTooltip(hwnd: HWND | Buffer | Uint8Array, tip: PWSTR): void; + setThumbnailClip(hwnd: HWND | Buffer | Uint8Array, prcClip: bigint | Buffer): void; +} diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js new file mode 100644 index 00000000..9006398f --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/ITaskbarList3.js @@ -0,0 +1,97 @@ +// Generated by dynwinrt-codegen — do not edit +import { DynCom, DynComMethodSig, WinGuid } from '@microsoft/dynwinrt/com'; +import { TBPFLAG } from './TBPFLAG.js'; + +export const IID_ITaskbarList3 = WinGuid.parse('ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf'); + +let _ITaskbarList3Cache; +const _ITaskbarList3 = new Proxy({}, { + get(_target, prop) { + _ITaskbarList3Cache ??= DynCom.registerIUnknownInterface('Windows.Win32.UI.Shell.ITaskbarList3', IID_ITaskbarList3) + .addMethod('HrInit', new DynComMethodSig()) + .addMethod('AddTab', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('DeleteTab', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('ActivateTab', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('SetActiveAlt', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('MarkFullscreenWindow', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) + .addMethod('SetProgressValue', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u64Type()).addIn(DynCom.u64Type())) + .addMethod('SetProgressState', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.i32Type())) + .addMethod('RegisterTab', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('UnregisterTab', new DynComMethodSig().addIn(DynCom.pointerType())) + .addMethod('SetTabOrder', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('SetTabActive', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addIn(DynCom.u32Type())) + .addMethod('ThumbBarAddButtons', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u32Type()).addIn(DynCom.pointerType())) + .addMethod('ThumbBarUpdateButtons', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.u32Type()).addIn(DynCom.pointerType())) + .addMethod('ThumbBarSetImageList', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('SetOverlayIcon', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('SetThumbnailTooltip', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())) + .addMethod('SetThumbnailClip', new DynComMethodSig().addIn(DynCom.pointerType()).addIn(DynCom.pointerType())); + const value = _ITaskbarList3Cache[prop]; + return typeof value === 'function' ? value.bind(_ITaskbarList3Cache) : value; + }, +}); + +export class ITaskbarList3 { + _obj; + constructor(obj) { this._obj = obj; } + static _fromNative(obj) { return new ITaskbarList3(obj); } + /** Create a new `ITaskbarList3` via `CoCreateInstance` on `CLSID_TaskbarList`. */ + static create() { + const _obj = DynCom.coCreateInstance('56fdf344-fd6d-11d0-958a-006097c9a090', IID_ITaskbarList3); + return new ITaskbarList3(_obj); + } + hrInit() { + _ITaskbarList3.method(3).invoke(this._obj, []); + } + addTab(hwnd) { + _ITaskbarList3.method(4).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(hwnd))]); + } + deleteTab(hwnd) { + _ITaskbarList3.method(5).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(hwnd))]); + } + activateTab(hwnd) { + _ITaskbarList3.method(6).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(hwnd))]); + } + setActiveAlt(hwnd) { + _ITaskbarList3.method(7).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(hwnd))]); + } + markFullscreenWindow(hwnd, fFullscreen) { + _ITaskbarList3.method(8).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(hwnd)), DynCom.i32(fFullscreen ? 1 : 0)]); + } + setProgressValue(hwnd, ullCompleted, ullTotal) { + _ITaskbarList3.method(9).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(hwnd)), DynCom.u64(BigInt(ullCompleted)), DynCom.u64(BigInt(ullTotal))]); + } + setProgressState(hwnd, tbpFlags) { + _ITaskbarList3.method(10).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(hwnd)), DynCom.i32(tbpFlags)]); + } + registerTab(tab, mDI) { + _ITaskbarList3.method(11).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(tab)), DynCom.pointer(DynCom.handleValue(mDI))]); + } + unregisterTab(tab) { + _ITaskbarList3.method(12).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(tab))]); + } + setTabOrder(tab, insertBefore) { + _ITaskbarList3.method(13).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(tab)), DynCom.pointer(DynCom.handleValue(insertBefore))]); + } + setTabActive(tab, mDI, reserved) { + _ITaskbarList3.method(14).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(tab)), DynCom.pointer(DynCom.handleValue(mDI)), DynCom.u32(reserved)]); + } + thumbBarAddButtons(hwnd, cButtons, pButton) { + _ITaskbarList3.method(15).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(hwnd)), DynCom.u32(cButtons), DynCom.pointer(pButton)]); + } + thumbBarUpdateButtons(hwnd, cButtons, pButton) { + _ITaskbarList3.method(16).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(hwnd)), DynCom.u32(cButtons), DynCom.pointer(pButton)]); + } + thumbBarSetImageList(hwnd, himl) { + _ITaskbarList3.method(17).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(hwnd)), DynCom.pointer(himl)]); + } + setOverlayIcon(hwnd, hIcon, description) { + _ITaskbarList3.method(18).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(hwnd)), DynCom.pointer(hIcon), DynCom.pointer(description)]); + } + setThumbnailTooltip(hwnd, tip) { + _ITaskbarList3.method(19).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(hwnd)), DynCom.pointer(tip)]); + } + setThumbnailClip(hwnd, prcClip) { + _ITaskbarList3.method(20).invoke(this._obj, [DynCom.pointer(DynCom.handleValue(hwnd)), DynCom.pointer(prcClip)]); + } +} diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.d.ts b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.d.ts new file mode 100644 index 00000000..cad22793 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.d.ts @@ -0,0 +1,9 @@ +// Generated by dynwinrt-codegen — do not edit +export type TBPFLAG = (typeof TBPFLAG)[keyof typeof TBPFLAG]; +export declare const TBPFLAG: { + readonly TBPF_NOPROGRESS: 0; + readonly TBPF_INDETERMINATE: 1; + readonly TBPF_NORMAL: 2; + readonly TBPF_ERROR: 4; + readonly TBPF_PAUSED: 8; +}; diff --git a/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.js b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.js new file mode 100644 index 00000000..58af8cf8 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3/TBPFLAG.js @@ -0,0 +1,8 @@ +// Generated by dynwinrt-codegen — do not edit +export const TBPFLAG = Object.freeze({ + TBPF_NOPROGRESS: 0, + TBPF_INDETERMINATE: 1, + TBPF_NORMAL: 2, + TBPF_ERROR: 4, + TBPF_PAUSED: 8, +}); diff --git a/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs b/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs new file mode 100644 index 00000000..4d4c5300 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/win32_com_interop_test.rs @@ -0,0 +1,469 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! TDD tests for the *Interop HWND pattern in classic-COM code generation. +//! +//! These tests drive the `getForWindow(hwnd, REFIID, out void**)` special case: +//! - IUnknown-rooted interop (e.g. `IDataTransferManagerInterop`, base=+3) +//! - IInspectable-rooted interop (e.g. `ISystemMediaTransportControlsInterop`, base=+6) +//! +//! The interop shape is: last two params are `(riid: In, out_ptr: Out)`, plus +//! zero or more natural in-params (HWND, HSTRING, …). The generated wrapper +//! MUST hide the REFIID + void** — the caller only supplies the natural +//! parameters, and the wrapper returns the projected WinRT object. +//! +//! Windows.winmd is auto-discovered from the newest installed Windows SDK by +//! the classic-COM interop codegen (see `com::resolve_projected_default_iid`), +//! so these tests do not require a specific SDK version — they only need any +//! recent SDK to be installed AND the Windows.Win32 metadata (path from `win32_winmd`, override via `DYNWINRT_WIN32_WINMD`). + +use std::fs; +use std::path::{Path, PathBuf}; + +use dynwinrt_codegen::codegen::com; +use dynwinrt_codegen::com_metadata; + +/// Path to `Windows.Win32.winmd`. Overridable via the `DYNWINRT_WIN32_WINMD` +/// environment variable so this suite can run on CI and other machines without +/// editing the source; falls back to the common local checkout path. +fn win32_winmd() -> String { + std::env::var("DYNWINRT_WIN32_WINMD") + .unwrap_or_else(|_| r"C:\s\win32metadata\Windows.Win32.winmd".to_string()) +} + +fn win32_available() -> bool { + Path::new(&win32_winmd()).exists() +} + +/// Ensure any recent installed Windows SDK is present so the interop generator +/// can auto-resolve the projected class IID. Uses the SAME discovery logic the +/// codegen itself uses — no pinned version. +fn newest_windows_winmd_available() -> bool { + com_metadata::discover_newest_windows_winmd().is_some() +} + +/// 1. IDataTransferManagerInterop parses cleanly, is IUnknown-rooted (+3), +/// and its `GetForWindow` is at slot 3. +#[test] +fn parse_data_transfer_manager_interop() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .expect("IDataTransferManagerInterop must exist"); + assert!(com.is_iunknown_rooted); + assert_eq!(com.base_offset, 3); + let get_for_window = com + .interface + .methods + .iter() + .find(|m| m.name == "GetForWindow") + .expect("GetForWindow method must exist"); + assert_eq!(get_for_window.vtable_index, 3); + // Last two params must be (In riid, Out out_ptr) — the interop shape. + assert_eq!(get_for_window.params.len(), 3, "HWND + riid + out"); +} + +/// 2. ISystemMediaTransportControlsInterop parses cleanly, is IInspectable-rooted (+6), +/// `GetForWindow` at slot 6. +#[test] +fn parse_smtc_interop() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.WinRT", + "ISystemMediaTransportControlsInterop", + ) + .expect("ISystemMediaTransportControlsInterop must exist"); + assert!( + !com.is_iunknown_rooted, + "SMTC interop derives from IInspectable, not IUnknown" + ); + assert_eq!(com.base_offset, 6); + let get_for_window = com + .interface + .methods + .iter() + .find(|m| m.name == "GetForWindow") + .expect("GetForWindow method must exist"); + assert_eq!(get_for_window.vtable_index, 6); +} + +/// 3. Codegen recognises the interop shape and emits a natural +/// `getForWindow(hwnd)` — hiding both the REFIID and the void** out-ptr. +#[test] +fn interop_dts_hides_riid_and_out_ptr_for_datatransfermanager() { + if !win32_available() || !newest_windows_winmd_available() { + eprintln!("Skipping: winmd(s) not available"); + return; + } + let com = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com, &win32_winmd()) + .expect("interop codegen must succeed when winmds are present"); + let dts = out.dts.as_str(); + + // The natural signature: hwnd only, NO riid, NO out-ptr. + // Accept either single-arg or single-arg + optional projection hint. + // The signature must contain `getForWindow(` followed by a SINGLE + // typed parameter (HWND-like) and NO `riid`/`REFIID` mention. + assert!( + dts.contains("getForWindow"), + ".d.ts must expose getForWindow (camelCased):\n{}", + dts + ); + assert!( + !dts.contains("riid") && !dts.contains("REFIID"), + "REFIID/riid must not appear in .d.ts:\n{}", + dts + ); + assert!( + !dts.contains("void**") && !dts.to_lowercase().contains("out_ptr"), + "void**/out_ptr must not appear in .d.ts:\n{}", + dts + ); + + // Return type — must be the explicit WinRT bridge value (`DynWinRtValue`), + // NOT the raw `bigint | Buffer` ABI leak and NOT a synthesized WinRT + // runtime-class projection. The runtime-class name only ever appears as + // part of the interop class name `IDataTransferManagerInterop`, never as + // the `getForWindow` return type (asserting a bare `DataTransferManager` + // substring would be a false positive that matches the class name). + assert!( + dts.contains("getForWindow(appWindow: HWND | Buffer | Uint8Array): DynWinRtValue;"), + ".d.ts getForWindow must return the DynWinRtValue bridge:\n{}", + dts + ); + assert!( + !dts.contains("getForWindow(hwnd: bigint | Buffer, riid"), + "riid must not leak into the natural signature:\n{}", + dts + ); +} + +/// 4. The generated JS synthesises the target IID (default interface IID of +/// the WinRT runtime class) INSIDE the method body — the caller supplies +/// only the HWND. +#[test] +fn interop_js_synthesizes_target_iid_for_datatransfermanager() { + if !win32_available() || !newest_windows_winmd_available() { + eprintln!("Skipping: winmd(s) not available"); + return; + } + let com = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com, &win32_winmd()) + .expect("interop codegen must succeed when winmds are present"); + let js = out.js.as_str(); + + // The IDataTransferManager default interface IID must be embedded in .js + // (it's the runtime class's default interface's IID: + // a5caee9b-8708-49d1-8d36-67d25a8da00c). + assert!( + js.contains("a5caee9b-8708-49d1-8d36-67d25a8da00c"), + ".js must embed the IDataTransferManager default interface IID:\n{}", + js + ); + // The interop's own IID must also be present. + assert!( + js.contains("3a3dcd6c-3eab-43dc-bcde-45671ce800c8"), + ".js must embed the IDataTransferManagerInterop IID:\n{}", + js + ); + + // GetForWindow lives at vtable slot 3 (IUnknown+3). + assert!( + js.contains("method(3)"), + ".js must invoke slot 3 for GetForWindow:\n{}", + js + ); + assert!( + js.contains("DynCom.adoptComPointer(_raw, IID_DataTransferManager_default)"), + ".js must adopt the AddRef-owned void** result:\n{}", + js + ); + + // Activation: uses activationFactory (WinRT) for the projected class + // + QI to the interop IID — NOT CoCreateInstance (which is for classic COM CLSIDs). + assert!( + js.contains("activationFactory") || js.contains("activation_factory"), + ".js must use activationFactory to reach the interop:\n{}", + js + ); + assert!( + !js.contains("coCreateInstance"), + "interop must NOT use coCreateInstance (only WinRT interop path):\n{}", + js + ); +} + +/// 5. SMTC-specific: the SMTC interop generates a wrapper whose registration +/// uses the +6 (IInspectable) base, and its GetForWindow invokes slot 6. +#[test] +fn smtc_interop_js_uses_inspectable_base_slot_6() { + if !win32_available() || !newest_windows_winmd_available() { + eprintln!("Skipping: winmd(s) not available"); + return; + } + let com = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.WinRT", + "ISystemMediaTransportControlsInterop", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com, &win32_winmd()) + .expect("interop codegen must succeed when winmds are present"); + let js = out.js.as_str(); + + // IInspectable-rooted → register with the WinRT base (registerInterface), + // not the IUnknown-rooted registration path. + assert!( + js.contains("DynCom.registerIInspectableInterface(") + && !js.contains("DynCom.registerIUnknownInterface("), + ".js for an IInspectable-rooted interop must use registerInterface \ + (base_slot=6), got:\n{}", + js + ); + assert!( + js.contains("method(6)"), + ".js must invoke slot 6 for GetForWindow:\n{}", + js + ); + + // Return type = SystemMediaTransportControls; default interface IID + // (ISystemMediaTransportControls = 99fa3ff4-1742-42a6-902e-087d41f965ec). + assert!( + js.contains("99fa3ff4-1742-42a6-902e-087d41f965ec"), + ".js must embed the ISystemMediaTransportControls default interface IID:\n{}", + js + ); +} + +/// 6. The COM projection returns the bridge value without synthesizing a +/// partial WinRT runtime-class projection. +#[test] +fn interop_return_is_explicit_winrt_bridge_value() { + if !win32_available() || !newest_windows_winmd_available() { + eprintln!("Skipping: winmd(s) not available"); + return; + } + let com = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com, &win32_winmd()) + .expect("interop codegen must succeed when winmds are present"); + + assert!( + out.dts + .contains("getForWindow(appWindow: HWND | Buffer | Uint8Array): DynWinRtValue;"), + "interop .d.ts must expose the WinRT bridge value:\n{}", + out.dts + ); + assert!( + !out.extra_files + .iter() + .any(|(name, _)| name.starts_with("DataTransferManager.")), + "COM codegen must not synthesize a WinRT class projection" + ); +} + +/// 7. Interop generation is deterministic (byte-identical across two runs). +#[test] +fn interop_generation_is_deterministic() { + if !win32_available() || !newest_windows_winmd_available() { + eprintln!("Skipping: winmd(s) not available"); + return; + } + let mk = || { + let com = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .unwrap(); + com::generate_com_interface_files(&com, &win32_winmd()) + .expect("interop codegen must succeed when winmds are present") + }; + let a = mk(); + let b = mk(); + assert_eq!(a.js, b.js); + assert_eq!(a.dts, b.dts); + assert_eq!(a.extra_files, b.extra_files); +} + +/// 8. Snapshot: lock the generated IDataTransferManagerInterop files +/// against committed reference files. +#[test] +fn snapshot_datatransfermanager_interop() { + if !win32_available() || !newest_windows_winmd_available() { + eprintln!("Skipping: winmd(s) not available"); + return; + } + let com = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com, &win32_winmd()) + .expect("interop codegen must succeed when winmds are present"); + + let snapshot_dir: PathBuf = + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots/idatatransfermanagerinterop"); + assert!( + snapshot_dir.exists(), + "Snapshot directory not found: {}", + snapshot_dir.display() + ); + + let mut generated: Vec<(String, String)> = Vec::new(); + generated.push(("IDataTransferManagerInterop.js".into(), out.js.clone())); + generated.push(("IDataTransferManagerInterop.d.ts".into(), out.dts.clone())); + for (name, content) in &out.extra_files { + generated.push((name.clone(), content.clone())); + } + + let mut mismatches = Vec::new(); + for (name, actual) in &generated { + let path = snapshot_dir.join(name); + if !path.exists() { + mismatches.push(format!(" missing snapshot: {}", name)); + continue; + } + let expected = fs::read_to_string(&path).unwrap(); + if actual.trim_end() != expected.trim_end() { + mismatches.push(format!(" differs: {}", name)); + } + } + if let Ok(entries) = fs::read_dir(&snapshot_dir) { + let names: std::collections::HashSet = + generated.iter().map(|(n, _)| n.clone()).collect(); + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if !names.contains(&name) { + mismatches.push(format!(" extra snapshot not generated: {}", name)); + } + } + } + + if !mismatches.is_empty() { + panic!( + "IDataTransferManagerInterop snapshot mismatch!\n{}\n\n\ + To update, re-run the generator or copy the actual output.", + mismatches.join("\n") + ); + } +} + +// ------------------------------------------------------------------------- +// Fix 1 (portability): interop IID resolution +// ------------------------------------------------------------------------- + +/// FIX 1 (portability): the interop generator MUST NOT depend on a specific +/// SDK-versioned `Windows.winmd` path. On this box (and any developer/CI +/// machine with the Win32 metadata + a recent Windows SDK installed), the +/// generator resolves the projected class IID correctly, and the tests +/// actively assert that IID rather than self-skipping. +#[test] +fn fix1_interop_iid_resolution_is_portable_and_asserted() { + if !win32_available() { + eprintln!( + "Skipping fix1_interop_iid_resolution_is_portable_and_asserted: Win32 winmd not available at {}", + &win32_winmd() + ); + return; + } + if !newest_windows_winmd_available() { + eprintln!( + "Skipping fix1_interop_iid_resolution_is_portable_and_asserted: no Windows SDK Windows.winmd discoverable" + ); + return; + } + + // 1. IDataTransferManager: default interface IID must resolve to the + // well-known value regardless of which SDK version is installed. + let (ns_dtm, _iface_dtm, iid_dtm) = com_metadata::find_runtime_class_default_iid( + &com_metadata::discover_newest_windows_winmd().unwrap(), + "DataTransferManager", + ) + .expect("DataTransferManager must resolve via discovered SDK winmd"); + assert_eq!(ns_dtm, "Windows.ApplicationModel.DataTransfer"); + assert_eq!(iid_dtm, "a5caee9b-8708-49d1-8d36-67d25a8da00c"); + + // 2. SystemMediaTransportControls: same portability contract. + let (ns_smtc, _iface_smtc, iid_smtc) = com_metadata::find_runtime_class_default_iid( + &com_metadata::discover_newest_windows_winmd().unwrap(), + "SystemMediaTransportControls", + ) + .expect("SystemMediaTransportControls must resolve via discovered SDK winmd"); + assert_eq!(ns_smtc, "Windows.Media"); + assert_eq!(iid_smtc, "99fa3ff4-1742-42a6-902e-087d41f965ec"); + + // 3. End-to-end: the classic-COM interop wrapper embeds the correct IID. + // Test intentionally passes ONLY the Win32 winmd (no Windows.winmd in + // winmd_paths) to exercise the newest-SDK fallback path. + let com = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .expect("IDataTransferManagerInterop must exist"); + let out = com::generate_com_interface_files(&com, &win32_winmd()) + .expect("interop codegen must resolve IID via newest-SDK fallback"); + assert!( + out.js.contains(&iid_dtm), + "generated .js must embed the resolved DataTransferManager IID `{}`:\n{}", + iid_dtm, + out.js + ); + // Must NEVER emit the silent NULL riid sentinel that the pre-fix code + // could produce when resolution failed. + assert!( + !out.js.contains("DynCom.pointer(0n)"), + "generator must not emit a NULL riid — indicates silent failure:\n{}", + out.js + ); +} + +/// FIX 1 (portability): the generator MUST prefer the winmd paths passed to +/// it OVER the auto-discovered SDK winmd. This preserves reproducibility for +/// integrators who pin a specific SDK via `--ref`. +#[test] +fn fix1_interop_iid_prefers_passed_winmds_over_sdk() { + if !win32_available() || !newest_windows_winmd_available() { + eprintln!("Skipping: winmd(s) not available"); + return; + } + let sdk = com_metadata::discover_newest_windows_winmd().unwrap(); + // Pass Windows.winmd as part of winmd_paths — the generator should find + // the runtime class immediately without hitting the fallback path. + let combined = format!("{};{}", &win32_winmd(), sdk); + let com = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com, &combined) + .expect("interop codegen must succeed when Windows.winmd is in winmd_paths"); + assert!(out.js.contains("a5caee9b-8708-49d1-8d36-67d25a8da00c")); +} diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs new file mode 100644 index 00000000..8d0655d5 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -0,0 +1,1462 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! TDD tests for classic-COM (option A) code generation from Windows.Win32.winmd. +//! +//! These tests drive the implementation of: +//! - Base-aware vtable slot computation (walks interface_impls chain) +//! - IUnknown vs IInspectable base offset (3 vs 6) +//! - Coclass CLSID discovery for `create()` activation +//! - Natural TS/JS wrapper generation for classic-COM interfaces +//! +//! Tests are skipped (with an `eprintln!` note) when the Win32 winmd is not +//! present at the well-known path. + +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use dynwinrt_codegen::codegen::com; +use dynwinrt_codegen::codegen::project::{get_import_name, set_import_name}; +use dynwinrt_codegen::com_metadata; +use dynwinrt_codegen::meta; +use dynwinrt_codegen::types::TypeMeta; + +/// Path to `Windows.Win32.winmd`. Overridable via the `DYNWINRT_WIN32_WINMD` +/// environment variable so this suite can run on CI and other machines without +/// editing the source; falls back to the common local checkout path. +fn win32_winmd() -> String { + std::env::var("DYNWINRT_WIN32_WINMD") + .unwrap_or_else(|_| r"C:\s\win32metadata\Windows.Win32.winmd".to_string()) +} + +fn win32_available() -> bool { + Path::new(&win32_winmd()).exists() +} + +#[test] +fn required_win32_metadata_is_present() { + if std::env::var("DYNWINRT_REQUIRE_WIN32_METADATA").as_deref() == Ok("1") { + assert!( + win32_available(), + "DYNWINRT_REQUIRE_WIN32_METADATA=1 but metadata is missing at {}", + win32_winmd() + ); + } +} + +/// Resolve a `Windows.winmd` from the newest installed Windows SDK, matching +/// the discovery logic the codegen itself uses. Returns `None` if no SDK is +/// installed on this machine (the test that calls this should skip in that +/// case, consistent with other tests in this module). +fn discovered_windows_winmd() -> Option { + com_metadata::discover_newest_windows_winmd() +} + +// ------------------------------------------------------------------------- +// NORMAL tests +// ------------------------------------------------------------------------- + +/// 1. Parse ITaskbarList3 from Win32 metadata → correct IID. +#[test] +fn parse_itaskbarlist3_iid() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available at {}", &win32_winmd()); + return; + } + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .expect("ITaskbarList3 must exist in Win32 metadata"); + assert_eq!(com_iface.interface.name, "ITaskbarList3"); + assert_eq!(com_iface.interface.namespace, "Windows.Win32.UI.Shell"); + assert_eq!( + com_iface.interface.iid, + "ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf" + ); +} + +/// 2. Base-aware vtable slots: full interface_impls chain determines absolute slots. +/// ITaskbarList3 inherits: IUnknown (3 methods) + ITaskbarList (5) + ITaskbarList2 (1). +/// So HrInit = 3, SetProgressValue = 9, SetProgressState = 10. +#[test] +fn parse_itaskbarlist3_vtable_slots() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .expect("ITaskbarList3 must exist"); + + let by_name = |n: &str| -> usize { + com_iface + .interface + .methods + .iter() + .find(|m| m.name == n) + .unwrap_or_else(|| { + panic!( + "method {} not found (methods: {:?})", + n, + com_iface + .interface + .methods + .iter() + .map(|m| &m.name) + .collect::>() + ) + }) + .vtable_index + }; + + assert_eq!( + by_name("HrInit"), + 3, + "HrInit is the first ITaskbarList method after IUnknown" + ); + assert_eq!(by_name("AddTab"), 4); + assert_eq!(by_name("DeleteTab"), 5); + assert_eq!(by_name("ActivateTab"), 6); + assert_eq!(by_name("SetActiveAlt"), 7); + assert_eq!( + by_name("MarkFullscreenWindow"), + 8, + "ITaskbarList2's only method" + ); + assert_eq!(by_name("SetProgressValue"), 9); + assert_eq!(by_name("SetProgressState"), 10); +} + +/// 3. Base detection: ITaskbarList3 is IUnknown-rooted → base offset (first user +/// method slot) is 3, NOT 6. +#[test] +fn itaskbarlist3_is_iunknown_rooted() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); + assert_eq!(com_iface.base_offset, 3); + assert!(com_iface.is_iunknown_rooted); + // Base chain should include ITaskbarList2, ITaskbarList (and stop at IUnknown) + let base_names: Vec<&str> = com_iface.base_chain.iter().map(|s| s.as_str()).collect(); + assert_eq!( + base_names, + ["ITaskbarList2", "ITaskbarList", "IUnknown"], + "base chain order matters" + ); +} + +/// 4. CLSID resolution: ITaskbarList3 → TaskbarList coclass → CLSID +/// 56fdf344-fd6d-11d0-958a-006097c9a090 +#[test] +fn itaskbarlist3_clsid_resolution() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); + assert_eq!( + com_iface.coclass_clsid.as_deref(), + Some("56fdf344-fd6d-11d0-958a-006097c9a090") + ); + assert_eq!(com_iface.coclass_name.as_deref(), Some("TaskbarList")); +} + +/// 5. Param type mapping: HWND → pointer/handle, TBPFLAG → enum, HRESULT → void. +#[test] +fn param_type_mapping() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); + + // Generate wrapper as a text bundle we can inspect for the mapping decisions + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) + .expect("codegen must succeed for classic-COM interface"); + + let dts = out.dts.as_str(); + let js = out.js.as_str(); + + // HWND inputs accept Electron's pointer-width Buffer through the centralized + // runtime conversion, while the HWND value alias remains numeric. + assert!( + dts.contains("export type HWND = bigint | number;"), + "HWND value aliases should remain numeric in .d.ts, got:\n{}", + dts + ); + assert!( + dts.contains("hwnd: HWND | Buffer | Uint8Array") + && js.contains("DynCom.pointer(DynCom.handleValue(hwnd))") + && !js.contains("function _handleArg("), + "HWND inputs must use centralized DynCom.handleValue in .js, got:\n{}", + js + ); + + // ULONGLONG (U64) → bigint + // setProgressValue's completed/total params are U64 + assert!( + dts.contains("bigint"), + "U64 params should surface as bigint" + ); + + // TBPFLAG enum → surfaced by name (either an enum decl or a union) + assert!( + dts.contains("TBPFLAG") || dts.contains("TbpFlag"), + ".d.ts must reference the TBPFLAG enum:\n{}", + dts + ); + + // HRESULT-returning methods project to `void` (throw on failure); no HRESULT surface + assert!( + !dts.contains(": HRESULT") + && !dts.contains("-> HRESULT") + && !dts.contains("Promise"), + "HRESULT must not leak into the .d.ts surface:\n{}", + dts + ); + + // JS body: the SetProgressState signature must include u32 (TBPFLAG's underlying) for the enum arg + // Look for slot 10 invocation: + assert!( + js.contains("method(10)"), + ".js must call vtable slot 10 for SetProgressState" + ); + assert!( + js.contains("method(9)"), + ".js must call vtable slot 9 for SetProgressValue" + ); +} + +#[test] +fn shelllink_scalar_out_pointers_preserve_pointee_types() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let interface = + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "IShellLinkW") + .unwrap(); + + let get_show_cmd = interface + .interface + .methods + .iter() + .find(|method| method.name == "GetShowCmd") + .unwrap(); + assert!(matches!( + &get_show_cmd.params[0].typ, + TypeMeta::Enum { underlying, .. } if matches!(**underlying, TypeMeta::I32) + )); + let get_hotkey = interface + .interface + .methods + .iter() + .find(|method| method.name == "GetHotkey") + .unwrap(); + assert!(matches!(get_hotkey.params[0].typ, TypeMeta::U16)); + let get_icon_location = interface + .interface + .methods + .iter() + .find(|method| method.name == "GetIconLocation") + .unwrap(); + assert!(matches!(get_icon_location.params[2].typ, TypeMeta::I32)); + + let output = com::generate_com_interface_files(&interface, &win32_winmd()).unwrap(); + assert!( + output + .js + .contains(".addMethod('GetHotkey', new DynComMethodSig().addOut(DynCom.u16Type()))") + ); + assert!( + output + .js + .contains(".addMethod('GetShowCmd', new DynComMethodSig().addOut(DynCom.i32Type()))") + ); + assert!( + output + .dts + .contains("getIconLocation(cch?: number): [string, number];") + ); +} + +/// 6. Partial generation: generating a single class-name yields ONLY that +/// interface plus its immediate deps (enum, coclass metadata), NOT the +/// entire Windows.Win32.UI.Shell namespace. +#[test] +fn partial_generation_only_emits_target_interface() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) + .expect("codegen must succeed for classic-COM interface"); + + // Expected files: ITaskbarList3.js, ITaskbarList3.d.ts, TBPFLAG.js, TBPFLAG.d.ts + let file_names: Vec<&str> = out.extra_files.iter().map(|(n, _)| n.as_str()).collect(); + + // Should NOT include unrelated Shell types like IShellItem or IApplicationActivationManager + assert!( + !file_names.iter().any(|n| n.starts_with("IShellItem")), + "Partial generation must not include IShellItem: {:?}", + file_names + ); + assert!( + !file_names + .iter() + .any(|n| n.starts_with("IApplicationActivationManager")), + "Partial generation must not include unrelated types: {:?}", + file_names + ); + + // Should include TBPFLAG (a direct dep) + let has_tbpflag = file_names.iter().any(|n| n.starts_with("TBPFLAG")); + assert!( + has_tbpflag, + "TBPFLAG (direct enum dep) must be included: {:?}", + file_names + ); +} + +/// 7. Generated `.d.ts` has PascalCase type + camelCase methods and +/// no raw IID/vtable-index/CoCreateInstance leaked into the TYPED surface. +#[test] +fn dts_surface_is_natural_and_clean() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) + .expect("codegen must succeed for classic-COM interface"); + let dts = out.dts.as_str(); + + // PascalCase class name + assert!( + dts.contains("class ITaskbarList3"), + ".d.ts must export class ITaskbarList3, got:\n{}", + dts + ); + + // camelCase methods + for cc in &["hrInit", "setProgressValue", "setProgressState", "addTab"] { + assert!( + dts.contains(cc), + ".d.ts must declare camelCase method `{}`, got:\n{}", + cc, + dts + ); + } + // No PascalCase leaked method names + for pc in &[ + "HrInit(", + "SetProgressValue(", + "SetProgressState(", + "AddTab(", + ] { + assert!( + !dts.contains(pc), + ".d.ts must not expose PascalCase method `{}`, got:\n{}", + pc, + dts + ); + } + + // No raw IID leak in .d.ts + assert!( + !dts.contains("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf"), + "raw IID must not leak into .d.ts:\n{}", + dts + ); + // No raw CLSID leak + assert!( + !dts.contains("56fdf344-fd6d-11d0-958a-006097c9a090"), + "raw CLSID must not leak into .d.ts:\n{}", + dts + ); + // No CoCreateInstance leak + assert!( + !dts.contains("CoCreateInstance") && !dts.contains("coCreateInstance"), + "CoCreateInstance must not leak into .d.ts:\n{}", + dts + ); + // No vtable index leak in .d.ts + for slot in &["method(3)", "method(9)", "method(10)", "vtable"] { + assert!( + !dts.contains(slot), + "vtable detail `{}` must not appear in .d.ts:\n{}", + slot, + dts + ); + } +} + +/// 8. Generated `.js`: activation uses a CoCreateInstance path with CLSID + IID; +/// methods invoke at the correct base-aware slots. +#[test] +fn js_body_uses_cocreateinstance_and_correct_slots() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) + .expect("codegen must succeed for classic-COM interface"); + let js = out.js.as_str(); + + // CLSID + IID appear in .js + assert!( + js.contains("56fdf344-fd6d-11d0-958a-006097c9a090"), + ".js must embed the CLSID:\n{}", + js + ); + assert!( + js.contains("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf"), + ".js must embed the IID:\n{}", + js + ); + + // Activation via coCreateInstance + assert!( + js.contains("coCreateInstance"), + ".js must use coCreateInstance for activation:\n{}", + js + ); + + // Classic COM registration is kept out of the WinRT type namespace. + assert!( + js.contains("DynCom.registerIUnknownInterface"), + ".js must use DynCom registration for classic COM:\n{}", + js + ); + assert!(js.contains("Windows.Win32.UI.Shell.ITaskbarList3")); + + // Base-aware slots + assert!(js.contains("method(3)"), "HrInit slot 3"); + assert!(js.contains("method(9)"), "SetProgressValue slot 9"); + assert!(js.contains("method(10)"), "SetProgressState slot 10"); + + // Should NOT contain WinRT `.method(6)` for a user method (that would be + // the IInspectable-rooted slot for the first user method). + // HrInit at slot 6 would be the failing case — we accept `method(6)` only + // if that's ActivateTab (slot 6). ActivateTab IS at 6, so it's a valid + // occurrence. Just check the file doesn't say something like `HrInit ... method(6)`. + // This is covered by the exact per-method assertion above. +} + +// ------------------------------------------------------------------------- +// CORNER tests +// ------------------------------------------------------------------------- + +/// 9. Regression: WinRT-style (IInspectable-based) interfaces still compute +/// base offset 6 (i.e. the existing WinRT path is unaffected). +#[test] +fn winrt_interfaces_still_use_offset_6() { + // Parse a well-known WinRT interface (Windows.Foundation.IUriRuntimeClass or similar) + // via the existing WinRT path — its first method should still have vtable_index = 6. + let Some(windows_winmd) = discovered_windows_winmd() else { + eprintln!( + "Skipping winrt_interfaces_still_use_offset_6: no Windows SDK Windows.winmd discoverable" + ); + return; + }; + // Take Windows.Foundation.Uri's default interface — pick one that has methods. + let class = meta::parse_class(&windows_winmd, "Windows.Foundation", "Uri") + .expect("Windows.Foundation.Uri must be present"); + let default_iface = class + .default_interface + .as_ref() + .expect("Uri must have a default interface"); + + // Its first method's vtable_index must still be 6 (unchanged from existing + // WinRT behavior); classic-COM support must not regress this. + let first_slot = default_iface + .methods + .first() + .map(|m| m.vtable_index) + .expect("Uri default interface must have methods"); + assert_eq!( + first_slot, 6, + "WinRT interfaces retain the IInspectable base offset of 6" + ); +} + +/// 10. Interface-not-found is a clean Option::None, not a panic. +#[test] +fn interface_not_found_is_clean_none() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let missing = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IDoesNotExist_XYZ", + ); + assert!(missing.is_none()); +} + +/// 11. QI-only interface (no coclass) → wrapper emitted WITHOUT `create()`, +/// only a static from-raw / QI entry point. +#[test] +fn qi_only_interface_has_no_create() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + // IPersist is IUnknown-rooted (has 1 own method: GetClassID) and has NO + // "Persist" coclass anywhere in the metadata — verified via probe. + let com_iface = + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.System.Com", "IPersist") + .expect("IPersist must exist in Win32 metadata"); + assert!( + com_iface.coclass_clsid.is_none(), + "IPersist has no associated coclass CLSID" + ); + let get_class_id = com_iface + .interface + .methods + .iter() + .find(|method| method.name == "GetClassID") + .unwrap(); + assert!(matches!(get_class_id.params[0].typ, TypeMeta::Guid)); + + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) + .expect("codegen must succeed for classic-COM interface"); + let js = out.js.as_str(); + let dts = out.dts.as_str(); + assert!(js.contains(".addOut(DynCom.guidType())")); + + // No `create()` in either surface + assert!( + !dts.contains("static create()") && !dts.contains("static create(): "), + "QI-only interface must not expose static create() in .d.ts:\n{}", + dts + ); + assert!( + !js.contains("coCreateInstance"), + "QI-only interface must not call coCreateInstance in .js:\n{}", + js + ); + + // Must still have a fromNative / QI-only entry + assert!( + js.contains("_fromNative") || js.contains("fromRaw"), + "QI-only interface must expose a from-raw entry:\n{}", + js + ); + + // Slot 3 for GetClassID (only method, IUnknown-rooted) + assert!( + js.contains("method(3)"), + "IPersist.GetClassID must invoke slot 3:\n{}", + js + ); +} + +/// 12. Determinism: regenerating ITaskbarList3 twice produces byte-identical output. +#[test] +fn generation_is_deterministic() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + let a = { + let com = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); + com::generate_com_interface_files(&com, &win32_winmd()) + .expect("codegen must succeed for classic-COM interface") + }; + let b = { + let com = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .unwrap(); + com::generate_com_interface_files(&com, &win32_winmd()) + .expect("codegen must succeed for classic-COM interface") + }; + assert_eq!(a.js, b.js); + assert_eq!(a.dts, b.dts); + assert_eq!(a.extra_files, b.extra_files); +} + +// ------------------------------------------------------------------------- +// SNAPSHOT test +// ------------------------------------------------------------------------- + +/// Snapshot test: lock generated ITaskbarList3 .js + .d.ts against committed files. +/// +/// To update snapshots after an intentional change: +/// cargo run -p dynwinrt-codegen -- generate \ +/// --winmd C:\s\win32metadata\Windows.Win32.winmd \ +/// --namespace Windows.Win32.UI.Shell \ +/// --class-name ITaskbarList3 \ +/// --output tools/dynwinrt-codegen/tests/snapshots/itaskbarlist3 +#[test] +fn snapshot_itaskbarlist3() { + if !win32_available() { + eprintln!("Skipping snapshot test: Win32 winmd not available"); + return; + } + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .expect("ITaskbarList3 must exist"); + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) + .expect("codegen must succeed for classic-COM interface"); + + let snapshot_dir: PathBuf = + Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/snapshots/itaskbarlist3"); + assert!( + snapshot_dir.exists(), + "Snapshot directory not found: {}", + snapshot_dir.display() + ); + + let mut generated: Vec<(String, String)> = Vec::new(); + generated.push(("ITaskbarList3.js".into(), out.js.clone())); + generated.push(("ITaskbarList3.d.ts".into(), out.dts.clone())); + for (name, content) in &out.extra_files { + generated.push((name.clone(), content.clone())); + } + + let mut mismatches = Vec::new(); + for (name, actual) in &generated { + let path = snapshot_dir.join(name); + if !path.exists() { + mismatches.push(format!(" missing snapshot: {}", name)); + continue; + } + let expected = fs::read_to_string(&path).unwrap(); + if actual.trim_end() != expected.trim_end() { + mismatches.push(format!(" differs: {}", name)); + } + } + + // Any extra snapshot file not produced by the generator is also a mismatch. + if let Ok(entries) = fs::read_dir(&snapshot_dir) { + let names: std::collections::HashSet = + generated.iter().map(|(n, _)| n.clone()).collect(); + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if !names.contains(&name) { + mismatches.push(format!(" extra snapshot not generated: {}", name)); + } + } + } + + if !mismatches.is_empty() { + panic!( + "ITaskbarList3 snapshot mismatch!\n{}\n\n\ + To update, re-run the generator or copy the actual output into the snapshot dir.", + mismatches.join("\n") + ); + } +} + +// ------------------------------------------------------------------------- +// --import-name honored by classic-COM path +// ------------------------------------------------------------------------- + +/// Regression test for a bug where the classic-COM generator hardcoded the +/// runtime import as `'@microsoft/dynwinrt'`, ignoring the `--import-name` +/// CLI flag (which the WinRT path already honored via +/// `codegen::project::set_import_name`). Fixing this makes it possible to +/// regenerate the Node E2E wrappers from `Windows.Win32.winmd` without +/// hand-patching the import line. +/// +/// The test uses the same thread-local as `set_import_name`, so it +/// save/restores the default around the assertion to avoid contaminating +/// other tests that assume the `@microsoft/dynwinrt` default (notably the +/// snapshot tests). `#[serial]` is intentionally NOT used — because +/// `RUNTIME_IMPORT_NAME` is a `thread_local!`, cargo's parallel test runner +/// gives each thread its own copy; restoring on the same thread is enough. +#[test] +fn import_name_flag_is_honored_by_com_path() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let previous = get_import_name(); + set_import_name("../dist/com.js"); + + let result = std::panic::catch_unwind(|| { + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .expect("ITaskbarList3 must exist"); + com::generate_com_interface_files(&com_iface, &win32_winmd()) + .expect("codegen must succeed for classic-COM interface") + }); + + // Always restore before propagating any assertion failure. + set_import_name(&previous); + + let out = result.unwrap_or_else(|e| std::panic::resume_unwind(e)); + + // Custom import must appear on the runtime import line... + assert!( + out.js.contains("from '../dist/com.js'"), + "classic-COM .js must honor --import-name (expected `from '../dist/com.js'`):\n{}", + out.js + ); + // ...and the hardcoded default must NOT be present in the generated body. + assert!( + !out.js.contains("'@microsoft/dynwinrt'"), + "classic-COM .js must NOT hardcode '@microsoft/dynwinrt' when --import-name is set:\n{}", + out.js + ); + + // Sanity: after restoring the default, subsequent generation reverts. + let default_out = { + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "ITaskbarList3", + ) + .expect("ITaskbarList3 must exist"); + com::generate_com_interface_files(&com_iface, &win32_winmd()) + .expect("codegen must succeed for classic-COM interface") + }; + assert!( + default_out.js.contains("from '@microsoft/dynwinrt/com'"), + "after restoring, default import must use '@microsoft/dynwinrt/com':\n{}", + default_out.js + ); +} + +/// Same test for the interop bridge generation path. +#[test] +fn import_name_flag_is_honored_by_interop_wrapper() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + if discovered_windows_winmd().is_none() { + eprintln!( + "Skipping: no Windows SDK Windows.winmd discoverable (needed for interop resolution)" + ); + return; + } + + let previous = get_import_name(); + set_import_name("../dist/com.js"); + + let result = std::panic::catch_unwind(|| { + let com_iface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IDataTransferManagerInterop", + ) + .expect("IDataTransferManagerInterop must exist"); + com::generate_com_interface_files(&com_iface, &win32_winmd()) + .expect("codegen must succeed for classic-COM interop interface") + }); + + set_import_name(&previous); + let out = result.unwrap_or_else(|e| std::panic::resume_unwind(e)); + + // The interop .js itself must honor the flag. + assert!( + out.js.contains("from '../dist/com.js'"), + "interop .js must honor --import-name:\n{}", + out.js + ); + assert!( + !out.js.contains("'@microsoft/dynwinrt'"), + "interop .js must NOT hardcode '@microsoft/dynwinrt':\n{}", + out.js + ); + + assert!( + out.dts.contains("from '../dist/com.js'"), + "interop .d.ts must honor --import-name:\n{}", + out.dts + ); + assert!( + !out.extra_files + .iter() + .any(|(name, _)| name.starts_with("DataTransferManager.")), + "COM codegen must not emit a projected WinRT companion" + ); +} + +#[test] +fn shellitem_getdisplayname_is_not_classified_as_caller_owned_string_buffer() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let com_iface = + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "IShellItem") + .expect("IShellItem must exist"); + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) + .expect("codegen must succeed for IShellItem"); + + assert!( + out.js.contains(".addMethod('GetDisplayName', new DynComMethodSig().addIn(DynCom.i32Type()).addOut(DynCom.pointerType()))"), + "PWSTR* callee-allocated output must remain addOut(pointer), not caller-owned buffer:\n{}", + out.js + ); + assert!( + !out.js.contains("getDisplayName(sigdnName = 260)") + && !out.js.contains("_decodeWideString"), + "IShellItem.GetDisplayName must not allocate/decode a caller-owned buffer:\n{}", + out.js + ); +} + +#[test] +fn u16_input_param_uses_existing_u16_value_ctor_not_u16value() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + // IShellLinkW.SetHotkey takes a [in] u16 (WORD). The classic-COM value + // ctor is DynCom.u16(...) — there is no `u16Value`/`i16Value`. Regression + // guard: the arg-wrapper must emit the ctor that actually exists, or the + // generated call throws at runtime. + let com_iface = + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "IShellLinkW") + .expect("IShellLinkW must exist"); + let out = com::generate_com_interface_files(&com_iface, &win32_winmd()) + .expect("codegen must succeed for IShellLinkW"); + + assert!( + out.js.contains("DynCom.u16(wHotkey)"), + "u16 input param must wrap via the existing DynCom.u16(...):\n{}", + out.js + ); + assert!( + !out.js.contains("u16Value(") && !out.js.contains("i16Value("), + "codegen must not emit non-existent u16Value/i16Value ctor:\n{}", + out.js + ); +} + +#[test] +fn sid_buffers_keep_data_pointer_address_semantics() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.Storage.FileSystem", + "IDiskQuotaControl", + ) + .expect("IDiskQuotaControl must exist"); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("IDiskQuotaControl generation should succeed"); + + assert!( + output + .dts + .contains("addUserSid(pUserSid: PSID | Buffer | Uint8Array"), + "PSID input must accept backing storage rather than handle bytes:\n{}", + output.dts + ); + assert!( + output.js.contains("DynCom.pointer(pUserSid)") + && !output.js.contains("handleValue(pUserSid)"), + "PSID Buffer must pass its address, not decoded contents:\n{}", + output.js + ); +} + +#[test] +fn native_array_buffers_fail_closed() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.Storage.Imapi", + "IDiscRecorder", + ) + .expect("IDiscRecorder must exist"); + let error = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect_err("NativeArrayInfo byte buffers must not become scalar in/out storage"); + + assert!( + error.contains("GetRecorderGUID") + && error.contains("pbyUniqueID") + && error.contains("caller-sized native buffers are not supported"), + "generation must fail with a targeted buffer diagnostic: {error}" + ); +} + +#[test] +fn pointer_sized_integers_use_runtime_width() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.ClrHosting", + "IApartmentCallback", + ) + .expect("IApartmentCallback must exist"); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("pointer-sized parameters should be supported"); + + assert!( + output + .js + .contains(".addIn(DynCom.usizeType()).addIn(DynCom.usizeType())"), + "USize parameters must use runtime-width ABI types:\n{}", + output.js + ); + assert!( + output.js.contains("DynCom.usize(BigInt(pFunc))") + && output.js.contains("DynCom.usize(BigInt(pData))"), + "USize values must use runtime-width constructors:\n{}", + output.js + ); + assert!( + !output.js.contains("DynCom.u64Type()"), + "pointer-sized parameters must not be fixed to 64 bits:\n{}", + output.js + ); +} + +#[test] +fn required_parameters_after_string_buffer_count_remain_required() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IExtractImage", + ) + .expect("IExtractImage must exist"); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("IExtractImage generation should succeed"); + + assert!( + output + .js + .contains("getLocation(cch, pdwPriority, prgSize, recClrDepth, pdwFlags)"), + "required parameters, including cch before them, must not get defaults:\n{}", + output.js + ); + assert!( + !output.js.contains("prgSize = 0") + && !output.js.contains("dwRecClrDepth = 0") + && !output.js.contains("pdwFlags = 0"), + "required native arguments must not be silently defaulted:\n{}", + output.js + ); + assert!( + output.dts.contains( + "getLocation(cch: number, pdwPriority: number, prgSize: bigint | Buffer, recClrDepth: number, pdwFlags: number)" + ), + "declarations must keep the parameters required:\n{}", + output.dts + ); +} + +#[test] +fn bstr_outputs_are_decoded_and_freed() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.System.Com", "IErrorInfo") + .expect("IErrorInfo must exist"); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("IErrorInfo generation should succeed"); + + assert!( + output.js.contains("return DynCom.takeBstr(_out);"), + "BSTR outputs must be converted through the freeing helper:\n{}", + output.js + ); + assert!( + output.dts.contains("getDescription(): string;"), + "BSTR outputs must project as strings:\n{}", + output.dts + ); +} + +#[test] +fn unsigned_enum_values_preserve_their_value() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let enum_type = com_metadata::parse_com_enum( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "FILEOPERATION_FLAGS", + ) + .expect("FILEOPERATION_FLAGS must exist"); + let value = enum_type + .members + .iter() + .find(|member| member.name == "FOFX_DONTDISPLAYLOCATIONS") + .expect("FOFX_DONTDISPLAYLOCATIONS must exist") + .value + .clone(); + + assert!(matches!(enum_type.underlying, TypeMeta::U32)); + assert_eq!(value, com_metadata::ComEnumValue::Unsigned(2_147_483_648)); + + let shared_type = meta::parse_enums(&win32_winmd(), "Windows.Win32.UI.Shell") + .into_iter() + .find(|typ| { + matches!( + typ, + TypeMeta::Enum { name, .. } if name == "FILEOPERATION_FLAGS" + ) + }) + .expect("shared enum parser must still find FILEOPERATION_FLAGS"); + let TypeMeta::Enum { + underlying, + members, + .. + } = shared_type + else { + unreachable!() + }; + assert!( + matches!(*underlying, TypeMeta::I32), + "the shared WinRT model must remain unchanged" + ); + assert_eq!( + members + .iter() + .find(|member| member.name == "FOFX_DONTDISPLAYLOCATIONS") + .unwrap() + .value, + i32::MIN, + "unsigned Win32 values must be corrected only in the COM-local model" + ); +} + +#[test] +fn optional_string_buffer_placeholders_do_not_precede_required_parameters() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.UI.Shell", "IShellLinkW") + .expect("IShellLinkW must exist"); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("IShellLinkW generation should succeed"); + + assert!( + output + .dts + .contains("getPath(cch: number, pfd: bigint | Buffer, fFlags: number)"), + "a required parameter must not follow an optional pfd placeholder:\n{}", + output.dts + ); + assert!( + !output.js.contains("getPath(cch = 260") && !output.js.contains("pfd = 0, fFlags"), + "JavaScript defaults must obey the same trailing-optional rule:\n{}", + output.js + ); +} + +#[test] +fn namespace_mode_rejects_classic_com_instead_of_using_winrt_slots() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let output = Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args([ + "generate", + "--winmd", + &win32_winmd(), + "--namespace", + "Windows.Win32.UI.Shell", + "--dry-run", + ]) + .output() + .expect("spawn dynwinrt-codegen"); + let stderr = String::from_utf8_lossy(&output.stderr); + + assert!(!output.status.success(), "namespace mode must fail closed"); + assert!( + stderr.contains("classic-COM namespace projection is not supported") + && stderr.contains("--class-name"), + "failure must direct callers to the safe class mode:\n{stderr}" + ); +} + +#[test] +fn correlation_vector_hstring_output_is_owned_and_projected_as_string() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.WinRT", + "ICorrelationVectorSource", + ) + .expect("ICorrelationVectorSource must exist"); + let method = interface + .interface + .methods + .iter() + .find(|method| method.name == "get_CorrelationVector") + .expect("get_CorrelationVector must exist"); + + assert!(matches!(method.params[0].typ, TypeMeta::String)); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("HSTRING output generation must succeed"); + assert!(output.js.contains(".addOut(DynCom.hstringType())")); + assert!(output.js.contains("return _out.toString();")); + assert!(output.dts.contains("get_CorrelationVector(): string;")); +} + +#[test] +fn unresolved_external_interface_fails_until_reference_metadata_is_loaded() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let unresolved = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.WinRT.Composition", + "ICompositorInterop", + ) + .expect("ICompositorInterop must exist"); + let error = com::generate_com_interface_files(&unresolved, &win32_winmd()) + .expect_err("missing Windows metadata must fail closed"); + assert!(error.contains("ICompositionSurface")); + assert!(error.contains("--ref")); + + let Some(windows_winmd) = discovered_windows_winmd() else { + eprintln!("Skipping resolved-reference half: Windows.winmd not available"); + return; + }; + let metadata = format!("{};{}", win32_winmd(), windows_winmd); + let resolved = com_metadata::parse_com_interface( + &metadata, + "Windows.Win32.System.WinRT.Composition", + "ICompositorInterop", + ) + .expect("ICompositorInterop must resolve with Windows.winmd"); + let output = com::generate_com_interface_files(&resolved, &metadata) + .expect("resolved external interface generation must succeed"); + + let create_graphics_device = resolved + .interface + .methods + .iter() + .find(|method| method.name == "CreateGraphicsDevice") + .expect("CreateGraphicsDevice must exist"); + let TypeMeta::RuntimeClass { + default_interface: Some(default_interface), + .. + } = &create_graphics_device.params[1].typ + else { + panic!("CreateGraphicsDevice must return a resolved runtime class"); + }; + let TypeMeta::Interface { iid, .. } = default_interface.as_ref() else { + panic!("runtime class default must resolve to an interface"); + }; + assert!(!iid.is_empty()); + assert!(output.js.contains(&format!( + ".addOut(DynCom.interfaceType(WinGuid.parse('{iid}')))" + ))); + assert!(output.dts.contains("DynWinRtValue")); +} + +#[test] +fn semantic_hresult_preserves_metadata_and_documented_contracts() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let mut interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.Com", + "IPersistFile", + ) + .expect("IPersistFile must exist"); + let is_dirty = interface + .interface + .methods + .iter() + .find(|method| method.name == "IsDirty") + .expect("IsDirty must exist"); + let load = interface + .interface + .methods + .iter() + .find(|method| method.name == "Load") + .expect("Load must exist"); + let get_cur_file = interface + .interface + .methods + .iter() + .find(|method| method.name == "GetCurFile") + .expect("GetCurFile must exist"); + + assert!(is_dirty.preserve_hresult); + assert!(get_cur_file.preserve_hresult); + assert!(!load.preserve_hresult); + + let mut get_cur_file_interface = interface.clone(); + interface + .interface + .methods + .retain(|method| method.name == "IsDirty"); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("semantic HRESULT generation must succeed"); + assert!(output.js.contains(".preserveHresult()")); + assert!(output.js.contains("return DynCom.toNumber(_out);")); + assert!(output.dts.contains("isDirty(): number;")); + + get_cur_file_interface + .interface + .methods + .retain(|method| method.name == "GetCurFile"); + let output = com::generate_com_interface_files(&get_cur_file_interface, &win32_winmd()) + .expect("GetCurFile semantic HRESULT generation must succeed"); + assert!(output.js.contains(".preserveHresult()")); + assert!(output.js.contains(".invokeAll(")); + assert!(output.js.contains("DynCom.toNumber(_r[0])")); + assert!(output.js.contains("DynCom.takeCoTaskMemWideString(_r[1])")); + assert!(output.dts.contains("getCurFile(): [number, string];")); +} + +#[test] +fn scalar_typedef_uses_its_underlying_abi_not_pointer_abi() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.UI.Shell", + "IPreviewHandlerVisuals", + ) + .expect("IPreviewHandlerVisuals must exist"); + let output = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect("COLORREF scalar typedef must generate"); + + assert!(output.dts.contains("export type COLORREF = number;")); + assert!( + output + .dts + .contains("setBackgroundColor(color: COLORREF): void;") + ); + assert!(output.js.contains(".addIn(DynCom.u32Type())")); + assert!(output.js.contains("DynCom.u32(color)")); + assert!(!output.js.contains("DynCom.pointer(color)")); +} + +#[test] +fn metadata_delegate_parameter_fails_closed_as_a_delegate() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.Graphics.Direct2D", + "ID2D1Factory1", + ) + .expect("ID2D1Factory1 must exist"); + let register = interface + .interface + .methods + .iter() + .find(|method| method.name == "RegisterEffectFromStream") + .expect("RegisterEffectFromStream must exist"); + assert!(register.params.iter().any(|param| { + matches!( + ¶m.typ, + TypeMeta::Delegate { name, .. } if name == "PD2D1_EFFECT_FACTORY" + ) + })); + + let error = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect_err("delegate parameters require a managed callback projection"); + assert!(error.contains("PD2D1_EFFECT_FACTORY")); + assert!(error.contains("managed callback projection")); +} + +#[test] +fn by_value_guid_is_not_treated_as_a_dynamic_iid_pointer() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let interface = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.WinRT.Display", + "IDisplayDeviceInterop", + ) + .expect("IDisplayDeviceInterop must exist"); + let open = interface + .interface + .methods + .iter() + .find(|method| method.name == "OpenSharedHandle") + .expect("OpenSharedHandle must exist"); + assert!(matches!(open.params[1].typ, TypeMeta::Guid)); + + let error = com::generate_com_interface_files(&interface, &win32_winmd()) + .expect_err("by-value GUID plus void** must not be treated as REFIID interop"); + assert!(error.contains("untyped pointer output has no ownership projection")); +} + +#[test] +fn com_only_generation_emits_an_importable_package_shape() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let output_dir = std::env::temp_dir().join(format!( + "dynwinrt-codegen-com-package-{}", + std::process::id() + )); + if output_dir.exists() { + fs::remove_dir_all(&output_dir).expect("remove stale COM package test directory"); + } + + let output = Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args([ + "generate", + "--winmd", + &win32_winmd(), + "--namespace", + "Windows.Win32.UI.Shell", + "--class-name", + "ITaskbarList3", + "--output", + output_dir.to_str().unwrap(), + ]) + .output() + .expect("spawn dynwinrt-codegen"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(output.status.success(), "COM generation failed:\n{stderr}"); + + for name in ["index.js", "index.d.ts", "package.json"] { + assert!( + output_dir.join(name).is_file(), + "COM-only output must include {name}" + ); + } + let index = fs::read_to_string(output_dir.join("index.js")).unwrap(); + assert!(index.contains("ITaskbarList3") && index.contains("TBPFLAG")); + let package = fs::read_to_string(output_dir.join("package.json")).unwrap(); + assert!(package.contains("\"type\": \"module\"")); + assert!(package.contains("\"./ITaskbarList3\"")); + + let incremental = Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args([ + "generate", + "--winmd", + &win32_winmd(), + "--namespace", + "Windows.Win32.UI.Shell", + "--class-name", + "IShellLinkW", + "--output", + output_dir.to_str().unwrap(), + ]) + .output() + .expect("spawn incremental dynwinrt-codegen"); + let incremental_stderr = String::from_utf8_lossy(&incremental.stderr); + assert!( + incremental.status.success(), + "incremental COM generation failed:\n{incremental_stderr}" + ); + let incremental_index = fs::read_to_string(output_dir.join("index.js")).unwrap(); + assert!( + incremental_index.contains("ITaskbarList3") + && incremental_index.contains("IShellLinkW") + && incremental_index.contains("TBPFLAG") + && incremental_index.contains("SHOW_WINDOW_CMD"), + "incremental generation must preserve earlier exports:\n{incremental_index}" + ); + let incremental_package = fs::read_to_string(output_dir.join("package.json")).unwrap(); + assert!( + incremental_package.contains("\"./ITaskbarList3\"") + && incremental_package.contains("\"./IShellLinkW\""), + "incremental generation must preserve earlier package subpaths:\n{incremental_package}" + ); + + fs::remove_dir_all(&output_dir).expect("remove COM package test directory"); +}