From 0bb3ae4cff9da0615e777de5444636a64fe988b2 Mon Sep 17 00:00:00 2001 From: "Leilei Zhang (from Dev Box)" Date: Tue, 18 Aug 2026 11:04:27 +0800 Subject: [PATCH 1/4] Add safe flat Win32 code generation Introduce metadata-driven flat Win32 call planning, safe and unsafe JavaScript entrypoints, resource ownership, pointer-bearing structures, OVERLAPPED I/O, coverage reporting, samples, documentation, and end-to-end tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6307886d-3c24-4596-8924-ba44b0e850a5 --- .github/workflows/build.yml | 22 + .gitignore | 6 + Cargo.lock | 2 + README.md | 7 + bindings/js/Cargo.toml | 2 + bindings/js/README.md | 7 + bindings/js/__test__/index.spec.ts | 219 +- bindings/js/package.json | 18 + bindings/js/scripts/generate-entrypoints.mjs | 39 +- bindings/js/src/com.rs | 146 +- bindings/js/src/lib.rs | 5 + bindings/js/src/win32.rs | 2248 +++++++++++++++++ crates/dynwinrt/Cargo.toml | 4 + crates/dynwinrt/src/call.rs | 31 + crates/dynwinrt/src/lib.rs | 1 + crates/dynwinrt/src/win32.rs | 1585 ++++++++++++ docs/architecture/flat-win32-support.md | 200 ++ docs/guides/windows/flat-win32-usage.md | 139 + samples/js/win32/.gitignore | 3 + samples/js/win32/README.md | 63 + samples/js/win32/generate.ps1 | 52 + samples/js/win32/overlapped-file.mjs | 51 + samples/js/win32/package.json | 13 + samples/js/win32/registry-product-name.mjs | 42 + samples/js/win32/system-info.mjs | 26 + tests/e2e/e2e_test.ps1 | 83 +- tests/e2e/runners/win32/registry.mjs | 55 + tests/e2e/runners/win32/returns.mjs | 453 ++++ tools/dynwinrt-codegen/Cargo.toml | 3 +- tools/dynwinrt-codegen/src/codegen/mod.rs | 1 + tools/dynwinrt-codegen/src/codegen/package.rs | 76 +- .../dynwinrt-codegen/src/codegen/win32/ir.rs | 489 ++++ .../dynwinrt-codegen/src/codegen/win32/mod.rs | 1225 +++++++++ .../src/codegen/win32/model.rs | 1467 +++++++++++ .../src/codegen/win32/project.rs | 1052 ++++++++ .../src/codegen/win32/render.rs | 983 +++++++ tools/dynwinrt-codegen/src/lib.rs | 1 + tools/dynwinrt-codegen/src/main.rs | 570 ++++- tools/dynwinrt-codegen/src/win32_metadata.rs | 1530 +++++++++++ .../tests/win32_bindgen_oracle_test.rs | 24 + .../dynwinrt-codegen/tests/win32_flat_test.rs | 745 ++++++ 41 files changed, 13655 insertions(+), 33 deletions(-) create mode 100644 bindings/js/src/win32.rs create mode 100644 crates/dynwinrt/src/win32.rs create mode 100644 docs/architecture/flat-win32-support.md create mode 100644 docs/guides/windows/flat-win32-usage.md create mode 100644 samples/js/win32/.gitignore create mode 100644 samples/js/win32/README.md create mode 100644 samples/js/win32/generate.ps1 create mode 100644 samples/js/win32/overlapped-file.mjs create mode 100644 samples/js/win32/package.json create mode 100644 samples/js/win32/registry-product-name.mjs create mode 100644 samples/js/win32/system-info.mjs create mode 100644 tests/e2e/runners/win32/registry.mjs create mode 100644 tests/e2e/runners/win32/returns.mjs create mode 100644 tools/dynwinrt-codegen/src/codegen/win32/ir.rs create mode 100644 tools/dynwinrt-codegen/src/codegen/win32/mod.rs create mode 100644 tools/dynwinrt-codegen/src/codegen/win32/model.rs create mode 100644 tools/dynwinrt-codegen/src/codegen/win32/project.rs create mode 100644 tools/dynwinrt-codegen/src/codegen/win32/render.rs create mode 100644 tools/dynwinrt-codegen/src/win32_metadata.rs create mode 100644 tools/dynwinrt-codegen/tests/win32_bindgen_oracle_test.rs create mode 100644 tools/dynwinrt-codegen/tests/win32_flat_test.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b6d083a3..5061ea4a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -58,6 +58,24 @@ jobs: throw "Classic COM coverage fell below 70%: $($result.coverage_percent)" } Write-Host "Classic COM coverage: $($result.complete_interfaces)/$($result.eligible_interfaces) ($($result.coverage_percent)%)" + - name: Enforce flat Win32 coverage baseline + shell: pwsh + run: | + $json = cargo run -p dynwinrt-codegen --quiet -- win32-census ` + --winmd $env:DYNWINRT_WIN32_WINMD ` + --json + if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + $result = $json | ConvertFrom-Json + if ($result.eligible_functions -ne 18321) { + throw "Flat Win32 census denominator changed: $($result.eligible_functions)" + } + if ($result.complete_functions -lt 8959) { + throw "Flat Win32 complete coverage regressed: $($result.complete_functions)" + } + if ($result.coverage_percent -lt 48.9) { + throw "Flat Win32 coverage fell below 48.9%: $($result.coverage_percent)" + } + Write-Host "Flat Win32 coverage: $($result.complete_functions)/$($result.eligible_functions) ($($result.coverage_percent)%)" - name: Test Classic COM failure cleanup contracts run: | cargo test -p dynwinrt failing_hresult_releases_written_interface_output @@ -193,3 +211,7 @@ jobs: bindings/js/dist/com.d.ts bindings/js/dist/com-unsafe.js bindings/js/dist/com-unsafe.d.ts + bindings/js/dist/win32.js + bindings/js/dist/win32.d.ts + bindings/js/dist/win32-unsafe.js + bindings/js/dist/win32-unsafe.d.ts diff --git a/.gitignore b/.gitignore index 3d125e27..f6e9a947 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,12 @@ mono_crash.* x64/ x86/ [Ww][Ii][Nn]32/ +!tools/dynwinrt-codegen/src/codegen/win32/ +!tools/dynwinrt-codegen/src/codegen/win32/** +!tests/e2e/runners/win32/ +!tests/e2e/runners/win32/** +!samples/js/win32/ +!samples/js/win32/** [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ [Aa][Rr][Mm]64[Ee][Cc]/ diff --git a/Cargo.lock b/Cargo.lock index 3c494e51..a7695e3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -501,6 +501,7 @@ name = "jswinrt_rs" version = "0.1.0" dependencies = [ "dynwinrt", + "libffi", "napi", "napi-build", "napi-derive", @@ -508,6 +509,7 @@ dependencies = [ "serde_json", "windows", "windows-future", + "windows-link", "windows-string", ] diff --git a/README.md b/README.md index a37b702c..614da868 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,13 @@ common-interface test matrix, unsupported native types, and ownership rules. See [Classic COM JavaScript usage](docs/guides/windows/classic-com-usage.md) for codegen, GUID/IID/CLSID, lifecycle, Automation, and explicit unsafe ABI examples. +Flat Win32 `[DllImport]` bindings use the separate +`@microsoft/dynwinrt/win32` runtime and generated namespace subpaths. Numeric +data addresses require `@microsoft/dynwinrt/win32/unsafe`; safe wrappers retain +Buffer storage and manage owned handles. See +[Flat Win32 architecture](docs/architecture/flat-win32-support.md) and +[Flat Win32 usage](docs/guides/windows/flat-win32-usage.md). + 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 diff --git a/bindings/js/Cargo.toml b/bindings/js/Cargo.toml index d7f11c06..392b08d1 100644 --- a/bindings/js/Cargo.toml +++ b/bindings/js/Cargo.toml @@ -22,6 +22,8 @@ windows-future = "0.3.2" windows-string = "0.0.0" pollster = "0.4.0" serde_json = "1" +libffi = "5.1.0" +windows-link = "0.2.1" [dependencies.windows] version = ">=0.59, <=0.62" diff --git a/bindings/js/README.md b/bindings/js/README.md index 7f4b0bc6..55f26df2 100644 --- a/bindings/js/README.md +++ b/bindings/js/README.md @@ -62,6 +62,13 @@ See the repository's [Classic COM JavaScript usage guide](../../docs/guides/windows/classic-com-usage.md) for codegen, GUID/IID/CLSID, lifetime, Automation, and `/com/unsafe` examples. +Flat Win32 exports use `@microsoft/dynwinrt/win32`. Generated wrappers bind an +immutable native call plan, accept retained Buffer storage for dereferenced +pointers, and return `DynWin32Resource` for owned handles. Arbitrary numeric +addresses and manual raw ABI plans require +`@microsoft/dynwinrt/win32/unsafe`. See the +[flat Win32 usage guide](../../docs/guides/windows/flat-win32-usage.md). + 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__/index.spec.ts b/bindings/js/__test__/index.spec.ts index e2db2a3c..25f12553 100644 --- a/bindings/js/__test__/index.spec.ts +++ b/bindings/js/__test__/index.spec.ts @@ -31,12 +31,20 @@ import { DynComUnsafe, DynComVariant, } from '../dist/com-unsafe.js' +import { DynWin32 } from '../dist/win32.js' +import * as win32Runtime from '../dist/win32.js' +import { + DynWin32Function, + DynWin32Unsafe, +} from '../dist/win32-unsafe.js' const requireFromTest = createRequire(import.meta.url) const nativeRuntime = requireFromTest('../dist/index.js') as Record const winrtCjsRuntime = requireFromTest('../dist/winrt.js') as Record const comCjsRuntime = requireFromTest('../dist/com.js') as Record const unsafeComRuntime = requireFromTest('../dist/com-unsafe.js') as Record +const win32CjsRuntime = requireFromTest('../dist/win32.js') as Record +const unsafeWin32Runtime = requireFromTest('../dist/win32-unsafe.js') as Record const moduleKeys = (value: object) => Object.keys(value) @@ -110,7 +118,9 @@ test('Classic COM is isolated from the WinRT root entrypoint', (t) => { test('package facades exactly partition native exports', (t) => { const nativeKeys = moduleKeys(nativeRuntime) - const expectedWinrt = nativeKeys.filter((name) => !name.startsWith('DynCom') && name !== 'initializeCom') + const expectedWinrt = nativeKeys.filter( + (name) => !name.startsWith('DynCom') && !name.startsWith('DynWin32') && name !== 'initializeCom', + ) const safeComNames = new Set([ 'DynComDispatchParams', 'DynComAllocation', @@ -137,6 +147,20 @@ test('package facades exactly partition native exports', (t) => { 'DynComUnsafe', 'DynComUnsafeInterface', ]) + const safeWin32Names = new Set([ + 'DynWin32', + 'DynWin32NativeStruct', + 'DynWin32OverlappedOperation', + 'DynWin32Resource', + 'DynWin32Value', + 'DynWinRtValue', + ]) + const unsafeWin32Names = new Set([ + ...safeWin32Names, + 'DynWin32CallResult', + 'DynWin32Function', + 'DynWin32Unsafe', + ]) t.deepEqual(moduleKeys(winrtCjsRuntime), expectedWinrt) t.deepEqual( @@ -147,6 +171,14 @@ test('package facades exactly partition native exports', (t) => { moduleKeys(unsafeComRuntime), nativeKeys.filter((name) => unsafeComNames.has(name)), ) + t.deepEqual( + moduleKeys(win32CjsRuntime), + nativeKeys.filter((name) => safeWin32Names.has(name)), + ) + t.deepEqual( + moduleKeys(unsafeWin32Runtime), + nativeKeys.filter((name) => unsafeWin32Names.has(name)), + ) t.is(winrtCjsRuntime.WinGuid, comCjsRuntime.WinGuid) t.is(comCjsRuntime.initializeCom, unsafeComRuntime.initializeCom) @@ -157,6 +189,9 @@ test('package facades exactly partition native exports', (t) => { for (const name of moduleKeys(comCjsRuntime)) { t.true(moduleKeys(unsafeComRuntime).includes(name), `${name} must remain available from /com/unsafe`) } + for (const name of moduleKeys(win32CjsRuntime)) { + t.true(moduleKeys(unsafeWin32Runtime).includes(name), `${name} must remain available from /win32/unsafe`) + } }) test('COM allocation declaration is opaque and non-constructible', (t) => { @@ -231,6 +266,188 @@ test('DynCom verifies the projected WinRT async interface IID', (t) => { } }) +test('flat Win32 is isolated and raw addresses require the unsafe entrypoint', (t) => { + t.false(Object.prototype.hasOwnProperty.call(winrtRuntime, 'DynWin32')) + t.false(Object.prototype.hasOwnProperty.call(comRuntime, 'DynWin32')) + t.false(Object.prototype.hasOwnProperty.call(win32Runtime, 'DynWin32Unsafe')) + t.false(Object.prototype.hasOwnProperty.call(win32Runtime, 'DynWin32Function')) + t.is(typeof DynWin32.dataPointer, 'function') + t.is(typeof win32Runtime.DynWin32Value, 'function') + t.truthy(DynWin32Unsafe) + + t.throws(() => (DynWin32.dataPointer as unknown as (value: bigint) => unknown)(0x1234n), { + message: /arbitrary numeric addresses/, + }) + const pointer = DynWin32.dataPointer(Buffer.alloc(4)) + t.truthy(pointer) + t.throws(() => DynWin32.toBigint(pointer), { message: /not a 64-bit integer/ }) + t.truthy(DynWin32Unsafe.pointerAddress(pointer)) + t.truthy(DynWin32Unsafe.pointer(0x1234n)) + t.truthy(DynWin32.handle(null, true)) + t.throws(() => DynWin32.handle(null), { message: /explicitly nullable/ }) + t.throws(() => DynWin32.dataPointer(Buffer.alloc(0)), { + message: /non-empty backing storage/, + }) + t.truthy(DynWin32.dataPointer(Buffer.alloc(0), true)) +}) + +test('flat Win32 immutable plans validate ABI before native dispatch', (t) => { + const mulDiv = DynWin32Function.bind({ + dll: 'kernel32.dll', + entryPoint: 'MulDiv', + parameters: [ + { type: 'i32', direction: 'in' }, + { type: 'i32', direction: 'in' }, + { type: 'i32', direction: 'in' }, + ], + returnType: 'i32', + successRule: 'always', + captureLastError: false, + }) + + const call = mulDiv.invoke([DynWin32.i32(100), DynWin32.i32(3), DynWin32.i32(2)]) + t.is(DynWin32.toNumber(call.returnValue!), 150) + t.deepEqual(call.outputs, []) + t.throws(() => mulDiv.invoke([DynWin32.u32(100), DynWin32.i32(3), DynWin32.i32(2)]), { + message: /does not match I32/, + }) + + const consuming = DynWin32Function.bind({ + dll: 'advapi32.dll', + entryPoint: 'RegCloseKey', + parameters: [ + { + type: 'handle', + direction: 'in', + consumesResource: true, + resourceCleanup: 'regCloseKey', + }, + ], + returnType: 'i32', + successRule: 'zero', + }) + t.throws(() => consuming.invoke([DynWin32.handle(0x80000002n)]), { + message: /managed resource object/, + }) +}) + +test('flat Win32 native aggregate storage is aligned, branded, and mutable', (t) => { + const descriptor = JSON.stringify({ + name: 'Tests.POINT', + kind: 'struct', + x86: { size: 8, alignment: 4, fields: [] }, + x64: { size: 8, alignment: 4, fields: [] }, + arm64: { size: 8, alignment: 4, fields: [] }, + }) + const point = DynWin32.createNativeStruct( + descriptor, + Buffer.from([1, 0, 0, 0, 2, 0, 0, 0]), + ) + t.is(point.length, 8) + t.deepEqual([...point.bytes], [1, 0, 0, 0, 2, 0, 0, 0]) + t.truthy(DynWin32.nativeStruct(point, descriptor)) + t.throws( + () => DynWin32.nativeStruct(point, descriptor.replace('POINT', 'SIZE')), + { message: /type mismatch/ }, + ) + + const hugeDescriptor = JSON.stringify({ + name: 'Tests.HUGE', + kind: 'struct', + x86: { size: 16 * 1024 * 1024 + 8, alignment: 8, fields: [] }, + x64: { size: 16 * 1024 * 1024 + 8, alignment: 8, fields: [] }, + arm64: { size: 16 * 1024 * 1024 + 8, alignment: 8, fields: [] }, + }) + t.throws(() => DynWin32.createNativeStruct(hugeDescriptor), { + message: /safety limit/, + }) + t.throws(() => DynWin32.createNativeStruct(`${descriptor}${' '.repeat(1024 * 1024)}`), { + message: /descriptor exceeds/, + }) +}) + +test('flat Win32 multi-strings require double-NUL storage', (t) => { + t.truthy(DynWin32.wideMultiString(['en-US', 'fr-FR'])) + t.truthy(DynWin32.ansiMultiString(['alpha', 'beta'])) + t.throws(() => DynWin32.wideMultiString(Buffer.from([65, 0, 0, 0])), { + message: /two NUL code units/, + }) + t.throws(() => DynWin32.ansiMultiString(Buffer.from([65, 0])), { + message: /two NUL bytes/, + }) + t.truthy(DynWin32.wideMultiString(Buffer.from([65, 0, 0, 0, 0, 0]))) + t.truthy(DynWin32.ansiMultiString(Buffer.from([65, 0, 0]))) +}) + +test('flat Win32 pointer-bearing aggregates retain safe field owners', (t) => { + const descriptor = JSON.stringify({ + name: 'Windows.Win32.Security.SECURITY_ATTRIBUTES', + kind: 'struct', + x86: { + size: 12, + alignment: 4, + fields: [ + { name: 'nLength', offset: 0, count: 1, type: { kind: 'u32' } }, + { name: 'lpSecurityDescriptor', offset: 4, count: 1, type: { kind: 'pointer' } }, + { name: 'bInheritHandle', offset: 8, count: 1, type: { kind: 'i32' } }, + ], + }, + x64: { + size: 24, + alignment: 8, + fields: [ + { name: 'nLength', offset: 0, count: 1, type: { kind: 'u32' } }, + { name: 'lpSecurityDescriptor', offset: 8, count: 1, type: { kind: 'pointer' } }, + { name: 'bInheritHandle', offset: 16, count: 1, type: { kind: 'i32' } }, + ], + }, + arm64: { + size: 24, + alignment: 8, + fields: [ + { name: 'nLength', offset: 0, count: 1, type: { kind: 'u32' } }, + { name: 'lpSecurityDescriptor', offset: 8, count: 1, type: { kind: 'pointer' } }, + { name: 'bInheritHandle', offset: 16, count: 1, type: { kind: 'i32' } }, + ], + }, + }) + t.throws(() => DynWin32.createNativeStruct(descriptor, Buffer.alloc(24)), { + message: /cannot be initialized from raw bytes/, + }) + + const attributes = DynWin32.createNativeStruct(descriptor) + DynWin32.setNativeStructU32(attributes, descriptor, 'nLength', attributes.length) + DynWin32.setNativeStructBool32(attributes, descriptor, 'bInheritHandle', true) + const descriptorBytes = new Uint8Array(20) + DynWin32.setNativeStructPointer( + attributes, + descriptor, + 'lpSecurityDescriptor', + DynWin32.dataPointer(descriptorBytes), + ) + t.throws(() => attributes.bytes, { message: /unavailable/ }) + t.throws( + () => + DynWin32.setNativeStructPointer( + attributes, + descriptor, + 'lpSecurityDescriptor', + DynWin32Unsafe.pointer(0x1234n), + ), + { message: /retained Buffer or string storage/ }, + ) + + const aggregate = DynWin32.nativeStruct(attributes, descriptor) + structuredClone(descriptorBytes.buffer, { transfer: [descriptorBytes.buffer] }) + const noArgs = DynWin32Function.bind({ + dll: 'kernel32.dll', + entryPoint: 'GetLastError', + parameters: [], + returnType: 'u32', + }) + t.throws(() => noArgs.invoke([aggregate]), { message: /detached/ }) +}) + test('DynCom rejects pointers after their TypedArray backing store is detached', (t) => { const bytes = new Uint8Array(16) const pointer = DynCom.pointer(bytes) diff --git a/bindings/js/package.json b/bindings/js/package.json index 141f02c0..e4f6ffe3 100644 --- a/bindings/js/package.json +++ b/bindings/js/package.json @@ -23,6 +23,18 @@ "require": "./dist/com-unsafe.js", "default": "./dist/com-unsafe.js" }, + "./win32": { + "types": "./dist/win32.d.ts", + "import": "./dist/win32.js", + "require": "./dist/win32.js", + "default": "./dist/win32.js" + }, + "./win32/unsafe": { + "types": "./dist/win32-unsafe.d.ts", + "import": "./dist/win32-unsafe.js", + "require": "./dist/win32-unsafe.js", + "default": "./dist/win32-unsafe.js" + }, "./package.json": "./package.json" }, "typesVersions": { @@ -32,6 +44,12 @@ ], "com/unsafe": [ "dist/com-unsafe.d.ts" + ], + "win32": [ + "dist/win32.d.ts" + ], + "win32/unsafe": [ + "dist/win32-unsafe.d.ts" ] } }, diff --git a/bindings/js/scripts/generate-entrypoints.mjs b/bindings/js/scripts/generate-entrypoints.mjs index f193dc1d..2d4e325b 100644 --- a/bindings/js/scripts/generate-entrypoints.mjs +++ b/bindings/js/scripts/generate-entrypoints.mjs @@ -8,9 +8,9 @@ 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]) +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') @@ -62,10 +62,29 @@ const comUnsafeTypeExports = [ ...comTypeAliases, 'DynComSafeArrayBound', ] +const win32Exports = new Set([ + 'DynWin32', + 'DynWin32NativeStruct', + 'DynWin32OverlappedOperation', + 'DynWin32Resource', + 'DynWin32Value', + 'DynWinRtValue', +]) +const win32TypeExports = [...win32Exports] +const win32UnsafeExports = new Set([ + ...win32Exports, + 'DynWin32CallResult', + 'DynWin32Function', + 'DynWin32Unsafe', + 'DynWin32Value', +]) +const win32UnsafeTypeExports = [...win32UnsafeExports, 'DynWin32FunctionSpec', 'DynWin32ParameterSpec'] writeFacade( 'winrt', - nativeExports.filter((name) => !name.startsWith('DynCom') && name !== 'initializeCom'), + nativeExports.filter( + (name) => !name.startsWith('DynCom') && !name.startsWith('DynWin32') && name !== 'initializeCom', + ), ) writeFacade( 'com', @@ -81,6 +100,18 @@ writeFacade( comUnsafeExports, opaqueComDeclarations, ) +writeFacade( + 'win32', + nativeExports.filter((name) => win32Exports.has(name)), + win32TypeExports, + win32Exports, +) +writeFacade( + 'win32-unsafe', + nativeExports.filter((name) => win32UnsafeExports.has(name)), + win32UnsafeTypeExports, + win32UnsafeExports, +) function writeFacade( name, diff --git a/bindings/js/src/com.rs b/bindings/js/src/com.rs index 52f892f6..2e4a60a8 100644 --- a/bindings/js/src/com.rs +++ b/bindings/js/src/com.rs @@ -347,7 +347,10 @@ fn get_error_info() -> napi::Result> { .transpose() } -fn try_cast(value: &DynWinRTValue, iid: &WinGUID) -> napi::Result> { +pub(super) fn try_cast( + value: &DynWinRTValue, + iid: &WinGUID, +) -> napi::Result> { const E_NOINTERFACE: windows::core::HRESULT = windows::core::HRESULT(0x80004002u32 as i32); value.ensure_existing_com_apartment()?; @@ -655,7 +658,7 @@ fn uint8_array_info( })) } -fn pointer(value: Unknown) -> napi::Result { +pub(super) fn pointer(value: Unknown) -> napi::Result { use napi::sys; let env = value.value().env; @@ -731,7 +734,7 @@ fn pointer(value: Unknown) -> napi::Result { )) } -fn safe_data_pointer(value: Unknown, nullable: bool) -> napi::Result { +pub(super) fn safe_data_pointer(value: Unknown, nullable: bool) -> napi::Result { use napi::sys; let env = value.value().env; @@ -820,14 +823,34 @@ fn ansi_string_pointer(value: Unknown) -> napi::Result { pointer(value) } -fn safe_wide_string_pointer(value: Unknown, nullable: bool) -> napi::Result { +pub(super) fn safe_wide_string_pointer( + value: Unknown, + nullable: bool, +) -> napi::Result { safe_string_pointer(value, nullable, true) } -fn safe_ansi_string_pointer(value: Unknown, nullable: bool) -> napi::Result { +pub(super) fn safe_ansi_string_pointer( + value: Unknown, + nullable: bool, +) -> napi::Result { safe_string_pointer(value, nullable, false) } +pub(super) fn safe_wide_multi_string_pointer( + value: Unknown, + nullable: bool, +) -> napi::Result { + safe_multi_string_pointer(value, nullable, true) +} + +pub(super) fn safe_ansi_multi_string_pointer( + value: Unknown, + nullable: bool, +) -> napi::Result { + safe_multi_string_pointer(value, nullable, false) +} + fn safe_string_pointer(value: Unknown, nullable: bool, wide: bool) -> napi::Result { use napi::sys; @@ -859,6 +882,113 @@ fn safe_string_pointer(value: Unknown, nullable: bool, wide: bool) -> napi::Resu )) } +fn safe_multi_string_pointer( + value: Unknown, + nullable: bool, + wide: bool, +) -> 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 if nullable { + pointer(value) + } else { + Err(napi::Error::from_reason( + "safe multi-string pointer: null requires an explicitly nullable parameter", + )) + }; + } + + let mut is_array = false; + napi::check_status!( + unsafe { sys::napi_is_array(env, raw, &mut is_array) }, + "Failed to inspect multi-string input" + )?; + if value_type == sys::ValueType::napi_string || is_array { + let values = if is_array { + unsafe { Vec::::from_napi_value(env, raw) }? + } else { + vec![unsafe { String::from_napi_value(env, raw) }?] + }; + if values.iter().any(|value| value.contains('\0')) { + return Err(napi::Error::from_reason( + "multi-string array entries cannot contain NUL characters", + )); + } + return if wide { + let mut storage = Vec::::new(); + if values.is_empty() { + storage.push(0); + } else { + for value in values { + storage.extend(value.encode_utf16()); + storage.push(0); + } + } + storage.push(0); + let mut storage = storage.into_boxed_slice(); + let ptr = storage.as_mut_ptr().cast(); + Ok(DynWinRTValue::with_pointer_owner( + dynwinrt::WinRTValue::RawPtr(ptr), + NativePointerOwner::WideString(storage), + )) + } else { + if values.iter().any(|value| !value.is_ascii()) { + return Err(napi::Error::from_reason( + "ANSI multi-string values must be ASCII; use an explicitly encoded Buffer for other code pages", + )); + } + let mut storage = Vec::::new(); + if values.is_empty() { + storage.push(0); + } else { + for value in values { + storage.extend(value.into_bytes()); + storage.push(0); + } + } + storage.push(0); + let mut storage = storage.into_boxed_slice(); + let ptr = storage.as_mut_ptr().cast(); + Ok(DynWinRTValue::with_pointer_owner( + dynwinrt::WinRTValue::RawPtr(ptr), + NativePointerOwner::AnsiString(storage), + )) + }; + } + + if let Some(array) = uint8_array_info(env, raw)? { + let bytes = unsafe { std::slice::from_raw_parts(array.data, array.length) }; + if wide { + if (!array.data.is_null() && (array.data as usize) % std::mem::align_of::() != 0) + || bytes.len() < 4 + || bytes.len() % 2 != 0 + || bytes[bytes.len() - 4..] != [0, 0, 0, 0] + { + return Err(napi::Error::from_reason( + "wide multi-string Buffer/Uint8Array must be aligned UTF-16LE storage ending in two NUL code units", + )); + } + } else if bytes.len() < 2 || bytes[bytes.len() - 2..] != [0, 0] { + return Err(napi::Error::from_reason( + "ANSI multi-string Buffer/Uint8Array must end in two NUL bytes", + )); + } + return pointer(value); + } + + Err(napi::Error::from_reason( + "safe multi-string pointer: expected string, string[], Buffer, or Uint8Array", + )) +} + fn handle_value(value: Unknown) -> napi::Result { use napi::sys; @@ -1077,13 +1207,17 @@ fn take_bstr(value: &mut DynWinRTValue) -> napi::Result { String::try_from(&value).map_err(|error| napi::Error::from_reason(error.to_string())) } -fn validate_pointer_owner(value: &DynWinRTValue) -> napi::Result<()> { +pub(super) fn validate_pointer_owner(value: &DynWinRTValue) -> napi::Result<()> { if let Some(owner) = &value.1 { owner.validate()?; } Ok(()) } +pub(super) fn has_native_pointer_owner(value: &DynWinRTValue) -> bool { + value.1.is_some() +} + fn take_native_output_pointer( value: &mut DynWinRTValue, expected: PointerProvenance, diff --git a/bindings/js/src/lib.rs b/bindings/js/src/lib.rs index 54d9fa2c..18887529 100644 --- a/bindings/js/src/lib.rs +++ b/bindings/js/src/lib.rs @@ -23,6 +23,11 @@ pub use com::{ DynComNativeStructArray, DynComNativeUnion, DynComPropVariant, DynComSafeArray, DynComSafeArrayBound, DynComType, DynComUnsafe, DynComUnsafeInterface, DynComVariant, }; +mod win32; +pub use win32::{ + DynWin32, DynWin32CallResult, DynWin32Function, DynWin32FunctionSpec, DynWin32NativeStruct, + DynWin32ParameterSpec, DynWin32Resource, DynWin32Unsafe, DynWin32Value, +}; mod async_promise; mod managed_tsfn; mod scheduled_start; diff --git a/bindings/js/src/win32.rs b/bindings/js/src/win32.rs new file mode 100644 index 00000000..ad7c6a26 --- /dev/null +++ b/bindings/js/src/win32.rs @@ -0,0 +1,2248 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::cell::UnsafeCell; +use std::collections::{BTreeMap, VecDeque}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Condvar, LazyLock, Mutex}; + +use napi::bindgen_prelude::{BigInt, Buffer, FromNapiValue, Function, ToNapiValue, Unknown}; +use napi::JsValue; +use napi_derive::napi; + +use super::{com, managed_tsfn::ManagedTsfn, DynWinRTValue, WinGUID}; + +const ERROR_IO_PENDING: u32 = 997; +const ERROR_OPERATION_ABORTED: u32 = 995; +const ERROR_HANDLE_EOF: u32 = 38; +const ERROR_BROKEN_PIPE: u32 = 109; +const MAX_NATIVE_AGGREGATE_DESCRIPTOR_LENGTH: usize = 1024 * 1024; +const OVERLAPPED_WAITER_THREADS: usize = 8; + +#[repr(C)] +struct NativeOverlapped { + internal: usize, + internal_high: usize, + offset: u32, + offset_high: u32, + event: *mut std::ffi::c_void, +} + +windows_link::link!("kernel32.dll" "system" "CreateEventW" fn create_event_w( + event_attributes: *mut std::ffi::c_void, + manual_reset: i32, + initial_state: i32, + name: *const u16, +) -> *mut std::ffi::c_void); +windows_link::link!("kernel32.dll" "system" "ReadFile" fn read_file_overlapped( + file: *mut std::ffi::c_void, + buffer: *mut std::ffi::c_void, + bytes_to_read: u32, + bytes_read: *mut u32, + overlapped: *mut NativeOverlapped, +) -> i32); +windows_link::link!("kernel32.dll" "system" "WriteFile" fn write_file_overlapped( + file: *mut std::ffi::c_void, + buffer: *const std::ffi::c_void, + bytes_to_write: u32, + bytes_written: *mut u32, + overlapped: *mut NativeOverlapped, +) -> i32); +windows_link::link!("kernel32.dll" "system" "GetOverlappedResult" fn get_overlapped_result( + file: *mut std::ffi::c_void, + overlapped: *mut NativeOverlapped, + transferred: *mut u32, + wait: i32, +) -> i32); +windows_link::link!("kernel32.dll" "system" "CancelIoEx" fn cancel_io_ex( + file: *mut std::ffi::c_void, + overlapped: *mut NativeOverlapped, +) -> i32); +windows_link::link!("kernel32.dll" "system" "CloseHandle" fn close_native_handle( + handle: *mut std::ffi::c_void, +) -> i32); +windows_link::link!("kernel32.dll" "system" "GetLastError" fn get_last_error() -> u32); + +#[napi(object)] +pub struct DynWin32ParameterSpec { + #[napi(js_name = "type")] + pub typ: String, + pub direction: String, + pub nullable: Option, + pub cleanup: Option, + pub consumes_resource: Option, + pub resource_cleanup: Option, + pub aggregate_descriptor: Option, +} + +#[napi(object)] +pub struct DynWin32FunctionSpec { + pub dll: String, + pub entry_point: String, + pub parameters: Vec, + pub return_type: Option, + pub return_cleanup: Option, + pub success_rule: Option, + pub capture_last_error: Option, + pub calling_convention: Option, + pub return_aggregate_descriptor: Option, +} + +#[napi] +pub struct DynWin32Value { + value: dynwinrt::win32::Value, + pointer_owner: Option, +} + +enum Win32PointerOwner { + Native(Arc), + Aggregate(Arc), + PointerSlot { + inner: Arc, + slot: Box, + }, +} + +unsafe impl Send for DynWin32Value {} +unsafe impl Sync for DynWin32Value {} + +impl DynWin32Value { + fn new(value: dynwinrt::win32::Value) -> Self { + Self { + value, + pointer_owner: None, + } + } + + fn with_pointer_owner(value: dynwinrt::win32::Value, pointer_owner: DynWinRTValue) -> Self { + Self { + value, + pointer_owner: Some(Win32PointerOwner::Native(Arc::new(pointer_owner))), + } + } + + fn validate(&self) -> napi::Result<()> { + if let Some(Win32PointerOwner::Native(owner)) = &self.pointer_owner { + com::validate_pointer_owner(owner)?; + } + if let Some(Win32PointerOwner::Aggregate(owner)) = &self.pointer_owner { + let _ = owner.byte_length; + } + if let Some(Win32PointerOwner::PointerSlot { inner, slot }) = &self.pointer_owner { + com::validate_pointer_owner(inner)?; + let _ = **slot; + } + Ok(()) + } +} + +struct NativeAggregateStorage { + state: std::sync::Mutex, + byte_length: usize, + contains_pointers: bool, + owned_fields: Vec, +} + +#[derive(Clone, Copy)] +struct OwnedNativeField { + offset: usize, + cleanup: dynwinrt::win32::Cleanup, +} + +struct NativeAggregateState { + words: Vec, + owners: BTreeMap>, + call_succeeded: Option, +} + +impl NativeAggregateStorage { + fn new( + byte_length: usize, + bytes: Option<&[u8]>, + contains_pointers: bool, + owned_fields: Vec, + ) -> napi::Result { + if contains_pointers && bytes.is_some() { + return Err(napi::Error::from_reason( + "pointer-bearing native aggregates cannot be initialized from raw bytes", + )); + } + if byte_length > dynwinrt::win32::MAX_NATIVE_AGGREGATE_SIZE { + return Err(napi::Error::from_reason(format!( + "native aggregate exceeds the {} byte safety limit", + dynwinrt::win32::MAX_NATIVE_AGGREGATE_SIZE + ))); + } + let word_length = byte_length.div_ceil(std::mem::size_of::()); + let mut words = Vec::new(); + words.try_reserve_exact(word_length).map_err(|_| { + napi::Error::from_reason("Unable to allocate flat Win32 native aggregate storage") + })?; + words.resize(word_length, 0); + if let Some(bytes) = bytes { + if bytes.len() != byte_length { + return Err(napi::Error::from_reason(format!( + "native aggregate requires exactly {byte_length} bytes, received {}", + bytes.len() + ))); + } + unsafe { + std::ptr::copy_nonoverlapping(bytes.as_ptr(), words.as_mut_ptr().cast::(), byte_length); + } + } + Ok(Self { + state: std::sync::Mutex::new(NativeAggregateState { + words, + owners: BTreeMap::new(), + call_succeeded: None, + }), + byte_length, + contains_pointers, + owned_fields, + }) + } + + fn pointer(&self) -> *mut std::ffi::c_void { + self + .state + .lock() + .unwrap_or_else(|error| error.into_inner()) + .words + .as_mut_ptr() + .cast() + } + + fn bytes(&self) -> napi::Result> { + if self.contains_pointers { + return Err(napi::Error::from_reason( + "raw bytes are unavailable for pointer-bearing native aggregates", + )); + } + let state = self.state.lock().unwrap_or_else(|error| error.into_inner()); + Ok(unsafe { + std::slice::from_raw_parts(state.words.as_ptr().cast::(), self.byte_length).to_vec() + }) + } + + fn write_field( + &self, + offset: usize, + bytes: &[u8], + owner: Option>, + ) -> napi::Result<()> { + let end = offset + .checked_add(bytes.len()) + .filter(|end| *end <= self.byte_length) + .ok_or_else(|| napi::Error::from_reason("native aggregate field exceeds its layout"))?; + let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner()); + unsafe { + std::ptr::copy_nonoverlapping( + bytes.as_ptr(), + state.words.as_mut_ptr().cast::().add(offset), + end - offset, + ); + } + state.owners.remove(&offset); + if let Some(owner) = owner { + state.owners.insert(offset, owner); + } + Ok(()) + } + + fn read_field(&self, offset: usize) -> napi::Result<[u8; N]> { + let end = offset + .checked_add(N) + .filter(|end| *end <= self.byte_length) + .ok_or_else(|| napi::Error::from_reason("native aggregate field exceeds its layout"))?; + let state = self.state.lock().unwrap_or_else(|error| error.into_inner()); + let mut bytes = [0u8; N]; + unsafe { + std::ptr::copy_nonoverlapping( + state.words.as_ptr().cast::().add(offset), + bytes.as_mut_ptr(), + end - offset, + ); + } + Ok(bytes) + } + + fn take_usize(&self, offset: usize) -> napi::Result { + let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner()); + let end = offset + .checked_add(std::mem::size_of::()) + .filter(|end| *end <= self.byte_length) + .ok_or_else(|| napi::Error::from_reason("native handle field exceeds its layout"))?; + let mut bytes = [0u8; std::mem::size_of::()]; + unsafe { + std::ptr::copy_nonoverlapping( + state.words.as_ptr().cast::().add(offset), + bytes.as_mut_ptr(), + end - offset, + ); + std::ptr::write_bytes( + state.words.as_mut_ptr().cast::().add(offset), + 0, + end - offset, + ); + } + Ok(usize::from_le_bytes(bytes)) + } + + fn mark_call_result(&self, succeeded: bool) { + self + .state + .lock() + .unwrap_or_else(|error| error.into_inner()) + .call_succeeded = Some(succeeded); + } + + fn prepare_call(&self) -> napi::Result<()> { + self.cleanup_owned_fields(true)?; + self.mark_call_result(false); + Ok(()) + } + + fn require_success(&self) -> napi::Result<()> { + match self + .state + .lock() + .unwrap_or_else(|error| error.into_inner()) + .call_succeeded + { + Some(true) => Ok(()), + Some(false) => Err(napi::Error::from_reason( + "native aggregate outputs are unavailable because the native call failed", + )), + None => Err(napi::Error::from_reason( + "native aggregate outputs are unavailable before a successful native call", + )), + } + } + + fn cleanup_owned_fields(&self, only_after_success: bool) -> napi::Result<()> { + if only_after_success + && self + .state + .lock() + .unwrap_or_else(|error| error.into_inner()) + .call_succeeded + != Some(true) + { + return Ok(()); + } + for field in &self.owned_fields { + let bits = { + let state = self.state.lock().unwrap_or_else(|error| error.into_inner()); + read_usize_from_words(&state.words, self.byte_length, field.offset)? + }; + if bits == 0 { + continue; + } + unsafe { dynwinrt::win32::cleanup_owned_resource(bits, field.cleanup) } + .map_err(|error| napi::Error::from_reason(error.to_string()))?; + let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner()); + if read_usize_from_words(&state.words, self.byte_length, field.offset)? == bits { + write_usize_to_words(&mut state.words, self.byte_length, field.offset, 0)?; + } + } + Ok(()) + } +} + +impl Drop for NativeAggregateStorage { + fn drop(&mut self) { + let _ = self.cleanup_owned_fields(true); + } +} + +fn read_usize_from_words(words: &[u64], byte_length: usize, offset: usize) -> napi::Result { + let end = offset + .checked_add(std::mem::size_of::()) + .filter(|end| *end <= byte_length) + .ok_or_else(|| napi::Error::from_reason("native handle field exceeds its layout"))?; + let mut bytes = [0u8; std::mem::size_of::()]; + unsafe { + std::ptr::copy_nonoverlapping( + words.as_ptr().cast::().add(offset), + bytes.as_mut_ptr(), + end - offset, + ); + } + Ok(usize::from_le_bytes(bytes)) +} + +fn write_usize_to_words( + words: &mut [u64], + byte_length: usize, + offset: usize, + value: usize, +) -> napi::Result<()> { + let end = offset + .checked_add(std::mem::size_of::()) + .filter(|end| *end <= byte_length) + .ok_or_else(|| napi::Error::from_reason("native handle field exceeds its layout"))?; + unsafe { + std::ptr::copy_nonoverlapping( + value.to_le_bytes().as_ptr(), + words.as_mut_ptr().cast::().add(offset), + end - offset, + ); + } + Ok(()) +} + +#[napi] +pub struct DynWin32NativeStruct { + descriptor: String, + storage: Arc, +} + +#[napi] +impl DynWin32NativeStruct { + #[napi(getter)] + pub fn bytes(&self) -> napi::Result { + Ok(Buffer::from(self.storage.bytes()?)) + } + + #[napi(getter)] + pub fn length(&self) -> u32 { + self.storage.byte_length as u32 + } +} + +#[napi] +pub struct DynWin32Resource(Arc); + +#[napi] +impl DynWin32Resource { + #[napi(getter)] + pub fn value(&self) -> BigInt { + BigInt::from(self.0.raw() as u64) + } + + #[napi(getter)] + pub fn closed(&self) -> bool { + self.0.is_closed() + } + + #[napi(getter)] + pub fn busy(&self) -> bool { + self.0.has_async_leases() + } + + #[napi(getter)] + pub fn active(&self) -> bool { + self.0.has_active_async_io() + } + + #[napi] + pub fn close(&self) -> napi::Result<()> { + self + .0 + .close() + .map_err(|error| napi::Error::from_reason(error.to_string())) + } +} + +#[napi] +pub struct DynWin32Function(Arc); + +#[napi] +impl DynWin32Function { + #[napi(factory)] + pub fn bind(spec: DynWin32FunctionSpec) -> napi::Result { + bind_function(spec) + } + + #[napi(getter)] + pub fn dll(&self) -> String { + self.0.dll().to_string() + } + + #[napi(getter)] + pub fn entry_point(&self) -> String { + self.0.entry_point().to_string() + } + + #[napi] + pub fn invoke(&self, args: Vec<&DynWin32Value>) -> napi::Result { + for value in &args { + value.validate()?; + } + let mut aggregates = args + .iter() + .filter_map(|value| match &value.pointer_owner { + Some(Win32PointerOwner::Aggregate(owner)) => Some(owner), + _ => None, + }) + .collect::>(); + aggregates.sort_by_key(|owner| Arc::as_ptr(owner) as usize); + if aggregates + .windows(2) + .any(|pair| Arc::ptr_eq(pair[0], pair[1])) + { + return Err(napi::Error::from_reason( + "the same native aggregate cannot occupy multiple parameters in one call", + )); + } + let _aggregate_guards = aggregates + .into_iter() + .map(|owner| { + owner + .state + .lock() + .unwrap_or_else(|error| error.into_inner()) + }) + .collect::>(); + for state in &_aggregate_guards { + for owner in state.owners.values() { + com::validate_pointer_owner(owner)?; + } + } + let values = args + .into_iter() + .map(|value| value.value.clone()) + .collect::>(); + let result = unsafe { self.0.invoke(&values) }.map_err(|error| { + napi::Error::from_reason(format!( + "DynWin32Function {}!{}: {}", + self.0.dll(), + self.0.entry_point(), + error.message() + )) + })?; + Ok(DynWin32CallResult { + return_value: result.return_value.map(DynWin32Value::new), + outputs: Some(result.outputs.into_iter().map(DynWin32Value::new).collect()), + last_error: result.last_error, + succeeded: result.succeeded, + }) + } +} + +#[napi] +pub struct DynWin32CallResult { + return_value: Option, + outputs: Option>, + last_error: Option, + succeeded: bool, +} + +#[napi] +impl DynWin32CallResult { + #[napi(getter)] + pub fn return_value(&mut self) -> napi::Result> { + Ok(self.return_value.take()) + } + + #[napi(getter)] + pub fn outputs(&mut self) -> napi::Result> { + self + .outputs + .take() + .ok_or_else(|| napi::Error::from_reason("Win32 outputs were already consumed")) + } + + #[napi(getter)] + pub fn last_error(&self) -> Option { + self.last_error + } + + #[napi(getter)] + pub fn succeeded(&self) -> bool { + self.succeeded + } +} + +#[derive(Clone, Copy)] +enum OverlappedIoKind { + Read, + Write, +} + +struct OverlappedControl { + active: bool, + handle: usize, +} + +struct OverlappedState { + overlapped: UnsafeCell, + control: Mutex, + cancelled: AtomicBool, +} + +unsafe impl Send for OverlappedState {} +unsafe impl Sync for OverlappedState {} + +impl OverlappedState { + fn new(offset: u64) -> Arc { + Arc::new(Self { + overlapped: UnsafeCell::new(NativeOverlapped { + internal: 0, + internal_high: 0, + offset: offset as u32, + offset_high: (offset >> 32) as u32, + event: std::ptr::null_mut(), + }), + control: Mutex::new(OverlappedControl { + active: false, + handle: 0, + }), + cancelled: AtomicBool::new(false), + }) + } + + fn activate(&self, handle: usize, event: *mut std::ffi::c_void) { + unsafe { + (*self.overlapped.get()).event = event; + } + let mut control = self + .control + .lock() + .unwrap_or_else(|error| error.into_inner()); + control.handle = handle; + control.active = true; + } + + fn deactivate(&self) { + let mut control = self + .control + .lock() + .unwrap_or_else(|error| error.into_inner()); + control.active = false; + control.handle = 0; + } + + fn cancel(&self) { + self.cancelled.store(true, Ordering::Release); + let control = self + .control + .lock() + .unwrap_or_else(|error| error.into_inner()); + if control.active { + unsafe { + cancel_io_ex( + control.handle as *mut std::ffi::c_void, + self.overlapped.get(), + ); + } + } + } +} + +struct NativeEvent(*mut std::ffi::c_void); + +impl Drop for NativeEvent { + fn drop(&mut self) { + if !self.0.is_null() { + unsafe { + close_native_handle(self.0); + } + } + } +} + +pub struct OverlappedIoTask { + kind: OverlappedIoKind, + lease: dynwinrt::win32::OwnedResourceAsyncLease, + buffer: Option, + buffer_len: usize, + native_buffer: Vec, + state: Arc, +} + +struct OverlappedCompletion { + task: OverlappedIoTask, + result: Result, +} + +struct OverlappedWork { + task: OverlappedIoTask, + completion: ManagedTsfn, +} + +struct OverlappedWaiterQueue { + work: Mutex>, + available: Condvar, + in_flight: AtomicUsize, +} + +struct OverlappedWaiterPool { + queue: Arc, +} + +struct OverlappedInFlight<'a>(&'a AtomicUsize); + +impl Drop for OverlappedInFlight<'_> { + fn drop(&mut self) { + self.0.fetch_sub(1, Ordering::AcqRel); + } +} + +static OVERLAPPED_WAITER_POOL: LazyLock> = + LazyLock::new(OverlappedWaiterPool::new); + +impl OverlappedWaiterPool { + fn new() -> Result { + let queue = Arc::new(OverlappedWaiterQueue { + work: Mutex::new(VecDeque::new()), + available: Condvar::new(), + in_flight: AtomicUsize::new(0), + }); + for index in 0..OVERLAPPED_WAITER_THREADS { + let worker_queue = Arc::clone(&queue); + std::thread::Builder::new() + .name(format!("dynwinrt-overlapped-waiter-{index}")) + .spawn(move || overlapped_waiter_loop(&worker_queue)) + .map_err(|error| format!("Failed to create bounded OVERLAPPED waiter: {error}"))?; + } + Ok(Self { queue }) + } + + fn submit(&self, work: OverlappedWork) -> napi::Result<()> { + self + .queue + .in_flight + .fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| { + (count < OVERLAPPED_WAITER_THREADS).then_some(count + 1) + }) + .map_err(|_| { + napi::Error::from_reason(format!( + "OVERLAPPED waiter capacity is full ({OVERLAPPED_WAITER_THREADS} active operations)" + )) + })?; + let mut queue = self + .queue + .work + .lock() + .unwrap_or_else(|error| error.into_inner()); + queue.push_back(work); + self.queue.available.notify_one(); + Ok(()) + } +} + +fn overlapped_waiter_loop(queue: &OverlappedWaiterQueue) { + loop { + let work = { + let mut pending = queue.work.lock().unwrap_or_else(|error| error.into_inner()); + while pending.is_empty() { + pending = queue + .available + .wait(pending) + .unwrap_or_else(|error| error.into_inner()); + } + pending.pop_front().expect("waiter queue is not empty") + }; + let _in_flight = OverlappedInFlight(&queue.in_flight); + let OverlappedWork { + mut task, + completion, + } = work; + let result = task.compute().map_err(|error| error.reason.clone()); + let _ = completion.call(OverlappedCompletion { task, result }); + } +} + +#[napi] +pub struct DynWin32OverlappedOperation { + task: Option, + state: Arc, +} + +#[napi] +impl DynWin32OverlappedOperation { + #[napi] + pub fn cancel(&self) { + self.state.cancel(); + } + + #[napi] + pub fn start( + &mut self, + #[napi(ts_arg_type = "(error: Error | null, bytesTransferred?: number) => void")] + callback: Function<'static, (), ()>, + ) -> napi::Result<()> { + let task = self + .task + .take() + .ok_or_else(|| napi::Error::from_reason("OVERLAPPED operation was already started"))?; + let env = callback.value().env; + let raw_callback = napi::JsValue::raw(&callback); + let completion = ManagedTsfn::create( + env, + raw_callback, + 1, + false, + |completion: OverlappedCompletion, env| completion.into_js_arguments(env), + None, + )?; + OVERLAPPED_WAITER_POOL + .as_ref() + .map_err(|error| napi::Error::from_reason(error.clone()))? + .submit(OverlappedWork { task, completion }) + } +} + +impl OverlappedIoTask { + fn compute(&mut self) -> napi::Result { + let handle = self.lease.raw(); + perform_overlapped_io( + self.kind, + handle, + &mut self.native_buffer, + &self.state, + &mut self.lease, + ) + } + + fn resolve(mut self, env: napi::sys::napi_env, output: u32) -> napi::Result { + if matches!(self.kind, OverlappedIoKind::Read) { + let transferred = usize::try_from(output) + .map_err(|_| napi::Error::from_reason("OVERLAPPED result exceeds usize"))?; + if transferred > self.buffer_len { + return Err(napi::Error::from_reason( + "OVERLAPPED result exceeds the original Buffer length", + )); + } + let buffer = self + .buffer + .take() + .ok_or_else(|| napi::Error::from_reason("OVERLAPPED read Buffer is unavailable"))?; + let raw = unsafe { Buffer::to_napi_value(env, buffer) }?; + let mut is_buffer = false; + napi::check_status!( + unsafe { napi::sys::napi_is_buffer(env, raw, &mut is_buffer) }, + "Failed to revalidate OVERLAPPED read Buffer" + )?; + if !is_buffer { + return Err(napi::Error::from_reason( + "OVERLAPPED read Buffer is no longer a Node Buffer", + )); + } + let mut pointer = std::ptr::null_mut(); + let mut length = 0usize; + napi::check_status!( + unsafe { napi::sys::napi_get_buffer_info(env, raw, &mut pointer, &mut length) }, + "Failed to revalidate OVERLAPPED read Buffer backing storage" + )?; + if length != self.buffer_len || (length != 0 && pointer.is_null()) { + return Err(napi::Error::from_reason( + "OVERLAPPED read Buffer backing ArrayBuffer was detached or changed", + )); + } + if transferred != 0 { + unsafe { + std::ptr::copy_nonoverlapping( + self.native_buffer.as_ptr(), + pointer.cast::(), + transferred, + ); + } + } + } + Ok(output) + } +} + +impl OverlappedCompletion { + fn into_js_arguments(self, env: napi::sys::napi_env) -> napi::Result> { + let result = self.result.and_then(|output| { + self + .task + .resolve(env, output) + .map_err(|error| error.reason.clone()) + }); + match result { + Ok(output) => { + let mut null = std::ptr::null_mut(); + napi::check_status!( + unsafe { napi::sys::napi_get_null(env, &mut null) }, + "Failed to create OVERLAPPED completion null" + )?; + let output = unsafe { u32::to_napi_value(env, output) }?; + Ok(vec![null, output]) + } + Err(reason) => { + let mut message = std::ptr::null_mut(); + napi::check_status!( + unsafe { + napi::sys::napi_create_string_utf8( + env, + reason.as_ptr().cast(), + reason.len() as isize, + &mut message, + ) + }, + "Failed to create OVERLAPPED completion error message" + )?; + let mut error = std::ptr::null_mut(); + napi::check_status!( + unsafe { napi::sys::napi_create_error(env, std::ptr::null_mut(), message, &mut error) }, + "Failed to create OVERLAPPED completion error" + )?; + Ok(vec![error]) + } + } + } +} + +fn perform_overlapped_io( + kind: OverlappedIoKind, + handle: usize, + buffer: &mut [u8], + state: &Arc, + lease: &mut dynwinrt::win32::OwnedResourceAsyncLease, +) -> napi::Result { + if state.cancelled.load(Ordering::Acquire) { + return Err(napi::Error::from_reason("OVERLAPPED operation was aborted")); + } + let event = unsafe { create_event_w(std::ptr::null_mut(), 1, 0, std::ptr::null()) }; + if event.is_null() { + return Err(last_error("CreateEventW")); + } + let _event = NativeEvent(event); + state.activate(handle, event); + let length = u32::try_from(buffer.len()) + .map_err(|_| napi::Error::from_reason("OVERLAPPED buffer exceeds u32"))?; + let started = unsafe { + match kind { + OverlappedIoKind::Read => read_file_overlapped( + handle as *mut std::ffi::c_void, + buffer.as_mut_ptr().cast(), + length, + std::ptr::null_mut(), + state.overlapped.get(), + ), + OverlappedIoKind::Write => write_file_overlapped( + handle as *mut std::ffi::c_void, + buffer.as_ptr().cast(), + length, + std::ptr::null_mut(), + state.overlapped.get(), + ), + } + }; + if started == 0 { + let error = unsafe { get_last_error() }; + if error != ERROR_IO_PENDING { + state.deactivate(); + if is_read_eof(kind, error) { + return Ok(0); + } + return Err(native_error( + match kind { + OverlappedIoKind::Read => "ReadFile", + OverlappedIoKind::Write => "WriteFile", + }, + error, + )); + } + lease.mark_active(); + } + if state.cancelled.load(Ordering::Acquire) { + state.cancel(); + } + let mut transferred = 0u32; + let completed = unsafe { + get_overlapped_result( + handle as *mut std::ffi::c_void, + state.overlapped.get(), + &mut transferred, + 1, + ) + }; + let error = (completed == 0).then(|| unsafe { get_last_error() }); + state.deactivate(); + lease.mark_inactive(); + if let Some(error) = error { + if is_read_eof(kind, error) { + return Ok(0); + } + return Err(native_error( + if error == ERROR_OPERATION_ABORTED { + "OVERLAPPED operation" + } else { + "GetOverlappedResult" + }, + error, + )); + } + Ok(transferred) +} + +fn is_read_eof(kind: OverlappedIoKind, error: u32) -> bool { + matches!(kind, OverlappedIoKind::Read) && matches!(error, ERROR_HANDLE_EOF | ERROR_BROKEN_PIPE) +} + +fn last_error(function: &str) -> napi::Error { + native_error(function, unsafe { get_last_error() }) +} + +fn native_error(function: &str, error: u32) -> napi::Error { + napi::Error::from_reason(format!("{function} failed with Win32 error {error}")) +} + +#[napi] +pub struct DynWin32; + +#[napi] +impl DynWin32 { + #[napi] + pub fn bool8(value: bool) -> DynWin32Value { + DynWin32Value::new(dynwinrt::win32::Value::U8(u8::from(value))) + } + + #[napi] + pub fn bool32(value: bool) -> DynWin32Value { + DynWin32Value::new(dynwinrt::win32::Value::Bool(value)) + } + + #[napi] + pub fn i8(value: i8) -> DynWin32Value { + DynWin32Value::new(dynwinrt::win32::Value::I8(value)) + } + + #[napi] + pub fn u8(value: u8) -> DynWin32Value { + DynWin32Value::new(dynwinrt::win32::Value::U8(value)) + } + + #[napi] + pub fn i16(value: i16) -> DynWin32Value { + DynWin32Value::new(dynwinrt::win32::Value::I16(value)) + } + + #[napi] + pub fn u16(value: u16) -> DynWin32Value { + DynWin32Value::new(dynwinrt::win32::Value::U16(value)) + } + + #[napi] + pub fn i32(value: i32) -> DynWin32Value { + DynWin32Value::new(dynwinrt::win32::Value::I32(value)) + } + + #[napi] + pub fn u32(value: u32) -> DynWin32Value { + DynWin32Value::new(dynwinrt::win32::Value::U32(value)) + } + + #[napi] + pub fn i64(value: BigInt) -> napi::Result { + let (value, lossless) = value.get_i64(); + if !lossless { + return Err(napi::Error::from_reason( + "DynWin32.i64(): value must fit a signed 64-bit integer", + )); + } + Ok(DynWin32Value::new(dynwinrt::win32::Value::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( + "DynWin32.u64(): value must fit an unsigned 64-bit integer", + )); + } + Ok(DynWin32Value::new(dynwinrt::win32::Value::U64(value))) + } + + #[napi] + pub fn f32(value: f64) -> DynWin32Value { + DynWin32Value::new(dynwinrt::win32::Value::F32(value as f32)) + } + + #[napi] + pub fn f64(value: f64) -> DynWin32Value { + DynWin32Value::new(dynwinrt::win32::Value::F64(value)) + } + + #[napi] + pub fn handle( + #[napi(ts_arg_type = "bigint | number | DynWin32Resource | null | undefined")] value: Unknown, + nullable: Option, + ) -> napi::Result { + handle_value(value, nullable.unwrap_or(false)) + } + + #[napi] + pub fn resource(value: &DynWin32Resource, cleanup: String) -> napi::Result { + let cleanup = parse_cleanup(&cleanup)?; + if cleanup == dynwinrt::win32::Cleanup::None || value.0.cleanup() != cleanup { + return Err(napi::Error::from_reason( + "Managed Win32 resource cleanup does not match the consuming API", + )); + } + if value.0.is_closed() { + return Err(napi::Error::from_reason( + "Cannot consume a closed flat Win32 resource", + )); + } + Ok(DynWin32Value::new(dynwinrt::win32::Value::Resource( + Arc::clone(&value.0), + ))) + } + + #[napi] + pub fn com_object( + #[napi(ts_arg_type = "DynWinRtValue | null | undefined")] value: Unknown, + iid: String, + nullable: Option, + ) -> napi::Result { + use windows::core::Interface; + + let env = value.value().env; + let raw = value.value().value; + let mut value_type = napi::sys::ValueType::napi_undefined; + unsafe { napi::sys::napi_typeof(env, raw, &mut value_type) }; + if matches!( + value_type, + napi::sys::ValueType::napi_null | napi::sys::ValueType::napi_undefined + ) { + return if nullable.unwrap_or(false) { + Ok(DynWin32Value::new(dynwinrt::win32::Value::Null)) + } else { + Err(napi::Error::from_reason( + "DynWin32.comObject(): null requires an explicitly nullable interface", + )) + }; + } + let value = unsafe { <&DynWinRTValue>::from_napi_value(env, raw) }?; + let iid = WinGUID( + windows::core::GUID::try_from(iid.as_str()) + .map_err(|_| napi::Error::from_reason("Invalid COM interface IID"))?, + ); + let owner = com::try_cast(value, &iid)?.ok_or_else(|| { + napi::Error::from_reason("Managed object does not implement the required Win32 interface") + })?; + let pointer = owner + .0 + .as_object() + .ok_or_else(|| napi::Error::from_reason("Managed value is not a COM object"))? + .as_raw(); + Ok(DynWin32Value { + value: dynwinrt::win32::Value::Pointer(pointer), + pointer_owner: Some(Win32PointerOwner::Native(Arc::new(owner))), + }) + } + + #[napi] + pub fn data_pointer( + #[napi(ts_arg_type = "Buffer | Uint8Array | null | undefined")] value: Unknown, + nullable: Option, + ) -> napi::Result { + let nullable = nullable.unwrap_or(false); + let value = pointer_value(com::safe_data_pointer(value, nullable)?)?; + reject_required_null_pointer(&value, nullable)?; + Ok(value) + } + + #[napi] + pub fn aligned_data_pointer( + #[napi(ts_arg_type = "Buffer | Uint8Array | null | undefined")] value: Unknown, + alignment: u32, + nullable: Option, + ) -> napi::Result { + if alignment == 0 || !alignment.is_power_of_two() || alignment > 8 { + return Err(napi::Error::from_reason( + "DynWin32.alignedDataPointer(): alignment must be 1, 2, 4, or 8", + )); + } + let nullable = nullable.unwrap_or(false); + let value = pointer_value(com::safe_data_pointer(value, nullable)?)?; + reject_required_null_pointer(&value, nullable)?; + if let dynwinrt::win32::Value::Pointer(pointer) = &value.value { + if !pointer.is_null() && (*pointer as usize) % alignment as usize != 0 { + return Err(napi::Error::from_reason(format!( + "native buffer address is not aligned to {alignment} bytes" + ))); + } + } + Ok(value) + } + + #[napi] + pub fn wide_string( + #[napi(ts_arg_type = "string | Buffer | Uint8Array | null | undefined")] value: Unknown, + nullable: Option, + ) -> napi::Result { + pointer_value(com::safe_wide_string_pointer( + value, + nullable.unwrap_or(false), + )?) + } + + #[napi] + pub fn ansi_string( + #[napi(ts_arg_type = "string | Buffer | Uint8Array | null | undefined")] value: Unknown, + nullable: Option, + ) -> napi::Result { + pointer_value(com::safe_ansi_string_pointer( + value, + nullable.unwrap_or(false), + )?) + } + + #[napi] + pub fn wide_multi_string( + #[napi(ts_arg_type = "string | readonly string[] | Buffer | Uint8Array | null | undefined")] + value: Unknown, + nullable: Option, + ) -> napi::Result { + pointer_value(com::safe_wide_multi_string_pointer( + value, + nullable.unwrap_or(false), + )?) + } + + #[napi] + pub fn ansi_multi_string( + #[napi(ts_arg_type = "string | readonly string[] | Buffer | Uint8Array | null | undefined")] + value: Unknown, + nullable: Option, + ) -> napi::Result { + pointer_value(com::safe_ansi_multi_string_pointer( + value, + nullable.unwrap_or(false), + )?) + } + + #[napi] + pub fn wide_string_pointer_pointer( + #[napi(ts_arg_type = "string | Buffer | Uint8Array | null | undefined")] value: Unknown, + nullable: Option, + ) -> napi::Result { + string_pointer_pointer(value, nullable.unwrap_or(false), true) + } + + #[napi] + pub fn ansi_string_pointer_pointer( + #[napi(ts_arg_type = "string | Buffer | Uint8Array | null | undefined")] value: Unknown, + nullable: Option, + ) -> napi::Result { + string_pointer_pointer(value, nullable.unwrap_or(false), false) + } + + #[napi] + pub fn null_pointer() -> DynWin32Value { + DynWin32Value::new(dynwinrt::win32::Value::Null) + } + + #[napi] + pub fn begin_read_file( + file: &DynWin32Resource, + buffer: Buffer, + offset: Option, + ) -> napi::Result { + overlapped_io_task(OverlappedIoKind::Read, file, buffer, offset) + } + + #[napi] + pub fn begin_write_file( + file: &DynWin32Resource, + buffer: Buffer, + offset: Option, + ) -> napi::Result { + overlapped_io_task(OverlappedIoKind::Write, file, buffer, offset) + } + + #[napi] + pub fn create_native_struct( + descriptor: String, + bytes: Option, + ) -> napi::Result { + let (_, size, alignment, contains_pointers, owned_fields) = + native_aggregate_layout(&descriptor)?; + if alignment > 8 { + return Err(napi::Error::from_reason( + "flat Win32 native aggregate alignment above 8 is unsupported", + )); + } + Ok(DynWin32NativeStruct { + descriptor, + storage: Arc::new(NativeAggregateStorage::new( + size, + bytes.as_ref().map(|bytes| bytes.as_ref()), + contains_pointers, + owned_fields, + )?), + }) + } + + #[napi] + pub fn set_native_struct_u32( + value: &DynWin32NativeStruct, + descriptor: String, + field: String, + input: u32, + ) -> napi::Result<()> { + validate_native_struct(value, &descriptor)?; + let (offset, kind, _) = native_aggregate_field(&descriptor, &field)?; + if kind != "u32" { + return Err(napi::Error::from_reason(format!( + "native field `{field}` is not u32" + ))); + } + value + .storage + .write_field(offset, &input.to_le_bytes(), None) + } + + #[napi] + pub fn set_native_struct_bool32( + value: &DynWin32NativeStruct, + descriptor: String, + field: String, + input: bool, + ) -> napi::Result<()> { + validate_native_struct(value, &descriptor)?; + let (offset, kind, _) = native_aggregate_field(&descriptor, &field)?; + if kind != "i32" { + return Err(napi::Error::from_reason(format!( + "native field `{field}` is not BOOL" + ))); + } + value + .storage + .write_field(offset, &i32::from(input).to_le_bytes(), None) + } + + #[napi] + pub fn set_native_struct_pointer( + value: &DynWin32NativeStruct, + descriptor: String, + field: String, + pointer: &DynWin32Value, + ) -> napi::Result<()> { + validate_native_struct(value, &descriptor)?; + pointer.validate()?; + let (offset, kind, _) = native_aggregate_field(&descriptor, &field)?; + if kind != "pointer" { + return Err(napi::Error::from_reason(format!( + "native field `{field}` is not a data pointer" + ))); + } + let (bits, owner) = match (&pointer.value, &pointer.pointer_owner) { + (dynwinrt::win32::Value::Null, _) => (0usize, None), + (dynwinrt::win32::Value::Pointer(pointer), _) if pointer.is_null() => (0usize, None), + (dynwinrt::win32::Value::Pointer(pointer), Some(Win32PointerOwner::Native(owner))) + if com::has_native_pointer_owner(owner) => + { + (*pointer as usize, Some(Arc::clone(owner))) + } + (dynwinrt::win32::Value::Pointer(_), _) => { + return Err(napi::Error::from_reason( + "native struct pointer fields require retained Buffer or string storage", + )); + } + _ => { + return Err(napi::Error::from_reason( + "native struct pointer field value is not a data pointer", + )); + } + }; + value + .storage + .write_field(offset, &bits.to_le_bytes(), owner) + } + + #[napi] + pub fn get_native_struct_u32( + value: &DynWin32NativeStruct, + descriptor: String, + field: String, + ) -> napi::Result { + validate_native_struct(value, &descriptor)?; + value.storage.require_success()?; + let (offset, kind, _) = native_aggregate_field(&descriptor, &field)?; + if kind != "u32" { + return Err(napi::Error::from_reason(format!( + "native field `{field}` is not u32" + ))); + } + Ok(u32::from_le_bytes(value.storage.read_field(offset)?)) + } + + #[napi] + pub fn take_native_struct_resource( + value: &DynWin32NativeStruct, + descriptor: String, + field: String, + cleanup: String, + ) -> napi::Result> { + validate_native_struct(value, &descriptor)?; + value.storage.require_success()?; + let (offset, kind, field_cleanup) = native_aggregate_field(&descriptor, &field)?; + if kind != "handle" || field_cleanup.as_deref() != Some(cleanup.as_str()) { + return Err(napi::Error::from_reason(format!( + "native field `{field}` does not have cleanup `{cleanup}`" + ))); + } + let bits = value.storage.take_usize(offset)?; + if bits == 0 { + return Ok(None); + } + let cleanup = parse_cleanup(&cleanup)?; + let resource = unsafe { dynwinrt::win32::OwnedResource::adopt(bits, cleanup) } + .map_err(|error| napi::Error::from_reason(error.message()))?; + Ok(Some(DynWin32Resource(resource))) + } + + #[napi] + pub fn mark_native_struct_call_result( + value: &DynWin32NativeStruct, + descriptor: String, + succeeded: bool, + ) -> napi::Result<()> { + validate_native_struct(value, &descriptor)?; + value.storage.mark_call_result(succeeded); + Ok(()) + } + + #[napi] + pub fn prepare_native_struct_call( + value: &DynWin32NativeStruct, + descriptor: String, + ) -> napi::Result<()> { + validate_native_struct(value, &descriptor)?; + value.storage.prepare_call() + } + + #[napi] + pub fn native_struct( + #[napi(ts_arg_type = "DynWin32NativeStruct | null | undefined")] value: Unknown, + descriptor: String, + nullable: Option, + ) -> napi::Result { + let env = value.value().env; + let raw = value.value().value; + let mut value_type = napi::sys::ValueType::napi_undefined; + unsafe { napi::sys::napi_typeof(env, raw, &mut value_type) }; + if matches!( + value_type, + napi::sys::ValueType::napi_null | napi::sys::ValueType::napi_undefined + ) { + return if nullable.unwrap_or(false) { + Ok(DynWin32Value::new(dynwinrt::win32::Value::Null)) + } else { + Err(napi::Error::from_reason( + "DynWin32.nativeStruct(): null requires an explicitly nullable aggregate pointer", + )) + }; + } + let value = unsafe { <&DynWin32NativeStruct>::from_napi_value(env, raw) }?; + if value.descriptor != descriptor { + return Err(napi::Error::from_reason( + "DynWin32.nativeStruct(): native aggregate type mismatch", + )); + } + let pointer = value.storage.pointer(); + Ok(DynWin32Value { + value: dynwinrt::win32::Value::Pointer(pointer), + pointer_owner: Some(Win32PointerOwner::Aggregate(Arc::clone(&value.storage))), + }) + } + + #[napi] + pub fn native_struct_value( + value: &DynWin32NativeStruct, + descriptor: String, + ) -> napi::Result { + if value.descriptor != descriptor { + return Err(napi::Error::from_reason( + "DynWin32.nativeStructValue(): native aggregate type mismatch", + )); + } + let layout = native_aggregate_call_layout(&descriptor)?; + Ok(DynWin32Value { + value: dynwinrt::win32::Value::Aggregate { + layout, + pointer: value.storage.pointer(), + }, + pointer_owner: Some(Win32PointerOwner::Aggregate(Arc::clone(&value.storage))), + }) + } + + #[napi] + pub fn to_native_struct( + value: &DynWin32Value, + descriptor: String, + ) -> napi::Result { + let dynwinrt::win32::Value::OwnedAggregate { layout, bytes } = &value.value else { + return Err(napi::Error::from_reason( + "Win32 value is not an owned native aggregate", + )); + }; + if layout.identity() != descriptor { + return Err(napi::Error::from_reason( + "native aggregate return identity mismatch", + )); + } + Ok(DynWin32NativeStruct { + descriptor, + storage: Arc::new(NativeAggregateStorage::new( + bytes.len(), + Some(bytes), + false, + Vec::new(), + )?), + }) + } + + #[napi] + pub fn to_number(value: &DynWin32Value) -> napi::Result { + Ok(match &value.value { + dynwinrt::win32::Value::Bool(value) => u8::from(*value) as f64, + dynwinrt::win32::Value::I8(value) => *value as f64, + dynwinrt::win32::Value::U8(value) => *value as f64, + dynwinrt::win32::Value::I16(value) => *value as f64, + dynwinrt::win32::Value::U16(value) => *value as f64, + dynwinrt::win32::Value::I32(value) => *value as f64, + dynwinrt::win32::Value::U32(value) => *value as f64, + dynwinrt::win32::Value::F32(value) => *value as f64, + dynwinrt::win32::Value::F64(value) => *value, + _ => { + return Err(napi::Error::from_reason( + "Win32 value is not a JavaScript number", + )); + } + }) + } + + #[napi] + pub fn to_bigint(value: &DynWin32Value) -> napi::Result { + match &value.value { + dynwinrt::win32::Value::I64(value) => Ok(BigInt::from(*value)), + dynwinrt::win32::Value::U64(value) => Ok(BigInt::from(*value)), + dynwinrt::win32::Value::FunctionPointer(value) => Ok(BigInt::from(*value as u64)), + dynwinrt::win32::Value::Handle(value) => Ok(BigInt::from(*value as u64)), + dynwinrt::win32::Value::Resource(value) => Ok(BigInt::from(value.raw() as u64)), + dynwinrt::win32::Value::Null => Ok(BigInt::from(0u64)), + _ => Err(napi::Error::from_reason( + "Win32 value is not a 64-bit integer, pointer, or handle", + )), + } + } + + #[napi] + pub fn to_boolean(value: &DynWin32Value) -> napi::Result { + match value.value { + dynwinrt::win32::Value::Bool(value) => Ok(value), + dynwinrt::win32::Value::U8(value) => Ok(value != 0), + _ => Err(napi::Error::from_reason("Win32 value is not BOOL")), + } + } + + #[napi] + pub fn to_resource(value: &DynWin32Value) -> napi::Result> { + if matches!( + &value.value, + dynwinrt::win32::Value::Handle(0) | dynwinrt::win32::Value::Null + ) { + return Ok(None); + } + value + .value + .resource() + .cloned() + .map(DynWin32Resource) + .map(Some) + .ok_or_else(|| napi::Error::from_reason("Win32 value is not an owned resource")) + } +} + +fn reject_required_null_pointer(value: &DynWin32Value, nullable: bool) -> napi::Result<()> { + if !nullable + && matches!( + &value.value, + dynwinrt::win32::Value::Pointer(pointer) if pointer.is_null() + ) + { + return Err(napi::Error::from_reason( + "non-nullable native pointer requires non-empty backing storage", + )); + } + Ok(()) +} + +fn overlapped_io_task( + kind: OverlappedIoKind, + file: &DynWin32Resource, + buffer: Buffer, + offset: Option, +) -> napi::Result { + if file.0.cleanup() != dynwinrt::win32::Cleanup::CloseHandle { + return Err(napi::Error::from_reason( + "OVERLAPPED I/O requires a CloseHandle resource", + )); + } + if file.0.is_closed() { + return Err(napi::Error::from_reason( + "OVERLAPPED I/O cannot use a closed Win32 resource", + )); + } + let handle_bits = file.0.raw(); + if handle_bits == 0 || handle_bits == usize::MAX { + return Err(napi::Error::from_reason( + "OVERLAPPED I/O requires a valid file HANDLE", + )); + } + let offset = match offset { + Some(value) => { + let (negative, value, lossless) = value.get_u64(); + if negative || !lossless { + return Err(napi::Error::from_reason( + "OVERLAPPED offset must fit an unsigned 64-bit integer", + )); + } + value + } + None => 0, + }; + u32::try_from(buffer.len()) + .map_err(|_| napi::Error::from_reason("OVERLAPPED buffer exceeds u32"))?; + let lease = file + .0 + .async_lease(dynwinrt::win32::Cleanup::CloseHandle) + .map_err(|error| napi::Error::from_reason(error.message()))?; + let state = OverlappedState::new(offset); + Ok(DynWin32OverlappedOperation { + task: Some(OverlappedIoTask { + kind, + lease, + native_buffer: try_copy_io_buffer(kind, &buffer)?, + buffer_len: buffer.len(), + buffer: Some(buffer), + state: Arc::clone(&state), + }), + state, + }) +} + +fn validate_native_struct(value: &DynWin32NativeStruct, descriptor: &str) -> napi::Result<()> { + if value.descriptor != descriptor { + return Err(napi::Error::from_reason("native aggregate type mismatch")); + } + Ok(()) +} + +fn native_aggregate_field( + descriptor: &str, + field: &str, +) -> napi::Result<(usize, String, Option)> { + let root = parse_native_aggregate_descriptor(descriptor)?; + #[cfg(target_arch = "x86")] + let architecture = "x86"; + #[cfg(target_arch = "x86_64")] + let architecture = "x64"; + #[cfg(target_arch = "aarch64")] + let architecture = "arm64"; + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))] + return Err(napi::Error::from_reason( + "flat Win32 native aggregates support only x86, x64, and ARM64", + )); + let fields = root + .get(architecture) + .and_then(|layout| layout.get("fields")) + .and_then(serde_json::Value::as_array) + .ok_or_else(|| napi::Error::from_reason("native aggregate descriptor has no fields"))?; + let field = fields + .iter() + .find(|candidate| candidate.get("name").and_then(serde_json::Value::as_str) == Some(field)) + .ok_or_else(|| napi::Error::from_reason(format!("unknown native field `{field}`")))?; + let offset = field + .get("offset") + .and_then(serde_json::Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .ok_or_else(|| napi::Error::from_reason("native field has invalid offset"))?; + let kind = field + .get("type") + .and_then(|typ| typ.get("kind")) + .and_then(serde_json::Value::as_str) + .ok_or_else(|| napi::Error::from_reason("native field has invalid type"))?; + let cleanup = field + .get("type") + .and_then(|typ| typ.get("cleanup")) + .and_then(serde_json::Value::as_str) + .map(str::to_string); + Ok((offset, kind.to_string(), cleanup)) +} + +fn native_aggregate_layout( + descriptor: &str, +) -> napi::Result<(String, usize, usize, bool, Vec)> { + let root = parse_native_aggregate_descriptor(descriptor)?; + let name = root + .get("name") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| napi::Error::from_reason("native aggregate descriptor is missing `name`"))?; + #[cfg(target_arch = "x86")] + let architecture = "x86"; + #[cfg(target_arch = "x86_64")] + let architecture = "x64"; + #[cfg(target_arch = "aarch64")] + let architecture = "arm64"; + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))] + return Err(napi::Error::from_reason( + "flat Win32 native aggregates support only x86, x64, and ARM64", + )); + let layout = root.get(architecture).ok_or_else(|| { + napi::Error::from_reason(format!( + "native aggregate descriptor is missing `{architecture}`" + )) + })?; + let size = layout + .get("size") + .and_then(serde_json::Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .filter(|value| *value > 0) + .ok_or_else(|| napi::Error::from_reason("native aggregate has invalid `size`"))?; + if size > dynwinrt::win32::MAX_NATIVE_AGGREGATE_SIZE { + return Err(napi::Error::from_reason(format!( + "native aggregate exceeds the {} byte safety limit", + dynwinrt::win32::MAX_NATIVE_AGGREGATE_SIZE + ))); + } + let alignment = layout + .get("alignment") + .and_then(serde_json::Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .filter(|value| value.is_power_of_two() && size % *value == 0) + .ok_or_else(|| napi::Error::from_reason("native aggregate has invalid `alignment`"))?; + Ok(( + name.to_string(), + size, + alignment, + native_layout_contains_pointers(layout), + native_layout_owned_fields(layout)?, + )) +} + +fn native_layout_owned_fields(layout: &serde_json::Value) -> napi::Result> { + layout + .get("fields") + .and_then(serde_json::Value::as_array) + .into_iter() + .flatten() + .filter_map(|field| { + let typ = field.get("type")?; + (typ.get("kind").and_then(serde_json::Value::as_str) == Some("handle")) + .then_some((field, typ)) + }) + .map(|(field, typ)| { + let offset = field + .get("offset") + .and_then(serde_json::Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .ok_or_else(|| napi::Error::from_reason("owned native field has invalid offset"))?; + let cleanup = typ + .get("cleanup") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| napi::Error::from_reason("owned native field has no cleanup")) + .and_then(parse_cleanup)?; + if cleanup == dynwinrt::win32::Cleanup::None { + return Ok(None); + } + Ok(Some(OwnedNativeField { offset, cleanup })) + }) + .filter_map(|result| result.transpose()) + .collect() +} + +fn native_layout_contains_pointers(layout: &serde_json::Value) -> bool { + layout + .get("fields") + .and_then(serde_json::Value::as_array) + .is_some_and(|fields| { + fields.iter().any(|field| { + field + .get("type") + .and_then(|typ| { + typ + .get("kind") + .and_then(serde_json::Value::as_str) + .map(|kind| (typ, kind)) + }) + .is_some_and(|(typ, kind)| match kind { + "pointer" | "handle" => true, + "struct" | "union" => typ + .get("layout") + .is_some_and(native_layout_contains_pointers), + _ => false, + }) + }) + }) +} + +fn native_aggregate_call_layout( + descriptor: &str, +) -> napi::Result> { + let root = parse_native_aggregate_descriptor(descriptor)?; + #[cfg(target_arch = "x86")] + let architecture = "x86"; + #[cfg(target_arch = "x86_64")] + let architecture = "x64"; + #[cfg(target_arch = "aarch64")] + let architecture = "arm64"; + #[cfg(not(any(target_arch = "x86", target_arch = "x86_64", target_arch = "aarch64")))] + return Err(napi::Error::from_reason( + "flat Win32 native aggregates support only x86, x64, and ARM64", + )); + let layout = root.get(architecture).ok_or_else(|| { + napi::Error::from_reason(format!( + "native aggregate descriptor is missing `{architecture}`" + )) + })?; + let size = layout + .get("size") + .and_then(serde_json::Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .ok_or_else(|| napi::Error::from_reason("native aggregate has invalid `size`"))?; + let alignment = layout + .get("alignment") + .and_then(serde_json::Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .ok_or_else(|| napi::Error::from_reason("native aggregate has invalid `alignment`"))?; + let ffi_type = native_aggregate_ffi_type(layout)?; + dynwinrt::win32::NativeAggregateLayout::new(descriptor, size, alignment, ffi_type) + .map_err(|error| napi::Error::from_reason(error.message())) +} + +fn parse_native_aggregate_descriptor(descriptor: &str) -> napi::Result { + if descriptor.len() > MAX_NATIVE_AGGREGATE_DESCRIPTOR_LENGTH { + return Err(napi::Error::from_reason(format!( + "flat Win32 native aggregate descriptor exceeds the {MAX_NATIVE_AGGREGATE_DESCRIPTOR_LENGTH} byte safety limit" + ))); + } + serde_json::from_str(descriptor).map_err(|error| { + napi::Error::from_reason(format!( + "Invalid flat Win32 native aggregate descriptor: {error}" + )) + }) +} + +fn try_copy_io_buffer(kind: OverlappedIoKind, buffer: &Buffer) -> napi::Result> { + let mut native = Vec::new(); + native + .try_reserve_exact(buffer.len()) + .map_err(|_| napi::Error::from_reason("Unable to allocate private OVERLAPPED I/O buffer"))?; + match kind { + OverlappedIoKind::Read => native.resize(buffer.len(), 0), + OverlappedIoKind::Write => native.extend_from_slice(buffer), + } + Ok(native) +} + +fn native_aggregate_ffi_type(layout: &serde_json::Value) -> napi::Result { + let size = layout + .get("size") + .and_then(serde_json::Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .ok_or_else(|| napi::Error::from_reason("native aggregate has invalid `size`"))?; + let mut fields = layout + .get("fields") + .and_then(serde_json::Value::as_array) + .ok_or_else(|| napi::Error::from_reason("native aggregate is missing `fields`"))? + .iter() + .collect::>(); + fields.sort_by_key(|field| { + field + .get("offset") + .and_then(serde_json::Value::as_u64) + .unwrap_or(u64::MAX) + }); + let mut elements = Vec::new(); + let mut cursor = 0usize; + for field in fields { + let offset = field + .get("offset") + .and_then(serde_json::Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .ok_or_else(|| napi::Error::from_reason("native field has invalid `offset`"))?; + if offset < cursor { + return Err(napi::Error::from_reason( + "overlapping native fields cannot be passed by value", + )); + } + elements.extend(std::iter::repeat_with(libffi::middle::Type::u8).take(offset - cursor)); + let count = field + .get("count") + .and_then(serde_json::Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .filter(|value| *value > 0) + .ok_or_else(|| napi::Error::from_reason("native field has invalid `count`"))?; + let typ = field + .get("type") + .ok_or_else(|| napi::Error::from_reason("native field is missing `type`"))?; + let (field_type, field_size) = native_ffi_field_type(typ)?; + for _ in 0..count { + elements.push(field_type.clone()); + } + cursor = offset + .checked_add( + field_size + .checked_mul(count) + .ok_or_else(|| napi::Error::from_reason("native aggregate field size overflow"))?, + ) + .ok_or_else(|| napi::Error::from_reason("native aggregate field end overflow"))?; + } + if cursor > size { + return Err(napi::Error::from_reason( + "native aggregate fields exceed declared size", + )); + } + elements.extend(std::iter::repeat_with(libffi::middle::Type::u8).take(size - cursor)); + Ok(libffi::middle::Type::structure(elements)) +} + +fn native_ffi_field_type(typ: &serde_json::Value) -> napi::Result<(libffi::middle::Type, usize)> { + let kind = typ + .get("kind") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| napi::Error::from_reason("native field type is missing `kind`"))?; + Ok(match kind { + "i8" => (libffi::middle::Type::i8(), 1), + "u8" => (libffi::middle::Type::u8(), 1), + "i16" => (libffi::middle::Type::i16(), 2), + "u16" => (libffi::middle::Type::u16(), 2), + "i32" => (libffi::middle::Type::i32(), 4), + "u32" => (libffi::middle::Type::u32(), 4), + "i64" | "isize" => (libffi::middle::Type::i64(), 8), + "u64" | "usize" => (libffi::middle::Type::u64(), 8), + "f32" => (libffi::middle::Type::f32(), 4), + "f64" => (libffi::middle::Type::f64(), 8), + "pointer" => ( + libffi::middle::Type::pointer(), + std::mem::size_of::(), + ), + "guid" => { + let mut fields = vec![ + libffi::middle::Type::u32(), + libffi::middle::Type::u16(), + libffi::middle::Type::u16(), + ]; + fields.extend(std::iter::repeat_with(libffi::middle::Type::u8).take(8)); + (libffi::middle::Type::structure(fields), 16) + } + "struct" => { + let layout = typ + .get("layout") + .ok_or_else(|| napi::Error::from_reason("nested struct is missing `layout`"))?; + let size = layout + .get("size") + .and_then(serde_json::Value::as_u64) + .and_then(|value| usize::try_from(value).ok()) + .ok_or_else(|| napi::Error::from_reason("nested struct has invalid `size`"))?; + (native_aggregate_ffi_type(layout)?, size) + } + "union" => { + return Err(napi::Error::from_reason( + "nested unions cannot be passed by value safely", + )); + } + _ => { + return Err(napi::Error::from_reason(format!( + "unsupported native aggregate field kind `{kind}`" + ))); + } + }) +} + +#[napi] +pub struct DynWin32Unsafe; + +#[napi] +impl DynWin32Unsafe { + #[napi] + pub fn bind(spec: DynWin32FunctionSpec) -> napi::Result { + bind_function(spec) + } + + #[napi] + pub fn pointer( + #[napi(ts_arg_type = "bigint | number | Buffer | Uint8Array | null | undefined")] + value: Unknown, + ) -> napi::Result { + pointer_value(com::pointer(value)?) + } + + #[napi] + pub fn pointer_address(value: &DynWin32Value) -> napi::Result { + value.validate()?; + match value.value { + dynwinrt::win32::Value::Pointer(pointer) => Ok(BigInt::from(pointer as usize as u64)), + dynwinrt::win32::Value::Null => Ok(BigInt::from(0u64)), + _ => Err(napi::Error::from_reason( + "DynWin32Unsafe.pointerAddress(): value is not a data pointer", + )), + } + } +} + +fn bind_function(spec: DynWin32FunctionSpec) -> napi::Result { + let mut parameter_aggregates = Vec::with_capacity(spec.parameters.len()); + let parameters = spec + .parameters + .into_iter() + .map(|parameter| { + let aggregate = parameter + .aggregate_descriptor + .as_deref() + .map(native_aggregate_call_layout) + .transpose()?; + let typ = if aggregate.is_some() { + dynwinrt::win32::Type::Pointer + } else { + parse_type(¶meter.typ)? + }; + parameter_aggregates.push(aggregate); + Ok(dynwinrt::win32::Parameter { + typ, + direction: parse_direction(¶meter.direction)?, + nullable: parameter.nullable.unwrap_or(false), + cleanup: parse_cleanup(parameter.cleanup.as_deref().unwrap_or("none"))?, + consumes_resource: parameter.consumes_resource.unwrap_or(false), + resource_cleanup: parse_cleanup(parameter.resource_cleanup.as_deref().unwrap_or("none"))?, + }) + }) + .collect::>>()?; + let return_aggregate = spec + .return_aggregate_descriptor + .as_deref() + .map(native_aggregate_call_layout) + .transpose()?; + let return_type = spec + .return_type + .as_deref() + .filter(|value| !value.eq_ignore_ascii_case("void")) + .map(parse_type) + .transpose()? + .filter(|_| return_aggregate.is_none()); + let plan = unsafe { + dynwinrt::win32::CallPlan::new(dynwinrt::win32::CallPlanSpec { + dll: spec.dll, + entry_point: spec.entry_point, + parameters, + return_type, + return_cleanup: parse_cleanup(spec.return_cleanup.as_deref().unwrap_or("none"))?, + success_rule: parse_success_rule(spec.success_rule.as_deref().unwrap_or("always"))?, + capture_last_error: spec.capture_last_error.unwrap_or(false), + calling_convention: parse_calling_convention( + spec.calling_convention.as_deref().unwrap_or("system"), + )?, + parameter_aggregates, + return_aggregate, + }) + } + .map_err(|error| napi::Error::from_reason(error.message()))?; + Ok(DynWin32Function(plan)) +} + +fn parse_type(value: &str) -> napi::Result { + use dynwinrt::win32::Type; + match value.to_ascii_lowercase().as_str() { + "bool32" | "bool" => Ok(Type::Bool32), + "i8" => Ok(Type::I8), + "u8" => Ok(Type::U8), + "i16" => Ok(Type::I16), + "u16" | "char16" => Ok(Type::U16), + "i32" => Ok(Type::I32), + "u32" => Ok(Type::U32), + "i64" => Ok(Type::I64), + "u64" => Ok(Type::U64), + "f32" => Ok(Type::F32), + "f64" => Ok(Type::F64), + "pointer" | "ptr" => Ok(Type::Pointer), + "functionpointer" | "function_pointer" => Ok(Type::FunctionPointer), + "handle" => Ok(Type::Handle), + _ => Err(napi::Error::from_reason(format!( + "Unsupported flat Win32 ABI type `{value}`" + ))), + } +} + +fn parse_direction(value: &str) -> napi::Result { + use dynwinrt::win32::Direction; + match value.to_ascii_lowercase().as_str() { + "in" => Ok(Direction::In), + "out" => Ok(Direction::Out), + "inout" | "in_out" | "in,out" => Ok(Direction::InOut), + _ => Err(napi::Error::from_reason(format!( + "Unsupported flat Win32 parameter direction `{value}`" + ))), + } +} + +fn parse_cleanup(value: &str) -> napi::Result { + use dynwinrt::win32::Cleanup; + match value.to_ascii_lowercase().as_str() { + "none" => Ok(Cleanup::None), + "closehandle" => Ok(Cleanup::CloseHandle), + "regclosekey" => Ok(Cleanup::RegCloseKey), + "localfree" => Ok(Cleanup::LocalFree), + "globalfree" => Ok(Cleanup::GlobalFree), + "freelibrary" => Ok(Cleanup::FreeLibrary), + "closeservicehandle" => Ok(Cleanup::CloseServiceHandle), + "cotaskmemfree" => Ok(Cleanup::CoTaskMemFree), + "credfree" => Ok(Cleanup::CredFree), + _ => Err(napi::Error::from_reason(format!( + "Unsupported flat Win32 cleanup `{value}`" + ))), + } +} + +fn parse_success_rule(value: &str) -> napi::Result { + use dynwinrt::win32::SuccessRule; + match value.to_ascii_lowercase().as_str() { + "always" => Ok(SuccessRule::Always), + "zero" | "returnzero" => Ok(SuccessRule::ReturnZero), + "nonzero" | "returnnonzero" => Ok(SuccessRule::ReturnNonZero), + "nonnull" | "returnnonnull" => Ok(SuccessRule::ReturnNonNull), + "hresult" | "hresultsucceeded" => Ok(SuccessRule::HResultSucceeded), + "signednonnegative" | "nonnegative" => Ok(SuccessRule::SignedNonNegative), + "validhandle" | "returnvalidhandle" => Ok(SuccessRule::ReturnValidHandle), + _ => Err(napi::Error::from_reason(format!( + "Unsupported flat Win32 success rule `{value}`" + ))), + } +} + +fn parse_calling_convention(value: &str) -> napi::Result { + use dynwinrt::win32::CallingConvention; + match value.to_ascii_lowercase().as_str() { + "system" | "winapi" => Ok(CallingConvention::System), + "cdecl" | "c" => Ok(CallingConvention::Cdecl), + _ => Err(napi::Error::from_reason(format!( + "Unsupported flat Win32 calling convention `{value}`" + ))), + } +} + +fn pointer_value(owner: DynWinRTValue) -> napi::Result { + let value = match owner.0 { + dynwinrt::WinRTValue::RawPtr(value) => dynwinrt::win32::Value::Pointer(value), + dynwinrt::WinRTValue::Null => dynwinrt::win32::Value::Null, + _ => { + return Err(napi::Error::from_reason( + "native pointer helper did not produce a pointer value", + )); + } + }; + Ok(DynWin32Value::with_pointer_owner(value, owner)) +} + +fn string_pointer_pointer( + value: Unknown, + nullable: bool, + wide: bool, +) -> napi::Result { + let env = value.value().env; + let raw = value.value().value; + let mut value_type = napi::sys::ValueType::napi_undefined; + unsafe { napi::sys::napi_typeof(env, raw, &mut value_type) }; + if matches!( + value_type, + napi::sys::ValueType::napi_null | napi::sys::ValueType::napi_undefined + ) { + return if nullable { + Ok(DynWin32Value::new(dynwinrt::win32::Value::Null)) + } else { + Err(napi::Error::from_reason( + "string pointer slot null requires an explicitly nullable parameter", + )) + }; + } + let inner = if wide { + com::safe_wide_string_pointer(value, false)? + } else { + com::safe_ansi_string_pointer(value, false)? + }; + let pointer = match &inner.0 { + dynwinrt::WinRTValue::RawPtr(pointer) => *pointer as usize, + _ => { + return Err(napi::Error::from_reason( + "string pointer helper did not produce native storage", + )); + } + }; + let mut slot = Box::new(pointer); + let slot_pointer = (&mut *slot as *mut usize).cast(); + Ok(DynWin32Value { + value: dynwinrt::win32::Value::Pointer(slot_pointer), + pointer_owner: Some(Win32PointerOwner::PointerSlot { + inner: Arc::new(inner), + slot, + }), + }) +} + +fn handle_value(value: Unknown, nullable: bool) -> napi::Result { + use napi::sys; + + let env = value.value().env; + let raw = value.value().value; + if let Ok(resource) = unsafe { <&DynWin32Resource>::from_napi_value(env, raw) } { + if resource.0.is_closed() { + return Err(napi::Error::from_reason( + "Cannot use a closed flat Win32 resource", + )); + } + return Ok(DynWin32Value::new(dynwinrt::win32::Value::Resource( + Arc::clone(&resource.0), + ))); + } + + 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 if nullable { + Ok(DynWin32Value::new(dynwinrt::win32::Value::Null)) + } else { + Err(napi::Error::from_reason( + "DynWin32.handle(): null requires an explicitly nullable handle parameter", + )) + }; + } + let bits = if value_type == sys::ValueType::napi_bigint { + let value = unsafe { BigInt::from_napi_value(env, raw) }?; + let (signed, signed_lossless) = value.get_i64(); + if signed_lossless { + signed as u64 + } else { + let (negative, unsigned, unsigned_lossless) = value.get_u64(); + if negative || !unsigned_lossless { + return Err(napi::Error::from_reason( + "DynWin32.handle(): bigint must fit in signed or unsigned pointer bits", + )); + } + unsigned + } + } else 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.fract() != 0.0 || number.abs() > 9_007_199_254_740_991.0 { + return Err(napi::Error::from_reason( + "DynWin32.handle(): number must be a safe integer", + )); + } + (number as i64) as u64 + } else { + return Err(napi::Error::from_reason( + "DynWin32.handle(): expected bigint, number, or DynWin32Resource", + )); + }; + if bits as usize as u64 != bits { + return Err(napi::Error::from_reason( + "DynWin32.handle(): value does not fit this process pointer width", + )); + } + Ok(DynWin32Value::new(dynwinrt::win32::Value::Handle( + bits as usize, + ))) +} diff --git a/crates/dynwinrt/Cargo.toml b/crates/dynwinrt/Cargo.toml index ad0b6623..87cad4bd 100644 --- a/crates/dynwinrt/Cargo.toml +++ b/crates/dynwinrt/Cargo.toml @@ -32,10 +32,14 @@ features = [ "Win32_System_Com", "Win32_System_Com_StructuredStorage", "Win32_System_LibraryLoader", + "Win32_System_Memory", "Win32_System_Ole", + "Win32_System_Registry", + "Win32_System_Services", "Win32_System_Threading", "Win32_System_Variant", "Win32_System_WinRT", + "Win32_Security_Credentials", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging", "Win32_Storage_Packaging_Appx", diff --git a/crates/dynwinrt/src/call.rs b/crates/dynwinrt/src/call.rs index 3778cb91..2358abe3 100644 --- a/crates/dynwinrt/src/call.rs +++ b/crates/dynwinrt/src/call.rs @@ -229,6 +229,37 @@ pub fn get_vtable_function_ptr(obj: *mut c_void, method_index: usize) -> *mut c_ } } +pub(crate) unsafe fn call_native_function( + cif: &libffi::middle::Cif, + function: *mut c_void, + args: &[Arg<'_>], + return_type: Option, +) -> Option { + use libffi::middle::CodePtr; + + match return_type { + None => { + unsafe { cif.call::<()>(CodePtr(function), args) }; + None + } + Some(AbiType::Bool) => Some(AbiValue::Bool(unsafe { cif.call(CodePtr(function), args) })), + Some(AbiType::I8) => Some(AbiValue::I8(unsafe { cif.call(CodePtr(function), args) })), + Some(AbiType::U8) => Some(AbiValue::U8(unsafe { cif.call(CodePtr(function), args) })), + Some(AbiType::I16) => Some(AbiValue::I16(unsafe { cif.call(CodePtr(function), args) })), + Some(AbiType::U16) => Some(AbiValue::U16(unsafe { cif.call(CodePtr(function), args) })), + Some(AbiType::I32) => Some(AbiValue::I32(unsafe { cif.call(CodePtr(function), args) })), + Some(AbiType::U32) => Some(AbiValue::U32(unsafe { cif.call(CodePtr(function), args) })), + Some(AbiType::I64) => Some(AbiValue::I64(unsafe { cif.call(CodePtr(function), args) })), + Some(AbiType::U64) => Some(AbiValue::U64(unsafe { cif.call(CodePtr(function), args) })), + Some(AbiType::F32) => Some(AbiValue::F32(unsafe { cif.call(CodePtr(function), args) })), + Some(AbiType::F64) => Some(AbiValue::F64(unsafe { cif.call(CodePtr(function), args) })), + Some(AbiType::Guid) => Some(AbiValue::Guid(unsafe { cif.call(CodePtr(function), args) })), + Some(AbiType::Ptr) => Some(AbiValue::Pointer(unsafe { + cif.call(CodePtr(function), args) + })), + } +} + pub fn call_winrt_method_0(vtable_index: usize, obj: *mut c_void) -> HRESULT { let method_ptr = get_vtable_function_ptr(obj, vtable_index); unsafe { diff --git a/crates/dynwinrt/src/lib.rs b/crates/dynwinrt/src/lib.rs index 884f7e7f..e69553a4 100644 --- a/crates/dynwinrt/src/lib.rs +++ b/crates/dynwinrt/src/lib.rs @@ -13,6 +13,7 @@ mod result; mod roapi; mod signature; mod value; +pub mod win32; mod winapp; mod xaml_application; diff --git a/crates/dynwinrt/src/win32.rs b/crates/dynwinrt/src/win32.rs new file mode 100644 index 00000000..09215de9 --- /dev/null +++ b/crates/dynwinrt/src/win32.rs @@ -0,0 +1,1585 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Immutable call plans for flat Win32 exports. +//! +//! This module deliberately has its own ABI vocabulary. WinRT and Classic COM +//! types do not participate in flat export planning. + +use core::ffi::c_void; +#[cfg(not(all(windows, target_pointer_width = "32")))] +use std::collections::HashMap; +#[cfg(not(all(windows, target_pointer_width = "32")))] +use std::ffi::CString; +#[cfg(not(all(windows, target_pointer_width = "32")))] +use std::sync::OnceLock; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; + +use libffi::middle::Type as FfiType; +use libffi::middle::{Arg, Cif, Ret, arg}; +use windows::Win32::Foundation::{ + CloseHandle, FreeLibrary, GetLastError, HANDLE, HLOCAL, HMODULE, LocalFree, +}; +use windows::Win32::Security::Credentials::CredFree; +#[cfg(not(all(windows, target_pointer_width = "32")))] +use windows::Win32::System::LibraryLoader::{ + GetProcAddress, LOAD_LIBRARY_SEARCH_SYSTEM32, LoadLibraryExW, +}; +use windows::Win32::System::Registry::{HKEY, RegCloseKey}; +use windows::Win32::System::Services::{CloseServiceHandle, SC_HANDLE}; +use windows_core::HRESULT; +#[cfg(not(all(windows, target_pointer_width = "32")))] +use windows_core::{HSTRING, PCSTR}; + +use crate::abi::{AbiType, AbiValue}; +#[cfg(not(all(windows, target_pointer_width = "32")))] +use crate::native_call::system_cif; +use crate::result::{Error, Result}; + +const E_INVALIDARG: HRESULT = HRESULT(0x80070057u32 as i32); +#[cfg(all(windows, target_pointer_width = "32"))] +const E_NOTIMPL: HRESULT = HRESULT(0x80004001u32 as i32); +pub const MAX_NATIVE_AGGREGATE_SIZE: usize = 16 * 1024 * 1024; + +windows_link::link!("kernel32.dll" "system" "GlobalFree" fn global_free_raw(hmem: *mut c_void) -> *mut c_void); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Type { + Bool32, + I8, + U8, + I16, + U16, + I32, + U32, + I64, + U64, + F32, + F64, + Pointer, + FunctionPointer, + Handle, +} + +#[derive(Debug, Clone)] +pub struct NativeAggregateLayout { + identity: String, + size: usize, + alignment: usize, + ffi_type: FfiType, +} + +impl PartialEq for NativeAggregateLayout { + fn eq(&self, other: &Self) -> bool { + self.identity == other.identity + && self.size == other.size + && self.alignment == other.alignment + } +} + +impl Eq for NativeAggregateLayout {} + +impl NativeAggregateLayout { + pub fn new( + identity: impl Into, + size: usize, + alignment: usize, + ffi_type: FfiType, + ) -> Result> { + let identity = identity.into(); + if identity.trim().is_empty() + || size == 0 + || alignment == 0 + || !alignment.is_power_of_two() + || size % alignment != 0 + || alignment > 8 + || size > MAX_NATIVE_AGGREGATE_SIZE + { + return Err(invalid_argument("invalid native aggregate call layout")); + } + Ok(Arc::new(Self { + identity, + size, + alignment, + ffi_type, + })) + } + + pub fn identity(&self) -> &str { + &self.identity + } + + pub const fn size(&self) -> usize { + self.size + } + + fn libffi_type(&self) -> FfiType { + self.ffi_type.clone() + } +} + +impl Type { + fn abi_type(self) -> AbiType { + match self { + Self::Bool32 | Self::I32 => AbiType::I32, + Self::I8 => AbiType::I8, + Self::U8 => AbiType::U8, + Self::I16 => AbiType::I16, + Self::U16 => AbiType::U16, + Self::U32 => AbiType::U32, + Self::I64 => AbiType::I64, + Self::U64 => AbiType::U64, + Self::F32 => AbiType::F32, + Self::F64 => AbiType::F64, + Self::Pointer | Self::FunctionPointer | Self::Handle => AbiType::Ptr, + } + } + + #[cfg(not(all(windows, target_pointer_width = "32")))] + fn is_pointer_like(self) -> bool { + matches!(self, Self::Pointer | Self::FunctionPointer | Self::Handle) + } + + fn default_abi_value(self) -> AbiValue { + self.abi_type().default_value() + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + In, + Out, + InOut, +} + +impl Direction { + #[cfg(not(all(windows, target_pointer_width = "32")))] + fn is_input(self) -> bool { + matches!(self, Self::In | Self::InOut) + } + + fn is_output(self) -> bool { + matches!(self, Self::Out | Self::InOut) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Cleanup { + None, + CloseHandle, + RegCloseKey, + LocalFree, + GlobalFree, + FreeLibrary, + CloseServiceHandle, + CoTaskMemFree, + CredFree, +} + +impl Cleanup { + fn owns_resource(self) -> bool { + self != Self::None + } + + unsafe fn run(self, value: usize) -> windows_core::Result<()> { + if value == 0 || self == Self::None { + return Ok(()); + } + match self { + Self::None => Ok(()), + Self::CloseHandle => unsafe { CloseHandle(HANDLE(value as *mut c_void)) }, + Self::RegCloseKey => { + let status = unsafe { RegCloseKey(HKEY(value as *mut c_void)) }; + if status.0 == 0 { + Ok(()) + } else { + Err(windows_core::Error::from_hresult(hresult_from_win32( + status.0 as u32, + ))) + } + } + Self::LocalFree => { + let remaining = unsafe { LocalFree(Some(HLOCAL(value as *mut c_void))) }; + if remaining.is_invalid() { + Ok(()) + } else { + Err(windows_core::Error::from_hresult(hresult_from_win32( + unsafe { GetLastError().0 }, + ))) + } + } + Self::GlobalFree => { + let remaining = unsafe { global_free_raw(value as *mut c_void) }; + if remaining.is_null() { + Ok(()) + } else { + Err(windows_core::Error::from_hresult(hresult_from_win32( + unsafe { GetLastError().0 }, + ))) + } + } + Self::FreeLibrary => unsafe { FreeLibrary(HMODULE(value as *mut c_void)) }, + Self::CloseServiceHandle => unsafe { + CloseServiceHandle(SC_HANDLE(value as *mut c_void)) + }, + Self::CoTaskMemFree => { + unsafe { + windows::Win32::System::Com::CoTaskMemFree(Some(value as *const c_void)); + } + Ok(()) + } + Self::CredFree => { + unsafe { CredFree(value as *const c_void) }; + Ok(()) + } + } + } +} + +/// Releases a native resource whose cleanup contract was validated by a +/// semantic projection and whose ownership has not been adopted elsewhere. +pub unsafe fn cleanup_owned_resource(value: usize, cleanup: Cleanup) -> windows_core::Result<()> { + unsafe { cleanup.run(value) } +} + +fn hresult_from_win32(code: u32) -> HRESULT { + if code == 0 { + HRESULT(0) + } else { + HRESULT((0x80070000u32 | (code & 0xffff)) as i32) + } +} + +#[derive(Debug)] +pub struct OwnedResource { + value: Mutex, + cleanup: Cleanup, + async_leases: AtomicUsize, + active_async_io: AtomicUsize, +} + +pub struct OwnedResourceLease<'a> { + value: std::sync::MutexGuard<'a, usize>, +} + +pub struct OwnedResourceAsyncLease { + resource: Arc, + value: usize, + active: bool, +} + +impl OwnedResourceAsyncLease { + pub fn raw(&self) -> usize { + self.value + } + + pub fn mark_active(&mut self) { + if !self.active { + self.resource.active_async_io.fetch_add(1, Ordering::AcqRel); + self.active = true; + } + } + + pub fn mark_inactive(&mut self) { + if self.active { + self.resource.active_async_io.fetch_sub(1, Ordering::AcqRel); + self.active = false; + } + } +} + +impl Drop for OwnedResourceAsyncLease { + fn drop(&mut self) { + self.mark_inactive(); + self.resource.async_leases.fetch_sub(1, Ordering::AcqRel); + } +} + +impl OwnedResourceLease<'_> { + pub fn raw(&self) -> usize { + *self.value + } +} + +impl OwnedResource { + fn new(value: usize, cleanup: Cleanup) -> Self { + debug_assert!(value != 0); + debug_assert!(cleanup.owns_resource()); + Self { + value: Mutex::new(value), + cleanup, + async_leases: AtomicUsize::new(0), + active_async_io: AtomicUsize::new(0), + } + } + + /// Adopts a native resource whose exact ownership and cleanup were + /// validated by the flat Win32 semantic projection. + pub unsafe fn adopt(value: usize, cleanup: Cleanup) -> Result> { + if value == 0 || !cleanup.owns_resource() { + return Err(invalid_argument( + "owned Win32 resource requires a nonzero value and cleanup", + )); + } + Ok(Arc::new(Self::new(value, cleanup))) + } + + pub fn raw(&self) -> usize { + *self.value.lock().unwrap_or_else(|error| error.into_inner()) + } + + pub fn is_closed(&self) -> bool { + self.raw() == 0 + } + + pub fn cleanup(&self) -> Cleanup { + self.cleanup + } + + pub fn has_async_leases(&self) -> bool { + self.async_leases.load(Ordering::Acquire) != 0 + } + + pub fn has_active_async_io(&self) -> bool { + self.active_async_io.load(Ordering::Acquire) != 0 + } + + pub fn lease(&self, expected_cleanup: Cleanup) -> Result> { + if self.cleanup != expected_cleanup { + return Err(invalid_argument( + "managed Win32 resource cleanup kind does not match", + )); + } + let value = self.value.lock().unwrap_or_else(|error| error.into_inner()); + if *value == 0 { + return Err(invalid_argument("cannot lease a closed Win32 resource")); + } + Ok(OwnedResourceLease { value }) + } + + fn lock_for_call(&self, consumes_resource: bool) -> Result> { + let value = self.value.lock().unwrap_or_else(|error| error.into_inner()); + if consumes_resource && self.has_async_leases() { + return Err(invalid_argument( + "cannot consume a Win32 resource while asynchronous I/O is pending", + )); + } + Ok(value) + } + + pub fn async_lease( + self: &Arc, + expected_cleanup: Cleanup, + ) -> Result { + if self.cleanup != expected_cleanup { + return Err(invalid_argument( + "managed Win32 resource cleanup kind does not match", + )); + } + let value = self.value.lock().unwrap_or_else(|error| error.into_inner()); + if *value == 0 { + return Err(invalid_argument("cannot lease a closed Win32 resource")); + } + self.async_leases.fetch_add(1, Ordering::AcqRel); + Ok(OwnedResourceAsyncLease { + resource: Arc::clone(self), + value: *value, + active: false, + }) + } + + pub fn close(&self) -> windows_core::Result<()> { + let mut value = self.value.lock().unwrap_or_else(|error| error.into_inner()); + if self.has_async_leases() { + return Err(windows_core::Error::new( + HRESULT(0x800700AAu32 as i32), + "cannot close a Win32 resource while asynchronous I/O is pending", + )); + } + unsafe { self.cleanup.run(*value) }?; + *value = 0; + Ok(()) + } +} + +impl Drop for OwnedResource { + fn drop(&mut self) { + let value = *self + .value + .get_mut() + .unwrap_or_else(|error| error.into_inner()); + let _ = unsafe { self.cleanup.run(value) }; + } +} + +#[derive(Debug, Clone)] +pub enum Value { + Bool(bool), + I8(i8), + U8(u8), + I16(i16), + U16(u16), + I32(i32), + U32(u32), + I64(i64), + U64(u64), + F32(f32), + F64(f64), + Pointer(*mut c_void), + FunctionPointer(usize), + Handle(usize), + Resource(Arc), + Aggregate { + layout: Arc, + pointer: *mut c_void, + }, + OwnedAggregate { + layout: Arc, + bytes: Vec, + }, + Null, +} + +unsafe impl Send for Value {} +unsafe impl Sync for Value {} + +impl Value { + pub fn resource(&self) -> Option<&Arc> { + match self { + Self::Resource(value) => Some(value), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Parameter { + pub typ: Type, + pub direction: Direction, + pub nullable: bool, + pub cleanup: Cleanup, + pub consumes_resource: bool, + pub resource_cleanup: Cleanup, +} + +impl Parameter { + pub const fn input(typ: Type, nullable: bool) -> Self { + Self { + typ, + direction: Direction::In, + nullable, + cleanup: Cleanup::None, + consumes_resource: false, + resource_cleanup: Cleanup::None, + } + } + + pub const fn output(typ: Type, cleanup: Cleanup) -> Self { + Self { + typ, + direction: Direction::Out, + nullable: false, + cleanup, + consumes_resource: false, + resource_cleanup: Cleanup::None, + } + } + + pub const fn input_output(typ: Type, nullable: bool, cleanup: Cleanup) -> Self { + Self { + typ, + direction: Direction::InOut, + nullable, + cleanup, + consumes_resource: false, + resource_cleanup: Cleanup::None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SuccessRule { + Always, + ReturnZero, + ReturnNonZero, + ReturnNonNull, + HResultSucceeded, + SignedNonNegative, + ReturnValidHandle, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CallingConvention { + System, + Cdecl, +} + +#[derive(Debug, Clone)] +pub struct CallPlanSpec { + pub dll: String, + pub entry_point: String, + pub parameters: Vec, + pub return_type: Option, + pub return_cleanup: Cleanup, + pub success_rule: SuccessRule, + pub capture_last_error: bool, + pub calling_convention: CallingConvention, + pub parameter_aggregates: Vec>>, + pub return_aggregate: Option>, +} + +#[derive(Debug)] +struct PlannedParameter { + spec: Parameter, + input_index: Option, + output_index: Option, +} + +#[derive(Debug)] +pub struct CallPlan { + dll: String, + entry_point: String, + function: usize, + parameters: Vec, + input_count: usize, + output_count: usize, + return_type: Option, + return_cleanup: Cleanup, + success_rule: SuccessRule, + capture_last_error: bool, + calling_convention: CallingConvention, + parameter_aggregates: Vec>>, + return_aggregate: Option>, + cif: Cif, +} + +// Safety: plans are completely built before publication and immutable +// afterwards. libffi reads the prepared CIF and type graph during ffi_call. +unsafe impl Send for CallPlan {} +unsafe impl Sync for CallPlan {} + +#[derive(Debug)] +pub struct CallResult { + pub return_value: Option, + pub outputs: Vec, + pub last_error: Option, + pub succeeded: bool, +} + +impl CallPlan { + /// # Safety + /// + /// The specification must exactly match the target export's native ABI. + pub unsafe fn new(spec: CallPlanSpec) -> Result> { + #[cfg(all(windows, target_pointer_width = "32"))] + { + let _ = spec; + return Err(not_implemented( + "flat Win32 plans currently reject 32-bit targets because metadata calling conventions are not yet projected", + )); + } + + #[cfg(not(all(windows, target_pointer_width = "32")))] + { + validate_spec(&spec)?; + let module = get_cached_module(&spec.dll)?; + let function = proc_address(module, &spec.dll, &spec.entry_point)? as usize; + + let mut input_count = 0; + let mut output_count = 0; + let parameters = spec + .parameters + .iter() + .copied() + .map(|parameter| { + let input_index = parameter.direction.is_input().then(|| { + let index = input_count; + input_count += 1; + index + }); + let output_index = parameter.direction.is_output().then(|| { + let index = output_count; + output_count += 1; + index + }); + PlannedParameter { + spec: parameter, + input_index, + output_index, + } + }) + .collect::>(); + if spec.parameter_aggregates.len() != parameters.len() { + return Err(invalid_argument( + "flat Win32 aggregate descriptor count must match native parameters", + )); + } + let argument_types = parameters + .iter() + .enumerate() + .map(|(index, parameter)| { + if parameter.spec.direction.is_output() { + FfiType::pointer() + } else if let Some(layout) = &spec.parameter_aggregates[index] { + layout.libffi_type() + } else { + parameter.spec.typ.abi_type().libffi_type() + } + }) + .collect::>(); + let return_ffi_type = spec.return_aggregate.as_ref().map_or_else( + || { + spec.return_type + .map_or_else(FfiType::void, |typ| typ.abi_type().libffi_type()) + }, + |layout| layout.libffi_type(), + ); + let cif = match spec.calling_convention { + CallingConvention::System => system_cif(argument_types, return_ffi_type), + CallingConvention::Cdecl => Cif::new(argument_types, return_ffi_type), + }; + + Ok(Arc::new(Self { + dll: spec.dll, + entry_point: spec.entry_point, + function, + parameters, + input_count, + output_count, + return_type: spec.return_type, + return_cleanup: spec.return_cleanup, + success_rule: spec.success_rule, + capture_last_error: spec.capture_last_error, + calling_convention: spec.calling_convention, + parameter_aggregates: spec.parameter_aggregates, + return_aggregate: spec.return_aggregate, + cif, + })) + } + } + + pub fn dll(&self) -> &str { + &self.dll + } + + pub fn entry_point(&self) -> &str { + &self.entry_point + } + + pub fn input_count(&self) -> usize { + self.input_count + } + + pub fn output_count(&self) -> usize { + self.output_count + } + + pub fn calling_convention(&self) -> CallingConvention { + self.calling_convention + } + + /// # Safety + /// + /// Every pointer and aggregate argument must remain valid for the complete + /// native call and satisfy the contract used to construct this plan. + pub unsafe fn invoke(&self, args: &[Value]) -> Result { + if args.len() != self.input_count { + return Err(invalid_argument(&format!( + "{}!{} expects {} inputs, received {}", + self.dll, + self.entry_point, + self.input_count, + args.len() + ))); + } + for parameter in &self.parameters { + if !parameter.spec.consumes_resource { + continue; + } + let input_index = parameter + .input_index + .expect("consuming parameter is an input"); + let Value::Resource(resource) = &args[input_index] else { + return Err(invalid_argument( + "consuming Win32 parameters require a managed resource object", + )); + }; + if resource.cleanup() != parameter.spec.resource_cleanup { + return Err(invalid_argument( + "managed Win32 resource cleanup kind does not match the consuming parameter", + )); + } + } + + let mut resources = args + .iter() + .enumerate() + .filter_map(|(index, value)| match value { + Value::Resource(resource) => Some((index, resource)), + _ => None, + }) + .collect::>(); + resources.sort_by_key(|(_, resource)| Arc::as_ptr(resource) as usize); + if resources + .windows(2) + .any(|pair| Arc::ptr_eq(pair[0].1, pair[1].1)) + { + return Err(invalid_argument( + "the same managed Win32 resource cannot occupy multiple parameters in one call", + )); + } + let mut resource_guard_by_arg = vec![None; args.len()]; + let mut resource_guards = Vec::with_capacity(resources.len()); + for (arg_index, resource) in resources { + let consumes_resource = self.parameters.iter().any(|parameter| { + parameter.input_index == Some(arg_index) && parameter.spec.consumes_resource + }); + let guard = resource.lock_for_call(consumes_resource)?; + let guard_index = resource_guards.len(); + resource_guard_by_arg[arg_index] = Some(guard_index); + resource_guards.push(guard); + } + + let input_storage = self + .parameters + .iter() + .enumerate() + .map(|(parameter_index, parameter)| { + if self.parameter_aggregates[parameter_index].is_some() { + return Ok(None); + } + parameter + .input_index + .map(|index| { + value_to_abi( + parameter.spec.typ, + &args[index], + parameter.spec.nullable, + parameter.spec.resource_cleanup, + resource_guard_by_arg[index] + .map(|guard_index| *resource_guards[guard_index]), + ) + }) + .transpose() + }) + .collect::>>()?; + let mut output_storage = self + .parameters + .iter() + .filter(|parameter| parameter.spec.direction.is_output()) + .map(|parameter| { + if parameter.spec.direction == Direction::InOut { + value_to_abi( + parameter.spec.typ, + &args[parameter.input_index.expect("in/out input")], + parameter.spec.nullable, + parameter.spec.resource_cleanup, + parameter + .input_index + .and_then(|index| resource_guard_by_arg[index]) + .map(|guard_index| *resource_guards[guard_index]), + ) + } else { + Ok(parameter.spec.typ.default_abi_value()) + } + }) + .collect::>>()?; + debug_assert_eq!(output_storage.len(), self.output_count); + let output_pointers = output_storage + .iter() + .map(|value| value.as_out_ptr() as *mut c_void) + .collect::>(); + + let mut ffi_args = Vec::>::with_capacity(self.parameters.len()); + for (index, parameter) in self.parameters.iter().enumerate() { + if let Some(output_index) = parameter.output_index { + ffi_args.push(arg(&output_pointers[output_index])); + } else if let Some(expected) = &self.parameter_aggregates[index] { + let input_index = parameter.input_index.expect("aggregate input index"); + let Value::Aggregate { layout, pointer } = &args[input_index] else { + return Err(invalid_argument( + "flat Win32 argument does not match native aggregate", + )); + }; + if layout != expected || pointer.is_null() { + return Err(invalid_argument( + "flat Win32 native aggregate identity mismatch", + )); + } + ffi_args.push(Arg::new(unsafe { &*pointer.cast::() })); + } else { + ffi_args.push(abi_arg( + input_storage[index] + .as_ref() + .expect("input parameter has ABI storage"), + )); + } + } + + let mut aggregate_return_words = self + .return_aggregate + .as_ref() + .map(|layout| try_zeroed_words(layout.size())) + .transpose()?; + let raw_return = if let (Some(layout), Some(words)) = + (&self.return_aggregate, aggregate_return_words.as_mut()) + { + let bytes = unsafe { + std::slice::from_raw_parts_mut(words.as_mut_ptr().cast::(), layout.size()) + }; + unsafe { + self.cif.call_return_into( + libffi::middle::CodePtr(self.function as *mut c_void), + &ffi_args, + Ret::new(bytes), + ) + }; + None + } else { + unsafe { + crate::call::call_native_function( + &self.cif, + self.function as *mut c_void, + &ffi_args, + self.return_type.map(Type::abi_type), + ) + } + }; + let last_error = self.capture_last_error.then(|| unsafe { GetLastError().0 }); + let succeeded = success_matches(self.success_rule, raw_return.as_ref())?; + if succeeded { + for parameter in &self.parameters { + if !parameter.spec.consumes_resource { + continue; + } + let input_index = parameter + .input_index + .expect("consuming parameter is an input"); + if let Some(guard_index) = resource_guard_by_arg[input_index] { + *resource_guards[guard_index] = 0; + } + } + } + + let return_value = if let (Some(layout), Some(words)) = + (&self.return_aggregate, aggregate_return_words) + { + let source = + unsafe { std::slice::from_raw_parts(words.as_ptr().cast::(), layout.size()) }; + let mut bytes = Vec::new(); + bytes + .try_reserve_exact(source.len()) + .map_err(|_| out_of_memory("native aggregate return"))?; + bytes.extend_from_slice(source); + Some(Value::OwnedAggregate { + layout: Arc::clone(layout), + bytes, + }) + } else { + match (self.return_type, raw_return) { + (Some(typ), Some(value)) => Some(finalize_value( + typ, + value, + self.return_cleanup, + succeeded, + false, + )?), + (None, None) => None, + _ => { + return Err(invalid_argument( + "flat Win32 call plan produced an inconsistent return value", + )); + } + } + }; + + let mut outputs = Vec::with_capacity(self.output_count); + for parameter in &self.parameters { + let Some(output_index) = parameter.output_index else { + continue; + }; + let raw = std::mem::replace( + &mut output_storage[output_index], + parameter.spec.typ.default_abi_value(), + ); + outputs.push(finalize_value( + parameter.spec.typ, + raw, + parameter.spec.cleanup, + succeeded, + true, + )?); + } + + Ok(CallResult { + return_value, + outputs, + last_error, + succeeded, + }) + } +} + +fn try_zeroed_words(byte_length: usize) -> Result> { + let word_length = byte_length.div_ceil(std::mem::size_of::()); + let mut words = Vec::new(); + words + .try_reserve_exact(word_length) + .map_err(|_| out_of_memory("native aggregate storage"))?; + words.resize(word_length, 0); + Ok(words) +} + +#[cfg(not(all(windows, target_pointer_width = "32")))] +fn validate_spec(spec: &CallPlanSpec) -> Result<()> { + if !is_bare_system_module_name(&spec.dll) { + return Err(invalid_argument( + "flat Win32 DLL names must be bare .dll or .drv names loaded from System32", + )); + } + if spec.entry_point.is_empty() || spec.entry_point.as_bytes().contains(&0) { + return Err(invalid_argument( + "flat Win32 entry point must be a non-empty NUL-free export name", + )); + } + if spec.return_cleanup.owns_resource() && !spec.return_type.is_some_and(Type::is_pointer_like) { + return Err(invalid_argument( + "owned flat Win32 returns must be pointer or handle values", + )); + } + if spec.return_type.is_some() && spec.return_aggregate.is_some() { + return Err(invalid_argument( + "flat Win32 return cannot be both scalar and aggregate", + )); + } + if spec.return_aggregate.is_some() && spec.return_cleanup != Cleanup::None { + return Err(invalid_argument( + "native aggregate returns cannot use pointer cleanup", + )); + } + if spec.success_rule != SuccessRule::Always && spec.return_type.is_none() { + return Err(invalid_argument( + "flat Win32 success rules require a direct return value", + )); + } + if matches!( + spec.success_rule, + SuccessRule::HResultSucceeded | SuccessRule::SignedNonNegative + ) && spec.return_type != Some(Type::I32) + { + return Err(invalid_argument( + "signed success rules require an i32 return type", + )); + } + for (index, parameter) in spec.parameters.iter().enumerate() { + if spec + .parameter_aggregates + .get(index) + .is_some_and(Option::is_some) + && (parameter.direction != Direction::In || parameter.typ != Type::Pointer) + { + return Err(invalid_argument( + "by-value native aggregates require direct pointer-typed input plans", + )); + } + if parameter.nullable && !parameter.typ.is_pointer_like() { + return Err(invalid_argument( + "only pointer and handle inputs may be nullable", + )); + } + if parameter.cleanup.owns_resource() + && (!parameter.direction.is_output() || !parameter.typ.is_pointer_like()) + { + return Err(invalid_argument( + "flat Win32 cleanup applies only to pointer or handle outputs", + )); + } + if parameter.consumes_resource + && (parameter.direction != Direction::In + || parameter.typ != Type::Handle + || !parameter.resource_cleanup.owns_resource()) + { + return Err(invalid_argument( + "consuming Win32 parameters must be direct managed handle inputs with exact cleanup", + )); + } + if parameter.resource_cleanup != Cleanup::None + && (parameter.direction == Direction::Out || parameter.typ != Type::Handle) + { + return Err(invalid_argument( + "managed resource compatibility applies only to handle inputs", + )); + } + } + Ok(()) +} + +fn value_to_abi( + typ: Type, + value: &Value, + nullable: bool, + resource_cleanup: Cleanup, + resource_bits: Option, +) -> Result { + let mismatch = || invalid_argument(&format!("flat Win32 argument does not match {typ:?}")); + Ok(match (typ, value) { + (Type::Bool32, Value::Bool(value)) => AbiValue::I32(i32::from(*value)), + (Type::I8, Value::I8(value)) => AbiValue::I8(*value), + (Type::U8, Value::U8(value)) => AbiValue::U8(*value), + (Type::I16, Value::I16(value)) => AbiValue::I16(*value), + (Type::U16, Value::U16(value)) => AbiValue::U16(*value), + (Type::I32, Value::I32(value)) => AbiValue::I32(*value), + (Type::U32, Value::U32(value)) => AbiValue::U32(*value), + (Type::I64, Value::I64(value)) => AbiValue::I64(*value), + (Type::U64, Value::U64(value)) => AbiValue::U64(*value), + (Type::F32, Value::F32(value)) => AbiValue::F32(*value), + (Type::F64, Value::F64(value)) => AbiValue::F64(*value), + (Type::Pointer, Value::Pointer(value)) => AbiValue::Pointer(*value), + (Type::FunctionPointer, Value::FunctionPointer(value)) => { + AbiValue::Pointer(*value as *mut c_void) + } + (Type::Handle, Value::Handle(value)) => AbiValue::Pointer(*value as *mut c_void), + (Type::Handle, Value::Resource(resource)) => { + if resource_cleanup == Cleanup::None || resource.cleanup() != resource_cleanup { + return Err(invalid_argument( + "managed Win32 resource cleanup kind does not match the handle parameter", + )); + } + let raw = resource_bits.ok_or_else(|| { + invalid_argument("managed Win32 resource has no active call lease") + })?; + if raw == 0 { + return Err(invalid_argument( + "cannot pass a closed flat Win32 resource handle", + )); + } + AbiValue::Pointer(raw as *mut c_void) + } + (Type::Pointer | Type::FunctionPointer | Type::Handle, Value::Null) if nullable => { + AbiValue::Pointer(std::ptr::null_mut()) + } + _ => return Err(mismatch()), + }) +} + +fn abi_arg(value: &AbiValue) -> Arg<'_> { + match value { + AbiValue::Bool(value) => arg(value), + AbiValue::I8(value) => arg(value), + AbiValue::U8(value) => arg(value), + AbiValue::I16(value) => arg(value), + AbiValue::U16(value) => arg(value), + AbiValue::I32(value) => arg(value), + AbiValue::U32(value) => arg(value), + AbiValue::I64(value) => arg(value), + AbiValue::U64(value) => arg(value), + AbiValue::F32(value) => arg(value), + AbiValue::F64(value) => arg(value), + AbiValue::Guid(value) => arg(value), + AbiValue::Pointer(value) => arg(value), + } +} + +fn finalize_value( + typ: Type, + value: AbiValue, + cleanup: Cleanup, + succeeded: bool, + cleanup_on_failure: bool, +) -> Result { + if cleanup.owns_resource() { + let bits = match value { + AbiValue::Pointer(value) => value as usize, + _ => { + return Err(invalid_argument( + "owned flat Win32 output was not pointer-shaped", + )); + } + }; + if !succeeded { + if cleanup_on_failure { + unsafe { cleanup.run(bits) }.map_err(Error::WindowsError)?; + } + return Ok(Value::Handle(0)); + } + return if bits == 0 { + Ok(Value::Handle(0)) + } else { + Ok(Value::Resource(Arc::new(OwnedResource::new(bits, cleanup)))) + }; + } + + Ok(match (typ, value) { + (Type::Bool32, AbiValue::I32(value)) => Value::Bool(value != 0), + (Type::I8, AbiValue::I8(value)) => Value::I8(value), + (Type::U8, AbiValue::U8(value)) => Value::U8(value), + (Type::I16, AbiValue::I16(value)) => Value::I16(value), + (Type::U16, AbiValue::U16(value)) => Value::U16(value), + (Type::I32, AbiValue::I32(value)) => Value::I32(value), + (Type::U32, AbiValue::U32(value)) => Value::U32(value), + (Type::I64, AbiValue::I64(value)) => Value::I64(value), + (Type::U64, AbiValue::U64(value)) => Value::U64(value), + (Type::F32, AbiValue::F32(value)) => Value::F32(value), + (Type::F64, AbiValue::F64(value)) => Value::F64(value), + (Type::Pointer, AbiValue::Pointer(value)) => Value::Pointer(value), + (Type::FunctionPointer, AbiValue::Pointer(value)) => Value::FunctionPointer(value as usize), + (Type::Handle, AbiValue::Pointer(value)) => Value::Handle(value as usize), + _ => { + return Err(invalid_argument( + "flat Win32 native result did not match its immutable call plan", + )); + } + }) +} + +fn success_matches(rule: SuccessRule, value: Option<&AbiValue>) -> Result { + let scalar = |value: &AbiValue| -> Option { + match value { + AbiValue::Bool(value) => Some(*value as i128), + AbiValue::I8(value) => Some(*value as i128), + AbiValue::U8(value) => Some(*value as i128), + AbiValue::I16(value) => Some(*value as i128), + AbiValue::U16(value) => Some(*value as i128), + AbiValue::I32(value) => Some(*value as i128), + AbiValue::U32(value) => Some(*value as i128), + AbiValue::I64(value) => Some(*value as i128), + AbiValue::U64(value) => Some(*value as i128), + AbiValue::Pointer(value) => Some(*value as usize as i128), + AbiValue::F32(_) | AbiValue::F64(_) | AbiValue::Guid(_) => None, + } + }; + match rule { + SuccessRule::Always => Ok(true), + SuccessRule::ReturnZero => value + .and_then(scalar) + .map(|value| value == 0) + .ok_or_else(|| invalid_argument("ReturnZero requires an integer or pointer return")), + SuccessRule::ReturnNonZero | SuccessRule::ReturnNonNull => value + .and_then(scalar) + .map(|value| value != 0) + .ok_or_else(|| invalid_argument("nonzero success rule requires a scalar return")), + SuccessRule::HResultSucceeded => match value { + Some(AbiValue::I32(value)) => Ok(*value >= 0), + _ => Err(invalid_argument( + "HRESULT success rule requires a signed 32-bit return", + )), + }, + SuccessRule::SignedNonNegative => match value { + Some(AbiValue::I32(value)) => Ok(*value >= 0), + _ => Err(invalid_argument( + "signed-nonnegative success rule requires a signed 32-bit return", + )), + }, + SuccessRule::ReturnValidHandle => match value { + Some(AbiValue::Pointer(value)) => { + let bits = *value as usize; + Ok(bits != 0 && bits != usize::MAX) + } + _ => Err(invalid_argument( + "valid-handle success rule requires a pointer return", + )), + }, + } +} + +#[cfg(not(all(windows, target_pointer_width = "32")))] +struct CachedModule(HMODULE); + +#[cfg(not(all(windows, target_pointer_width = "32")))] +unsafe impl Send for CachedModule {} + +#[cfg(not(all(windows, target_pointer_width = "32")))] +fn module_cache() -> &'static Mutex> { + static CACHE: OnceLock>> = OnceLock::new(); + CACHE.get_or_init(|| Mutex::new(HashMap::new())) +} + +#[cfg(not(all(windows, target_pointer_width = "32")))] +fn get_cached_module(dll: &str) -> Result { + if !is_bare_system_module_name(dll) { + return Err(invalid_argument("invalid System32 module name")); + } + let key = dll.to_ascii_lowercase(); + if let Some(module) = module_cache() + .lock() + .unwrap_or_else(|error| error.into_inner()) + .get(&key) + .map(|module| module.0) + { + return Ok(module); + } + let module = unsafe { LoadLibraryExW(&HSTRING::from(dll), None, LOAD_LIBRARY_SEARCH_SYSTEM32) } + .map_err(Error::WindowsError)?; + let mut cache = module_cache() + .lock() + .unwrap_or_else(|error| error.into_inner()); + Ok(cache.entry(key).or_insert(CachedModule(module)).0) +} + +#[cfg(not(all(windows, target_pointer_width = "32")))] +fn proc_address(module: HMODULE, dll: &str, entry: &str) -> Result<*mut c_void> { + let entry = CString::new(entry).map_err(|_| invalid_argument("invalid export name"))?; + let function = unsafe { GetProcAddress(module, PCSTR(entry.as_ptr().cast())) }; + function + .map(|function| function as *const () as *mut c_void) + .ok_or_else(|| { + Error::WindowsError(windows_core::Error::new( + HRESULT(0x8007007Fu32 as i32), + &format!( + "Export `{}` was not found in `{dll}`", + entry.to_string_lossy() + ), + )) + }) +} + +#[cfg(not(all(windows, target_pointer_width = "32")))] +fn is_bare_system_module_name(dll: &str) -> bool { + let lower = dll.to_ascii_lowercase(); + !dll.is_empty() + && (lower.ends_with(".dll") || lower.ends_with(".drv")) + && !dll.encode_utf16().any(|unit| unit == 0) + && !dll + .chars() + .any(|character| matches!(character, '/' | '\\' | ':')) + && !matches!(dll, "." | "..") +} + +fn invalid_argument(message: &str) -> Error { + Error::WindowsError(windows_core::Error::new(E_INVALIDARG, message)) +} + +fn out_of_memory(context: &str) -> Error { + Error::WindowsError(windows_core::Error::new( + HRESULT(0x8007000Eu32 as i32), + format!("Unable to allocate {context}"), + )) +} + +#[cfg(all(windows, target_pointer_width = "32"))] +fn not_implemented(message: &str) -> Error { + Error::WindowsError(windows_core::Error::new(E_NOTIMPL, message)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(not(all(windows, target_pointer_width = "32")))] + fn wide(value: &str) -> Vec { + value.encode_utf16().chain(std::iter::once(0)).collect() + } + + #[cfg(not(all(windows, target_pointer_width = "32")))] + fn mul_div_plan() -> Arc { + unsafe { + CallPlan::new(CallPlanSpec { + dll: "kernel32.dll".into(), + entry_point: "MulDiv".into(), + parameters: vec![ + Parameter::input(Type::I32, false), + Parameter::input(Type::I32, false), + Parameter::input(Type::I32, false), + ], + return_type: Some(Type::I32), + return_cleanup: Cleanup::None, + success_rule: SuccessRule::Always, + capture_last_error: false, + calling_convention: CallingConvention::System, + parameter_aggregates: vec![None; 3], + return_aggregate: None, + }) + } + .unwrap() + } + + #[test] + fn immutable_plan_invokes_exact_scalar_signature() { + #[cfg(target_pointer_width = "64")] + { + let plan = mul_div_plan(); + let result = + unsafe { plan.invoke(&[Value::I32(100), Value::I32(3), Value::I32(2)]) }.unwrap(); + assert!(matches!(result.return_value, Some(Value::I32(150)))); + assert!(result.outputs.is_empty()); + } + } + + #[test] + fn immutable_plan_rejects_wrong_argument_kind_before_dispatch() { + #[cfg(target_pointer_width = "64")] + { + let plan = mul_div_plan(); + let error = unsafe { plan.invoke(&[Value::U32(100), Value::I32(3), Value::I32(2)]) } + .unwrap_err(); + assert!(error.message().contains("does not match I32")); + } + } + + #[test] + fn cdecl_plan_invokes_ldap_scalar_export() { + #[cfg(target_pointer_width = "64")] + { + let plan = unsafe { + CallPlan::new(CallPlanSpec { + dll: "wldap32.dll".into(), + entry_point: "LdapGetLastError".into(), + parameters: vec![], + return_type: Some(Type::U32), + return_cleanup: Cleanup::None, + success_rule: SuccessRule::Always, + capture_last_error: false, + calling_convention: CallingConvention::Cdecl, + parameter_aggregates: vec![], + return_aggregate: None, + }) + } + .unwrap(); + assert_eq!(plan.calling_convention(), CallingConvention::Cdecl); + let result = unsafe { plan.invoke(&[]) }.unwrap(); + assert!(matches!(result.return_value, Some(Value::U32(_)))); + } + } + + #[cfg(not(all(windows, target_pointer_width = "32")))] + #[test] + fn system32_policy_rejects_paths() { + assert!(!is_bare_system_module_name( + r"C:\Windows\System32\kernel32.dll" + )); + assert!(is_bare_system_module_name("kernel32.dll")); + } + + #[cfg(all(windows, target_pointer_width = "32"))] + #[test] + fn x86_plan_binding_fails_before_loading_or_dispatch() { + let error = unsafe { + CallPlan::new(CallPlanSpec { + dll: "kernel32.dll".into(), + entry_point: "MulDiv".into(), + parameters: vec![ + Parameter::input(Type::I32, false), + Parameter::input(Type::I32, false), + Parameter::input(Type::I32, false), + ], + return_type: Some(Type::I32), + return_cleanup: Cleanup::None, + success_rule: SuccessRule::Always, + capture_last_error: false, + calling_convention: CallingConvention::System, + parameter_aggregates: vec![None; 3], + return_aggregate: None, + }) + } + .unwrap_err(); + assert!(error.message().contains("reject 32-bit targets")); + } + + #[test] + fn call_plan_is_publishable_across_threads() { + fn assert_send_sync() {} + assert_send_sync::(); + } + + #[test] + fn hresult_success_rule_accepts_s_false_and_rejects_failures() { + assert!(success_matches(SuccessRule::HResultSucceeded, Some(&AbiValue::I32(1))).unwrap()); + assert!(!success_matches(SuccessRule::HResultSucceeded, Some(&AbiValue::I32(-1))).unwrap()); + } + + #[test] + fn failed_direct_handle_sentinel_is_never_cleaned() { + let value = finalize_value( + Type::Handle, + AbiValue::Pointer(usize::MAX as *mut c_void), + Cleanup::CloseHandle, + false, + false, + ) + .unwrap(); + assert!(matches!(value, Value::Handle(0))); + } + + #[test] + fn registry_handle_output_is_adopted_only_on_success() { + #[cfg(target_pointer_width = "64")] + { + let plan = unsafe { + CallPlan::new(CallPlanSpec { + dll: "advapi32.dll".into(), + entry_point: "RegOpenKeyExW".into(), + parameters: vec![ + Parameter::input(Type::Handle, false), + Parameter::input(Type::Pointer, false), + Parameter::input(Type::U32, false), + Parameter::input(Type::U32, false), + Parameter::output(Type::Handle, Cleanup::RegCloseKey), + ], + return_type: Some(Type::I32), + return_cleanup: Cleanup::None, + success_rule: SuccessRule::ReturnZero, + capture_last_error: false, + calling_convention: CallingConvention::System, + parameter_aggregates: vec![None; 5], + return_aggregate: None, + }) + } + .unwrap(); + let existing = wide(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion"); + let opened = unsafe { + plan.invoke(&[ + Value::Handle(0x80000002), + Value::Pointer(existing.as_ptr() as *mut c_void), + Value::U32(0), + Value::U32(0x20019), + ]) + } + .unwrap(); + assert!(opened.succeeded); + let resource = opened.outputs[0].resource().unwrap(); + assert_ne!(resource.raw(), 0); + let wrong_cleanup = unsafe { + CallPlan::new(CallPlanSpec { + dll: "kernel32.dll".into(), + entry_point: "CloseHandle".into(), + parameters: vec![Parameter { + typ: Type::Handle, + direction: Direction::In, + nullable: false, + cleanup: Cleanup::None, + consumes_resource: true, + resource_cleanup: Cleanup::CloseHandle, + }], + return_type: Some(Type::Bool32), + return_cleanup: Cleanup::None, + success_rule: SuccessRule::ReturnNonZero, + capture_last_error: true, + calling_convention: CallingConvention::System, + parameter_aggregates: vec![None], + return_aggregate: None, + }) + } + .unwrap(); + let error = unsafe { wrong_cleanup.invoke(&[Value::Resource(Arc::clone(resource))]) } + .unwrap_err(); + assert!(error.message().contains("cleanup kind does not match")); + assert!(!resource.is_closed()); + let close = unsafe { + CallPlan::new(CallPlanSpec { + dll: "advapi32.dll".into(), + entry_point: "RegCloseKey".into(), + parameters: vec![Parameter { + typ: Type::Handle, + direction: Direction::In, + nullable: false, + cleanup: Cleanup::None, + consumes_resource: true, + resource_cleanup: Cleanup::RegCloseKey, + }], + return_type: Some(Type::I32), + return_cleanup: Cleanup::None, + success_rule: SuccessRule::ReturnZero, + capture_last_error: false, + calling_convention: CallingConvention::System, + parameter_aggregates: vec![None], + return_aggregate: None, + }) + } + .unwrap(); + let raw_error = unsafe { close.invoke(&[Value::Handle(resource.raw())]) }.unwrap_err(); + assert!( + raw_error + .message() + .contains("require a managed resource object") + ); + assert!(!resource.is_closed()); + let closed = unsafe { close.invoke(&[Value::Resource(Arc::clone(resource))]) }.unwrap(); + assert!(closed.succeeded); + assert!(resource.is_closed()); + resource.close().unwrap(); + + let missing = wide(r"SOFTWARE\DynWinRT\DefinitelyMissing"); + let failed = unsafe { + plan.invoke(&[ + Value::Handle(0x80000002), + Value::Pointer(missing.as_ptr() as *mut c_void), + Value::U32(0), + Value::U32(0x20019), + ]) + } + .unwrap(); + assert!(!failed.succeeded); + assert!(matches!(failed.outputs[0], Value::Handle(0))); + } + } + + #[test] + fn native_aggregate_layout_rejects_excessive_sizes() { + let error = NativeAggregateLayout::new( + "Tests.Huge", + MAX_NATIVE_AGGREGATE_SIZE + 8, + 8, + FfiType::structure(vec![FfiType::u64()]), + ) + .unwrap_err(); + assert!(error.message().contains("invalid native aggregate")); + } + + #[test] + fn global_alloc_resource_uses_global_free_once() { + use windows::Win32::System::Memory::{GMEM_FIXED, GlobalAlloc}; + + let allocation = unsafe { GlobalAlloc(GMEM_FIXED, 32) }.unwrap(); + let resource = OwnedResource::new(allocation.0 as usize, Cleanup::GlobalFree); + assert!(!resource.is_closed()); + resource.close().unwrap(); + assert!(resource.is_closed()); + resource.close().unwrap(); + } + + #[test] + fn consuming_call_lock_rejects_an_async_lease() { + use windows::Win32::System::Memory::{GMEM_FIXED, GlobalAlloc}; + + let allocation = unsafe { GlobalAlloc(GMEM_FIXED, 32) }.unwrap(); + let resource = + unsafe { OwnedResource::adopt(allocation.0 as usize, Cleanup::GlobalFree) }.unwrap(); + let lease = resource.async_lease(Cleanup::GlobalFree).unwrap(); + let error = resource.lock_for_call(true).unwrap_err(); + assert!(error.message().contains("asynchronous I/O is pending")); + drop(lease); + + let guard = resource.lock_for_call(true).unwrap(); + assert_eq!(*guard, allocation.0 as usize); + drop(guard); + resource.close().unwrap(); + } + + #[test] + fn last_error_is_captured_with_the_native_result() { + #[cfg(target_pointer_width = "64")] + { + let plan = unsafe { + CallPlan::new(CallPlanSpec { + dll: "kernel32.dll".into(), + entry_point: "GetModuleHandleW".into(), + parameters: vec![Parameter::input(Type::Pointer, false)], + return_type: Some(Type::Handle), + return_cleanup: Cleanup::None, + success_rule: SuccessRule::Always, + capture_last_error: true, + calling_convention: CallingConvention::System, + parameter_aggregates: vec![None], + return_aggregate: None, + }) + } + .unwrap(); + let missing = wide("dynwinrt-module-that-is-not-loaded.dll"); + let result = + unsafe { plan.invoke(&[Value::Pointer(missing.as_ptr() as *mut c_void)]) }.unwrap(); + assert!(matches!(result.return_value, Some(Value::Handle(0)))); + assert_eq!(result.last_error, Some(126)); + } + } +} diff --git a/docs/architecture/flat-win32-support.md b/docs/architecture/flat-win32-support.md new file mode 100644 index 00000000..a71958a8 --- /dev/null +++ b/docs/architecture/flat-win32-support.md @@ -0,0 +1,200 @@ +# Flat Win32 support + +Flat Win32 APIs are DLL exports described by `[DllImport]` methods in +`Windows.Win32.winmd`. They are a third frontend, separate from WinRT +activation and Classic COM vtables. + +```text +Windows.Win32.winmd facts + -> flat-local semantic ABI contracts + -> validated projected function IR + -> immutable Win32CallPlan + -> shared behavior-neutral libffi executor + -> System32 export +``` + +The npm surfaces remain isolated: + +- `@microsoft/dynwinrt` is WinRT-only; +- `@microsoft/dynwinrt/com` is managed Classic COM support; +- `@microsoft/dynwinrt/com/unsafe` is raw Classic COM ABI access; +- `@microsoft/dynwinrt/win32` is the safe flat Win32 runtime; and +- `@microsoft/dynwinrt/win32/unsafe` permits numeric native addresses. + +## Runtime architecture + +`Win32CallPlan` fixes the DLL, export, ordered parameter ABI, parameter +direction, nullability, return ABI, success rule, LastError behavior, and +resource cleanup before the first invocation. Its libffi CIF is prepared once +and then treated as immutable. + +The executor never derives an ABI type from a JavaScript value. Invocation +first validates every value against the plan, creates stable output slots, calls +the resolved export, captures LastError immediately when requested, and adopts +owned resources only when the function's success rule succeeds. + +Modules are loaded only from System32 with `LOAD_LIBRARY_SEARCH_SYSTEM32` and +remain loaded for the process lifetime. Bare `.dll` and `.drv` names are +accepted; paths are rejected. + +The runtime supports x64 and ARM64 with explicit `system` and `cdecl` plans. +A 32-bit build compiles, but plan binding fails explicitly until generation +also carries target-specific availability and all x86 convention variants. + +`ReadFile` and `WriteFile` use a separate OVERLAPPED Promise path rather than +the synchronous call plan. It owns an event and private native buffer, leases a +managed file handle for the operation, handles immediate and +`ERROR_IO_PENDING` completion, supports `AbortSignal`/`CancelIoEx`, and copies +read results back on the JavaScript thread. Completion waits run on a shared, +fixed eight-thread native waiter rather than the libuv worker pool; excess +operations are rejected explicitly instead of allocating unbounded OS threads. +Before copying a read result, the JS thread reacquires and revalidates the Node +Buffer backing store so a transferred/detached ArrayBuffer cannot leave a stale +destination pointer. EOF resolves with zero bytes. + +## Metadata and codegen layers + +```text +win32_metadata.rs + -> codegen/win32/model.rs + -> codegen/win32/project.rs + -> codegen/win32/render.rs +``` + +Raw metadata retains: + +- native name and scalar/enum/handle/pointer category; +- explicit pointer depth and constness; +- `In`, `Out`, and `InOut`; +- nullability; +- reserved-zero and single-/double-NUL termination attributes; +- element-count and byte-count relationships; +- sequential/union layout, packing, fixed arrays, forced alignment, and nested fields; +- architecture and calling convention; +- status-return and LastError metadata; and +- exact handle cleanup metadata. + +The semantic model is closed. Unknown layouts, pointer ownership, callback +thunks, writable buffers without size relationships, unsupported cleanup, and +pointer returns without lifetime evidence are omitted with a diagnostic. +Renderers consume projected IR only and have no pointer or Buffer fallback. + +Validated native aggregate pointers use aligned, branded call storage. Plain +structs without nested unions may also pass and return by value through libffi. +Typed scalar/GUID pointers and fixed-layout POD buffers use call-local or +caller-owned storage with exact size and alignment checks. COM interface inputs +require an exact IID QueryInterface and remain borrowed for the synchronous +call. +Byte-counted buffers with exactly one data indirection may remain opaque even +when their element is a variable native record. This supports safe two-call +size queries such as `GetAdaptersAddresses` without pretending JavaScript can +interpret the record's internal pointers. Exact adapters supply missing +character-buffer relationships for `LCMapStringA`, `FoldString*`, +`GetLocaleInfo*`, and `QueryFullProcessImageName*`. The wide `LCMapString` +forms remain closed because sort-key flags change the count unit from UTF-16 +characters to bytes. + +The pinned metadata reader loses parent row identity for some anonymous nested +types. Those layouts are never matched globally by simple name. Exact adapters +currently restore the `SYSTEM_INFO` and `INPUT` anonymous unions; the former +still fails closed because its outer structure contains pointers, while the +latter enables safe caller-owned `SendInput` buffers. +Pointer-bearing aggregates remain closed by default. Exact support currently +includes: + +- `SECURITY_ATTRIBUTES`: an input-only builder retains its optional security + descriptor Buffer for the synchronous call; +- `STARTUPINFOA/W`: zero-initialized builders set the required `cb` field and + keep optional pointer/handle fields null; and +- `PROCESS_INFORMATION`: successful calls expose PID/TID getters and + success-gated `CloseHandle` resource adoption for process/thread handles. + +Pointer owners are tracked per field, replaced atomically, and revalidated +under the aggregate call lock. Raw bytes are unavailable for pointer-bearing +storage. Unclaimed owned output fields are closed on aggregate drop or before +reuse. Other pointer-bearing structs remain unsupported until every pointee +has an exact retained/borrowed/owned projection. Non-default packed or +forced-aligned aggregates are not passed by value. + +## Safe JavaScript projection + +Confirmed handle values accept `bigint`, safe-integer `number`, or a managed +`DynWin32Resource`. Dereferenced data addresses accept retained +`Buffer`/`Uint8Array` storage only. UTF-16 strings accept JavaScript strings or +validated terminated storage; ANSI strings require ASCII or caller-encoded +terminated bytes. Consuming handle APIs accept only a managed resource with the +exact cleanup kind; raw handle values cannot bypass close-state or asynchronous +lease checks. Lease state is checked while holding the same resource mutex used +for lease creation and consuming calls. Double-NUL string-list inputs accept +string arrays or validated encoded storage. + +Native aggregate descriptors and layouts have explicit size limits and use +fallible allocation paths. Reserved inputs are hidden and supplied as exact +zero/null ABI values. + +Scalar returns are direct: + +```js +const ticks = getTickCount64(); // bigint +``` + +Win32 status APIs retain status objects because codes such as +`ERROR_MORE_DATA` are normal control flow: + +```js +const opened = regOpenKeyEx(HKEY_LOCAL_MACHINE, path, 0, KEY_READ); +if (opened.status !== 0) { + // handle status +} +opened.key?.close(); +``` + +Unicode `W` exports also receive an unsuffixed alias when it cannot collide +with another generated name. ANSI `A` exports remain explicit. + +## Ownership + +Owned outputs are represented by `DynWin32Resource`. Explicit `close()` is +idempotent, and dropping the wrapper invokes the exact cleanup: + +- `HKEY` -> `RegCloseKey`; +- `HANDLE` -> `CloseHandle`; +- `HLOCAL` -> `LocalFree`; +- `HGLOBAL` -> `GlobalFree`; +- owned `HMODULE` -> `FreeLibrary`; +- `SC_HANDLE` -> `CloseServiceHandle`; +- task allocator pointers -> `CoTaskMemFree`; and +- credential allocations -> `CredFree`. + +A typedef's cleanup attribute does not by itself prove that a direct function +return transfers ownership. Direct handle returns remain borrowed unless a +per-function ownership and success-sentinel contract is registered. Current +exact direct-return evidence includes common kernel handles, `LocalAlloc`, +`GlobalAlloc`, `LoadLibrary*`, and service-control-manager handles. + +## Reproducibility + +Generated files are tracked by +`win32/.dynwinrt-win32-manifest.json`. Regenerating one metadata root removes +only stale files previously owned by that root. + +The machine-readable census is: + +```powershell +dynwinrt-codegen win32-census ` + --winmd C:\path\to\Windows.Win32.winmd ` + --json +``` + +For `Microsoft.Windows.SDK.Win32Metadata 71.0.14-preview`, the baseline is +8,959 complete safe functions out of 18,321 DllImport rows +(48.900169204737736%). Omission reasons are grouped into stable categories. + +`windows-metadata` remains behind the flat-local adapter. Parameter rows are +associated by ECMA-335 `Param.Sequence`, and calling convention remains a raw +fact through semantic projection and immutable plan construction. +The pinned metadata reader's lossy convention helper is not used: the adapter +decodes the ECMA `ImplMap` convention mask exactly and rejects stdcall, +thiscall, and fastcall until their target-specific runtime plans exist. +`windows`-generated declarations are used only as differential ABI oracles; +they do not replace the runtime semantic model. diff --git a/docs/guides/windows/flat-win32-usage.md b/docs/guides/windows/flat-win32-usage.md new file mode 100644 index 00000000..e0687bf7 --- /dev/null +++ b/docs/guides/windows/flat-win32-usage.md @@ -0,0 +1,139 @@ +# Using generated flat Win32 bindings + +Generate a flat `Apis` container from the restored Win32 metadata package: + +```powershell +dynwinrt-codegen generate ` + --winmd C:\path\to\Windows.Win32.winmd ` + --namespace Windows.Win32.System.Registry ` + --output .\generated +``` + +The namespace is emitted under its own domain: + +```text +generated/ + package.json + win32/ + Windows.Win32.System.Registry/ + Apis.js + Apis.d.ts + index.js + index.d.ts +``` + +Import the generated namespace subpath: + +```js +import { + regOpenKeyEx, + regQueryValueEx, +} from "@winapp/bindings/win32/Windows.Win32.System.Registry"; + +const HKEY_LOCAL_MACHINE = 0x80000002n; +const KEY_READ = 0x20019; + +const opened = regOpenKeyEx( + HKEY_LOCAL_MACHINE, + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", + 0, + KEY_READ, +); + +if (opened.status !== 0 || !opened.key) { + throw new Error(`RegOpenKeyEx failed: ${opened.status}`); +} + +try { + const probe = regQueryValueEx(opened.key, "ProductName", null); + const data = Buffer.alloc(probe.dataSize); + const result = regQueryValueEx(opened.key, "ProductName", data); + if (result.status !== 0) { + throw new Error(`RegQueryValueEx failed: ${result.status}`); + } +} finally { + opened.key.close(); +} +``` + +Generated safe wrappers do not accept numeric data addresses. Use +`Buffer`/`Uint8Array` for native storage and `bigint` only for confirmed handle +values. Manual ABI declarations and arbitrary native addresses require the +explicit `@microsoft/dynwinrt/win32/unsafe` entrypoint. + +APIs that consume and close a handle require `DynWin32Resource`; passing +`resource.value` or another numeric handle is rejected. Double-NUL string-list +parameters accept `string[]` or explicitly encoded, double-terminated storage. + +Validated native structs receive generated factories and branded storage: + +```js +const systemTime = createSYSTEMTIME(); +getSystemTime(systemTime); +const year = systemTime.bytes.readUInt16LE(0); +``` + +By-value structs use the same branded value. Typed buffers enforce metadata +size/count relationships and native alignment before dispatch. + +Byte-counted APIs expose a nullable caller buffer and return the updated size, +so standard Win32 two-call queries remain explicit: + +```js +let query = getAdaptersAddresses(0, 0, null); +const data = Buffer.alloc(query.sizePointer); +query = getAdaptersAddresses(0, 0, data); +if (query.result !== 0) { + throw new Error(`GetAdaptersAddresses failed: ${query.result}`); +} +``` + +The buffer remains opaque when its native records contain internal pointers. +This preserves memory safety without inventing a JavaScript object model for +unvalidated pointees. + +Exact pointer-bearing structs use generated builders rather than raw bytes: + +```js +const attributes = createSecurityAttributes({ + securityDescriptor: null, + inheritHandle: false, +}); +const pipe = createPipe(attributes, 0); +pipe.hReadPipe?.close(); +pipe.hWritePipe?.close(); +``` + +`PROCESS_INFORMATION` outputs are success-gated and adopted explicitly: + +```js +const startup = createStartupInfoW(); +const processInfo = createProcessInformation(); +const created = createProcessW( + null, commandLine, null, null, false, 0, null, startup, processInfo, +); +if (created.result) { + const process = takeProcessInformationProcess(processInfo); + const thread = takeProcessInformationThread(processInfo); + process?.close(); + thread?.close(); +} +``` + +Generated `ReadFile`/`WriteFile` projections use dedicated OVERLAPPED Promises: + +```js +const written = await writeFileAsync(file, data, 0n, abortController.signal); +const read = await readFileAsync(file, destination, 0n); +``` + +The runtime holds the file resource and private native storage until completion +or cancellation, waits on a fixed-capacity native waiter outside the libuv +worker pool, and revalidates read Buffers before copying data back. More than +eight concurrent operations are rejected explicitly. The file must have been +opened with `FILE_FLAG_OVERLAPPED`. + +Runnable examples are available under +[`samples/js/win32`](../../../samples/js/win32/README.md). They cover direct +64-bit returns, branded native structs, caller-owned Registry buffers, and +deterministic handle cleanup without using the unsafe entrypoint. diff --git a/samples/js/win32/.gitignore b/samples/js/win32/.gitignore new file mode 100644 index 00000000..1d4f48dc --- /dev/null +++ b/samples/js/win32/.gitignore @@ -0,0 +1,3 @@ +generated/ +node_modules/ +package-lock.json diff --git a/samples/js/win32/README.md b/samples/js/win32/README.md new file mode 100644 index 00000000..4ce3838d --- /dev/null +++ b/samples/js/win32/README.md @@ -0,0 +1,63 @@ +# Flat Win32 JavaScript samples + +These samples generate and call safe flat Win32 bindings: + +- `system-info.mjs` reads the 64-bit Windows uptime and fills a branded + `SYSTEMTIME` native struct. +- `registry-product-name.mjs` demonstrates a two-phase caller-owned buffer and + deterministic `HKEY` cleanup. +- `overlapped-file.mjs` opens a file with `FILE_FLAG_OVERLAPPED`, writes and + reads it with generated Promises, and passes an `AbortSignal`. + +Neither sample requires administrator privileges, network access at runtime, +or the unsafe Win32 entrypoint. + +## Prerequisites + +- Windows 10 or later with Node.js 18+, Rust, and NuGet available. +- `Microsoft.Windows.SDK.Win32Metadata` containing `Windows.Win32.winmd`. + +From the repository root, build the runtime and code generator: + +```powershell +Push-Location bindings\js +npm install +npm run build +Pop-Location + +cargo build -p dynwinrt-codegen --release +``` + +Restore the same metadata package used by CI if it is not already installed: + +```powershell +$metadataRoot = Join-Path $env:TEMP "dynwinrt-win32metadata" +nuget install Microsoft.Windows.SDK.Win32Metadata ` + -Version 71.0.14-preview ` + -OutputDirectory $metadataRoot ` + -DirectDownload ` + -NonInteractive +$winmd = Get-ChildItem $metadataRoot -Filter Windows.Win32.winmd -File -Recurse | + Select-Object -First 1 +``` + +Generate the bindings and install the locally built runtime: + +```powershell +Push-Location samples\js\win32 +npm install +.\generate.ps1 ` + -Win32Winmd $winmd.FullName ` + -Codegen ..\..\..\target\release\dynwinrt-codegen.exe +``` + +Run any sample: + +```powershell +npm run system-info +npm run registry +npm run overlapped-file +Pop-Location +``` + +Generated files and `node_modules` are local artifacts and are not tracked. diff --git a/samples/js/win32/generate.ps1 b/samples/js/win32/generate.ps1 new file mode 100644 index 00000000..19636c4f --- /dev/null +++ b/samples/js/win32/generate.ps1 @@ -0,0 +1,52 @@ +#!/usr/bin/env pwsh + +param( + [Parameter(Mandatory)] + [string]$Win32Winmd, + + [string]$Codegen = "dynwinrt-codegen" +) + +$ErrorActionPreference = "Stop" +$caller = (Get-Location).Path + +function Resolve-CallerPath([string]$Path) { + if ([System.IO.Path]::IsPathRooted($Path)) { + return [System.IO.Path]::GetFullPath($Path) + } + return [System.IO.Path]::GetFullPath((Join-Path $caller $Path)) +} + +$Win32Winmd = Resolve-CallerPath $Win32Winmd +if (-not (Test-Path -LiteralPath $Win32Winmd -PathType Leaf)) { + throw "Windows.Win32.winmd was not found: $Win32Winmd" +} + +$codegenCandidate = Resolve-CallerPath $Codegen +if (Test-Path -LiteralPath $codegenCandidate -PathType Leaf) { + $codegenCommand = $codegenCandidate +} else { + $codegenCommand = (Get-Command $Codegen -ErrorAction Stop).Source +} + +$output = Join-Path $PSScriptRoot "generated" +if (Test-Path -LiteralPath $output) { + Remove-Item -LiteralPath $output -Recurse -Force +} + +foreach ($namespace in @( + "Windows.Win32.System.SystemInformation", + "Windows.Win32.System.Registry", + "Windows.Win32.Storage.FileSystem" +)) { + & $codegenCommand generate ` + --winmd $Win32Winmd ` + --namespace $namespace ` + --class-name Apis ` + --output $output + if ($LASTEXITCODE -ne 0) { + throw "dynwinrt-codegen failed for $namespace with exit code $LASTEXITCODE" + } +} + +Write-Host "Generated flat Win32 bindings in $output" diff --git a/samples/js/win32/overlapped-file.mjs b/samples/js/win32/overlapped-file.mjs new file mode 100644 index 00000000..522838d0 --- /dev/null +++ b/samples/js/win32/overlapped-file.mjs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "node:assert/strict"; +import { rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +import { + createFileW, + readFileAsync, + writeFileAsync, +} from "./generated/win32/Windows.Win32.Storage.FileSystem/index.mjs"; + +const GENERIC_READ_WRITE = 0xc0000000; +const CREATE_ALWAYS = 2; +const FILE_ATTRIBUTE_NORMAL = 0x80; +const FILE_FLAG_OVERLAPPED = 0x40000000; + +const path = join(tmpdir(), `dynwinrt-overlapped-${process.pid}-${Date.now()}.tmp`); +const opened = createFileW( + path, + GENERIC_READ_WRITE, + 0, + null, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, + null, +); +if (!opened.result) { + throw new Error(`CreateFileW failed with error ${opened.lastError}`); +} + +const file = opened.result; +try { + const controller = new AbortController(); + const expected = Buffer.from("Hello from OVERLAPPED Win32 I/O"); + + const written = await writeFileAsync(file, expected, 0n, controller.signal); + assert.equal(written, expected.length); + + const actual = Buffer.alloc(expected.length); + const read = await readFileAsync(file, actual, 0n, controller.signal); + assert.equal(read, expected.length); + assert.deepEqual(actual, expected); + + console.log(actual.toString("utf8")); +} finally { + file.close(); + await rm(path, { force: true }); +} diff --git a/samples/js/win32/package.json b/samples/js/win32/package.json new file mode 100644 index 00000000..30ab13fb --- /dev/null +++ b/samples/js/win32/package.json @@ -0,0 +1,13 @@ +{ + "name": "dynwinrt-win32-samples", + "private": true, + "type": "module", + "scripts": { + "system-info": "node system-info.mjs", + "registry": "node registry-product-name.mjs", + "overlapped-file": "node overlapped-file.mjs" + }, + "dependencies": { + "@microsoft/dynwinrt": "file:../../../bindings/js" + } +} diff --git a/samples/js/win32/registry-product-name.mjs b/samples/js/win32/registry-product-name.mjs new file mode 100644 index 00000000..bb46785b --- /dev/null +++ b/samples/js/win32/registry-product-name.mjs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + regOpenKeyEx, + regQueryValueEx, +} from "./generated/win32/Windows.Win32.System.Registry/index.mjs"; + +const HKEY_LOCAL_MACHINE = 0x80000002n; +const KEY_READ = 0x20019; +const ERROR_SUCCESS = 0; + +const opened = regOpenKeyEx( + HKEY_LOCAL_MACHINE, + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", + 0, + KEY_READ, +); +if (opened.status !== ERROR_SUCCESS || !opened.key) { + throw new Error(`RegOpenKeyEx failed with status ${opened.status}`); +} + +try { + const probe = regQueryValueEx(opened.key, "ProductName", null); + if (probe.status !== ERROR_SUCCESS) { + throw new Error(`RegQueryValueEx size query failed with status ${probe.status}`); + } + + const data = Buffer.alloc(probe.dataSize); + const result = regQueryValueEx(opened.key, "ProductName", data); + if (result.status !== ERROR_SUCCESS) { + throw new Error(`RegQueryValueEx failed with status ${result.status}`); + } + + let byteLength = result.dataSize; + if (byteLength >= 2 && data.readUInt16LE(byteLength - 2) === 0) { + byteLength -= 2; + } + console.log(data.toString("utf16le", 0, byteLength)); +} finally { + opened.key.close(); +} diff --git a/samples/js/win32/system-info.mjs b/samples/js/win32/system-info.mjs new file mode 100644 index 00000000..84eb7a78 --- /dev/null +++ b/samples/js/win32/system-info.mjs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + createSYSTEMTIME, + getSystemTime, + getTickCount64, +} from "./generated/win32/Windows.Win32.System.SystemInformation/index.mjs"; + +const uptimeMilliseconds = getTickCount64(); + +const systemTime = createSYSTEMTIME(); +getSystemTime(systemTime); +const bytes = systemTime.bytes; +const utc = new Date(Date.UTC( + bytes.readUInt16LE(0), + bytes.readUInt16LE(2) - 1, + bytes.readUInt16LE(6), + bytes.readUInt16LE(8), + bytes.readUInt16LE(10), + bytes.readUInt16LE(12), + bytes.readUInt16LE(14), +)); + +console.log(`Windows uptime: ${uptimeMilliseconds} ms`); +console.log(`System UTC time: ${utc.toISOString()}`); diff --git a/tests/e2e/e2e_test.ps1 b/tests/e2e/e2e_test.ps1 index 4edf738a..8a61a4db 100644 --- a/tests/e2e/e2e_test.ps1 +++ b/tests/e2e/e2e_test.ps1 @@ -11,6 +11,7 @@ # .\tests\e2e\e2e_test.ps1 -Lang py # Python only # .\tests\e2e\e2e_test.ps1 -Lang ts # TypeScript only # .\tests\e2e\e2e_test.ps1 -Lang com # Classic COM only +# .\tests\e2e\e2e_test.ps1 -Lang win32 # Flat Win32 only param( [switch]$SkipBuild, @@ -18,8 +19,8 @@ param( [string]$CargoProfile = "release", [string]$CargoTarget, [string]$Python, - [ValidateSet("py", "ts", "com")] - [string[]]$Lang = @("py", "ts", "com") + [ValidateSet("py", "ts", "com", "win32")] + [string[]]$Lang = @("py", "ts", "com", "win32") ) $ErrorActionPreference = "Stop" @@ -37,6 +38,7 @@ $comStreamDir = Join-Path $comBindingsDir "stream" $comAutomationDir = Join-Path $comBindingsDir "automation" $comInfrastructureDir = Join-Path $comBindingsDir "infrastructure" $comSmtcDir = Join-Path $comBindingsDir "smtc" +$win32BindingsDir = Join-Path $e2eDir "win32" [string[]]$cargoProfileArgs = @( if ($CargoProfile -eq "release") { "--release" @@ -71,9 +73,9 @@ 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 -or "com" -in $Lang) -and -not $hasNode) { +if (("ts" -in $Lang -or "com" -in $Lang -or "win32" -in $Lang) -and -not $hasNode) { Write-Host " SKIP JavaScript E2E (Node.js not installed)" -ForegroundColor DarkYellow - $Lang = @($Lang | Where-Object { $_ -notin @("ts", "com") }) + $Lang = @($Lang | Where-Object { $_ -notin @("ts", "com", "win32") }) } function Find-Win32Winmd { @@ -95,15 +97,15 @@ function Find-Win32Winmd { } $win32Winmd = $null -if ("com" -in $Lang) { +if ("com" -in $Lang -or "win32" -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." + Write-Error "Classic COM and flat Win32 E2E require 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" }) + Write-Host " SKIP Classic COM/flat Win32 (Windows.Win32.winmd not found)" -ForegroundColor DarkYellow + $Lang = @($Lang | Where-Object { $_ -notin @("com", "win32") }) } else { $env:DYNWINRT_WIN32_WINMD = $win32Winmd Write-Host " Win32 metadata: $win32Winmd" @@ -112,6 +114,18 @@ if ("com" -in $Lang) { if ($Lang.Count -eq 0) { Write-Error "No languages available"; exit 1 } +function Invoke-NodeRunner([string]$runnerPath, [int]$timeoutSeconds = 180) { + $nodePath = (Get-Command node).Source + $process = Start-Process -FilePath $nodePath -ArgumentList @($runnerPath) -NoNewWindow -PassThru + if (-not $process.WaitForExit($timeoutSeconds * 1000)) { + Write-Host "TIMEOUT: $runnerPath exceeded ${timeoutSeconds}s" -ForegroundColor Red + Stop-Process -Id $process.Id -Force + $process.WaitForExit() + return 124 + } + return $process.ExitCode +} + # -------------------------------------------------------------------------- # Build (optional) # -------------------------------------------------------------------------- @@ -151,7 +165,7 @@ if (-not $SkipBuild) { Pop-Location } - if ("ts" -in $Lang -or "com" -in $Lang) { + if ("ts" -in $Lang -or "com" -in $Lang -or "win32" -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 } @@ -301,6 +315,31 @@ if ("com" -in $Lang) { if ($LASTEXITCODE -ne 0) { Write-Error "SMTC WinRT generation failed"; exit 1 } } +if ("win32" -in $Lang) { + Write-Host "`n--- Generate (flat Win32) ---" -ForegroundColor Yellow + $win32RuntimeImport = "../../../../../../bindings/js/dist/win32.js" + foreach ($ns in @( + "Windows.Win32.System.Registry", + "Windows.Win32.System.SystemInformation", + "Windows.Win32.System.LibraryLoader", + "Windows.Win32.System.AddressBook", + "Windows.Win32.System.Threading", + "Windows.Win32.System.Com", + "Windows.Win32.Networking.Ldap", + "Windows.Win32.NetworkManagement.IpHelper", + "Windows.Win32.System.Pipes", + "Windows.Win32.Storage.FileSystem" + )) { + & cargo run -p dynwinrt-codegen @cargoProfileArgs @cargoTargetArgs --quiet -- generate ` + --winmd $win32Winmd ` + --namespace $ns ` + --class-name Apis ` + --output $win32BindingsDir ` + --import-name $win32RuntimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "Flat Win32 generation failed: $ns"; exit 1 } + } +} + # -------------------------------------------------------------------------- # Run language-specific runners # -------------------------------------------------------------------------- @@ -370,8 +409,8 @@ if ("com" -in $Lang) { $comFailed = 0 foreach ($runner in $comRunners) { Write-Host " $runner" - & node (Join-Path $runnersDir "com\$runner") - if ($LASTEXITCODE -eq 0) { + $runnerExitCode = Invoke-NodeRunner (Join-Path $runnersDir "com\$runner") + if ($runnerExitCode -eq 0) { $comPassed++ } else { $comFailed++ @@ -385,6 +424,28 @@ if ("com" -in $Lang) { } } +if ("win32" -in $Lang) { + Write-Host "`n--- Flat Win32 E2E ---" -ForegroundColor Yellow + $win32Runners = @("registry.mjs", "returns.mjs") + $win32Passed = 0 + $win32Failed = 0 + foreach ($runner in $win32Runners) { + Write-Host " $runner" + $runnerExitCode = Invoke-NodeRunner (Join-Path $runnersDir "win32\$runner") + if ($runnerExitCode -eq 0) { + $win32Passed++ + } else { + $win32Failed++ + } + } + if ($win32Failed -eq 0) { $totalPass++ } else { $totalFail++ } + $allResults += [pscustomobject]@{ + language = "win32" + passed = $win32Passed + total = $win32Runners.Count + } +} + # -------------------------------------------------------------------------- # Summary # -------------------------------------------------------------------------- diff --git a/tests/e2e/runners/win32/registry.mjs b/tests/e2e/runners/win32/registry.mjs new file mode 100644 index 00000000..72bf5bf4 --- /dev/null +++ b/tests/e2e/runners/win32/registry.mjs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "node:assert/strict"; +import { + regCloseKey, + regOpenKeyEx, + regQueryValueEx, +} from "../../e2e_generated/win32/win32/Windows.Win32.System.Registry/index.mjs"; + +const HKEY_LOCAL_MACHINE = 0x80000002n; +const KEY_READ = 0x20019; + +const opened = regOpenKeyEx( + HKEY_LOCAL_MACHINE, + "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", + 0, + KEY_READ, +); +assert.equal(opened.status, 0); +assert(opened.key); +assert.equal(opened.key.closed, false); + +try { + const probe = regQueryValueEx(opened.key, "ProductName", null); + assert.equal(probe.status, 0); + assert(probe.dataSize > 0); + + const data = Buffer.alloc(probe.dataSize); + const read = regQueryValueEx(opened.key, "ProductName", data); + assert.equal(read.status, 0); + let end = read.dataSize; + if (end >= 2 && data.readUInt16LE(end - 2) === 0) { + end -= 2; + } + const productName = data.toString("utf16le", 0, end); + assert.match(productName, /Windows/i); + assert.throws(() => regCloseKey(opened.key.value), /DynWin32Resource|resource/i); + assert.equal(opened.key.closed, false); +} finally { + const closed = regCloseKey(opened.key); + assert.equal(closed.status, 0); +} +assert.equal(opened.key.closed, true); + +const missing = regOpenKeyEx( + HKEY_LOCAL_MACHINE, + "SOFTWARE\\DynWinRT\\DefinitelyMissing", + 0, + KEY_READ, +); +assert.equal(missing.status, 2); +assert.equal(missing.key, null); + +console.log("PASS"); diff --git a/tests/e2e/runners/win32/returns.mjs b/tests/e2e/runners/win32/returns.mjs new file mode 100644 index 00000000..703ca999 --- /dev/null +++ b/tests/e2e/runners/win32/returns.mjs @@ -0,0 +1,453 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "node:assert/strict"; +import { + getModuleHandleW, + getProcAddress, + loadLibraryW, +} from "../../e2e_generated/win32/win32/Windows.Win32.System.LibraryLoader/Apis.js"; +import { ldapGetLastError } from "../../e2e_generated/win32/win32/Windows.Win32.Networking.Ldap/Apis.js"; +import { getAdaptersAddresses } from "../../e2e_generated/win32/win32/Windows.Win32.NetworkManagement.IpHelper/Apis.js"; +import { + createFILETIME, + ftAddFt, +} from "../../e2e_generated/win32/win32/Windows.Win32.System.AddressBook/Apis.js"; +import { + createProcessInformation, + createStartupInfoW, + createProcessW, + getProcessInformationProcessId, + getProcessInformationThreadId, + openProcess, + takeProcessInformationProcess, + takeProcessInformationThread, +} from "../../e2e_generated/win32/win32/Windows.Win32.System.Threading/Apis.js"; +import { + createSecurityAttributes, + createPipe, +} from "../../e2e_generated/win32/win32/Windows.Win32.System.Pipes/Apis.js"; +import { + createFileW, + readFileAsync, + writeFileAsync, +} from "../../e2e_generated/win32/win32/Windows.Win32.Storage.FileSystem/Apis.js"; +import { coIsHandlerConnected } from "../../e2e_generated/win32/win32/Windows.Win32.System.Com/Apis.js"; +import { + DynWinRtValue, + roInitialize, +} from "../../../../bindings/js/dist/winrt.js"; +import { rm } from "node:fs/promises"; +import { pbkdf2 } from "node:crypto"; +import { once } from "node:events"; +import { createServer } from "node:net"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { promisify } from "node:util"; +import { + createSYSTEMTIME, + getSystemTime, + getTickCount64, +} from "../../e2e_generated/win32/win32/Windows.Win32.System.SystemInformation/Apis.js"; + +async function withTimeout(promise, timeoutMs, message, onTimeout = () => {}) { + let timer; + const timeout = new Promise((_, reject) => { + timer = setTimeout(() => { + try { + onTimeout(); + } finally { + reject(new Error(message)); + } + }, timeoutMs); + }); + try { + return await Promise.race([promise, timeout]); + } finally { + clearTimeout(timer); + } +} + +async function withAbortTimeout(start, timeoutMs, label) { + const controller = new AbortController(); + let timedOut = false; + const timer = setTimeout(() => { + timedOut = true; + controller.abort(); + }, timeoutMs); + try { + return await start(controller.signal); + } catch (error) { + if (timedOut && error.name === "AbortError") { + throw new Error(`${label} timed out`, { cause: error }); + } + throw error; + } finally { + clearTimeout(timer); + } +} + +const first = getTickCount64(); +await new Promise((resolve) => setTimeout(resolve, 10)); +const second = getTickCount64(); +assert.equal(typeof first, "bigint"); +assert(second >= first); + +const missing = getModuleHandleW("dynwinrt-module-that-is-not-loaded.dll"); +assert.equal(missing.result, 0n); +assert.equal(missing.lastError, 126); + +const kernel = getModuleHandleW("kernel32.dll").result; +assert.notEqual(kernel, 0n); +const proc = getProcAddress(kernel, Buffer.from("GetTickCount64\0", "ascii")); +assert.equal(typeof proc.result, "bigint"); +assert.notEqual(proc.result, 0n); + +const loadedKernelCall = loadLibraryW("kernel32.dll"); +const loadedKernel = loadedKernelCall.result; +assert(loadedKernel); +assert.equal(loadedKernel.closed, false); +assert.notEqual( + getProcAddress(loadedKernel, Buffer.from("GetTickCount64\0", "ascii")).result, + 0n, +); +loadedKernel.close(); +assert.equal(loadedKernel.closed, true); + +assert.equal(typeof ldapGetLastError(), "number"); +console.log("[win32-e2e] scalar, module, and cdecl calls passed"); + +let adapters = getAdaptersAddresses(0, 0, null); +assert.equal(adapters.result, 111); +assert(adapters.sizePointer > 0); +let adapterBuffer = Buffer.alloc(adapters.sizePointer); +adapters = getAdaptersAddresses(0, 0, adapterBuffer); +if (adapters.result === 111) { + adapterBuffer = Buffer.alloc(adapters.sizePointer); + adapters = getAdaptersAddresses(0, 0, adapterBuffer); +} +assert.equal(adapters.result, 0); +console.log("[win32-e2e] adapter buffer query passed"); + +const systemTime = createSYSTEMTIME(); +getSystemTime(systemTime); +const systemTimeBytes = systemTime.bytes; +const year = systemTimeBytes.readUInt16LE(0); +const month = systemTimeBytes.readUInt16LE(2); +assert(year >= 2020); +assert(month >= 1 && month <= 12); + +const oneTick = createFILETIME(Buffer.from([1, 0, 0, 0, 0, 0, 0, 0])); +const twoTicks = createFILETIME(Buffer.from([2, 0, 0, 0, 0, 0, 0, 0])); +assert.equal(ftAddFt(oneTick, twoTicks).bytes.readUInt32LE(0), 3); + +const processHandle = openProcess(0x1000, false, process.pid); +assert(processHandle.result); +processHandle.result.close(); +assert(processHandle.result.closed); + +const attributes = createSecurityAttributes({ + securityDescriptor: null, + inheritHandle: false, +}); +const pipe = createPipe(attributes, 0); +assert.equal(pipe.result, true); +assert(pipe.hReadPipe); +assert(pipe.hWritePipe); +pipe.hReadPipe.close(); +pipe.hWritePipe.close(); +console.log("[win32-e2e] aggregate and resource calls passed"); + +const startupInfo = createStartupInfoW(); +const processInformation = createProcessInformation(); +const command = `"${process.env.ComSpec}" /d /c exit 0`; +const created = createProcessW( + null, + command, + null, + null, + false, + 0x08000000, + null, + startupInfo, + processInformation, +); +assert.equal( + created.result, + true, + `CreateProcessW failed: ${created.lastError}`, +); +assert(getProcessInformationProcessId(processInformation) > 0); +assert(getProcessInformationThreadId(processInformation) > 0); +const childProcess = takeProcessInformationProcess(processInformation); +const childThread = takeProcessInformationThread(processInformation); +assert(childProcess); +assert(childThread); +childProcess.close(); +childThread.close(); +assert.equal(takeProcessInformationProcess(processInformation), null); +assert.equal(takeProcessInformationThread(processInformation), null); + +const failedProcessInformation = createProcessInformation(); +const failedProcess = createProcessW( + null, + "dynwinrt-definitely-missing-executable.exe", + null, + null, + false, + 0x08000000, + null, + startupInfo, + failedProcessInformation, +); +assert.equal(failedProcess.result, false); +assert.throws( + () => getProcessInformationProcessId(failedProcessInformation), + /native call failed/, +); +assert.throws( + () => takeProcessInformationProcess(failedProcessInformation), + /native call failed/, +); +console.log("[win32-e2e] process creation and output ownership passed"); + +const asyncPath = join( + tmpdir(), + `dynwinrt-overlapped-${process.pid}-${Date.now()}.tmp`, +); +const asyncFileCall = createFileW( + asyncPath, + 0xc0000000, + 0, + null, + 2, + 0x40000080, + null, +); +assert(asyncFileCall.result, `CreateFileW failed: ${asyncFileCall.lastError}`); +const asyncFile = asyncFileCall.result; +console.log("[win32-e2e] starting file OVERLAPPED I/O"); +try { + const payload = Buffer.from("dynwinrt-overlapped"); + const aborted = new AbortController(); + aborted.abort(); + await assert.rejects( + readFileAsync(asyncFile, Buffer.alloc(1), 0n, aborted.signal), + (error) => error.name === "AbortError", + ); + let invalidSignal; + assert.doesNotThrow(() => { + invalidSignal = readFileAsync(asyncFile, Buffer.alloc(1), 0n, {}); + }); + await assert.rejects(invalidSignal, /signal must be an AbortSignal/); + assert.equal(asyncFile.busy, false); + assert.equal( + await withAbortTimeout( + (signal) => writeFileAsync(asyncFile, payload, 0n, signal), + 10000, + "file WriteFile", + ), + payload.length, + ); + const received = Buffer.alloc(payload.length); + assert.equal( + await withAbortTimeout( + (signal) => readFileAsync(asyncFile, received, 0n, signal), + 10000, + "file ReadFile", + ), + payload.length, + ); + assert.deepEqual(received, payload); + assert.equal( + await withAbortTimeout( + (signal) => + readFileAsync( + asyncFile, + Buffer.alloc(1), + BigInt(payload.length), + signal, + ), + 10000, + "file EOF ReadFile", + ), + 0, + ); +} finally { + asyncFile.close(); + await rm(asyncPath, { force: true }); +} +console.log("[win32-e2e] file OVERLAPPED I/O passed"); + +const pipePath = `\\\\.\\pipe\\dynwinrt-overlapped-cancel-${process.pid}-${Date.now()}`; +const pipeServer = createServer(); +pipeServer.listen(pipePath); +await withTimeout( + once(pipeServer, "listening"), + 10000, + "named-pipe server did not start", + () => pipeServer.close(), +); +const connected = once(pipeServer, "connection"); +const pipeClientCall = createFileW( + pipePath, + 0x80000000, + 0, + null, + 3, + 0x40000000, + null, +); +assert( + pipeClientCall.result, + `CreateFileW named pipe failed: ${pipeClientCall.lastError}`, +); +const pipeClient = pipeClientCall.result; +const [pipeServerSocket] = await withTimeout( + connected, + 10000, + "named-pipe client did not connect", + () => pipeServer.close(), +); +try { + const controller = new AbortController(); + const pendingRead = readFileAsync( + pipeClient, + Buffer.alloc(1), + 0n, + controller.signal, + ); + assert.equal(pipeClient.busy, true); + assert.throws(() => pipeClient.close(), /asynchronous I\/O is pending/); + const activeDeadline = Date.now() + 5000; + while (!pipeClient.active && Date.now() < activeDeadline) { + await new Promise((resolve) => setTimeout(resolve, 10)); + } + if (!pipeClient.active) { + controller.abort(); + await pendingRead.catch(() => {}); + assert.fail("named-pipe ReadFile did not enter ERROR_IO_PENDING"); + } + controller.abort(); + await assert.rejects( + pendingRead, + (error) => + error.name === "AbortError" && + /Win32 error 995/i.test(error.cause?.message ?? ""), + ); + assert.equal(pipeClient.active, false); + assert.equal(pipeClient.busy, false); + console.log("[win32-e2e] pending cancellation passed"); + + const poolController = new AbortController(); + const concurrentReads = Array.from({ length: 16 }, () => + readFileAsync(pipeClient, Buffer.alloc(1), 0n, poolController.signal), + ); + const poolDeadline = Date.now() + 5000; + while (!pipeClient.active && Date.now() < poolDeadline) { + await new Promise((resolve) => setTimeout(resolve, 10)); + } + assert.equal( + pipeClient.active, + true, + "concurrent reads did not enter pending I/O", + ); + const queueLimitError = await withTimeout( + Promise.any( + concurrentReads.map((read) => + read.then( + () => + Promise.reject(new Error("pending read completed unexpectedly")), + (error) => { + if (/waiter capacity is full/i.test(error.message)) { + return error; + } + throw error; + }, + ), + ), + ), + 10000, + "bounded OVERLAPPED waiter did not reject excess work", + () => poolController.abort(), + ); + assert.match(queueLimitError.message, /waiter capacity is full/i); + await Promise.race([ + promisify(pbkdf2)("dynwinrt", "win32", 1, 16, "sha256"), + new Promise((_, reject) => + setTimeout( + () => + reject(new Error("OVERLAPPED reads exhausted the libuv worker pool")), + 2000, + ), + ), + ]); + poolController.abort(); + const cancelledReads = await withTimeout( + Promise.allSettled(concurrentReads), + 10000, + "bounded OVERLAPPED reads did not settle after cancellation", + () => pipeServerSocket.destroy(), + ); + assert( + cancelledReads.every( + (result) => + result.status === "rejected" && + (result.reason.name === "AbortError" || + /waiter capacity is full/i.test(result.reason.message)), + ), + ); + assert.equal(pipeClient.busy, false); + console.log("[win32-e2e] bounded waiter and libuv availability passed"); + + const detachable = new ArrayBuffer(1); + const detachedBuffer = Buffer.from(detachable); + const detachedController = new AbortController(); + const detachedRead = readFileAsync( + pipeClient, + detachedBuffer, + 0n, + detachedController.signal, + ); + const detachedDeadline = Date.now() + 5000; + while (!pipeClient.active && Date.now() < detachedDeadline) { + await new Promise((resolve) => setTimeout(resolve, 10)); + } + assert.equal( + pipeClient.active, + true, + "detachment read did not enter pending I/O", + ); + structuredClone(detachable, { transfer: [detachable] }); + assert.equal(detachedBuffer.length, 0); + pipeServerSocket.write(Buffer.from([42])); + await withTimeout( + assert.rejects(detachedRead, /backing ArrayBuffer was detached or changed/), + 10000, + "detached read did not settle", + () => detachedController.abort(), + ); + assert.equal(pipeClient.busy, false); + console.log("[win32-e2e] detached read rejection passed"); +} finally { + if (!pipeClient.busy) { + pipeClient.close(); + } + pipeServerSocket.destroy(); + await new Promise((resolve) => pipeServer.close(resolve)); +} +let closedRead; +assert.doesNotThrow(() => { + closedRead = readFileAsync(pipeClient, Buffer.alloc(1), 0n); +}); +await assert.rejects(closedRead, /closed Win32 resource/); + +roInitialize(1); +const activationFactory = DynWinRtValue.activationFactory( + "Windows.Foundation.Uri", +); +assert.equal(typeof coIsHandlerConnected(activationFactory), "boolean"); +activationFactory.release(); +console.log("[win32-e2e] WinRT compatibility assertion passed"); + +console.log("PASS"); diff --git a/tools/dynwinrt-codegen/Cargo.toml b/tools/dynwinrt-codegen/Cargo.toml index 498ca1a9..1f77f017 100644 --- a/tools/dynwinrt-codegen/Cargo.toml +++ b/tools/dynwinrt-codegen/Cargo.toml @@ -8,7 +8,7 @@ repository = "https://github.com/microsoft/dynwinrt" authors = ["Microsoft"] [dependencies] -windows-metadata = "0.59.0" +windows-metadata = "=0.59.0" clap = { version = "4", features = ["derive"] } roxmltree = "0.20" regex = "1" @@ -26,6 +26,7 @@ features = [ "Win32_Graphics_DirectComposition", "Win32_Graphics_DirectManipulation", "Win32_Media_MediaFoundation", + "Win32_Networking_Ldap", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_Ole", diff --git a/tools/dynwinrt-codegen/src/codegen/mod.rs b/tools/dynwinrt-codegen/src/codegen/mod.rs index 23ec598c..a581e291 100644 --- a/tools/dynwinrt-codegen/src/codegen/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/mod.rs @@ -4,6 +4,7 @@ pub mod com; pub mod common; pub mod package; +pub mod win32; pub mod winrt; // Preserve the existing public module paths while callers migrate to diff --git a/tools/dynwinrt-codegen/src/codegen/package.rs b/tools/dynwinrt-codegen/src/codegen/package.rs index 6d1c68c8..b63d9c7a 100644 --- a/tools/dynwinrt-codegen/src/codegen/package.rs +++ b/tools/dynwinrt-codegen/src/codegen/package.rs @@ -14,6 +14,7 @@ pub struct BindingsPackageManifestInput<'a> { pub has_winrt_root: bool, pub winrt_subpath_names: &'a BTreeSet, pub com_subpath_names: &'a BTreeSet, + pub win32_subpath_names: &'a BTreeSet, } pub struct PythonPackageManifestInput<'a> { @@ -27,10 +28,12 @@ pub struct PythonPackageManifestInput<'a> { /// Preserve the existing WinRT-only renderer API and byte-for-byte output. pub fn render_package_json(input: &PackageManifestInput<'_>) -> String { let com_subpath_names = BTreeSet::new(); + let win32_subpath_names = BTreeSet::new(); render_bindings_package_json(&BindingsPackageManifestInput { has_winrt_root: true, winrt_subpath_names: input.subpath_names, com_subpath_names: &com_subpath_names, + win32_subpath_names: &win32_subpath_names, }) } @@ -38,7 +41,7 @@ pub fn render_bindings_package_json(input: &BindingsPackageManifestInput<'_>) -> if input.has_winrt_root { render_winrt_package(input) } else { - render_com_only_package(input.com_subpath_names) + render_domain_only_package(input.com_subpath_names, input.win32_subpath_names) } } @@ -105,25 +108,35 @@ fn render_winrt_package(input: &BindingsPackageManifestInput<'_>) -> String { } append_com_exports(&mut out, input.com_subpath_names); + append_win32_exports(&mut out, input.win32_subpath_names); out.push_str("\n }\n"); out.push_str("}\n"); out } -fn render_com_only_package(com_subpath_names: &BTreeSet) -> String { +fn render_domain_only_package( + com_subpath_names: &BTreeSet, + win32_subpath_names: &BTreeSet, +) -> String { + let root = if !com_subpath_names.is_empty() { + "com" + } else { + "win32" + }; let mut out = String::new(); out.push_str("{\n"); out.push_str(" \"name\": \"@winapp/bindings\",\n"); out.push_str(" \"type\": \"commonjs\",\n"); out.push_str(" \"sideEffects\": false,\n"); - out.push_str(" \"main\": \"./index.js\",\n"); - out.push_str(" \"types\": \"./index.d.ts\",\n"); + out.push_str(&format!(" \"main\": \"./{root}/index.js\",\n")); + out.push_str(&format!(" \"types\": \"./{root}/index.d.ts\",\n")); out.push_str(" \"exports\": {\n"); out.push_str(" \".\": {\n"); - out.push_str(" \"types\": \"./com/index.d.ts\",\n"); - out.push_str(" \"import\": \"./com/index.mjs\",\n"); - out.push_str(" \"require\": \"./com/index.js\",\n"); - out.push_str(" \"default\": \"./com/index.js\"\n"); + out.push_str(&format!(" \"types\": \"./{root}/index.d.ts\",\n")); + let root_import = format!("./{root}/index.mjs"); + out.push_str(&format!(" \"import\": \"{root_import}\",\n")); + out.push_str(&format!(" \"require\": \"./{root}/index.js\",\n")); + out.push_str(&format!(" \"default\": \"./{root}/index.js\"\n")); out.push_str(" }"); // Preserve the original COM-only deep-import paths while storing all COM @@ -139,11 +152,37 @@ fn render_com_only_package(com_subpath_names: &BTreeSet) -> String { } append_com_exports(&mut out, com_subpath_names); + append_win32_exports(&mut out, win32_subpath_names); out.push_str("\n }\n"); out.push_str("}\n"); out } +fn append_win32_exports(out: &mut String, win32_subpath_names: &BTreeSet) { + if win32_subpath_names.is_empty() { + return; + } + + out.push_str(",\n"); + out.push_str(" \"./win32\": {\n"); + out.push_str(" \"types\": \"./win32/index.d.ts\",\n"); + out.push_str(" \"import\": \"./win32/index.mjs\",\n"); + out.push_str(" \"require\": \"./win32/index.js\"\n"); + out.push_str(" }"); + for name in win32_subpath_names { + out.push_str(",\n"); + out.push_str(&format!(" \"./win32/{name}\": {{\n")); + out.push_str(&format!( + " \"types\": \"./win32/{name}/index.d.ts\",\n" + )); + out.push_str(&format!( + " \"import\": \"./win32/{name}/index.mjs\",\n" + )); + out.push_str(&format!(" \"require\": \"./win32/{name}/index.js\"\n")); + out.push_str(" }"); + } +} + fn append_com_exports(out: &mut String, com_subpath_names: &BTreeSet) { if com_subpath_names.is_empty() { return; @@ -226,10 +265,12 @@ mod tests { fn mixed_package_keeps_duplicate_names_in_separate_domains() { let winrt = BTreeSet::from(["Uri".to_string()]); let com = BTreeSet::from(["Uri".to_string()]); + let win32 = BTreeSet::new(); let out = render_bindings_package_json(&BindingsPackageManifestInput { has_winrt_root: true, winrt_subpath_names: &winrt, com_subpath_names: &com, + win32_subpath_names: &win32, }); assert!(out.contains("\"./Uri\"")); @@ -245,10 +286,12 @@ mod tests { fn com_only_package_preserves_legacy_root_subpaths() { let com = BTreeSet::from(["ITaskbarList3".to_string()]); let winrt = BTreeSet::new(); + let win32 = BTreeSet::new(); let out = render_bindings_package_json(&BindingsPackageManifestInput { has_winrt_root: false, winrt_subpath_names: &winrt, com_subpath_names: &com, + win32_subpath_names: &win32, }); assert!(out.contains("\"type\": \"commonjs\"")); @@ -261,6 +304,23 @@ mod tests { assert!(out.contains("\"./com/*\"")); } + #[test] + fn win32_only_package_exports_namespace_subpaths() { + let winrt = BTreeSet::new(); + let com = BTreeSet::new(); + let win32 = BTreeSet::from(["Windows.Win32.System.Registry".to_string()]); + let out = render_bindings_package_json(&BindingsPackageManifestInput { + has_winrt_root: false, + winrt_subpath_names: &winrt, + com_subpath_names: &com, + win32_subpath_names: &win32, + }); + assert!(out.contains("\"./win32\"")); + assert!(out.contains("\"./win32/Windows.Win32.System.Registry\"")); + assert!(out.contains("./win32/Windows.Win32.System.Registry/index.js")); + assert!(out.contains("\"import\": \"./win32/Windows.Win32.System.Registry/index.mjs\"")); + } + #[test] fn python_manifest_pins_runtime_and_typed_package_data() { let out = render_python_pyproject(&PythonPackageManifestInput { diff --git a/tools/dynwinrt-codegen/src/codegen/win32/ir.rs b/tools/dynwinrt-codegen/src/codegen/win32/ir.rs new file mode 100644 index 00000000..95d82d95 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/win32/ir.rs @@ -0,0 +1,489 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AbiType { + Bool32, + I8, + U8, + I16, + U16, + I32, + U32, + I64, + U64, + F32, + F64, + Pointer, + FunctionPointer, + Handle, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Direction { + In, + Out, + InOut, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Constness { + Const, + Mutable, + Unspecified, + Mixed, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Cleanup { + None, + CloseHandle, + RegCloseKey, + LocalFree, + GlobalFree, + FreeLibrary, + CloseServiceHandle, + CoTaskMemFree, + CredFree, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SuccessRule { + Always, + ReturnZero, + ReturnNonZero, + ReturnNonNull, + HResultSucceeded, + SignedNonNegative, + ReturnValidHandle, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CallingConvention { + System, + Cdecl, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StringEncoding { + Wide, + Ansi, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Scalar { + Bool8, + Bool32, + I8, + U8, + I16, + U16, + I32, + U32, + I64, + U64, + F32, + F64, + NativeIsize, + NativeUsize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EnumUnderlying { + I8, + U8, + I16, + U16, + I32, + U32, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EnumMember { + pub name: String, + pub value: i128, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EnumDefinition { + pub namespace: String, + pub name: String, + pub underlying: EnumUnderlying, + pub members: Vec, + pub is_flags: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeScalar { + I8, + U8, + I16, + U16, + I32, + U32, + I64, + U64, + F32, + F64, + NativeIsize, + NativeUsize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeAggregateKind { + Struct, + Union, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NativeFieldType { + Scalar(NativeScalar), + Guid, + Pointer, + Handle { + cleanup: Cleanup, + }, + Struct { + name: String, + layout: Box, + by_value_compatible: bool, + }, + Union { + name: String, + layout: Box, + by_value_compatible: bool, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NativeField { + pub name: String, + pub offset: usize, + pub count: u32, + pub typ: NativeFieldType, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NativeArchitectureLayout { + pub size: usize, + pub alignment: usize, + pub fields: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NativeLayout { + pub namespace: String, + pub name: String, + pub kind: NativeAggregateKind, + pub by_value_compatible: bool, + pub x86: NativeArchitectureLayout, + pub x64: NativeArchitectureLayout, + pub arm64: NativeArchitectureLayout, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ValueType { + Scalar(Scalar), + Enum { + namespace: String, + name: String, + underlying: EnumUnderlying, + }, + Handle { + namespace: String, + name: String, + }, + DataPointer, + StringPointer(StringEncoding), + FunctionPointer, + NativeStructPointer { + layout: NativeLayout, + }, + NativeUnionPointer { + layout: NativeLayout, + }, + NativeStruct { + layout: NativeLayout, + }, + ScalarPointer { + scalar: Scalar, + }, + GuidPointer, + NullPointer, + ComInterface { + name: String, + iid: String, + }, + StringPointerPointer(StringEncoding), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParameterContract { + pub name: String, + pub native_name: Option<(String, String)>, + pub typ: ValueType, + pub abi: AbiType, + pub pointer_depth: u8, + pub constness: Constness, + pub direction: Direction, + pub nullable: bool, + pub reserved: bool, + pub null_null_terminated: bool, + pub cleanup: Cleanup, + pub consumes_resource: bool, + pub resource_cleanup: Cleanup, + pub buffer: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BufferContract { + pub count_parameter: Option, + pub constant_count: Option, + pub count_is_bytes: bool, + pub element_size: usize, + pub element_alignment: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FunctionContract { + pub namespace: String, + pub container: String, + pub name: String, + pub dll: String, + pub entry_point: String, + pub parameters: Vec, + pub return_type: Option, + pub return_abi: Option, + pub return_aggregate: Option, + pub return_native_name: Option<(String, String)>, + pub return_pointer_depth: u8, + pub return_constness: Constness, + pub return_cleanup: Cleanup, + pub return_is_status: bool, + pub success_rule: SuccessRule, + pub capture_last_error: bool, + pub calling_convention: CallingConvention, + pub enums: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SurfaceType { + Boolean, + Number, + BigInt, + Enum(String), + Handle(String), + Buffer, + String(StringEncoding), + MultiString(StringEncoding), + ManagedResource, + Resource, + NativeStruct(String), + NativeUnion(String), + ComInterface(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SurfaceParameter { + pub name: String, + pub typ: SurfaceType, + pub nullable: bool, + pub minimum_bytes: Option, + pub alignment: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum InputExpression { + Surface { + parameter_index: usize, + conversion: Conversion, + }, + BufferLength { + parameter_index: usize, + divisor: usize, + abi: AbiType, + }, + NullPointer, + Zero(AbiType), + NativeAggregate { + parameter_index: usize, + layout: NativeLayout, + nullable: bool, + by_value: bool, + }, + ScalarPointer { + parameter_index: usize, + scalar: Scalar, + nullable: bool, + }, + ComInterface { + parameter_index: usize, + iid: String, + }, + StringPointerPointer { + parameter_index: usize, + encoding: StringEncoding, + nullable: bool, + }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Conversion { + Boolean8, + Boolean, + I8, + U8, + I16, + U16, + I32, + U32, + I64, + U64, + F32, + F64, + Handle, + DataPointer, + WideString, + AnsiString, + WideMultiString, + AnsiMultiString, + ResourceInput(Cleanup), + Number, + BigInt, + Resource, + NativeAggregate, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProjectedOutput { + pub name: String, + pub output_index: usize, + pub typ: SurfaceType, + pub conversion: Conversion, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RuntimeParameter { + pub abi: AbiType, + pub direction: Direction, + pub nullable: bool, + pub cleanup: Cleanup, + pub consumes_resource: bool, + pub resource_cleanup: Cleanup, + pub aggregate: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RuntimePlan { + pub dll: String, + pub entry_point: String, + pub parameters: Vec, + pub return_abi: Option, + pub return_aggregate: Option, + pub return_cleanup: Cleanup, + pub success_rule: SuccessRule, + pub capture_last_error: bool, + pub calling_convention: CallingConvention, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ReturnShape { + Void, + Direct { + typ: SurfaceType, + conversion: Conversion, + }, + Object { + status: bool, + return_value: Option<(SurfaceType, Conversion)>, + outputs: Vec, + last_error: bool, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProjectedFunction { + pub metadata_name: String, + pub js_name: String, + pub unicode_alias: Option, + pub parameters: Vec, + pub inputs: Vec, + pub runtime: RuntimePlan, + pub return_shape: ReturnShape, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProjectedApis { + pub namespace: String, + pub class_name: String, + pub functions: Vec, + pub enums: Vec, + pub native_builders: Vec, + pub async_functions: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AsyncIoKind { + Read, + Write, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProjectedAsyncFunction { + pub js_name: String, + pub kind: AsyncIoKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProjectedNativeBuilder { + pub layout_name: String, + pub js_name: String, + pub size_field: Option, + pub fields: Vec, + pub outputs: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeBuilderFieldKind { + Boolean, + DataPointer { nullable: bool }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProjectedNativeBuilderField { + pub native_name: String, + pub surface_name: String, + pub kind: NativeBuilderFieldKind, + pub optional: bool, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeOutputFieldKind { + U32, + Resource { cleanup: Cleanup }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProjectedNativeOutputField { + pub native_name: String, + pub surface_name: String, + pub kind: NativeOutputFieldKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct OmittedFunction { + pub identity: String, + pub reason: String, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProjectionResult { + pub projected: ProjectedApis, + pub omitted: Vec, +} + +impl ProjectionResult { + pub fn complete_count(&self) -> usize { + self.projected.functions.len() + self.projected.async_functions.len() + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/win32/mod.rs b/tools/dynwinrt-codegen/src/codegen/win32/mod.rs new file mode 100644 index 00000000..e80612b1 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/win32/mod.rs @@ -0,0 +1,1225 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod ir; +mod model; +mod project; +mod render; + +use crate::win32_metadata::RawApis; + +pub use ir::{OmittedFunction, ProjectionResult}; +pub use render::GeneratedOutput; + +pub fn generate_apis_files( + raw: &RawApis, + runtime_import: &str, +) -> (GeneratedOutput, Vec) { + let projection = project::project_apis(raw); + let output = render::render(&projection.projected, runtime_import); + (output, projection.omitted) +} + +pub fn project_apis(raw: &RawApis) -> ProjectionResult { + project::project_apis(raw) +} + +pub fn validate_function(function: &crate::win32_metadata::RawFunction) -> Result<(), String> { + model::validate_function(function).map(|_| ()) +} + +#[cfg(test)] +mod tests { + use super::ir::{ + AbiType, Conversion, Direction as ProjectedDirection, ReturnShape, SurfaceType, + }; + use super::*; + use crate::win32_metadata::{ + RawApis, RawArchitectures, RawBaseType, RawBuffer, RawBufferSize, RawCallingConvention, + RawConstness, RawDirection, RawEnumMember, RawFunction, RawLayoutKind, RawNamedKind, + RawNativeField, RawNativeLayout, RawNativeLayoutSet, RawPacking, RawParameter, RawScalar, + RawStatusSemantics, RawType, + }; + + fn scalar(scalar: RawScalar) -> RawType { + RawType { + base: RawBaseType::Scalar(scalar), + pointer_depth: 0, + constness: RawConstness::Unspecified, + } + } + + fn synthetic_function(name: &str) -> RawFunction { + RawFunction { + namespace: "Tests".into(), + container: "Apis".into(), + name: name.into(), + dll: "kernel32.dll".into(), + entry_point: name.into(), + return_type: scalar(RawScalar::U32), + parameters: Vec::new(), + return_status: RawStatusSemantics::None, + return_free_with: None, + supports_last_error: false, + calling_convention: RawCallingConvention::System, + architectures: RawArchitectures { + x86: true, + x64: true, + arm64: true, + }, + variadic: false, + } + } + + #[test] + fn registry_projection_uses_immutable_plans_and_safe_pointers() { + let Ok(winmd) = std::env::var("DYNWINRT_WIN32_WINMD") else { + return; + }; + if !std::path::Path::new(&winmd).is_file() { + return; + } + let raw = + crate::win32_metadata::parse_apis(&winmd, "Windows.Win32.System.Registry", "Apis") + .unwrap(); + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(output.js.contains("DynWin32Function.bind")); + assert!(!output.js.contains("DynWin32Unsafe")); + assert!(!output.dts.contains("bigint | Buffer")); + assert!(output.js.contains("exports.regOpenKeyExW"), "{omitted:#?}"); + assert!(output.js.contains("exports.regOpenKeyEx"), "{omitted:#?}"); + } + + #[test] + fn fixed_buffer_projection_validates_minimum_storage() { + let mut function = synthetic_function("ReadFourBytes"); + function.parameters.push(RawParameter { + name: "buffer".into(), + typ: RawType { + base: RawBaseType::Scalar(RawScalar::U8), + pointer_depth: 1, + constness: RawConstness::Mutable, + }, + direction: RawDirection::Out, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: Some(RawBuffer { + element: scalar(RawScalar::U8), + size: RawBufferSize::Constant(4), + }), + free_with: None, + }); + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }; + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(omitted.is_empty()); + assert!(output.js.contains("must contain at least 4 bytes")); + } + + #[test] + fn duplicate_exports_fail_closed_before_rendering() { + let function = synthetic_function("Duplicate"); + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function.clone(), function], + }; + let projection = project_apis(&raw); + assert_eq!(projection.complete_count(), 0); + assert_eq!(projection.omitted.len(), 2); + assert!( + projection + .omitted + .iter() + .all(|omission| omission.reason.contains("collision")) + ); + } + + #[test] + fn by_value_inout_handle_remains_a_direct_input() { + let mut function = synthetic_function("FindClose"); + function.parameters.push(RawParameter { + name: "hFindFile".into(), + typ: RawType { + base: RawBaseType::Named { + namespace: "Windows.Win32.Foundation".into(), + name: "HANDLE".into(), + kind: RawNamedKind::Handle { + cleanup: Some("CloseHandle".into()), + }, + }, + pointer_depth: 0, + constness: RawConstness::Unspecified, + }, + direction: RawDirection::InOut, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }; + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(omitted.is_empty()); + assert!(output.js.contains( + r#"type: "handle", direction: "in", nullable: false, cleanup: "none", consumesResource: false, resourceCleanup: "closeHandle", aggregateDescriptor: undefined"# + )); + assert!(!output.dts.contains("readonly hFindFile")); + } + + #[test] + fn bool_failure_rule_precedes_owned_output_adoption() { + let mut function = synthetic_function("OpenToken"); + function.return_type = scalar(RawScalar::Bool32); + function.parameters.push(RawParameter { + name: "token".into(), + typ: RawType { + base: RawBaseType::Named { + namespace: "Windows.Win32.Foundation".into(), + name: "HANDLE".into(), + kind: RawNamedKind::Handle { + cleanup: Some("CloseHandle".into()), + }, + }, + pointer_depth: 1, + constness: RawConstness::Mutable, + }, + direction: RawDirection::Out, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: Some("CloseHandle".into()), + }); + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }; + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(omitted.is_empty()); + assert!(output.js.contains(r#"successRule: "nonzero""#)); + } + + #[test] + fn generic_handle_output_without_function_cleanup_fails_closed() { + let mut function = synthetic_function("LsaConnectUntrusted"); + function.parameters.push(RawParameter { + name: "handle".into(), + typ: RawType { + base: RawBaseType::Named { + namespace: "Windows.Win32.Foundation".into(), + name: "HANDLE".into(), + kind: RawNamedKind::Handle { + cleanup: Some("CloseHandle".into()), + }, + }, + pointer_depth: 1, + constness: RawConstness::Mutable, + }, + direction: RawDirection::Out, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }; + let projection = project_apis(&raw); + assert_eq!(projection.complete_count(), 0); + assert!( + projection.omitted[0] + .reason + .contains("function-specific ownership") + ); + } + + #[test] + fn owning_inout_handle_fails_closed() { + let mut function = synthetic_function("ReplaceHandle"); + function.parameters.push(RawParameter { + name: "handle".into(), + typ: RawType { + base: RawBaseType::Named { + namespace: "Windows.Win32.Foundation".into(), + name: "HANDLE".into(), + kind: RawNamedKind::Handle { + cleanup: Some("CloseHandle".into()), + }, + }, + pointer_depth: 1, + constness: RawConstness::Mutable, + }, + direction: RawDirection::InOut, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: Some("CloseHandle".into()), + }); + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }; + let projection = project_apis(&raw); + assert_eq!(projection.complete_count(), 0); + assert!(projection.omitted[0].reason.contains("owning InOut handle")); + } + + #[test] + fn ntstatus_uses_signed_nonnegative_success() { + let mut function = synthetic_function("NtFunction"); + function.return_type = scalar(RawScalar::I32); + function.return_status = RawStatusSemantics::SignedNonNegativeIsSuccess; + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }; + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(omitted.is_empty()); + assert!(output.js.contains(r#"successRule: "nonnegative""#)); + } + + #[test] + fn nullable_handle_uses_the_explicit_null_contract() { + let mut function = synthetic_function("OptionalWindow"); + function.parameters.push(RawParameter { + name: "hWnd".into(), + typ: RawType { + base: RawBaseType::Named { + namespace: "Windows.Win32.Foundation".into(), + name: "HWND".into(), + kind: RawNamedKind::Handle { cleanup: None }, + }, + pointer_depth: 0, + constness: RawConstness::Unspecified, + }, + direction: RawDirection::In, + nullable: true, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }; + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(omitted.is_empty()); + assert!(output.js.contains("DynWin32.handle(hWnd, true)")); + assert!(output.dts.contains("hWnd: HWND | null")); + } + + #[test] + fn cdecl_is_preserved_and_variadic_functions_fail_closed() { + let mut function = synthetic_function("CdeclScalar"); + function.calling_convention = RawCallingConvention::Cdecl; + function.parameters.push(RawParameter { + name: "value".into(), + typ: scalar(RawScalar::F32), + direction: RawDirection::In, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + function.return_type = scalar(RawScalar::F32); + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function.clone()], + }; + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(omitted.is_empty()); + assert!(output.js.contains(r#"callingConvention: "cdecl""#)); + + function.variadic = true; + let projection = project_apis(&RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }); + assert_eq!(projection.complete_count(), 0); + assert!(projection.omitted[0].reason.contains("variadic")); + } + + fn point_layout(bitfield: bool) -> RawNativeLayoutSet { + let fields = ["x", "y"] + .into_iter() + .map(|name| RawNativeField { + name: name.into(), + typ: scalar(RawScalar::I32), + fixed_count: None, + bitfield, + flexible_array: false, + }) + .collect(); + RawNativeLayoutSet { + recursive: false, + variants: vec![RawNativeLayout { + architectures: RawArchitectures { + x86: true, + x64: true, + arm64: true, + }, + kind: RawLayoutKind::Sequential, + packing: RawPacking::Default, + declared_size: None, + forced_alignment: None, + fields, + }], + } + } + + #[test] + fn native_struct_pointer_projects_typed_aligned_storage() { + let mut function = synthetic_function("OffsetPoint"); + function.parameters.push(RawParameter { + name: "point".into(), + typ: RawType { + base: RawBaseType::Named { + namespace: "Tests".into(), + name: "POINT".into(), + kind: RawNamedKind::NativeStruct { + layout: Box::new(point_layout(false)), + }, + }, + pointer_depth: 1, + constness: RawConstness::Mutable, + }, + direction: RawDirection::InOut, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }; + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(omitted.is_empty(), "{omitted:#?}"); + assert!(output.js.contains("DynWin32.createNativeStruct")); + assert!(output.js.contains("DynWin32.nativeStruct(point")); + assert!( + output + .dts + .contains("createPOINT(bytes?: Buffer | Uint8Array): POINT") + ); + } + + #[test] + fn native_bitfield_layout_fails_closed() { + let mut function = synthetic_function("Bitfield"); + function.parameters.push(RawParameter { + name: "value".into(), + typ: RawType { + base: RawBaseType::Named { + namespace: "Tests".into(), + name: "BITS".into(), + kind: RawNamedKind::NativeStruct { + layout: Box::new(point_layout(true)), + }, + }, + pointer_depth: 1, + constness: RawConstness::Mutable, + }, + direction: RawDirection::In, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + let projection = project_apis(&RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }); + assert_eq!(projection.complete_count(), 0); + assert!(projection.omitted[0].reason.contains("bitfield")); + } + + #[test] + fn zero_argument_native_struct_return_declares_its_layout() { + let mut function = synthetic_function("GetPoint"); + function.return_type = RawType { + base: RawBaseType::Named { + namespace: "Tests".into(), + name: "POINT".into(), + kind: RawNamedKind::NativeStruct { + layout: Box::new(point_layout(false)), + }, + }, + pointer_depth: 0, + constness: RawConstness::Unspecified, + }; + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }; + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(omitted.is_empty(), "{omitted:#?}"); + assert!(output.js.contains("const _nativeLayout_POINT")); + assert!( + output + .js + .contains("DynWin32.toNativeStruct(_return, _nativeLayout_POINT)") + ); + } + + #[test] + fn one_byte_metadata_bool_is_not_win32_bool() { + let mut function = synthetic_function("Boolean8"); + function.parameters.push(RawParameter { + name: "value".into(), + typ: scalar(RawScalar::Bool8), + direction: RawDirection::In, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + function.return_type = scalar(RawScalar::Bool8); + let (output, omitted) = generate_apis_files( + &RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }, + "@microsoft/dynwinrt/win32", + ); + assert!(omitted.is_empty()); + assert!(output.js.contains("type: \"u8\"")); + assert!(output.js.contains("DynWin32.bool8(value)")); + assert!(output.dts.contains("boolean8(value: boolean): boolean")); + } + + #[test] + fn scalar_returns_preserve_exact_abi_widths_and_js_shapes() { + let cases = [ + ("ReturnI8", RawScalar::I8, AbiType::I8, SurfaceType::Number), + ( + "ReturnI16", + RawScalar::I16, + AbiType::I16, + SurfaceType::Number, + ), + ( + "ReturnI64", + RawScalar::I64, + AbiType::I64, + SurfaceType::BigInt, + ), + ( + "ReturnU64", + RawScalar::U64, + AbiType::U64, + SurfaceType::BigInt, + ), + ( + "ReturnF32", + RawScalar::F32, + AbiType::F32, + SurfaceType::Number, + ), + ( + "ReturnF64", + RawScalar::F64, + AbiType::F64, + SurfaceType::Number, + ), + ]; + let functions = cases + .iter() + .map(|(name, scalar, _, _)| { + let mut function = synthetic_function(name); + function.return_type = self::scalar(*scalar); + function + }) + .collect(); + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions, + }; + + let projection = project_apis(&raw); + assert!(projection.omitted.is_empty()); + for (name, _, abi, surface) in &cases { + let function = projection + .projected + .functions + .iter() + .find(|function| function.metadata_name == *name) + .unwrap(); + assert_eq!(function.runtime.return_abi, Some(*abi)); + assert!(matches!( + &function.return_shape, + ReturnShape::Direct { typ, .. } if typ == surface + )); + } + + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(omitted.is_empty()); + for abi in ["i8", "i16", "i64", "u64", "f32", "f64"] { + assert!(output.js.contains(&format!("returnType: \"{abi}\""))); + } + assert_eq!( + output + .js + .matches("return DynWin32.toBigint(_return)") + .count(), + 2 + ); + assert!(output.dts.contains("returnI8(): number")); + assert!(output.dts.contains("returnI16(): number")); + assert!(output.dts.contains("returnI64(): bigint")); + assert!(output.dts.contains("returnU64(): bigint")); + assert!(output.dts.contains("returnF32(): number")); + assert!(output.dts.contains("returnF64(): number")); + } + + #[test] + fn bool32_return_and_output_project_as_booleans() { + let mut direct = synthetic_function("ReturnsBool32"); + direct.return_type = scalar(RawScalar::Bool32); + + let mut output = synthetic_function("GetFlag"); + output.return_type = RawType { + base: RawBaseType::Void, + pointer_depth: 0, + constness: RawConstness::Unspecified, + }; + output.parameters.push(RawParameter { + name: "enabled".into(), + typ: RawType { + base: RawBaseType::Scalar(RawScalar::Bool32), + pointer_depth: 1, + constness: RawConstness::Mutable, + }, + direction: RawDirection::Out, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![direct, output], + }; + let projection = project_apis(&raw); + assert!(projection.omitted.is_empty()); + let get_flag = projection + .projected + .functions + .iter() + .find(|function| function.metadata_name == "GetFlag") + .unwrap(); + assert_eq!( + get_flag.runtime.parameters[0].direction, + ProjectedDirection::Out + ); + assert!(matches!( + &get_flag.return_shape, + ReturnShape::Object { outputs, .. } + if outputs.len() == 1 + && outputs[0].typ == SurfaceType::Boolean + && outputs[0].conversion == Conversion::Boolean + )); + + let (generated, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(omitted.is_empty()); + assert!(generated.js.contains("return DynWin32.toBoolean(_return)")); + assert!( + generated + .js + .contains("enabled: DynWin32.toBoolean(_outputs[0])") + ); + assert!(generated.dts.contains("returnsBool32(): boolean")); + assert!( + generated + .dts + .contains("getFlag(): { readonly enabled: boolean }") + ); + } + + #[test] + fn no_argument_void_returns_omit_result_fields() { + let void_type = RawType { + base: RawBaseType::Void, + pointer_depth: 0, + constness: RawConstness::Unspecified, + }; + let mut no_outputs = synthetic_function("VoidNoOutputs"); + no_outputs.return_type = void_type.clone(); + + let mut with_output = synthetic_function("VoidWithOutput"); + with_output.return_type = void_type; + with_output.parameters.push(RawParameter { + name: "value".into(), + typ: RawType { + base: RawBaseType::Scalar(RawScalar::U32), + pointer_depth: 1, + constness: RawConstness::Mutable, + }, + direction: RawDirection::Out, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![no_outputs, with_output], + }; + + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(omitted.is_empty()); + assert!(output.js.contains("returnType: \"void\"")); + assert!(output.js.contains("return undefined")); + assert!(output.dts.contains("voidNoOutputs(): void")); + assert!( + output + .dts + .contains("voidWithOutput(): { readonly value: number }") + ); + assert!(!output.dts.contains("readonly result")); + } + + #[test] + fn unknown_by_value_type_fails_but_explicit_data_pointer_is_safe() { + let mut unknown = synthetic_function("UnknownValue"); + unknown.parameters.push(RawParameter { + name: "value".into(), + typ: RawType { + base: RawBaseType::Unknown("missing native definition".into()), + pointer_depth: 0, + constness: RawConstness::Unspecified, + }, + direction: RawDirection::In, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + + let mut pointer = synthetic_function("OpaquePointer"); + pointer.parameters.push(RawParameter { + name: "data".into(), + typ: RawType { + base: RawBaseType::Named { + namespace: "Tests".into(), + name: "PVOID".into(), + kind: RawNamedKind::DataPointer, + }, + pointer_depth: 0, + constness: RawConstness::Const, + }, + direction: RawDirection::In, + nullable: true, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![unknown, pointer], + }; + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert_eq!(omitted.len(), 1, "{omitted:#?}"); + assert!(omitted[0].reason.contains("native type is unknown")); + assert!(!output.js.contains("unknownValue")); + assert!(output.js.contains("DynWin32.dataPointer(data, true)")); + assert!( + output + .dts + .contains("opaquePointer(data: Buffer | Uint8Array | null): number") + ); + } + + #[test] + fn pointer_returns_require_explicit_lifetime_and_cleanup() { + let pointer_type = RawType { + base: RawBaseType::Named { + namespace: "Tests".into(), + name: "PVOID".into(), + kind: RawNamedKind::DataPointer, + }, + pointer_depth: 0, + constness: RawConstness::Mutable, + }; + let mut unowned = synthetic_function("UnownedPointer"); + unowned.return_type = pointer_type.clone(); + + let mut owned = synthetic_function("OwnedPointer"); + owned.return_type = pointer_type; + owned.return_free_with = Some("LocalFree".into()); + + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![unowned, owned], + }; + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert_eq!(omitted.len(), 1, "{omitted:#?}"); + assert!( + omitted[0] + .reason + .contains("pointer return lifetime and ownership") + ); + assert!(!output.js.contains("unownedPointer")); + assert!(output.js.contains("returnCleanup: \"localFree\"")); + assert!(output.js.contains("return DynWin32.toResource(_return)")); + assert!( + output + .dts + .contains("ownedPointer(): DynWin32Resource | null") + ); + } + + #[test] + fn pointer_depth_two_fails_closed_before_rendering() { + let mut function = synthetic_function("DoublePointerOutput"); + function.parameters.push(RawParameter { + name: "values".into(), + typ: RawType { + base: RawBaseType::Scalar(RawScalar::U32), + pointer_depth: 2, + constness: RawConstness::Mutable, + }, + direction: RawDirection::Out, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + let projection = project_apis(&RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }); + assert_eq!(projection.complete_count(), 0); + assert_eq!(projection.omitted.len(), 1); + assert!( + projection.omitted[0] + .reason + .contains("unsupported scalar pointer depth 2") + ); + } + + #[test] + fn counted_buffer_hides_and_derives_its_element_count() { + let mut function = synthetic_function("WriteWords"); + function.parameters = vec![ + RawParameter { + name: "values".into(), + typ: RawType { + base: RawBaseType::Scalar(RawScalar::U16), + pointer_depth: 1, + constness: RawConstness::Const, + }, + direction: RawDirection::In, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: Some(RawBuffer { + element: scalar(RawScalar::U16), + size: RawBufferSize::ElementCountParam(1), + }), + free_with: None, + }, + RawParameter { + name: "count".into(), + typ: scalar(RawScalar::U32), + direction: RawDirection::In, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }, + ]; + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }; + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(omitted.is_empty()); + assert!(output.js.contains("_bufferCount(values, 2)")); + assert!( + output + .dts + .contains("writeWords(values: Buffer | Uint8Array): number") + ); + assert!(!output.dts.contains("count: number")); + } + + fn enum_type(namespace: &str, name: &str, underlying: RawScalar) -> RawType { + RawType { + base: RawBaseType::Named { + namespace: namespace.into(), + name: name.into(), + kind: RawNamedKind::Enum { + underlying, + members: vec![ + RawEnumMember { + name: "NONE".into(), + value: 0, + }, + RawEnumMember { + name: "HIGH_BIT".into(), + value: 0x8000_0000, + }, + ], + is_flags: true, + }, + }, + pointer_depth: 0, + constness: RawConstness::Unspecified, + } + } + + #[test] + fn unsigned_enum_high_bit_preserves_u32_abi_and_value() { + let mut function = synthetic_function("SetSecurity"); + function.parameters.push(RawParameter { + name: "securityInformation".into(), + typ: enum_type("Tests.Security", "SECURITY_INFORMATION", RawScalar::U32), + direction: RawDirection::In, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }; + let projection = project_apis(&raw); + assert!(projection.omitted.is_empty()); + let projected = &projection.projected.functions[0]; + assert_eq!(projected.runtime.parameters[0].abi, AbiType::U32); + assert!(matches!( + projected.inputs[0], + super::ir::InputExpression::Surface { + conversion: Conversion::U32, + .. + } + )); + + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(omitted.is_empty()); + assert!(output.js.contains("DynWin32.u32(securityInformation)")); + let enum_js = output + .extra_files + .iter() + .find(|(name, _)| name == "SECURITY_INFORMATION.js") + .map(|(_, content)| content) + .unwrap(); + assert!(enum_js.contains("HIGH_BIT: 2147483648")); + } + + #[test] + fn enum_simple_name_collisions_fail_closed() { + let mut first = synthetic_function("UseFirstMode"); + first.parameters.push(RawParameter { + name: "mode".into(), + typ: enum_type("Tests.First", "MODE", RawScalar::U32), + direction: RawDirection::In, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + let mut second = synthetic_function("UseSecondMode"); + second.parameters.push(RawParameter { + name: "mode".into(), + typ: enum_type("Tests.Second", "MODE", RawScalar::U32), + direction: RawDirection::In, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + + let projection = project_apis(&RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![first, second], + }); + assert_eq!(projection.complete_count(), 0); + assert_eq!(projection.omitted.len(), 2); + assert!( + projection + .omitted + .iter() + .all(|omission| { omission.reason.contains("enum simple name is ambiguous") }) + ); + } + + #[test] + fn unsupported_enum_underlying_type_fails_closed() { + let mut function = synthetic_function("LargeEnum"); + function.return_type = enum_type("Tests", "LARGE_ENUM", RawScalar::U64); + let projection = project_apis(&RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }); + assert_eq!(projection.complete_count(), 0); + assert!( + projection.omitted[0] + .reason + .contains("enum underlying type is not representable"), + "{:#?}", + projection.omitted + ); + } + + #[test] + fn generation_is_deterministic_without_renderer_snapshots() { + let mut enum_function = synthetic_function("SetSecurity"); + enum_function.parameters.push(RawParameter { + name: "securityInformation".into(), + typ: enum_type("Tests", "SECURITY_INFORMATION", RawScalar::U32), + direction: RawDirection::In, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }); + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![synthetic_function("Zulu"), enum_function], + }; + let first = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + let second = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert_eq!(first, second); + } + + #[test] + fn byte_counted_opaque_buffer_does_not_require_element_layout() { + let mut function = synthetic_function("QueryOpaqueData"); + function.parameters = vec![ + RawParameter { + name: "data".into(), + typ: RawType { + base: RawBaseType::Unknown("variable native record".into()), + pointer_depth: 1, + constness: RawConstness::Mutable, + }, + direction: RawDirection::Out, + nullable: true, + reserved: false, + null_null_terminated: false, + buffer: Some(RawBuffer { + element: RawType { + base: RawBaseType::Unknown("variable native record".into()), + pointer_depth: 0, + constness: RawConstness::Unspecified, + }, + size: RawBufferSize::ByteCountParam(1), + }), + free_with: None, + }, + RawParameter { + name: "size".into(), + typ: RawType { + base: RawBaseType::Scalar(RawScalar::U32), + pointer_depth: 1, + constness: RawConstness::Mutable, + }, + direction: RawDirection::InOut, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }, + ]; + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function.clone()], + }; + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(omitted.is_empty(), "{omitted:#?}"); + assert!(output.js.contains("_bufferCount(data, 1)")); + assert!( + output + .js + .contains("DynWin32.alignedDataPointer(data, 8, true)") + ); + assert!( + output + .dts + .contains("queryOpaqueData(data: Buffer | Uint8Array | null)") + ); + assert!(output.dts.contains("readonly size: number")); + + function.parameters[0].buffer.as_mut().unwrap().size = RawBufferSize::ElementCountParam(1); + let projection = project_apis(&RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }); + assert_eq!(projection.complete_count(), 0); + assert!( + projection.omitted[0] + .reason + .contains("native buffer element") + ); + + function = synthetic_function("QueryDoublePointer"); + function.parameters = vec![ + RawParameter { + name: "data".into(), + typ: RawType { + base: RawBaseType::Scalar(RawScalar::U8), + pointer_depth: 2, + constness: RawConstness::Mutable, + }, + direction: RawDirection::Out, + nullable: true, + reserved: false, + null_null_terminated: false, + buffer: Some(RawBuffer { + element: scalar(RawScalar::U8), + size: RawBufferSize::ByteCountParam(1), + }), + free_with: None, + }, + RawParameter { + name: "size".into(), + typ: scalar(RawScalar::U32), + direction: RawDirection::In, + nullable: false, + reserved: false, + null_null_terminated: false, + buffer: None, + free_with: None, + }, + ]; + let projection = project_apis(&RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions: vec![function], + }); + assert_eq!(projection.complete_count(), 0); + assert!( + projection.omitted[0] + .reason + .contains("one data indirection") + ); + } + + fn handle_type(namespace: &str, name: &str, cleanup: &str) -> RawType { + RawType { + base: RawBaseType::Named { + namespace: namespace.into(), + name: name.into(), + kind: RawNamedKind::Handle { + cleanup: Some(cleanup.into()), + }, + }, + pointer_depth: 0, + constness: RawConstness::Unspecified, + } + } + + #[test] + fn direct_handle_ownership_uses_exact_function_evidence() { + let cases = [ + ( + "Windows.Win32.System.Memory", + "LocalAlloc", + "HLOCAL", + "LocalFree", + "localFree", + ), + ( + "Windows.Win32.System.Memory", + "GlobalAlloc", + "HGLOBAL", + "GlobalFree", + "globalFree", + ), + ( + "Windows.Win32.System.LibraryLoader", + "LoadLibraryW", + "HMODULE", + "FreeLibrary", + "freeLibrary", + ), + ( + "Windows.Win32.System.Services", + "OpenSCManagerW", + "SC_HANDLE", + "CloseServiceHandle", + "closeServiceHandle", + ), + ]; + let mut functions = Vec::new(); + for (namespace, name, handle, cleanup, _) in cases { + let mut function = synthetic_function(name); + function.namespace = namespace.into(); + function.return_type = handle_type(namespace, handle, cleanup); + functions.push(function); + } + let mut unknown = synthetic_function("MysteryAlloc"); + unknown.namespace = "Windows.Win32.System.Memory".into(); + unknown.return_type = handle_type("Windows.Win32.Foundation", "HGLOBAL", "GlobalFree"); + functions.push(unknown); + let raw = RawApis { + namespace: "Tests".into(), + class_name: "Apis".into(), + functions, + }; + let (output, omitted) = generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert_eq!(omitted.len(), 1, "{omitted:#?}"); + assert!(omitted[0].identity.ends_with("::MysteryAlloc")); + for (_, _, _, _, cleanup) in cases { + assert!(output.js.contains(&format!("returnCleanup: \"{cleanup}\""))); + } + assert_eq!(output.js.matches("successRule: \"validHandle\"").count(), 4); + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/win32/model.rs b/tools/dynwinrt-codegen/src/codegen/win32/model.rs new file mode 100644 index 00000000..29080b82 --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/win32/model.rs @@ -0,0 +1,1467 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::{BTreeMap, BTreeSet}; + +use crate::win32_metadata::{ + RawApis, RawArchitectures, RawBaseType, RawBufferSize, RawCallingConvention, RawDirection, + RawFunction, RawLayoutKind, RawNamedKind, RawNativeLayout, RawNativeLayoutSet, RawPacking, + RawScalar, RawStatusSemantics, RawStringEncoding, RawType, +}; + +use super::ir::{ + AbiType, BufferContract, CallingConvention, Cleanup, Constness, Direction, EnumDefinition, + EnumMember, EnumUnderlying, FunctionContract, NativeAggregateKind, NativeArchitectureLayout, + NativeField, NativeFieldType, NativeLayout, NativeScalar, ParameterContract, Scalar, + StringEncoding, SuccessRule, ValueType, +}; + +pub(super) fn validate_apis(raw: &RawApis) -> (Vec, Vec<(String, String)>) { + let name_counts = + raw.functions + .iter() + .fold(BTreeMap::<&str, usize>::new(), |mut counts, function| { + *counts.entry(&function.name).or_default() += 1; + counts + }); + let mut enum_namespaces = BTreeMap::>::new(); + for function in &raw.functions { + if let Ok(definitions) = enum_definitions(function) { + for definition in definitions { + enum_namespaces + .entry(definition.name) + .or_default() + .insert(definition.namespace); + } + } + } + let ambiguous_enums = enum_namespaces + .into_iter() + .filter_map(|(name, namespaces)| (namespaces.len() > 1).then_some(name)) + .collect::>(); + let mut functions = Vec::new(); + let mut omitted = Vec::new(); + for function in &raw.functions { + if name_counts + .get(function.name.as_str()) + .copied() + .unwrap_or(0) + > 1 + { + omitted.push(( + format!( + "{}.{}::{}", + function.namespace, function.container, function.name + ), + "unresolved metadata overload or architecture collision".into(), + )); + continue; + } + if enum_definitions(function).is_ok_and(|definitions| { + definitions + .iter() + .any(|definition| ambiguous_enums.contains(&definition.name)) + }) { + omitted.push(( + format!( + "{}.{}::{}", + function.namespace, function.container, function.name + ), + "referenced enum simple name is ambiguous across namespaces".into(), + )); + continue; + } + match validate_function(function) { + Ok(function) => functions.push(function), + Err(reason) => omitted.push(( + format!( + "{}.{}::{}", + function.namespace, function.container, function.name + ), + reason, + )), + } + } + (functions, omitted) +} + +pub(super) fn validate_function(raw: &RawFunction) -> Result { + let calling_convention = match raw.calling_convention { + RawCallingConvention::System => CallingConvention::System, + RawCallingConvention::Cdecl => CallingConvention::Cdecl, + RawCallingConvention::Unsupported => { + return Err("unsupported native calling convention".into()); + } + }; + if raw.variadic { + return Err("variadic flat Win32 exports are unsupported".into()); + } + if !raw.architectures.x64 || !raw.architectures.arm64 { + return Err("export is not available on both x64 and ARM64".into()); + } + validate_module(&raw.dll)?; + if raw.entry_point.is_empty() || raw.entry_point.as_bytes().contains(&0) { + return Err("entry point is empty or contains NUL".into()); + } + + let (return_type, mut return_abi, return_cleanup) = map_return(raw)?; + let return_aggregate = match &return_type { + Some(ValueType::NativeStruct { layout }) => { + return_abi = None; + Some(layout.clone()) + } + _ => None, + }; + let mut parameters = raw + .parameters + .iter() + .map(|parameter| { + if parameter.reserved && parameter.direction != RawDirection::In { + return Err(format!( + "reserved parameter `{}` must be input-only", + parameter.name + )); + } + let buffer = parameter + .buffer + .as_ref() + .map(|buffer| -> Result { + let (element_size, element_alignment) = match buffer.size { + RawBufferSize::ByteCountParam(_) + if parameter.typ.pointer_depth == 1 + || (parameter.typ.pointer_depth == 0 + && matches!( + parameter.typ.base, + RawBaseType::Named { + kind: RawNamedKind::DataPointer + | RawNamedKind::StringPointer { .. }, + .. + } + )) => + { + let alignment = map_buffer_element(&buffer.element) + .map(|(_, alignment)| alignment) + .unwrap_or(8); + (1, alignment) + } + RawBufferSize::ByteCountParam(_) => { + return Err( + "byte-sized native buffer does not have one data indirection" + .into(), + ); + } + _ => map_buffer_element(&buffer.element)?, + }; + let (count_parameter, constant_count, count_is_bytes) = match buffer.size { + RawBufferSize::ElementCountParam(index) => (Some(index), None, false), + RawBufferSize::ByteCountParam(index) => (Some(index), None, true), + RawBufferSize::Constant(count) => (None, Some(count), false), + RawBufferSize::Unknown => { + return Err("native buffer has no complete size contract".into()); + } + }; + Ok(BufferContract { + count_parameter, + constant_count, + count_is_bytes, + element_size, + element_alignment, + }) + }) + .transpose()?; + let reserved_pointer = parameter.reserved + && (parameter.typ.pointer_depth > 0 + || matches!( + parameter.typ.base, + RawBaseType::Named { + kind: + RawNamedKind::DataPointer + | RawNamedKind::StringPointer { .. } + | RawNamedKind::FunctionPointer + | RawNamedKind::ComInterface { .. }, + .. + } + )); + let nullable_void = parameter.nullable + && parameter.direction == RawDirection::In + && parameter.typ.pointer_depth == 1 + && matches!(parameter.typ.base, RawBaseType::Void); + let (typ, abi, mut cleanup) = if nullable_void { + (ValueType::NullPointer, AbiType::Pointer, Cleanup::None) + } else if buffer.is_some() || reserved_pointer { + ( + ValueType::DataPointer, + AbiType::Pointer, + if reserved_pointer { + Cleanup::None + } else { + parameter + .free_with + .as_deref() + .map(parse_cleanup) + .transpose()? + .unwrap_or(Cleanup::None) + }, + ) + } else { + map_parameter_type( + ¶meter.typ, + parameter.direction, + parameter.free_with.as_deref(), + )? + }; + if matches!(&typ, ValueType::Handle { .. }) + && parameter.typ.pointer_depth == 1 + && matches!(parameter.direction, RawDirection::Out | RawDirection::InOut) + { + if cleanup == Cleanup::None { + cleanup = known_handle_output_cleanup(raw, parameter, &typ).ok_or_else(|| { + format!( + "handle output `{}` has no function-specific ownership and cleanup contract", + parameter.name + ) + })?; + } + if parameter.direction == RawDirection::InOut { + return Err(format!( + "owning InOut handle `{}` is unsupported until replacement ownership is modeled", + parameter.name + )); + } + } + let consumes_resource = known_consuming_handle_input(raw, parameter, &typ); + let resource_cleanup = handle_resource_cleanup(¶meter.typ); + let supported_double_null = matches!( + (&typ, parameter.direction), + (ValueType::StringPointer(_), RawDirection::In) + | (ValueType::DataPointer, RawDirection::Out) + ) && (matches!(&typ, ValueType::StringPointer(_)) || buffer.is_some()); + if parameter.null_null_terminated && !supported_double_null { + return Err(format!( + "NullNullTerminated parameter `{}` must be an input string pointer or an explicit output buffer", + parameter.name + )); + } + Ok(ParameterContract { + name: parameter.name.clone(), + native_name: raw_native_name(¶meter.typ), + nullable: (parameter.nullable || reserved_pointer) + && matches!( + &typ, + ValueType::Handle { .. } + | ValueType::DataPointer + | ValueType::StringPointer(_) + | ValueType::FunctionPointer + | ValueType::NativeStructPointer { .. } + | ValueType::NativeUnionPointer { .. } + | ValueType::ScalarPointer { .. } + | ValueType::GuidPointer + | ValueType::NullPointer + | ValueType::ComInterface { .. } + | ValueType::StringPointerPointer(_) + ), + typ, + abi, + pointer_depth: parameter.typ.pointer_depth, + constness: map_constness(parameter.typ.constness), + direction: map_direction(parameter.direction), + reserved: parameter.reserved, + null_null_terminated: parameter.null_null_terminated, + cleanup, + consumes_resource, + resource_cleanup, + buffer, + }) + }) + .collect::, String>>()?; + + for parameter in &mut parameters { + if known_mutable_in_place_string(raw, parameter) { + parameter.direction = Direction::In; + } + } + validate_buffers(¶meters)?; + if let Some(parameter) = parameters.iter().find(|parameter| { + parameter.pointer_depth == 0 + && parameter.direction == Direction::Out + && parameter.buffer.is_none() + && matches!( + parameter.typ, + ValueType::Scalar(_) | ValueType::Enum { .. } | ValueType::Handle { .. } + ) + }) { + return Err(format!( + "by-value parameter `{}` cannot use an output-only contract", + parameter.name + )); + } + let enums = enum_definitions(raw)?; + let success_rule = match raw.return_status { + RawStatusSemantics::ZeroIsSuccess => SuccessRule::ReturnZero, + RawStatusSemantics::SignedNonNegativeIsSuccess => SuccessRule::SignedNonNegative, + RawStatusSemantics::None + if matches!(&return_type, Some(ValueType::Scalar(Scalar::Bool32))) => + { + SuccessRule::ReturnNonZero + } + RawStatusSemantics::None + if return_cleanup != Cleanup::None + && matches!(&return_type, Some(ValueType::Handle { .. })) => + { + SuccessRule::ReturnValidHandle + } + RawStatusSemantics::None if return_cleanup != Cleanup::None => SuccessRule::ReturnNonNull, + RawStatusSemantics::None => SuccessRule::Always, + }; + + Ok(FunctionContract { + namespace: raw.namespace.clone(), + container: raw.container.clone(), + name: raw.name.clone(), + dll: raw.dll.clone(), + entry_point: raw.entry_point.clone(), + parameters, + return_type, + return_abi, + return_aggregate, + return_native_name: raw_native_name(&raw.return_type), + return_pointer_depth: raw.return_type.pointer_depth, + return_constness: map_constness(raw.return_type.constness), + return_cleanup, + return_is_status: raw.return_status != RawStatusSemantics::None, + success_rule, + capture_last_error: raw.supports_last_error, + calling_convention, + enums, + }) +} + +fn known_mutable_in_place_string(function: &RawFunction, parameter: &ParameterContract) -> bool { + parameter.name == "lpCommandLine" + && matches!(parameter.typ, ValueType::StringPointer(_)) + && matches!( + function.name.as_str(), + "CreateProcessA" + | "CreateProcessW" + | "CreateProcessAsUserA" + | "CreateProcessAsUserW" + | "CreateProcessWithLogonW" + | "CreateProcessWithTokenW" + ) +} + +fn map_return( + function: &RawFunction, +) -> Result<(Option, Option, Cleanup), String> { + let raw = &function.return_type; + if matches!(raw.base, RawBaseType::Void) && raw.pointer_depth == 0 { + return Ok((None, None, Cleanup::None)); + } + if raw.pointer_depth > 0 + && let Some(cleanup) = function.return_free_with.as_deref() + { + return Ok(( + Some(ValueType::DataPointer), + Some(AbiType::Pointer), + parse_cleanup(cleanup)?, + )); + } + if let RawBaseType::Named { + namespace, + name, + kind: RawNamedKind::Handle { cleanup }, + } = &raw.base + && raw.pointer_depth == 0 + { + let cleanup = if let Some(cleanup) = function.return_free_with.as_deref() { + parse_cleanup(cleanup)? + } else if cleanup.is_none() || known_borrowed_handle_return(function, name) { + Cleanup::None + } else if let Some(cleanup) = known_owned_handle_return(function, name) { + cleanup + } else { + return Err(format!( + "direct handle return `{name}` has no verified ownership and success-sentinel contract" + )); + }; + return Ok(( + Some(ValueType::Handle { + namespace: namespace.clone(), + name: name.clone(), + }), + Some(AbiType::Handle), + cleanup, + )); + } + if let RawBaseType::Named { + kind: RawNamedKind::FunctionPointer, + .. + } = &raw.base + && raw.pointer_depth == 0 + { + return Ok(( + Some(ValueType::FunctionPointer), + Some(AbiType::FunctionPointer), + Cleanup::None, + )); + } + let (typ, abi, cleanup) = + map_type(raw, RawDirection::In, function.return_free_with.as_deref())?; + if matches!( + typ, + ValueType::DataPointer + | ValueType::StringPointer(_) + | ValueType::NativeStructPointer { .. } + | ValueType::NativeUnionPointer { .. } + | ValueType::ScalarPointer { .. } + | ValueType::GuidPointer + | ValueType::NullPointer + | ValueType::ComInterface { .. } + | ValueType::StringPointerPointer(_) + ) && cleanup == Cleanup::None + { + return Err("pointer return lifetime and ownership are not modeled".into()); + } + Ok((Some(typ), Some(abi), cleanup)) +} + +fn map_parameter_type( + raw: &RawType, + direction: RawDirection, + free_with: Option<&str>, +) -> Result<(ValueType, AbiType, Cleanup), String> { + let (typ, abi, mut cleanup) = map_type(raw, direction, free_with)?; + if direction == RawDirection::In { + cleanup = Cleanup::None; + } + Ok((typ, abi, cleanup)) +} + +fn map_type( + raw: &RawType, + direction: RawDirection, + free_with: Option<&str>, +) -> Result<(ValueType, AbiType, Cleanup), String> { + match (&raw.base, raw.pointer_depth) { + (RawBaseType::Scalar(scalar), 0) => { + let scalar = map_scalar(*scalar); + Ok((ValueType::Scalar(scalar), scalar_abi(scalar), Cleanup::None)) + } + (RawBaseType::Scalar(scalar), 1) + if matches!(direction, RawDirection::Out | RawDirection::InOut) => + { + let scalar = map_scalar(*scalar); + Ok((ValueType::Scalar(scalar), scalar_abi(scalar), Cleanup::None)) + } + (RawBaseType::Scalar(scalar), 1) if direction == RawDirection::In => { + let scalar = map_scalar(*scalar); + Ok(( + ValueType::ScalarPointer { scalar }, + AbiType::Pointer, + Cleanup::None, + )) + } + ( + RawBaseType::Named { + namespace, + name, + kind: + RawNamedKind::Enum { + underlying, + members, + is_flags, + }, + }, + pointer_depth, + ) if pointer_depth == 0 + || (pointer_depth == 1 + && matches!(direction, RawDirection::Out | RawDirection::InOut)) => + { + let underlying = map_enum_underlying(*underlying)?; + let _ = (members, is_flags); + Ok(( + ValueType::Enum { + namespace: namespace.clone(), + name: name.clone(), + underlying, + }, + enum_abi(underlying), + Cleanup::None, + )) + } + ( + RawBaseType::Named { + namespace, + name, + kind: RawNamedKind::Handle { cleanup: _ }, + }, + pointer_depth, + ) if pointer_depth == 0 + || (pointer_depth == 1 + && matches!(direction, RawDirection::Out | RawDirection::InOut)) => + { + let cleanup = if pointer_depth == 0 || direction == RawDirection::In { + Cleanup::None + } else { + free_with + .map(parse_cleanup) + .transpose()? + .unwrap_or(Cleanup::None) + }; + Ok(( + ValueType::Handle { + namespace: namespace.clone(), + name: name.clone(), + }, + AbiType::Handle, + cleanup, + )) + } + + ( + RawBaseType::Named { + kind: RawNamedKind::DataPointer, + .. + }, + 0, + ) => Ok(( + ValueType::DataPointer, + AbiType::Pointer, + free_with + .map(parse_cleanup) + .transpose()? + .unwrap_or(Cleanup::None), + )), + ( + RawBaseType::Named { + kind: RawNamedKind::StringPointer { encoding }, + .. + }, + 0, + ) => Ok(( + ValueType::StringPointer(match encoding { + RawStringEncoding::Utf16 => StringEncoding::Wide, + RawStringEncoding::Ansi => StringEncoding::Ansi, + }), + AbiType::Pointer, + free_with + .map(parse_cleanup) + .transpose()? + .unwrap_or(Cleanup::None), + )), + ( + RawBaseType::Named { + kind: RawNamedKind::FunctionPointer, + .. + }, + 0, + ) if direction == RawDirection::In => { + Err("managed callback thunks are not implemented".into()) + } + ( + RawBaseType::Named { + kind: RawNamedKind::FunctionPointer, + .. + }, + 0, + ) => Ok(( + ValueType::FunctionPointer, + AbiType::FunctionPointer, + Cleanup::None, + )), + ( + RawBaseType::Named { + namespace, + name, + kind: RawNamedKind::NativeStruct { layout }, + }, + 1, + ) => { + let layout = validate_native_layout(namespace, name, layout)?; + let typ = match layout.kind { + NativeAggregateKind::Struct => ValueType::NativeStructPointer { layout }, + NativeAggregateKind::Union => ValueType::NativeUnionPointer { layout }, + }; + Ok((typ, AbiType::Pointer, Cleanup::None)) + } + ( + RawBaseType::Named { + kind: RawNamedKind::NativeStruct { .. }, + .. + }, + 0, + ) => { + let RawBaseType::Named { + namespace, + name, + kind: RawNamedKind::NativeStruct { layout }, + } = &raw.base + else { + unreachable!() + }; + let layout = validate_native_layout(namespace, name, layout)?; + if layout.kind != NativeAggregateKind::Struct + || !native_layout_supports_by_value(&layout) + { + return Err("by-value native aggregate contains unsupported union fields".into()); + } + Ok(( + ValueType::NativeStruct { layout }, + AbiType::Pointer, + Cleanup::None, + )) + } + ( + RawBaseType::Named { + kind: RawNamedKind::Guid, + .. + }, + 1, + ) => Ok((ValueType::GuidPointer, AbiType::Pointer, Cleanup::None)), + ( + RawBaseType::Named { + name, + kind: RawNamedKind::ComInterface { iid }, + .. + }, + 0, + ) if direction == RawDirection::In => Ok(( + ValueType::ComInterface { + name: name.clone(), + iid: iid.clone(), + }, + AbiType::Pointer, + Cleanup::None, + )), + ( + RawBaseType::Named { + kind: RawNamedKind::StringPointer { encoding }, + .. + }, + 1, + ) if direction == RawDirection::In => Ok(( + ValueType::StringPointerPointer(match encoding { + RawStringEncoding::Utf16 => StringEncoding::Wide, + RawStringEncoding::Ansi => StringEncoding::Ansi, + }), + AbiType::Pointer, + Cleanup::None, + )), + ( + RawBaseType::Named { + namespace, + name, + kind, + }, + _, + ) => Err(format!( + "unsupported {} {namespace}.{name} pointer depth {}", + raw_named_kind_label(kind), + raw.pointer_depth + )), + (RawBaseType::Unknown(reason), _) => Err(format!("native type is unknown: {reason}")), + (RawBaseType::Void, _) => Err("void is not a parameter or pointer value".into()), + (RawBaseType::Scalar(_), _) => Err(format!( + "unsupported scalar pointer depth {}", + raw.pointer_depth + )), + } +} + +fn raw_named_kind_label(kind: &RawNamedKind) -> &'static str { + match kind { + RawNamedKind::Enum { .. } => "enum", + RawNamedKind::Handle { .. } => "handle", + RawNamedKind::StringPointer { .. } => "string pointer", + RawNamedKind::DataPointer => "data pointer", + RawNamedKind::FunctionPointer => "function pointer", + RawNamedKind::Guid => "GUID", + RawNamedKind::ComInterface { .. } => "COM interface", + RawNamedKind::NativeStruct { .. } => "native aggregate", + RawNamedKind::Unknown => "unknown native type", + } +} + +fn native_layout_supports_by_value(layout: &NativeLayout) -> bool { + fn architecture(layout: &NativeArchitectureLayout) -> bool { + layout.fields.iter().all(|field| match &field.typ { + NativeFieldType::Union { .. } => false, + NativeFieldType::Struct { + layout, + by_value_compatible, + .. + } => *by_value_compatible && architecture(layout), + NativeFieldType::Scalar(_) | NativeFieldType::Guid => true, + NativeFieldType::Pointer | NativeFieldType::Handle { .. } => false, + }) + } + layout.by_value_compatible + && architecture(&layout.x86) + && architecture(&layout.x64) + && architecture(&layout.arm64) +} + +#[derive(Debug, Clone, Copy)] +enum LayoutArchitecture { + X86, + X64, + Arm64, +} + +impl LayoutArchitecture { + const fn pointer_size(self) -> usize { + match self { + Self::X86 => 4, + Self::X64 | Self::Arm64 => 8, + } + } + + const fn supports(self, architectures: RawArchitectures) -> bool { + match self { + Self::X86 => architectures.x86, + Self::X64 => architectures.x64, + Self::Arm64 => architectures.arm64, + } + } +} + +fn validate_native_layout( + namespace: &str, + name: &str, + raw: &RawNativeLayoutSet, +) -> Result { + if raw.recursive { + return Err(format!( + "recursive by-value native layout {namespace}.{name}" + )); + } + let mut visiting = BTreeSet::new(); + let x86 = compute_native_layout(raw, LayoutArchitecture::X86, &mut visiting)?; + let x64 = compute_native_layout(raw, LayoutArchitecture::X64, &mut visiting)?; + let arm64 = compute_native_layout(raw, LayoutArchitecture::Arm64, &mut visiting)?; + let kind = raw + .variants + .first() + .map(|variant| match variant.kind { + RawLayoutKind::Sequential => NativeAggregateKind::Struct, + RawLayoutKind::Union => NativeAggregateKind::Union, + RawLayoutKind::Unknown => NativeAggregateKind::Struct, + }) + .ok_or_else(|| format!("{namespace}.{name} has no native layout variants"))?; + if raw.variants.iter().any(|variant| { + matches!( + (kind, variant.kind), + (NativeAggregateKind::Struct, RawLayoutKind::Union) + | (NativeAggregateKind::Union, RawLayoutKind::Sequential) + ) + }) { + return Err(format!( + "architecture variants disagree on aggregate kind for {namespace}.{name}" + )); + } + Ok(NativeLayout { + namespace: namespace.into(), + name: name.into(), + kind, + by_value_compatible: raw.variants.iter().all(|variant| { + variant.packing == RawPacking::Default + && variant.forced_alignment.is_none() + && variant.kind == RawLayoutKind::Sequential + }) && [&x86, &x64, &arm64].into_iter().all(|layout| { + layout.fields.iter().all(|field| { + !matches!( + field.typ, + NativeFieldType::Pointer | NativeFieldType::Handle { .. } + ) + }) + }), + x86, + x64, + arm64, + }) +} + +fn compute_native_layout( + raw: &RawNativeLayoutSet, + architecture: LayoutArchitecture, + visiting: &mut BTreeSet, +) -> Result { + let candidates = raw + .variants + .iter() + .filter(|variant| architecture.supports(variant.architectures)) + .collect::>(); + let [variant] = candidates.as_slice() else { + return Err(if candidates.is_empty() { + format!("missing {architecture:?} native layout facts") + } else { + format!("ambiguous {architecture:?} native layout facts") + }); + }; + compute_native_layout_variant(variant, architecture, visiting) +} + +fn compute_native_layout_variant( + raw: &RawNativeLayout, + architecture: LayoutArchitecture, + visiting: &mut BTreeSet, +) -> Result { + if raw.kind == RawLayoutKind::Unknown || raw.fields.is_empty() { + return Err("native aggregate has unknown or empty layout".into()); + } + let packing = match raw.packing { + RawPacking::Default => 8usize, + RawPacking::Explicit(value) if value.is_power_of_two() => usize::from(value), + RawPacking::Explicit(value) => { + return Err(format!("invalid native packing {value}")); + } + }; + let mut fields = Vec::with_capacity(raw.fields.len()); + let mut cursor = 0usize; + let mut aggregate_alignment = 1usize; + for raw_field in &raw.fields { + if raw_field.bitfield { + return Err(format!( + "native bitfield `{}` requires dedicated accessors", + raw_field.name + )); + } + if raw_field.flexible_array { + return Err(format!( + "flexible native array `{}` requires a variable-size contract", + raw_field.name + )); + } + if let Some(forced_alignment) = raw.forced_alignment { + if !forced_alignment.is_power_of_two() || forced_alignment > 8 { + return Err(format!( + "unsupported forced native alignment {forced_alignment}" + )); + } + aggregate_alignment = aggregate_alignment.max(forced_alignment); + } + let (typ, element_size, element_alignment) = + native_field_type(&raw_field.typ, architecture, visiting)?; + let count = raw_field.fixed_count.unwrap_or(1); + let count_u32 = u32::try_from(count) + .map_err(|_| format!("fixed array `{}` exceeds u32", raw_field.name))?; + if count_u32 == 0 { + return Err(format!("fixed array `{}` has zero length", raw_field.name)); + } + let field_size = element_size + .checked_mul(count) + .ok_or_else(|| format!("field `{}` size overflows", raw_field.name))?; + let effective_alignment = element_alignment.min(packing); + aggregate_alignment = aggregate_alignment.max(effective_alignment); + let offset = if raw.kind == RawLayoutKind::Union { + 0 + } else { + align_up(cursor, effective_alignment)? + }; + cursor = cursor.max( + offset + .checked_add(field_size) + .ok_or_else(|| format!("field `{}` end overflows", raw_field.name))?, + ); + fields.push(NativeField { + name: raw_field.name.clone(), + offset, + count: count_u32, + typ, + }); + } + let natural_size = align_up(cursor, aggregate_alignment)?; + let size = match raw.declared_size { + Some(size) if size < cursor || size % aggregate_alignment != 0 => { + return Err(format!( + "declared aggregate size {size} cannot contain {cursor} bytes at alignment {aggregate_alignment}" + )); + } + Some(size) => size, + None => natural_size, + }; + Ok(NativeArchitectureLayout { + size, + alignment: aggregate_alignment, + fields, + }) +} + +fn native_field_type( + raw: &RawType, + architecture: LayoutArchitecture, + visiting: &mut BTreeSet, +) -> Result<(NativeFieldType, usize, usize), String> { + if let RawBaseType::Named { + name, + kind: RawNamedKind::DataPointer, + .. + } = &raw.base + && raw.pointer_depth == 0 + && (name == "SECURITY_ATTRIBUTES.lpSecurityDescriptor" + || name.starts_with("STARTUPINFOA.") + || name.starts_with("STARTUPINFOW.")) + { + let width = architecture.pointer_size(); + return Ok((NativeFieldType::Pointer, width, width)); + } + if let RawBaseType::Named { + name, + kind: RawNamedKind::Handle { cleanup }, + .. + } = &raw.base + && raw.pointer_depth == 0 + && (matches!( + name.as_str(), + "PROCESS_INFORMATION.hProcess" | "PROCESS_INFORMATION.hThread" + ) || name.starts_with("STARTUPINFOA.") + || name.starts_with("STARTUPINFOW.")) + { + let cleanup = cleanup + .as_deref() + .map(parse_cleanup) + .transpose()? + .unwrap_or(Cleanup::None); + let width = architecture.pointer_size(); + return Ok((NativeFieldType::Handle { cleanup }, width, width)); + } + if raw.pointer_depth > 0 + || matches!( + raw.base, + RawBaseType::Named { + kind: RawNamedKind::Handle { .. } + | RawNamedKind::DataPointer + | RawNamedKind::StringPointer { .. } + | RawNamedKind::FunctionPointer + | RawNamedKind::ComInterface { .. }, + .. + } + ) + { + return Err("pointer-bearing native aggregate requires retained pointee ownership".into()); + } + match &raw.base { + RawBaseType::Scalar(scalar) => { + let scalar = native_scalar(*scalar)?; + let (size, alignment) = native_scalar_size_alignment(scalar, architecture); + Ok((NativeFieldType::Scalar(scalar), size, alignment)) + } + RawBaseType::Named { + kind: RawNamedKind::Guid, + .. + } => Ok((NativeFieldType::Guid, 16, 4)), + RawBaseType::Named { + namespace, + name, + kind: RawNamedKind::Enum { underlying, .. }, + } => { + let _ = (namespace, name); + let scalar = native_scalar(*underlying)?; + let (size, alignment) = native_scalar_size_alignment(scalar, architecture); + Ok((NativeFieldType::Scalar(scalar), size, alignment)) + } + RawBaseType::Named { + namespace, + name, + kind: RawNamedKind::NativeStruct { layout }, + } => { + let identity = format!("{namespace}.{name}"); + if !visiting.insert(identity.clone()) { + return Err(format!("recursive nested native layout {identity}")); + } + let nested = compute_native_layout(layout, architecture, visiting)?; + visiting.remove(&identity); + let typ = match layout.variants.first().map(|variant| variant.kind) { + Some(RawLayoutKind::Sequential) => NativeFieldType::Struct { + name: identity, + layout: Box::new(nested.clone()), + by_value_compatible: false, + }, + Some(RawLayoutKind::Union) => NativeFieldType::Union { + name: identity, + layout: Box::new(nested.clone()), + by_value_compatible: false, + }, + _ => return Err("nested native aggregate has unknown layout kind".into()), + }; + Ok((typ, nested.size, nested.alignment)) + } + RawBaseType::Void => Err("void native aggregate field".into()), + RawBaseType::Unknown(reason) => Err(format!("unknown aggregate field type: {reason}")), + RawBaseType::Named { + namespace, + name, + kind: RawNamedKind::Unknown, + } => Err(format!( + "unsupported aggregate field type {namespace}.{name}" + )), + RawBaseType::Named { kind, .. } => { + Err(format!("unsupported aggregate field type {kind:?}")) + } + } +} + +fn native_scalar(scalar: RawScalar) -> Result { + Ok(match scalar { + RawScalar::I8 => NativeScalar::I8, + RawScalar::Bool8 | RawScalar::U8 => NativeScalar::U8, + RawScalar::I16 => NativeScalar::I16, + RawScalar::U16 | RawScalar::Char16 => NativeScalar::U16, + RawScalar::I32 | RawScalar::Bool32 => NativeScalar::I32, + RawScalar::U32 => NativeScalar::U32, + RawScalar::I64 => NativeScalar::I64, + RawScalar::U64 => NativeScalar::U64, + RawScalar::F32 => NativeScalar::F32, + RawScalar::F64 => NativeScalar::F64, + RawScalar::NativeIsize => NativeScalar::NativeIsize, + RawScalar::NativeUsize => NativeScalar::NativeUsize, + }) +} + +fn native_scalar_size_alignment( + scalar: NativeScalar, + architecture: LayoutArchitecture, +) -> (usize, usize) { + match scalar { + NativeScalar::I8 | NativeScalar::U8 => (1, 1), + NativeScalar::I16 | NativeScalar::U16 => (2, 2), + NativeScalar::I32 | NativeScalar::U32 | NativeScalar::F32 => (4, 4), + NativeScalar::I64 | NativeScalar::U64 | NativeScalar::F64 => (8, 8), + NativeScalar::NativeIsize | NativeScalar::NativeUsize => { + let width = architecture.pointer_size(); + (width, width) + } + } +} + +fn align_up(value: usize, alignment: usize) -> Result { + value + .checked_add(alignment - 1) + .map(|value| value & !(alignment - 1)) + .ok_or_else(|| "native layout alignment overflow".into()) +} + +fn known_handle_output_cleanup( + function: &RawFunction, + parameter: &crate::win32_metadata::RawParameter, + typ: &ValueType, +) -> Option { + let ValueType::Handle { + namespace, name, .. + } = typ + else { + return None; + }; + if namespace == "Windows.Win32.System.Registry" + && name == "HKEY" + && parameter.direction == RawDirection::Out + && matches!( + function.name.as_str(), + "RegConnectRegistryA" + | "RegConnectRegistryW" + | "RegConnectRegistryExA" + | "RegConnectRegistryExW" + | "RegCreateKeyA" + | "RegCreateKeyW" + | "RegCreateKeyExA" + | "RegCreateKeyExW" + | "RegCreateKeyTransactedA" + | "RegCreateKeyTransactedW" + | "RegLoadAppKeyA" + | "RegLoadAppKeyW" + | "RegOpenCurrentUser" + | "RegOpenKeyA" + | "RegOpenKeyW" + | "RegOpenKeyExA" + | "RegOpenKeyExW" + | "RegOpenKeyTransactedA" + | "RegOpenKeyTransactedW" + | "RegOpenUserClassesRoot" + ) + { + Some(Cleanup::RegCloseKey) + } else if namespace == "Windows.Win32.Foundation" + && name == "HANDLE" + && function.namespace == "Windows.Win32.System.Pipes" + && function.name == "CreatePipe" + && parameter.direction == RawDirection::Out + && matches!(parameter.name.as_str(), "hReadPipe" | "hWritePipe") + { + Some(Cleanup::CloseHandle) + } else { + None + } +} + +fn known_borrowed_handle_return(function: &RawFunction, handle_name: &str) -> bool { + handle_name == "HMODULE" + && function.namespace == "Windows.Win32.System.LibraryLoader" + && matches!( + function.name.as_str(), + "GetModuleHandleA" | "GetModuleHandleW" + ) +} + +fn known_owned_handle_return(function: &RawFunction, handle_name: &str) -> Option { + match ( + function.namespace.as_str(), + function.name.as_str(), + handle_name, + ) { + ( + _, + "CreateFileA" + | "CreateFileW" + | "CreateEventA" + | "CreateEventW" + | "CreateEventExA" + | "CreateEventExW" + | "OpenEventA" + | "OpenEventW" + | "CreateMutexA" + | "CreateMutexW" + | "CreateMutexExA" + | "CreateMutexExW" + | "OpenMutexA" + | "OpenMutexW" + | "CreateSemaphoreA" + | "CreateSemaphoreW" + | "CreateSemaphoreExA" + | "CreateSemaphoreExW" + | "OpenSemaphoreA" + | "OpenSemaphoreW" + | "CreateWaitableTimerA" + | "CreateWaitableTimerW" + | "CreateWaitableTimerExA" + | "CreateWaitableTimerExW" + | "OpenWaitableTimerA" + | "OpenWaitableTimerW" + | "OpenProcess" + | "OpenThread" + | "CreateNamedPipeA" + | "CreateNamedPipeW" + | "CreateJobObjectA" + | "CreateJobObjectW" + | "OpenJobObjectA" + | "OpenJobObjectW", + "HANDLE", + ) => Some(Cleanup::CloseHandle), + ("Windows.Win32.System.Memory", "LocalAlloc", "HLOCAL") => Some(Cleanup::LocalFree), + ("Windows.Win32.System.Memory", "GlobalAlloc", "HGLOBAL") => Some(Cleanup::GlobalFree), + ( + "Windows.Win32.System.LibraryLoader", + "LoadLibraryA" | "LoadLibraryW" | "LoadLibraryExA" | "LoadLibraryExW", + "HMODULE", + ) => Some(Cleanup::FreeLibrary), + ( + "Windows.Win32.System.Services", + "OpenSCManagerA" | "OpenSCManagerW" | "OpenServiceA" | "OpenServiceW" + | "CreateServiceA" | "CreateServiceW", + "SC_HANDLE", + ) => Some(Cleanup::CloseServiceHandle), + _ => None, + } +} + +fn known_consuming_handle_input( + function: &RawFunction, + parameter: &crate::win32_metadata::RawParameter, + typ: &ValueType, +) -> bool { + if parameter.direction != RawDirection::In || parameter.typ.pointer_depth != 0 { + return false; + } + + match typ { + ValueType::Handle { name, .. } if name == "HKEY" => function.entry_point == "RegCloseKey", + ValueType::Handle { name, .. } if name == "HANDLE" => function.entry_point == "CloseHandle", + ValueType::Handle { name, .. } if name == "HMODULE" => { + function.entry_point == "FreeLibrary" + } + ValueType::Handle { name, .. } if name == "SC_HANDLE" => { + function.entry_point == "CloseServiceHandle" + } + _ => false, + } +} + +fn handle_resource_cleanup(raw: &RawType) -> Cleanup { + let RawBaseType::Named { + kind: RawNamedKind::Handle { cleanup }, + .. + } = &raw.base + else { + return Cleanup::None; + }; + cleanup + .as_deref() + .and_then(|cleanup| parse_cleanup(cleanup).ok()) + .unwrap_or(Cleanup::None) +} + +fn validate_buffers(parameters: &[ParameterContract]) -> Result<(), String> { + for (index, parameter) in parameters.iter().enumerate() { + let Some(buffer) = ¶meter.buffer else { + if matches!( + parameter.typ, + ValueType::DataPointer | ValueType::StringPointer(_) + ) && parameter.direction != Direction::In + { + return Err(format!( + "writable pointer parameter `{}` has no size relationship", + parameter.name + )); + } + continue; + }; + if let Some(count_index) = buffer.count_parameter { + let count = parameters.get(count_index).ok_or_else(|| { + format!( + "buffer `{}` references missing count parameter {count_index}", + parameter.name + ) + })?; + if count_index == index { + return Err(format!("buffer `{}` counts itself", parameter.name)); + } + if !matches!( + count.typ, + ValueType::Scalar( + Scalar::U16 + | Scalar::I16 + | Scalar::U32 + | Scalar::I32 + | Scalar::U64 + | Scalar::I64 + ) + ) { + return Err(format!( + "buffer `{}` count parameter `{}` is not an integer scalar", + parameter.name, count.name + )); + } + } + } + Ok(()) +} + +pub(super) fn enum_definitions(raw: &RawFunction) -> Result, String> { + let mut definitions = BTreeMap::<(String, String), EnumDefinition>::new(); + for parameter in &raw.parameters { + collect_raw_enum(¶meter.typ, &mut definitions)?; + if let Some(buffer) = ¶meter.buffer { + collect_raw_enum(&buffer.element, &mut definitions)?; + } + } + collect_raw_enum(&raw.return_type, &mut definitions)?; + Ok(definitions.into_values().collect()) +} + +fn collect_raw_enum( + raw: &RawType, + definitions: &mut BTreeMap<(String, String), EnumDefinition>, +) -> Result<(), String> { + let RawBaseType::Named { + namespace, + name, + kind: + RawNamedKind::Enum { + underlying, + members, + is_flags, + }, + } = &raw.base + else { + return Ok(()); + }; + let definition = EnumDefinition { + namespace: namespace.clone(), + name: name.clone(), + underlying: map_enum_underlying(*underlying)?, + members: members + .iter() + .map(|member| EnumMember { + name: member.name.clone(), + value: member.value, + }) + .collect(), + is_flags: *is_flags, + }; + if let Some(existing) = + definitions.insert((namespace.clone(), name.clone()), definition.clone()) + && existing != definition + { + return Err(format!("enum metadata disagrees for {namespace}.{name}")); + } + Ok(()) +} + +fn map_direction(direction: RawDirection) -> Direction { + match direction { + RawDirection::In => Direction::In, + RawDirection::Out => Direction::Out, + RawDirection::InOut => Direction::InOut, + } +} + +fn map_constness(constness: crate::win32_metadata::RawConstness) -> Constness { + match constness { + crate::win32_metadata::RawConstness::Const => Constness::Const, + crate::win32_metadata::RawConstness::Mutable => Constness::Mutable, + crate::win32_metadata::RawConstness::Unspecified => Constness::Unspecified, + crate::win32_metadata::RawConstness::Mixed => Constness::Mixed, + } +} + +fn raw_native_name(raw: &RawType) -> Option<(String, String)> { + match &raw.base { + RawBaseType::Named { + namespace, name, .. + } => Some((namespace.clone(), name.clone())), + RawBaseType::Void | RawBaseType::Scalar(_) | RawBaseType::Unknown(_) => None, + } +} + +fn map_scalar(scalar: RawScalar) -> Scalar { + match scalar { + RawScalar::Bool8 => Scalar::Bool8, + RawScalar::Bool32 => Scalar::Bool32, + RawScalar::I8 => Scalar::I8, + RawScalar::U8 => Scalar::U8, + RawScalar::I16 => Scalar::I16, + RawScalar::U16 | RawScalar::Char16 => Scalar::U16, + RawScalar::I32 => Scalar::I32, + RawScalar::U32 => Scalar::U32, + RawScalar::I64 => Scalar::I64, + RawScalar::U64 => Scalar::U64, + RawScalar::F32 => Scalar::F32, + RawScalar::F64 => Scalar::F64, + RawScalar::NativeIsize => Scalar::NativeIsize, + RawScalar::NativeUsize => Scalar::NativeUsize, + } +} + +fn map_enum_underlying(scalar: RawScalar) -> Result { + match scalar { + RawScalar::I8 => Ok(EnumUnderlying::I8), + RawScalar::U8 => Ok(EnumUnderlying::U8), + RawScalar::I16 => Ok(EnumUnderlying::I16), + RawScalar::U16 => Ok(EnumUnderlying::U16), + RawScalar::I32 => Ok(EnumUnderlying::I32), + RawScalar::U32 => Ok(EnumUnderlying::U32), + RawScalar::I64 + | RawScalar::U64 + | RawScalar::NativeIsize + | RawScalar::NativeUsize + | RawScalar::F32 + | RawScalar::F64 + | RawScalar::Char16 + | RawScalar::Bool8 + | RawScalar::Bool32 => { + Err("enum underlying type is not representable as a JS number enum".into()) + } + } +} + +fn scalar_abi(scalar: Scalar) -> AbiType { + match scalar { + Scalar::Bool8 => AbiType::U8, + Scalar::Bool32 => AbiType::Bool32, + Scalar::I8 => AbiType::I8, + Scalar::U8 => AbiType::U8, + Scalar::I16 => AbiType::I16, + Scalar::U16 => AbiType::U16, + Scalar::I32 => AbiType::I32, + Scalar::U32 => AbiType::U32, + Scalar::I64 => AbiType::I64, + Scalar::U64 => AbiType::U64, + Scalar::F32 => AbiType::F32, + Scalar::F64 => AbiType::F64, + Scalar::NativeIsize => AbiType::I64, + Scalar::NativeUsize => AbiType::U64, + } +} + +fn enum_abi(underlying: EnumUnderlying) -> AbiType { + match underlying { + EnumUnderlying::I8 => AbiType::I8, + EnumUnderlying::U8 => AbiType::U8, + EnumUnderlying::I16 => AbiType::I16, + EnumUnderlying::U16 => AbiType::U16, + EnumUnderlying::I32 => AbiType::I32, + EnumUnderlying::U32 => AbiType::U32, + } +} + +fn map_buffer_element(raw: &RawType) -> Result<(usize, usize), String> { + match (&raw.base, raw.pointer_depth) { + (RawBaseType::Scalar(scalar), 0) => { + let size = element_size(*scalar); + Ok((size, size.min(8))) + } + ( + RawBaseType::Named { + namespace, + name, + kind: RawNamedKind::NativeStruct { layout }, + }, + 0, + ) => { + let layout = validate_native_layout(namespace, name, layout)?; + if layout.x64.size != layout.arm64.size + || layout.x64.alignment != layout.arm64.alignment + { + return Err("native buffer element layout differs between x64 and ARM64".into()); + } + Ok((layout.x64.size, layout.x64.alignment)) + } + ( + RawBaseType::Named { + kind: RawNamedKind::Guid, + .. + }, + 0, + ) => Ok((16, 4)), + _ => Err("native buffer element has no validated fixed layout".into()), + } +} + +fn element_size(scalar: RawScalar) -> usize { + match scalar { + RawScalar::Bool8 | RawScalar::I8 | RawScalar::U8 => 1, + RawScalar::I16 | RawScalar::U16 | RawScalar::Char16 => 2, + RawScalar::I32 | RawScalar::U32 | RawScalar::F32 | RawScalar::Bool32 => 4, + RawScalar::I64 + | RawScalar::U64 + | RawScalar::F64 + | RawScalar::NativeIsize + | RawScalar::NativeUsize => 8, + } +} + +fn parse_cleanup(value: &str) -> Result { + match value { + "CloseHandle" => Ok(Cleanup::CloseHandle), + "RegCloseKey" => Ok(Cleanup::RegCloseKey), + "LocalFree" => Ok(Cleanup::LocalFree), + "GlobalFree" => Ok(Cleanup::GlobalFree), + "FreeLibrary" => Ok(Cleanup::FreeLibrary), + "CloseServiceHandle" => Ok(Cleanup::CloseServiceHandle), + "CoTaskMemFree" => Ok(Cleanup::CoTaskMemFree), + "CredFree" => Ok(Cleanup::CredFree), + other => Err(format!("unsupported native cleanup `{other}`")), + } +} + +fn validate_module(module: &str) -> Result<(), String> { + let lower = module.to_ascii_lowercase(); + if module.is_empty() + || !(lower.ends_with(".dll") || lower.ends_with(".drv")) + || module + .chars() + .any(|character| matches!(character, '/' | '\\' | ':')) + { + return Err(format!("module `{module}` is not a bare System32 DLL name")); + } + Ok(()) +} diff --git a/tools/dynwinrt-codegen/src/codegen/win32/project.rs b/tools/dynwinrt-codegen/src/codegen/win32/project.rs new file mode 100644 index 00000000..99ebf3cd --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/win32/project.rs @@ -0,0 +1,1052 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::{BTreeMap, BTreeSet}; + +use crate::win32_metadata::{ + RawApis, RawBaseType, RawBufferSize, RawCallingConvention, RawDirection, RawFunction, + RawNamedKind, RawScalar, +}; + +use super::ir::{ + AbiType, AsyncIoKind, Cleanup, Conversion, Direction, FunctionContract, InputExpression, + NativeBuilderFieldKind, NativeFieldType, NativeLayout, NativeOutputFieldKind, OmittedFunction, + ProjectedApis, ProjectedAsyncFunction, ProjectedFunction, ProjectedNativeBuilder, + ProjectedNativeBuilderField, ProjectedNativeOutputField, ProjectedOutput, ProjectionResult, + ReturnShape, RuntimeParameter, RuntimePlan, Scalar, StringEncoding, SurfaceParameter, + SurfaceType, ValueType, +}; +use super::model; + +pub(super) fn project_apis(raw: &RawApis) -> ProjectionResult { + let mut async_functions = Vec::new(); + let mut async_names = BTreeSet::new(); + for name in ["ReadFile", "WriteFile"] { + let candidates = raw + .functions + .iter() + .filter(|function| function.name == name) + .collect::>(); + if let [function] = candidates.as_slice() + && let Some(projected) = project_async_function(function) + { + async_names.insert(name.to_string()); + async_functions.push(projected); + } + } + let sync_raw = RawApis { + namespace: raw.namespace.clone(), + class_name: raw.class_name.clone(), + functions: raw + .functions + .iter() + .filter(|function| !async_names.contains(&function.name)) + .cloned() + .collect(), + }; + let (contracts, mut omitted) = model::validate_apis(&sync_raw); + let mut functions = Vec::new(); + let mut enums = BTreeMap::new(); + for contract in contracts { + match project_function(&contract) { + Ok(function) => { + for definition in &contract.enums { + enums.insert( + (definition.namespace.clone(), definition.name.clone()), + definition.clone(), + ); + } + functions.push(function); + } + Err(reason) => omitted.push(( + format!("{}.{}::{}", raw.namespace, raw.class_name, contract.name), + reason, + )), + } + } + functions.sort_by(|left, right| left.js_name.cmp(&right.js_name)); + assign_unicode_aliases(&mut functions); + let native_builders = project_native_builders(&functions); + ProjectionResult { + projected: ProjectedApis { + namespace: raw.namespace.clone(), + class_name: raw.class_name.clone(), + functions, + enums: enums.into_values().collect(), + native_builders, + async_functions, + }, + omitted: omitted + .into_iter() + .map(|(identity, reason)| OmittedFunction { identity, reason }) + .collect(), + } +} + +fn project_async_function(function: &RawFunction) -> Option { + let kind = match ( + function.namespace.as_str(), + function.name.as_str(), + function.dll.to_ascii_lowercase().as_str(), + ) { + ("Windows.Win32.Storage.FileSystem", "ReadFile", "kernel32.dll") => AsyncIoKind::Read, + ("Windows.Win32.Storage.FileSystem", "WriteFile", "kernel32.dll") => AsyncIoKind::Write, + _ => return None, + }; + if function.parameters.len() != 5 + || function.calling_convention != RawCallingConvention::System + || function.variadic + || !function.architectures.x64 + || !function.architectures.arm64 + || !function.supports_last_error + || !matches!( + function.return_type.base, + RawBaseType::Scalar(RawScalar::Bool32) + ) + { + return None; + } + let [file, buffer, count, transferred, overlapped] = function.parameters.as_slice() else { + return None; + }; + let file_ok = file.name == "hFile" + && file.direction == RawDirection::In + && file.typ.pointer_depth == 0 + && matches!( + &file.typ.base, + RawBaseType::Named { + name, + kind: RawNamedKind::Handle { .. }, + .. + } if name == "HANDLE" + ); + let buffer_ok = buffer.name == "lpBuffer" + && buffer.direction + == match kind { + AsyncIoKind::Read => RawDirection::Out, + AsyncIoKind::Write => RawDirection::In, + } + && buffer.typ.pointer_depth > 0 + && matches!( + buffer.buffer.as_ref().map(|buffer| &buffer.size), + Some(RawBufferSize::ByteCountParam(2)) + ); + let expected_count = match kind { + AsyncIoKind::Read => "nNumberOfBytesToRead", + AsyncIoKind::Write => "nNumberOfBytesToWrite", + }; + let expected_transferred = match kind { + AsyncIoKind::Read => "lpNumberOfBytesRead", + AsyncIoKind::Write => "lpNumberOfBytesWritten", + }; + let count_ok = count.name == expected_count + && count.direction == RawDirection::In + && count.typ.pointer_depth == 0 + && matches!(count.typ.base, RawBaseType::Scalar(RawScalar::U32)); + let transferred_ok = transferred.name == expected_transferred + && transferred.direction == RawDirection::Out + && transferred.typ.pointer_depth == 1 + && matches!(transferred.typ.base, RawBaseType::Scalar(RawScalar::U32)); + let overlapped_ok = overlapped.name == "lpOverlapped" + && overlapped.direction == RawDirection::InOut + && overlapped.nullable + && overlapped.typ.pointer_depth == 1 + && matches!( + &overlapped.typ.base, + RawBaseType::Named { + name, + kind: RawNamedKind::NativeStruct { .. }, + .. + } if name == "OVERLAPPED" + ); + (file_ok && buffer_ok && count_ok && transferred_ok && overlapped_ok).then(|| { + ProjectedAsyncFunction { + js_name: format!("{}Async", camel_case(&function.name)), + kind, + } + }) +} + +fn project_native_builders(functions: &[ProjectedFunction]) -> Vec { + let mut layouts = BTreeMap::<(String, String), &NativeLayout>::new(); + for function in functions { + for input in &function.inputs { + if let InputExpression::NativeAggregate { layout, .. } = input { + layouts + .entry((layout.namespace.clone(), layout.name.clone())) + .or_insert(layout); + } + } + } + layouts + .into_values() + .filter_map(project_native_builder) + .collect() +} + +fn project_native_builder(layout: &NativeLayout) -> Option { + let fields = &layout.x64.fields; + if layout.namespace == "Windows.Win32.Security" && layout.name == "SECURITY_ATTRIBUTES" { + if !fields.iter().any(|field| { + field.name == "nLength" + && matches!( + field.typ, + NativeFieldType::Scalar(super::ir::NativeScalar::U32) + ) + }) || !fields.iter().any(|field| { + field.name == "lpSecurityDescriptor" && matches!(field.typ, NativeFieldType::Pointer) + }) || !fields.iter().any(|field| { + field.name == "bInheritHandle" + && matches!( + field.typ, + NativeFieldType::Scalar(super::ir::NativeScalar::I32) + ) + }) { + return None; + } + return Some(ProjectedNativeBuilder { + layout_name: layout.name.clone(), + js_name: "SecurityAttributes".into(), + size_field: Some("nLength".into()), + fields: vec![ + ProjectedNativeBuilderField { + native_name: "lpSecurityDescriptor".into(), + surface_name: "securityDescriptor".into(), + kind: NativeBuilderFieldKind::DataPointer { nullable: true }, + optional: true, + }, + ProjectedNativeBuilderField { + native_name: "bInheritHandle".into(), + surface_name: "inheritHandle".into(), + kind: NativeBuilderFieldKind::Boolean, + optional: true, + }, + ], + outputs: Vec::new(), + }); + } + if layout.namespace == "Windows.Win32.System.Threading" + && matches!(layout.name.as_str(), "STARTUPINFOA" | "STARTUPINFOW") + && fields.iter().any(|field| { + field.name == "cb" + && matches!( + field.typ, + NativeFieldType::Scalar(super::ir::NativeScalar::U32) + ) + }) + { + return Some(ProjectedNativeBuilder { + layout_name: layout.name.clone(), + js_name: if layout.name == "STARTUPINFOA" { + "StartupInfoA" + } else { + "StartupInfoW" + } + .into(), + size_field: Some("cb".into()), + fields: Vec::new(), + outputs: Vec::new(), + }); + } + if layout.namespace == "Windows.Win32.System.Threading" && layout.name == "PROCESS_INFORMATION" + { + let handle = |name: &str| { + fields.iter().any(|field| { + field.name == name + && matches!( + field.typ, + NativeFieldType::Handle { + cleanup: Cleanup::CloseHandle + } + ) + }) + }; + let u32_field = |name: &str| { + fields.iter().any(|field| { + field.name == name + && matches!( + field.typ, + NativeFieldType::Scalar(super::ir::NativeScalar::U32) + ) + }) + }; + if !(handle("hProcess") + && handle("hThread") + && u32_field("dwProcessId") + && u32_field("dwThreadId")) + { + return None; + } + return Some(ProjectedNativeBuilder { + layout_name: layout.name.clone(), + js_name: "ProcessInformation".into(), + size_field: None, + fields: Vec::new(), + outputs: vec![ + ProjectedNativeOutputField { + native_name: "hProcess".into(), + surface_name: "process".into(), + kind: NativeOutputFieldKind::Resource { + cleanup: Cleanup::CloseHandle, + }, + }, + ProjectedNativeOutputField { + native_name: "hThread".into(), + surface_name: "thread".into(), + kind: NativeOutputFieldKind::Resource { + cleanup: Cleanup::CloseHandle, + }, + }, + ProjectedNativeOutputField { + native_name: "dwProcessId".into(), + surface_name: "processId".into(), + kind: NativeOutputFieldKind::U32, + }, + ProjectedNativeOutputField { + native_name: "dwThreadId".into(), + surface_name: "threadId".into(), + kind: NativeOutputFieldKind::U32, + }, + ], + }); + } + None +} + +fn project_function(contract: &FunctionContract) -> Result { + let count_buffers = count_buffer_relations(contract)?; + let mut parameters = Vec::::new(); + let mut native_surface = vec![None; contract.parameters.len()]; + let mut inputs = Vec::::new(); + let mut runtime_parameters = Vec::::new(); + let mut output_index = 0; + let mut outputs = Vec::new(); + + for (index, parameter) in contract.parameters.iter().enumerate() { + let is_buffer_count = count_buffers.contains_key(&index); + if should_surface_input(parameter, is_buffer_count) { + let surface_index = parameters.len(); + let minimum_bytes = parameter + .buffer + .as_ref() + .and_then(|buffer| { + buffer.constant_count.map(|count| { + count.checked_mul(buffer.element_size).ok_or_else(|| { + format!("buffer `{}` fixed size overflows usize", parameter.name) + }) + }) + }) + .transpose()? + .or(matches!(parameter.typ, ValueType::GuidPointer).then_some(16)); + parameters.push(SurfaceParameter { + name: input_name(¶meter.name, surface_index), + typ: if parameter.consumes_resource { + SurfaceType::ManagedResource + } else if parameter.null_null_terminated + && matches!(parameter.typ, ValueType::StringPointer(_)) + { + match parameter.typ { + ValueType::StringPointer(encoding) => SurfaceType::MultiString(encoding), + _ => unreachable!("validated NullNullTerminated string pointer"), + } + } else { + input_surface_type(¶meter.typ) + }, + nullable: parameter.nullable, + minimum_bytes, + alignment: parameter + .buffer + .as_ref() + .map(|buffer| buffer.element_alignment) + .or(matches!(parameter.typ, ValueType::GuidPointer).then_some(4)), + }); + native_surface[index] = Some(surface_index); + } + } + + for (index, parameter) in contract.parameters.iter().enumerate() { + let reserved_pointer = + parameter.reserved && matches!(parameter.typ, ValueType::DataPointer); + let surface_index = native_surface[index]; + + let runtime = if let ValueType::NativeStruct { layout } = ¶meter.typ { + RuntimeParameter { + abi: AbiType::Pointer, + direction: Direction::In, + nullable: false, + cleanup: Cleanup::None, + consumes_resource: false, + resource_cleanup: Cleanup::None, + aggregate: Some(layout.clone()), + } + } else if matches!( + ¶meter.typ, + ValueType::NativeStructPointer { .. } | ValueType::NativeUnionPointer { .. } + ) { + RuntimeParameter { + abi: AbiType::Pointer, + direction: Direction::In, + nullable: parameter.nullable, + cleanup: Cleanup::None, + consumes_resource: false, + resource_cleanup: Cleanup::None, + aggregate: None, + } + } else if matches!(parameter.typ, ValueType::ScalarPointer { .. }) { + RuntimeParameter { + abi: AbiType::Pointer, + direction: Direction::In, + nullable: parameter.nullable, + cleanup: Cleanup::None, + consumes_resource: false, + resource_cleanup: Cleanup::None, + aggregate: None, + } + } else if matches!(parameter.typ, ValueType::GuidPointer) { + RuntimeParameter { + abi: AbiType::Pointer, + direction: Direction::In, + nullable: parameter.nullable, + cleanup: Cleanup::None, + consumes_resource: false, + resource_cleanup: Cleanup::None, + aggregate: None, + } + } else if matches!(parameter.typ, ValueType::NullPointer) { + RuntimeParameter { + abi: AbiType::Pointer, + direction: Direction::In, + nullable: true, + cleanup: Cleanup::None, + consumes_resource: false, + resource_cleanup: Cleanup::None, + aggregate: None, + } + } else if matches!(parameter.typ, ValueType::ComInterface { .. }) { + RuntimeParameter { + abi: AbiType::Pointer, + direction: Direction::In, + nullable: parameter.nullable, + cleanup: Cleanup::None, + consumes_resource: false, + resource_cleanup: Cleanup::None, + aggregate: None, + } + } else if matches!(parameter.typ, ValueType::StringPointerPointer(_)) { + RuntimeParameter { + abi: AbiType::Pointer, + direction: Direction::In, + nullable: parameter.nullable, + cleanup: Cleanup::None, + consumes_resource: false, + resource_cleanup: Cleanup::None, + aggregate: None, + } + } else if parameter.buffer.is_some() { + RuntimeParameter { + abi: AbiType::Pointer, + direction: Direction::In, + nullable: parameter.nullable, + cleanup: Cleanup::None, + consumes_resource: false, + resource_cleanup: Cleanup::None, + aggregate: None, + } + } else if reserved_pointer { + RuntimeParameter { + abi: AbiType::Pointer, + direction: Direction::In, + nullable: true, + cleanup: Cleanup::None, + consumes_resource: false, + resource_cleanup: Cleanup::None, + aggregate: None, + } + } else if parameter.pointer_depth == 0 { + RuntimeParameter { + abi: parameter.abi, + direction: Direction::In, + nullable: parameter.nullable, + cleanup: Cleanup::None, + consumes_resource: parameter.consumes_resource, + resource_cleanup: parameter.resource_cleanup, + aggregate: None, + } + } else { + RuntimeParameter { + abi: parameter.abi, + direction: parameter.direction, + nullable: parameter.nullable, + cleanup: parameter.cleanup, + consumes_resource: parameter.consumes_resource, + resource_cleanup: if matches!(parameter.direction, Direction::In | Direction::InOut) + { + parameter.resource_cleanup + } else { + Cleanup::None + }, + aggregate: None, + } + }; + + if matches!(runtime.direction, Direction::In | Direction::InOut) { + let expression = if parameter.reserved { + if runtime.abi == AbiType::Pointer { + InputExpression::NullPointer + } else { + InputExpression::Zero(runtime.abi) + } + } else if let ValueType::NativeStruct { layout } = ¶meter.typ { + InputExpression::NativeAggregate { + parameter_index: surface_index.ok_or_else(|| { + format!( + "native aggregate parameter `{}` has no projected input", + parameter.name + ) + })?, + layout: layout.clone(), + nullable: false, + by_value: true, + } + } else if let ValueType::NativeStructPointer { layout } + | ValueType::NativeUnionPointer { layout } = ¶meter.typ + { + InputExpression::NativeAggregate { + parameter_index: surface_index.ok_or_else(|| { + format!( + "native aggregate parameter `{}` has no projected input", + parameter.name + ) + })?, + layout: layout.clone(), + nullable: parameter.nullable, + by_value: false, + } + } else if let ValueType::ScalarPointer { scalar } = parameter.typ { + InputExpression::ScalarPointer { + parameter_index: surface_index.ok_or_else(|| { + format!( + "scalar pointer parameter `{}` has no projected input", + parameter.name + ) + })?, + scalar, + nullable: parameter.nullable, + } + } else if let ValueType::ComInterface { iid, .. } = ¶meter.typ { + InputExpression::ComInterface { + parameter_index: surface_index.ok_or_else(|| { + format!( + "COM interface parameter `{}` has no projected input", + parameter.name + ) + })?, + iid: iid.clone(), + } + } else if let ValueType::StringPointerPointer(encoding) = parameter.typ { + InputExpression::StringPointerPointer { + parameter_index: surface_index.ok_or_else(|| { + format!( + "string pointer slot `{}` has no projected input", + parameter.name + ) + })?, + encoding, + nullable: parameter.nullable, + } + } else if matches!(parameter.typ, ValueType::NullPointer) { + InputExpression::NullPointer + } else if let Some(buffer_index) = count_buffers.get(&index).copied() { + let buffer = &contract.parameters[buffer_index]; + let buffer_surface = native_surface[buffer_index] + .ok_or_else(|| format!("buffer `{}` has no projected input", buffer.name))?; + let buffer_contract = buffer.buffer.as_ref().expect("count relation"); + InputExpression::BufferLength { + parameter_index: buffer_surface, + divisor: if buffer_contract.count_is_bytes { + 1 + } else { + buffer_contract.element_size + }, + abi: parameter.abi, + } + } else { + let surface = surface_index.ok_or_else(|| { + format!( + "input parameter `{}` has no projected input", + parameter.name + ) + })?; + InputExpression::Surface { + parameter_index: surface, + conversion: input_conversion(parameter), + } + }; + inputs.push(expression); + } + + if matches!(runtime.direction, Direction::Out | Direction::InOut) { + outputs.push(ProjectedOutput { + name: output_name(parameter), + output_index, + typ: output_surface_type(parameter), + conversion: output_conversion(parameter), + }); + output_index += 1; + } + runtime_parameters.push(runtime); + } + + let return_shape = if contract.return_is_status { + ReturnShape::Object { + status: true, + return_value: None, + outputs, + last_error: contract.capture_last_error, + } + } else if !outputs.is_empty() || contract.capture_last_error { + ReturnShape::Object { + status: false, + return_value: contract.return_type.as_ref().map(|typ| { + ( + return_surface_type(typ, contract.return_cleanup), + return_conversion(typ, contract.return_cleanup), + ) + }), + outputs, + last_error: contract.capture_last_error, + } + } else if let Some(typ) = &contract.return_type { + ReturnShape::Direct { + typ: return_surface_type(typ, contract.return_cleanup), + conversion: return_conversion(typ, contract.return_cleanup), + } + } else { + ReturnShape::Void + }; + + Ok(ProjectedFunction { + metadata_name: contract.name.clone(), + js_name: camel_case(&contract.name), + unicode_alias: None, + parameters, + inputs, + runtime: RuntimePlan { + dll: contract.dll.clone(), + entry_point: contract.entry_point.clone(), + parameters: runtime_parameters, + return_abi: contract.return_abi, + return_aggregate: contract.return_aggregate.clone(), + return_cleanup: contract.return_cleanup, + success_rule: contract.success_rule, + capture_last_error: contract.capture_last_error, + calling_convention: contract.calling_convention, + }, + return_shape, + }) +} + +fn count_buffer_relations(contract: &FunctionContract) -> Result, String> { + let mut relations = BTreeMap::new(); + for (buffer_index, parameter) in contract.parameters.iter().enumerate() { + let Some(count_index) = parameter + .buffer + .as_ref() + .and_then(|buffer| buffer.count_parameter) + else { + continue; + }; + if let Some(existing) = relations.insert(count_index, buffer_index) + && existing != buffer_index + { + return Err(format!( + "count parameter {} controls multiple buffers; grouped buffer projection is not implemented", + contract.parameters[count_index].name + )); + } + } + Ok(relations) +} + +fn should_surface_input(parameter: &super::ir::ParameterContract, is_buffer_count: bool) -> bool { + if parameter.reserved || is_buffer_count || matches!(parameter.typ, ValueType::NullPointer) { + return false; + } + parameter.buffer.is_some() + || matches!( + ¶meter.typ, + ValueType::NativeStructPointer { .. } | ValueType::NativeUnionPointer { .. } + ) + || matches!(parameter.typ, ValueType::NativeStruct { .. }) + || matches!(parameter.typ, ValueType::ScalarPointer { .. }) + || matches!(parameter.typ, ValueType::GuidPointer) + || matches!(parameter.direction, Direction::In | Direction::InOut) +} + +fn input_surface_type(typ: &ValueType) -> SurfaceType { + match typ { + ValueType::Scalar(Scalar::Bool8 | Scalar::Bool32) => SurfaceType::Boolean, + ValueType::Scalar( + Scalar::I64 | Scalar::U64 | Scalar::NativeIsize | Scalar::NativeUsize, + ) => SurfaceType::BigInt, + ValueType::Scalar(_) => SurfaceType::Number, + ValueType::Enum { name, .. } => SurfaceType::Enum(name.clone()), + ValueType::Handle { name, .. } => SurfaceType::Handle(name.clone()), + ValueType::DataPointer => SurfaceType::Buffer, + ValueType::StringPointer(encoding) => SurfaceType::String(*encoding), + ValueType::FunctionPointer => SurfaceType::BigInt, + ValueType::NativeStructPointer { layout } => SurfaceType::NativeStruct(layout.name.clone()), + ValueType::NativeUnionPointer { layout } => SurfaceType::NativeUnion(layout.name.clone()), + ValueType::NativeStruct { layout } => SurfaceType::NativeStruct(layout.name.clone()), + ValueType::ScalarPointer { scalar } => scalar_surface_type(*scalar), + ValueType::GuidPointer => SurfaceType::Buffer, + ValueType::NullPointer => unreachable!("null-only pointer is hidden"), + ValueType::ComInterface { name, .. } => SurfaceType::ComInterface(name.clone()), + ValueType::StringPointerPointer(encoding) => SurfaceType::String(*encoding), + } +} + +fn scalar_surface_type(scalar: Scalar) -> SurfaceType { + match scalar { + Scalar::Bool8 | Scalar::Bool32 => SurfaceType::Boolean, + Scalar::I64 | Scalar::U64 | Scalar::NativeIsize | Scalar::NativeUsize => { + SurfaceType::BigInt + } + Scalar::I8 + | Scalar::U8 + | Scalar::I16 + | Scalar::U16 + | Scalar::I32 + | Scalar::U32 + | Scalar::F32 + | Scalar::F64 => SurfaceType::Number, + } +} + +fn output_surface_type(parameter: &super::ir::ParameterContract) -> SurfaceType { + if parameter.cleanup != Cleanup::None { + SurfaceType::Resource + } else { + match ¶meter.typ { + ValueType::Scalar(Scalar::Bool8 | Scalar::Bool32) => SurfaceType::Boolean, + ValueType::Scalar( + Scalar::I64 | Scalar::U64 | Scalar::NativeIsize | Scalar::NativeUsize, + ) => SurfaceType::BigInt, + ValueType::Scalar(_) => SurfaceType::Number, + ValueType::Enum { name, .. } => SurfaceType::Enum(name.clone()), + ValueType::Handle { name, .. } => SurfaceType::Handle(name.clone()), + ValueType::DataPointer | ValueType::FunctionPointer => SurfaceType::BigInt, + ValueType::StringPointer(_) => SurfaceType::BigInt, + ValueType::NativeStructPointer { layout } => { + SurfaceType::NativeStruct(layout.name.clone()) + } + ValueType::NativeUnionPointer { layout } => { + SurfaceType::NativeUnion(layout.name.clone()) + } + ValueType::NativeStruct { layout } => SurfaceType::NativeStruct(layout.name.clone()), + ValueType::ScalarPointer { scalar } => scalar_surface_type(*scalar), + ValueType::GuidPointer => SurfaceType::Buffer, + ValueType::NullPointer => unreachable!("null-only pointer has no output"), + ValueType::ComInterface { name, .. } => SurfaceType::ComInterface(name.clone()), + ValueType::StringPointerPointer(encoding) => SurfaceType::String(*encoding), + } + } +} + +fn return_surface_type(typ: &ValueType, cleanup: Cleanup) -> SurfaceType { + if cleanup != Cleanup::None { + SurfaceType::Resource + } else { + input_surface_type(typ) + } +} + +fn input_conversion(parameter: &super::ir::ParameterContract) -> Conversion { + if parameter.consumes_resource { + return Conversion::ResourceInput(parameter.resource_cleanup); + } + if parameter.null_null_terminated && matches!(parameter.typ, ValueType::StringPointer(_)) { + return match parameter.typ { + ValueType::StringPointer(StringEncoding::Wide) => Conversion::WideMultiString, + ValueType::StringPointer(StringEncoding::Ansi) => Conversion::AnsiMultiString, + _ => unreachable!("validated NullNullTerminated string pointer"), + }; + } + match ¶meter.typ { + ValueType::Scalar(Scalar::Bool8) => Conversion::Boolean8, + ValueType::Scalar(Scalar::Bool32) => Conversion::Boolean, + ValueType::Scalar(Scalar::I8) => Conversion::I8, + ValueType::Scalar(Scalar::U8) => Conversion::U8, + ValueType::Scalar(Scalar::I16) => Conversion::I16, + ValueType::Scalar(Scalar::U16) => Conversion::U16, + ValueType::Scalar(Scalar::I32) => Conversion::I32, + ValueType::Scalar(Scalar::U32) => Conversion::U32, + ValueType::Scalar(Scalar::I64) => Conversion::I64, + ValueType::Scalar(Scalar::U64) => Conversion::U64, + ValueType::Scalar(Scalar::F32) => Conversion::F32, + ValueType::Scalar(Scalar::F64) => Conversion::F64, + ValueType::Scalar(Scalar::NativeIsize) => Conversion::I64, + ValueType::Scalar(Scalar::NativeUsize) => Conversion::U64, + ValueType::Enum { underlying, .. } => match underlying { + super::ir::EnumUnderlying::I8 => Conversion::I8, + super::ir::EnumUnderlying::U8 => Conversion::U8, + super::ir::EnumUnderlying::I16 => Conversion::I16, + super::ir::EnumUnderlying::U16 => Conversion::U16, + super::ir::EnumUnderlying::I32 => Conversion::I32, + super::ir::EnumUnderlying::U32 => Conversion::U32, + }, + ValueType::Handle { .. } => Conversion::Handle, + ValueType::DataPointer => Conversion::DataPointer, + ValueType::StringPointer(StringEncoding::Wide) => Conversion::WideString, + ValueType::StringPointer(StringEncoding::Ansi) => Conversion::AnsiString, + ValueType::FunctionPointer => Conversion::BigInt, + ValueType::NativeStructPointer { .. } | ValueType::NativeUnionPointer { .. } => { + unreachable!("native aggregate inputs use a dedicated expression") + } + ValueType::NativeStruct { .. } => { + unreachable!("by-value native aggregate inputs use a dedicated expression") + } + ValueType::ScalarPointer { .. } => { + unreachable!("scalar pointer inputs use a dedicated expression") + } + ValueType::GuidPointer => Conversion::DataPointer, + ValueType::NullPointer => unreachable!("null-only pointer has no surface input"), + ValueType::ComInterface { .. } => { + unreachable!("COM interface inputs use a dedicated expression") + } + ValueType::StringPointerPointer(_) => { + unreachable!("string pointer slots use a dedicated expression") + } + } +} + +fn output_conversion(parameter: &super::ir::ParameterContract) -> Conversion { + if parameter.cleanup != Cleanup::None { + Conversion::Resource + } else { + return_conversion(¶meter.typ, Cleanup::None) + } +} + +fn return_conversion(typ: &ValueType, cleanup: Cleanup) -> Conversion { + if cleanup != Cleanup::None { + return Conversion::Resource; + } + match typ { + ValueType::Scalar(Scalar::Bool8 | Scalar::Bool32) => Conversion::Boolean, + ValueType::Scalar( + Scalar::I64 | Scalar::U64 | Scalar::NativeIsize | Scalar::NativeUsize, + ) => Conversion::BigInt, + ValueType::Scalar(_) | ValueType::Enum { .. } => Conversion::Number, + ValueType::Handle { .. } + | ValueType::DataPointer + | ValueType::StringPointer(_) + | ValueType::FunctionPointer => Conversion::BigInt, + ValueType::NativeStructPointer { .. } | ValueType::NativeUnionPointer { .. } => { + unreachable!("native aggregate pointer results are not projected") + } + ValueType::NativeStruct { .. } => Conversion::NativeAggregate, + ValueType::ScalarPointer { .. } => { + unreachable!("scalar pointer results are not projected") + } + ValueType::GuidPointer => { + unreachable!("GUID pointer results are caller-owned buffers") + } + ValueType::NullPointer => unreachable!("null-only pointer cannot be a return"), + ValueType::ComInterface { .. } => { + unreachable!("COM interface returns require ownership projection") + } + ValueType::StringPointerPointer(_) => { + unreachable!("string pointer slot returns require ownership projection") + } + } +} + +fn input_name(raw: &str, index: usize) -> String { + let stripped = strip_prefix(raw); + safe_identifier(if stripped.is_empty() { + format!("arg{index}") + } else { + lower_first(stripped) + }) +} + +fn output_name(parameter: &super::ir::ParameterContract) -> String { + if let ValueType::Handle { name, .. } = ¶meter.typ + && parameter.name.to_ascii_lowercase().ends_with("result") + { + return lower_first(name.trim_start_matches('H')); + } + let mut name = strip_prefix(¶meter.name).to_string(); + let lower = parameter.name.to_ascii_lowercase(); + if lower.contains("cb") { + name.push_str("Size"); + } else if lower.contains("cch") || lower.contains("ch") { + name.push_str("Length"); + } + if name.eq_ignore_ascii_case("result") { + name = "value".into(); + } + safe_identifier(lower_first(&name)) +} + +fn strip_prefix(value: &str) -> &str { + for prefix in [ + "lpp", "lpcb", "lpch", "lpcch", "lpdw", "lp", "pp", "phk", "ph", "pcb", "pdw", "pcch", + "pch", "p", + ] { + if let Some(rest) = value.strip_prefix(prefix) + && rest + .chars() + .next() + .is_some_and(|character| character.is_ascii_uppercase()) + { + return rest; + } + } + value +} + +fn lower_first(value: &str) -> String { + if value + .chars() + .all(|character| !character.is_ascii_alphabetic() || character.is_ascii_uppercase()) + { + return value.to_ascii_lowercase(); + } + let mut characters = value.chars(); + let Some(first) = characters.next() else { + return String::new(); + }; + first.to_ascii_lowercase().to_string() + characters.as_str() +} + +fn safe_identifier(value: String) -> String { + if matches!( + value.as_str(), + "await" + | "break" + | "case" + | "catch" + | "class" + | "const" + | "continue" + | "debugger" + | "default" + | "delete" + | "do" + | "else" + | "enum" + | "export" + | "extends" + | "false" + | "finally" + | "for" + | "function" + | "if" + | "implements" + | "import" + | "in" + | "instanceof" + | "interface" + | "let" + | "new" + | "null" + | "package" + | "private" + | "protected" + | "public" + | "return" + | "static" + | "super" + | "switch" + | "this" + | "throw" + | "true" + | "try" + | "typeof" + | "var" + | "void" + | "while" + | "with" + | "yield" + | "status" + | "result" + | "lastError" + ) { + format!("{value}_") + } else { + value + } +} + +fn camel_case(value: &str) -> String { + let uppercase = value + .chars() + .take_while(|character| character.is_ascii_uppercase()) + .count(); + if uppercase <= 1 { + return lower_first(value); + } + if uppercase == value.len() { + return value.to_ascii_lowercase(); + } + value[..uppercase - 1].to_ascii_lowercase() + &value[uppercase - 1..] +} + +fn assign_unicode_aliases(functions: &mut [ProjectedFunction]) { + let names = functions + .iter() + .map(|function| function.js_name.clone()) + .collect::>(); + let mut aliases = BTreeSet::new(); + for function in functions { + let Some(base) = function.js_name.strip_suffix('W') else { + continue; + }; + if !base.is_empty() && !names.contains(base) && aliases.insert(base.to_string()) { + function.unicode_alias = Some(base.to_string()); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn unicode_name_gets_natural_alias() { + let mut functions = vec![ProjectedFunction { + metadata_name: "RegOpenKeyExW".into(), + js_name: "regOpenKeyExW".into(), + unicode_alias: None, + parameters: vec![], + inputs: vec![], + runtime: RuntimePlan { + dll: "advapi32.dll".into(), + entry_point: "RegOpenKeyExW".into(), + parameters: vec![], + return_abi: Some(AbiType::I32), + return_aggregate: None, + return_cleanup: Cleanup::None, + success_rule: super::super::ir::SuccessRule::ReturnZero, + capture_last_error: false, + calling_convention: super::super::ir::CallingConvention::System, + }, + return_shape: ReturnShape::Object { + status: true, + return_value: None, + outputs: vec![], + last_error: false, + }, + }]; + assign_unicode_aliases(&mut functions); + assert_eq!(functions[0].unicode_alias.as_deref(), Some("regOpenKeyEx")); + } + + #[test] + fn javascript_reserved_parameter_names_are_escaped() { + assert_eq!(input_name("lpIn", 0), "in_"); + assert_eq!(input_name("class", 0), "class_"); + } +} diff --git a/tools/dynwinrt-codegen/src/codegen/win32/render.rs b/tools/dynwinrt-codegen/src/codegen/win32/render.rs new file mode 100644 index 00000000..964ffc9a --- /dev/null +++ b/tools/dynwinrt-codegen/src/codegen/win32/render.rs @@ -0,0 +1,983 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::collections::{BTreeMap, BTreeSet}; + +use super::ir::{ + AbiType, AsyncIoKind, Cleanup, Conversion, Direction, EnumDefinition, InputExpression, + NativeArchitectureLayout, NativeBuilderFieldKind, NativeFieldType, NativeLayout, + NativeOutputFieldKind, NativeScalar, ProjectedApis, ProjectedAsyncFunction, ProjectedFunction, + ProjectedNativeBuilder, ReturnShape, StringEncoding, SurfaceType, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GeneratedOutput { + pub js: String, + pub dts: String, + pub extra_files: Vec<(String, String)>, +} + +pub(super) fn render(apis: &ProjectedApis, runtime_import: &str) -> GeneratedOutput { + let js = render_js(apis, runtime_import, &unsafe_runtime_import(runtime_import)); + let dts = render_dts(apis, runtime_import); + let mut extra_files = Vec::new(); + for definition in &apis.enums { + let (js, dts) = render_enum(definition); + extra_files.push((format!("{}.js", definition.name), js)); + extra_files.push((format!("{}.d.ts", definition.name), dts)); + } + extra_files.sort_by(|left, right| left.0.cmp(&right.0)); + GeneratedOutput { + js, + dts, + extra_files, + } +} + +fn render_js(apis: &ProjectedApis, runtime_import: &str, unsafe_runtime_import: &str) -> String { + let mut output = String::new(); + output.push_str("// Generated by dynwinrt-codegen - do not edit\n"); + output.push_str("'use strict'\n"); + output.push_str(&format!( + "const {{ DynWin32 }} = require({runtime_import:?})\n\ + const {{ DynWin32Function }} = require({unsafe_runtime_import:?})\n\n" + )); + let native_layouts = collect_native_layouts(apis); + for layout in native_layouts.values() { + let builder = apis + .native_builders + .iter() + .find(|builder| builder.layout_name == layout.name); + output.push_str(&format!( + "const _nativeLayout_{} = {}\n", + layout.name, + native_layout_descriptor_js(layout) + )); + if let Some(builder) = builder { + render_native_builder_js(&mut output, layout, builder); + } else { + output.push_str(&format!( + "function create{}(bytes) {{ return DynWin32.createNativeStruct(_nativeLayout_{}, bytes) }}\nexports.create{} = create{}\n\n", + layout.name, layout.name, layout.name, layout.name + )); + } + } + + fn render_native_builder_js( + output: &mut String, + layout: &NativeLayout, + builder: &ProjectedNativeBuilder, + ) { + output.push_str(&format!( + "function create{}(init) {{\n const value = DynWin32.createNativeStruct(_nativeLayout_{})\n init ??= {{}}\n", + builder.js_name, layout.name + )); + if let Some(size_field) = &builder.size_field { + output.push_str(&format!( + " DynWin32.setNativeStructU32(value, _nativeLayout_{}, {:?}, value.length)\n", + layout.name, size_field + )); + } + for field in &builder.fields { + match field.kind { + NativeBuilderFieldKind::Boolean => output.push_str(&format!( + " DynWin32.setNativeStructBool32(value, _nativeLayout_{}, {:?}, Boolean(init.{}))\n", + layout.name, field.native_name, field.surface_name + )), + NativeBuilderFieldKind::DataPointer { nullable } => output.push_str(&format!( + " DynWin32.setNativeStructPointer(value, _nativeLayout_{}, {:?}, DynWin32.dataPointer(init.{} ?? null, {}))\n", + layout.name, field.native_name, field.surface_name, nullable + )), + } + } + output.push_str(&format!( + " return value\n}}\nexports.create{0} = create{0}\n\n", + builder.js_name + )); + for field in &builder.outputs { + let function = match field.kind { + NativeOutputFieldKind::U32 => { + format!("get{}{}", builder.js_name, upper_first(&field.surface_name)) + } + NativeOutputFieldKind::Resource { .. } => { + format!( + "take{}{}", + builder.js_name, + upper_first(&field.surface_name) + ) + } + }; + let expression = match field.kind { + NativeOutputFieldKind::U32 => format!( + "DynWin32.getNativeStructU32(value, _nativeLayout_{}, {:?})", + layout.name, field.native_name + ), + NativeOutputFieldKind::Resource { cleanup } => format!( + "DynWin32.takeNativeStructResource(value, _nativeLayout_{}, {:?}, {:?})", + layout.name, + field.native_name, + cleanup_name(cleanup) + ), + }; + output.push_str(&format!( + "function {function}(value) {{ return {expression} }}\nexports.{function} = {function}\n" + )); + } + if !builder.outputs.is_empty() { + output.push('\n'); + } + } + + if apis.functions.iter().any(|function| { + function + .inputs + .iter() + .any(|input| matches!(input, InputExpression::BufferLength { .. })) + }) { + output.push_str( + "function _bufferCount(value, divisor) {\n\ + \x20 if (value == null) return 0\n\ + \x20 if (!ArrayBuffer.isView(value)) throw new TypeError('expected Buffer or TypedArray')\n\ + \x20 if (value.byteLength % divisor !== 0) throw new RangeError('buffer byte length is not divisible by its native element size')\n\ + \x20 const count = value.byteLength / divisor\n\ + \x20 if (!Number.isSafeInteger(count)) throw new RangeError('native buffer count is not a safe integer')\n\ + \x20 return count\n\ + }\n\n", + ); + } + if apis.functions.iter().any(|function| { + function + .inputs + .iter() + .any(|input| matches!(input, InputExpression::ScalarPointer { .. })) + }) { + output.push_str( + "function _scalarPointer(kind, value) {\n\ + \x20 switch (kind) {\n\ + \x20 case 'bool8': { const b = Buffer.alloc(1); b.writeUInt8(value ? 1 : 0); return b }\n\ + \x20 case 'bool32': { const b = Buffer.alloc(4); b.writeInt32LE(value ? 1 : 0); return b }\n\ + \x20 case 'i8': { const b = Buffer.alloc(1); b.writeInt8(value); return b }\n\ + \x20 case 'u8': { const b = Buffer.alloc(1); b.writeUInt8(value); return b }\n\ + \x20 case 'i16': { const b = Buffer.alloc(2); b.writeInt16LE(value); return b }\n\ + \x20 case 'u16': { const b = Buffer.alloc(2); b.writeUInt16LE(value); return b }\n\ + \x20 case 'i32': { const b = Buffer.alloc(4); b.writeInt32LE(value); return b }\n\ + \x20 case 'u32': { const b = Buffer.alloc(4); b.writeUInt32LE(value); return b }\n\ + \x20 case 'i64': { const b = Buffer.alloc(8); b.writeBigInt64LE(BigInt(value)); return b }\n\ + \x20 case 'u64': { const b = Buffer.alloc(8); b.writeBigUInt64LE(BigInt(value)); return b }\n\ + \x20 case 'f32': { const b = Buffer.alloc(4); b.writeFloatLE(value); return b }\n\ + \x20 case 'f64': { const b = Buffer.alloc(8); b.writeDoubleLE(value); return b }\n\ + \x20 default: throw new TypeError(`unsupported scalar pointer kind ${kind}`)\n\ + \x20 }\n\ + }\n\n", + ); + } + + for function in &apis.functions { + render_function_js(&mut output, function, apis); + output.push('\n'); + } + for function in &apis.async_functions { + render_async_function_js(&mut output, function); + output.push('\n'); + } + output.push_str("const Apis = Object.freeze({\n"); + for function in &apis.functions { + output.push_str(&format!(" {}: {},\n", function.js_name, function.js_name)); + if let Some(alias) = &function.unicode_alias { + output.push_str(&format!(" {alias}: {alias},\n")); + } + } + for function in &apis.async_functions { + output.push_str(&format!(" {}: {},\n", function.js_name, function.js_name)); + } + + output.push_str("})\nexports.Apis = Apis\n"); + output +} + +fn upper_first(value: &str) -> String { + let mut chars = value.chars(); + chars + .next() + .map(|first| first.to_ascii_uppercase().to_string() + chars.as_str()) + .unwrap_or_default() +} + +fn render_async_function_js(output: &mut String, function: &ProjectedAsyncFunction) { + let runtime_method = match function.kind { + AsyncIoKind::Read => "beginReadFile", + AsyncIoKind::Write => "beginWriteFile", + }; + output.push_str(&format!( + "function {}(file, buffer, offset, signal) {{\n try {{\n if (signal != null && (typeof signal.addEventListener !== 'function' || typeof signal.removeEventListener !== 'function' || typeof signal.aborted !== 'boolean')) return Promise.reject(new TypeError('signal must be an AbortSignal'))\n if (signal?.aborted) {{ const error = new Error('The operation was aborted'); error.name = 'AbortError'; return Promise.reject(error) }}\n }} catch (error) {{ return Promise.reject(error) }}\n let operation\n let aborted = false\n const abort = () => {{ aborted = true; operation?.cancel() }}\n try {{ signal?.addEventListener('abort', abort, {{ once: true }}) }} catch (error) {{ return Promise.reject(error) }}\n try {{ operation = DynWin32.{}(file, buffer, offset ?? 0n) }} catch (error) {{ signal?.removeEventListener('abort', abort); return Promise.reject(error) }}\n if (aborted) operation.cancel()\n const promise = new Promise((resolve, reject) => {{\n try {{ operation.start((error, bytesTransferred) => error ? reject(error) : resolve(bytesTransferred)) }} catch (error) {{ reject(error) }}\n }})\n return promise.catch((cause) => {{\n const message = String(cause?.message ?? '')\n const cancelled = message.includes('Win32 error 995') || message.includes('OVERLAPPED operation was aborted')\n if (!aborted || !cancelled) throw cause\n const error = new Error('The operation was aborted')\n error.name = 'AbortError'\n error.cause = cause\n throw error\n }}).finally(() => signal?.removeEventListener('abort', abort))\n}}\nexports.{0} = {0}\n", + function.js_name, runtime_method + )); +} + +fn unsafe_runtime_import(runtime_import: &str) -> String { + if runtime_import.ends_with("/win32") { + format!("{runtime_import}/unsafe") + } else if let Some(prefix) = runtime_import.strip_suffix("win32.js") { + format!("{prefix}win32-unsafe.js") + } else { + format!("{runtime_import}/unsafe") + } +} + +fn render_function_js(output: &mut String, function: &ProjectedFunction, apis: &ProjectedApis) { + let plan_name = format!("_{}Plan", function.js_name); + let bind_name = format!("_bind{}Plan", function.metadata_name); + output.push_str(&format!( + "let {plan_name}\nfunction {bind_name}() {{\n return {plan_name} ??= DynWin32Function.bind({{\n dll: {:?},\n entryPoint: {:?},\n parameters: [\n", + function.runtime.dll, function.runtime.entry_point + )); + for parameter in &function.runtime.parameters { + output.push_str(&format!( + " {{ type: {:?}, direction: {:?}, nullable: {}, cleanup: {:?}, consumesResource: {}, resourceCleanup: {:?}, aggregateDescriptor: {} }},\n", + abi_name(parameter.abi), + direction_name(parameter.direction), + parameter.nullable, + cleanup_name(parameter.cleanup), + parameter.consumes_resource, + cleanup_name(parameter.resource_cleanup), + parameter + .aggregate + .as_ref() + .map(native_layout_descriptor_js) + .unwrap_or_else(|| "undefined".into()), + )); + } + output.push_str(&format!( + " ],\n returnType: {:?},\n returnCleanup: {:?},\n successRule: {:?},\n captureLastError: {},\n callingConvention: {:?},\n returnAggregateDescriptor: {},\n }})\n}}\n", + function + .runtime + .return_abi + .map(abi_name) + .unwrap_or("void"), + cleanup_name(function.runtime.return_cleanup), + success_name(function.runtime.success_rule), + function.runtime.capture_last_error, + calling_convention_name(function.runtime.calling_convention), + function + .runtime + .return_aggregate + .as_ref() + .map(native_layout_descriptor_js) + .unwrap_or_else(|| "undefined".into()), + )); + output.push_str(&format!( + "function {}({}) {{\n", + function.js_name, + function + .parameters + .iter() + .map(|parameter| parameter.name.as_str()) + .collect::>() + .join(", ") + )); + for parameter in &function.parameters { + if let Some(minimum) = parameter.minimum_bytes { + output.push_str(&format!( + " if ({} != null && {}.byteLength < {}) throw new RangeError({:?})\n", + parameter.name, + parameter.name, + minimum, + format!( + "{} must contain at least {minimum} bytes for its native fixed-size contract", + parameter.name + ), + )); + } + } + let arguments = function + .inputs + .iter() + .map(|input| render_input(input, function)) + .collect::>() + .join(", "); + let output_aggregates = function + .inputs + .iter() + .filter_map(|input| { + let InputExpression::NativeAggregate { + parameter_index, + layout, + by_value: false, + .. + } = input + else { + return None; + }; + apis.native_builders + .iter() + .find(|builder| builder.layout_name == layout.name && !builder.outputs.is_empty()) + .map(|_| (&function.parameters[*parameter_index].name, &layout.name)) + }) + .collect::>(); + for (parameter, layout) in &output_aggregates { + output.push_str(&format!( + " DynWin32.prepareNativeStructCall({parameter}, _nativeLayout_{layout})\n" + )); + } + output.push_str(&format!( + " const _call = {bind_name}().invoke([{arguments}])\n const _return = _call.returnValue\n const _outputs = _call.outputs\n" + )); + for (parameter, layout) in &output_aggregates { + output.push_str(&format!( + " DynWin32.markNativeStructCallResult({parameter}, _nativeLayout_{layout}, _call.succeeded)\n" + )); + } + match &function.return_shape { + ReturnShape::Void => output.push_str(" return undefined\n"), + ReturnShape::Direct { typ, conversion } => { + if let SurfaceType::NativeStruct(name) = typ { + output.push_str(&format!( + " return DynWin32.toNativeStruct(_return, _nativeLayout_{name})\n" + )); + } else { + output.push_str(&format!( + " return {}\n", + render_output_conversion(*conversion, "_return") + )); + } + } + ReturnShape::Object { + status, + return_value, + outputs, + last_error, + } => { + output.push_str(" return {\n"); + if *status { + output.push_str(" status: DynWin32.toNumber(_return),\n"); + } else if let Some((typ, conversion)) = return_value { + if let SurfaceType::NativeStruct(name) = typ { + output.push_str(&format!( + " result: DynWin32.toNativeStruct(_return, _nativeLayout_{name}),\n" + )); + } else { + output.push_str(&format!( + " result: {},\n", + render_output_conversion(*conversion, "_return") + )); + } + } + for result in outputs { + output.push_str(&format!( + " {}: {},\n", + result.name, + render_output_conversion( + result.conversion, + &format!("_outputs[{}]", result.output_index) + ) + )); + } + if *last_error { + output.push_str(" lastError: _call.lastError,\n"); + } + output.push_str(" }\n"); + } + } + output.push_str("}\n"); + output.push_str(&format!("exports.{0} = {0}\n", function.js_name)); + if let Some(alias) = &function.unicode_alias { + output.push_str(&format!( + "const {alias} = {}\nexports.{alias} = {alias}\n", + function.js_name + )); + } +} + +fn render_input(input: &InputExpression, function: &ProjectedFunction) -> String { + match input { + InputExpression::Surface { + parameter_index, + conversion, + } => { + let parameter = &function.parameters[*parameter_index]; + let value = ¶meter.name; + match conversion { + Conversion::Boolean => format!("DynWin32.bool32({value})"), + Conversion::Boolean8 => format!("DynWin32.bool8({value})"), + Conversion::I8 => format!("DynWin32.i8({value})"), + Conversion::U8 => format!("DynWin32.u8({value})"), + Conversion::I16 => format!("DynWin32.i16({value})"), + Conversion::U16 => format!("DynWin32.u16({value})"), + Conversion::I32 => format!("DynWin32.i32({value})"), + Conversion::U32 => format!("DynWin32.u32({value})"), + Conversion::I64 => format!("DynWin32.i64({value})"), + Conversion::U64 => format!("DynWin32.u64({value})"), + Conversion::F32 => format!("DynWin32.f32({value})"), + Conversion::F64 => format!("DynWin32.f64({value})"), + Conversion::Handle => { + format!("DynWin32.handle({value}, {})", parameter.nullable) + } + Conversion::DataPointer => { + if parameter.alignment.unwrap_or(1) > 1 { + format!( + "DynWin32.alignedDataPointer({value}, {}, {})", + parameter.alignment.unwrap(), + parameter.nullable + ) + } else { + format!("DynWin32.dataPointer({value}, {})", parameter.nullable) + } + } + Conversion::WideString => { + format!("DynWin32.wideString({value}, {})", parameter.nullable) + } + Conversion::AnsiString => { + format!("DynWin32.ansiString({value}, {})", parameter.nullable) + } + Conversion::WideMultiString => { + format!("DynWin32.wideMultiString({value}, {})", parameter.nullable) + } + Conversion::AnsiMultiString => { + format!("DynWin32.ansiMultiString({value}, {})", parameter.nullable) + } + Conversion::ResourceInput(cleanup) => { + format!("DynWin32.resource({value}, {:?})", cleanup_name(*cleanup)) + } + Conversion::BigInt => format!("DynWin32.handle({value})"), + Conversion::Number | Conversion::Resource | Conversion::NativeAggregate => { + unreachable!("result conversion cannot be an input") + } + } + } + InputExpression::BufferLength { + parameter_index, + divisor, + abi, + } => { + let value = &function.parameters[*parameter_index].name; + format!( + "DynWin32.{}(_bufferCount({}, {}))", + abi_name(*abi), + value, + divisor + ) + } + InputExpression::NullPointer => "DynWin32.nullPointer()".into(), + InputExpression::Zero(abi) => render_zero(*abi), + InputExpression::NativeAggregate { + parameter_index, + layout, + nullable, + by_value, + } => { + let value = &function.parameters[*parameter_index].name; + if *by_value { + format!( + "DynWin32.nativeStructValue({value}, _nativeLayout_{})", + layout.name + ) + } else { + format!( + "DynWin32.nativeStruct({value}, _nativeLayout_{}, {nullable})", + layout.name + ) + } + } + InputExpression::ScalarPointer { + parameter_index, + scalar, + nullable, + } => { + let value = &function.parameters[*parameter_index].name; + let kind = scalar_pointer_kind(*scalar); + let alignment = scalar_pointer_alignment(*scalar); + let converted = format!( + "DynWin32.alignedDataPointer(_scalarPointer({kind:?}, {value}), {alignment}, false)" + ); + if *nullable { + format!("({value} == null ? DynWin32.nullPointer() : {converted})") + } else { + converted + } + } + InputExpression::ComInterface { + parameter_index, + iid, + } => { + let value = &function.parameters[*parameter_index].name; + format!( + "DynWin32.comObject({value}, {iid:?}, {})", + function.parameters[*parameter_index].nullable + ) + } + InputExpression::StringPointerPointer { + parameter_index, + encoding, + nullable, + } => { + let value = &function.parameters[*parameter_index].name; + match encoding { + StringEncoding::Wide => { + format!("DynWin32.wideStringPointerPointer({value}, {nullable})") + } + StringEncoding::Ansi => { + format!("DynWin32.ansiStringPointerPointer({value}, {nullable})") + } + } + } + } +} + +fn render_output_conversion(conversion: Conversion, value: &str) -> String { + match conversion { + Conversion::Boolean => format!("DynWin32.toBoolean({value})"), + Conversion::Number => format!("DynWin32.toNumber({value})"), + Conversion::BigInt => format!("DynWin32.toBigint({value})"), + Conversion::Resource => format!("DynWin32.toResource({value})"), + Conversion::NativeAggregate => { + unreachable!("native aggregate output requires its descriptor") + } + Conversion::I8 + | Conversion::Boolean8 + | Conversion::U8 + | Conversion::I16 + | Conversion::U16 + | Conversion::I32 + | Conversion::U32 + | Conversion::I64 + | Conversion::U64 + | Conversion::F32 + | Conversion::F64 + | Conversion::Handle + | Conversion::DataPointer + | Conversion::WideString + | Conversion::AnsiString + | Conversion::WideMultiString + | Conversion::AnsiMultiString + | Conversion::ResourceInput(_) => unreachable!("input conversion cannot be a result"), + } +} + +fn render_dts(apis: &ProjectedApis, runtime_import: &str) -> String { + let mut output = String::new(); + output.push_str("// Generated by dynwinrt-codegen - do not edit\n"); + output.push_str(&format!( + "import type {{ DynWin32NativeStruct, DynWin32Resource, DynWinRtValue }} from {runtime_import:?}\n" + )); + for definition in &apis.enums { + output.push_str(&format!( + "import type {{ {} }} from './{}.js'\n", + definition.name, definition.name + )); + } + output.push('\n'); + + let mut handles = BTreeSet::new(); + if !apis.async_functions.is_empty() { + handles.insert("HANDLE".to_string()); + } + for function in &apis.functions { + for parameter in &function.parameters { + collect_handle_alias(¶meter.typ, &mut handles); + } + match &function.return_shape { + ReturnShape::Direct { typ, .. } => collect_handle_alias(typ, &mut handles), + ReturnShape::Object { + return_value, + outputs, + .. + } => { + if let Some((typ, _)) = return_value { + collect_handle_alias(typ, &mut handles); + } + for output in outputs { + collect_handle_alias(&output.typ, &mut handles); + } + } + ReturnShape::Void => {} + } + } + for handle in handles { + output.push_str(&format!( + "export type {handle} = bigint | number | DynWin32Resource\n" + )); + } + for layout in collect_native_layouts(apis).values() { + if let Some(builder) = apis + .native_builders + .iter() + .find(|builder| builder.layout_name == layout.name) + { + output.push_str(&format!("export interface {}Init {{\n", builder.js_name)); + for field in &builder.fields { + let typ = match field.kind { + NativeBuilderFieldKind::Boolean => "boolean", + NativeBuilderFieldKind::DataPointer { nullable: true } => { + "Buffer | Uint8Array | null" + } + NativeBuilderFieldKind::DataPointer { nullable: false } => { + "Buffer | Uint8Array" + } + }; + output.push_str(&format!( + " {}{}: {}\n", + field.surface_name, + if field.optional { "?" } else { "" }, + typ + )); + } + output.push_str(&format!( + "}}\nexport type {0} = DynWin32NativeStruct\nexport declare function create{1}(init?: {1}Init): {0}\n", + layout.name, builder.js_name + )); + for field in &builder.outputs { + let (prefix, typ) = match field.kind { + NativeOutputFieldKind::U32 => ("get", "number"), + NativeOutputFieldKind::Resource { .. } => ("take", "DynWin32Resource | null"), + }; + output.push_str(&format!( + "export declare function {prefix}{}{}(value: {}): {typ}\n", + builder.js_name, + upper_first(&field.surface_name), + layout.name + )); + } + } else { + output.push_str(&format!( + "export type {} = DynWin32NativeStruct\nexport declare function create{}(bytes?: Buffer | Uint8Array): {}\n", + layout.name, layout.name, layout.name + )); + } + } + if !apis.functions.is_empty() { + output.push('\n'); + } + for function in &apis.functions { + let parameters = function + .parameters + .iter() + .map(|parameter| { + let mut typ = dts_type(¶meter.typ); + if parameter.nullable { + typ.push_str(" | null"); + } + format!("{}: {typ}", parameter.name) + }) + .collect::>() + .join(", "); + output.push_str(&format!( + "export declare function {}({parameters}): {}\n", + function.js_name, + dts_return_shape(&function.return_shape) + )); + if let Some(alias) = &function.unicode_alias { + output.push_str(&format!( + "export declare const {alias}: typeof {}\n", + function.js_name + )); + } + } + for function in &apis.async_functions { + output.push_str(&format!( + "export declare function {}(file: DynWin32Resource, buffer: Buffer, offset?: bigint, signal?: AbortSignal): Promise\n", + function.js_name + )); + } + output.push_str("\nexport declare const Apis: Readonly<{\n"); + for function in &apis.functions { + output.push_str(&format!( + " {}: typeof {}\n", + function.js_name, function.js_name + )); + if let Some(alias) = &function.unicode_alias { + output.push_str(&format!(" {alias}: typeof {alias}\n")); + } + } + for function in &apis.async_functions { + output.push_str(&format!( + " {}: typeof {}\n", + function.js_name, function.js_name + )); + } + output.push_str("}>\n"); + output +} + +fn collect_handle_alias(typ: &SurfaceType, handles: &mut BTreeSet) { + if let SurfaceType::Handle(name) = typ { + handles.insert(name.clone()); + } +} + +fn dts_return_shape(shape: &ReturnShape) -> String { + match shape { + ReturnShape::Void => "void".into(), + ReturnShape::Direct { typ, .. } => dts_type(typ), + ReturnShape::Object { + status, + return_value, + outputs, + last_error, + } => { + let mut fields = Vec::new(); + if *status { + fields.push("readonly status: number".into()); + } else if let Some((typ, _)) = return_value { + fields.push(format!("readonly result: {}", dts_type(typ))); + } + fields.extend( + outputs + .iter() + .map(|output| format!("readonly {}: {}", output.name, dts_type(&output.typ))), + ); + if *last_error { + fields.push("readonly lastError: number".into()); + } + format!("{{ {} }}", fields.join("; ")) + } + } +} + +fn dts_type(typ: &SurfaceType) -> String { + match typ { + SurfaceType::Boolean => "boolean".into(), + SurfaceType::Number => "number".into(), + SurfaceType::BigInt => "bigint".into(), + SurfaceType::Enum(name) | SurfaceType::Handle(name) => name.clone(), + SurfaceType::Buffer => "Buffer | Uint8Array".into(), + SurfaceType::String(StringEncoding::Wide | StringEncoding::Ansi) => { + "string | Buffer | Uint8Array".into() + } + SurfaceType::MultiString(StringEncoding::Wide | StringEncoding::Ansi) => { + "string | readonly string[] | Buffer | Uint8Array".into() + } + SurfaceType::ManagedResource => "DynWin32Resource".into(), + SurfaceType::Resource => "DynWin32Resource | null".into(), + SurfaceType::NativeStruct(name) | SurfaceType::NativeUnion(name) => name.clone(), + SurfaceType::ComInterface(_) => "DynWinRtValue".into(), + } +} + +fn render_zero(abi: AbiType) -> String { + match abi { + AbiType::Bool32 => "DynWin32.bool32(false)".into(), + AbiType::I8 => "DynWin32.i8(0)".into(), + AbiType::U8 => "DynWin32.u8(0)".into(), + AbiType::I16 => "DynWin32.i16(0)".into(), + AbiType::U16 => "DynWin32.u16(0)".into(), + AbiType::I32 => "DynWin32.i32(0)".into(), + AbiType::U32 => "DynWin32.u32(0)".into(), + AbiType::I64 => "DynWin32.i64(0n)".into(), + AbiType::U64 => "DynWin32.u64(0n)".into(), + AbiType::F32 => "DynWin32.f32(0)".into(), + AbiType::F64 => "DynWin32.f64(0)".into(), + AbiType::Pointer => "DynWin32.nullPointer()".into(), + AbiType::FunctionPointer | AbiType::Handle => "DynWin32.handle(0n, true)".into(), + } +} + +fn collect_native_layouts(apis: &ProjectedApis) -> BTreeMap { + let mut layouts = BTreeMap::new(); + for function in &apis.functions { + for input in &function.inputs { + if let InputExpression::NativeAggregate { layout, .. } = input { + layouts + .entry(layout.name.clone()) + .or_insert_with(|| layout.clone()); + } + } + if let Some(layout) = &function.runtime.return_aggregate { + layouts + .entry(layout.name.clone()) + .or_insert_with(|| layout.clone()); + } + } + layouts +} + +fn native_layout_descriptor_js(layout: &NativeLayout) -> String { + let descriptor = format!( + "{{\"name\":\"{}.{}\",\"kind\":\"{}\",\"x86\":{},\"x64\":{},\"arm64\":{}}}", + layout.namespace, + layout.name, + match layout.kind { + super::ir::NativeAggregateKind::Struct => "struct", + super::ir::NativeAggregateKind::Union => "union", + }, + native_architecture_json(&layout.x86), + native_architecture_json(&layout.x64), + native_architecture_json(&layout.arm64), + ); + format!( + "'{}'", + descriptor.replace('\\', "\\\\").replace('\'', "\\'") + ) +} + +fn native_architecture_json(layout: &NativeArchitectureLayout) -> String { + let fields = layout + .fields + .iter() + .map(|field| { + format!( + "{{\"name\":\"{}\",\"offset\":{},\"count\":{},\"type\":{}}}", + field.name, + field.offset, + field.count, + native_field_type_json(&field.typ) + ) + }) + .collect::>() + .join(","); + format!( + "{{\"size\":{},\"alignment\":{},\"fields\":[{}]}}", + layout.size, layout.alignment, fields + ) +} + +fn native_field_type_json(typ: &NativeFieldType) -> String { + match typ { + NativeFieldType::Scalar(scalar) => { + format!("{{\"kind\":\"{}\"}}", native_scalar_name(*scalar)) + } + NativeFieldType::Guid => "{\"kind\":\"guid\"}".into(), + NativeFieldType::Pointer => "{\"kind\":\"pointer\"}".into(), + NativeFieldType::Handle { cleanup } => format!( + "{{\"kind\":\"handle\",\"cleanup\":\"{}\"}}", + cleanup_name(*cleanup) + ), + NativeFieldType::Struct { name, layout, .. } => format!( + "{{\"kind\":\"struct\",\"name\":\"{name}\",\"layout\":{}}}", + native_architecture_json(layout) + ), + NativeFieldType::Union { name, layout, .. } => format!( + "{{\"kind\":\"union\",\"name\":\"{name}\",\"layout\":{}}}", + native_architecture_json(layout) + ), + } +} + +fn native_scalar_name(scalar: NativeScalar) -> &'static str { + match scalar { + NativeScalar::I8 => "i8", + NativeScalar::U8 => "u8", + NativeScalar::I16 => "i16", + NativeScalar::U16 => "u16", + NativeScalar::I32 => "i32", + NativeScalar::U32 => "u32", + NativeScalar::I64 => "i64", + NativeScalar::U64 => "u64", + NativeScalar::F32 => "f32", + NativeScalar::F64 => "f64", + NativeScalar::NativeIsize => "isize", + NativeScalar::NativeUsize => "usize", + } +} + +fn scalar_pointer_kind(scalar: super::ir::Scalar) -> &'static str { + match scalar { + super::ir::Scalar::Bool8 => "bool8", + super::ir::Scalar::Bool32 => "bool32", + super::ir::Scalar::I8 => "i8", + super::ir::Scalar::U8 => "u8", + super::ir::Scalar::I16 => "i16", + super::ir::Scalar::U16 => "u16", + super::ir::Scalar::I32 => "i32", + super::ir::Scalar::U32 => "u32", + super::ir::Scalar::I64 | super::ir::Scalar::NativeIsize => "i64", + super::ir::Scalar::U64 | super::ir::Scalar::NativeUsize => "u64", + super::ir::Scalar::F32 => "f32", + super::ir::Scalar::F64 => "f64", + } +} + +fn scalar_pointer_alignment(scalar: super::ir::Scalar) -> usize { + match scalar { + super::ir::Scalar::Bool32 + | super::ir::Scalar::I32 + | super::ir::Scalar::U32 + | super::ir::Scalar::F32 => 4, + super::ir::Scalar::Bool8 | super::ir::Scalar::I8 | super::ir::Scalar::U8 => 1, + super::ir::Scalar::I16 | super::ir::Scalar::U16 => 2, + super::ir::Scalar::I64 + | super::ir::Scalar::U64 + | super::ir::Scalar::F64 + | super::ir::Scalar::NativeIsize + | super::ir::Scalar::NativeUsize => 8, + } +} + +fn render_enum(definition: &EnumDefinition) -> (String, String) { + let mut js = String::from("// Generated by dynwinrt-codegen - do not edit\n'use strict'\n"); + js.push_str(&format!("const {} = Object.freeze({{\n", definition.name)); + for member in &definition.members { + js.push_str(&format!(" {}: {},\n", member.name, member.value)); + } + js.push_str(&format!("}})\nexports.{0} = {0}\n", definition.name)); + + let mut dts = String::from("// Generated by dynwinrt-codegen - do not edit\n"); + dts.push_str(&format!( + "export type {0} = (typeof {0})[keyof typeof {0}]\nexport declare const {0}: {{\n", + definition.name + )); + for member in &definition.members { + dts.push_str(&format!(" readonly {}: {}\n", member.name, member.value)); + } + dts.push_str("}\n"); + (js, dts) +} + +fn abi_name(typ: AbiType) -> &'static str { + match typ { + AbiType::Bool32 => "bool32", + AbiType::I8 => "i8", + AbiType::U8 => "u8", + AbiType::I16 => "i16", + AbiType::U16 => "u16", + AbiType::I32 => "i32", + AbiType::U32 => "u32", + AbiType::I64 => "i64", + AbiType::U64 => "u64", + AbiType::F32 => "f32", + AbiType::F64 => "f64", + AbiType::Pointer => "pointer", + AbiType::FunctionPointer => "functionPointer", + AbiType::Handle => "handle", + } +} + +fn direction_name(direction: Direction) -> &'static str { + match direction { + Direction::In => "in", + Direction::Out => "out", + Direction::InOut => "inout", + } +} + +fn cleanup_name(cleanup: Cleanup) -> &'static str { + match cleanup { + Cleanup::None => "none", + Cleanup::CloseHandle => "closeHandle", + Cleanup::RegCloseKey => "regCloseKey", + Cleanup::LocalFree => "localFree", + Cleanup::GlobalFree => "globalFree", + Cleanup::FreeLibrary => "freeLibrary", + Cleanup::CloseServiceHandle => "closeServiceHandle", + Cleanup::CoTaskMemFree => "coTaskMemFree", + Cleanup::CredFree => "credFree", + } +} + +fn success_name(success: super::ir::SuccessRule) -> &'static str { + match success { + super::ir::SuccessRule::Always => "always", + super::ir::SuccessRule::ReturnZero => "zero", + super::ir::SuccessRule::ReturnNonZero => "nonzero", + super::ir::SuccessRule::ReturnNonNull => "nonnull", + super::ir::SuccessRule::HResultSucceeded => "hresult", + super::ir::SuccessRule::SignedNonNegative => "nonnegative", + super::ir::SuccessRule::ReturnValidHandle => "validHandle", + } +} + +fn calling_convention_name(convention: super::ir::CallingConvention) -> &'static str { + match convention { + super::ir::CallingConvention::System => "system", + super::ir::CallingConvention::Cdecl => "cdecl", + } +} diff --git a/tools/dynwinrt-codegen/src/lib.rs b/tools/dynwinrt-codegen/src/lib.rs index 34795fa1..88a02dda 100644 --- a/tools/dynwinrt-codegen/src/lib.rs +++ b/tools/dynwinrt-codegen/src/lib.rs @@ -8,4 +8,5 @@ pub mod com_metadata; mod com_safe_array_registry; pub mod meta; pub mod types; +pub mod win32_metadata; pub mod xml_doc; diff --git a/tools/dynwinrt-codegen/src/main.rs b/tools/dynwinrt-codegen/src/main.rs index bfe2b110..291659c4 100644 --- a/tools/dynwinrt-codegen/src/main.rs +++ b/tools/dynwinrt-codegen/src/main.rs @@ -12,11 +12,13 @@ use dynwinrt_codegen::codegen::com; use dynwinrt_codegen::codegen::package; use dynwinrt_codegen::codegen::python; use dynwinrt_codegen::codegen::typescript; +use dynwinrt_codegen::codegen::win32; use dynwinrt_codegen::codegen::winrt::extensions::winui; 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::win32_metadata; use dynwinrt_codegen::xml_doc::DocTable; #[derive(Parser)] @@ -58,6 +60,17 @@ enum Commands { json: bool, }, + /// Measure safe flat Win32 export generation. + Win32Census { + /// Path(s) to Windows.Win32.winmd metadata, separated by ';'. + #[arg(long, value_name = "PATH")] + winmd: String, + + /// Emit one machine-readable JSON object. + #[arg(long)] + json: bool, + }, + /// Generate bindings from .winmd files #[command( long_about = "Parse .winmd metadata and generate typed binding files.\n\n\ @@ -140,6 +153,7 @@ enum Commands { } const COM_MANIFEST_FILE: &str = ".dynwinrt-com-manifest.json"; +const WIN32_MANIFEST_FILE: &str = ".dynwinrt-win32-manifest.json"; #[derive(Debug, Default, Deserialize, Serialize)] struct ComGenerationManifest { @@ -152,6 +166,18 @@ struct ComManifestUpdate { stale_files: BTreeSet, } +#[derive(Debug, Default, Deserialize, Serialize)] +struct Win32GenerationManifest { + version: u32, + roots: BTreeMap>, +} + +#[derive(Debug)] +struct Win32ManifestUpdate { + manifest: Win32GenerationManifest, + stale_files: BTreeSet, +} + #[derive(Debug, Serialize)] struct ComCensusResult { metadata: String, @@ -161,6 +187,16 @@ struct ComCensusResult { coverage_percent: f64, } +#[derive(Debug, Serialize)] +struct Win32CensusResult { + metadata: String, + eligible_functions: usize, + complete_functions: usize, + omitted_functions: usize, + coverage_percent: f64, + omission_reasons: BTreeMap, +} + fn main() { if let Err(e) = run() { eprintln!("error: {}", e); @@ -209,6 +245,93 @@ fn run_com_census(winmd: &str, json: bool) -> Result<(), String> { Ok(()) } +fn run_win32_census(winmd: &str, json: bool) -> Result<(), String> { + let functions = win32_metadata::parse_all_functions(winmd) + .ok_or_else(|| format!("Failed to load flat Win32 metadata from {winmd}"))?; + let mut containers = BTreeMap::<(String, String), Vec>::new(); + for function in functions.iter().cloned() { + containers + .entry((function.namespace.clone(), function.container.clone())) + .or_default() + .push(function); + } + let mut omission_reasons = BTreeMap::new(); + let mut complete = 0usize; + for ((namespace, class_name), functions) in containers { + let projection = win32::project_apis(&win32_metadata::RawApis { + namespace, + class_name, + functions, + }); + complete += projection.complete_count(); + for omission in projection.omitted { + *omission_reasons + .entry(win32_omission_reason_code(&omission.reason).to_string()) + .or_insert(0) += 1; + } + } + let result = Win32CensusResult { + metadata: winmd.to_string(), + eligible_functions: functions.len(), + complete_functions: complete, + omitted_functions: functions.len() - complete, + coverage_percent: if functions.is_empty() { + 0.0 + } else { + complete as f64 * 100.0 / functions.len() as f64 + }, + omission_reasons, + }; + if json { + println!( + "{}", + serde_json::to_string(&result) + .map_err(|error| format!("Failed to serialize Win32 census: {error}"))? + ); + } else { + println!( + "Flat Win32 complete functions: {}/{} ({:.6}%)", + result.complete_functions, result.eligible_functions, result.coverage_percent + ); + for (reason, count) in &result.omission_reasons { + println!(" {count:>5} {reason}"); + } + } + Ok(()) +} + +fn win32_omission_reason_code(reason: &str) -> &'static str { + if reason.contains("calling convention") { + "calling-convention" + } else if reason.contains("variadic") { + "variadic" + } else if reason.contains("both x64 and ARM64") { + "architecture" + } else if reason.contains("System32 DLL") { + "module-policy" + } else if reason.contains("callback thunk") { + "callback" + } else if reason.contains("cleanup") { + "cleanup" + } else if reason.contains("pointer return lifetime") { + "return-ownership" + } else if reason.contains("native buffer") || reason.contains("count parameter") { + "buffer-contract" + } else if reason.contains("writable pointer") { + "writable-pointer" + } else if reason.contains("NativeStruct") { + "native-layout" + } else if reason.contains("pointer depth") || reason.contains("void is not") { + "pointer-contract" + } else if reason.contains("enum underlying") { + "enum-abi" + } else if reason.contains("unknown") || reason.contains("Unknown") { + "unknown-native-type" + } else { + "other" + } +} + fn parse_class_requests( class_names: &str, default_namespace: Option<&str>, @@ -246,6 +369,9 @@ fn run() -> Result<(), String> { Commands::ComCensus { winmd, json } => { run_com_census(&winmd, json)?; } + Commands::Win32Census { winmd, json } => { + run_win32_census(&winmd, json)?; + } Commands::Generate { winmd, winmd_list, @@ -386,11 +512,16 @@ fn run() -> Result<(), String> { if let Some(ref cls_arg) = class_name { let class_requests = parse_class_requests(cls_arg, namespace.as_deref())?; - // First: partition into WinRT classes and classic-COM interfaces. + // First: partition into WinRT, Classic COM, and flat Win32 domains. let mut classes = Vec::new(); let mut com_interfaces: Vec = Vec::new(); let mut com_coclasses: Vec = Vec::new(); + let mut win32_apis: Vec = Vec::new(); for (ns, cls) in &class_requests { + if let Some(apis) = win32_metadata::parse_apis(&winmd, ns, cls) { + win32_apis.push(apis); + continue; + } 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 @@ -440,7 +571,11 @@ fn run() -> Result<(), String> { // 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() || !com_coclasses.is_empty()) { + if lang != "js" + && (!com_interfaces.is_empty() + || !com_coclasses.is_empty() + || !win32_apis.is_empty()) + { let mut offenders: Vec = Vec::new(); for ci in &com_interfaces { offenders.push(format!( @@ -454,6 +589,12 @@ fn run() -> Result<(), String> { coclass.namespace, coclass.name )); } + for apis in &win32_apis { + offenders.push(format!( + "{}.{} (flat Win32 DllImport container)", + apis.namespace, apis.class_name + )); + } return Err(format!( "`--lang {}` is not supported for classic-COM interfaces \ (they emit only `.js` + `.d.ts` today). \ @@ -466,6 +607,18 @@ fn run() -> Result<(), String> { )); } + if !win32_apis.is_empty() { + let runtime_import = if import_name == "@microsoft/dynwinrt" { + "@microsoft/dynwinrt/win32".to_string() + } else { + import_name.clone() + }; + generate_win32_apis_batch(output_dir, &win32_apis, &runtime_import, dry_run)?; + if classes.is_empty() && com_interfaces.is_empty() && com_coclasses.is_empty() { + return Ok(()); + } + } + // Classic COM occupies its own ESM subpackage so its symbols // cannot collide with or leak into the WinRT root barrel. if !com_interfaces.is_empty() || !com_coclasses.is_empty() { @@ -679,6 +832,29 @@ fn run() -> Result<(), String> { } } } else { + if let Some(flat_namespace) = namespace + .as_deref() + .filter(|namespace| namespace.starts_with("Windows.Win32.")) + .filter(|namespace| { + com_metadata::first_classic_com_interface_in_namespace(&winmd, namespace) + .is_none() + }) + && let Some(apis) = win32_metadata::parse_apis(&winmd, flat_namespace, "Apis") + { + if lang != "js" { + return Err(format!( + "`--lang {lang}` is not supported for flat Win32 namespace \ + `{flat_namespace}`; re-run with `--lang js`" + )); + } + let runtime_import = if import_name == "@microsoft/dynwinrt" { + "@microsoft/dynwinrt/win32".to_string() + } else { + import_name.clone() + }; + generate_win32_apis_batch(output_dir, &[apis], &runtime_import, dry_run)?; + return Ok(()); + } if lang == "py" && !dry_run { clean_python_generated_output(output_dir)?; } @@ -1319,6 +1495,257 @@ fn write_js_barrel_and_manifest(output_dir: &Path, index_content: &str) -> Resul Ok(()) } +fn generate_win32_apis_batch( + output_dir: &Path, + apis: &[win32_metadata::RawApis], + runtime_import: &str, + dry_run: bool, +) -> Result<(), String> { + let win32_output_dir = output_dir.join("win32"); + let mut planned_files = BTreeMap::::new(); + let mut root_files = BTreeMap::>::new(); + let mut namespaces = BTreeSet::new(); + + for apis in apis { + let (generated, omitted) = win32::generate_apis_files(apis, runtime_import); + for omission in &omitted { + eprintln!( + "warning: dynwinrt-codegen: omitting flat Win32 export `{}` - {}", + omission.identity, omission.reason + ); + } + if generated.js.lines().all(|line| { + !line.trim_start().starts_with("exports.") + || line.trim_start().starts_with("exports.Apis") + }) { + return Err(format!( + "Flat Win32 codegen for {}.{} produced no complete safe exports", + apis.namespace, apis.class_name + )); + } + + namespaces.insert(apis.namespace.clone()); + let root = format!("{}.{}", apis.namespace, apis.class_name); + let mut files = vec![ + (format!("{}.js", apis.class_name), generated.js), + (format!("{}.d.ts", apis.class_name), generated.dts), + ]; + files.extend(generated.extra_files); + for (name, content) in files { + let relative = format!("{}/{}", apis.namespace, name); + if let Some(existing) = planned_files.insert(relative.clone(), content.clone()) + && existing != content + { + return Err(format!( + "Flat Win32 generation produced conflicting `{relative}` outputs" + )); + } + root_files.entry(root.clone()).or_default().insert(relative); + } + } + + if dry_run { + for apis in apis { + println!( + "[dry-run] Would generate flat Win32 {}.{}", + apis.namespace, apis.class_name + ); + } + return Ok(()); + } + + fs::create_dir_all(&win32_output_dir).map_err(|error| { + format!( + "Failed to create flat Win32 output directory {}: {error}", + win32_output_dir.display() + ) + })?; + let manifest_update = prepare_win32_generation_manifest(&win32_output_dir, &root_files)?; + for (relative, content) in &planned_files { + let path = win32_output_dir.join(relative); + let parent = path + .parent() + .ok_or_else(|| format!("Flat Win32 output `{relative}` has no parent"))?; + fs::create_dir_all(parent) + .map_err(|error| format!("Failed to create {}: {error}", parent.display()))?; + fs::write(&path, content) + .map_err(|error| format!("Failed to write {}: {error}", path.display()))?; + } + apply_win32_generation_manifest(&win32_output_dir, manifest_update)?; + for namespace in namespaces { + write_win32_namespace_index(&win32_output_dir.join(namespace))?; + } + write_win32_root_index(&win32_output_dir)?; + write_bindings_manifest(output_dir)?; + Ok(()) +} + +fn prepare_win32_generation_manifest( + win32_output_dir: &Path, + updated_roots: &BTreeMap>, +) -> Result { + let path = win32_output_dir.join(WIN32_MANIFEST_FILE); + let mut manifest = if path.exists() { + let content = fs::read_to_string(&path) + .map_err(|error| format!("Failed to read {}: {error}", path.display()))?; + serde_json::from_str::(&content).map_err(|error| { + format!( + "Invalid flat Win32 generation manifest {}: {error}", + path.display() + ) + })? + } else { + Win32GenerationManifest { + version: 1, + roots: BTreeMap::new(), + } + }; + if manifest.version != 1 { + return Err(format!( + "Unsupported flat Win32 generation manifest version {} in {}", + manifest.version, + path.display() + )); + } + for files in manifest.roots.values().chain(updated_roots.values()) { + for relative in files { + validate_win32_manifest_path(relative, &path)?; + } + } + + let previous_files = updated_roots + .keys() + .filter_map(|root| manifest.roots.get(root)) + .flatten() + .cloned() + .collect::>(); + for (root, files) in updated_roots { + manifest.roots.insert(root.clone(), files.clone()); + } + let retained_files = manifest + .roots + .values() + .flatten() + .cloned() + .collect::>(); + let stale_files = previous_files + .difference(&retained_files) + .cloned() + .collect(); + Ok(Win32ManifestUpdate { + manifest, + stale_files, + }) +} + +fn validate_win32_manifest_path(relative: &str, manifest: &Path) -> Result<(), String> { + let path = Path::new(relative); + if path.is_absolute() + || path + .components() + .any(|component| !matches!(component, std::path::Component::Normal(_))) + || !(relative.ends_with(".js") || relative.ends_with(".d.ts")) + { + return Err(format!( + "Refusing unsafe path `{relative}` in flat Win32 generation manifest {}", + manifest.display() + )); + } + Ok(()) +} + +fn apply_win32_generation_manifest( + win32_output_dir: &Path, + update: Win32ManifestUpdate, +) -> Result<(), String> { + for relative in &update.stale_files { + let path = win32_output_dir.join(relative); + if path.exists() { + fs::remove_file(&path) + .map_err(|error| format!("Failed to remove {}: {error}", path.display()))?; + } + } + let path = win32_output_dir.join(WIN32_MANIFEST_FILE); + let content = serde_json::to_string_pretty(&update.manifest) + .map_err(|error| format!("Failed to serialize flat Win32 manifest: {error}"))?; + fs::write(&path, format!("{content}\n")) + .map_err(|error| format!("Failed to write {}: {error}", path.display())) +} + +fn write_win32_namespace_index(namespace_dir: &Path) -> Result<(), String> { + let mut modules = fs::read_dir(namespace_dir) + .map_err(|error| format!("Failed to read {}: {error}", namespace_dir.display()))? + .flatten() + .filter_map(|entry| { + let path = entry.path(); + let name = path.file_stem()?.to_str()?.to_string(); + (path.extension()?.to_str()? == "js" && name != "index").then_some(name) + }) + .collect::>(); + modules.remove("index"); + let mut js = String::from("// Generated by dynwinrt-codegen - do not edit\n'use strict'\n"); + let mut mjs = String::from("// Generated by dynwinrt-codegen - do not edit\n"); + let mut dts = String::from("// Generated by dynwinrt-codegen - do not edit\n"); + let mut exported = BTreeMap::::new(); + for module in modules { + js.push_str(&format!( + "Object.assign(exports, require('./{module}.js'))\n" + )); + dts.push_str(&format!("export * from './{module}.js'\n")); + let content = fs::read_to_string(namespace_dir.join(format!("{module}.js"))) + .map_err(|error| format!("Failed to read flat Win32 module `{module}`: {error}"))?; + let exports = collect_com_cjs_exports(&content); + if !exports.is_empty() { + let binding = format!("_module_{}", normalize_python_package_name(&module)); + mjs.push_str(&format!("import {binding} from './{module}.js'\n")); + for name in exports { + if let Some(existing) = exported.insert(name.clone(), module.clone()) { + return Err(format!( + "Flat Win32 namespace export `{name}` is ambiguous between `{existing}` and `{module}`" + )); + } + mjs.push_str(&format!("export const {name} = {binding}.{name}\n")); + } + } + } + fs::write(namespace_dir.join("index.js"), js) + .map_err(|error| format!("Failed to write Win32 namespace index: {error}"))?; + fs::write(namespace_dir.join("index.mjs"), mjs) + .map_err(|error| format!("Failed to write Win32 namespace ESM index: {error}"))?; + fs::write(namespace_dir.join("index.d.ts"), dts) + .map_err(|error| format!("Failed to write Win32 namespace declarations: {error}")) +} + +fn write_win32_root_index(win32_output_dir: &Path) -> Result<(), String> { + let namespaces = fs::read_dir(win32_output_dir) + .map_err(|error| format!("Failed to read {}: {error}", win32_output_dir.display()))? + .flatten() + .filter(|entry| entry.path().is_dir() && entry.path().join("index.js").is_file()) + .filter_map(|entry| entry.file_name().to_str().map(str::to_string)) + .collect::>(); + let mut js = String::from("// Generated by dynwinrt-codegen - do not edit\n'use strict'\n"); + let mut mjs = String::from("// Generated by dynwinrt-codegen - do not edit\n"); + let mut dts = String::from("// Generated by dynwinrt-codegen - do not edit\n"); + for namespace in namespaces { + let identifier = namespace.replace('.', "_"); + js.push_str(&format!( + "exports.{identifier} = require('./{namespace}/index.js')\n" + )); + mjs.push_str(&format!( + "export * as {identifier} from './{namespace}/index.mjs'\n" + )); + dts.push_str(&format!( + "export * as {identifier} from './{namespace}/index.js'\n" + )); + } + fs::write(win32_output_dir.join("index.js"), js) + .map_err(|error| format!("Failed to write flat Win32 root index: {error}"))?; + fs::write(win32_output_dir.join("index.mjs"), mjs) + .map_err(|error| format!("Failed to write flat Win32 root ESM index: {error}"))?; + fs::write(win32_output_dir.join("index.d.ts"), dts) + .map_err(|error| format!("Failed to write flat Win32 root declarations: {error}")) +} + fn prepare_com_generation_manifest( com_output_dir: &Path, updated_roots: &BTreeMap>, @@ -1635,16 +2062,35 @@ fn write_bindings_manifest(output_dir: &Path) -> Result<(), String> { BTreeSet::new() }; let com_subpath_names = collect_com_subpath_names(&output_dir.join("com"))?; + let win32_subpath_names = collect_win32_subpath_names(&output_dir.join("win32"))?; let content = package::render_bindings_package_json(&package::BindingsPackageManifestInput { has_winrt_root, winrt_subpath_names: &winrt_subpath_names, com_subpath_names: &com_subpath_names, + win32_subpath_names: &win32_subpath_names, }); let path = output_dir.join("package.json"); fs::write(&path, content) .map_err(|error| format!("Failed to write {}: {error}", path.display())) } +fn collect_win32_subpath_names(win32_output_dir: &Path) -> Result, String> { + if !win32_output_dir.is_dir() { + return Ok(BTreeSet::new()); + } + Ok(fs::read_dir(win32_output_dir) + .map_err(|error| { + format!( + "Failed to read flat Win32 output directory {}: {error}", + win32_output_dir.display() + ) + })? + .flatten() + .filter(|entry| entry.path().is_dir() && entry.path().join("index.d.ts").is_file()) + .filter_map(|entry| entry.file_name().to_str().map(str::to_string)) + .collect()) +} + fn collect_com_subpath_names(com_output_dir: &Path) -> Result, String> { let index_path = com_output_dir.join("index.d.ts"); if !index_path.is_file() { @@ -3269,6 +3715,10 @@ fn print_capabilities() { "input.winmd-list", "input.ref-list", "selector.namespace-class", + "domain.classic-com", + "domain.flat-win32", + "census.classic-com", + "census.flat-win32", ] { println!("{}", capability); } @@ -3350,6 +3800,122 @@ mod tests { )) } + #[test] + fn win32_manifest_removes_only_files_owned_by_updated_roots() { + let output = test_directory("win32-manifest"); + fs::create_dir_all(output.join("Ns")).unwrap(); + fs::write(output.join("Ns").join("A.js"), "a").unwrap(); + fs::write(output.join("Ns").join("Shared.js"), "shared").unwrap(); + let manifest = Win32GenerationManifest { + version: 1, + roots: BTreeMap::from([ + ( + "Ns.A".into(), + BTreeSet::from(["Ns/A.js".into(), "Ns/Shared.js".into()]), + ), + ("Ns.B".into(), BTreeSet::from(["Ns/Shared.js".into()])), + ]), + }; + fs::write( + output.join(WIN32_MANIFEST_FILE), + serde_json::to_string(&manifest).unwrap(), + ) + .unwrap(); + + let update = prepare_win32_generation_manifest( + &output, + &BTreeMap::from([("Ns.A".into(), BTreeSet::from(["Ns/New.js".into()]))]), + ) + .unwrap(); + apply_win32_generation_manifest(&output, update).unwrap(); + assert!(!output.join("Ns").join("A.js").exists()); + assert!(output.join("Ns").join("Shared.js").exists()); + fs::remove_dir_all(output).unwrap(); + } + + #[test] + fn win32_manifest_rejects_parent_paths() { + let output = test_directory("win32-manifest-unsafe"); + fs::create_dir_all(&output).unwrap(); + fs::write( + output.join(WIN32_MANIFEST_FILE), + r#"{"version":1,"roots":{"bad":["../outside.js"]}}"#, + ) + .unwrap(); + let error = prepare_win32_generation_manifest( + &output, + &BTreeMap::from([("good".into(), BTreeSet::from(["Ns/A.js".into()]))]), + ) + .expect_err("unsafe paths must fail before generation writes"); + assert!(error.contains("Refusing unsafe path")); + fs::remove_dir_all(output).unwrap(); + } + + #[test] + fn win32_generation_preserves_isolated_incremental_namespace_packages() { + fn apis(namespace: &str, function_name: &str) -> win32_metadata::RawApis { + win32_metadata::RawApis { + namespace: namespace.into(), + class_name: "Apis".into(), + functions: vec![win32_metadata::RawFunction { + namespace: namespace.into(), + container: "Apis".into(), + name: function_name.into(), + dll: "kernel32.dll".into(), + entry_point: function_name.into(), + return_type: win32_metadata::RawType { + base: win32_metadata::RawBaseType::Scalar(win32_metadata::RawScalar::U32), + pointer_depth: 0, + constness: win32_metadata::RawConstness::Unspecified, + }, + parameters: Vec::new(), + return_status: win32_metadata::RawStatusSemantics::None, + return_free_with: None, + supports_last_error: false, + calling_convention: win32_metadata::RawCallingConvention::System, + architectures: win32_metadata::RawArchitectures { + x86: true, + x64: true, + arm64: true, + }, + variadic: false, + }], + } + } + + let output = test_directory("win32-incremental-namespaces"); + generate_win32_apis_batch( + &output, + &[apis("Tests.Win32.First", "FirstValue")], + "@microsoft/dynwinrt/win32", + false, + ) + .unwrap(); + generate_win32_apis_batch( + &output, + &[apis("Tests.Win32.Second", "SecondValue")], + "@microsoft/dynwinrt/win32", + false, + ) + .unwrap(); + + let win32 = output.join("win32"); + assert!(win32.join("Tests.Win32.First").join("Apis.js").is_file()); + assert!(win32.join("Tests.Win32.Second").join("Apis.js").is_file()); + let root_index = fs::read_to_string(win32.join("index.js")).unwrap(); + assert!(root_index.contains("Tests_Win32_First")); + assert!(root_index.contains("Tests_Win32_Second")); + let manifest = fs::read_to_string(win32.join(WIN32_MANIFEST_FILE)).unwrap(); + assert!(manifest.contains("Tests.Win32.First.Apis")); + assert!(manifest.contains("Tests.Win32.Second.Apis")); + let package = fs::read_to_string(output.join("package.json")).unwrap(); + assert!(package.contains("\"./win32\"")); + assert!(package.contains("\"./win32/Tests.Win32.First\"")); + assert!(package.contains("\"./win32/Tests.Win32.Second\"")); + + fs::remove_dir_all(output).unwrap(); + } + #[test] fn com_barrel_deduplicates_only_identical_pod_factories() { let descriptor = diff --git a/tools/dynwinrt-codegen/src/win32_metadata.rs b/tools/dynwinrt-codegen/src/win32_metadata.rs new file mode 100644 index 00000000..d9c8c505 --- /dev/null +++ b/tools/dynwinrt-codegen/src/win32_metadata.rs @@ -0,0 +1,1530 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Raw metadata facts for flat Win32 `[DllImport]` exports. +//! +//! This module preserves native facts only. Language projection and ABI +//! support decisions belong to `codegen::win32`. + +use std::collections::HashSet; + +use windows_metadata::{HasAttributes, reader}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RawScalar { + Bool8, + I8, + U8, + I16, + U16, + I32, + U32, + I64, + U64, + F32, + F64, + Char16, + Bool32, + NativeIsize, + NativeUsize, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RawConstness { + Const, + Mutable, + Unspecified, + Mixed, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RawDirection { + In, + Out, + InOut, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RawStringEncoding { + Utf16, + Ansi, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RawCallingConvention { + System, + Cdecl, + Unsupported, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RawStatusSemantics { + None, + ZeroIsSuccess, + SignedNonNegativeIsSuccess, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct RawArchitectures { + pub x86: bool, + pub x64: bool, + pub arm64: bool, +} + +impl RawArchitectures { + fn all() -> Self { + Self { + x86: true, + x64: true, + arm64: true, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawEnumMember { + pub name: String, + pub value: i128, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RawNamedKind { + Enum { + underlying: RawScalar, + members: Vec, + is_flags: bool, + }, + Handle { + cleanup: Option, + }, + StringPointer { + encoding: RawStringEncoding, + }, + DataPointer, + FunctionPointer, + Guid, + ComInterface { + iid: String, + }, + NativeStruct { + layout: Box, + }, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RawLayoutKind { + Sequential, + Union, + Unknown, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RawPacking { + Default, + Explicit(u16), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawNativeField { + pub name: String, + pub typ: RawType, + pub fixed_count: Option, + pub bitfield: bool, + pub flexible_array: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawNativeLayout { + pub architectures: RawArchitectures, + pub kind: RawLayoutKind, + pub packing: RawPacking, + pub declared_size: Option, + pub forced_alignment: Option, + pub fields: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawNativeLayoutSet { + pub recursive: bool, + pub variants: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RawBaseType { + Void, + Scalar(RawScalar), + Named { + namespace: String, + name: String, + kind: RawNamedKind, + }, + Unknown(String), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawType { + pub base: RawBaseType, + pub pointer_depth: u8, + pub constness: RawConstness, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RawBufferSize { + ElementCountParam(usize), + ByteCountParam(usize), + Constant(usize), + Unknown, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawBuffer { + pub element: RawType, + pub size: RawBufferSize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawParameter { + pub name: String, + pub typ: RawType, + pub direction: RawDirection, + pub nullable: bool, + pub reserved: bool, + pub null_null_terminated: bool, + pub buffer: Option, + pub free_with: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawFunction { + pub namespace: String, + pub container: String, + pub name: String, + pub dll: String, + pub entry_point: String, + pub return_type: RawType, + pub parameters: Vec, + pub return_status: RawStatusSemantics, + pub return_free_with: Option, + pub supports_last_error: bool, + pub calling_convention: RawCallingConvention, + pub architectures: RawArchitectures, + pub variadic: bool, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RawApis { + pub namespace: String, + pub class_name: String, + pub functions: Vec, +} + +pub fn parse_apis(winmd_paths: &str, namespace: &str, class_name: &str) -> Option { + let index = crate::meta::load_index(winmd_paths)?; + let definition = index.get(namespace, class_name).next()?; + let functions = definition + .methods() + .filter_map(|method| parse_function(&index, namespace, class_name, &method)) + .collect::>(); + (!functions.is_empty()).then(|| RawApis { + namespace: namespace.to_string(), + class_name: class_name.to_string(), + functions, + }) +} + +pub fn parse_all_functions(winmd_paths: &str) -> Option> { + let index = crate::meta::load_index(winmd_paths)?; + let mut functions = Vec::new(); + for definition in index.all() { + let namespace = definition.namespace().to_string(); + let container = definition.name().to_string(); + functions.extend( + definition + .methods() + .filter_map(|method| parse_function(&index, &namespace, &container, &method)), + ); + } + Some(functions) +} + +fn parse_function( + index: &reader::Index, + namespace: &str, + container: &str, + method: &reader::MethodDef, +) -> Option { + let import = method.impl_map()?; + if matches!(method.name(), ".ctor" | ".cctor") { + return None; + } + let signature = method.signature(&[]); + let (return_definition, definitions) = match params_by_sequence(method, signature.types.len()) { + Ok(definitions) => definitions, + Err(reason) => { + return Some(invalid_function( + namespace, + container, + method.name(), + import.import_scope().name(), + import.import_name(), + &reason, + )); + } + }; + + let return_type = map_type(index, &signature.return_type); + let mut parameters = Vec::with_capacity(definitions.len()); + for (position, (definition, typ)) in definitions.iter().zip(&signature.types).enumerate() { + let mapped = map_type(index, typ); + let (name, direction, nullable, reserved, null_null_terminated, buffer, free_with) = + match definition { + Some(definition) => { + let flags = definition.flags(); + ( + definition.name().to_string(), + param_direction(flags), + flags.contains(windows_metadata::ParamAttributes::Optional) + || definition.has_attribute("OptionalAttribute"), + definition.has_attribute("ReservedAttribute"), + definition.has_attribute("NullNullTerminatedAttribute"), + buffer_size(definition).map(|size| RawBuffer { + element: buffer_element(&mapped), + size, + }), + free_with(definition), + ) + } + None => ( + format!("arg{position}"), + RawDirection::In, + false, + false, + false, + None, + None, + ), + }; + parameters.push(RawParameter { + name, + typ: mapped, + direction, + nullable, + reserved, + null_null_terminated, + buffer, + free_with, + }); + } + apply_known_buffer_contracts(namespace, method.name(), &mut parameters); + + let flags = import.flags(); + Some(RawFunction { + namespace: namespace.to_string(), + container: container.to_string(), + name: method.name().to_string(), + dll: import.import_scope().name().to_string(), + entry_point: import.import_name().to_string(), + return_status: status_semantics(&signature.return_type, &return_type), + return_free_with: return_definition.as_ref().and_then(free_with), + return_type, + parameters, + supports_last_error: flags.contains(windows_metadata::PInvokeAttributes::SupportsLastError), + calling_convention: calling_convention(method), + architectures: architectures(method), + variadic: signature + .flags + .contains(windows_metadata::MethodCallAttributes::VARARG), + }) +} + +fn invalid_function( + namespace: &str, + container: &str, + name: &str, + dll: &str, + entry_point: &str, + reason: &str, +) -> RawFunction { + RawFunction { + namespace: namespace.to_string(), + container: container.to_string(), + name: name.to_string(), + dll: dll.to_string(), + entry_point: entry_point.to_string(), + return_type: RawType { + base: RawBaseType::Unknown(reason.to_string()), + pointer_depth: 0, + constness: RawConstness::Unspecified, + }, + parameters: Vec::new(), + return_status: RawStatusSemantics::None, + return_free_with: None, + supports_last_error: false, + calling_convention: RawCallingConvention::Unsupported, + architectures: RawArchitectures { + x86: false, + x64: false, + arm64: false, + }, + variadic: false, + } +} + +fn params_by_sequence<'a>( + method: &'a reader::MethodDef<'a>, + parameter_count: usize, +) -> Result< + ( + Option>, + Vec>>, + ), + String, +> { + let mut parameters = vec![None; parameter_count]; + let mut return_parameter = None; + for parameter in method.params() { + let sequence = parameter.sequence(); + if sequence == 0 { + if return_parameter.replace(parameter).is_some() { + return Err("duplicate return parameter sequence 0".into()); + } + continue; + } + let position = sequence as usize - 1; + let Some(slot) = parameters.get_mut(position) else { + return Err(format!( + "parameter sequence {sequence} exceeds signature arity {parameter_count}" + )); + }; + if slot.replace(parameter).is_some() { + return Err(format!("duplicate parameter sequence {sequence}")); + } + } + Ok((return_parameter, parameters)) +} + +fn param_direction(flags: windows_metadata::ParamAttributes) -> RawDirection { + match ( + flags.contains(windows_metadata::ParamAttributes::In), + flags.contains(windows_metadata::ParamAttributes::Out), + ) { + (true, true) => RawDirection::InOut, + (_, true) => RawDirection::Out, + _ => RawDirection::In, + } +} + +fn map_type(index: &reader::Index, typ: &windows_metadata::Type) -> RawType { + use windows_metadata::Type; + + match typ { + Type::Void => raw_base(RawBaseType::Void), + Type::Bool => raw_scalar(RawScalar::Bool8), + Type::Char => raw_scalar(RawScalar::Char16), + Type::I8 => raw_scalar(RawScalar::I8), + Type::U8 => raw_scalar(RawScalar::U8), + Type::I16 => raw_scalar(RawScalar::I16), + Type::U16 => raw_scalar(RawScalar::U16), + Type::I32 => raw_scalar(RawScalar::I32), + Type::U32 => raw_scalar(RawScalar::U32), + Type::I64 => raw_scalar(RawScalar::I64), + Type::U64 => raw_scalar(RawScalar::U64), + Type::F32 => raw_scalar(RawScalar::F32), + Type::F64 => raw_scalar(RawScalar::F64), + Type::ISize => raw_scalar(RawScalar::NativeIsize), + Type::USize => raw_scalar(RawScalar::NativeUsize), + Type::PtrMut(inner, depth) => map_pointer(index, inner, *depth, RawConstness::Mutable), + Type::PtrConst(inner, depth) => map_pointer(index, inner, *depth, RawConstness::Const), + Type::Name(name) => map_named(index, &name.namespace, &name.name, &mut Vec::new()), + other => RawType { + base: RawBaseType::Unknown(format!("{other:?}")), + pointer_depth: 0, + constness: RawConstness::Unspecified, + }, + } +} + +fn raw_base(base: RawBaseType) -> RawType { + RawType { + base, + pointer_depth: 0, + constness: RawConstness::Unspecified, + } +} + +fn raw_scalar(scalar: RawScalar) -> RawType { + raw_base(RawBaseType::Scalar(scalar)) +} + +fn map_pointer( + index: &reader::Index, + inner: &windows_metadata::Type, + depth: usize, + outer_constness: RawConstness, +) -> RawType { + let mut mapped = map_type(index, inner); + let Ok(depth) = u8::try_from(depth) else { + mapped.base = RawBaseType::Unknown("pointer depth exceeds u8".into()); + return mapped; + }; + mapped.pointer_depth = match mapped.pointer_depth.checked_add(depth) { + Some(depth) => depth, + None => { + mapped.base = RawBaseType::Unknown("pointer depth overflow".into()); + return mapped; + } + }; + mapped.constness = match (mapped.constness, outer_constness) { + (RawConstness::Unspecified, value) => value, + (value, RawConstness::Unspecified) => value, + (left, right) if left == right => left, + _ => RawConstness::Mixed, + }; + mapped +} + +fn map_named( + index: &reader::Index, + namespace: &str, + name: &str, + layout_stack: &mut Vec<(String, String)>, +) -> RawType { + if (namespace == "System" && name == "Guid") + || (namespace == "Windows.Win32.Foundation" && name == "GUID") + { + return named(namespace, name, RawNamedKind::Guid); + } + if namespace == "Windows.Win32.Foundation" { + match name { + "BOOL" => return raw_scalar(RawScalar::Bool32), + "BOOLEAN" => return raw_scalar(RawScalar::U8), + "HRESULT" | "NTSTATUS" | "LSTATUS" => return raw_scalar(RawScalar::I32), + "PWSTR" | "PCWSTR" | "LPWSTR" | "LPCWSTR" => { + return named_string( + namespace, + name, + RawStringEncoding::Utf16, + matches!(name, "PCWSTR" | "LPCWSTR"), + ); + } + "PSTR" | "PCSTR" | "LPSTR" | "LPCSTR" => { + return named_string( + namespace, + name, + RawStringEncoding::Ansi, + matches!(name, "PCSTR" | "LPCSTR"), + ); + } + "FARPROC" | "PROC" | "NEARPROC" => { + return named(namespace, name, RawNamedKind::FunctionPointer); + } + "BSTR" => { + return named(namespace, name, RawNamedKind::Unknown); + } + _ => {} + } + } + + if is_data_pointer_alias(name) { + return named(namespace, name, RawNamedKind::DataPointer); + } + if let Some(cleanup) = handle_cleanup(index, namespace, name) { + return named( + namespace, + name, + RawNamedKind::Handle { + cleanup: Some(cleanup), + }, + ); + } + if is_handle_alias(name) { + return named( + namespace, + name, + RawNamedKind::Handle { + cleanup: handle_cleanup(index, namespace, name), + }, + ); + } + + let Some(definition) = index.get(namespace, name).next() else { + return named(namespace, name, RawNamedKind::Unknown); + }; + let iid = crate::meta::extract_iid(&definition); + if definition.extends().is_none() && !iid.is_empty() { + return named(namespace, name, RawNamedKind::ComInterface { iid }); + } + let Some(extends) = definition.extends() else { + return named(namespace, name, RawNamedKind::Unknown); + }; + if extends.namespace() == "System" && extends.name() == "Enum" { + let (underlying, members) = parse_enum(&definition); + return named( + namespace, + name, + RawNamedKind::Enum { + underlying, + members, + is_flags: definition.has_attribute("FlagsAttribute"), + }, + ); + } + if extends.namespace() == "System" && matches!(extends.name(), "Delegate" | "MulticastDelegate") + { + return named(namespace, name, RawNamedKind::FunctionPointer); + } + if extends.namespace() == "System" && extends.name() == "ValueType" { + if definition.has_attribute("NativeTypedefAttribute") { + let fields = definition.fields().collect::>(); + if fields.len() == 1 && fields[0].name() == "Value" { + return map_transparent_typedef(index, namespace, name, &fields[0].ty()); + } + } + return named( + namespace, + name, + RawNamedKind::NativeStruct { + layout: Box::new(parse_native_layouts(index, namespace, name, layout_stack)), + }, + ); + } + + fn parse_native_layouts( + index: &reader::Index, + namespace: &str, + name: &str, + layout_stack: &mut Vec<(String, String)>, + ) -> RawNativeLayoutSet { + let key = (namespace.to_string(), name.to_string()); + if layout_stack.contains(&key) { + return RawNativeLayoutSet { + recursive: true, + variants: Vec::new(), + }; + } + layout_stack.push(key); + let variants = index + .get(namespace, name) + .filter(|definition| { + definition + .extends() + .is_some_and(|base| base.namespace() == "System" && base.name() == "ValueType") + }) + .map(|definition| { + let flags = definition.flags(); + let kind = if flags.contains(windows_metadata::TypeAttributes::SequentialLayout) { + RawLayoutKind::Sequential + } else if flags.contains(windows_metadata::TypeAttributes::ExplicitLayout) { + RawLayoutKind::Union + } else { + RawLayoutKind::Unknown + }; + let (packing, declared_size) = + definition + .class_layout() + .map_or((RawPacking::Default, None), |layout| { + ( + if layout.packing_size() == 0 { + RawPacking::Default + } else { + RawPacking::Explicit(layout.packing_size()) + }, + (layout.class_size() != 0).then_some(layout.class_size() as usize), + ) + }); + let fields = definition + .fields() + .filter(|field| field.name() != "value__" && field.constant().is_none()) + .map(|field| { + let field_type = field.ty(); + let (typ, fixed_count) = match field_type { + windows_metadata::Type::ArrayFixed(ref element, count) => ( + map_layout_field_type( + index, + element, + Some(definition), + layout_stack, + ), + Some(count), + ), + _ => ( + map_layout_field_type( + index, + &field_type, + Some(definition), + layout_stack, + ), + None, + ), + }; + let typ = known_native_field_type(&definition, field.name(), typ); + RawNativeField { + name: field.name().to_string(), + typ, + fixed_count, + bitfield: field.has_attribute("NativeBitfieldAttribute"), + flexible_array: field.has_attribute("FlexibleArrayAttribute"), + } + }) + .collect(); + RawNativeLayout { + architectures: architectures(&definition), + kind, + packing, + declared_size, + forced_alignment: definition.find_attribute("AlignmentAttribute").and_then( + |attribute| match attribute.value().first() { + Some((_, windows_metadata::Value::I32(value))) if *value > 0 => { + Some(*value as usize) + } + _ => None, + }, + ), + fields, + } + }) + .collect(); + layout_stack.pop(); + RawNativeLayoutSet { + recursive: false, + variants, + } + } + + fn known_native_field_type( + enclosing: &reader::TypeDef, + field: &str, + mapped: RawType, + ) -> RawType { + match (enclosing.namespace(), enclosing.name(), field) { + ("Windows.Win32.Security", "SECURITY_ATTRIBUTES", "lpSecurityDescriptor") + if mapped.pointer_depth == 1 && matches!(mapped.base, RawBaseType::Void) => + { + named( + enclosing.namespace(), + "SECURITY_ATTRIBUTES.lpSecurityDescriptor", + RawNamedKind::DataPointer, + ) + } + ("Windows.Win32.System.Threading", "PROCESS_INFORMATION", "hProcess" | "hThread") => { + named( + enclosing.namespace(), + &format!("PROCESS_INFORMATION.{field}"), + RawNamedKind::Handle { + cleanup: Some("CloseHandle".into()), + }, + ) + } + ( + "Windows.Win32.System.Threading", + "STARTUPINFOA" | "STARTUPINFOW", + "lpReserved" | "lpDesktop" | "lpTitle" | "lpReserved2", + ) if mapped.pointer_depth > 0 + || matches!( + mapped.base, + RawBaseType::Named { + kind: RawNamedKind::StringPointer { .. } | RawNamedKind::DataPointer, + .. + } + ) => + { + named( + enclosing.namespace(), + &format!("{}.{field}", enclosing.name()), + RawNamedKind::DataPointer, + ) + } + ( + "Windows.Win32.System.Threading", + "STARTUPINFOA" | "STARTUPINFOW", + "hStdInput" | "hStdOutput" | "hStdError", + ) => named( + enclosing.namespace(), + &format!("{}.{field}", enclosing.name()), + RawNamedKind::Handle { cleanup: None }, + ), + _ => mapped, + } + } + + fn map_layout_field_type<'a>( + index: &'a reader::Index, + typ: &windows_metadata::Type, + enclosing: Option>, + layout_stack: &mut Vec<(String, String)>, + ) -> RawType { + match typ { + windows_metadata::Type::Name(name) => { + if let Some(enclosing) = enclosing + && let Some(nested) = index + .nested(enclosing) + .find(|nested| nested.name() == name.name) + { + return map_nested_layout(index, enclosing, nested, layout_stack); + } + if let Some(enclosing) = enclosing + && let Some(known) = + known_anonymous_layout(index, enclosing, &name.name, layout_stack) + { + return known; + } + map_named(index, &name.namespace, &name.name, layout_stack) + } + windows_metadata::Type::PtrMut(inner, depth) => { + let mut mapped = map_layout_field_type(index, inner, enclosing, layout_stack); + mapped.pointer_depth = mapped + .pointer_depth + .saturating_add(u8::try_from(*depth).unwrap_or(u8::MAX)); + mapped.constness = RawConstness::Mutable; + mapped + } + windows_metadata::Type::PtrConst(inner, depth) => { + let mut mapped = map_layout_field_type(index, inner, enclosing, layout_stack); + mapped.pointer_depth = mapped + .pointer_depth + .saturating_add(u8::try_from(*depth).unwrap_or(u8::MAX)); + mapped.constness = RawConstness::Const; + mapped + } + windows_metadata::Type::Void => raw_base(RawBaseType::Void), + windows_metadata::Type::Bool => raw_scalar(RawScalar::Bool8), + windows_metadata::Type::Char => raw_scalar(RawScalar::Char16), + windows_metadata::Type::I8 => raw_scalar(RawScalar::I8), + windows_metadata::Type::U8 => raw_scalar(RawScalar::U8), + windows_metadata::Type::I16 => raw_scalar(RawScalar::I16), + windows_metadata::Type::U16 => raw_scalar(RawScalar::U16), + windows_metadata::Type::I32 => raw_scalar(RawScalar::I32), + windows_metadata::Type::U32 => raw_scalar(RawScalar::U32), + windows_metadata::Type::I64 => raw_scalar(RawScalar::I64), + windows_metadata::Type::U64 => raw_scalar(RawScalar::U64), + windows_metadata::Type::F32 => raw_scalar(RawScalar::F32), + windows_metadata::Type::F64 => raw_scalar(RawScalar::F64), + windows_metadata::Type::ISize => raw_scalar(RawScalar::NativeIsize), + windows_metadata::Type::USize => raw_scalar(RawScalar::NativeUsize), + other => RawType { + base: RawBaseType::Unknown(format!("{other:?}")), + pointer_depth: 0, + constness: RawConstness::Unspecified, + }, + } + } + + fn known_anonymous_layout( + index: &reader::Index, + enclosing: reader::TypeDef, + name: &str, + layout_stack: &mut Vec<(String, String)>, + ) -> Option { + if name != "_Anonymous_e__Union" { + return None; + } + let namespace = enclosing.namespace(); + let identity = format!("{}+{name}", enclosing.name()); + let field = |name: &str, typ: RawType| RawNativeField { + name: name.into(), + typ, + fixed_count: None, + bitfield: false, + flexible_array: false, + }; + let fields = match (namespace, enclosing.name()) { + ("Windows.Win32.System.SystemInformation", "SYSTEM_INFO") => { + let words = named( + namespace, + "SYSTEM_INFO+_Anonymous_e__Union+_Anonymous_e__Struct", + RawNamedKind::NativeStruct { + layout: Box::new(RawNativeLayoutSet { + recursive: false, + variants: vec![RawNativeLayout { + architectures: architectures(&enclosing), + kind: RawLayoutKind::Sequential, + packing: RawPacking::Default, + declared_size: None, + forced_alignment: None, + fields: vec![ + field( + "wProcessorArchitecture", + map_named( + index, + namespace, + "PROCESSOR_ARCHITECTURE", + layout_stack, + ), + ), + field("wReserved", raw_scalar(RawScalar::U16)), + ], + }], + }), + }, + ); + vec![ + field("dwOemId", raw_scalar(RawScalar::U32)), + field("Anonymous", words), + ] + } + ("Windows.Win32.UI.Input.KeyboardAndMouse", "INPUT") => vec![ + field( + "mi", + map_named(index, namespace, "MOUSEINPUT", layout_stack), + ), + field( + "ki", + map_named(index, namespace, "KEYBDINPUT", layout_stack), + ), + field( + "hi", + map_named(index, namespace, "HARDWAREINPUT", layout_stack), + ), + ], + _ => return None, + }; + Some(named( + namespace, + &identity, + RawNamedKind::NativeStruct { + layout: Box::new(RawNativeLayoutSet { + recursive: false, + variants: vec![RawNativeLayout { + architectures: architectures(&enclosing), + kind: RawLayoutKind::Union, + packing: RawPacking::Default, + declared_size: None, + forced_alignment: None, + fields, + }], + }), + }, + )) + } + + fn map_nested_layout<'a>( + index: &'a reader::Index, + enclosing: reader::TypeDef<'a>, + nested: reader::TypeDef<'a>, + layout_stack: &mut Vec<(String, String)>, + ) -> RawType { + let namespace = enclosing.namespace(); + let identity = format!("{}+{}", enclosing.name(), nested.name()); + let key = (namespace.to_string(), identity.clone()); + if layout_stack.contains(&key) { + return named( + namespace, + &identity, + RawNamedKind::NativeStruct { + layout: Box::new(RawNativeLayoutSet { + recursive: true, + variants: Vec::new(), + }), + }, + ); + } + layout_stack.push(key); + let flags = nested.flags(); + let kind = if flags.contains(windows_metadata::TypeAttributes::SequentialLayout) { + RawLayoutKind::Sequential + } else if flags.contains(windows_metadata::TypeAttributes::ExplicitLayout) { + RawLayoutKind::Union + } else { + RawLayoutKind::Unknown + }; + let (packing, declared_size) = + nested + .class_layout() + .map_or((RawPacking::Default, None), |layout| { + ( + if layout.packing_size() == 0 { + RawPacking::Default + } else { + RawPacking::Explicit(layout.packing_size()) + }, + (layout.class_size() != 0).then_some(layout.class_size() as usize), + ) + }); + let fields = nested + .fields() + .filter(|field| field.name() != "value__" && field.constant().is_none()) + .map(|field| { + let field_type = field.ty(); + let (typ, fixed_count) = match field_type { + windows_metadata::Type::ArrayFixed(ref element, count) => ( + map_layout_field_type(index, element, Some(nested), layout_stack), + Some(count), + ), + _ => ( + map_layout_field_type(index, &field_type, Some(nested), layout_stack), + None, + ), + }; + RawNativeField { + name: field.name().to_string(), + typ, + fixed_count, + bitfield: field.has_attribute("NativeBitfieldAttribute"), + flexible_array: field.has_attribute("FlexibleArrayAttribute"), + } + }) + .collect(); + let layout = RawNativeLayout { + architectures: architectures(&nested), + kind, + packing, + declared_size, + forced_alignment: nested + .find_attribute("AlignmentAttribute") + .and_then(|attribute| match attribute.value().first() { + Some((_, windows_metadata::Value::I32(value))) if *value > 0 => { + Some(*value as usize) + } + _ => None, + }), + fields, + }; + layout_stack.pop(); + named( + namespace, + &identity, + RawNamedKind::NativeStruct { + layout: Box::new(RawNativeLayoutSet { + recursive: false, + variants: vec![layout], + }), + }, + ) + } + named(namespace, name, RawNamedKind::Unknown) +} + +fn map_transparent_typedef( + index: &reader::Index, + namespace: &str, + name: &str, + typ: &windows_metadata::Type, +) -> RawType { + let mapped = map_type(index, typ); + match mapped.base { + RawBaseType::Scalar(scalar) if mapped.pointer_depth == 0 => raw_scalar(scalar), + RawBaseType::Named { + kind: RawNamedKind::StringPointer { encoding }, + .. + } => { + let mut value = named(namespace, name, RawNamedKind::StringPointer { encoding }); + value.constness = mapped.constness; + value + } + _ => named(namespace, name, RawNamedKind::Unknown), + } +} + +fn named(namespace: &str, name: &str, kind: RawNamedKind) -> RawType { + RawType { + base: RawBaseType::Named { + namespace: namespace.to_string(), + name: name.to_string(), + kind, + }, + pointer_depth: 0, + constness: RawConstness::Unspecified, + } +} + +fn named_string( + namespace: &str, + name: &str, + encoding: RawStringEncoding, + is_const: bool, +) -> RawType { + let mut value = named(namespace, name, RawNamedKind::StringPointer { encoding }); + value.constness = if is_const { + RawConstness::Const + } else { + RawConstness::Mutable + }; + value +} + +fn parse_enum(definition: &reader::TypeDef) -> (RawScalar, Vec) { + let mut underlying = RawScalar::I32; + let mut members = Vec::new(); + for field in definition.fields() { + if field.name() == "value__" { + underlying = match field.ty() { + windows_metadata::Type::I8 => RawScalar::I8, + windows_metadata::Type::U8 => RawScalar::U8, + windows_metadata::Type::I16 => RawScalar::I16, + windows_metadata::Type::U16 => RawScalar::U16, + windows_metadata::Type::I32 => RawScalar::I32, + windows_metadata::Type::U32 => RawScalar::U32, + windows_metadata::Type::I64 => RawScalar::I64, + windows_metadata::Type::U64 => RawScalar::U64, + _ => RawScalar::I32, + }; + continue; + } + let Some(constant) = field.constant() else { + continue; + }; + let value = match constant.value() { + windows_metadata::Value::I8(value) => value as i128, + windows_metadata::Value::U8(value) => value as i128, + windows_metadata::Value::I16(value) => value as i128, + windows_metadata::Value::U16(value) => value as i128, + windows_metadata::Value::I32(value) => value as i128, + windows_metadata::Value::U32(value) => value as i128, + windows_metadata::Value::I64(value) => value as i128, + windows_metadata::Value::U64(value) => value as i128, + _ => continue, + }; + members.push(RawEnumMember { + name: field.name().to_string(), + value, + }); + } + (underlying, members) +} + +fn buffer_element(typ: &RawType) -> RawType { + let mut element = typ.clone(); + if element.pointer_depth > 0 { + element.pointer_depth -= 1; + } else if matches!( + element.base, + RawBaseType::Named { + kind: RawNamedKind::DataPointer | RawNamedKind::StringPointer { .. }, + .. + } + ) { + element.base = match &element.base { + RawBaseType::Named { + kind: + RawNamedKind::StringPointer { + encoding: RawStringEncoding::Utf16, + }, + .. + } => RawBaseType::Scalar(RawScalar::Char16), + _ => RawBaseType::Scalar(RawScalar::U8), + }; + } + element +} + +fn buffer_size(parameter: &reader::MethodParam) -> Option { + if let Some(attribute) = parameter.find_attribute("NativeArrayInfoAttribute") { + let values = attribute.value(); + if let Some(index) = attribute_usize(&values, "CountParamIndex") { + return Some(RawBufferSize::ElementCountParam(index)); + } + if let Some(count) = attribute_usize(&values, "CountConst") { + return Some(RawBufferSize::Constant(count)); + } + return Some(RawBufferSize::Unknown); + } + if let Some(attribute) = parameter.find_attribute("MemorySizeAttribute") { + return Some( + attribute_usize(&attribute.value(), "BytesParamIndex") + .map(RawBufferSize::ByteCountParam) + .unwrap_or(RawBufferSize::Unknown), + ); + } + None +} + +fn apply_known_buffer_contracts(namespace: &str, function: &str, parameters: &mut [RawParameter]) { + if namespace == "Windows.Win32.Globalization" + && matches!(function, "LCMapStringW" | "LCMapStringEx") + && parameters + .get(4) + .is_some_and(|parameter| parameter.name == "lpDestStr") + { + parameters[4].buffer = None; + return; + } + let contract = match (namespace, function) { + ("Windows.Win32.Globalization", "LCMapStringA") => { + Some((4, "lpDestStr", 5, "cchDest", true)) + } + ("Windows.Win32.Globalization", "FoldStringA" | "FoldStringW") => { + Some((3, "lpDestStr", 4, "cchDest", true)) + } + ( + "Windows.Win32.Globalization", + "GetLocaleInfoA" | "GetLocaleInfoW" | "GetLocaleInfoEx", + ) => Some((2, "lpLCData", 3, "cchData", true)), + ( + "Windows.Win32.System.Threading", + "QueryFullProcessImageNameA" | "QueryFullProcessImageNameW", + ) => Some((2, "lpExeName", 3, "lpdwSize", false)), + _ => None, + }; + let Some((buffer_index, buffer_name, count_index, count_name, nullable)) = contract else { + return; + }; + if buffer_index >= count_index || count_index >= parameters.len() { + return; + } + let (before_count, from_count) = parameters.split_at_mut(count_index); + let buffer = &mut before_count[buffer_index]; + let count = &from_count[0]; + if buffer.name != buffer_name || count.name != count_name || buffer.buffer.is_some() { + return; + } + buffer.buffer = Some(RawBuffer { + element: buffer_element(&buffer.typ), + size: RawBufferSize::ElementCountParam(count_index), + }); + buffer.nullable = nullable; +} + +fn attribute_usize(values: &[(String, windows_metadata::Value)], name: &str) -> Option { + values + .iter() + .find(|(candidate, _)| candidate == name) + .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, + }) +} + +fn free_with(parameter: &reader::MethodParam) -> Option { + parameter + .find_attribute("FreeWithAttribute") + .and_then(|attribute| first_attribute_string(&attribute.value())) +} + +fn handle_cleanup(index: &reader::Index, namespace: &str, name: &str) -> Option { + let metadata = index + .get(namespace, name) + .next() + .and_then(|definition| definition.find_attribute("RAIIFreeAttribute")) + .and_then(|attribute| first_attribute_string(&attribute.value())); + metadata.or_else(|| { + match name { + "HKEY" => Some("RegCloseKey"), + "HANDLE" | "SC_HANDLE" => Some("CloseHandle"), + "HLOCAL" => Some("LocalFree"), + _ => None, + } + .map(str::to_string) + }) +} + +fn first_attribute_string(values: &[(String, windows_metadata::Value)]) -> Option { + values.iter().find_map(|(_, value)| match value { + windows_metadata::Value::Utf8(value) | windows_metadata::Value::Utf16(value) => { + Some(value.to_string()) + } + _ => None, + }) +} + +fn architectures<'a, T: HasAttributes<'a>>(item: &T) -> RawArchitectures { + let Some(attribute) = item.find_attribute("SupportedArchitectureAttribute") else { + return RawArchitectures::all(); + }; + let Some(bits) = attribute + .value() + .first() + .and_then(|(_, value)| match value { + windows_metadata::Value::I32(value) => Some(*value as u32), + windows_metadata::Value::U32(value) => Some(*value), + windows_metadata::Value::AttributeEnum(_, value) => Some(*value as u32), + _ => None, + }) + else { + return RawArchitectures { + x86: false, + x64: false, + arm64: false, + }; + }; + RawArchitectures { + x86: bits == 0 || bits & 0x1 != 0, + x64: bits == 0 || bits & 0x2 != 0, + arm64: bits == 0 || bits & 0x4 != 0, + } +} + +fn calling_convention(method: &reader::MethodDef) -> RawCallingConvention { + let Some(flags) = method.impl_map().map(|mapping| mapping.flags()) else { + return RawCallingConvention::Unsupported; + }; + let Some(bits) = parse_pinvoke_attribute_bits(&format!("{flags:?}")) else { + return RawCallingConvention::Unsupported; + }; + calling_convention_bits(bits) +} + +fn parse_pinvoke_attribute_bits(value: &str) -> Option { + value + .strip_prefix("PInvokeAttributes(")? + .strip_suffix(')')? + .parse() + .ok() +} + +fn calling_convention_bits(bits: u16) -> RawCallingConvention { + match bits & 0x0700 { + 0x0100 => RawCallingConvention::System, + 0x0200 => RawCallingConvention::Cdecl, + _ => RawCallingConvention::Unsupported, + } +} + +fn status_semantics(raw: &windows_metadata::Type, mapped: &RawType) -> RawStatusSemantics { + if let windows_metadata::Type::Name(name) = raw { + if matches!( + name.name.as_ref(), + "HRESULT" | "NTSTATUS" | "SECURITY_STATUS" + ) { + return RawStatusSemantics::SignedNonNegativeIsSuccess; + } + if matches!( + name.name.as_ref(), + "LSTATUS" | "WIN32_ERROR" | "CONFIGRET" | "RPC_STATUS" | "NET_API_STATUS" | "MMRESULT" + ) { + return RawStatusSemantics::ZeroIsSuccess; + } + } + if matches!( + &mapped.base, + RawBaseType::Named { + kind: RawNamedKind::Enum { .. }, + name, + .. + } if name == "WIN32_ERROR" + ) { + RawStatusSemantics::ZeroIsSuccess + } else { + RawStatusSemantics::None + } +} + +fn is_data_pointer_alias(name: &str) -> bool { + matches!( + name, + "PSID" + | "PSECURITY_DESCRIPTOR" + | "MEMORY_MAPPED_VIEW_ADDRESS" + | "LPPROC_THREAD_ATTRIBUTE_LIST" + | "PVOID" + | "PCVOID" + | "LPVOID" + | "LPCVOID" + ) +} + +fn is_handle_alias(name: &str) -> bool { + const HANDLES: &[&str] = &[ + "HANDLE", + "HWND", + "HACCEL", + "HBITMAP", + "HBRUSH", + "HCURSOR", + "HDC", + "HDESK", + "HDWP", + "HENHMETAFILE", + "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", + ]; + HANDLES.contains(&name) +} + +pub fn distinct_containers(functions: &[RawFunction]) -> usize { + functions + .iter() + .map(|function| (&function.namespace, &function.container)) + .collect::>() + .len() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn configured_winmd() -> Option { + std::env::var("DYNWINRT_WIN32_WINMD") + .ok() + .filter(|path| std::path::Path::new(path).is_file()) + } + + #[test] + fn pinvoke_convention_mask_is_decoded_exactly() { + assert_eq!( + calling_convention_bits(0x0100), + RawCallingConvention::System + ); + assert_eq!(calling_convention_bits(0x0200), RawCallingConvention::Cdecl); + for bits in [0x0000, 0x0300, 0x0400, 0x0500] { + assert_eq!( + calling_convention_bits(bits), + RawCallingConvention::Unsupported + ); + } + assert_eq!( + parse_pinvoke_attribute_bits("PInvokeAttributes(768)"), + Some(0x0300) + ); + assert_eq!(parse_pinvoke_attribute_bits("changed-format"), None); + } + + #[test] + fn registry_metadata_preserves_handle_cleanup_and_buffer_relation() { + let Some(winmd) = configured_winmd() else { + return; + }; + let apis = parse_apis(&winmd, "Windows.Win32.System.Registry", "Apis").unwrap(); + let open = apis + .functions + .iter() + .find(|function| function.name == "RegOpenKeyExW") + .unwrap(); + let output = open.parameters.last().unwrap(); + assert_eq!(output.direction, RawDirection::Out); + assert!(matches!( + &output.typ.base, + RawBaseType::Named { + kind: RawNamedKind::Handle { cleanup: Some(cleanup) }, + .. + } if cleanup == "RegCloseKey" + )); + assert_eq!(output.typ.pointer_depth, 1); + + let query = apis + .functions + .iter() + .find(|function| function.name == "RegQueryValueExW") + .unwrap(); + assert!(matches!( + query.parameters[4] + .buffer + .as_ref() + .map(|buffer| &buffer.size), + Some(RawBufferSize::ByteCountParam(5)) + )); + } + + #[test] + fn known_string_buffer_contracts_fill_metadata_gaps_exactly() { + let Some(winmd) = configured_winmd() else { + return; + }; + let apis = parse_apis(&winmd, "Windows.Win32.Globalization", "Apis").unwrap(); + let function = apis + .functions + .iter() + .find(|function| function.name == "LCMapStringA") + .unwrap(); + let buffer = &function.parameters[4]; + assert_eq!(buffer.name, "lpDestStr"); + assert!(buffer.nullable); + assert!(matches!( + buffer.buffer.as_ref().map(|buffer| &buffer.size), + Some(RawBufferSize::ElementCountParam(5)) + )); + for name in ["LCMapStringW", "LCMapStringEx"] { + let function = apis + .functions + .iter() + .find(|function| function.name == name) + .unwrap(); + assert!(function.parameters[4].buffer.is_none()); + } + } + + #[test] + fn nested_anonymous_layouts_are_resolved_from_their_enclosing_type() { + let Some(winmd) = configured_winmd() else { + return; + }; + let apis = parse_apis(&winmd, "Windows.Win32.System.SystemInformation", "Apis").unwrap(); + let system_info = apis + .functions + .iter() + .find(|function| function.name == "GetSystemInfo") + .expect("GetSystemInfo metadata"); + let system_info = &system_info.parameters[0].typ; + let RawBaseType::Named { + kind: RawNamedKind::NativeStruct { layout }, + .. + } = &system_info.base + else { + panic!("SYSTEM_INFO must retain native layout"); + }; + let variant = layout.variants.first().expect("SYSTEM_INFO layout"); + assert!( + variant.fields.iter().any(|field| { + matches!( + &field.typ.base, + RawBaseType::Named { + name, + kind: RawNamedKind::NativeStruct { layout }, + .. + } if name.contains("_Anonymous_e__Union") + && layout.variants.first().is_some_and(|nested| nested.kind == RawLayoutKind::Union) + ) + }), + "{:#?}", + variant.fields + ); + } + + #[test] + fn hfile_preserves_its_signed_i32_abi() { + let Some(winmd) = configured_winmd() else { + return; + }; + let functions = parse_all_functions(&winmd).unwrap(); + let function = functions + .iter() + .find(|function| function.name == "_lopen") + .expect("configured metadata contains _lopen"); + assert!(matches!( + function.return_type, + RawType { + base: RawBaseType::Scalar(RawScalar::I32), + pointer_depth: 0, + .. + } + )); + + let config = functions + .iter() + .find(|function| function.name == "CM_Disable_DevNode") + .expect("configured metadata contains CM_Disable_DevNode"); + assert_eq!(config.return_status, RawStatusSemantics::ZeroIsSuccess); + + let ldap = functions + .iter() + .find(|function| function.name == "LdapGetLastError") + .expect("configured metadata contains LdapGetLastError"); + assert_eq!(ldap.calling_convention, RawCallingConvention::Cdecl); + assert!(!ldap.variadic); + } +} diff --git a/tools/dynwinrt-codegen/tests/win32_bindgen_oracle_test.rs b/tools/dynwinrt-codegen/tests/win32_bindgen_oracle_test.rs new file mode 100644 index 00000000..a83f8f13 --- /dev/null +++ b/tools/dynwinrt-codegen/tests/win32_bindgen_oracle_test.rs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::mem::{align_of, size_of}; + +use windows::Win32::Foundation::{FILETIME, HANDLE, RECT, SYSTEMTIME}; + +#[test] +fn generated_windows_types_match_stock_windows_abi() { + assert_eq!(size_of::(), size_of::()); + assert_eq!(align_of::(), align_of::()); + assert_eq!(size_of::(), 8); + assert_eq!(align_of::(), 4); + assert_eq!(size_of::(), 16); + assert_eq!(align_of::(), 4); + assert_eq!(size_of::(), 16); + assert_eq!(align_of::(), 2); +} + +#[test] +fn generated_cdecl_oracle_is_callable() { + let result = unsafe { windows::Win32::Networking::Ldap::LdapGetLastError() }; + let _: u32 = result; +} diff --git a/tools/dynwinrt-codegen/tests/win32_flat_test.rs b/tools/dynwinrt-codegen/tests/win32_flat_test.rs new file mode 100644 index 00000000..4b6dce4e --- /dev/null +++ b/tools/dynwinrt-codegen/tests/win32_flat_test.rs @@ -0,0 +1,745 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +fn configured_winmd() -> Option { + std::env::var("DYNWINRT_WIN32_WINMD") + .ok() + .filter(|path| std::path::Path::new(path).is_file()) +} + +#[test] +fn registry_projection_is_natural_and_ownership_aware() { + let Some(winmd) = configured_winmd() else { + return; + }; + let raw = dynwinrt_codegen::win32_metadata::parse_apis( + &winmd, + "Windows.Win32.System.Registry", + "Apis", + ) + .unwrap(); + let (output, omissions) = + dynwinrt_codegen::codegen::win32::generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + + assert!(output.js.contains("DynWin32Function.bind")); + assert!(output.js.contains("DynWin32.dataPointer(data, true)")); + assert!(output.js.contains("consumesResource: true")); + assert!( + output + .js + .contains(r#"DynWin32.resource(hKey, "regCloseKey")"#) + ); + assert!(output.js.contains("const regOpenKeyEx = regOpenKeyExW")); + assert!(output.js.contains("exports.regOpenKeyEx = regOpenKeyEx")); + assert!(output.dts.contains( + "regOpenKeyExW(hKey: HKEY, subKey: string | Buffer | Uint8Array | null, ulOptions: number, samDesired: REG_SAM_FLAGS): { readonly status: number; readonly key: DynWin32Resource | null }" + )); + assert!(output.dts.contains( + "regQueryValueExW(hKey: HKEY, valueName: string | Buffer | Uint8Array | null, data: Buffer | Uint8Array | null): { readonly status: number; readonly type: REG_VALUE_TYPE; readonly dataSize: number }" + )); + assert!(output.dts.contains( + "export declare function regCloseKey(hKey: DynWin32Resource): { readonly status: number }" + )); + assert!(!output.dts.contains("bigint | Buffer")); + assert!( + output.js.contains("exports.regCreateKeyExW"), + "{omissions:#?}" + ); +} + +#[test] +fn scalar_return_projects_directly_without_result_wrapper() { + let Some(winmd) = configured_winmd() else { + return; + }; + let raw = dynwinrt_codegen::win32_metadata::parse_apis( + &winmd, + "Windows.Win32.System.SystemInformation", + "Apis", + ) + .unwrap(); + let (output, _) = + dynwinrt_codegen::codegen::win32::generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!( + output + .dts + .contains("export declare function getTickCount64(): bigint") + ); + assert!(output.js.contains("return DynWin32.toBigint(_return)")); + assert!(output.dts.contains( + "export declare function createSYSTEMTIME(bytes?: Buffer | Uint8Array): SYSTEMTIME" + )); + assert!( + output + .dts + .contains("export declare function getSystemTime(systemTime: SYSTEMTIME): void") + ); + assert!(output.js.contains("DynWin32.nativeStruct(systemTime")); +} + +#[test] +fn generated_safe_surface_uses_unsafe_binding_only_internally() { + let Some(winmd) = configured_winmd() else { + return; + }; + let raw = dynwinrt_codegen::win32_metadata::parse_apis( + &winmd, + "Windows.Win32.System.LibraryLoader", + "Apis", + ) + .unwrap(); + let (output, _) = + dynwinrt_codegen::codegen::win32::generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!(!output.js.contains("DynWin32Unsafe")); + assert!(output.js.contains("@microsoft/dynwinrt/win32/unsafe")); + assert!(!output.dts.contains("/win32/unsafe")); + assert!(output.js.contains("getModuleHandleW")); + assert!(output.dts.contains("export type HMODULE =")); +} + +#[test] +fn ldap_cdecl_metadata_reaches_the_immutable_plan() { + let Some(winmd) = configured_winmd() else { + return; + }; + let raw = dynwinrt_codegen::win32_metadata::parse_apis( + &winmd, + "Windows.Win32.Networking.Ldap", + "Apis", + ) + .unwrap(); + let (output, omitted) = + dynwinrt_codegen::codegen::win32::generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!( + output + .dts + .contains("export declare function ldapGetLastError(): number"), + "{omitted:#?}" + ); + assert!(output.js.contains(r#"callingConvention: "cdecl""#)); +} + +#[test] +fn com_interface_inputs_require_managed_values_and_exact_iids() { + let Some(winmd) = configured_winmd() else { + return; + }; + let raw = + dynwinrt_codegen::win32_metadata::parse_apis(&winmd, "Windows.Win32.System.Com", "Apis") + .unwrap(); + let (output, omissions) = + dynwinrt_codegen::codegen::win32::generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!( + output + .dts + .contains("export declare function coIsHandlerConnected(") + && output.dts.contains("DynWinRtValue): boolean"), + "{omissions:#?}" + ); + assert!(output.js.contains("DynWin32.comObject(")); +} + +#[test] +fn unsigned_registry_enum_preserves_high_bit_u32_semantics() { + let Some(winmd) = configured_winmd() else { + return; + }; + let raw = dynwinrt_codegen::win32_metadata::parse_apis( + &winmd, + "Windows.Win32.System.Registry", + "Apis", + ) + .unwrap(); + let function = raw + .functions + .iter() + .find(|function| function.name == "RegSetKeySecurity") + .expect("RegSetKeySecurity metadata"); + let parameter = function + .parameters + .iter() + .find(|parameter| parameter.name == "SecurityInformation") + .expect("SecurityInformation metadata"); + let dynwinrt_codegen::win32_metadata::RawBaseType::Named { + name, + kind: + dynwinrt_codegen::win32_metadata::RawNamedKind::Enum { + underlying, + members, + .. + }, + .. + } = ¶meter.typ.base + else { + panic!("SecurityInformation must remain a named enum"); + }; + assert_eq!(name, "OBJECT_SECURITY_INFORMATION"); + assert_eq!( + *underlying, + dynwinrt_codegen::win32_metadata::RawScalar::U32 + ); + assert!( + members + .iter() + .any(|member| member.value > i128::from(i32::MAX)) + ); + + let (output, omissions) = + dynwinrt_codegen::codegen::win32::generate_apis_files(&raw, "@microsoft/dynwinrt/win32"); + assert!( + output.js.contains("DynWin32.u32(securityInformation)"), + "{omissions:#?}" + ); +} + +#[test] +fn unowned_double_pointer_output_fails_closed() { + let Some(winmd) = configured_winmd() else { + return; + }; + let raw = dynwinrt_codegen::win32_metadata::parse_apis( + &winmd, + "Windows.Win32.System.Com.StructuredStorage", + "Apis", + ) + .unwrap(); + let function = raw + .functions + .iter() + .find(|function| function.name == "PropVariantToUInt32VectorAlloc") + .expect("PropVariantToUInt32VectorAlloc metadata"); + let parameter = function + .parameters + .iter() + .find(|parameter| parameter.name == "pprgn") + .expect("pprgn metadata"); + assert_eq!(parameter.typ.pointer_depth, 2); + + let projection = dynwinrt_codegen::codegen::win32::project_apis( + &dynwinrt_codegen::win32_metadata::RawApis { + namespace: raw.namespace, + class_name: raw.class_name, + functions: vec![function.clone()], + }, + ); + assert_eq!(projection.complete_count(), 0); + assert!( + projection.omitted[0] + .identity + .ends_with("::PropVariantToUInt32VectorAlloc") + ); +} + +#[test] +fn sid_data_pointer_and_color_scalar_are_not_handles() { + let Some(winmd) = configured_winmd() else { + return; + }; + let security = + dynwinrt_codegen::win32_metadata::parse_apis(&winmd, "Windows.Win32.Security", "Apis") + .unwrap(); + let is_valid_sid = security + .functions + .iter() + .find(|function| function.name == "IsValidSid") + .expect("IsValidSid metadata"); + let sid = is_valid_sid + .parameters + .iter() + .find(|parameter| parameter.name == "pSid") + .expect("pSid metadata"); + assert!(matches!( + sid.typ.base, + dynwinrt_codegen::win32_metadata::RawBaseType::Named { + kind: dynwinrt_codegen::win32_metadata::RawNamedKind::DataPointer, + .. + } + )); + + let gdi = + dynwinrt_codegen::win32_metadata::parse_apis(&winmd, "Windows.Win32.Graphics.Gdi", "Apis") + .unwrap(); + let get_pixel = gdi + .functions + .iter() + .find(|function| function.name == "GetPixel") + .expect("GetPixel metadata"); + assert_eq!( + get_pixel.return_type.base, + dynwinrt_codegen::win32_metadata::RawBaseType::Scalar( + dynwinrt_codegen::win32_metadata::RawScalar::U32 + ) + ); +} + +#[test] +fn flat_win32_cli_rejects_python_generation() { + let Some(winmd) = configured_winmd() else { + return; + }; + let output = std::process::Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args([ + "generate", + "--winmd", + &winmd, + "--namespace", + "Windows.Win32.System.Registry", + "--class-name", + "Apis", + "--lang", + "py", + "--dry-run", + ]) + .output() + .expect("run dynwinrt-codegen"); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("`--lang py` is not supported"), "{stderr}"); + assert!( + stderr.contains("flat Win32 DllImport container"), + "{stderr}" + ); +} + +#[test] +fn flat_win32_namespace_mode_routes_to_flat_projection() { + let Some(winmd) = configured_winmd() else { + return; + }; + let js = std::process::Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args([ + "generate", + "--winmd", + &winmd, + "--namespace", + "Windows.Win32.System.SystemInformation", + "--dry-run", + ]) + .output() + .expect("run dynwinrt-codegen"); + assert!( + js.status.success(), + "{}", + String::from_utf8_lossy(&js.stderr) + ); + assert!( + String::from_utf8_lossy(&js.stdout) + .contains("Would generate flat Win32 Windows.Win32.System.SystemInformation.Apis") + ); + + let py = std::process::Command::new(env!("CARGO_BIN_EXE_dynwinrt-codegen")) + .args([ + "generate", + "--winmd", + &winmd, + "--namespace", + "Windows.Win32.System.SystemInformation", + "--lang", + "py", + "--dry-run", + ]) + .output() + .expect("run dynwinrt-codegen"); + assert!(!py.status.success()); + assert!( + String::from_utf8_lossy(&py.stderr).contains("is not supported for flat Win32 namespace") + ); +} + +#[test] +fn reserved_and_double_null_metadata_drive_projection() { + let Some(winmd) = configured_winmd() else { + return; + }; + let com = + dynwinrt_codegen::win32_metadata::parse_apis(&winmd, "Windows.Win32.System.Com", "Apis") + .unwrap(); + let (com_output, com_omissions) = dynwinrt_codegen::codegen::win32::generate_apis_files( + &dynwinrt_codegen::win32_metadata::RawApis { + namespace: com.namespace, + class_name: com.class_name, + functions: com + .functions + .into_iter() + .filter(|function| { + matches!( + function.name.as_str(), + "CoDisconnectObject" | "CoFreeUnusedLibrariesEx" + ) + }) + .collect(), + }, + "@microsoft/dynwinrt/win32", + ); + assert!(com_omissions.is_empty(), "{com_omissions:#?}"); + assert!( + com_output + .dts + .contains("coDisconnectObject(unk: DynWinRtValue)") + ); + assert!( + com_output + .dts + .contains("coFreeUnusedLibrariesEx(dwUnloadDelay: number): void") + ); + assert!(com_output.js.contains("DynWin32.u32(0)")); + assert!(!com_output.dts.contains("dwReserved")); + + let globalization = + dynwinrt_codegen::win32_metadata::parse_apis(&winmd, "Windows.Win32.Globalization", "Apis") + .unwrap(); + let (globalization_output, globalization_omissions) = + dynwinrt_codegen::codegen::win32::generate_apis_files( + &dynwinrt_codegen::win32_metadata::RawApis { + namespace: globalization.namespace, + class_name: globalization.class_name, + functions: globalization + .functions + .into_iter() + .filter(|function| function.name == "SetProcessPreferredUILanguages") + .collect(), + }, + "@microsoft/dynwinrt/win32", + ); + assert!( + globalization_omissions.is_empty(), + "{globalization_omissions:#?}" + ); + assert!( + globalization_output + .dts + .contains("string | readonly string[] | Buffer | Uint8Array | null") + ); + assert!( + globalization_output + .js + .contains("DynWin32.wideMultiString(pwszLanguagesBuffer, true)") + ); +} + +#[test] +fn opaque_byte_sized_iphelper_buffer_projects_safely() { + let Some(winmd) = configured_winmd() else { + return; + }; + let raw = dynwinrt_codegen::win32_metadata::parse_apis( + &winmd, + "Windows.Win32.NetworkManagement.IpHelper", + "Apis", + ) + .unwrap(); + let function = raw + .functions + .iter() + .find(|function| function.name == "GetAdaptersAddresses") + .expect("GetAdaptersAddresses metadata") + .clone(); + let selected = dynwinrt_codegen::win32_metadata::RawApis { + namespace: raw.namespace, + class_name: raw.class_name, + functions: vec![function], + }; + let (output, omissions) = dynwinrt_codegen::codegen::win32::generate_apis_files( + &selected, + "@microsoft/dynwinrt/win32", + ); + assert!(omissions.is_empty(), "{omissions:#?}"); + assert!(output.dts.contains("getAdaptersAddresses(")); + assert!(output.dts.contains("Buffer | Uint8Array | null")); + assert!( + output + .js + .contains("DynWin32.alignedDataPointer(adapterAddresses, 8, true)") + ); +} + +#[test] +fn exact_string_buffer_overrides_generate_queryable_surfaces() { + let Some(winmd) = configured_winmd() else { + return; + }; + let raw = + dynwinrt_codegen::win32_metadata::parse_apis(&winmd, "Windows.Win32.Globalization", "Apis") + .unwrap(); + let functions = raw + .functions + .iter() + .filter(|function| function.name == "LCMapStringA") + .cloned() + .collect(); + let selected = dynwinrt_codegen::win32_metadata::RawApis { + namespace: raw.namespace, + class_name: raw.class_name, + functions, + }; + let (output, omissions) = dynwinrt_codegen::codegen::win32::generate_apis_files( + &selected, + "@microsoft/dynwinrt/win32", + ); + assert!(omissions.is_empty(), "{omissions:#?}"); + assert!( + output + .dts + .contains("lcMapStringA(locale: number, dwMapFlags:") + ); + assert!(output.dts.contains("destStr: Buffer | Uint8Array | null")); + assert!(output.js.contains("_bufferCount(destStr, 1)")); +} + +#[test] +fn nested_system_info_union_resolves_but_pointer_fields_stay_closed() { + let Some(winmd) = configured_winmd() else { + return; + }; + let raw = dynwinrt_codegen::win32_metadata::parse_apis( + &winmd, + "Windows.Win32.System.SystemInformation", + "Apis", + ) + .unwrap(); + let function = raw + .functions + .iter() + .find(|function| function.name == "GetSystemInfo") + .expect("GetSystemInfo metadata") + .clone(); + let parameter = &function.parameters[0]; + let dynwinrt_codegen::win32_metadata::RawBaseType::Named { + kind: dynwinrt_codegen::win32_metadata::RawNamedKind::NativeStruct { layout }, + .. + } = ¶meter.typ.base + else { + panic!("SYSTEM_INFO native layout"); + }; + assert!(layout.variants[0].fields.iter().any(|field| { + matches!( + &field.typ.base, + dynwinrt_codegen::win32_metadata::RawBaseType::Named { + name, + kind: dynwinrt_codegen::win32_metadata::RawNamedKind::NativeStruct { .. }, + .. + } if name.contains("_Anonymous_e__Union") + ) + })); + + let projection = dynwinrt_codegen::codegen::win32::project_apis( + &dynwinrt_codegen::win32_metadata::RawApis { + namespace: raw.namespace, + class_name: raw.class_name, + functions: vec![function], + }, + ); + assert_eq!(projection.complete_count(), 0); + assert!( + projection.omitted[0] + .reason + .contains("retained pointee ownership") + ); + + let input = dynwinrt_codegen::win32_metadata::parse_apis( + &winmd, + "Windows.Win32.UI.Input.KeyboardAndMouse", + "Apis", + ) + .unwrap(); + let send_input = input + .functions + .iter() + .find(|function| function.name == "SendInput") + .expect("SendInput metadata") + .clone(); + let (output, omissions) = dynwinrt_codegen::codegen::win32::generate_apis_files( + &dynwinrt_codegen::win32_metadata::RawApis { + namespace: input.namespace, + class_name: input.class_name, + functions: vec![send_input], + }, + "@microsoft/dynwinrt/win32", + ); + assert!(omissions.is_empty(), "{omissions:#?}"); + assert!(output.dts.contains("sendInput(")); + assert!(output.js.contains("_bufferCount(inputs,")); +} + +#[test] +fn exact_direct_handle_ownership_projects_managed_resources() { + let Some(winmd) = configured_winmd() else { + return; + }; + for (namespace, functions, expected_cleanup) in [ + ( + "Windows.Win32.System.Memory", + &["LocalAlloc", "GlobalAlloc"][..], + &["localFree", "globalFree"][..], + ), + ( + "Windows.Win32.System.LibraryLoader", + &["LoadLibraryW"][..], + &["freeLibrary"][..], + ), + ( + "Windows.Win32.System.Services", + &["OpenSCManagerW"][..], + &["closeServiceHandle"][..], + ), + ] { + let raw = dynwinrt_codegen::win32_metadata::parse_apis(&winmd, namespace, "Apis").unwrap(); + let selected = dynwinrt_codegen::win32_metadata::RawApis { + namespace: raw.namespace, + class_name: raw.class_name, + functions: raw + .functions + .into_iter() + .filter(|function| functions.contains(&function.name.as_str())) + .collect(), + }; + let (output, omissions) = dynwinrt_codegen::codegen::win32::generate_apis_files( + &selected, + "@microsoft/dynwinrt/win32", + ); + assert!(omissions.is_empty(), "{namespace}: {omissions:#?}"); + for cleanup in expected_cleanup { + assert!( + output.js.contains(&format!("returnCleanup: \"{cleanup}\"")), + "{namespace}: {}", + output.js + ); + } + assert!(output.dts.contains("DynWin32Resource | null")); + } +} + +#[test] +fn pointer_struct_builders_and_overlapped_io_project_exact_surfaces() { + let Some(winmd) = configured_winmd() else { + return; + }; + + let pipes = + dynwinrt_codegen::win32_metadata::parse_apis(&winmd, "Windows.Win32.System.Pipes", "Apis") + .unwrap(); + let (pipes_output, pipes_omissions) = dynwinrt_codegen::codegen::win32::generate_apis_files( + &dynwinrt_codegen::win32_metadata::RawApis { + namespace: pipes.namespace, + class_name: pipes.class_name, + functions: pipes + .functions + .into_iter() + .filter(|function| function.name == "CreatePipe") + .collect(), + }, + "@microsoft/dynwinrt/win32", + ); + assert!(pipes_omissions.is_empty(), "{pipes_omissions:#?}"); + assert!( + pipes_output.dts.contains( + "createSecurityAttributes(init?: SecurityAttributesInit): SECURITY_ATTRIBUTES" + ) + ); + assert!(pipes_output.dts.contains("createPipe(")); + assert!(pipes_output.js.contains("DynWin32.setNativeStructPointer")); + + let threading = dynwinrt_codegen::win32_metadata::parse_apis( + &winmd, + "Windows.Win32.System.Threading", + "Apis", + ) + .unwrap(); + let (process_output, process_omissions) = dynwinrt_codegen::codegen::win32::generate_apis_files( + &dynwinrt_codegen::win32_metadata::RawApis { + namespace: threading.namespace, + class_name: threading.class_name, + functions: threading + .functions + .into_iter() + .filter(|function| function.name == "CreateProcessW") + .collect(), + }, + "@microsoft/dynwinrt/win32", + ); + assert!(process_omissions.is_empty(), "{process_omissions:#?}"); + assert!(process_output.dts.contains("createStartupInfoW(")); + assert!(process_output.dts.contains("createProcessInformation(")); + assert!( + process_output + .dts + .contains("takeProcessInformationProcess(") + ); + assert!( + process_output + .dts + .contains("getProcessInformationProcessId(") + ); + + let files = dynwinrt_codegen::win32_metadata::parse_apis( + &winmd, + "Windows.Win32.Storage.FileSystem", + "Apis", + ) + .unwrap(); + let read_file = files + .functions + .iter() + .find(|function| function.name == "ReadFile") + .expect("ReadFile metadata") + .clone(); + let duplicate_projection = dynwinrt_codegen::codegen::win32::project_apis( + &dynwinrt_codegen::win32_metadata::RawApis { + namespace: files.namespace.clone(), + class_name: files.class_name.clone(), + functions: vec![read_file.clone(), read_file], + }, + ); + assert_eq!(duplicate_projection.complete_count(), 0); + assert_eq!(duplicate_projection.omitted.len(), 2); + assert!(duplicate_projection.omitted.iter().all(|omission| { + omission + .reason + .contains("overload or architecture collision") + })); + let (file_output, file_omissions) = dynwinrt_codegen::codegen::win32::generate_apis_files( + &dynwinrt_codegen::win32_metadata::RawApis { + namespace: files.namespace, + class_name: files.class_name, + functions: files + .functions + .into_iter() + .filter(|function| matches!(function.name.as_str(), "ReadFile" | "WriteFile")) + .collect(), + }, + "@microsoft/dynwinrt/win32", + ); + assert!(file_omissions.is_empty(), "{file_omissions:#?}"); + assert_eq!( + file_output.dts.matches("function readFileAsync(").count(), + 1 + ); + assert_eq!( + file_output.dts.matches("function writeFileAsync(").count(), + 1 + ); + assert!(!file_output.dts.contains("function readFile(")); + assert!( + file_output + .js + .contains("DynWin32.beginReadFile(file, buffer") + ); + assert!( + file_output + .js + .contains("operation.start((error, bytesTransferred)") + ); + assert!(!file_output.js.contains("operation.promise()")); + assert!(file_output.js.contains("signal must be an AbortSignal")); + assert!( + file_output + .js + .contains("catch (error) { return Promise.reject(error) }") + ); + assert!( + file_output + .js + .contains("message.includes('Win32 error 995')") + ); + assert!(file_output.js.contains("error.name = 'AbortError'")); +} From 0341d3851a2b61c16f8033f820b2e8a40911bfd1 Mon Sep 17 00:00:00 2001 From: "Leilei Zhang (from Dev Box)" Date: Tue, 18 Aug 2026 15:00:29 +0800 Subject: [PATCH 2/4] Add Win32 E2E stage diagnostics Split the synchronous flat Win32 runner path into API-level milestones so hosted CI hangs identify the exact native call. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6307886d-3c24-4596-8924-ba44b0e850a5 --- tests/e2e/runners/win32/returns.mjs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/e2e/runners/win32/returns.mjs b/tests/e2e/runners/win32/returns.mjs index 703ca999..e21b8f6e 100644 --- a/tests/e2e/runners/win32/returns.mjs +++ b/tests/e2e/runners/win32/returns.mjs @@ -136,21 +136,26 @@ const year = systemTimeBytes.readUInt16LE(0); const month = systemTimeBytes.readUInt16LE(2); assert(year >= 2020); assert(month >= 1 && month <= 12); +console.log("[win32-e2e] system time aggregate passed"); const oneTick = createFILETIME(Buffer.from([1, 0, 0, 0, 0, 0, 0, 0])); const twoTicks = createFILETIME(Buffer.from([2, 0, 0, 0, 0, 0, 0, 0])); assert.equal(ftAddFt(oneTick, twoTicks).bytes.readUInt32LE(0), 3); +console.log("[win32-e2e] by-value FILETIME call passed"); const processHandle = openProcess(0x1000, false, process.pid); assert(processHandle.result); processHandle.result.close(); assert(processHandle.result.closed); +console.log("[win32-e2e] OpenProcess ownership passed"); const attributes = createSecurityAttributes({ securityDescriptor: null, inheritHandle: false, }); +console.log("[win32-e2e] SECURITY_ATTRIBUTES builder passed"); const pipe = createPipe(attributes, 0); +console.log("[win32-e2e] CreatePipe call returned"); assert.equal(pipe.result, true); assert(pipe.hReadPipe); assert(pipe.hWritePipe); From 779fbc654a0bab453bc8f016805f7edb8ab2b76b Mon Sep 17 00:00:00 2001 From: "Leilei Zhang (from Dev Box)" Date: Tue, 18 Aug 2026 15:25:43 +0800 Subject: [PATCH 3/4] Fail closed on uninitialized MAPI exports Exclude mapi32.dll from safe flat Win32 generation until its required initialization lifecycle is modeled, and replace the hosted E2E probe with deterministic PtInRect by-value ABI coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6307886d-3c24-4596-8924-ba44b0e850a5 --- .github/workflows/build.yml | 6 ++-- docs/architecture/flat-win32-support.md | 7 ++-- docs/guides/windows/flat-win32-usage.md | 2 ++ tests/e2e/e2e_test.ps1 | 2 +- tests/e2e/runners/win32/returns.mjs | 21 ++++++++---- .../src/codegen/win32/model.rs | 6 ++++ .../dynwinrt-codegen/tests/win32_flat_test.rs | 32 +++++++++++++++++++ 7 files changed, 62 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5061ea4a..2778e22a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,11 +69,11 @@ jobs: if ($result.eligible_functions -ne 18321) { throw "Flat Win32 census denominator changed: $($result.eligible_functions)" } - if ($result.complete_functions -lt 8959) { + if ($result.complete_functions -lt 8943) { throw "Flat Win32 complete coverage regressed: $($result.complete_functions)" } - if ($result.coverage_percent -lt 48.9) { - throw "Flat Win32 coverage fell below 48.9%: $($result.coverage_percent)" + if ($result.coverage_percent -lt 48.8) { + throw "Flat Win32 coverage fell below 48.8%: $($result.coverage_percent)" } Write-Host "Flat Win32 coverage: $($result.complete_functions)/$($result.eligible_functions) ($($result.coverage_percent)%)" - name: Test Classic COM failure cleanup contracts diff --git a/docs/architecture/flat-win32-support.md b/docs/architecture/flat-win32-support.md index a71958a8..02a8801b 100644 --- a/docs/architecture/flat-win32-support.md +++ b/docs/architecture/flat-win32-support.md @@ -35,7 +35,8 @@ owned resources only when the function's success rule succeeds. Modules are loaded only from System32 with `LOAD_LIBRARY_SEARCH_SYSTEM32` and remain loaded for the process lifetime. Bare `.dll` and `.drv` names are -accepted; paths are rejected. +accepted; paths are rejected. `mapi32.dll` is excluded from the safe projection +until MAPI/MAPI utility initialization and shutdown are modeled explicitly. The runtime supports x64 and ARM64 with explicit `system` and `cdecl` plans. A 32-bit build compiles, but plan binding fails explicitly until generation @@ -187,8 +188,8 @@ dynwinrt-codegen win32-census ` ``` For `Microsoft.Windows.SDK.Win32Metadata 71.0.14-preview`, the baseline is -8,959 complete safe functions out of 18,321 DllImport rows -(48.900169204737736%). Omission reasons are grouped into stable categories. +8,943 complete safe functions out of 18,321 DllImport rows +(48.8128377271983%). Omission reasons are grouped into stable categories. `windows-metadata` remains behind the flat-local adapter. Parameter rows are associated by ECMA-335 `Param.Sequence`, and calling convention remains a raw diff --git a/docs/guides/windows/flat-win32-usage.md b/docs/guides/windows/flat-win32-usage.md index e0687bf7..92d31c95 100644 --- a/docs/guides/windows/flat-win32-usage.md +++ b/docs/guides/windows/flat-win32-usage.md @@ -64,6 +64,8 @@ explicit `@microsoft/dynwinrt/win32/unsafe` entrypoint. APIs that consume and close a handle require `DynWin32Resource`; passing `resource.value` or another numeric handle is rejected. Double-NUL string-list parameters accept `string[]` or explicitly encoded, double-terminated storage. +MAPI exports are omitted from the safe projection until their required +initialization lifecycle is modeled. Validated native structs receive generated factories and branded storage: diff --git a/tests/e2e/e2e_test.ps1 b/tests/e2e/e2e_test.ps1 index 8a61a4db..c006f1d1 100644 --- a/tests/e2e/e2e_test.ps1 +++ b/tests/e2e/e2e_test.ps1 @@ -322,7 +322,7 @@ if ("win32" -in $Lang) { "Windows.Win32.System.Registry", "Windows.Win32.System.SystemInformation", "Windows.Win32.System.LibraryLoader", - "Windows.Win32.System.AddressBook", + "Windows.Win32.Graphics.Gdi", "Windows.Win32.System.Threading", "Windows.Win32.System.Com", "Windows.Win32.Networking.Ldap", diff --git a/tests/e2e/runners/win32/returns.mjs b/tests/e2e/runners/win32/returns.mjs index e21b8f6e..afbc040e 100644 --- a/tests/e2e/runners/win32/returns.mjs +++ b/tests/e2e/runners/win32/returns.mjs @@ -10,9 +10,10 @@ import { import { ldapGetLastError } from "../../e2e_generated/win32/win32/Windows.Win32.Networking.Ldap/Apis.js"; import { getAdaptersAddresses } from "../../e2e_generated/win32/win32/Windows.Win32.NetworkManagement.IpHelper/Apis.js"; import { - createFILETIME, - ftAddFt, -} from "../../e2e_generated/win32/win32/Windows.Win32.System.AddressBook/Apis.js"; + createPOINT, + createRECT, + ptInRect, +} from "../../e2e_generated/win32/win32/Windows.Win32.Graphics.Gdi/Apis.js"; import { createProcessInformation, createStartupInfoW, @@ -138,10 +139,16 @@ assert(year >= 2020); assert(month >= 1 && month <= 12); console.log("[win32-e2e] system time aggregate passed"); -const oneTick = createFILETIME(Buffer.from([1, 0, 0, 0, 0, 0, 0, 0])); -const twoTicks = createFILETIME(Buffer.from([2, 0, 0, 0, 0, 0, 0, 0])); -assert.equal(ftAddFt(oneTick, twoTicks).bytes.readUInt32LE(0), 3); -console.log("[win32-e2e] by-value FILETIME call passed"); +const rectBytes = Buffer.alloc(16); +rectBytes.writeInt32LE(0, 0); +rectBytes.writeInt32LE(0, 4); +rectBytes.writeInt32LE(10, 8); +rectBytes.writeInt32LE(10, 12); +const pointBytes = Buffer.alloc(8); +pointBytes.writeInt32LE(5, 0); +pointBytes.writeInt32LE(5, 4); +assert.equal(ptInRect(createRECT(rectBytes), createPOINT(pointBytes)), true); +console.log("[win32-e2e] by-value POINT call passed"); const processHandle = openProcess(0x1000, false, process.pid); assert(processHandle.result); diff --git a/tools/dynwinrt-codegen/src/codegen/win32/model.rs b/tools/dynwinrt-codegen/src/codegen/win32/model.rs index 29080b82..449f5df1 100644 --- a/tools/dynwinrt-codegen/src/codegen/win32/model.rs +++ b/tools/dynwinrt-codegen/src/codegen/win32/model.rs @@ -1463,5 +1463,11 @@ fn validate_module(module: &str) -> Result<(), String> { { return Err(format!("module `{module}` is not a bare System32 DLL name")); } + if lower == "mapi32.dll" { + return Err( + "module `mapi32.dll` requires MAPI/MAPI utility initialization that the safe flat Win32 projection does not model" + .into(), + ); + } Ok(()) } diff --git a/tools/dynwinrt-codegen/tests/win32_flat_test.rs b/tools/dynwinrt-codegen/tests/win32_flat_test.rs index 4b6dce4e..c3997a3a 100644 --- a/tools/dynwinrt-codegen/tests/win32_flat_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_flat_test.rs @@ -77,6 +77,38 @@ fn scalar_return_projects_directly_without_result_wrapper() { assert!(output.js.contains("DynWin32.nativeStruct(systemTime")); } +#[test] +fn mapi_exports_fail_closed_without_an_initialization_contract() { + let Some(winmd) = configured_winmd() else { + return; + }; + let raw = dynwinrt_codegen::win32_metadata::parse_apis( + &winmd, + "Windows.Win32.System.AddressBook", + "Apis", + ) + .unwrap(); + let function = raw + .functions + .iter() + .find(|function| function.name == "FtAddFt") + .expect("FtAddFt metadata") + .clone(); + let projection = dynwinrt_codegen::codegen::win32::project_apis( + &dynwinrt_codegen::win32_metadata::RawApis { + namespace: raw.namespace, + class_name: raw.class_name, + functions: vec![function], + }, + ); + assert_eq!(projection.complete_count(), 0); + assert!( + projection.omitted[0] + .reason + .contains("requires MAPI/MAPI utility initialization") + ); +} + #[test] fn generated_safe_surface_uses_unsafe_binding_only_internally() { let Some(winmd) = configured_winmd() else { From 13e47588fc54b14cf11364af329aa882ddefaddc Mon Sep 17 00:00:00 2001 From: "Leilei Zhang (from Dev Box)" Date: Wed, 19 Aug 2026 16:29:13 +0800 Subject: [PATCH 4/4] Use IOCP and explicit Win32 subsystem contexts Replace blocking OVERLAPPED waiters with a windows-rs IOCP runtime and add guarded Winsock, GDI+, Media Foundation, and MAPI utility initialization contexts with generated codegen requirements and live E2E coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6307886d-3c24-4596-8924-ba44b0e850a5 --- .github/workflows/build.yml | 6 +- Cargo.lock | 1 - bindings/js/Cargo.toml | 7 +- bindings/js/README.md | 3 +- bindings/js/__test__/index.spec.ts | 120 ++- bindings/js/scripts/generate-entrypoints.mjs | 1 + bindings/js/src/lib.rs | 2 + bindings/js/src/win32.rs | 706 ++++++++++++------ bindings/js/src/win32_subsystem.rs | 447 +++++++++++ docs/architecture/flat-win32-support.md | 44 +- docs/guides/windows/flat-win32-usage.md | 35 +- tests/e2e/e2e_test.ps1 | 5 +- tests/e2e/runners/win32/returns.mjs | 28 +- tests/e2e/runners/win32/subsystems.mjs | 46 ++ .../dynwinrt-codegen/src/codegen/win32/ir.rs | 9 + .../src/codegen/win32/model.rs | 33 +- .../src/codegen/win32/project.rs | 2 + .../src/codegen/win32/render.rs | 87 ++- .../dynwinrt-codegen/tests/win32_flat_test.rs | 98 +++ 19 files changed, 1305 insertions(+), 375 deletions(-) create mode 100644 bindings/js/src/win32_subsystem.rs create mode 100644 tests/e2e/runners/win32/subsystems.mjs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2778e22a..8d57c91a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,11 +69,11 @@ jobs: if ($result.eligible_functions -ne 18321) { throw "Flat Win32 census denominator changed: $($result.eligible_functions)" } - if ($result.complete_functions -lt 8943) { + if ($result.complete_functions -lt 8936) { throw "Flat Win32 complete coverage regressed: $($result.complete_functions)" } - if ($result.coverage_percent -lt 48.8) { - throw "Flat Win32 coverage fell below 48.8%: $($result.coverage_percent)" + if ($result.coverage_percent -lt 48.7) { + throw "Flat Win32 coverage fell below 48.7%: $($result.coverage_percent)" } Write-Host "Flat Win32 coverage: $($result.complete_functions)/$($result.eligible_functions) ($($result.coverage_percent)%)" - name: Test Classic COM failure cleanup contracts diff --git a/Cargo.lock b/Cargo.lock index a7695e3b..ff0a4331 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -509,7 +509,6 @@ dependencies = [ "serde_json", "windows", "windows-future", - "windows-link", "windows-string", ] diff --git a/bindings/js/Cargo.toml b/bindings/js/Cargo.toml index 392b08d1..4d97f3da 100644 --- a/bindings/js/Cargo.toml +++ b/bindings/js/Cargo.toml @@ -23,7 +23,6 @@ windows-string = "0.0.0" pollster = "0.4.0" serde_json = "1" libffi = "5.1.0" -windows-link = "0.2.1" [dependencies.windows] version = ">=0.59, <=0.62" @@ -31,6 +30,12 @@ features = [ "Data_Xml_Dom", "Win32_Security", "Win32_System_Threading", + "Win32_System_IO", + "Win32_Storage_FileSystem", + "Win32_Networking_WinSock", + "Win32_Graphics_GdiPlus", + "Win32_Media_MediaFoundation", + "Win32_System_AddressBook", "Win32_Storage_Packaging_Appx", "Web_Http", "Win32_System_SystemInformation", diff --git a/bindings/js/README.md b/bindings/js/README.md index 55f26df2..89c5369d 100644 --- a/bindings/js/README.md +++ b/bindings/js/README.md @@ -66,7 +66,8 @@ Flat Win32 exports use `@microsoft/dynwinrt/win32`. Generated wrappers bind an immutable native call plan, accept retained Buffer storage for dereferenced pointers, and return `DynWin32Resource` for owned handles. Arbitrary numeric addresses and manual raw ABI plans require -`@microsoft/dynwinrt/win32/unsafe`. See the +`@microsoft/dynwinrt/win32/unsafe`. Winsock, GDI+, and Media Foundation +namespaces expose explicit initialization contexts. See the [flat Win32 usage guide](../../docs/guides/windows/flat-win32-usage.md). Unambiguous public WinRT activation metadata is projected as JavaScript constructors. diff --git a/bindings/js/__test__/index.spec.ts b/bindings/js/__test__/index.spec.ts index 25f12553..b3176b91 100644 --- a/bindings/js/__test__/index.spec.ts +++ b/bindings/js/__test__/index.spec.ts @@ -33,10 +33,7 @@ import { } from '../dist/com-unsafe.js' import { DynWin32 } from '../dist/win32.js' import * as win32Runtime from '../dist/win32.js' -import { - DynWin32Function, - DynWin32Unsafe, -} from '../dist/win32-unsafe.js' +import { DynWin32Function, DynWin32Unsafe } from '../dist/win32-unsafe.js' const requireFromTest = createRequire(import.meta.url) const nativeRuntime = requireFromTest('../dist/index.js') as Record @@ -152,15 +149,11 @@ test('package facades exactly partition native exports', (t) => { 'DynWin32NativeStruct', 'DynWin32OverlappedOperation', 'DynWin32Resource', + 'DynWin32SubsystemContext', 'DynWin32Value', 'DynWinRtValue', ]) - const unsafeWin32Names = new Set([ - ...safeWin32Names, - 'DynWin32CallResult', - 'DynWin32Function', - 'DynWin32Unsafe', - ]) + const unsafeWin32Names = new Set([...safeWin32Names, 'DynWin32CallResult', 'DynWin32Function', 'DynWin32Unsafe']) t.deepEqual(moduleKeys(winrtCjsRuntime), expectedWinrt) t.deepEqual( @@ -195,10 +188,7 @@ test('package facades exactly partition native exports', (t) => { }) test('COM allocation declaration is opaque and non-constructible', (t) => { - const declaration = readFileSync( - fileURLToPath(new URL('../dist/com.d.ts', import.meta.url)), - 'utf8', - ) + const declaration = readFileSync(fileURLToPath(new URL('../dist/com.d.ts', import.meta.url)), 'utf8') t.regex(declaration, /export interface DynComAllocation/) t.notRegex(declaration, /export \{[^}]*DynComAllocation[^}]*\} from/) }) @@ -240,13 +230,9 @@ test('Classic COM raw ABI access requires the explicit unsafe entrypoint', (t) = }) test('DynCom rejects invalid WinRT async result signatures', (t) => { - const invalidAsyncType = DynWinRtType.iAsyncOperation( - DynWinRtType.arrayType(DynWinRtType.i32()), - ) + const invalidAsyncType = DynWinRtType.iAsyncOperation(DynWinRtType.arrayType(DynWinRtType.i32())) - const error = t.throws(() => - DynCom.projectWinRtAsync(DynWinRtValue.nullValue(), invalidAsyncType), - ) + const error = t.throws(() => DynCom.projectWinRtAsync(DynWinRtValue.nullValue(), invalidAsyncType)) t.regex(error.message, /valid WinRT signature/) }) @@ -254,12 +240,7 @@ test('DynCom verifies the projected WinRT async interface IID', (t) => { roInitialize(1) const factory = DynWinRtValue.activationFactory('Windows.Foundation.Uri') try { - const error = t.throws(() => - DynCom.projectWinRtAsync( - factory, - DynWinRtType.iAsyncOperation(DynWinRtType.i32()), - ), - ) + const error = t.throws(() => DynCom.projectWinRtAsync(factory, DynWinRtType.iAsyncOperation(DynWinRtType.i32()))) t.regex(error.message, /0x80004002|interface/i) } finally { factory.release() @@ -339,17 +320,11 @@ test('flat Win32 native aggregate storage is aligned, branded, and mutable', (t) x64: { size: 8, alignment: 4, fields: [] }, arm64: { size: 8, alignment: 4, fields: [] }, }) - const point = DynWin32.createNativeStruct( - descriptor, - Buffer.from([1, 0, 0, 0, 2, 0, 0, 0]), - ) + const point = DynWin32.createNativeStruct(descriptor, Buffer.from([1, 0, 0, 0, 2, 0, 0, 0])) t.is(point.length, 8) t.deepEqual([...point.bytes], [1, 0, 0, 0, 2, 0, 0, 0]) t.truthy(DynWin32.nativeStruct(point, descriptor)) - t.throws( - () => DynWin32.nativeStruct(point, descriptor.replace('POINT', 'SIZE')), - { message: /type mismatch/ }, - ) + t.throws(() => DynWin32.nativeStruct(point, descriptor.replace('POINT', 'SIZE')), { message: /type mismatch/ }) const hugeDescriptor = JSON.stringify({ name: 'Tests.HUGE', @@ -379,6 +354,32 @@ test('flat Win32 multi-strings require double-NUL storage', (t) => { t.truthy(DynWin32.ansiMultiString(Buffer.from([65, 0, 0]))) }) +test('flat Win32 subsystem contexts enforce kind and close state', (t) => { + const winsock = DynWin32.initializeWinsock() + t.is(winsock.subsystem, 'winsock') + t.false(winsock.closed) + t.notThrows(() => DynWin32.requireSubsystem(winsock, 'winsock')) + t.throws(() => DynWin32.requireSubsystem(winsock, 'gdiplus'), { + message: /require a gdiplus context, received winsock/, + }) + winsock.close() + t.true(winsock.closed) + t.throws(() => DynWin32.requireSubsystem(winsock, 'winsock'), { + message: /context is closed/, + }) + t.notThrows(() => winsock.close()) + + const gdiplus = DynWin32.initializeGdiPlus() + t.notThrows(() => DynWin32.requireSubsystem(gdiplus, 'gdiplus')) + gdiplus.close() + + const mediaFoundation = DynWin32.initializeMediaFoundation() + t.notThrows(() => DynWin32.requireSubsystem(mediaFoundation, 'mediaFoundation')) + mediaFoundation.close() + + t.is(typeof DynWin32.initializeMapiUtilities, 'function') +}) + test('flat Win32 pointer-bearing aggregates retain safe field owners', (t) => { const descriptor = JSON.stringify({ name: 'Windows.Win32.Security.SECURITY_ATTRIBUTES', @@ -419,31 +420,21 @@ test('flat Win32 pointer-bearing aggregates retain safe field owners', (t) => { DynWin32.setNativeStructU32(attributes, descriptor, 'nLength', attributes.length) DynWin32.setNativeStructBool32(attributes, descriptor, 'bInheritHandle', true) const descriptorBytes = new Uint8Array(20) - DynWin32.setNativeStructPointer( - attributes, - descriptor, - 'lpSecurityDescriptor', - DynWin32.dataPointer(descriptorBytes), - ) + DynWin32.setNativeStructPointer(attributes, descriptor, 'lpSecurityDescriptor', DynWin32.dataPointer(descriptorBytes)) t.throws(() => attributes.bytes, { message: /unavailable/ }) t.throws( () => - DynWin32.setNativeStructPointer( - attributes, - descriptor, - 'lpSecurityDescriptor', - DynWin32Unsafe.pointer(0x1234n), - ), + DynWin32.setNativeStructPointer(attributes, descriptor, 'lpSecurityDescriptor', DynWin32Unsafe.pointer(0x1234n)), { message: /retained Buffer or string storage/ }, ) const aggregate = DynWin32.nativeStruct(attributes, descriptor) structuredClone(descriptorBytes.buffer, { transfer: [descriptorBytes.buffer] }) const noArgs = DynWin32Function.bind({ - dll: 'kernel32.dll', - entryPoint: 'GetLastError', - parameters: [], - returnType: 'u32', + dll: 'kernel32.dll', + entryPoint: 'GetLastError', + parameters: [], + returnType: 'u32', }) t.throws(() => noArgs.invoke([aggregate]), { message: /detached/ }) }) @@ -462,24 +453,18 @@ test('DynCom rejects pointers after their TypedArray backing store is detached', test('Generated COM pointer helpers reject arbitrary numeric addresses', (t) => { t.truthy(DynCom.safeDataPointer(Buffer.alloc(8))) t.truthy(DynCom.safeDataPointer(new Uint8Array(8))) - t.throws( - () => - (DynCom.safeDataPointer as unknown as (value: bigint) => unknown)(0x1234n), - { message: /arbitrary numeric addresses/ }, - ) + t.throws(() => (DynCom.safeDataPointer as unknown as (value: bigint) => unknown)(0x1234n), { + message: /arbitrary numeric addresses/, + }) t.truthy(DynCom.safeWideStringPointer('wide')) t.truthy(DynCom.safeAnsiStringPointer('ansi')) - t.throws( - () => - (DynCom.safeWideStringPointer as unknown as (value: bigint) => unknown)(0x1234n), - { message: /arbitrary numeric addresses/ }, - ) - t.throws( - () => - (DynCom.safeAnsiStringPointer as unknown as (value: number) => unknown)(0x1234), - { message: /arbitrary numeric addresses/ }, - ) + t.throws(() => (DynCom.safeWideStringPointer as unknown as (value: bigint) => unknown)(0x1234n), { + message: /arbitrary numeric addresses/, + }) + t.throws(() => (DynCom.safeAnsiStringPointer as unknown as (value: number) => unknown)(0x1234), { + message: /arbitrary numeric addresses/, + }) }) test('DynCom rejects detached counted buffers and accepts typed backing widths', (t) => { @@ -1202,10 +1187,7 @@ test('u64 arrays and struct fields preserve the full unsigned range', (t) => { message: /fit in an unsigned 64-bit integer/, }) - const structType = DynWinRtType.structType('DynWinRT.Tests.IntegerBoundary', [ - DynWinRtType.u64(), - DynWinRtType.i64(), - ]) + const structType = DynWinRtType.structType('DynWinRT.Tests.IntegerBoundary', [DynWinRtType.u64(), DynWinRtType.i64()]) const value = DynWinRtStruct.create(structType) value.setU64(0, 2n ** 64n - 1n) t.is(value.getU64(0), 2n ** 64n - 1n) diff --git a/bindings/js/scripts/generate-entrypoints.mjs b/bindings/js/scripts/generate-entrypoints.mjs index 2d4e325b..81f94db7 100644 --- a/bindings/js/scripts/generate-entrypoints.mjs +++ b/bindings/js/scripts/generate-entrypoints.mjs @@ -67,6 +67,7 @@ const win32Exports = new Set([ 'DynWin32NativeStruct', 'DynWin32OverlappedOperation', 'DynWin32Resource', + 'DynWin32SubsystemContext', 'DynWin32Value', 'DynWinRtValue', ]) diff --git a/bindings/js/src/lib.rs b/bindings/js/src/lib.rs index 18887529..3f7f3282 100644 --- a/bindings/js/src/lib.rs +++ b/bindings/js/src/lib.rs @@ -28,6 +28,8 @@ pub use win32::{ DynWin32, DynWin32CallResult, DynWin32Function, DynWin32FunctionSpec, DynWin32NativeStruct, DynWin32ParameterSpec, DynWin32Resource, DynWin32Unsafe, DynWin32Value, }; +mod win32_subsystem; +pub use win32_subsystem::DynWin32SubsystemContext; mod async_promise; mod managed_tsfn; mod scheduled_start; diff --git a/bindings/js/src/win32.rs b/bindings/js/src/win32.rs index ad7c6a26..8b125908 100644 --- a/bindings/js/src/win32.rs +++ b/bindings/js/src/win32.rs @@ -1,67 +1,32 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use std::cell::UnsafeCell; -use std::collections::{BTreeMap, VecDeque}; -use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; -use std::sync::{Arc, Condvar, LazyLock, Mutex}; +use std::collections::{BTreeMap, HashMap}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, LazyLock, Mutex, Weak}; use napi::bindgen_prelude::{BigInt, Buffer, FromNapiValue, Function, ToNapiValue, Unknown}; use napi::JsValue; use napi_derive::napi; +use windows::Win32::Foundation::{HANDLE, INVALID_HANDLE_VALUE}; +use windows::Win32::Storage::FileSystem::{ReadFile, WriteFile}; +use windows::Win32::System::IO::{ + CancelIoEx, CreateIoCompletionPort, GetQueuedCompletionStatus, OVERLAPPED, OVERLAPPED_0_0, +}; -use super::{com, managed_tsfn::ManagedTsfn, DynWinRTValue, WinGUID}; +use super::{ + com, managed_tsfn::ManagedTsfn, win32_subsystem, DynWin32SubsystemContext, DynWinRTValue, WinGUID, +}; const ERROR_IO_PENDING: u32 = 997; const ERROR_OPERATION_ABORTED: u32 = 995; const ERROR_HANDLE_EOF: u32 = 38; const ERROR_BROKEN_PIPE: u32 = 109; const MAX_NATIVE_AGGREGATE_DESCRIPTOR_LENGTH: usize = 1024 * 1024; -const OVERLAPPED_WAITER_THREADS: usize = 8; - -#[repr(C)] -struct NativeOverlapped { - internal: usize, - internal_high: usize, - offset: u32, - offset_high: u32, - event: *mut std::ffi::c_void, -} - -windows_link::link!("kernel32.dll" "system" "CreateEventW" fn create_event_w( - event_attributes: *mut std::ffi::c_void, - manual_reset: i32, - initial_state: i32, - name: *const u16, -) -> *mut std::ffi::c_void); -windows_link::link!("kernel32.dll" "system" "ReadFile" fn read_file_overlapped( - file: *mut std::ffi::c_void, - buffer: *mut std::ffi::c_void, - bytes_to_read: u32, - bytes_read: *mut u32, - overlapped: *mut NativeOverlapped, -) -> i32); -windows_link::link!("kernel32.dll" "system" "WriteFile" fn write_file_overlapped( - file: *mut std::ffi::c_void, - buffer: *const std::ffi::c_void, - bytes_to_write: u32, - bytes_written: *mut u32, - overlapped: *mut NativeOverlapped, -) -> i32); -windows_link::link!("kernel32.dll" "system" "GetOverlappedResult" fn get_overlapped_result( - file: *mut std::ffi::c_void, - overlapped: *mut NativeOverlapped, - transferred: *mut u32, - wait: i32, -) -> i32); -windows_link::link!("kernel32.dll" "system" "CancelIoEx" fn cancel_io_ex( - file: *mut std::ffi::c_void, - overlapped: *mut NativeOverlapped, -) -> i32); -windows_link::link!("kernel32.dll" "system" "CloseHandle" fn close_native_handle( - handle: *mut std::ffi::c_void, -) -> i32); -windows_link::link!("kernel32.dll" "system" "GetLastError" fn get_last_error() -> u32); +const IOCP_COMPLETION_WORKERS_MAX: usize = 4; +const IOCP_MAX_PENDING_OPERATIONS: usize = 1024; +const IOCP_MAX_OPERATION_BUFFER_BYTES: usize = 64 * 1024 * 1024; +const IOCP_MAX_PENDING_BUFFER_BYTES: usize = 256 * 1024 * 1024; #[napi(object)] pub struct DynWin32ParameterSpec { @@ -466,6 +431,23 @@ impl DynWin32Function { #[napi] pub fn invoke(&self, args: Vec<&DynWin32Value>) -> napi::Result { + self.invoke_impl(args) + } + + #[napi] + pub fn invoke_with_subsystem( + &self, + context: &DynWin32SubsystemContext, + subsystem: String, + args: Vec<&DynWin32Value>, + ) -> napi::Result { + let _subsystem_guard = win32_subsystem::call_guard(context, &subsystem)?; + self.invoke_impl(args) + } +} + +impl DynWin32Function { + fn invoke_impl(&self, args: Vec<&DynWin32Value>) -> napi::Result { for value in &args { value.validate()?; } @@ -563,44 +545,38 @@ enum OverlappedIoKind { struct OverlappedControl { active: bool, handle: usize, + overlapped: *const OVERLAPPED, } struct OverlappedState { - overlapped: UnsafeCell, control: Mutex, cancelled: AtomicBool, } +// Safety: the OVERLAPPED pointer is read only while protected by `control` and +// remains pinned in the IOCP registry until `deactivate` clears it. unsafe impl Send for OverlappedState {} unsafe impl Sync for OverlappedState {} impl OverlappedState { - fn new(offset: u64) -> Arc { + fn new() -> Arc { Arc::new(Self { - overlapped: UnsafeCell::new(NativeOverlapped { - internal: 0, - internal_high: 0, - offset: offset as u32, - offset_high: (offset >> 32) as u32, - event: std::ptr::null_mut(), - }), control: Mutex::new(OverlappedControl { active: false, handle: 0, + overlapped: std::ptr::null(), }), cancelled: AtomicBool::new(false), }) } - fn activate(&self, handle: usize, event: *mut std::ffi::c_void) { - unsafe { - (*self.overlapped.get()).event = event; - } + fn activate(&self, handle: usize, overlapped: *const OVERLAPPED) { let mut control = self .control .lock() .unwrap_or_else(|error| error.into_inner()); control.handle = handle; + control.overlapped = overlapped; control.active = true; } @@ -611,6 +587,7 @@ impl OverlappedState { .unwrap_or_else(|error| error.into_inner()); control.active = false; control.handle = 0; + control.overlapped = std::ptr::null(); } fn cancel(&self) { @@ -619,36 +596,27 @@ impl OverlappedState { .control .lock() .unwrap_or_else(|error| error.into_inner()); - if control.active { - unsafe { - cancel_io_ex( - control.handle as *mut std::ffi::c_void, - self.overlapped.get(), - ); - } - } - } -} - -struct NativeEvent(*mut std::ffi::c_void); - -impl Drop for NativeEvent { - fn drop(&mut self) { - if !self.0.is_null() { - unsafe { - close_native_handle(self.0); - } + if control.active && !control.overlapped.is_null() { + let _ = unsafe { + CancelIoEx( + HANDLE(control.handle as *mut std::ffi::c_void), + Some(control.overlapped), + ) + }; } } } pub struct OverlappedIoTask { kind: OverlappedIoKind, + resource: Arc, lease: dynwinrt::win32::OwnedResourceAsyncLease, buffer: Option, buffer_len: usize, native_buffer: Vec, + offset: u64, state: Arc, + _reservation: Option, } struct OverlappedCompletion { @@ -656,94 +624,358 @@ struct OverlappedCompletion { result: Result, } -struct OverlappedWork { +struct IocpOperation { + overlapped: OVERLAPPED, task: OverlappedIoTask, completion: ManagedTsfn, } -struct OverlappedWaiterQueue { - work: Mutex>, - available: Condvar, - in_flight: AtomicUsize, +// Safety: the operation has exclusive ownership while it is moved into the +// mutex-protected IOCP registry and moved out exactly once on completion. +unsafe impl Send for IocpOperation {} + +#[derive(Default)] +struct IocpRegistry { + operations: HashMap>, } -struct OverlappedWaiterPool { - queue: Arc, +#[derive(Default)] +struct IocpCapacity { + operations: usize, + buffer_bytes: usize, } -struct OverlappedInFlight<'a>(&'a AtomicUsize); +struct IocpReservation { + capacity: Arc>, + buffer_bytes: usize, +} + +impl IocpReservation { + fn acquire(capacity: &Arc>, buffer_bytes: usize) -> napi::Result { + let mut state = capacity.lock().unwrap_or_else(|error| error.into_inner()); + validate_iocp_capacity(state.operations, state.buffer_bytes, buffer_bytes)?; + state.operations += 1; + state.buffer_bytes += buffer_bytes; + drop(state); + Ok(Self { + capacity: Arc::clone(capacity), + buffer_bytes, + }) + } +} -impl Drop for OverlappedInFlight<'_> { +impl Drop for IocpReservation { fn drop(&mut self) { - self.0.fetch_sub(1, Ordering::AcqRel); + let mut state = self + .capacity + .lock() + .unwrap_or_else(|error| error.into_inner()); + state.operations = state + .operations + .checked_sub(1) + .expect("IOCP operation accounting remains balanced"); + state.buffer_bytes = state + .buffer_bytes + .checked_sub(self.buffer_bytes) + .expect("IOCP Buffer accounting remains balanced"); } } -static OVERLAPPED_WAITER_POOL: LazyLock> = - LazyLock::new(OverlappedWaiterPool::new); +struct IocpRuntime { + port: usize, + associations: Mutex>>, + registry: Mutex, + capacity: Arc>, + shutting_down: AtomicBool, +} -impl OverlappedWaiterPool { - fn new() -> Result { - let queue = Arc::new(OverlappedWaiterQueue { - work: Mutex::new(VecDeque::new()), - available: Condvar::new(), - in_flight: AtomicUsize::new(0), +static IOCP_RUNTIME: LazyLock, String>> = LazyLock::new(IocpRuntime::new); + +impl IocpRuntime { + fn new() -> Result, String> { + let worker_count = std::thread::available_parallelism() + .map(|count| count.get().min(IOCP_COMPLETION_WORKERS_MAX)) + .unwrap_or(2) + .max(1); + let port = unsafe { + CreateIoCompletionPort( + INVALID_HANDLE_VALUE, + None, + 0, + u32::try_from(worker_count).expect("IOCP worker count fits u32"), + ) + } + .map_err(|error| format!("CreateIoCompletionPort failed: {error}"))?; + let runtime = Arc::new(Self { + port: port.0 as usize, + associations: Mutex::new(HashMap::new()), + registry: Mutex::new(IocpRegistry::default()), + capacity: Arc::new(Mutex::new(IocpCapacity::default())), + shutting_down: AtomicBool::new(false), }); - for index in 0..OVERLAPPED_WAITER_THREADS { - let worker_queue = Arc::clone(&queue); - std::thread::Builder::new() - .name(format!("dynwinrt-overlapped-waiter-{index}")) - .spawn(move || overlapped_waiter_loop(&worker_queue)) - .map_err(|error| format!("Failed to create bounded OVERLAPPED waiter: {error}"))?; + for index in 0..worker_count { + let worker = Arc::clone(&runtime); + if let Err(error) = std::thread::Builder::new() + .name(format!("dynwinrt-iocp-completion-{index}")) + .spawn(move || worker.completion_loop()) + { + runtime.shutting_down.store(true, Ordering::Release); + let _ = unsafe { windows::Win32::Foundation::CloseHandle(port) }; + return Err(format!("Failed to create IOCP completion worker: {error}")); + } } - Ok(Self { queue }) + Ok(runtime) } - fn submit(&self, work: OverlappedWork) -> napi::Result<()> { - self - .queue - .in_flight - .fetch_update(Ordering::AcqRel, Ordering::Acquire, |count| { - (count < OVERLAPPED_WAITER_THREADS).then_some(count + 1) - }) - .map_err(|_| { - napi::Error::from_reason(format!( - "OVERLAPPED waiter capacity is full ({OVERLAPPED_WAITER_THREADS} active operations)" - )) - })?; - let mut queue = self - .queue - .work + fn port(&self) -> HANDLE { + HANDLE(self.port as *mut std::ffi::c_void) + } + + fn associate( + &self, + resource: &Arc, + handle: usize, + ) -> napi::Result<()> { + let identity = Arc::as_ptr(resource) as usize; + let mut associations = self + .associations .lock() .unwrap_or_else(|error| error.into_inner()); - queue.push_back(work); - self.queue.available.notify_one(); + associations.retain(|_, resource| resource.strong_count() != 0); + if associations + .get(&identity) + .and_then(Weak::upgrade) + .is_some_and(|existing| Arc::ptr_eq(&existing, resource)) + { + return Ok(()); + } + associations.remove(&identity); + let associated = unsafe { + CreateIoCompletionPort( + HANDLE(handle as *mut std::ffi::c_void), + Some(self.port()), + 0, + 0, + ) + } + .map_err(|error| { + napi::Error::from_reason(format!( + "Failed to associate Win32 resource with dynwinrt IOCP: {error}" + )) + })?; + if associated != self.port() { + return Err(napi::Error::from_reason( + "Win32 resource was associated with an unexpected IOCP", + )); + } + associations.insert(identity, Arc::downgrade(resource)); Ok(()) } -} -fn overlapped_waiter_loop(queue: &OverlappedWaiterQueue) { - loop { - let work = { - let mut pending = queue.work.lock().unwrap_or_else(|error| error.into_inner()); - while pending.is_empty() { - pending = queue - .available - .wait(pending) - .unwrap_or_else(|error| error.into_inner()); - } - pending.pop_front().expect("waiter queue is not empty") + fn submit( + &self, + mut task: OverlappedIoTask, + completion: ManagedTsfn, + ) -> napi::Result<()> { + if task.state.cancelled.load(Ordering::Acquire) { + return Err(napi::Error::from_reason("OVERLAPPED operation was aborted")); + } + let handle = task.lease.raw(); + task._reservation = Some(IocpReservation::acquire( + &self.capacity, + task.native_buffer.len(), + )?); + + let mut overlapped = OVERLAPPED::default(); + overlapped.Anonymous.Anonymous = OVERLAPPED_0_0 { + Offset: task.offset as u32, + OffsetHigh: (task.offset >> 32) as u32, }; - let _in_flight = OverlappedInFlight(&queue.in_flight); - let OverlappedWork { - mut task, + let mut operation = Box::new(IocpOperation { + overlapped, + task, completion, - } = work; - let result = task.compute().map_err(|error| error.reason.clone()); - let _ = completion.call(OverlappedCompletion { task, result }); + }); + let overlapped_ptr = &mut operation.overlapped as *mut OVERLAPPED; + let key = overlapped_ptr as usize; + + let mut registry = self + .registry + .lock() + .unwrap_or_else(|error| error.into_inner()); + self.associate(&operation.task.resource, handle)?; + if registry.operations.contains_key(&key) { + return Err(napi::Error::from_reason( + "duplicate OVERLAPPED operation address", + )); + } + operation.task.state.activate(handle, overlapped_ptr); + operation.task.lease.mark_active(); + let previous = registry.operations.insert(key, operation); + debug_assert!(previous.is_none()); + + let result = { + let operation = registry + .operations + .get_mut(&key) + .expect("IOCP operation was just registered"); + unsafe { + match operation.task.kind { + OverlappedIoKind::Read => ReadFile( + HANDLE(handle as *mut std::ffi::c_void), + Some(operation.task.native_buffer.as_mut_slice()), + None, + Some(overlapped_ptr), + ), + OverlappedIoKind::Write => WriteFile( + HANDLE(handle as *mut std::ffi::c_void), + Some(operation.task.native_buffer.as_slice()), + None, + Some(overlapped_ptr), + ), + } + } + }; + let error = result.err().map(|error| win32_error_code(&error)); + if let Some(error) = error.filter(|error| *error != ERROR_IO_PENDING) { + let mut operation = + remove_iocp_operation(&mut registry, key).expect("failed IOCP operation was registered"); + drop(registry); + operation.task.state.deactivate(); + operation.task.lease.mark_inactive(); + if is_read_eof(operation.task.kind, error) { + let _ = operation.completion.call(OverlappedCompletion { + task: operation.task, + result: Ok(0), + }); + return Ok(()); + } + return Err(native_error( + match operation.task.kind { + OverlappedIoKind::Read => "ReadFile", + OverlappedIoKind::Write => "WriteFile", + }, + error, + )); + } + if registry + .operations + .get(&key) + .expect("submitted IOCP operation remains registered") + .task + .state + .cancelled + .load(Ordering::Acquire) + { + registry + .operations + .get(&key) + .expect("submitted IOCP operation remains registered") + .task + .state + .cancel(); + } + Ok(()) + } + + fn completion_loop(self: &Arc) { + loop { + let mut transferred = 0u32; + let mut completion_key = 0usize; + let mut overlapped = std::ptr::null_mut(); + let result = unsafe { + GetQueuedCompletionStatus( + self.port(), + &mut transferred, + &mut completion_key, + &mut overlapped, + u32::MAX, + ) + }; + if overlapped.is_null() { + if self.shutting_down.load(Ordering::Acquire) { + return; + } + if let Err(error) = result { + eprintln!("[dynwinrt] IOCP completion wait failed: {error}"); + } + continue; + } + let error = result.err().map(|error| win32_error_code(&error)); + self.complete(overlapped, transferred, error); + } + } + + fn complete(&self, overlapped: *mut OVERLAPPED, transferred: u32, error: Option) { + let mut registry = self + .registry + .lock() + .unwrap_or_else(|error| error.into_inner()); + let Some(mut operation) = remove_iocp_operation(&mut registry, overlapped as usize) else { + eprintln!( + "[dynwinrt] ignored completion for unknown OVERLAPPED {:p}", + overlapped + ); + return; + }; + drop(registry); + + operation.task.state.deactivate(); + operation.task.lease.mark_inactive(); + let result = match error { + Some(error) if is_read_eof(operation.task.kind, error) => Ok(0), + Some(error) => Err(format!( + "{} failed with Win32 error {error}", + if error == ERROR_OPERATION_ABORTED { + "OVERLAPPED operation" + } else { + "IOCP completion" + } + )), + None => Ok(transferred), + }; + let _ = operation.completion.call(OverlappedCompletion { + task: operation.task, + result, + }); } } +fn validate_iocp_capacity( + operation_count: usize, + buffer_bytes: usize, + new_buffer_bytes: usize, +) -> napi::Result<()> { + validate_iocp_operation_buffer(new_buffer_bytes)?; + if operation_count >= IOCP_MAX_PENDING_OPERATIONS { + return Err(napi::Error::from_reason(format!( + "IOCP pending operation limit ({IOCP_MAX_PENDING_OPERATIONS}) was reached" + ))); + } + let total = buffer_bytes + .checked_add(new_buffer_bytes) + .ok_or_else(|| napi::Error::from_reason("IOCP pending Buffer accounting overflow"))?; + if total > IOCP_MAX_PENDING_BUFFER_BYTES { + return Err(napi::Error::from_reason(format!( + "IOCP pending native Buffer limit ({IOCP_MAX_PENDING_BUFFER_BYTES} bytes) would be exceeded" + ))); + } + Ok(()) +} + +fn validate_iocp_operation_buffer(buffer_bytes: usize) -> napi::Result<()> { + if buffer_bytes > IOCP_MAX_OPERATION_BUFFER_BYTES { + return Err(napi::Error::from_reason(format!( + "IOCP operation Buffer exceeds the {IOCP_MAX_OPERATION_BUFFER_BYTES} byte limit" + ))); + } + Ok(()) +} + +fn remove_iocp_operation(registry: &mut IocpRegistry, key: usize) -> Option> { + registry.operations.remove(&key) +} + #[napi] pub struct DynWin32OverlappedOperation { task: Option, @@ -777,25 +1009,14 @@ impl DynWin32OverlappedOperation { |completion: OverlappedCompletion, env| completion.into_js_arguments(env), None, )?; - OVERLAPPED_WAITER_POOL + IOCP_RUNTIME .as_ref() .map_err(|error| napi::Error::from_reason(error.clone()))? - .submit(OverlappedWork { task, completion }) + .submit(task, completion) } } impl OverlappedIoTask { - fn compute(&mut self) -> napi::Result { - let handle = self.lease.raw(); - perform_overlapped_io( - self.kind, - handle, - &mut self.native_buffer, - &self.state, - &mut self.lease, - ) - } - fn resolve(mut self, env: napi::sys::napi_env, output: u32) -> napi::Result { if matches!(self.kind, OverlappedIoKind::Read) { let transferred = usize::try_from(output) @@ -887,96 +1108,17 @@ impl OverlappedCompletion { } } -fn perform_overlapped_io( - kind: OverlappedIoKind, - handle: usize, - buffer: &mut [u8], - state: &Arc, - lease: &mut dynwinrt::win32::OwnedResourceAsyncLease, -) -> napi::Result { - if state.cancelled.load(Ordering::Acquire) { - return Err(napi::Error::from_reason("OVERLAPPED operation was aborted")); - } - let event = unsafe { create_event_w(std::ptr::null_mut(), 1, 0, std::ptr::null()) }; - if event.is_null() { - return Err(last_error("CreateEventW")); - } - let _event = NativeEvent(event); - state.activate(handle, event); - let length = u32::try_from(buffer.len()) - .map_err(|_| napi::Error::from_reason("OVERLAPPED buffer exceeds u32"))?; - let started = unsafe { - match kind { - OverlappedIoKind::Read => read_file_overlapped( - handle as *mut std::ffi::c_void, - buffer.as_mut_ptr().cast(), - length, - std::ptr::null_mut(), - state.overlapped.get(), - ), - OverlappedIoKind::Write => write_file_overlapped( - handle as *mut std::ffi::c_void, - buffer.as_ptr().cast(), - length, - std::ptr::null_mut(), - state.overlapped.get(), - ), - } - }; - if started == 0 { - let error = unsafe { get_last_error() }; - if error != ERROR_IO_PENDING { - state.deactivate(); - if is_read_eof(kind, error) { - return Ok(0); - } - return Err(native_error( - match kind { - OverlappedIoKind::Read => "ReadFile", - OverlappedIoKind::Write => "WriteFile", - }, - error, - )); - } - lease.mark_active(); - } - if state.cancelled.load(Ordering::Acquire) { - state.cancel(); - } - let mut transferred = 0u32; - let completed = unsafe { - get_overlapped_result( - handle as *mut std::ffi::c_void, - state.overlapped.get(), - &mut transferred, - 1, - ) - }; - let error = (completed == 0).then(|| unsafe { get_last_error() }); - state.deactivate(); - lease.mark_inactive(); - if let Some(error) = error { - if is_read_eof(kind, error) { - return Ok(0); - } - return Err(native_error( - if error == ERROR_OPERATION_ABORTED { - "OVERLAPPED operation" - } else { - "GetOverlappedResult" - }, - error, - )); - } - Ok(transferred) -} - fn is_read_eof(kind: OverlappedIoKind, error: u32) -> bool { matches!(kind, OverlappedIoKind::Read) && matches!(error, ERROR_HANDLE_EOF | ERROR_BROKEN_PIPE) } -fn last_error(function: &str) -> napi::Error { - native_error(function, unsafe { get_last_error() }) +fn win32_error_code(error: &windows::core::Error) -> u32 { + let code = error.code().0 as u32; + if code & 0xffff_0000 == 0x8007_0000 { + code & 0xffff + } else { + code + } } fn native_error(function: &str, error: u32) -> napi::Error { @@ -988,6 +1130,34 @@ pub struct DynWin32; #[napi] impl DynWin32 { + #[napi] + pub fn initialize_winsock() -> napi::Result { + win32_subsystem::initialize("winsock") + } + + #[napi] + pub fn initialize_gdi_plus() -> napi::Result { + win32_subsystem::initialize("gdiplus") + } + + #[napi] + pub fn initialize_media_foundation() -> napi::Result { + win32_subsystem::initialize("mediaFoundation") + } + + #[napi] + pub fn initialize_mapi_utilities() -> napi::Result { + win32_subsystem::initialize("mapiUtilities") + } + + #[napi] + pub fn require_subsystem( + context: &DynWin32SubsystemContext, + subsystem: String, + ) -> napi::Result<()> { + win32_subsystem::require(context, &subsystem) + } + #[napi] pub fn bool8(value: bool) -> DynWin32Value { DynWin32Value::new(dynwinrt::win32::Value::U8(u8::from(value))) @@ -1604,19 +1774,23 @@ fn overlapped_io_task( }; u32::try_from(buffer.len()) .map_err(|_| napi::Error::from_reason("OVERLAPPED buffer exceeds u32"))?; + validate_iocp_operation_buffer(buffer.len())?; let lease = file .0 .async_lease(dynwinrt::win32::Cleanup::CloseHandle) .map_err(|error| napi::Error::from_reason(error.message()))?; - let state = OverlappedState::new(offset); + let state = OverlappedState::new(); Ok(DynWin32OverlappedOperation { task: Some(OverlappedIoTask { kind, + resource: Arc::clone(&file.0), lease, native_buffer: try_copy_io_buffer(kind, &buffer)?, buffer_len: buffer.len(), buffer: Some(buffer), + offset, state: Arc::clone(&state), + _reservation: None, }), state, }) @@ -2246,3 +2420,51 @@ fn handle_value(value: Unknown, nullable: bool) -> napi::Result { bits as usize, ))) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn iocp_capacity_bounds_operations_and_native_buffers() { + validate_iocp_capacity(IOCP_MAX_PENDING_OPERATIONS - 1, 0, 1).unwrap(); + assert!(validate_iocp_capacity(IOCP_MAX_PENDING_OPERATIONS, 0, 1) + .unwrap_err() + .reason + .contains("operation limit")); + + validate_iocp_capacity(0, IOCP_MAX_PENDING_BUFFER_BYTES - 1, 1).unwrap(); + assert!(validate_iocp_capacity(0, IOCP_MAX_PENDING_BUFFER_BYTES, 1) + .unwrap_err() + .reason + .contains("Buffer limit")); + assert!( + validate_iocp_operation_buffer(IOCP_MAX_OPERATION_BUFFER_BYTES + 1) + .unwrap_err() + .reason + .contains("operation Buffer") + ); + assert!(validate_iocp_capacity(0, usize::MAX, 1) + .unwrap_err() + .reason + .contains("accounting overflow")); + + let capacity = Arc::new(Mutex::new(IocpCapacity::default())); + let reservation = IocpReservation::acquire(&capacity, 16).unwrap(); + { + let state = capacity.lock().unwrap(); + assert_eq!((state.operations, state.buffer_bytes), (1, 16)); + } + drop(reservation); + let state = capacity.lock().unwrap(); + assert_eq!((state.operations, state.buffer_bytes), (0, 0)); + } + + #[test] + fn hresult_from_win32_is_decoded_for_iocp_errors() { + let error = windows::core::Error::from_hresult(windows::core::HRESULT( + 0x8007_0000u32.wrapping_add(ERROR_IO_PENDING) as i32, + )); + assert_eq!(win32_error_code(&error), ERROR_IO_PENDING); + } +} diff --git a/bindings/js/src/win32_subsystem.rs b/bindings/js/src/win32_subsystem.rs new file mode 100644 index 00000000..f5b4bfbd --- /dev/null +++ b/bindings/js/src/win32_subsystem.rs @@ -0,0 +1,447 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::mem::MaybeUninit; +use std::sync::{LazyLock, Mutex, MutexGuard}; + +use napi_derive::napi; +use windows::Win32::Graphics::GdiPlus::{ + GdiplusShutdown, GdiplusStartup, GdiplusStartupInput, Ok as GDIPLUS_OK, +}; +use windows::Win32::Media::MediaFoundation::{MFShutdown, MFStartup, MFSTARTUP_FULL, MF_VERSION}; +use windows::Win32::Networking::WinSock::{WSACleanup, WSAGetLastError, WSAStartup, WSADATA}; +use windows::Win32::System::AddressBook::{DeinitMapiUtil, ScInitMapiUtil}; + +const WINSOCK_VERSION_2_2: u16 = 0x0202; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum SubsystemKind { + Winsock, + GdiPlus, + MediaFoundation, + MapiUtilities, +} + +impl SubsystemKind { + fn parse(value: &str) -> napi::Result { + match value.to_ascii_lowercase().as_str() { + "winsock" => Ok(Self::Winsock), + "gdiplus" | "gdi+" => Ok(Self::GdiPlus), + "mediafoundation" | "media_foundation" => Ok(Self::MediaFoundation), + "mapiutilities" | "mapi_utilities" => Ok(Self::MapiUtilities), + _ => Err(napi::Error::from_reason(format!( + "Unknown flat Win32 subsystem `{value}`" + ))), + } + } + + const fn name(self) -> &'static str { + match self { + Self::Winsock => "winsock", + Self::GdiPlus => "gdiplus", + Self::MediaFoundation => "mediaFoundation", + Self::MapiUtilities => "mapiUtilities", + } + } +} + +#[derive(Default)] +struct CountedState { + leases: usize, +} + +#[derive(Default)] +struct GdiPlusState { + leases: usize, + token: usize, +} + +static WINSOCK_STATE: LazyLock> = + LazyLock::new(|| Mutex::new(CountedState::default())); +static GDIPLUS_STATE: LazyLock> = + LazyLock::new(|| Mutex::new(GdiPlusState::default())); +static MEDIA_FOUNDATION_STATE: LazyLock> = + LazyLock::new(|| Mutex::new(CountedState::default())); +static MAPI_UTILITIES_STATE: LazyLock> = + LazyLock::new(|| Mutex::new(CountedState::default())); + +#[napi] +pub struct DynWin32SubsystemContext { + kind: SubsystemKind, + closed: Mutex, +} + +pub(super) struct SubsystemCallGuard<'a> { + _closed: MutexGuard<'a, bool>, +} + +#[napi] +impl DynWin32SubsystemContext { + #[napi(getter)] + pub fn subsystem(&self) -> &'static str { + self.kind.name() + } + + #[napi(getter)] + pub fn closed(&self) -> bool { + *self + .closed + .lock() + .unwrap_or_else(|error| error.into_inner()) + } + + #[napi] + pub fn close(&self) -> napi::Result<()> { + let mut closed = self + .closed + .lock() + .unwrap_or_else(|error| error.into_inner()); + if *closed { + return Ok(()); + } + release(self.kind)?; + *closed = true; + Ok(()) + } +} + +impl Drop for DynWin32SubsystemContext { + fn drop(&mut self) { + let closed = self + .closed + .get_mut() + .unwrap_or_else(|error| error.into_inner()); + if !*closed { + if let Err(error) = release(self.kind) { + eprintln!( + "[dynwinrt] {} subsystem cleanup failed: {}", + self.kind.name(), + error.reason + ); + } + *closed = true; + } + } +} + +pub(super) fn initialize(subsystem: &str) -> napi::Result { + let kind = SubsystemKind::parse(subsystem)?; + acquire(kind)?; + Ok(DynWin32SubsystemContext { + kind, + closed: Mutex::new(false), + }) +} + +pub(super) fn require(context: &DynWin32SubsystemContext, subsystem: &str) -> napi::Result<()> { + drop(call_guard(context, subsystem)?); + Ok(()) +} + +pub(super) fn call_guard<'a>( + context: &'a DynWin32SubsystemContext, + subsystem: &str, +) -> napi::Result> { + let expected = SubsystemKind::parse(subsystem)?; + if context.kind != expected { + return Err(napi::Error::from_reason(format!( + "{} APIs require a {} context, received {}", + expected.name(), + expected.name(), + context.kind.name() + ))); + } + let closed = context + .closed + .lock() + .unwrap_or_else(|error| error.into_inner()); + if *closed { + return Err(napi::Error::from_reason(format!( + "{} subsystem context is closed", + expected.name() + ))); + } + if !is_active(expected) { + return Err(napi::Error::from_reason(format!( + "{} subsystem is not initialized", + expected.name() + ))); + } + Ok(SubsystemCallGuard { _closed: closed }) +} + +fn acquire(kind: SubsystemKind) -> napi::Result<()> { + match kind { + SubsystemKind::Winsock => acquire_winsock(), + SubsystemKind::GdiPlus => acquire_gdiplus(), + SubsystemKind::MediaFoundation => acquire_media_foundation(), + SubsystemKind::MapiUtilities => acquire_mapi_utilities(), + } +} + +fn release(kind: SubsystemKind) -> napi::Result<()> { + match kind { + SubsystemKind::Winsock => release_winsock(), + SubsystemKind::GdiPlus => release_gdiplus(), + SubsystemKind::MediaFoundation => release_media_foundation(), + SubsystemKind::MapiUtilities => release_mapi_utilities(), + } +} + +fn is_active(kind: SubsystemKind) -> bool { + match kind { + SubsystemKind::Winsock => { + WINSOCK_STATE + .lock() + .unwrap_or_else(|error| error.into_inner()) + .leases + != 0 + } + SubsystemKind::GdiPlus => { + GDIPLUS_STATE + .lock() + .unwrap_or_else(|error| error.into_inner()) + .leases + != 0 + } + SubsystemKind::MediaFoundation => { + MEDIA_FOUNDATION_STATE + .lock() + .unwrap_or_else(|error| error.into_inner()) + .leases + != 0 + } + SubsystemKind::MapiUtilities => { + MAPI_UTILITIES_STATE + .lock() + .unwrap_or_else(|error| error.into_inner()) + .leases + != 0 + } + } +} + +fn acquire_winsock() -> napi::Result<()> { + let mut state = WINSOCK_STATE + .lock() + .unwrap_or_else(|error| error.into_inner()); + if state.leases == 0 { + let mut data = MaybeUninit::::uninit(); + let status = unsafe { WSAStartup(WINSOCK_VERSION_2_2, data.as_mut_ptr()) }; + if status != 0 { + return Err(napi::Error::from_reason(format!( + "WSAStartup(2.2) failed with Winsock error {status}" + ))); + } + let data = unsafe { data.assume_init() }; + if data.wVersion != WINSOCK_VERSION_2_2 { + let _ = unsafe { WSACleanup() }; + return Err(napi::Error::from_reason(format!( + "Winsock 2.2 is unavailable; negotiated version 0x{:04x}", + data.wVersion + ))); + } + } + state.leases = state + .leases + .checked_add(1) + .ok_or_else(|| napi::Error::from_reason("Winsock context count overflow"))?; + Ok(()) +} + +fn release_winsock() -> napi::Result<()> { + let mut state = WINSOCK_STATE + .lock() + .unwrap_or_else(|error| error.into_inner()); + if state.leases == 0 { + return Err(napi::Error::from_reason( + "Winsock subsystem context is not active", + )); + } + if state.leases == 1 { + let status = unsafe { WSACleanup() }; + if status != 0 { + return Err(napi::Error::from_reason(format!( + "WSACleanup failed with Winsock error {}", + unsafe { WSAGetLastError().0 } + ))); + } + } + state.leases -= 1; + Ok(()) +} + +fn acquire_gdiplus() -> napi::Result<()> { + let mut state = GDIPLUS_STATE + .lock() + .unwrap_or_else(|error| error.into_inner()); + if state.leases == 0 { + let input = GdiplusStartupInput { + GdiplusVersion: 1, + DebugEventCallback: 0, + SuppressBackgroundThread: false.into(), + SuppressExternalCodecs: false.into(), + }; + let mut token = 0usize; + let status = unsafe { GdiplusStartup(&mut token, &input, std::ptr::null_mut()) }; + if status != GDIPLUS_OK { + return Err(napi::Error::from_reason(format!( + "GdiplusStartup failed with status {}", + status.0 + ))); + } + if token == 0 { + return Err(napi::Error::from_reason( + "GdiplusStartup returned an invalid token", + )); + } + state.token = token; + } + state.leases = state + .leases + .checked_add(1) + .ok_or_else(|| napi::Error::from_reason("GDI+ context count overflow"))?; + Ok(()) +} + +fn release_gdiplus() -> napi::Result<()> { + let mut state = GDIPLUS_STATE + .lock() + .unwrap_or_else(|error| error.into_inner()); + if state.leases == 0 { + return Err(napi::Error::from_reason( + "GDI+ subsystem context is not active", + )); + } + state.leases -= 1; + if state.leases == 0 { + let token = std::mem::take(&mut state.token); + unsafe { GdiplusShutdown(token) }; + } + Ok(()) +} + +fn acquire_media_foundation() -> napi::Result<()> { + let mut state = MEDIA_FOUNDATION_STATE + .lock() + .unwrap_or_else(|error| error.into_inner()); + if state.leases == 0 { + unsafe { MFStartup(MF_VERSION, MFSTARTUP_FULL) } + .map_err(|error| napi::Error::from_reason(format!("MFStartup failed: {error}")))?; + } + state.leases = state + .leases + .checked_add(1) + .ok_or_else(|| napi::Error::from_reason("Media Foundation context count overflow"))?; + Ok(()) +} + +fn release_media_foundation() -> napi::Result<()> { + let mut state = MEDIA_FOUNDATION_STATE + .lock() + .unwrap_or_else(|error| error.into_inner()); + if state.leases == 0 { + return Err(napi::Error::from_reason( + "Media Foundation subsystem context is not active", + )); + } + if state.leases == 1 { + unsafe { MFShutdown() } + .map_err(|error| napi::Error::from_reason(format!("MFShutdown failed: {error}")))?; + } + state.leases -= 1; + Ok(()) +} + +fn acquire_mapi_utilities() -> napi::Result<()> { + let mut state = MAPI_UTILITIES_STATE + .lock() + .unwrap_or_else(|error| error.into_inner()); + if state.leases == 0 { + let status = unsafe { ScInitMapiUtil(0) }; + if status != 0 { + return Err(napi::Error::from_reason(format!( + "ScInitMapiUtil(0) failed with SCODE 0x{:08x}", + status as u32 + ))); + } + } + state.leases = state + .leases + .checked_add(1) + .ok_or_else(|| napi::Error::from_reason("MAPI utility context count overflow"))?; + Ok(()) +} + +fn release_mapi_utilities() -> napi::Result<()> { + let mut state = MAPI_UTILITIES_STATE + .lock() + .unwrap_or_else(|error| error.into_inner()); + if state.leases == 0 { + return Err(napi::Error::from_reason( + "MAPI utility subsystem context is not active", + )); + } + state.leases -= 1; + if state.leases == 0 { + unsafe { DeinitMapiUtil() }; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn winsock_context_is_counted_and_rejects_use_after_close() { + let first = initialize("winsock").unwrap(); + let second = initialize("winsock").unwrap(); + require(&first, "winsock").unwrap(); + first.close().unwrap(); + assert!(require(&first, "winsock").is_err()); + require(&second, "winsock").unwrap(); + second.close().unwrap(); + } + + #[test] + fn context_kind_mismatch_is_rejected() { + let winsock = initialize("winsock").unwrap(); + let error = require(&winsock, "gdiplus").unwrap_err(); + assert!(error.reason.contains("received winsock")); + winsock.close().unwrap(); + } + + #[test] + fn call_guard_blocks_concurrent_close() { + let winsock = std::sync::Arc::new(initialize("winsock").unwrap()); + let guard = call_guard(&winsock, "winsock").unwrap(); + let closing = std::sync::Arc::clone(&winsock); + let (started_sender, started_receiver) = std::sync::mpsc::channel(); + let (finished_sender, finished_receiver) = std::sync::mpsc::channel(); + let thread = std::thread::spawn(move || { + started_sender.send(()).unwrap(); + let result = closing.close(); + finished_sender.send(result).unwrap(); + }); + + started_receiver.recv().unwrap(); + assert!(finished_receiver + .recv_timeout(std::time::Duration::from_millis(50)) + .is_err()); + drop(guard); + finished_receiver.recv().unwrap().unwrap(); + thread.join().unwrap(); + } + + #[test] + fn gdiplus_and_media_foundation_contexts_are_counted() { + for subsystem in ["gdiplus", "mediaFoundation"] { + let first = initialize(subsystem).unwrap(); + let second = initialize(subsystem).unwrap(); + first.close().unwrap(); + require(&second, subsystem).unwrap(); + second.close().unwrap(); + } + } +} diff --git a/docs/architecture/flat-win32-support.md b/docs/architecture/flat-win32-support.md index 02a8801b..4c5faaba 100644 --- a/docs/architecture/flat-win32-support.md +++ b/docs/architecture/flat-win32-support.md @@ -35,23 +35,41 @@ owned resources only when the function's success rule succeeds. Modules are loaded only from System32 with `LOAD_LIBRARY_SEARCH_SYSTEM32` and remain loaded for the process lifetime. Bare `.dll` and `.drv` names are -accepted; paths are rejected. `mapi32.dll` is excluded from the safe projection -until MAPI/MAPI utility initialization and shutdown are modeled explicitly. +accepted; paths are rejected. `mapi32.dll` remains excluded from the safe +projection until full MAPI/provider lifecycle requirements are classified per +function. + +Winsock, GDI+, and Media Foundation functions carry explicit subsystem +requirements in semantic and projected IR. Generated namespaces export +`initializeWinsock`, `initializeGdiPlus`, or `initializeMediaFoundation`, and +every dependent wrapper accepts the resulting `DynWin32SubsystemContext` as its +first argument. The runtime holds the context close lock across native dispatch +so another thread cannot shut down the subsystem between validation and the +call. Raw startup/shutdown exports are omitted from the safe surface. + +The runtime also exposes an explicit MAPI utility context backed by +`ScInitMapiUtil`/`DeinitMapiUtil` for manual/unsafe scenarios. Safe +`mapi32.dll` generation remains closed because full Extended MAPI initialization +is not present in Windows.Win32 metadata and the MAPI stub/provider lifecycle +requires a separately audited function classification. The runtime supports x64 and ARM64 with explicit `system` and `cdecl` plans. A 32-bit build compiles, but plan binding fails explicitly until generation also carries target-specific availability and all x86 convention variants. `ReadFile` and `WriteFile` use a separate OVERLAPPED Promise path rather than -the synchronous call plan. It owns an event and private native buffer, leases a -managed file handle for the operation, handles immediate and -`ERROR_IO_PENDING` completion, supports `AbortSignal`/`CancelIoEx`, and copies -read results back on the JavaScript thread. Completion waits run on a shared, -fixed eight-thread native waiter rather than the libuv worker pool; excess -operations are rejected explicitly instead of allocating unbounded OS threads. -Before copying a read result, the JS thread reacquires and revalidates the Node -Buffer backing store so a transferred/detached ArrayBuffer cannot leave a stale -destination pointer. EOF resolves with zero bytes. +the synchronous call plan. It associates each managed file handle once with a +process-wide IO completion port, pins one `OVERLAPPED` plus private native +buffer per operation, and leases the handle until the completion packet is +consumed. A small shared completion-worker set uses windows-rs IOCP primitives; +no thread blocks per operation and the libuv worker pool is not used. +`AbortSignal` calls `CancelIoEx`, but storage remains alive until IOCP reports +the cancelled completion. Before copying a read result, the JS thread reacquires +and revalidates the Node Buffer backing store so a transferred/detached +ArrayBuffer cannot leave a stale destination pointer. Pending work is bounded +to 1,024 operations, 64 MiB per operation, and 256 MiB of private native +buffers in total; reservations remain charged until the JS completion consumes +the task. EOF resolves with zero bytes. ## Metadata and codegen layers @@ -188,8 +206,8 @@ dynwinrt-codegen win32-census ` ``` For `Microsoft.Windows.SDK.Win32Metadata 71.0.14-preview`, the baseline is -8,943 complete safe functions out of 18,321 DllImport rows -(48.8128377271983%). Omission reasons are grouped into stable categories. +8,936 complete safe functions out of 18,321 DllImport rows +(48.77463020577479%). Omission reasons are grouped into stable categories. `windows-metadata` remains behind the flat-local adapter. Parameter rows are associated by ECMA-335 `Param.Sequence`, and calling convention remains a raw diff --git a/docs/guides/windows/flat-win32-usage.md b/docs/guides/windows/flat-win32-usage.md index 92d31c95..96b99641 100644 --- a/docs/guides/windows/flat-win32-usage.md +++ b/docs/guides/windows/flat-win32-usage.md @@ -64,8 +64,30 @@ explicit `@microsoft/dynwinrt/win32/unsafe` entrypoint. APIs that consume and close a handle require `DynWin32Resource`; passing `resource.value` or another numeric handle is rejected. Double-NUL string-list parameters accept `string[]` or explicitly encoded, double-terminated storage. -MAPI exports are omitted from the safe projection until their required -initialization lifecycle is modeled. +MAPI exports remain omitted from the safe projection until full +MAPI/provider requirements are classified per function. + +Subsystem-dependent namespaces expose an explicit context: + +```js +import { + initializeWinsock, + wsaSetLastError, +} from "@winapp/bindings/win32/Windows.Win32.Networking.WinSock"; + +const winsock = initializeWinsock(); +try { + wsaSetLastError(winsock, 0); +} finally { + winsock.close(); +} +``` + +GDI+ and Media Foundation use the same pattern through +`initializeGdiPlus()` and `initializeMediaFoundation()`. Generated wrappers +reject a context for the wrong subsystem and reject use after `close()`. +Close the context only after all sockets, GDI+ objects, Media Foundation +objects, callbacks, and work queues created under it have been released. Validated native structs receive generated factories and branded storage: @@ -130,10 +152,11 @@ const read = await readFileAsync(file, destination, 0n); ``` The runtime holds the file resource and private native storage until completion -or cancellation, waits on a fixed-capacity native waiter outside the libuv -worker pool, and revalidates read Buffers before copying data back. More than -eight concurrent operations are rejected explicitly. The file must have been -opened with `FILE_FLAG_OVERLAPPED`. +or cancellation, receives completions through a shared IOCP outside the libuv +worker pool, and revalidates read Buffers before copying data back. Cancellation +does not release storage until its completion packet arrives. The runtime +enforces process-wide pending-operation and native-buffer limits. The file must +have been opened with `FILE_FLAG_OVERLAPPED`. Runnable examples are available under [`samples/js/win32`](../../../samples/js/win32/README.md). They cover direct diff --git a/tests/e2e/e2e_test.ps1 b/tests/e2e/e2e_test.ps1 index c006f1d1..e85f8a79 100644 --- a/tests/e2e/e2e_test.ps1 +++ b/tests/e2e/e2e_test.ps1 @@ -326,7 +326,10 @@ if ("win32" -in $Lang) { "Windows.Win32.System.Threading", "Windows.Win32.System.Com", "Windows.Win32.Networking.Ldap", + "Windows.Win32.Networking.WinSock", "Windows.Win32.NetworkManagement.IpHelper", + "Windows.Win32.Graphics.GdiPlus", + "Windows.Win32.Media.MediaFoundation", "Windows.Win32.System.Pipes", "Windows.Win32.Storage.FileSystem" )) { @@ -426,7 +429,7 @@ if ("com" -in $Lang) { if ("win32" -in $Lang) { Write-Host "`n--- Flat Win32 E2E ---" -ForegroundColor Yellow - $win32Runners = @("registry.mjs", "returns.mjs") + $win32Runners = @("registry.mjs", "returns.mjs", "subsystems.mjs") $win32Passed = 0 $win32Failed = 0 foreach ($runner in $win32Runners) { diff --git a/tests/e2e/runners/win32/returns.mjs b/tests/e2e/runners/win32/returns.mjs index afbc040e..c1954c24 100644 --- a/tests/e2e/runners/win32/returns.mjs +++ b/tests/e2e/runners/win32/returns.mjs @@ -352,7 +352,7 @@ try { console.log("[win32-e2e] pending cancellation passed"); const poolController = new AbortController(); - const concurrentReads = Array.from({ length: 16 }, () => + const concurrentReads = Array.from({ length: 32 }, () => readFileAsync(pipeClient, Buffer.alloc(1), 0n, poolController.signal), ); const poolDeadline = Date.now() + 5000; @@ -364,26 +364,6 @@ try { true, "concurrent reads did not enter pending I/O", ); - const queueLimitError = await withTimeout( - Promise.any( - concurrentReads.map((read) => - read.then( - () => - Promise.reject(new Error("pending read completed unexpectedly")), - (error) => { - if (/waiter capacity is full/i.test(error.message)) { - return error; - } - throw error; - }, - ), - ), - ), - 10000, - "bounded OVERLAPPED waiter did not reject excess work", - () => poolController.abort(), - ); - assert.match(queueLimitError.message, /waiter capacity is full/i); await Promise.race([ promisify(pbkdf2)("dynwinrt", "win32", 1, 16, "sha256"), new Promise((_, reject) => @@ -404,13 +384,11 @@ try { assert( cancelledReads.every( (result) => - result.status === "rejected" && - (result.reason.name === "AbortError" || - /waiter capacity is full/i.test(result.reason.message)), + result.status === "rejected" && result.reason.name === "AbortError", ), ); assert.equal(pipeClient.busy, false); - console.log("[win32-e2e] bounded waiter and libuv availability passed"); + console.log("[win32-e2e] IOCP concurrency and libuv availability passed"); const detachable = new ArrayBuffer(1); const detachedBuffer = Buffer.from(detachable); diff --git a/tests/e2e/runners/win32/subsystems.mjs b/tests/e2e/runners/win32/subsystems.mjs new file mode 100644 index 00000000..d2977bb2 --- /dev/null +++ b/tests/e2e/runners/win32/subsystems.mjs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from "node:assert/strict"; +import { + initializeWinsock, + wsaGetLastError, + wsaSetLastError, +} from "../../e2e_generated/win32/win32/Windows.Win32.Networking.WinSock/Apis.js"; +import { + gdipGetImageDecodersSize, + initializeGdiPlus, +} from "../../e2e_generated/win32/win32/Windows.Win32.Graphics.GdiPlus/Apis.js"; +import { + initializeMediaFoundation, + mfGetTimerPeriodicity, +} from "../../e2e_generated/win32/win32/Windows.Win32.Media.MediaFoundation/Apis.js"; + +const winsock = initializeWinsock(); +assert.equal(winsock.subsystem, "winsock"); +wsaSetLastError(winsock, 12345); +assert.equal(wsaGetLastError().result, 12345); +winsock.close(); +assert.throws(() => wsaSetLastError(winsock, 0), /context is closed/); + +const gdiplus = initializeGdiPlus(); +assert.equal(gdiplus.subsystem, "gdiplus"); +const decoders = gdipGetImageDecodersSize(gdiplus); +assert.equal(decoders.result, 0); +assert(decoders.numDecoders > 0); +assert(decoders.size > 0); +gdiplus.close(); +assert.throws(() => gdipGetImageDecodersSize(gdiplus), /context is closed/); + +const mediaFoundation = initializeMediaFoundation(); +assert.equal(mediaFoundation.subsystem, "mediaFoundation"); +const timer = mfGetTimerPeriodicity(mediaFoundation); +assert.equal(timer.status, 0); +assert(timer.periodicity > 0); +mediaFoundation.close(); +assert.throws( + () => mfGetTimerPeriodicity(mediaFoundation), + /context is closed/, +); + +console.log("PASS"); diff --git a/tools/dynwinrt-codegen/src/codegen/win32/ir.rs b/tools/dynwinrt-codegen/src/codegen/win32/ir.rs index 95d82d95..d31fc9ee 100644 --- a/tools/dynwinrt-codegen/src/codegen/win32/ir.rs +++ b/tools/dynwinrt-codegen/src/codegen/win32/ir.rs @@ -64,6 +64,13 @@ pub enum CallingConvention { Cdecl, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] +pub enum Subsystem { + Winsock, + GdiPlus, + MediaFoundation, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StringEncoding { Wide, @@ -263,6 +270,7 @@ pub struct FunctionContract { pub success_rule: SuccessRule, pub capture_last_error: bool, pub calling_convention: CallingConvention, + pub subsystem: Option, pub enums: Vec, } @@ -410,6 +418,7 @@ pub struct ProjectedFunction { pub inputs: Vec, pub runtime: RuntimePlan, pub return_shape: ReturnShape, + pub subsystem: Option, } #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/tools/dynwinrt-codegen/src/codegen/win32/model.rs b/tools/dynwinrt-codegen/src/codegen/win32/model.rs index 449f5df1..e35057ce 100644 --- a/tools/dynwinrt-codegen/src/codegen/win32/model.rs +++ b/tools/dynwinrt-codegen/src/codegen/win32/model.rs @@ -13,7 +13,7 @@ use super::ir::{ AbiType, BufferContract, CallingConvention, Cleanup, Constness, Direction, EnumDefinition, EnumMember, EnumUnderlying, FunctionContract, NativeAggregateKind, NativeArchitectureLayout, NativeField, NativeFieldType, NativeLayout, NativeScalar, ParameterContract, Scalar, - StringEncoding, SuccessRule, ValueType, + StringEncoding, Subsystem, SuccessRule, ValueType, }; pub(super) fn validate_apis(raw: &RawApis) -> (Vec, Vec<(String, String)>) { @@ -100,6 +100,7 @@ pub(super) fn validate_function(raw: &RawFunction) -> Result Result Result, String> { + match raw.namespace.as_str() { + "Windows.Win32.Networking.WinSock" => match raw.name.as_str() { + "WSAStartup" | "WSACleanup" => { + Err("Winsock lifecycle is managed by the generated initialization adapter".into()) + } + "WSAGetLastError" => Ok(None), + _ => Ok(Some(Subsystem::Winsock)), + }, + "Windows.Win32.Graphics.GdiPlus" => match raw.name.as_str() { + "GdiplusStartup" + | "GdiplusShutdown" + | "GdiplusNotificationHook" + | "GdiplusNotificationUnhook" => { + Err("GDI+ lifecycle is managed by the generated initialization adapter".into()) + } + _ => Ok(Some(Subsystem::GdiPlus)), + }, + "Windows.Win32.Media.MediaFoundation" => match raw.name.as_str() { + "MFStartup" | "MFShutdown" => Err( + "Media Foundation lifecycle is managed by the generated initialization adapter" + .into(), + ), + _ => Ok(Some(Subsystem::MediaFoundation)), + }, + _ => Ok(None), + } +} + fn known_mutable_in_place_string(function: &RawFunction, parameter: &ParameterContract) -> bool { parameter.name == "lpCommandLine" && matches!(parameter.typ, ValueType::StringPointer(_)) diff --git a/tools/dynwinrt-codegen/src/codegen/win32/project.rs b/tools/dynwinrt-codegen/src/codegen/win32/project.rs index 99ebf3cd..465326a4 100644 --- a/tools/dynwinrt-codegen/src/codegen/win32/project.rs +++ b/tools/dynwinrt-codegen/src/codegen/win32/project.rs @@ -643,6 +643,7 @@ fn project_function(contract: &FunctionContract) -> Result>(); + if function.subsystem.is_some() { + surface_parameters.insert(0, "_subsystem"); + } output.push_str(&format!( "function {}({}) {{\n", function.js_name, - function - .parameters - .iter() - .map(|parameter| parameter.name.as_str()) - .collect::>() - .join(", ") + surface_parameters.join(", ") )); for parameter in &function.parameters { if let Some(minimum) = parameter.minimum_bytes { @@ -319,8 +336,17 @@ fn render_function_js(output: &mut String, function: &ProjectedFunction, apis: & " DynWin32.prepareNativeStructCall({parameter}, _nativeLayout_{layout})\n" )); } + let invocation = function.subsystem.map_or_else( + || format!("{bind_name}().invoke([{arguments}])"), + |subsystem| { + format!( + "{bind_name}().invokeWithSubsystem(_subsystem, {:?}, [{arguments}])", + subsystem_name(subsystem) + ) + }, + ); output.push_str(&format!( - " const _call = {bind_name}().invoke([{arguments}])\n const _return = _call.returnValue\n const _outputs = _call.outputs\n" + " const _call = {invocation}\n const _return = _call.returnValue\n const _outputs = _call.outputs\n" )); for (parameter, layout) in &output_aggregates { output.push_str(&format!( @@ -557,7 +583,7 @@ fn render_dts(apis: &ProjectedApis, runtime_import: &str) -> String { let mut output = String::new(); output.push_str("// Generated by dynwinrt-codegen - do not edit\n"); output.push_str(&format!( - "import type {{ DynWin32NativeStruct, DynWin32Resource, DynWinRtValue }} from {runtime_import:?}\n" + "import type {{ DynWin32NativeStruct, DynWin32Resource, DynWin32SubsystemContext, DynWinRtValue }} from {runtime_import:?}\n" )); for definition in &apis.enums { output.push_str(&format!( @@ -647,8 +673,15 @@ fn render_dts(apis: &ProjectedApis, runtime_import: &str) -> String { if !apis.functions.is_empty() { output.push('\n'); } + let subsystems = collect_subsystems(apis); + for subsystem in &subsystems { + let initializer = subsystem_initializer(*subsystem); + output.push_str(&format!( + "export declare function {initializer}(): DynWin32SubsystemContext\n" + )); + } for function in &apis.functions { - let parameters = function + let mut parameters = function .parameters .iter() .map(|parameter| { @@ -658,8 +691,11 @@ fn render_dts(apis: &ProjectedApis, runtime_import: &str) -> String { } format!("{}: {typ}", parameter.name) }) - .collect::>() - .join(", "); + .collect::>(); + if function.subsystem.is_some() { + parameters.insert(0, "_subsystem: DynWin32SubsystemContext".into()); + } + let parameters = parameters.join(", "); output.push_str(&format!( "export declare function {}({parameters}): {}\n", function.js_name, @@ -679,6 +715,10 @@ fn render_dts(apis: &ProjectedApis, runtime_import: &str) -> String { )); } output.push_str("\nexport declare const Apis: Readonly<{\n"); + for subsystem in &subsystems { + let initializer = subsystem_initializer(*subsystem); + output.push_str(&format!(" {initializer}: typeof {initializer}\n")); + } for function in &apis.functions { output.push_str(&format!( " {}: typeof {}\n", @@ -922,6 +962,29 @@ fn render_enum(definition: &EnumDefinition) -> (String, String) { (js, dts) } +fn collect_subsystems(apis: &ProjectedApis) -> BTreeSet { + apis.functions + .iter() + .filter_map(|function| function.subsystem) + .collect() +} + +fn subsystem_name(subsystem: Subsystem) -> &'static str { + match subsystem { + Subsystem::Winsock => "winsock", + Subsystem::GdiPlus => "gdiplus", + Subsystem::MediaFoundation => "mediaFoundation", + } +} + +fn subsystem_initializer(subsystem: Subsystem) -> &'static str { + match subsystem { + Subsystem::Winsock => "initializeWinsock", + Subsystem::GdiPlus => "initializeGdiPlus", + Subsystem::MediaFoundation => "initializeMediaFoundation", + } +} + fn abi_name(typ: AbiType) -> &'static str { match typ { AbiType::Bool32 => "bool32", diff --git a/tools/dynwinrt-codegen/tests/win32_flat_test.rs b/tools/dynwinrt-codegen/tests/win32_flat_test.rs index c3997a3a..7b76da9c 100644 --- a/tools/dynwinrt-codegen/tests/win32_flat_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_flat_test.rs @@ -109,6 +109,104 @@ fn mapi_exports_fail_closed_without_an_initialization_contract() { ); } +#[test] +fn subsystem_initializers_gate_generated_functions() { + let Some(winmd) = configured_winmd() else { + return; + }; + + let cases = [ + ( + "Windows.Win32.Networking.WinSock", + &[ + "WSAGetLastError", + "WSASetLastError", + "WSAStartup", + "WSACleanup", + ][..], + "initializeWinsock", + "winsock", + "wsaSetLastError(_subsystem: DynWin32SubsystemContext", + "wsaGetLastError()", + 2, + ), + ( + "Windows.Win32.Graphics.GdiPlus", + &[ + "GdipGetImageDecodersSize", + "GdiplusStartup", + "GdiplusShutdown", + "GdiplusNotificationHook", + "GdiplusNotificationUnhook", + ][..], + "initializeGdiPlus", + "gdiplus", + "gdipGetImageDecodersSize(_subsystem: DynWin32SubsystemContext", + "", + 4, + ), + ( + "Windows.Win32.Media.MediaFoundation", + &["MFGetTimerPeriodicity", "MFStartup", "MFShutdown"][..], + "initializeMediaFoundation", + "mediaFoundation", + "mfGetTimerPeriodicity(_subsystem: DynWin32SubsystemContext", + "", + 2, + ), + ]; + + for ( + namespace, + names, + initializer, + subsystem, + signature, + exempt_signature, + expected_omissions, + ) in cases + { + let raw = dynwinrt_codegen::win32_metadata::parse_apis(&winmd, namespace, "Apis").unwrap(); + let selected = dynwinrt_codegen::win32_metadata::RawApis { + namespace: raw.namespace, + class_name: raw.class_name, + functions: raw + .functions + .into_iter() + .filter(|function| names.contains(&function.name.as_str())) + .collect(), + }; + let (output, omissions) = dynwinrt_codegen::codegen::win32::generate_apis_files( + &selected, + "@microsoft/dynwinrt/win32", + ); + assert!(output.dts.contains(&format!( + "function {initializer}(): DynWin32SubsystemContext" + ))); + assert!(output.dts.contains(signature), "{}", output.dts); + assert!( + output + .js + .contains(&format!("invokeWithSubsystem(_subsystem, \"{subsystem}\"")), + "{}", + output.js + ); + if !exempt_signature.is_empty() { + assert!(output.dts.contains(exempt_signature)); + } + assert_eq!( + omissions.len(), + expected_omissions, + "{namespace}: {omissions:#?}" + ); + assert!(omissions.iter().all(|omission| { + omission + .reason + .contains("lifecycle is managed by the generated initialization adapter") + })); + } +} + #[test] fn generated_safe_surface_uses_unsafe_binding_only_internally() { let Some(winmd) = configured_winmd() else {