From 90f5151ef853fb6ce15be02d8d8801a4fce1b96a Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Tue, 11 Aug 2026 12:38:55 +0800 Subject: [PATCH 1/4] Add Electron Windows Hello sample Add a COM-to-WinRT async projection bridge for desktop interop APIs, cover progress-bearing operations, and demonstrate HWND-owned Windows Hello verification from Electron. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eca73d14-f74e-462f-87c7-b48f6031f8b8 --- bindings/js/__test__/index.spec.ts | 1 + bindings/js/src/com.rs | 19 +- bindings/js/src/lib.rs | 6 + crates/dynwinrt/src/com.rs | 93 +++++++++ samples/js/windows-hello/.gitignore | 2 + samples/js/windows-hello/README.md | 47 +++++ samples/js/windows-hello/check.js | 23 +++ samples/js/windows-hello/generate.ps1 | 60 ++++++ samples/js/windows-hello/index.html | 23 +++ samples/js/windows-hello/main.js | 102 ++++++++++ samples/js/windows-hello/package-lock.json | 210 +++++++++++++++++++++ samples/js/windows-hello/package.json | 18 ++ samples/js/windows-hello/preload.js | 9 + samples/js/windows-hello/renderer.js | 31 +++ samples/js/windows-hello/style.css | 69 +++++++ 15 files changed, 712 insertions(+), 1 deletion(-) create mode 100644 samples/js/windows-hello/.gitignore create mode 100644 samples/js/windows-hello/README.md create mode 100644 samples/js/windows-hello/check.js create mode 100644 samples/js/windows-hello/generate.ps1 create mode 100644 samples/js/windows-hello/index.html create mode 100644 samples/js/windows-hello/main.js create mode 100644 samples/js/windows-hello/package-lock.json create mode 100644 samples/js/windows-hello/package.json create mode 100644 samples/js/windows-hello/preload.js create mode 100644 samples/js/windows-hello/renderer.js create mode 100644 samples/js/windows-hello/style.css diff --git a/bindings/js/__test__/index.spec.ts b/bindings/js/__test__/index.spec.ts index e95cede1..da28655d 100644 --- a/bindings/js/__test__/index.spec.ts +++ b/bindings/js/__test__/index.spec.ts @@ -171,6 +171,7 @@ test('WinRT root facade exposes usable native primitives', (t) => { test('Classic COM raw ABI access requires the explicit unsafe entrypoint', (t) => { const iid = WinGuid.parse('00000000-0000-0000-c000-000000000046') + t.is(typeof DynCom.projectWinRtAsync, 'function') const raw = DynComUnsafe.registerIUnknownInterface('Unsafe.IUnknown', iid).addMethodAt( 3, 'Raw', diff --git a/bindings/js/src/com.rs b/bindings/js/src/com.rs index 3c2a4244..55116d13 100644 --- a/bindings/js/src/com.rs +++ b/bindings/js/src/com.rs @@ -6,7 +6,7 @@ use napi::JsValue; use napi_derive::napi; use windows::core::{IUnknown, Interface as _, GUID}; -use super::{DynWinRTValue, WinGUID, TABLE}; +use super::{DynWinRTType, DynWinRTValue, WinGUID, TABLE}; #[allow(dead_code)] pub(super) enum NativePointerOwner { @@ -881,6 +881,15 @@ fn adopt_com_pointer( } } +fn project_winrt_async( + value: &DynWinRTValue, + async_type: &DynWinRTType, +) -> napi::Result { + dynwinrt::com::project_winrt_async(&value.0, async_type.type_handle()) + .map(DynWinRTValue::new) + .map_err(|error| napi::Error::from_reason(error.message())) +} + fn explicit_raw_com_pointer( value: Unknown, operation: &str, @@ -4525,6 +4534,14 @@ impl DynCom { self::adopt_com_pointer(value, iid) } + #[napi] + pub fn project_win_rt_async( + value: &DynWinRTValue, + async_type: &DynWinRTType, + ) -> napi::Result { + self::project_winrt_async(value, async_type) + } + #[napi] pub fn adopt_co_task_mem_pointer(value: &mut DynWinRTValue) -> napi::Result { self::adopt_co_task_mem_pointer(value) diff --git a/bindings/js/src/lib.rs b/bindings/js/src/lib.rs index d1b22a8f..19b82ddb 100644 --- a/bindings/js/src/lib.rs +++ b/bindings/js/src/lib.rs @@ -135,6 +135,12 @@ pub(crate) fn set_winui_dispatcher_loop_active(active: bool) { #[napi] pub struct DynWinRTType(dynwinrt::TypeHandle); +impl DynWinRTType { + pub(crate) fn type_handle(&self) -> dynwinrt::TypeHandle { + self.0.clone() + } +} + #[napi] impl DynWinRTType { #[napi] diff --git a/crates/dynwinrt/src/com.rs b/crates/dynwinrt/src/com.rs index e79d55c4..95ae45b9 100644 --- a/crates/dynwinrt/src/com.rs +++ b/crates/dynwinrt/src/com.rs @@ -4377,6 +4377,30 @@ pub unsafe fn adopt_com_pointer(ptr: *mut c_void) -> WinRTValue { } } +/// Project a managed COM object as a typed WinRT async operation. +/// +/// The input remains owned by the caller. The returned `Async` value holds a +/// separate `IAsyncInfo` reference and can be awaited independently. +pub fn project_winrt_async( + value: &WinRTValue, + async_type: TypeHandle, +) -> result::Result { + if !async_type.is_async() { + return Err(invalid_argument( + "project_winrt_async requires a WinRT async type", + )); + } + let object = value + .as_object() + .ok_or_else(|| invalid_argument("project_winrt_async requires a COM object"))?; + let info: windows_future::IAsyncInfo = + object.cast().map_err(result::Error::WindowsError)?; + Ok(WinRTValue::Async(crate::value::AsyncInfo { + info, + async_type, + })) +} + #[cfg(test)] fn call_method( vtable_index: usize, @@ -4439,8 +4463,10 @@ mod tests { use std::sync::atomic::{AtomicU32, Ordering}; use windows::{ ApplicationModel::DataTransfer::DataTransferManager, + System::Threading::{ThreadPool, WorkItemHandler}, Win32::{ System::Com::{CoGetMalloc, IMalloc, IPersistFile, IStream}, + System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize}, UI::Shell::{IDataTransferManagerInterop, SHCreateMemStream}, UI::WindowsAndMessaging::{ CreateWindowExW, DestroyWindow, WINDOW_EX_STYLE, WS_OVERLAPPED, @@ -9168,6 +9194,73 @@ mod tests { Ok(()) } + #[test] + fn project_winrt_async_borrows_managed_object() -> result::Result<()> { + let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; + let handler = WorkItemHandler::new(|_| Ok(())); + let operation = ThreadPool::RunAsync(&handler).map_err(result::Error::WindowsError)?; + let object: IUnknown = operation.cast().map_err(result::Error::WindowsError)?; + let source = WinRTValue::Object(object); + + let projected = + project_winrt_async(&source, MetadataTable::new().async_action())?; + assert!(matches!(projected, WinRTValue::Async(_))); + assert!(matches!(source, WinRTValue::Object(_))); + + Ok(()) + } + + #[tokio::test] + async fn project_winrt_async_preserves_progress_contract() -> result::Result<()> { + use std::sync::{Arc, atomic::Ordering}; + use windows::Storage::Streams::{Buffer, IOutputStream, InMemoryRandomAccessStream}; + + let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; + let stream = InMemoryRandomAccessStream::new().map_err(result::Error::WindowsError)?; + let output: IOutputStream = stream.cast().map_err(result::Error::WindowsError)?; + let buffer = Buffer::Create(1234).map_err(result::Error::WindowsError)?; + buffer + .SetLength(1234) + .map_err(result::Error::WindowsError)?; + let operation = output + .WriteAsync(&buffer) + .map_err(result::Error::WindowsError)?; + let object: IUnknown = operation.cast().map_err(result::Error::WindowsError)?; + let source = WinRTValue::Object(object); + + let table = MetadataTable::new(); + let result_type = table.make(TypeKind::U32); + let progress_type = table.make(TypeKind::U32); + let async_type = + table.async_operation_with_progress(&result_type, &progress_type); + let projected = project_winrt_async(&source, async_type)?; + + let async_info = match &projected { + WinRTValue::Async(info) => info, + other => panic!("expected Async, got {other:?}"), + }; + assert_eq!(async_info.result_type().unwrap().kind(), TypeKind::U32); + assert_eq!(async_info.progress_type().unwrap().kind(), TypeKind::U32); + + let progress_count = Arc::new(AtomicU32::new(0)); + let callback_count = progress_count.clone(); + let handler = crate::create_progress_handler( + async_info.progress_handler_iid().unwrap(), + async_info.progress_type().unwrap(), + Box::new(move |value| { + assert!(matches!(value, WinRTValue::U32(_))); + callback_count.fetch_add(1, Ordering::SeqCst); + }), + ); + async_info.set_progress_handler(&handler)?; + + let result = projected.await?; + assert!(matches!(result, WinRTValue::U32(1234))); + assert!(matches!(source, WinRTValue::Object(_))); + + Ok(()) + } + #[test] fn co_create_instance_with_bogus_clsid_returns_error() -> result::Result<()> { initialize_apartment(ApartmentType::MultiThreaded)?; diff --git a/samples/js/windows-hello/.gitignore b/samples/js/windows-hello/.gitignore new file mode 100644 index 00000000..3274e688 --- /dev/null +++ b/samples/js/windows-hello/.gitignore @@ -0,0 +1,2 @@ +generated/ +node_modules/ diff --git a/samples/js/windows-hello/README.md b/samples/js/windows-hello/README.md new file mode 100644 index 00000000..b5864ca6 --- /dev/null +++ b/samples/js/windows-hello/README.md @@ -0,0 +1,47 @@ +# Windows Hello + +This Electron sample uses: + +- the WinRT `Windows.Security.Credentials.UI.UserConsentVerifier` runtime class + to check availability; and +- Classic COM `IUserConsentVerifierInterop` to associate the verification + dialog with the Electron HWND. + +It demonstrates the same WinRT API shape commonly wrapped by a custom Electron +native addon, without requiring sample-specific C++ or `node-gyp`. + +## Prerequisites + +- Windows 10 or 11; +- Windows Hello configured for the current user; +- Node.js and Rust/Cargo; and +- a Windows SDK containing `Windows.winmd`. + +## Run + +Build the local JavaScript runtime once from the repository root: + +```powershell +cd bindings\js +npm install +npm run build +``` + +Generate the WinRT and COM projections and run the sample: + +```powershell +cd samples\js\windows-hello +npm install +npm run generate +npm start +``` + +Click **Verify identity**. Windows displays its native verification dialog +owned by the Electron window. Complete or cancel the prompt; the result is +shown in the application. + +To check availability without displaying the verification dialog: + +```powershell +npm run check +``` diff --git a/samples/js/windows-hello/check.js b/samples/js/windows-hello/check.js new file mode 100644 index 00000000..eca5ff03 --- /dev/null +++ b/samples/js/windows-hello/check.js @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +const { roInitialize } = require('@microsoft/dynwinrt') +const { UserConsentVerifier } = require('./generated/UserConsentVerifier.js') +const { + UserConsentVerifierAvailability, +} = require('./generated/UserConsentVerifierAvailability.js') + +function enumName(values, value) { + return Object.entries(values).find(([, candidate]) => candidate === value)?.[0] ?? `Unknown (${value})` +} + +async function main() { + roInitialize(1) + const availability = await UserConsentVerifier.checkAvailabilityAsync() + console.log(`Windows Hello availability: ${enumName(UserConsentVerifierAvailability, availability)}`) +} + +main().catch((error) => { + console.error(error) + process.exitCode = 1 +}) diff --git a/samples/js/windows-hello/generate.ps1 b/samples/js/windows-hello/generate.ps1 new file mode 100644 index 00000000..53f52848 --- /dev/null +++ b/samples/js/windows-hello/generate.ps1 @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +$ErrorActionPreference = "Stop" + +$repoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..\..\..")).Path +$output = Join-Path $PSScriptRoot "generated" + +$windowsWinmd = Get-ChildItem ` + "C:\Program Files (x86)\Windows Kits\10\UnionMetadata" ` + -Filter Windows.winmd ` + -Recurse ` + -ErrorAction SilentlyContinue | + Sort-Object FullName -Descending | + Select-Object -First 1 -ExpandProperty FullName + +if (-not $windowsWinmd) { + throw "Windows.winmd was not found. Install a Windows 10/11 SDK." +} + +$win32Winmd = $env:DYNWINRT_WIN32_WINMD +if (-not $win32Winmd -or -not (Test-Path $win32Winmd)) { + $win32Winmd = Get-ChildItem ` + (Join-Path $env:USERPROFILE ".nuget\packages\microsoft.windows.sdk.win32metadata") ` + -Filter Windows.Win32.winmd ` + -Recurse ` + -ErrorAction SilentlyContinue | + Sort-Object FullName -Descending | + Select-Object -First 1 -ExpandProperty FullName +} + +if (-not $win32Winmd) { + throw "Windows.Win32.winmd was not found. Set DYNWINRT_WIN32_WINMD or install Microsoft.Windows.SDK.Win32Metadata." +} + +if (Test-Path $output) { + Remove-Item $output -Recurse -Force +} + +& cargo run --quiet --manifest-path (Join-Path $repoRoot "Cargo.toml") ` + -p dynwinrt-codegen -- generate ` + --winmd $windowsWinmd ` + --namespace Windows.Security.Credentials.UI ` + --class-name UserConsentVerifier ` + --output $output +if ($LASTEXITCODE -ne 0) { + throw "WinRT generation failed with exit code $LASTEXITCODE." +} + +& cargo run --quiet --manifest-path (Join-Path $repoRoot "Cargo.toml") ` + -p dynwinrt-codegen -- generate ` + --winmd $win32Winmd ` + --ref $windowsWinmd ` + --class-name Windows.Win32.System.WinRT.IUserConsentVerifierInterop ` + --output $output +if ($LASTEXITCODE -ne 0) { + throw "Classic COM generation failed with exit code $LASTEXITCODE." +} + +Write-Host "Generated Windows Hello bindings in $output" diff --git a/samples/js/windows-hello/index.html b/samples/js/windows-hello/index.html new file mode 100644 index 00000000..794f5001 --- /dev/null +++ b/samples/js/windows-hello/index.html @@ -0,0 +1,23 @@ + + + + + + + dynwinrt Windows Hello + + + +
+

Windows Hello

+

Verify the current user with face, fingerprint, or PIN.

+
Checking availability…
+ +
+
+ + + diff --git a/samples/js/windows-hello/main.js b/samples/js/windows-hello/main.js new file mode 100644 index 00000000..4ce53f53 --- /dev/null +++ b/samples/js/windows-hello/main.js @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +const path = require('node:path') +const { app, BrowserWindow, ipcMain } = require('electron') +const { + DynWinRtType, + DynWinRtValue, +} = require('@microsoft/dynwinrt') +const { DynCom } = require('@microsoft/dynwinrt/com/unsafe') +const { UserConsentVerifier } = require('./generated/UserConsentVerifier.js') +const { + UserConsentVerifierAvailability, +} = require('./generated/UserConsentVerifierAvailability.js') +const { + UserConsentVerificationResult, +} = require('./generated/UserConsentVerificationResult.js') +const { + IUserConsentVerifierInterop, +} = require('./generated/com/IUserConsentVerifierInterop.js') + +const resultType = DynWinRtType.enumType( + 'Windows.Security.Credentials.UI.UserConsentVerificationResult', + Object.keys(UserConsentVerificationResult), + Object.values(UserConsentVerificationResult), +) +const asyncResultType = DynWinRtType.iAsyncOperation(resultType) +const asyncResultIid = asyncResultType.iid().toString() + +function enumName(values, value) { + return Object.entries(values).find(([, candidate]) => candidate === value)?.[0] ?? `Unknown (${value})` +} + +function createInterop() { + const factory = DynWinRtValue.activationFactory( + 'Windows.Security.Credentials.UI.UserConsentVerifier', + ) + try { + return IUserConsentVerifierInterop._fromNative(factory) + } finally { + factory.release() + } +} + +ipcMain.handle('windows-hello:availability', async () => { + const availability = await UserConsentVerifier.checkAvailabilityAsync() + return { + value: availability, + name: enumName(UserConsentVerifierAvailability, availability), + } +}) + +ipcMain.handle('windows-hello:verify', async (event) => { + const window = BrowserWindow.fromWebContents(event.sender) + if (!window) { + throw new Error('The Electron window is unavailable.') + } + + window.show() + window.focus() + + const interop = createInterop() + let rawOperation + let asyncOperation + try { + rawOperation = interop.requestVerificationForWindowAsync( + window.getNativeWindowHandle(), + 'Verify dynwinrt Windows Hello support', + asyncResultIid, + ) + asyncOperation = DynCom.projectWinRtAsync(rawOperation, asyncResultType) + const resultValue = await asyncOperation.toPromise() + return { + value: resultValue.toNumber(), + name: enumName(UserConsentVerificationResult, resultValue.toNumber()), + } + } finally { + asyncOperation?.release() + rawOperation?.release() + interop.release() + } +}) + +function createWindow() { + const window = new BrowserWindow({ + width: 520, + height: 350, + resizable: false, + webPreferences: { + preload: path.join(__dirname, 'preload.js'), + contextIsolation: true, + nodeIntegration: false, + }, + }) + window.loadFile('index.html') +} + +app.whenReady().then(createWindow) + +app.on('window-all-closed', () => { + app.quit() +}) diff --git a/samples/js/windows-hello/package-lock.json b/samples/js/windows-hello/package-lock.json new file mode 100644 index 00000000..13fb85e9 --- /dev/null +++ b/samples/js/windows-hello/package-lock.json @@ -0,0 +1,210 @@ +{ + "name": "dynwinrt-windows-hello-sample", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dynwinrt-windows-hello-sample", + "version": "1.0.0", + "dependencies": { + "@microsoft/dynwinrt": "file:../../../bindings/js" + }, + "devDependencies": { + "electron": "^40.10.6" + } + }, + "../../../bindings/js": { + "name": "@microsoft/dynwinrt", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@emnapi/core": "^1.5.0", + "@emnapi/runtime": "^1.5.0", + "@napi-rs/cli": "^3.2.0", + "@napi-rs/wasm-runtime": "^1.0.4", + "@oxc-node/core": "^0.0.35", + "@taplo/cli": "^0.7.0", + "@tybys/wasm-util": "^0.10.0", + "ava": "^6.4.1", + "c8": "12.0.0", + "chalk": "^5.6.2", + "emnapi": "^1.5.0", + "husky": "^9.1.7", + "lint-staged": "^16.1.6", + "npm-run-all2": "^8.0.4", + "oxlint": "^1.14.0", + "prettier": "^3.6.2", + "tinybench": "^6.0.0", + "tsx": "^4.21.0", + "typescript": "^5.9.2" + }, + "engines": { + "node": ">= 12.22.0 < 13 || >= 14.17.0 < 15 || >= 15.12.0 < 16 || >= 16.0.0" + } + }, + "node_modules/@electron-internal/extract-zip": { + "version": "1.0.5", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@electron-internal/extract-zip/-/extract-zip-1.0.5.tgz", + "integrity": "sha1-Z4LA9gZuYLf9KG/npcdgD3ZQ1CA=", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@electron/get": { + "version": "5.1.0", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@electron/get/-/get-5.1.0.tgz", + "integrity": "sha1-+WyioOibJ0kP+Pe1o5K9TfaUKZg=", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "env-paths": "^3.0.0", + "graceful-fs": "^4.2.11", + "progress": "^2.0.3", + "semver": "^7.6.3", + "sumchecker": "^3.0.1" + }, + "engines": { + "node": ">=22.12.0" + }, + "optionalDependencies": { + "undici": "^7.24.4" + } + }, + "node_modules/@microsoft/dynwinrt": { + "resolved": "../../../bindings/js", + "link": true + }, + "node_modules/@types/node": { + "version": "24.13.3", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@types/node/-/node-24.13.3.tgz", + "integrity": "sha1-SfGL08ZHhm3NpRoHVsFF4UWQzhY=", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/debug/-/debug-4.4.3.tgz", + "integrity": "sha1-xq5DLZvZZiWC/OCHCbA4xY6ePWo=", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron": { + "version": "40.10.6", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/electron/-/electron-40.10.6.tgz", + "integrity": "sha1-TW/t3tmyp4nztC7OmyUZYRkth/k=", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@electron-internal/extract-zip": "^1.0.1", + "@electron/get": "^5.0.0", + "@types/node": "^24.9.0" + }, + "bin": { + "electron": "cli.js" + }, + "engines": { + "node": ">= 22.12.0" + } + }, + "node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha1-Lx6Jwvbb00COGxcR3YLWLjF/WNo=", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha1-QYPk6L8Iu24Fu7L30uDI9xLKQOM=", + "dev": true, + "license": "ISC" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/ms/-/ms-2.1.3.tgz", + "integrity": "sha1-V0yBOM4dK1hh8LRFedut1gxmFbI=", + "dev": true, + "license": "MIT" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/progress/-/progress-2.0.3.tgz", + "integrity": "sha1-foz42PW48jnBvGi+tOt4Vn1XLvg=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://ms-feed-25.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/semver/-/semver-7.8.5.tgz", + "integrity": "sha1-ObZGA33VDBT7RR5+TKxY7YuGP2k=", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sumchecker": { + "version": "3.0.1", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/sumchecker/-/sumchecker-3.0.1.tgz", + "integrity": "sha1-Y3fplnlauwttNI6bPh37JDRajkI=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/undici": { + "version": "7.29.0", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/undici/-/undici-7.29.0.tgz", + "integrity": "sha1-rg9vYuBuBXqcu3srX94rt095G48=", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha1-KTV6iee3ykrvO/D9P9DNc4hCKek=", + "dev": true, + "license": "MIT" + } + } +} diff --git a/samples/js/windows-hello/package.json b/samples/js/windows-hello/package.json new file mode 100644 index 00000000..7110fc73 --- /dev/null +++ b/samples/js/windows-hello/package.json @@ -0,0 +1,18 @@ +{ + "name": "dynwinrt-windows-hello-sample", + "version": "1.0.0", + "private": true, + "description": "Windows Hello user verification using dynwinrt", + "main": "main.js", + "scripts": { + "generate": "powershell -NoProfile -ExecutionPolicy Bypass -File ./generate.ps1", + "check": "node check.js", + "start": "electron ." + }, + "dependencies": { + "@microsoft/dynwinrt": "file:../../../bindings/js" + }, + "devDependencies": { + "electron": "^40.10.6" + } +} diff --git a/samples/js/windows-hello/preload.js b/samples/js/windows-hello/preload.js new file mode 100644 index 00000000..fa9d344c --- /dev/null +++ b/samples/js/windows-hello/preload.js @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +const { contextBridge, ipcRenderer } = require('electron') + +contextBridge.exposeInMainWorld('windowsHello', { + checkAvailability: () => ipcRenderer.invoke('windows-hello:availability'), + verify: () => ipcRenderer.invoke('windows-hello:verify'), +}) diff --git a/samples/js/windows-hello/renderer.js b/samples/js/windows-hello/renderer.js new file mode 100644 index 00000000..f1a5505b --- /dev/null +++ b/samples/js/windows-hello/renderer.js @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +const availability = document.querySelector('#availability') +const button = document.querySelector('#verify') +const result = document.querySelector('#result') + +async function initialize() { + try { + const state = await window.windowsHello.checkAvailability() + availability.textContent = `Availability: ${state.name}` + button.disabled = state.name !== 'Available' + } catch (error) { + availability.textContent = error instanceof Error ? error.message : String(error) + } +} + +button.addEventListener('click', async () => { + button.disabled = true + result.textContent = 'Waiting for Windows Hello…' + try { + const verification = await window.windowsHello.verify() + result.textContent = `Verification result: ${verification.name}` + } catch (error) { + result.textContent = error instanceof Error ? error.message : String(error) + } finally { + button.disabled = false + } +}) +void initialize() +void initialize() diff --git a/samples/js/windows-hello/style.css b/samples/js/windows-hello/style.css new file mode 100644 index 00000000..eb74b955 --- /dev/null +++ b/samples/js/windows-hello/style.css @@ -0,0 +1,69 @@ +:root { + color-scheme: light dark; + font-family: "Segoe UI Variable Text", "Segoe UI", system-ui, sans-serif; + color: #1a1a1a; + background: #f3f3f3; +} + +* { + box-sizing: border-box; +} + +body { + display: grid; + min-height: 100vh; + margin: 0; + place-items: center; +} + +main { + display: grid; + gap: 14px; + width: min(100% - 48px, 420px); +} + +h1, +p { + margin: 0; +} + +h1 { + font-size: 24px; +} + +p, +#availability, +#result { + color: #666; +} + +button { + height: 36px; + border: 1px solid #005a9e; + border-radius: 4px; + color: white; + background: #0067c0; + font: inherit; + font-weight: 600; +} + +button:disabled { + opacity: 0.55; +} + +#result { + min-height: 21px; +} + +@media (prefers-color-scheme: dark) { + :root { + color: white; + background: #202020; + } + + p, + #availability, + #result { + color: #c5c5c5; + } +} From f836ac230a48df30108f35beaa419dc6024fc1a5 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Tue, 11 Aug 2026 13:32:54 +0800 Subject: [PATCH 2/4] Normalize parameterized WinRT async projections Convert parameterized WinRT async PIIDs into dedicated async type handles before constructing AsyncInfo, reject malformed shapes, and cover operation and progress variants. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eca73d14-f74e-462f-87c7-b48f6031f8b8 --- crates/dynwinrt/src/com.rs | 65 +++++++++++++--- .../src/metadata_table/type_handle.rs | 77 +++++++++++-------- 2 files changed, 101 insertions(+), 41 deletions(-) diff --git a/crates/dynwinrt/src/com.rs b/crates/dynwinrt/src/com.rs index 95ae45b9..26b89380 100644 --- a/crates/dynwinrt/src/com.rs +++ b/crates/dynwinrt/src/com.rs @@ -4385,16 +4385,11 @@ pub fn project_winrt_async( value: &WinRTValue, async_type: TypeHandle, ) -> result::Result { - if !async_type.is_async() { - return Err(invalid_argument( - "project_winrt_async requires a WinRT async type", - )); - } + let async_type = async_type.normalized_async_type()?; let object = value .as_object() .ok_or_else(|| invalid_argument("project_winrt_async requires a COM object"))?; - let info: windows_future::IAsyncInfo = - object.cast().map_err(result::Error::WindowsError)?; + let info: windows_future::IAsyncInfo = object.cast().map_err(result::Error::WindowsError)?; Ok(WinRTValue::Async(crate::value::AsyncInfo { info, async_type, @@ -9202,14 +9197,58 @@ mod tests { let object: IUnknown = operation.cast().map_err(result::Error::WindowsError)?; let source = WinRTValue::Object(object); - let projected = - project_winrt_async(&source, MetadataTable::new().async_action())?; + let projected = project_winrt_async(&source, MetadataTable::new().async_action())?; assert!(matches!(projected, WinRTValue::Async(_))); assert!(matches!(source, WinRTValue::Object(_))); Ok(()) } + #[test] + fn project_winrt_async_rejects_malformed_parameterized_type() { + let table = MetadataTable::new(); + let generic = table.generic(crate::metadata_table::IASYNC_OPERATION, 1); + let malformed = table.parameterized(&generic, &[]); + + assert!(project_winrt_async(&WinRTValue::Null, malformed).is_err()); + } + + #[tokio::test] + async fn project_winrt_async_normalizes_parameterized_operation() -> result::Result<()> { + use windows::Storage::{IStorageFile, StorageFile}; + + let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; + let path = std::env::current_exe().map_err(|error| invalid_argument(error.to_string()))?; + let operation = + StorageFile::GetFileFromPathAsync(&HSTRING::from(path.to_string_lossy().as_ref())) + .map_err(result::Error::WindowsError)?; + let object: IUnknown = operation.cast().map_err(result::Error::WindowsError)?; + let source = WinRTValue::Object(object); + + let table = MetadataTable::new(); + let storage_file_interface = table.interface(IStorageFile::IID); + let storage_file_type = table.runtime_class( + "Windows.Storage.StorageFile".to_string(), + &storage_file_interface, + ); + let generic = table.generic(crate::metadata_table::IASYNC_OPERATION, 1); + let parameterized = table.parameterized(&generic, &[storage_file_type]); + let projected = project_winrt_async(&source, parameterized)?; + + let async_info = match &projected { + WinRTValue::Async(info) => info, + other => panic!("expected Async, got {other:?}"), + }; + assert!(matches!( + async_info.async_type.kind(), + TypeKind::IAsyncOperation(_) + )); + let _ = async_info.handler_iid(); + assert!(matches!(projected.await?, WinRTValue::Object(_))); + + Ok(()) + } + #[tokio::test] async fn project_winrt_async_preserves_progress_contract() -> result::Result<()> { use std::sync::{Arc, atomic::Ordering}; @@ -9231,14 +9270,20 @@ mod tests { let table = MetadataTable::new(); let result_type = table.make(TypeKind::U32); let progress_type = table.make(TypeKind::U32); + let generic = table.generic(crate::metadata_table::IASYNC_OPERATION_WITH_PROGRESS, 2); let async_type = - table.async_operation_with_progress(&result_type, &progress_type); + table.parameterized(&generic, &[result_type.clone(), progress_type.clone()]); let projected = project_winrt_async(&source, async_type)?; let async_info = match &projected { WinRTValue::Async(info) => info, other => panic!("expected Async, got {other:?}"), }; + assert!(matches!( + async_info.async_type.kind(), + TypeKind::IAsyncOperationWithProgress(_) + )); + let _ = async_info.handler_iid(); assert_eq!(async_info.result_type().unwrap().kind(), TypeKind::U32); assert_eq!(async_info.progress_type().unwrap().kind(), TypeKind::U32); diff --git a/crates/dynwinrt/src/metadata_table/type_handle.rs b/crates/dynwinrt/src/metadata_table/type_handle.rs index 8f9eb5c0..17dbd4cd 100644 --- a/crates/dynwinrt/src/metadata_table/type_handle.rs +++ b/crates/dynwinrt/src/metadata_table/type_handle.rs @@ -199,6 +199,20 @@ impl TypeHandle { } } + pub(crate) fn normalized_async_type(&self) -> crate::result::Result { + match self.kind { + TypeKind::IAsyncAction + | TypeKind::IAsyncActionWithProgress(_) + | TypeKind::IAsyncOperation(_) + | TypeKind::IAsyncOperationWithProgress(_) => Ok(self.clone()), + TypeKind::Parameterized(idx) => { + let (generic_def, args) = self.table.get_parameterized(idx); + normalize_async_type(generic_def, &args, &self.table) + } + _ => Err(invalid_async_type()), + } + } + /// Reverse-lookup an enum member name from its i32 value. /// Returns None if not an Enum type or no member matches. pub fn enum_member_name(&self, value: i32) -> Option { @@ -471,44 +485,45 @@ fn make_async_value_from_kind( args: &[TypeKind], table: &Arc, ) -> crate::result::Result { - let piid = match generic_def { - TypeKind::Generic { piid, .. } => piid, - TypeKind::Interface(iid) => iid, - _ => { - return Err(crate::result::Error::WindowsError( - windows_core::Error::from_hresult(windows_core::HRESULT(0x80004002u32 as i32)), - )); - } - }; + let async_type = normalize_async_type(generic_def, args, table)?; let info: windows_future::IAsyncInfo = raw .cast() .map_err(|e| crate::result::Error::WindowsError(e))?; - let async_type = if piid == IASYNC_ACTION { - table.async_action() - } else if piid == IASYNC_OPERATION { - let t = args.first().copied().unwrap_or(TypeKind::Object); - let t_h = table.make(t); - table.async_operation(&t_h) - } else if piid == IASYNC_ACTION_WITH_PROGRESS { - let p = args.first().copied().unwrap_or(TypeKind::Object); - let p_h = table.make(p); - table.async_action_with_progress(&p_h) - } else if piid == IASYNC_OPERATION_WITH_PROGRESS { - let t = args.first().copied().unwrap_or(TypeKind::Object); - let p = args.get(1).copied().unwrap_or(TypeKind::Object); - let t_h = table.make(t); - let p_h = table.make(p); - table.async_operation_with_progress(&t_h, &p_h) - } else { - return Err(crate::result::Error::WindowsError( - windows_core::Error::from_hresult(windows_core::HRESULT(0x80004002u32 as i32)), - )); - }; - Ok(WinRTValue::Async(crate::value::AsyncInfo { info, async_type, })) } + +fn normalize_async_type( + generic_def: TypeKind, + args: &[TypeKind], + table: &Arc, +) -> crate::result::Result { + let piid = match generic_def { + TypeKind::Generic { piid, .. } => piid, + TypeKind::Interface(iid) => iid, + _ => return Err(invalid_async_type()), + }; + + match (piid, args) { + (IASYNC_ACTION, []) => Ok(table.async_action()), + (IASYNC_OPERATION, [result]) => Ok(table.async_operation(&table.make(*result))), + (IASYNC_ACTION_WITH_PROGRESS, [progress]) => { + Ok(table.async_action_with_progress(&table.make(*progress))) + } + (IASYNC_OPERATION_WITH_PROGRESS, [result, progress]) => { + Ok(table.async_operation_with_progress(&table.make(*result), &table.make(*progress))) + } + _ => Err(invalid_async_type()), + } +} + +fn invalid_async_type() -> crate::result::Error { + crate::result::Error::WindowsError(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + "Invalid WinRT async type", + )) +} From f581e9bebd24aca07b7d735bcd5c09f6e9c8ecd5 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Tue, 11 Aug 2026 15:20:57 +0800 Subject: [PATCH 3/4] Reject invalid WinRT async signatures Validate closed WinRT signatures and generic arity before constructing AsyncInfo so invalid result or progress types return errors instead of reaching IID panics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eca73d14-f74e-462f-87c7-b48f6031f8b8 --- bindings/js/__test__/index.spec.ts | 11 ++ crates/dynwinrt/src/com.rs | 9 ++ crates/dynwinrt/src/metadata_table/iid.rs | 101 +++++++++++------- .../src/metadata_table/type_handle.rs | 17 ++- 4 files changed, 97 insertions(+), 41 deletions(-) diff --git a/bindings/js/__test__/index.spec.ts b/bindings/js/__test__/index.spec.ts index da28655d..5c962e5e 100644 --- a/bindings/js/__test__/index.spec.ts +++ b/bindings/js/__test__/index.spec.ts @@ -193,6 +193,17 @@ 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 error = t.throws(() => + DynCom.projectWinRtAsync(DynWinRtValue.nullValue(), invalidAsyncType), + ) + t.regex(error.message, /valid WinRT signature/) +}) + 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/crates/dynwinrt/src/com.rs b/crates/dynwinrt/src/com.rs index 26b89380..e2d0351a 100644 --- a/crates/dynwinrt/src/com.rs +++ b/crates/dynwinrt/src/com.rs @@ -9213,6 +9213,15 @@ mod tests { assert!(project_winrt_async(&WinRTValue::Null, malformed).is_err()); } + #[test] + fn project_winrt_async_rejects_invalid_result_signature() { + let table = MetadataTable::new(); + let array = table.array(&table.i32_type()); + let invalid = table.async_operation(&array); + + assert!(project_winrt_async(&WinRTValue::Null, invalid).is_err()); + } + #[tokio::test] async fn project_winrt_async_normalizes_parameterized_operation() -> result::Result<()> { use windows::Storage::{IStorageFile, StorageFile}; diff --git a/crates/dynwinrt/src/metadata_table/iid.rs b/crates/dynwinrt/src/metadata_table/iid.rs index f5353cef..64f7c669 100644 --- a/crates/dynwinrt/src/metadata_table/iid.rs +++ b/crates/dynwinrt/src/metadata_table/iid.rs @@ -21,14 +21,6 @@ impl MetadataTable { GUID::from_signature(buf) } - fn pinterface_signature(&self, piid: &GUID, type_args: &[TypeKind]) -> String { - let arg_sigs: Vec = type_args - .iter() - .map(|a| self.signature_string_kind(*a)) - .collect(); - pinterface_signature_from_strings(&format_guid_braced(piid), &arg_sigs) - } - fn async_type_args(&self, kind: TypeKind) -> Vec { match kind { TypeKind::IAsyncActionWithProgress(idx) | TypeKind::IAsyncOperation(idx) => { @@ -43,62 +35,91 @@ impl MetadataTable { } pub(crate) fn signature_string_kind(&self, kind: TypeKind) -> String { + self.try_signature_string_kind(kind) + .expect("Type has no valid WinRT signature") + } + + pub(crate) fn try_signature_string_kind( + &self, + kind: TypeKind, + ) -> crate::result::Result { if let Some(sig) = kind.signature() { - return sig.into(); + return Ok(sig.into()); } match kind { - TypeKind::Interface(iid) | TypeKind::Generic { piid: iid, .. } => { - format_guid_braced(&iid) - } - TypeKind::Delegate(iid) => { - format!("delegate({})", format_guid_braced(&iid)) - } + TypeKind::Interface(iid) => Ok(format_guid_braced(&iid)), + TypeKind::Delegate(iid) => Ok(format!("delegate({})", format_guid_braced(&iid))), TypeKind::RuntimeClass(idx) => { let (name, default_interface) = self.get_runtime_class(idx); - format!( + Ok(format!( "rc({};{})", name, - self.signature_string_kind(default_interface) - ) + self.try_signature_string_kind(default_interface)? + )) } TypeKind::Parameterized(idx) => { let (generic_def, args) = self.get_parameterized(idx); - let piid_sig = self.signature_string_kind(generic_def); - let arg_sigs: Vec = args + let piid = match generic_def { + TypeKind::Generic { piid, arity } if arity as usize == args.len() => piid, + TypeKind::Interface(iid) => iid, + _ => return Err(Self::invalid_signature(kind)), + }; + let arg_sigs: crate::result::Result> = args .iter() - .map(|a| self.signature_string_kind(*a)) + .map(|a| self.try_signature_string_kind(*a)) .collect(); - pinterface_signature_from_strings(&piid_sig, &arg_sigs) - } - TypeKind::IAsyncAction => format_guid_braced(&IASYNC_ACTION), - TypeKind::IAsyncActionWithProgress(_) => { - self.pinterface_signature(&IASYNC_ACTION_WITH_PROGRESS, &self.async_type_args(kind)) + Ok(pinterface_signature_from_strings( + &format_guid_braced(&piid), + &arg_sigs?, + )) } + TypeKind::IAsyncAction => Ok(format_guid_braced(&IASYNC_ACTION)), + TypeKind::IAsyncActionWithProgress(_) => self.try_pinterface_signature( + &IASYNC_ACTION_WITH_PROGRESS, + &self.async_type_args(kind), + ), TypeKind::IAsyncOperation(_) => { - self.pinterface_signature(&IASYNC_OPERATION, &self.async_type_args(kind)) + self.try_pinterface_signature(&IASYNC_OPERATION, &self.async_type_args(kind)) } - TypeKind::IAsyncOperationWithProgress(_) => self - .pinterface_signature(&IASYNC_OPERATION_WITH_PROGRESS, &self.async_type_args(kind)), - TypeKind::Object => "cinterface(IInspectable)".to_string(), - TypeKind::HResult => "i4".to_string(), + TypeKind::IAsyncOperationWithProgress(_) => self.try_pinterface_signature( + &IASYNC_OPERATION_WITH_PROGRESS, + &self.async_type_args(kind), + ), + TypeKind::Object => Ok("cinterface(IInspectable)".to_string()), + TypeKind::HResult => Ok("i4".to_string()), TypeKind::Enum(idx) => { let name = self.get_enum_name(idx); - format!("enum({};i4)", name) + Ok(format!("enum({};i4)", name)) } TypeKind::Struct(idx) => { let entry = &self.structs.read().unwrap()[idx as usize]; let name = &entry.name; - let field_sigs: Vec = entry + let field_sigs: crate::result::Result> = entry .field_kinds .iter() - .map(|k| self.signature_string_kind(*k)) + .map(|k| self.try_signature_string_kind(*k)) .collect(); - format!("struct({};{})", name, field_sigs.join(";")) + Ok(format!("struct({};{})", name, field_sigs?.join(";"))) } - _ => panic!("Type {:?} has no WinRT type signature", kind), + _ => Err(Self::invalid_signature(kind)), } } + fn try_pinterface_signature( + &self, + piid: &GUID, + type_args: &[TypeKind], + ) -> crate::result::Result { + let arg_sigs: crate::result::Result> = type_args + .iter() + .map(|a| self.try_signature_string_kind(*a)) + .collect(); + Ok(pinterface_signature_from_strings( + &format_guid_braced(piid), + &arg_sigs?, + )) + } + pub(crate) fn iid_kind(&self, kind: TypeKind) -> Option { match kind { TypeKind::Interface(iid) | TypeKind::Delegate(iid) => Some(iid), @@ -106,6 +127,7 @@ impl MetadataTable { let (_, default_interface) = self.get_runtime_class(idx); self.iid_kind(default_interface) } + TypeKind::IAsyncAction => Some(IASYNC_ACTION), TypeKind::Parameterized(_) | TypeKind::IAsyncActionWithProgress(_) @@ -118,6 +140,13 @@ impl MetadataTable { } } + fn invalid_signature(kind: TypeKind) -> crate::result::Error { + crate::result::Error::WindowsError(windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &format!("Type {kind:?} has no valid WinRT signature"), + )) + } + pub(crate) fn completed_handler_iid_kind(&self, kind: TypeKind) -> Option { let handler_piid = match kind { TypeKind::IAsyncAction => return Some(ASYNC_ACTION_COMPLETED_HANDLER), diff --git a/crates/dynwinrt/src/metadata_table/type_handle.rs b/crates/dynwinrt/src/metadata_table/type_handle.rs index 17dbd4cd..1532cf84 100644 --- a/crates/dynwinrt/src/metadata_table/type_handle.rs +++ b/crates/dynwinrt/src/metadata_table/type_handle.rs @@ -200,7 +200,7 @@ impl TypeHandle { } pub(crate) fn normalized_async_type(&self) -> crate::result::Result { - match self.kind { + let normalized = match self.kind { TypeKind::IAsyncAction | TypeKind::IAsyncActionWithProgress(_) | TypeKind::IAsyncOperation(_) @@ -210,7 +210,11 @@ impl TypeHandle { normalize_async_type(generic_def, &args, &self.table) } _ => Err(invalid_async_type()), - } + }?; + normalized + .table + .try_signature_string_kind(normalized.kind)?; + Ok(normalized) } /// Reverse-lookup an enum member name from its i32 value. @@ -502,11 +506,14 @@ fn normalize_async_type( args: &[TypeKind], table: &Arc, ) -> crate::result::Result { - let piid = match generic_def { - TypeKind::Generic { piid, .. } => piid, - TypeKind::Interface(iid) => iid, + let (piid, declared_arity) = match generic_def { + TypeKind::Generic { piid, arity } => (piid, Some(arity as usize)), + TypeKind::Interface(iid) => (iid, None), _ => return Err(invalid_async_type()), }; + if declared_arity.is_some_and(|arity| arity != args.len()) { + return Err(invalid_async_type()); + } match (piid, args) { (IASYNC_ACTION, []) => Ok(table.async_action()), From f095d571f6f9671fee4ae56af04ec054f3d24655 Mon Sep 17 00:00:00 2001 From: Leilei Zhang Date: Tue, 11 Aug 2026 16:05:39 +0800 Subject: [PATCH 4/4] Validate projected WinRT async interface identity Query the requested closed async IID before constructing AsyncInfo and restore Generic signature compatibility while keeping closed-type validation for async arguments. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: eca73d14-f74e-462f-87c7-b48f6031f8b8 --- bindings/js/__test__/index.spec.ts | 16 +++++++++ crates/dynwinrt/src/com.rs | 34 ++++++++++++++++++- crates/dynwinrt/src/metadata_table/iid.rs | 24 ++++++++++--- crates/dynwinrt/src/metadata_table/mod.rs | 4 +++ .../src/metadata_table/type_handle.rs | 2 +- 5 files changed, 74 insertions(+), 6 deletions(-) diff --git a/bindings/js/__test__/index.spec.ts b/bindings/js/__test__/index.spec.ts index 5c962e5e..21a008bc 100644 --- a/bindings/js/__test__/index.spec.ts +++ b/bindings/js/__test__/index.spec.ts @@ -204,6 +204,22 @@ test('DynCom rejects invalid WinRT async result signatures', (t) => { t.regex(error.message, /valid WinRT signature/) }) +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()), + ), + ) + t.regex(error.message, /0x80004002|interface/i) + } finally { + factory.release() + } +}) + 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/crates/dynwinrt/src/com.rs b/crates/dynwinrt/src/com.rs index e2d0351a..3c67d710 100644 --- a/crates/dynwinrt/src/com.rs +++ b/crates/dynwinrt/src/com.rs @@ -4389,7 +4389,15 @@ pub fn project_winrt_async( let object = value .as_object() .ok_or_else(|| invalid_argument("project_winrt_async requires a COM object"))?; - let info: windows_future::IAsyncInfo = object.cast().map_err(result::Error::WindowsError)?; + let iid = async_type + .iid() + .ok_or_else(|| invalid_argument("project_winrt_async requires a closed async IID"))?; + let mut concrete_ptr = std::ptr::null_mut(); + unsafe { object.query(&iid, &mut concrete_ptr) } + .ok() + .map_err(result::Error::WindowsError)?; + let concrete = unsafe { IUnknown::from_raw(concrete_ptr) }; + let info: windows_future::IAsyncInfo = concrete.cast().map_err(result::Error::WindowsError)?; Ok(WinRTValue::Async(crate::value::AsyncInfo { info, async_type, @@ -9222,6 +9230,30 @@ mod tests { assert!(project_winrt_async(&WinRTValue::Null, invalid).is_err()); } + #[test] + fn project_winrt_async_rejects_mismatched_interface_iid() -> result::Result<()> { + use windows::Storage::Streams::{Buffer, IOutputStream, InMemoryRandomAccessStream}; + + let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; + let stream = InMemoryRandomAccessStream::new().map_err(result::Error::WindowsError)?; + let output: IOutputStream = stream.cast().map_err(result::Error::WindowsError)?; + let buffer = Buffer::Create(16).map_err(result::Error::WindowsError)?; + buffer.SetLength(16).map_err(result::Error::WindowsError)?; + let operation = output + .WriteAsync(&buffer) + .map_err(result::Error::WindowsError)?; + let object: IUnknown = operation.cast().map_err(result::Error::WindowsError)?; + let source = WinRTValue::Object(object); + + let table = MetadataTable::new(); + let wrong_result = table.make(TypeKind::U64); + let wrong_progress = table.make(TypeKind::U64); + let wrong_type = table.async_operation_with_progress(&wrong_result, &wrong_progress); + + assert!(project_winrt_async(&source, wrong_type).is_err()); + Ok(()) + } + #[tokio::test] async fn project_winrt_async_normalizes_parameterized_operation() -> result::Result<()> { use windows::Storage::{IStorageFile, StorageFile}; diff --git a/crates/dynwinrt/src/metadata_table/iid.rs b/crates/dynwinrt/src/metadata_table/iid.rs index 64f7c669..eee8ca88 100644 --- a/crates/dynwinrt/src/metadata_table/iid.rs +++ b/crates/dynwinrt/src/metadata_table/iid.rs @@ -42,19 +42,35 @@ impl MetadataTable { pub(crate) fn try_signature_string_kind( &self, kind: TypeKind, + ) -> crate::result::Result { + self.try_signature_string_kind_impl(kind, true) + } + + pub(crate) fn try_closed_signature_string_kind( + &self, + kind: TypeKind, + ) -> crate::result::Result { + self.try_signature_string_kind_impl(kind, false) + } + + fn try_signature_string_kind_impl( + &self, + kind: TypeKind, + allow_open_generic: bool, ) -> crate::result::Result { if let Some(sig) = kind.signature() { return Ok(sig.into()); } match kind { TypeKind::Interface(iid) => Ok(format_guid_braced(&iid)), + TypeKind::Generic { piid, .. } if allow_open_generic => Ok(format_guid_braced(&piid)), TypeKind::Delegate(iid) => Ok(format!("delegate({})", format_guid_braced(&iid))), TypeKind::RuntimeClass(idx) => { let (name, default_interface) = self.get_runtime_class(idx); Ok(format!( "rc({};{})", name, - self.try_signature_string_kind(default_interface)? + self.try_signature_string_kind_impl(default_interface, false)? )) } TypeKind::Parameterized(idx) => { @@ -66,7 +82,7 @@ impl MetadataTable { }; let arg_sigs: crate::result::Result> = args .iter() - .map(|a| self.try_signature_string_kind(*a)) + .map(|a| self.try_signature_string_kind_impl(*a, false)) .collect(); Ok(pinterface_signature_from_strings( &format_guid_braced(&piid), @@ -97,7 +113,7 @@ impl MetadataTable { let field_sigs: crate::result::Result> = entry .field_kinds .iter() - .map(|k| self.try_signature_string_kind(*k)) + .map(|k| self.try_signature_string_kind_impl(*k, false)) .collect(); Ok(format!("struct({};{})", name, field_sigs?.join(";"))) } @@ -112,7 +128,7 @@ impl MetadataTable { ) -> crate::result::Result { let arg_sigs: crate::result::Result> = type_args .iter() - .map(|a| self.try_signature_string_kind(*a)) + .map(|a| self.try_signature_string_kind_impl(*a, false)) .collect(); Ok(pinterface_signature_from_strings( &format_guid_braced(piid), diff --git a/crates/dynwinrt/src/metadata_table/mod.rs b/crates/dynwinrt/src/metadata_table/mod.rs index 999e1d41..64fade9a 100644 --- a/crates/dynwinrt/src/metadata_table/mod.rs +++ b/crates/dynwinrt/src/metadata_table/mod.rs @@ -778,6 +778,10 @@ mod tests { assert_eq!(table.hstring().signature_string(), "string"); let g = table.generic(IASYNC_OPERATION, 1); + assert_eq!( + g.signature_string(), + "{9fc2b0bb-e446-44e2-aa61-9cab8f636af2}", + ); let sig = table.parameterized(&g, &[table.hstring()]); assert_eq!( sig.signature_string(), diff --git a/crates/dynwinrt/src/metadata_table/type_handle.rs b/crates/dynwinrt/src/metadata_table/type_handle.rs index 1532cf84..5aa81f32 100644 --- a/crates/dynwinrt/src/metadata_table/type_handle.rs +++ b/crates/dynwinrt/src/metadata_table/type_handle.rs @@ -213,7 +213,7 @@ impl TypeHandle { }?; normalized .table - .try_signature_string_kind(normalized.kind)?; + .try_closed_signature_string_kind(normalized.kind)?; Ok(normalized) }