From 5dca22cdc00145d1e7e7474d71da3e2fc3556004 Mon Sep 17 00:00:00 2001 From: leileizhang Date: Thu, 13 Aug 2026 13:41:56 +0800 Subject: [PATCH] Complete Classic COM infrastructure support Add complete IPropertyStore, BIND_OPTS, IStream, IMalloc, IClassFactory, and error-info projection/runtime support with exact ABI and ownership contracts. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- bindings/js/__test__/index.spec.ts | 13 +- bindings/js/scripts/generate-entrypoints.mjs | 34 +- bindings/js/src/com.rs | 588 ++++++++++++++- bindings/js/src/lib.rs | 11 + crates/dynwinrt/src/call.rs | 24 + crates/dynwinrt/src/com.rs | 700 +++++++++++++++++- crates/dynwinrt/src/native_call.rs | 42 +- docs/architecture/classic-com-support.md | 89 +-- docs/guides/windows/classic-com-usage.md | 17 + tests/e2e/e2e_test.ps1 | 29 +- tests/e2e/runners/com/automation-dispatch.mjs | 6 +- tests/e2e/runners/com/com-infrastructure.mjs | 85 +++ tests/e2e/runners/com/property-store.mjs | 50 ++ .../runners/com/sequential-stream-buffer.mjs | 23 +- tools/dynwinrt-codegen/src/codegen/com/ir.rs | 19 + .../src/codegen/com/javascript/render.rs | 50 +- .../src/codegen/com/javascript/types.rs | 68 +- .../src/codegen/com/model/abi.rs | 1 + .../src/codegen/com/model/metadata.rs | 49 +- .../src/codegen/com/model/method.rs | 2 + .../src/codegen/com/model/ownership.rs | 3 + .../src/codegen/com/project/mod.rs | 216 +++++- tools/dynwinrt-codegen/src/com_metadata.rs | 140 +++- .../dynwinrt-codegen/tests/win32_com_test.rs | 217 +++++- 25 files changed, 2315 insertions(+), 163 deletions(-) create mode 100644 tests/e2e/runners/com/com-infrastructure.mjs create mode 100644 tests/e2e/runners/com/property-store.mjs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e456b3d8..f1692c85 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -51,7 +51,7 @@ jobs: if ($result.eligible_interfaces -ne 7929) { throw "Classic COM census denominator changed: $($result.eligible_interfaces)" } - if ($result.complete_interfaces -lt 5560) { + if ($result.complete_interfaces -lt 5567) { throw "Classic COM complete coverage regressed: $($result.complete_interfaces)" } if ($result.coverage_percent -lt 70.0) { diff --git a/bindings/js/__test__/index.spec.ts b/bindings/js/__test__/index.spec.ts index 21a008bc..6c7e61ff 100644 --- a/bindings/js/__test__/index.spec.ts +++ b/bindings/js/__test__/index.spec.ts @@ -3,7 +3,7 @@ import test from 'ava' import { spawn, spawnSync } from 'node:child_process' -import { existsSync } from 'node:fs' +import { existsSync, readFileSync } from 'node:fs' import { createRequire } from 'node:module' import { resolve } from 'node:path' import { fileURLToPath } from 'node:url' @@ -113,12 +113,14 @@ test('package facades exactly partition native exports', (t) => { const expectedWinrt = nativeKeys.filter((name) => !name.startsWith('DynCom') && name !== 'initializeCom') const safeComNames = new Set([ 'DynComDispatchParams', + 'DynComAllocation', 'DynComExcepInfo', 'DynComNativeStruct', 'DynComNativeStructArray', 'DynComNativeUnion', 'DynComPropVariant', 'DynComSafeArray', + 'DynComStatStg', 'DynComVariant', 'DynWinRtValue', 'WinGuid', @@ -157,6 +159,15 @@ 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', + ) + t.regex(declaration, /export interface DynComAllocation/) + t.notRegex(declaration, /export \{[^}]*DynComAllocation[^}]*\} from/) +}) + test('WinRT root facade exposes usable native primitives', (t) => { t.truthy(winrtRuntime.DynWinRtType.i32()) t.is(winrtRuntime.DynWinRtValue.i32(1).toNumber(), 1) diff --git a/bindings/js/scripts/generate-entrypoints.mjs b/bindings/js/scripts/generate-entrypoints.mjs index 50645a12..f193dc1d 100644 --- a/bindings/js/scripts/generate-entrypoints.mjs +++ b/bindings/js/scripts/generate-entrypoints.mjs @@ -18,19 +18,34 @@ if (nativeExports.length === 0) { const comExports = new Set([ 'DynComDispatchParams', + 'DynComAllocation', 'DynComExcepInfo', 'DynComNativeStruct', 'DynComNativeStructArray', 'DynComNativeUnion', 'DynComPropVariant', 'DynComSafeArray', + 'DynComStatStg', 'DynComVariant', 'DynWinRtValue', 'WinGuid', 'initializeCom', ]) const comTypeAliases = ['DynWinRTValue', 'WinGUID'] -const comTypeExports = [...comExports, ...comTypeAliases, 'DynComSafeArrayBound'] +const opaqueComTypes = new Set(['DynComAllocation']) +const comTypeExports = [ + ...[...comExports].filter((name) => !opaqueComTypes.has(name)), + ...comTypeAliases, + 'DynComSafeArrayBound', +] +const opaqueComDeclarations = [ + 'declare const dynComAllocationBrand: unique symbol', + 'export interface DynComAllocation {', + ' readonly [dynComAllocationBrand]: never', + ' readonly released: boolean', + ' release(): void', + '}', +] const comUnsafeExports = new Set([ ...comExports, 'DynCom', @@ -42,7 +57,11 @@ const comUnsafeExports = new Set([ 'DynComUnsafe', 'DynComUnsafeInterface', ]) -const comUnsafeTypeExports = [...comUnsafeExports, ...comTypeAliases, 'DynComSafeArrayBound'] +const comUnsafeTypeExports = [ + ...[...comUnsafeExports].filter((name) => !opaqueComTypes.has(name)), + ...comTypeAliases, + 'DynComSafeArrayBound', +] writeFacade( 'winrt', @@ -53,15 +72,23 @@ writeFacade( nativeExports.filter((name) => comExports.has(name)), comTypeExports, comExports, + opaqueComDeclarations, ) writeFacade( 'com-unsafe', nativeExports.filter((name) => comUnsafeExports.has(name)), comUnsafeTypeExports, comUnsafeExports, + opaqueComDeclarations, ) -function writeFacade(name, exports, typeExports = exports, requiredExports = []) { +function writeFacade( + name, + exports, + typeExports = exports, + requiredExports = [], + extraTypeDeclarations = [], +) { const missing = [...requiredExports].filter((value) => !exports.includes(value)) if (missing.length > 0) { throw new Error(`Missing required ${name} exports: ${missing.join(', ')}`) @@ -77,6 +104,7 @@ function writeFacade(name, exports, typeExports = exports, requiredExports = []) const dts = [ '// Generated by scripts/generate-entrypoints.mjs - do not edit', `export { ${typeExports.join(', ')} } from './index.js'`, + ...extraTypeDeclarations, '', ].join('\n') diff --git a/bindings/js/src/com.rs b/bindings/js/src/com.rs index 55116d13..1ac6ef51 100644 --- a/bindings/js/src/com.rs +++ b/bindings/js/src/com.rs @@ -37,6 +37,7 @@ enum AutomationValueKind { PropVariant(dynwinrt::com::PropVariantValue), DispatchParams(dynwinrt::com::DispatchParamsValue), ExcepInfo(dynwinrt::com::ExcepInfoValue), + StatStg(dynwinrt::com::StatStgValue), } pub(super) struct AutomationValue { @@ -54,6 +55,7 @@ impl AutomationValue { dynwinrt::com::Value::PropVariant(value) => AutomationValueKind::PropVariant(value), dynwinrt::com::Value::DispatchParams(value) => AutomationValueKind::DispatchParams(value), dynwinrt::com::Value::ExcepInfo(value) => AutomationValueKind::ExcepInfo(value), + dynwinrt::com::Value::StatStg(value) => AutomationValueKind::StatStg(value), _ => unreachable!("AutomationValue requires an automation COM value"), }; Self { @@ -90,6 +92,7 @@ impl AutomationValue { dynwinrt::com::Value::DispatchParams(value.clone()) } AutomationValueKind::ExcepInfo(value) => dynwinrt::com::Value::ExcepInfo(value.clone()), + AutomationValueKind::StatStg(value) => dynwinrt::com::Value::StatStg(value.clone()), }) } @@ -137,6 +140,17 @@ impl AutomationValue { } } + pub(super) fn take_stat_stg(&mut self) -> napi::Result { + self.ensure_owner_thread()?; + match self.value.take() { + Some(AutomationValueKind::StatStg(value)) => Ok(value), + value => { + self.value = value; + Err(napi::Error::from_reason("Value is not COM STATSTG")) + } + } + } + pub(super) fn leak_for_shutdown(&mut self) { if let Some(value) = self.value.take() { std::mem::forget(value); @@ -280,16 +294,59 @@ impl Drop for NativePointerOwner { } } -fn co_create_instance(clsid: String, iid: &WinGUID) -> napi::Result { - let parsed = windows::core::GUID::try_from(clsid.as_str()) - .map_err(|_| napi::Error::from_reason(format!("Invalid CLSID: '{clsid}'")))?; - let mut value = dynwinrt::com::co_create_instance(parsed, iid.0) +fn parse_clsid(clsid: &str) -> napi::Result { + windows::core::GUID::try_from(clsid) + .map_err(|_| napi::Error::from_reason(format!("Invalid CLSID: '{clsid}'"))) +} + +fn bind_com_result(result: dynwinrt::Result) -> napi::Result { + let mut value = result .map(DynWinRTValue::new) .map_err(|error| napi::Error::from_reason(error.message()))?; value.bind_current_com_apartment()?; Ok(value) } +fn co_create_instance(clsid: String, iid: &WinGUID) -> napi::Result { + bind_com_result(dynwinrt::com::co_create_instance( + parse_clsid(&clsid)?, + iid.0, + )) +} + +fn co_get_class_object(clsid: String, iid: &WinGUID) -> napi::Result { + bind_com_result(dynwinrt::com::co_get_class_object( + parse_clsid(&clsid)?, + iid.0, + )) +} + +fn co_get_malloc() -> napi::Result { + bind_com_result(dynwinrt::com::co_get_malloc()) +} + +fn create_error_info() -> napi::Result { + bind_com_result(dynwinrt::com::create_error_info()) +} + +fn set_error_info(value: Option<&DynWinRTValue>) -> napi::Result<()> { + if let Some(value) = value { + value.ensure_existing_com_apartment()?; + } + dynwinrt::com::set_error_info(value.map(|value| &value.0)).map_err(com_error) +} + +fn get_error_info() -> napi::Result> { + dynwinrt::com::get_error_info() + .map_err(com_error)? + .map(|value| { + let mut value = DynWinRTValue::new(value); + value.bind_current_com_apartment()?; + Ok(value) + }) + .transpose() +} + fn try_cast(value: &DynWinRTValue, iid: &WinGUID) -> napi::Result> { const E_NOINTERFACE: windows::core::HRESULT = windows::core::HRESULT(0x80004002u32 as i32); @@ -1098,7 +1155,38 @@ fn native_struct_layout( "Native struct descriptor is missing `{architecture}`" )) })?; - parse_native_struct_variant(name, layout) + let parsed = parse_native_struct_variant(name, layout)?; + let mut parsed = std::sync::Arc::try_unwrap(parsed) + .map_err(|_| napi::Error::from_reason("Native struct layout is unexpectedly shared"))?; + let initializers = root + .get("initializers") + .map(|value| { + value + .as_array() + .ok_or_else(|| napi::Error::from_reason("Native struct `initializers` must be an array")) + }) + .transpose()?; + for initializer in initializers.into_iter().flatten() { + let kind = initializer + .get("kind") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| napi::Error::from_reason("Native struct initializer is missing `kind`"))?; + let field = initializer + .get("field") + .and_then(serde_json::Value::as_str) + .ok_or_else(|| napi::Error::from_reason("Native struct initializer is missing `field`"))?; + parsed = match kind { + "sizeOfLayout" => parsed + .with_size_field_initializer(field) + .map_err(|error| napi::Error::from_reason(error.message()))?, + _ => { + return Err(napi::Error::from_reason(format!( + "Unsupported native struct initializer `{kind}`" + ))); + } + }; + } + Ok(std::sync::Arc::new(parsed)) } fn native_union_layout( @@ -1971,6 +2059,33 @@ impl DynComUnsafe { self::co_create_instance(clsid, iid) } + #[napi] + pub fn co_get_class_object(clsid: String, iid: &WinGUID) -> napi::Result { + self::co_get_class_object(clsid, iid) + } + + #[napi] + pub fn co_get_malloc() -> napi::Result { + self::co_get_malloc() + } + + #[napi] + pub fn create_error_info() -> napi::Result { + self::create_error_info() + } + + #[napi] + pub fn set_error_info( + #[napi(ts_arg_type = "DynWinRtValue | null | undefined")] value: Option<&DynWinRTValue>, + ) -> napi::Result<()> { + self::set_error_info(value) + } + + #[napi] + pub fn get_error_info() -> napi::Result> { + self::get_error_info() + } + /// Takes ownership of one caller-supplied +1 COM reference. #[napi] pub fn adopt_owned_com_pointer( @@ -2463,6 +2578,332 @@ impl DynComExcepInfo { } } +#[napi] +pub struct DynComStatStg { + owner_thread: std::thread::ThreadId, + value: Option, +} + +impl DynComStatStg { + fn new(value: dynwinrt::com::StatStgValue) -> Self { + Self { + owner_thread: std::thread::current().id(), + value: Some(value), + } + } + + fn value(&self) -> napi::Result<&dynwinrt::com::StatStgValue> { + if std::thread::current().id() != self.owner_thread { + return Err(napi::Error::from_reason( + "Apartment-bound STATSTG used from a different thread", + )); + } + self + .value + .as_ref() + .ok_or_else(|| napi::Error::from_reason("STATSTG has been released")) + } +} + +// StatStgValue owns immutable Rust data after conversion; the native name +// pointer has already been adopted and nulled before this wrapper is created. +unsafe impl Send for DynComStatStg {} +unsafe impl Sync for DynComStatStg {} + +#[napi] +impl DynComStatStg { + #[napi(getter)] + pub fn name(&self) -> napi::Result> { + Ok(self.value()?.name().map(str::to_owned)) + } + + #[napi(getter)] + pub fn storage_type(&self) -> napi::Result { + Ok(self.value()?.stream_type()) + } + + #[napi(getter)] + pub fn size(&self) -> napi::Result { + Ok(self.value()?.size().into()) + } + + #[napi(getter)] + pub fn modified_time(&self) -> napi::Result { + Ok(self.value()?.modified_time().into()) + } + + #[napi(getter)] + pub fn creation_time(&self) -> napi::Result { + Ok(self.value()?.created_time().into()) + } + + #[napi(getter)] + pub fn access_time(&self) -> napi::Result { + Ok(self.value()?.accessed_time().into()) + } + + #[napi(getter)] + pub fn mode(&self) -> napi::Result { + Ok(self.value()?.mode()) + } + + #[napi(getter)] + pub fn locks_supported(&self) -> napi::Result { + Ok(self.value()?.locks_supported()) + } + + #[napi(getter)] + pub fn class_id(&self) -> napi::Result { + Ok(format!("{:?}", self.value()?.clsid())) + } + + #[napi(getter)] + pub fn state_bits(&self) -> napi::Result { + Ok(self.value()?.state_bits()) + } + + #[napi] + pub fn release(&mut self) -> napi::Result<()> { + if std::thread::current().id() != self.owner_thread { + return Err(napi::Error::from_reason( + "Apartment-bound STATSTG used from a different thread", + )); + } + self.value = None; + Ok(()) + } +} + +#[napi] +pub struct DynComAllocation { + owner_thread: std::thread::ThreadId, + allocator: Option, + pointer: usize, +} + +impl DynComAllocation { + fn new(allocator: windows::Win32::System::Com::IMalloc, pointer: *mut std::ffi::c_void) -> Self { + debug_assert!(!pointer.is_null()); + Self { + owner_thread: std::thread::current().id(), + allocator: Some(allocator), + pointer: pointer as usize, + } + } + + fn ensure_owner_thread(&self) -> napi::Result<()> { + if std::thread::current().id() == self.owner_thread { + Ok(()) + } else { + Err(napi::Error::from_reason( + "Apartment-bound IMalloc allocation used from a different thread", + )) + } + } + + fn validate_allocator( + &self, + allocator: &windows::Win32::System::Com::IMalloc, + ) -> napi::Result<()> { + self.ensure_owner_thread()?; + let expected: IUnknown = self + .allocator + .as_ref() + .ok_or_else(|| napi::Error::from_reason("IMalloc allocation has been released"))? + .cast() + .map_err(|error| napi::Error::from_reason(error.to_string()))?; + let actual: IUnknown = allocator + .cast() + .map_err(|error| napi::Error::from_reason(error.to_string()))?; + if expected.as_raw() != actual.as_raw() { + return Err(napi::Error::from_reason( + "IMalloc allocation belongs to a different allocator", + )); + } + if self.pointer == 0 { + return Err(napi::Error::from_reason( + "IMalloc allocation has been released", + )); + } + Ok(()) + } + + fn borrowed_pointer( + &self, + allocator: &windows::Win32::System::Com::IMalloc, + ) -> napi::Result<*mut std::ffi::c_void> { + self.validate_allocator(allocator)?; + Ok(self.pointer as *mut std::ffi::c_void) + } + + fn inspection_pointer(&self) -> napi::Result<*mut std::ffi::c_void> { + self.ensure_owner_thread()?; + if self.pointer == 0 { + return Err(napi::Error::from_reason( + "IMalloc allocation has been released", + )); + } + Ok(self.pointer as *mut std::ffi::c_void) + } + + fn take_pointer( + &mut self, + allocator: &windows::Win32::System::Com::IMalloc, + ) -> napi::Result<*mut std::ffi::c_void> { + self.validate_allocator(allocator)?; + let pointer = std::mem::replace(&mut self.pointer, 0); + self.allocator = None; + Ok(pointer as *mut std::ffi::c_void) + } + + fn release_inner(&mut self) { + if self.pointer != 0 { + if let Some(allocator) = &self.allocator { + unsafe { allocator.Free(Some(self.pointer as *mut std::ffi::c_void)) }; + } + self.pointer = 0; + self.allocator = None; + } + } +} + +impl Drop for DynComAllocation { + fn drop(&mut self) { + if std::thread::current().id() != self.owner_thread || super::winui_dispatcher_loop_exited() { + // Never invoke an apartment-bound allocator from a foreign or shut-down thread. + self.pointer = 0; + if let Some(allocator) = self.allocator.take() { + std::mem::forget(allocator); + } + return; + } + self.release_inner(); + } +} + +#[napi] +impl DynComAllocation { + #[napi(getter)] + pub fn released(&self) -> bool { + self.pointer == 0 + } + + #[napi] + pub fn release(&mut self) -> napi::Result<()> { + self.ensure_owner_thread()?; + self.release_inner(); + Ok(()) + } +} + +fn malloc_allocator(value: &DynWinRTValue) -> napi::Result { + value.ensure_existing_com_apartment()?; + value + .0 + .as_object() + .ok_or_else(|| napi::Error::from_reason("IMalloc operation requires a COM object"))? + .cast() + .map_err(|error| napi::Error::from_reason(error.to_string())) +} + +fn malloc_pointer_value(pointer: *mut std::ffi::c_void) -> DynWinRTValue { + DynWinRTValue::with_borrowed_pointer(dynwinrt::WinRTValue::RawPtr(pointer)) +} + +fn take_malloc_return_pointer(value: &mut DynWinRTValue) -> napi::Result<*mut std::ffi::c_void> { + if value.1.is_some() || value.2 != PointerProvenance::UnclassifiedOutput { + return Err(napi::Error::from_reason( + "IMalloc allocation requires an unowned direct pointer return", + )); + } + match std::mem::replace(&mut value.0, dynwinrt::WinRTValue::Null) { + dynwinrt::WinRTValue::RawPtr(pointer) => { + value.2 = PointerProvenance::None; + Ok(pointer) + } + dynwinrt::WinRTValue::Null => { + value.2 = PointerProvenance::None; + Ok(std::ptr::null_mut()) + } + other => { + value.0 = other; + Err(napi::Error::from_reason( + "IMalloc allocation result is not a native pointer", + )) + } + } +} + +fn malloc_allocation_pointer( + allocator: &DynWinRTValue, + allocation: Option<&DynComAllocation>, +) -> napi::Result { + let allocator = malloc_allocator(allocator)?; + allocation + .map(|allocation| allocation.borrowed_pointer(&allocator)) + .transpose() + .map(|pointer| malloc_pointer_value(pointer.unwrap_or(std::ptr::null_mut()))) +} + +fn malloc_inspection_pointer(allocation: Option<&DynComAllocation>) -> napi::Result { + allocation + .map(DynComAllocation::inspection_pointer) + .transpose() + .map(|pointer| malloc_pointer_value(pointer.unwrap_or(std::ptr::null_mut()))) +} + +fn take_malloc_allocation_pointer( + allocator: &DynWinRTValue, + allocation: Option<&mut DynComAllocation>, +) -> napi::Result { + let allocator = malloc_allocator(allocator)?; + allocation + .map(|allocation| allocation.take_pointer(&allocator)) + .transpose() + .map(|pointer| malloc_pointer_value(pointer.unwrap_or(std::ptr::null_mut()))) +} + +fn take_malloc_allocation( + allocator: &DynWinRTValue, + value: &mut DynWinRTValue, +) -> napi::Result> { + let allocator = malloc_allocator(allocator)?; + let pointer = take_malloc_return_pointer(value)?; + Ok((!pointer.is_null()).then(|| DynComAllocation::new(allocator, pointer))) +} + +fn finish_malloc_reallocation( + allocator: &DynWinRTValue, + allocation: Option<&mut DynComAllocation>, + size: BigInt, + value: &mut DynWinRTValue, +) -> napi::Result> { + let allocator = malloc_allocator(allocator)?; + if let Some(allocation) = allocation.as_deref() { + allocation.validate_allocator(&allocator)?; + } + let pointer = take_malloc_return_pointer(value)?; + if !pointer.is_null() { + if let Some(allocation) = allocation { + let _ = allocation.take_pointer(&allocator)?; + } + return Ok(Some(DynComAllocation::new(allocator, pointer))); + } + + let (negative, size, lossless) = size.get_u64(); + if negative || !lossless { + return Err(napi::Error::from_reason( + "IMalloc reallocation size must be an unsigned integer", + )); + } + if size == 0 { + if let Some(allocation) = allocation { + let _ = allocation.take_pointer(&allocator)?; + } + } + Ok(None) +} + #[napi(object)] pub struct DynComSafeArrayBound { pub lower_bound: f64, @@ -3696,6 +4137,11 @@ impl DynCom { DynComType(dynwinrt::com::Type::excep_info()) } + #[napi] + pub fn stat_stg_type() -> DynComType { + DynComType(dynwinrt::com::Type::stat_stg()) + } + #[napi] pub fn interface_type(iid: &WinGUID) -> DynComType { DynComType(dynwinrt::com::Type::winrt(TABLE.interface(iid.0))) @@ -3827,6 +4273,33 @@ impl DynCom { self::co_create_instance(clsid, iid) } + #[napi] + pub fn co_get_class_object(clsid: String, iid: &WinGUID) -> napi::Result { + self::co_get_class_object(clsid, iid) + } + + #[napi] + pub fn co_get_malloc() -> napi::Result { + self::co_get_malloc() + } + + #[napi] + pub fn create_error_info() -> napi::Result { + self::create_error_info() + } + + #[napi] + pub fn set_error_info( + #[napi(ts_arg_type = "DynWinRtValue | null | undefined")] value: Option<&DynWinRTValue>, + ) -> napi::Result<()> { + self::set_error_info(value) + } + + #[napi] + pub fn get_error_info() -> napi::Result> { + self::get_error_info() + } + #[napi] pub fn try_cast(value: &DynWinRTValue, iid: &WinGUID) -> napi::Result> { self::try_cast(value, iid) @@ -3845,6 +4318,47 @@ impl DynCom { self::pointer(value) } + #[napi] + pub fn malloc_allocation_pointer( + allocator: &DynWinRTValue, + allocation: Option<&DynComAllocation>, + ) -> napi::Result { + self::malloc_allocation_pointer(allocator, allocation) + } + + #[napi] + pub fn malloc_inspection_pointer( + allocation: Option<&DynComAllocation>, + ) -> napi::Result { + self::malloc_inspection_pointer(allocation) + } + + #[napi] + pub fn take_malloc_allocation_pointer( + allocator: &DynWinRTValue, + allocation: Option<&mut DynComAllocation>, + ) -> napi::Result { + self::take_malloc_allocation_pointer(allocator, allocation) + } + + #[napi] + pub fn take_malloc_allocation( + allocator: &DynWinRTValue, + value: &mut DynWinRTValue, + ) -> napi::Result> { + self::take_malloc_allocation(allocator, value) + } + + #[napi] + pub fn finish_malloc_reallocation( + allocator: &DynWinRTValue, + allocation: Option<&mut DynComAllocation>, + size: BigInt, + value: &mut DynWinRTValue, + ) -> napi::Result> { + self::finish_malloc_reallocation(allocator, allocation, size, value) + } + #[napi] pub fn safe_data_pointer( #[napi(ts_arg_type = "Buffer | Uint8Array | null | undefined")] value: Unknown, @@ -4308,18 +4822,15 @@ impl DynCom { bytes: Option, ) -> napi::Result { let layout = native_struct_layout(&descriptor)?; - let bytes = bytes - .map(|bytes| bytes.to_vec()) - .unwrap_or_else(|| vec![0; layout.size()]); - if bytes.len() != layout.size() { - return Err(napi::Error::from_reason(format!( - "Native struct `{}` requires exactly {} bytes, received {}", - layout.name(), - layout.size(), - bytes.len() - ))); + let value = match bytes { + Some(bytes) => dynwinrt::com::NativeStructValue::new(layout, bytes.to_vec()), + None => Ok(dynwinrt::com::NativeStructValue::zeroed(layout)), } - Ok(DynComNativeStruct { descriptor, bytes }) + .map_err(com_error)?; + Ok(DynComNativeStruct { + descriptor, + bytes: value.bytes().to_vec(), + }) } #[napi] @@ -4498,6 +5009,17 @@ impl DynCom { Ok(DynComExcepInfo::new(result)) } + #[napi] + pub fn take_stat_stg(value: &mut DynWinRTValue) -> napi::Result { + let result = value + .5 + .as_mut() + .ok_or_else(|| napi::Error::from_reason("Value is not COM STATSTG"))? + .take_stat_stg()?; + value.5 = None; + Ok(DynComStatStg::new(result)) + } + #[napi] pub fn wide_string_pointer( #[napi(ts_arg_type = "string | bigint | number | Buffer | Uint8Array | null | undefined")] @@ -4653,6 +5175,7 @@ mod tests { use std::ffi::c_void; const TEST_POD_DESCRIPTOR: &str = r#"{"name":"Test.Pod","x86":{"size":8,"alignment":4,"fields":[{"name":"first","offset":0,"count":1,"type":{"kind":"u32"}},{"name":"second","offset":4,"count":2,"type":{"kind":"u16"}}]},"x64":{"size":8,"alignment":4,"fields":[{"name":"first","offset":0,"count":1,"type":{"kind":"u32"}},{"name":"second","offset":4,"count":2,"type":{"kind":"u16"}}]},"arm64":{"size":8,"alignment":4,"fields":[{"name":"first","offset":0,"count":1,"type":{"kind":"u32"}},{"name":"second","offset":4,"count":2,"type":{"kind":"u16"}}]}}"#; + const TEST_INITIALIZED_POD_DESCRIPTOR: &str = r#"{"name":"Test.Initialized","initializers":[{"kind":"sizeOfLayout","field":"size"}],"x86":{"size":8,"alignment":4,"fields":[{"name":"size","offset":0,"count":1,"type":{"kind":"u32"}},{"name":"value","offset":4,"count":1,"type":{"kind":"u32"}}]},"x64":{"size":8,"alignment":4,"fields":[{"name":"size","offset":0,"count":1,"type":{"kind":"u32"}},{"name":"value","offset":4,"count":1,"type":{"kind":"u32"}}]},"arm64":{"size":8,"alignment":4,"fields":[{"name":"size","offset":0,"count":1,"type":{"kind":"u32"}},{"name":"value","offset":4,"count":1,"type":{"kind":"u32"}}]}}"#; const TEST_UNION_DESCRIPTOR: &str = r#"{"name":"Test.Union","x86":{"size":8,"alignment":8,"fields":[{"name":"integer","count":1,"type":{"kind":"u64"}},{"name":"pointer","count":1,"type":{"kind":"pointer"}}]},"x64":{"size":8,"alignment":8,"fields":[{"name":"integer","count":1,"type":{"kind":"u64"}},{"name":"pointer","count":1,"type":{"kind":"pointer"}}]},"arm64":{"size":8,"alignment":8,"fields":[{"name":"integer","count":1,"type":{"kind":"u64"}},{"name":"pointer","count":1,"type":{"kind":"pointer"}}]}}"#; #[repr(C)] @@ -4660,6 +5183,22 @@ mod tests { vtable: *const *mut c_void, } + #[test] + fn malloc_inspection_pointer_borrows_without_allocator_validation() { + dynwinrt::com::initialize_apartment(dynwinrt::com::ApartmentType::MultiThreaded).unwrap(); + let allocator = unsafe { windows::Win32::System::Com::CoGetMalloc(1) }.unwrap(); + let pointer = unsafe { allocator.Alloc(16) }; + assert!(!pointer.is_null()); + let allocation = DynComAllocation::new(allocator, pointer); + + let borrowed = malloc_inspection_pointer(Some(&allocation)).unwrap(); + assert_eq!( + as_pointer_bigint(&borrowed).unwrap().get_u64().1, + pointer as usize as u64 + ); + assert!(!allocation.released()); + } + #[test] fn bstr_values_are_not_apartment_bound() { let text = "embedded\0nul \u{1f642}"; @@ -4965,6 +5504,23 @@ mod tests { ) .is_err()); + let initialized_zeroed = + DynCom::create_native_struct(TEST_INITIALIZED_POD_DESCRIPTOR.into(), None).unwrap(); + assert_eq!( + initialized_zeroed.bytes.as_slice(), + &[8, 0, 0, 0, 0, 0, 0, 0] + ); + assert!(DynCom::create_native_struct( + TEST_INITIALIZED_POD_DESCRIPTOR.into(), + Some(Buffer::from(vec![0; 8])) + ) + .is_err()); + assert!(DynCom::create_native_struct( + TEST_INITIALIZED_POD_DESCRIPTOR.into(), + Some(Buffer::from(vec![8, 0, 0, 0, 1, 0, 0, 0])) + ) + .is_ok()); + let branded = DynComNativeStruct { descriptor: TEST_POD_DESCRIPTOR.into(), bytes: vec![1, 2, 3, 4, 5, 6, 7, 8], diff --git a/bindings/js/src/lib.rs b/bindings/js/src/lib.rs index 19b82ddb..78037eae 100644 --- a/bindings/js/src/lib.rs +++ b/bindings/js/src/lib.rs @@ -887,6 +887,17 @@ impl DynWinRTValue { ))), None, ), + dynwinrt::com::Value::StatStg(value) => Self( + dynwinrt::WinRTValue::Null, + None, + com::PointerProvenance::None, + None, + None, + Some(com::AutomationValue::new(dynwinrt::com::Value::StatStg( + value, + ))), + None, + ), dynwinrt::com::Value::Buffer(value) => Self( dynwinrt::WinRTValue::Null, None, diff --git a/crates/dynwinrt/src/call.rs b/crates/dynwinrt/src/call.rs index 16bd9b0b..3778cb91 100644 --- a/crates/dynwinrt/src/call.rs +++ b/crates/dynwinrt/src/call.rs @@ -516,6 +516,8 @@ fn call_method_dynamic_impl( std::collections::BTreeMap::>::new(); let mut excep_info_values = std::collections::BTreeMap::::new(); + let mut stat_stg_out_values = + std::collections::BTreeMap::::new(); let mut optional_out_requests: Vec> = Vec::with_capacity(out_count); // Array storage: Box'd for pointer stability (addresses don't change after creation) @@ -765,6 +767,19 @@ fn call_method_dynamic_impl( excep_info_out_values.insert(p.value_index, value); array_out_map.push(None); fill_array_map.push(None); + } else if p.typ.is_stat_stg() { + let mut value = crate::com::StatStgOutput::new(); + out_ptrs.push(value.as_mut_ptr().cast()); + out_values.push(AbiValue::Pointer(std::ptr::null_mut())); + struct_out_values.push(None); + guid_out_values.push(None); + native_struct_out_values.push(None); + variant_out_values.push(None); + safe_array_out_values.push(None); + prop_variant_out_values.push(None); + stat_stg_out_values.insert(p.value_index, value); + array_out_map.push(None); + fill_array_map.push(None); } else if p.typ.is_struct() { let val = if p.is_in_out() { args.get_value(p.input_index.expect("in/out input index")) @@ -1290,6 +1305,15 @@ fn call_method_dynamic_impl( result_values.push(NativeCallValue::PropVariant(value)); } else if let Some(value) = excep_info_values.remove(&p.value_index) { result_values.push(NativeCallValue::ExcepInfo(value)); + } else if let Some(value) = stat_stg_out_values.remove(&p.value_index) { + result_values.push(NativeCallValue::StatStg(value.into_value().map_err( + |error| { + windows_core::Error::new( + windows_core::HRESULT(0x80070057u32 as i32), + &error.message(), + ) + }, + )?)); } else if let Some(struct_val) = struct_out_values[p.value_index].take() { result_values.push(NativeCallValue::WinRt(WinRTValue::Struct(struct_val))); } else { diff --git a/crates/dynwinrt/src/com.rs b/crates/dynwinrt/src/com.rs index 3c67d710..2a2fdaf2 100644 --- a/crates/dynwinrt/src/com.rs +++ b/crates/dynwinrt/src/com.rs @@ -11,9 +11,9 @@ use std::{ use windows::Win32::System::Com::{ CLSCTX_INPROC_SERVER, COINIT_APARTMENTTHREADED, COINIT_MULTITHREADED, CoCreateInstance, - CoInitializeEx, CoUninitialize, + CoGetClassObject, CoGetMalloc, CoInitializeEx, CoUninitialize, }; -use windows_core::{GUID, IUnknown, Interface as WindowsInterface}; +use windows_core::{GUID, IUnknown, Interface as WindowsInterface, PCWSTR}; use crate::{ MetadataTable, TypeHandle, TypeKind, WinRTValue, @@ -180,6 +180,7 @@ pub struct NativeStructLayout { size: usize, alignment: usize, fields: Vec, + size_field_offsets: Vec, } impl NativeStructLayout { @@ -257,9 +258,68 @@ impl NativeStructLayout { size, alignment, fields, + size_field_offsets: Vec::new(), }) } + pub fn with_size_field_initializer(mut self, field_name: &str) -> result::Result { + u32::try_from(self.size) + .map_err(|_| invalid_argument("native struct size exceeds u32 initializer"))?; + let field = self + .fields + .iter() + .find(|field| field.name == field_name) + .ok_or_else(|| { + invalid_argument(format!( + "native struct `{}` has no size field `{field_name}`", + self.name + )) + })?; + if field.count != 1 || field.typ != NativeStructFieldType::Scalar(NativeStructScalar::U32) { + return Err(invalid_argument(format!( + "native struct `{}` size field `{field_name}` must be one u32", + self.name + ))); + } + let end = field + .offset + .checked_add(size_of::()) + .ok_or_else(|| invalid_argument("native struct size field offset overflow"))?; + if end > self.size { + return Err(invalid_argument(format!( + "native struct `{}` size field `{field_name}` extends past {} bytes", + self.name, self.size + ))); + } + self.size_field_offsets.push(field.offset); + Ok(self) + } + + fn initialize_bytes(&self, bytes: &mut [u8]) { + let size = u32::try_from(self.size).expect("validated native struct size initializer"); + for offset in &self.size_field_offsets { + bytes[*offset..*offset + size_of::()].copy_from_slice(&size.to_ne_bytes()); + } + } + + fn validate_bytes(&self, bytes: &[u8]) -> result::Result<()> { + let expected = u32::try_from(self.size).expect("validated native struct size initializer"); + for offset in &self.size_field_offsets { + let actual = u32::from_ne_bytes( + bytes[*offset..*offset + size_of::()] + .try_into() + .expect("validated native struct size field range"), + ); + if actual != expected { + return Err(invalid_argument(format!( + "native struct `{}` size field must be {}, received {actual}", + self.name, self.size + ))); + } + } + Ok(()) + } + pub const fn size(&self) -> usize { self.size } @@ -312,14 +372,14 @@ impl NativeStructValue { bytes.len() ))); } + layout.validate_bytes(&bytes)?; Ok(Self { layout, bytes }) } pub fn zeroed(layout: Arc) -> Self { - Self { - bytes: vec![0; layout.size], - layout, - } + let mut bytes = vec![0; layout.size]; + layout.initialize_bytes(&mut bytes); + Self { bytes, layout } } pub fn layout(&self) -> &Arc { @@ -331,6 +391,143 @@ impl NativeStructValue { } } +#[repr(C)] +struct RawStatStg { + name: *mut u16, + stream_type: u32, + size: u64, + modified_time: u64, + created_time: u64, + accessed_time: u64, + mode: u32, + locks_supported: u32, + clsid: GUID, + state_bits: u32, + reserved: u32, +} + +const _: [(); 8] = [(); align_of::()]; +#[cfg(target_pointer_width = "32")] +const _: [(); 72] = [(); size_of::()]; +#[cfg(target_pointer_width = "64")] +const _: [(); 80] = [(); size_of::()]; + +pub(crate) struct StatStgOutput { + raw: Box, +} + +impl StatStgOutput { + pub(crate) fn new() -> Self { + Self { + raw: Box::new(unsafe { std::mem::zeroed() }), + } + } + + pub(crate) fn as_mut_ptr(&mut self) -> *mut c_void { + (&mut *self.raw as *mut RawStatStg).cast() + } + + pub(crate) fn into_value(mut self) -> result::Result { + let name = if self.raw.name.is_null() { + None + } else { + let value = unsafe { PCWSTR(self.raw.name).to_string() } + .map_err(|_| invalid_argument("STATSTG name is not valid UTF-16"))?; + self.free_name(); + Some(value) + }; + Ok(StatStgValue { + name, + stream_type: self.raw.stream_type, + size: self.raw.size, + modified_time: self.raw.modified_time, + created_time: self.raw.created_time, + accessed_time: self.raw.accessed_time, + mode: self.raw.mode, + locks_supported: self.raw.locks_supported, + clsid: self.raw.clsid, + state_bits: self.raw.state_bits, + }) + } + + fn free_name(&mut self) { + if self.raw.name.is_null() { + return; + } + unsafe { + windows::Win32::System::Com::CoTaskMemFree(Some(self.raw.name.cast())); + } + #[cfg(test)] + STATSTG_TEST_FREES.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + self.raw.name = std::ptr::null_mut(); + } +} + +impl Drop for StatStgOutput { + fn drop(&mut self) { + self.free_name(); + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct StatStgValue { + name: Option, + stream_type: u32, + size: u64, + modified_time: u64, + created_time: u64, + accessed_time: u64, + mode: u32, + locks_supported: u32, + clsid: GUID, + state_bits: u32, +} + +impl StatStgValue { + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + pub const fn stream_type(&self) -> u32 { + self.stream_type + } + + pub const fn size(&self) -> u64 { + self.size + } + + pub const fn modified_time(&self) -> u64 { + self.modified_time + } + + pub const fn created_time(&self) -> u64 { + self.created_time + } + + pub const fn accessed_time(&self) -> u64 { + self.accessed_time + } + + pub const fn mode(&self) -> u32 { + self.mode + } + + pub const fn locks_supported(&self) -> u32 { + self.locks_supported + } + + pub const fn clsid(&self) -> GUID { + self.clsid + } + + pub const fn state_bits(&self) -> u32 { + self.state_bits + } +} + +#[cfg(test)] +static STATSTG_TEST_FREES: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0); + #[derive(Debug, Clone, PartialEq, Eq)] pub enum NativeUnionFieldType { Scalar(NativeStructScalar), @@ -543,6 +740,7 @@ pub enum Value { PropVariant(PropVariantValue), DispatchParams(DispatchParamsValue), ExcepInfo(ExcepInfoValue), + StatStg(StatStgValue), Buffer(ComBufferValue), } @@ -558,6 +756,7 @@ fn is_null_input_value(value: &Value) -> bool { | Value::PropVariant(_) | Value::DispatchParams(_) | Value::ExcepInfo(_) + | Value::StatStg(_) | Value::Buffer(_) => false, } } @@ -1243,7 +1442,8 @@ impl BufferElementPlan { | ParameterType::SafeArray { .. } | ParameterType::PropVariant | ParameterType::DispatchParams - | ParameterType::ExcepInfo => { + | ParameterType::ExcepInfo + | ParameterType::StatStg => { return Err(invalid_argument( "Automation buffer elements require dedicated ownership and cleanup plans", )); @@ -1439,6 +1639,13 @@ impl Type { } } + pub fn stat_stg() -> Self { + Self { + abi: ParameterType::stat_stg(), + pointer_output: PointerOutputKind::None, + } + } + fn pointer_with_output(pointer_output: PointerOutputKind) -> Self { Self { abi: ParameterType::pointer(), @@ -2096,7 +2303,8 @@ impl ComCallPlan { | Value::SafeArray(_) | Value::PropVariant(_) | Value::DispatchParams(_) - | Value::ExcepInfo(_) => Err(invalid_argument( + | Value::ExcepInfo(_) + | Value::StatStg(_) => Err(invalid_argument( "COM-local result requires the COM value invocation path", )), Value::Buffer(_) => Err(invalid_argument( @@ -2279,7 +2487,8 @@ impl ComCallPlan { | Value::SafeArray(_) | Value::PropVariant(_) | Value::DispatchParams(_) - | Value::ExcepInfo(_) => Err(invalid_argument( + | Value::ExcepInfo(_) + | Value::StatStg(_) => Err(invalid_argument( "COM-local value passed to a scalar COM method", )), Value::Buffer(_) => Err(invalid_argument( @@ -3736,6 +3945,24 @@ fn validate_automation_contracts( { return Err(invalid_argument("EXCEPINFO is output-only")); } + if parameter.typ.abi.is_stat_stg() + && !matches!( + parameter.direction, + ComParameterDirection::Out | ComParameterDirection::OptionalOut + ) + { + return Err(invalid_argument("STATSTG is output-only")); + } + if parameter.typ.abi.is_stat_stg() + && !matches!( + return_plan, + ComReturnPlan::HResult | ComReturnPlan::SemanticHResult + ) + { + return Err(invalid_argument( + "STATSTG outputs require an HRESULT return convention", + )); + } if parameter.typ.abi.is_excep_info() && !matches!( return_plan, @@ -4364,6 +4591,93 @@ pub fn co_create_instance(clsid: GUID, iid: GUID) -> result::Result Ok(WinRTValue::Object(unsafe { IUnknown::from_raw(result) })) } +pub fn co_get_class_object(clsid: GUID, iid: GUID) -> result::Result { + let unknown: IUnknown = unsafe { CoGetClassObject(&clsid, CLSCTX_INPROC_SERVER, None) } + .map_err(result::Error::WindowsError)?; + let mut result = std::ptr::null_mut(); + unsafe { unknown.query(&iid, &mut result) } + .ok() + .map_err(result::Error::WindowsError)?; + Ok(WinRTValue::Object(unsafe { IUnknown::from_raw(result) })) +} + +pub fn co_get_malloc() -> result::Result { + let allocator = unsafe { CoGetMalloc(1) }.map_err(result::Error::WindowsError)?; + Ok(WinRTValue::Object(allocator.into())) +} + +pub fn create_error_info() -> result::Result { + windows_link::link!("oleaut32.dll" "system" fn CreateErrorInfo( + error_info: *mut *mut c_void + ) -> windows_core::HRESULT); + + let mut error_info = std::ptr::null_mut(); + unsafe { CreateErrorInfo(&mut error_info) } + .ok() + .map_err(result::Error::WindowsError)?; + if error_info.is_null() { + return Err(invalid_argument( + "CreateErrorInfo succeeded without returning an interface", + )); + } + Ok(unsafe { adopt_com_pointer(error_info) }) +} + +pub fn set_error_info(value: Option<&WinRTValue>) -> result::Result<()> { + windows_link::link!("oleaut32.dll" "system" fn SetErrorInfo( + reserved: u32, + error_info: *mut c_void + ) -> windows_core::HRESULT); + + let error_info = value + .map(|value| { + let unknown = value + .as_object() + .ok_or_else(|| invalid_argument("SetErrorInfo requires a COM object"))?; + let mut error_info = std::ptr::null_mut(); + unsafe { + unknown.query( + &GUID::from_u128(0x1cf2b120_547d_101b_8e65_08002b2bd119), + &mut error_info, + ) + } + .ok() + .map_err(result::Error::WindowsError)?; + Ok::(unsafe { IUnknown::from_raw(error_info) }) + }) + .transpose()?; + unsafe { + SetErrorInfo( + 0, + error_info + .as_ref() + .map_or(std::ptr::null_mut(), |value| value.as_raw()), + ) + } + .ok() + .map_err(result::Error::WindowsError) +} + +pub fn get_error_info() -> result::Result> { + windows_link::link!("oleaut32.dll" "system" fn GetErrorInfo( + reserved: u32, + error_info: *mut *mut c_void + ) -> windows_core::HRESULT); + + let mut error_info = std::ptr::null_mut(); + let status = unsafe { GetErrorInfo(0, &mut error_info) }; + if status == windows_core::HRESULT(1) { + return Ok(None); + } + status.ok().map_err(result::Error::WindowsError)?; + if error_info.is_null() { + return Err(invalid_argument( + "GetErrorInfo succeeded without returning an interface", + )); + } + Ok(Some(unsafe { adopt_com_pointer(error_info) })) +} + /// Adopt an AddRef-owned COM interface pointer into a managed Object value. /// /// The pointer must represent a caller-owned COM reference (+1). This function @@ -4468,7 +4782,7 @@ mod tests { ApplicationModel::DataTransfer::DataTransferManager, System::Threading::{ThreadPool, WorkItemHandler}, Win32::{ - System::Com::{CoGetMalloc, IMalloc, IPersistFile, IStream}, + System::Com::{CoGetMalloc, CreateBindCtx, IBindCtx, IMalloc, IPersistFile, IStream}, System::WinRT::{RO_INIT_MULTITHREADED, RoInitialize}, UI::Shell::{IDataTransferManagerInterop, SHCreateMemStream}, UI::WindowsAndMessaging::{ @@ -4740,6 +5054,68 @@ mod tests { windows_core::HRESULT(0) } + unsafe fn set_stat_stg_name(output: *mut RawStatStg, units: &[u16]) { + let bytes = units.len() * size_of::(); + let name = unsafe { windows::Win32::System::Com::CoTaskMemAlloc(bytes) }.cast::(); + assert!(!name.is_null()); + unsafe { + std::ptr::copy_nonoverlapping(units.as_ptr(), name, units.len()); + (*output).name = name; + } + } + + unsafe extern "system" fn write_stat_stg( + _this: *mut c_void, + output: *mut RawStatStg, + flags: u32, + ) -> windows_core::HRESULT { + unsafe { + if flags == 0 { + set_stat_stg_name( + output, + &[b't' as u16, b'e' as u16, b's' as u16, b't' as u16, 0], + ); + } + (*output).stream_type = 2; + (*output).size = 1234; + (*output).modified_time = 11; + (*output).created_time = 12; + (*output).accessed_time = 13; + (*output).mode = 0x20; + (*output).locks_supported = 0x40; + (*output).clsid = GUID::from_u128(0x11111111_2222_3333_4444_555555555555); + (*output).state_bits = 0x80; + } + windows_core::HRESULT(0) + } + + unsafe extern "system" fn write_stat_stg_name_despite_noname( + _this: *mut c_void, + output: *mut RawStatStg, + _flags: u32, + ) -> windows_core::HRESULT { + unsafe { set_stat_stg_name(output, &[b'x' as u16, 0]) }; + windows_core::HRESULT(0) + } + + unsafe extern "system" fn write_stat_stg_then_fail( + _this: *mut c_void, + output: *mut RawStatStg, + _flags: u32, + ) -> windows_core::HRESULT { + unsafe { set_stat_stg_name(output, &[b'f' as u16, 0]) }; + windows_core::HRESULT(0x80004005u32 as i32) + } + + unsafe extern "system" fn write_invalid_stat_stg_name( + _this: *mut c_void, + output: *mut RawStatStg, + _flags: u32, + ) -> windows_core::HRESULT { + unsafe { set_stat_stg_name(output, &[0xd800, 0]) }; + windows_core::HRESULT(0) + } + unsafe extern "system" fn copy_variant_value( _this: *mut c_void, input: *const windows::Win32::System::Variant::VARIANT, @@ -5293,6 +5669,15 @@ mod tests { value: u64, } + #[repr(C)] + #[derive(Clone, Copy)] + struct TestBindOpts { + cb_struct: u32, + flags: u32, + mode: u32, + deadline: u32, + } + fn test_pod_layout(name: &str) -> Arc { Arc::new( NativeStructLayout::new( @@ -5342,6 +5727,75 @@ mod tests { unsafe { std::ptr::read_unaligned(value.bytes().as_ptr().cast::()) } } + fn test_bind_opts_layout() -> Arc { + Arc::new( + NativeStructLayout::new( + "Windows.Win32.System.Com.BIND_OPTS", + size_of::(), + align_of::(), + vec![ + NativeStructField::new( + "cbStruct", + 0, + 1, + NativeStructFieldType::Scalar(NativeStructScalar::U32), + ) + .unwrap(), + NativeStructField::new( + "grfFlags", + 4, + 1, + NativeStructFieldType::Scalar(NativeStructScalar::U32), + ) + .unwrap(), + NativeStructField::new( + "grfMode", + 8, + 1, + NativeStructFieldType::Scalar(NativeStructScalar::U32), + ) + .unwrap(), + NativeStructField::new( + "dwTickCountDeadline", + 12, + 1, + NativeStructFieldType::Scalar(NativeStructScalar::U32), + ) + .unwrap(), + ], + ) + .unwrap() + .with_size_field_initializer("cbStruct") + .unwrap(), + ) + } + + unsafe extern "system" fn read_bind_opts( + _this: *mut c_void, + value: *const TestBindOpts, + ) -> windows_core::HRESULT { + let value = unsafe { &*value }; + if value.cb_struct == size_of::() as u32 { + windows_core::HRESULT(0) + } else { + windows_core::HRESULT(0x80070057u32 as i32) + } + } + + unsafe extern "system" fn write_bind_opts( + _this: *mut c_void, + value: *mut TestBindOpts, + ) -> windows_core::HRESULT { + let value = unsafe { &mut *value }; + if value.cb_struct != size_of::() as u32 { + return windows_core::HRESULT(0x80070057u32 as i32); + } + value.flags = 7; + value.mode = 11; + value.deadline = 13; + windows_core::HRESULT(0) + } + unsafe extern "system" fn require_aligned_pod( _this: *mut c_void, value: *const AlignedPod, @@ -5818,6 +6272,17 @@ mod tests { .invoke_values((&mut object as *mut FakeComObject).cast(), args) } + fn invoke_test_stat_stg(function: *mut c_void, flags: u32) -> result::Result> { + let table = MetadataTable::new(); + invoke_test_pod( + function, + MethodSignature::new(&table) + .add_out(Type::stat_stg()) + .add_in(Type::winrt(table.u32_type())), + &[Value::WinRt(WinRTValue::U32(flags))], + ) + } + fn reset_bstr_counts() { crate::call::reset_bstr_test_counts(); BSTR_FAKE_ALLOCS.store(0, Ordering::Relaxed); @@ -7234,6 +7699,97 @@ mod tests { assert!(error.message().contains("type mismatch")); } + #[test] + fn native_struct_size_field_initializer_is_applied_and_validated() { + let layout = test_bind_opts_layout(); + let value = NativeStructValue::zeroed(layout.clone()); + assert_eq!( + u32::from_ne_bytes(value.bytes()[0..4].try_into().unwrap()), + size_of::() as u32 + ); + + let mut valid = vec![0; size_of::()]; + valid[0..4].copy_from_slice(&(size_of::() as u32).to_ne_bytes()); + NativeStructValue::new(layout.clone(), valid).unwrap(); + + let error = NativeStructValue::new(layout.clone(), vec![0; size_of::()]) + .expect_err("zero cbStruct must be rejected"); + assert!(error.message().contains("size field"), "{error:?}"); + + let table = MetadataTable::new(); + invoke_test_pod( + read_bind_opts as *mut c_void, + MethodSignature::new(&table).add_in(Type::native_struct_pointer(layout.clone())), + &[Value::NativeStruct(value.clone())], + ) + .unwrap(); + + let output = invoke_test_pod( + write_bind_opts as *mut c_void, + MethodSignature::new(&table).add_in_out(Type::native_struct(layout)), + &[Value::NativeStruct(value)], + ) + .unwrap(); + let Value::NativeStruct(output) = &output[0] else { + panic!("expected BIND_OPTS output"); + }; + let output = + unsafe { std::ptr::read_unaligned(output.bytes().as_ptr().cast::()) }; + assert_eq!( + (output.cb_struct, output.flags, output.mode, output.deadline), + (size_of::() as u32, 7, 11, 13) + ); + } + + #[test] + fn bind_ctx_get_set_options_preserves_initialized_cb_struct() -> result::Result<()> { + let _ = unsafe { RoInitialize(RO_INIT_MULTITHREADED) }; + let bind_ctx: IBindCtx = + unsafe { CreateBindCtx(0) }.map_err(result::Error::WindowsError)?; + let table = MetadataTable::new(); + let layout = test_bind_opts_layout(); + let interface = register_interface( + &table, + "Windows.Win32.System.Com.IBindCtx", + IBindCtx::IID, + InterfaceBase::IUnknown, + ) + .add_method_at( + 6, + "SetBindOptions", + MethodSignature::new(&table).add_in(Type::native_struct_pointer(layout.clone())), + )? + .add_method_at( + 7, + "GetBindOptions", + MethodSignature::new(&table).add_in_out(Type::native_struct(layout.clone())), + )?; + let initial = NativeStructValue::zeroed(layout); + let output = unsafe { + interface + .method(7) + .unwrap() + .invoke_values_with_output_kinds(bind_ctx.as_raw(), &[Value::NativeStruct(initial)]) + }?; + let Value::NativeStruct(options) = &output[0].0 else { + panic!("expected BIND_OPTS output"); + }; + assert_eq!( + u32::from_ne_bytes(options.bytes()[0..4].try_into().unwrap()), + size_of::() as u32 + ); + unsafe { + interface + .method(6) + .unwrap() + .invoke_values_with_output_kinds( + bind_ctx.as_raw(), + &[Value::NativeStruct(options.clone())], + ) + }?; + Ok(()) + } + #[test] fn native_union_runtime_requires_brand_and_active_field() { let table = MetadataTable::new(); @@ -7756,6 +8312,90 @@ mod tests { ); } + #[test] + fn stat_stg_layout_conversion_and_cleanup_are_owned() { + assert_eq!(align_of::(), 8); + assert_eq!( + size_of::(), + if cfg!(target_pointer_width = "32") { + 72 + } else { + 80 + } + ); + + STATSTG_TEST_FREES.store(0, Ordering::Relaxed); + let output = invoke_test_stat_stg(write_stat_stg as *mut c_void, 0).unwrap(); + let Value::StatStg(stat) = &output[0] else { + panic!("expected STATSTG output"); + }; + assert_eq!(stat.name(), Some("test")); + assert_eq!(stat.stream_type(), 2); + assert_eq!(stat.size(), 1234); + assert_eq!(stat.modified_time(), 11); + assert_eq!(stat.created_time(), 12); + assert_eq!(stat.accessed_time(), 13); + assert_eq!(stat.mode(), 0x20); + assert_eq!(stat.locks_supported(), 0x40); + assert_eq!( + stat.clsid(), + GUID::from_u128(0x11111111_2222_3333_4444_555555555555) + ); + assert_eq!(stat.state_bits(), 0x80); + assert_eq!(STATSTG_TEST_FREES.load(Ordering::Relaxed), 1); + drop(output); + assert_eq!(STATSTG_TEST_FREES.load(Ordering::Relaxed), 1); + + STATSTG_TEST_FREES.store(0, Ordering::Relaxed); + let output = invoke_test_stat_stg(write_stat_stg as *mut c_void, 1).unwrap(); + let Value::StatStg(stat) = &output[0] else { + panic!("expected STATSTG output"); + }; + assert_eq!(stat.name(), None); + assert_eq!(STATSTG_TEST_FREES.load(Ordering::Relaxed), 0); + + STATSTG_TEST_FREES.store(0, Ordering::Relaxed); + let output = + invoke_test_stat_stg(write_stat_stg_name_despite_noname as *mut c_void, 1).unwrap(); + let Value::StatStg(stat) = &output[0] else { + panic!("expected STATSTG output"); + }; + assert_eq!(stat.name(), Some("x")); + assert_eq!(STATSTG_TEST_FREES.load(Ordering::Relaxed), 1); + + STATSTG_TEST_FREES.store(0, Ordering::Relaxed); + let error = invoke_test_stat_stg(write_stat_stg_then_fail as *mut c_void, 0).unwrap_err(); + assert!(error.message().contains("0x80004005")); + assert_eq!(STATSTG_TEST_FREES.load(Ordering::Relaxed), 1); + + STATSTG_TEST_FREES.store(0, Ordering::Relaxed); + let error = + invoke_test_stat_stg(write_invalid_stat_stg_name as *mut c_void, 0).unwrap_err(); + assert!(error.message().contains("valid UTF-16")); + assert_eq!(STATSTG_TEST_FREES.load(Ordering::Relaxed), 1); + } + + #[test] + fn stat_stg_contract_is_output_only_hresult() { + let table = MetadataTable::new(); + let error = MethodSignature::new(&table) + .add_in(Type::stat_stg()) + .build(0) + .unwrap_err(); + assert!(error.message().contains("STATSTG is output-only")); + + let error = MethodSignature::new(&table) + .add_out(Type::stat_stg()) + .returns_void() + .build(0) + .unwrap_err(); + assert!( + error + .message() + .contains("STATSTG outputs require an HRESULT") + ); + } + #[test] fn excep_info_deferred_fill_runs_once_and_cleans_on_every_failure_path() { let table = MetadataTable::new(); @@ -9360,6 +10000,46 @@ mod tests { Ok(()) } + #[test] + fn com_p0_acquisition_returns_owned_interfaces() -> result::Result<()> { + initialize_apartment(ApartmentType::MultiThreaded)?; + + let allocator = co_get_malloc()?; + assert!(allocator.as_object().is_some()); + + let factory = co_get_class_object( + CLSID_SHELL_LINK, + GUID::from_u128(0x00000001_0000_0000_c000_000000000046), + )?; + assert!(factory.as_object().is_some()); + + let error_info = create_error_info()?; + assert!(error_info.as_object().is_some()); + Ok(()) + } + + #[test] + fn error_info_is_thread_local_and_consumed_once() -> result::Result<()> { + initialize_apartment(ApartmentType::MultiThreaded)?; + set_error_info(None)?; + assert!(get_error_info()?.is_none()); + + let created = create_error_info()?; + set_error_info(Some(&created))?; + + let other_thread = std::thread::spawn(|| -> result::Result { + initialize_apartment(ApartmentType::MultiThreaded)?; + Ok(get_error_info()?.is_none()) + }) + .join() + .expect("error-info worker must not panic")?; + assert!(other_thread); + + assert!(get_error_info()?.is_some()); + assert!(get_error_info()?.is_none()); + Ok(()) + } + #[test] fn co_create_instance_does_not_choose_an_apartment_implicitly() { let remains_uninitialized = std::thread::spawn(|| { diff --git a/crates/dynwinrt/src/native_call.rs b/crates/dynwinrt/src/native_call.rs index 9124a008..51b62479 100644 --- a/crates/dynwinrt/src/native_call.rs +++ b/crates/dynwinrt/src/native_call.rs @@ -34,6 +34,7 @@ pub(crate) enum NativeCallValue { SafeArray(crate::com::SafeArrayValue), PropVariant(crate::com::PropVariantValue), ExcepInfo(crate::com::ExcepInfoValue), + StatStg(crate::com::StatStgValue), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -67,6 +68,7 @@ pub(crate) enum ParameterType { PropVariant, DispatchParams, ExcepInfo, + StatStg, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -183,6 +185,10 @@ impl ParameterType { Self::ExcepInfo } + pub(crate) fn stat_stg() -> Self { + Self::StatStg + } + pub(crate) fn as_winrt(&self) -> Option<&TypeHandle> { match self { Self::WinRT(typ) => Some(typ), @@ -197,7 +203,8 @@ impl ParameterType { | Self::SafeArray { .. } | Self::PropVariant | Self::DispatchParams - | Self::ExcepInfo => None, + | Self::ExcepInfo + | Self::StatStg => None, } } @@ -214,7 +221,8 @@ impl ParameterType { | Self::SafeArray { .. } | Self::PropVariant | Self::DispatchParams - | Self::ExcepInfo => None, + | Self::ExcepInfo + | Self::StatStg => None, } } @@ -287,6 +295,10 @@ impl ParameterType { matches!(self, Self::ExcepInfo) } + pub(crate) fn is_stat_stg(&self) -> bool { + matches!(self, Self::StatStg) + } + pub(crate) fn is_array(&self) -> bool { self.as_winrt().is_some_and(TypeHandle::is_array) } @@ -350,7 +362,8 @@ impl ParameterType { | Self::SafeArray { .. } | Self::PropVariant | Self::DispatchParams - | Self::ExcepInfo => AbiType::Ptr, + | Self::ExcepInfo + | Self::StatStg => AbiType::Ptr, Self::NativeStruct(_) | Self::VariantByValue => { panic!("aggregate values do not have a scalar AbiType") } @@ -369,7 +382,8 @@ impl ParameterType { | Self::SafeArray { .. } | Self::PropVariant | Self::DispatchParams - | Self::ExcepInfo => libffi::middle::Type::pointer(), + | Self::ExcepInfo + | Self::StatStg => libffi::middle::Type::pointer(), Self::NativeStruct(layout) => layout.libffi_type(), Self::VariantByValue => variant_by_value_libffi_type(), } @@ -401,7 +415,8 @@ impl ParameterType { | Self::SafeArray { .. } | Self::PropVariant | Self::DispatchParams - | Self::ExcepInfo => { + | Self::ExcepInfo + | Self::StatStg => { panic!("native POD storage is allocated by the dynamic executor") } } @@ -421,7 +436,8 @@ impl ParameterType { | Self::SafeArray { .. } | Self::PropVariant | Self::DispatchParams - | Self::ExcepInfo => { + | Self::ExcepInfo + | Self::StatStg => { unreachable!("native POD output conversion uses NativeStructValue") } } @@ -456,7 +472,8 @@ impl ParameterType { | Self::SafeArray { .. } | Self::PropVariant | Self::DispatchParams - | Self::ExcepInfo, + | Self::ExcepInfo + | Self::StatStg, _, ) => { unreachable!("native POD output conversion uses NativeStructValue") @@ -474,6 +491,7 @@ impl ParameterType { Self::SafeArray { .. } => OutputCleanup::SafeArrayDestroy, Self::PropVariant => OutputCleanup::PropVariantClear, Self::ExcepInfo => OutputCleanup::None, + Self::StatStg => OutputCleanup::None, Self::Bstr { .. } => OutputCleanup::BstrFree, Self::CoTaskMemWideString => OutputCleanup::CoTaskMemFree, Self::WinRT(_) @@ -742,6 +760,7 @@ impl AbiMethodSignature { || p.typ.is_prop_variant() || p.typ.is_dispatch_params() || p.typ.is_excep_info() + || p.typ.is_stat_stg() }); // Check if the single in-param (if any) is a simple non-HString, non-Struct type @@ -1274,7 +1293,8 @@ impl call::ArgumentList for ComInvocationArgs<'_> { | crate::com::Value::SafeArray(_) | crate::com::Value::PropVariant(_) | crate::com::Value::DispatchParams(_) - | crate::com::Value::ExcepInfo(_) => { + | crate::com::Value::ExcepInfo(_) + | crate::com::Value::StatStg(_) => { panic!("COM-local argument requested as a WinRT value") } crate::com::Value::Buffer(_) => { @@ -1365,6 +1385,7 @@ impl Method { || parameter.typ.is_prop_variant() || parameter.typ.is_dispatch_params() || parameter.typ.is_excep_info() + || parameter.typ.is_stat_stg() }) || self.direct_return_type().is_some_and(|typ| { typ.native_struct_layout().is_some() || typ.native_union_layout().is_some() @@ -1375,6 +1396,7 @@ impl Method { || typ.is_prop_variant() || typ.is_dispatch_params() || typ.is_excep_info() + || typ.is_stat_stg() }) } @@ -1846,7 +1868,8 @@ impl Method { NativeCallValue::Variant(_) | NativeCallValue::SafeArray(_) | NativeCallValue::PropVariant(_) - | NativeCallValue::ExcepInfo(_) => Err(invalid_argument( + | NativeCallValue::ExcepInfo(_) + | NativeCallValue::StatStg(_) => Err(invalid_argument( "COM-local result reached the WinRT invocation path", )), }) @@ -2088,6 +2111,7 @@ impl Method { NativeCallValue::SafeArray(value) => crate::com::Value::SafeArray(value), NativeCallValue::PropVariant(value) => crate::com::Value::PropVariant(value), NativeCallValue::ExcepInfo(value) => crate::com::Value::ExcepInfo(value), + NativeCallValue::StatStg(value) => crate::com::Value::StatStg(value), }) .collect() }) diff --git a/docs/architecture/classic-com-support.md b/docs/architecture/classic-com-support.md index 861a0a9d..784f0584 100644 --- a/docs/architecture/classic-com-support.md +++ b/docs/architecture/classic-com-support.md @@ -320,10 +320,9 @@ The implemented Automation subset now provides: Complete inherited `IDispatch` now projects from real metadata. `GetIDsOfNames` retains its natural shared-count array surface, and `Invoke` takes `DynComDispatchParams` plus explicit LCID, flags, IID, and optional-output -request options. XML Automation, Task Scheduler, and `IPropertyStore` still -stop at their own unsupported BYREF/InOut, nested ownership, or `PROPERTYKEY` -contracts; support for `IDispatch` inheritance does not imply those derived -interfaces are complete. +request options. XML Automation and Task Scheduler still stop at their own +unsupported BYREF/InOut or nested-ownership contracts; support for `IDispatch` +inheritance does not imply those derived interfaces are complete. ### 6. SAFEARRAY @@ -578,8 +577,8 @@ and `@microsoft/dynwinrt/com`. | Allocator ownership | COM Release, BSTR output/replacement and array elements, VARIANT clear, CoTaskMem buffers/PWSTR elements, boxed GUID, retained JS buffers | LocalFree, custom allocators, allocator interfaces, unknown ownership | | Interface pointers | Typed input/output interfaces, QueryInterface, dynamic IID output | Interface in/out replacement and arbitrary implemented sink interfaces | | Apartments | Explicit initialization and same-thread invocation | Cross-apartment marshaling, GIT/agility handling, callback dispatch | -| Activation | In-process `CoCreateInstance` | `CoGetClassObject`, aggregation, arbitrary CLSCTX, and non-CoCreate factory functions | -| Direct pointer returns | Runtime signature supports them | Metadata codegen does not yet preserve raw-pointer direct-return semantics, so `IMalloc` generation fails closed | +| Activation | In-process `CoCreateInstance` and `CoGetClassObject` | Aggregation, arbitrary CLSCTX, and other non-CoCreate factory functions | +| Direct pointer returns | Runtime signature plus exact `IMalloc` codegen | Other direct pointer returns remain fail-closed without exact ownership and cleanup evidence | ### Not implemented @@ -606,7 +605,7 @@ and `@microsoft/dynwinrt/com`. | Semantic `HRESULT` methods | Supported | `CanReturnMultipleSuccessValuesAttribute` preserves successful values such as `S_OK` and `S_FALSE`; failed values still become errors. | | Native `void` returns | Supported | Used by interfaces such as `IMalloc`. | | Direct scalar returns | Supported | Includes signed/unsigned integers, floating point values, and enums. | -| Direct pointer returns | Runtime supported; codegen partial | The runtime can describe a pointer return explicitly. Metadata codegen currently fails closed for interfaces such as `IMalloc` because it does not preserve the raw-pointer return kind. | +| Direct pointer returns | Exact-contract support | `IMalloc` returns opaque allocator-bound values. Other direct pointer returns fail closed until ownership and cleanup are proven. | | `[in]`, `[out]`, and `[in, out]` parameters | Supported for modeled types | Scalars and validated native POD storage are supported. Other composite in/out types fail generation. | | Primitive integer and floating-point types | Supported | `i8` through `u64`, `f32`, `f64`, `BOOL`, and `HRESULT`. | | `ISize` / `USize` | Supported | Projected with the target pointer width; verified by an i686 compile check. | @@ -663,7 +662,7 @@ of every type in the 24 MB metadata file. | Unsupported `VARIANT` alternatives, aggregate directions, and BYREF/InOut | Automation APIs | Required input-only by-value VARIANT is supported. Optional aggregate defaults, bare aggregate output/InOut, DATE, DECIMAL, CY, ERROR, RECORD, unsupported flags, and every BYREF/InOut combination fail closed until their lifetime/replacement contracts are proven. | Runtime validation + Win32 winmd signatures | | Unsupported `DISPPARAMS` / `EXCEPINFO` shapes | Automation APIs outside exact `IDispatch::Invoke` | Output/InOut DISPPARAMS, input/InOut EXCEPINFO, nested compounds, reinstalled deferred callbacks, and unrelated function-pointer contracts fail closed. | Runtime validation + Win32 winmd signature | | Unsupported `PROPVARIANT` alternatives | Property System | Streams/interfaces, arrays, clipboard/storage alternatives, nested VT_VECTOR\|VT_VARIANT, BYREF, and unknown combinations are rejected. | Runtime validation + Win32 winmd signature | -| `PROPERTYKEY` and native structs outside the POD subset | `IPropertyStore::GetAt` | Every architecture-specific layout and nested field contract must reach the validated POD model; full `IPropertyStore` still fails closed before a complete interface can be emitted even though scalar PROPVARIANT storage is implemented. | Win32 winmd + codegen diagnostic | +| Native structs with nested owned pointers outside dedicated contracts | Storage and Shell APIs | `STATSTG` has a dedicated output-only model that adopts and frees its CoTaskMem name on every success and failure path. Arbitrary nested pointer structs still fail closed. | Runtime ownership tests + Win32 winmd signature | | Unsupported `SAFEARRAY` shapes | Automation and Office-style COM APIs | Exact declaration-registry entries support documented `VT_I4`, `VT_UI1`, `VT_UI4`, `VT_R8`, `VT_BSTR`, `VT_VARIANT`, and `VT_UNKNOWN` plus an exact interface IID. Unknown VARTYPE, signature drift, input `SAFEARRAY**`, InOut replacement, unsupported records/dispatch contracts, rank > 8, inconsistent bounds/length/element width, and unproven nullable outputs are rejected. | Exact Microsoft citations + SafeArray API validation + Win32 winmd signatures | | `FORMATETC` / `STGMEDIUM` | `IDataObject`, clipboard, drag-and-drop | `STGMEDIUM` is a union of handles and interfaces with type-specific release behavior. | Win32 winmd + codegen diagnostic | | Untagged/by-value/output/nested unions, bitfields, flexible arrays, and nested owned-resource structs | `STGMEDIUM`, `STRRET`, `BINDPTR`, audio/media formats | Tagged pointer-input unions support only safely POD fields. Missing discriminants, nested unions, BSTR/interfaces/resources, bitfields, and flexible tails fail closed. | Win32 winmd + codegen diagnostics | @@ -679,10 +678,10 @@ of every type in the 24 MB metadata file. | General out-of-process activation controls | Custom `CLSCTX` scenarios | The unsafe runtime's `DynCom.coCreateInstance()` currently uses `CLSCTX_INPROC_SERVER`. | Runtime/public-API boundary | | Flat Win32 DLL exports | `CreateFile`, registry functions, GDI, etc. | These are not COM interfaces and need a separate DLL-export/handle model. | Architecture boundary | -Consequently, `IPropertyStore` and `IDataObject` remain important, -widely encountered interfaces that are not currently supported as complete -generated bindings. `IDispatch` itself is complete; derived Automation -interfaces still validate all of their additional methods independently. +Consequently, `IDataObject` remains an important, widely encountered interface +that is not currently supported as a complete generated binding. +`IPropertyStore` and `IDispatch` are complete; derived Automation interfaces +still validate all of their additional methods independently. ## Complete-interface census after by-value VARIANT @@ -1181,12 +1180,12 @@ contracts and therefore add no complete-interface census entries. After removing enum-name ownership inference, requiring distinct actual-length parameters to be exact Out values, and separating counted character pointers from terminated strings, the exact final literal census is -**5,560 / 7,929 = 70.122336%**. The result remains above the 70% target without +**5,567 / 7,929 = 70.210619%**. The result remains above the 70% target without admitting any creator-owned, destroyable, InOut, undocumented, optional, or `HWND**` shape. CI reproduces this number with `dynwinrt-codegen com-census --json` and fails -if the denominator changes, complete generation drops below 5,560, or coverage +if the denominator changes, complete generation drops below 5,567, or coverage falls below 70%. ## Public-code frequency snapshot @@ -1235,8 +1234,8 @@ against the resolved namespace. | 1 | `ID3D11Device` | 27,552 | 87 | Yes | Fail closed: untyped output ownership | | 2 | `IDXGIFactory` | 17,432 | 83 | Yes | Fail closed: untyped output ownership | | 3 | `IDataObject` | 10,648 | 44 | Yes | Fail closed: `STGMEDIUM` has unmodeled discriminant/resource ownership | -| 4 | `IMalloc` | 10,624 | 56 | Yes | Fail closed: direct raw-pointer return mapping; runtime tested | -| 5 | `IClassFactory` | 6,712 | 70 | Yes | Generates; acquisition helper and live test still needed | +| 4 | `IMalloc` | 10,624 | 56 | Yes | Generates completely and is live-tested through `CoGetMalloc` | +| 5 | `IClassFactory` | 6,712 | 70 | Yes | Generates completely and is live-tested through `CoGetClassObject` | | 6 | `IDispatch` via `IID_IDispatch` | 6,408 | 46 | Yes | Complete inherited interface generates; `Invoke` uses dedicated DISPPARAMS/EXCEPINFO and explicit optional-output requests | | 7 | `IPersistFile` | 5,996 | 97 | Yes | Generates and live-tested | | 8 | `IConnectionPoint` | 5,832 | 51 | Yes | Generates; implementing event sinks is not supported | @@ -1248,11 +1247,11 @@ against the resolved namespace. | 14 | `IXMLDOMDocument` | 3,784 | 46 | Yes | Fail closed: inherited unsupported Automation shapes beyond scalar VARIANT | | 15 | `ID2D1Factory` | 3,752 | 92 | Yes | Generates; requires flat factory acquisition and native input structs | | 16 | `IDWriteFactory` | 3,712 | 76 | Yes | Generates; requires flat factory acquisition | -| 17 | `IStream` via `IID_IStream` | 3,560 | 41 | Yes | Fail closed on `STATSTG`; safe runtime subset is live-tested | -| 18 | `IPropertyStore` | 3,400 | 77 | Yes | Fail closed: `PROPERTYKEY`; PROPVARIANT runtime subset exists | +| 17 | `IStream` via `IID_IStream` | 3,560 | 41 | Yes | Generates completely; WIC live coverage exercises inherited buffers, seek, `STATSTG`, and Clone HRESULT propagation | +| 18 | `IPropertyStore` | 3,400 | 77 | Yes | Generates completely and is live-tested with an unsaved ShellLink | | 19 | `IShellItem` | 3,028 | 76 | Yes | Generates; acquisition/live test still needed | | 20 | `IMMDeviceEnumerator` | 2,932 | 83 | Yes | Generates; live result depends on audio services/devices | -| 21 | `IBindCtx` | 2,660 | 42 | Yes | Fails closed until `BIND_OPTS.cbStruct` initialization is modeled | +| 21 | `IBindCtx` | 2,660 | 42 | Yes | Generates with exact `BIND_OPTS.cbStruct = sizeof(BIND_OPTS)` initialization and live `CreateBindCtx` coverage | | 22 | `IFileOpenDialog` | 2,536 | 92 | Yes | Generates and live-tested without showing UI | | 23 | `IRunningObjectTable` | 2,532 | 50 | Yes | Generates with POD `FILETIME`; acquisition/live test still needed | | 24 | `IAudioClient` | 2,500 | 82 | Yes | Fail closed: format pointer/output ownership | @@ -1267,28 +1266,29 @@ against the resolved namespace. - **29 of 30** candidates are defined as `IUnknown`-rooted interfaces in Windows.Win32.winmd. `ICoreWebView2` is the only external-metadata case. -- **19 of 29** Win32-metadata candidates pass complete codegen validation. - **10 of 29** fail closed on an unsupported ABI or ownership shape. +- **23 of 29** Win32-metadata candidates pass complete codegen validation. + **6 of 29** fail closed on an unsupported ABI or ownership shape. - Among the **top 10** by `.cpp` hits, only `IClassFactory`, - `IDispatch`, `IPersistFile`, `IConnectionPoint`, and `IWICImagingFactory` - pass complete codegen. `IMalloc` has a tested runtime path but not a complete - generated interface. + `IDispatch`, `IPersistFile`, `IConnectionPoint`, `IWICImagingFactory`, and + `IMalloc` pass complete codegen. - The largest unsupported demand clusters are: - discriminated resource unions and non-POD layout (`IDataObject`, parts of Shell and streams); - Automation contracts beyond the exact supported `IDispatch` compounds (XML and Task Scheduler BYREF/InOut and nested ownership); - explicit output ownership (`DXGI`, audio); - interface in/out semantics (WMI); and - - remaining Property System types (`PROPERTYKEY` and unsupported PROPVARIANT alternatives). -- Six frequency-survey candidates have generated live coverage: + - unsupported PROPVARIANT alternatives in Property System APIs beyond + `IPropertyStore`. +- Ten frequency-survey candidates have generated live coverage: `IPersistFile`, `IWICImagingFactory`, `IFileDialog` through - `FileOpenDialog`, `TaskbarList`, `FileOperation`, and `IShellLinkW`. - `IMalloc` and `IStream` add runtime-only live coverage. + `FileOpenDialog`, `TaskbarList`, `FileOperation`, `IShellLinkW`, `IStream`, + `IPropertyStore`, `IMalloc`, and `IClassFactory`. `IBindCtx` also has live + runtime coverage. -This means the current ten-interface suite provides useful ABI breadth, but it +This means the current suite provides useful ABI breadth, but it does **not** cover every high-frequency interface. In particular, -`IDataObject`, `IPropertyStore`, graphics interfaces, WMI, audio, and live -real-object `IDispatch::Invoke` coverage remain material gaps. +`IDataObject`, graphics interfaces, WMI, audio, and live real-object +`IDispatch::Invoke` coverage remain material gaps. ## Engineering priority map @@ -1298,10 +1298,10 @@ hardware, and whether it adds a distinct ABI shape. | Interface | Typical use | Current status | |---|---|---| -| `ISequentialStream` / `IStream` | OLE streams, imaging, shell, serialization | Generated `Read`/`Write` wrappers use documented byte contracts; the core live memory-stream test covers typed input/output buffers, actual lengths, seek, and interface output. | +| `ISequentialStream` / `IStream` | OLE streams, imaging, shell, serialization | Complete generation includes documented `Read`/`Write` byte contracts and owned `STATSTG`; WIC live coverage exercises buffers, seek, stat, and Clone HRESULT propagation. | | `IOpcSignatureCustomObject` | OPC signature custom XML | `GetXml` generates as a CoTaskMem-owned callee byte buffer; acquisition is application-specific. | | `IDiscRecorder` | Legacy IMAPI recorder | Complete generation now includes the exact `GetRecorderGUID` two-call method and documentation-correct `getDisplayNames(): [string, string, string]` BSTR outputs. | -| `IMalloc` | COM task allocator | Core live test covers direct pointer, pointer-sized, scalar, and void returns. | +| `IMalloc` | COM task allocator | Complete generation is gated by exact IID/slot/shape evidence. Opaque values reject forged/stale addresses; destructive and size operations enforce allocator identity, while `DidAlloc` permits borrowed cross-allocator inspection. | | `IPersistFile` | Loading and saving persistent COM objects | Core tests query it from `IShellLinkW`; Node activates the Shell Link coclass directly as `IPersistFile` and verifies `GetClassID`. | | `IShellLinkW` | Shortcut creation and inspection | Core runtime tests cover strings, `u16`, enums, and scalar outputs; generated Node coverage round-trips `GetPath` with nested `FILETIME` and fixed WCHAR-array `WIN32_FIND_DATAW` POD storage. | | `FileOpenDialog` | Desktop file selection | Node test covers coclass construction and option round-trip without showing UI. | @@ -1310,21 +1310,21 @@ hardware, and whether it adds a distinct ABI shape. | `TaskbarList` / `ITaskbarList3` | Taskbar progress and window state | Node test covers `new`, inherited vtable slots, HWND values, BOOL, enums, `u64`, and `as`/`tryAs`/`supports`. | | `IDataTransferManagerInterop` | HWND-to-WinRT data-transfer bridge | Core and Node tests cover `IUnknown`-rooted interop and interface output. | | `ISystemMediaTransportControlsInterop` | HWND-to-WinRT media controls | Node test covers `IInspectable`-rooted interop and use of the returned WinRT object. | -| `IClassFactory` | Low-level COM activation | High-value next test; needs a public `CoGetClassObject` acquisition path. | -| `IBindCtx` / `IRunningObjectTable` | Monikers and object binding | `IRunningObjectTable` generates with validated `FILETIME`; `IBindCtx` fails closed because zero-initialized `BIND_OPTS.cbStruct` is invalid. | -| `ICreateErrorInfo` / `IErrorInfo` | COM rich error information | Good next test for GUID, wide strings, BSTR, and thread-local error state. | +| `IClassFactory` | Low-level COM activation | Complete generation and public `CoGetClassObject` acquisition are live-tested with paired server locking and owned `CreateInstance` output. | +| `IBindCtx` / `IRunningObjectTable` | Monikers and object binding | Both generate. `BIND_OPTS` carries an exact size initializer, and explicit bytes with a zero or incorrect `cbStruct` fail before native dispatch. | +| `ICreateErrorInfo` / `IErrorInfo` | COM rich error information | Complete generation and acquisition are live-tested for GUID, wide strings, owned BSTR output, thread-local storage, and one-shot consumption. | | `IMMDeviceEnumerator` | Audio endpoint discovery | Generates today, but live behavior depends on available audio endpoints. | | `IAudioClient` | Low-level audio streaming | Fails closed because its format and output-pointer shapes are not fully modeled. | | `IDispatch` | Automation and scripting | Complete inherited real-metadata generation passes. `GetIDsOfNames` projects as `string[] -> number[]`; `Invoke` accepts `DynComDispatchParams` and explicit result/excepInfo/argErr request options, returning dedicated owning wrappers. Derived Automation interfaces remain independently validated. | -| `IPropertyStore` | Shell/property metadata | The PROPVARIANT runtime subset is implemented; complete generation remains blocked by PROPERTYKEY projection and full-interface validation. | +| `IPropertyStore` | Shell/property metadata | Complete generation and live ShellLink `SetValue`/`GetValue`/`Commit` coverage pass with dedicated PROPVARIANT ownership. | | `IDataObject` | Clipboard and drag-and-drop | Unsupported until FORMATETC and STGMEDIUM are modeled. | ## Automated coverage -Eleven unique Classic COM interfaces are currently exercised. +Classic COM interfaces are exercised across core and generated Node coverage. Core live tests are in -[`crates/dynwinrt/src/com.rs`](../../crates/dynwinrt/src/com.rs). The eleven Node -runners, the counted-buffer runner, and two Automation runners (fourteen total) are in +[`crates/dynwinrt/src/com.rs`](../../crates/dynwinrt/src/com.rs). The sixteen Node +runners are in [`tests/e2e/runners/com`](../../tests/e2e/runners/com) and are generated and executed by [`tests/e2e/e2e_test.ps1`](../../tests/e2e/e2e_test.ps1). @@ -1363,9 +1363,12 @@ meaningful `argErr`, and generate an `Error` with `hresult` plus optional |---|---|---| | `IShellLinkW` | Core + Node E2E | Generated activation through its IID, wide strings, hotkeys/show command, and `GetPath` with zeroed 592-byte `WIN32_FIND_DATAW` POD storage. | | `IPersistFile` | Core + Node E2E | `QueryInterface`/direct IID activation, owned returned reference, GUID output, and deterministic release. | -| `IMalloc` | Core | Direct pointer return, `usize` return, direct `i32`, direct `void`, allocation cleanup. | -| `IStream` | Core | Typed counted byte input/output buffers, actual `u32` lengths, `i64` seek, `u64` output, and `IStream**` clone. | -| `ISequentialStream` | Node E2E | Generated caller-owned `Read` byte buffer, hidden capacity, actual-length slicing, and preserved semantic HRESULT against a stock WIC memory stream. | +| `IMalloc` | Core + Node E2E | Exact opaque allocation projection, allocator identity for ownership-sensitive operations, borrowed `DidAlloc` inspection, automatic/explicit cleanup, resize, and direct scalar/void returns. | +| `IClassFactory` | Core + Node E2E | Public `CoGetClassObject`, owned factory reference, paired `LockServer`, dynamic-IID `CreateInstance`, and +1 output adoption. | +| `ICreateErrorInfo` / `IErrorInfo` | Core + Node E2E | Public acquisition, GUID/PWSTR setters, owned BSTR getters, thread-local isolation, and consume-on-read behavior. | +| `IStream` | Core + Node E2E | Typed counted byte input/output buffers, actual `u32` lengths, `i64` seek, owned `STATSTG`, `IStream**` clone ABI, and stock-WIC Clone HRESULT propagation. | +| `IPropertyStore` | Node E2E | Generated `PROPERTYKEY` POD and owned PROPVARIANT values against an unsaved ShellLink, including `Commit`. | +| `IBindCtx` | Core + Node | Exact multi-architecture `BIND_OPTS` layout, automatic `cbStruct`, pre-dispatch validation, and live `CreateBindCtx` round trip. | | `TaskbarList` / `ITaskbarList3` | Node E2E | Coclass construction, inherited slots, runtime QI views, HWND, BOOL, enum, and `u64`. | | `FileOperation` | Node E2E | Coclass construction, unsigned flags, and state query. | | `FileOpenDialog` | Node E2E | STA coclass construction and get/set options without user interaction. | diff --git a/docs/guides/windows/classic-com-usage.md b/docs/guides/windows/classic-com-usage.md index f27f45d1..6ebcc29f 100644 --- a/docs/guides/windows/classic-com-usage.md +++ b/docs/guides/windows/classic-com-usage.md @@ -263,6 +263,9 @@ Do not treat every pointer as a Buffer: The following sources return a managed `+1` reference: - `CoCreateInstance`; +- `CoGetClassObject`; +- `CoGetMalloc`; +- `CreateErrorInfo` and a successful `GetErrorInfo`; - `QueryInterface`; - a validated interface out parameter; - the QI result returned by `DynComUnsafe.borrowComPointer()`. @@ -406,6 +409,20 @@ const object = DynComUnsafe.coCreateInstance( object.release(); ``` +`DynCom.coGetClassObject(clsid, iid)`, `DynCom.coGetMalloc()`, and +`DynCom.createErrorInfo()` expose the corresponding stock COM acquisition +paths to generated unsafe wrappers. `DynCom.getErrorInfo()` returns `null` +when the current logical thread has no error object and consumes a stored +object when one exists. + +IMalloc returns opaque `DynComAllocation` values only under the exact +`IMalloc` IID/slot/signature contract. Each value retains its originating +allocator and rejects forged or stale addresses. `free()`, `realloc()`, and +`getSize()` require that allocator identity; `didAlloc()` only borrows a live +allocation for inspection, as COM permits probing memory from another +allocator. `free()` and successful `realloc()` consume the old value; +`release()` provides deterministic cleanup. + ### 8.5 A by-value GUID is not a REFIID A by-value GUID: diff --git a/tests/e2e/e2e_test.ps1 b/tests/e2e/e2e_test.ps1 index 8cc52f61..4edf738a 100644 --- a/tests/e2e/e2e_test.ps1 +++ b/tests/e2e/e2e_test.ps1 @@ -35,6 +35,7 @@ $comInteropDir = Join-Path $comBindingsDir "interop" $comWicDir = Join-Path $comBindingsDir "wic" $comStreamDir = Join-Path $comBindingsDir "stream" $comAutomationDir = Join-Path $comBindingsDir "automation" +$comInfrastructureDir = Join-Path $comBindingsDir "infrastructure" $comSmtcDir = Join-Path $comBindingsDir "smtc" [string[]]$cargoProfileArgs = @( if ($CargoProfile -eq "release") { @@ -237,6 +238,14 @@ if ("com" -in $Lang) { --import-name $comRuntimeImport if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM persistence generation failed"; exit 1 } + & cargo run -p dynwinrt-codegen @cargoProfileArgs @cargoTargetArgs --quiet -- generate ` + --winmd $win32Winmd ` + --namespace Windows.Win32.UI.Shell.PropertiesSystem ` + --class-name IPropertyStore ` + --output $comShellDir ` + --import-name $comRuntimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM property store generation failed"; exit 1 } + & cargo run -p dynwinrt-codegen @cargoProfileArgs @cargoTargetArgs --quiet -- generate ` --winmd $win32Winmd ` --namespace Windows.Win32.System.WinRT ` @@ -256,7 +265,7 @@ if ("com" -in $Lang) { & cargo run -p dynwinrt-codegen @cargoProfileArgs @cargoTargetArgs --quiet -- generate ` --winmd $win32Winmd ` --namespace Windows.Win32.System.Com ` - --class-name ISequentialStream ` + --class-name IStream ` --output $comStreamDir ` --import-name $comRuntimeImport if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM stream generation failed"; exit 1 } @@ -268,6 +277,22 @@ if ("com" -in $Lang) { --import-name $comRuntimeImport if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM Automation generation failed"; exit 1 } + & cargo run -p dynwinrt-codegen @cargoProfileArgs @cargoTargetArgs --quiet -- generate ` + --winmd $win32Winmd ` + --namespace Windows.Win32.System.Com ` + --class-name "IMalloc,IClassFactory,IErrorInfo" ` + --output $comInfrastructureDir ` + --import-name $comRuntimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM infrastructure generation failed"; exit 1 } + + & cargo run -p dynwinrt-codegen @cargoProfileArgs @cargoTargetArgs --quiet -- generate ` + --winmd $win32Winmd ` + --namespace Windows.Win32.System.Ole ` + --class-name ICreateErrorInfo ` + --output $comInfrastructureDir ` + --import-name $comRuntimeImport + if ($LASTEXITCODE -ne 0) { Write-Error "Classic COM error-info generation failed"; exit 1 } + & cargo run -p dynwinrt-codegen @cargoProfileArgs @cargoTargetArgs --quiet -- generate ` --namespace Windows.Media ` --class-name SystemMediaTransportControls ` @@ -329,6 +354,7 @@ if ("com" -in $Lang) { "taskbarlist.mjs", "electron-hwnd-buffer.mjs", "persist-file.mjs", + "property-store.mjs", "shell-link-pod.mjs", "file-operation.mjs", "file-open-dialog.mjs", @@ -336,6 +362,7 @@ if ("com" -in $Lang) { "sequential-stream-buffer.mjs", "automation-values.mjs", "automation-dispatch.mjs", + "com-infrastructure.mjs", "dtm.mjs", "smtc.mjs" ) diff --git a/tests/e2e/runners/com/automation-dispatch.mjs b/tests/e2e/runners/com/automation-dispatch.mjs index 9e19503f..963da5e6 100644 --- a/tests/e2e/runners/com/automation-dispatch.mjs +++ b/tests/e2e/runners/com/automation-dispatch.mjs @@ -12,11 +12,11 @@ import { IID_IDispatch, } from "../../e2e_generated/com/automation/com/IDispatch.js"; import { IEnumVARIANT } from "../../e2e_generated/com/automation/com/IEnumVARIANT.js"; -import { ISequentialStream } from "../../e2e_generated/com/stream/com/ISequentialStream.js"; +import { IStream } from "../../e2e_generated/com/stream/com/IStream.js"; const CLSID_SHELL_APPLICATION = "13709620-c279-11ce-a49e-444553540000"; const DISPATCH_PROPERTYGET = 2; -const IID_NULL = Buffer.alloc(16); +const IID_NULL = "00000000-0000-0000-0000-000000000000"; initializeCom(1); @@ -49,7 +49,7 @@ function namedMember(dispatch, name, flags = DISPATCH_PROPERTYGET) { try { assert.throws( - () => ISequentialStream._fromNative(shell._obj), + () => IStream._fromNative(shell._obj), /QueryInterface failed/, ); assert.ok(shell.getTypeInfoCount() >= 1); diff --git a/tests/e2e/runners/com/com-infrastructure.mjs b/tests/e2e/runners/com/com-infrastructure.mjs new file mode 100644 index 00000000..6fffe3a0 --- /dev/null +++ b/tests/e2e/runners/com/com-infrastructure.mjs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from 'node:assert/strict'; +import { DynCom } from '../../../../bindings/js/dist/com-unsafe.js'; +import { + IClassFactory, + ICreateErrorInfo, + IErrorInfo, + IID_IClassFactory, + IID_IErrorInfo, + IMalloc, +} from '../../e2e_generated/com/infrastructure/com/index.mjs'; +import { + IID_ITaskbarList3, + ITaskbarList3, +} from '../../e2e_generated/com/shell/com/index.mjs'; + +const CLSID_TASKBAR_LIST = '56fdf344-fd6d-11d0-958a-006097c9a090'; +const ERROR_GUID = '12345678-1234-5678-90ab-1234567890ab'; + +DynCom.initialize(1); + +const allocatorValue = DynCom.coGetMalloc(); +const allocator = IMalloc._fromNative(allocatorValue); +allocatorValue.release(); +let allocation = allocator.alloc(32n); +assert.ok(allocation); +assert.ok(allocator.getSize(allocation) >= 32n); +assert.notEqual(allocator.didAlloc(allocation), 0); +const originalAllocation = allocation; +allocation = allocator.realloc(originalAllocation, 64); +assert.ok(allocation); +assert.equal(originalAllocation.released, true); +assert.ok(allocator.getSize(allocation) >= 64n); +allocator.free(allocation); +assert.equal(allocation.released, true); +allocator.heapMinimize(); +allocator.release(); + +const factoryValue = DynCom.coGetClassObject( + CLSID_TASKBAR_LIST, + IID_IClassFactory, +); +const factory = IClassFactory._fromNative(factoryValue); +factoryValue.release(); +factory.lockServer(true); +try { + const taskbarValue = factory.createInstance( + null, + IID_ITaskbarList3.toString(), + ); + const taskbar = ITaskbarList3._fromNative(taskbarValue); + taskbarValue.release(); + taskbar.hrInit(); + taskbar.release(); +} finally { + factory.lockServer(false); + factory.release(); +} + +const createValue = DynCom.createErrorInfo(); +const create = ICreateErrorInfo._fromNative(createValue); +createValue.release(); +create.setGUID(ERROR_GUID); +create.setSource('dynwinrt'); +create.setDescription('generated COM error info'); +create.setHelpFile('dynwinrt-help.chm'); +create.setHelpContext(42); +DynCom.setErrorInfo(create._obj); +create.release(); + +const errorValue = DynCom.getErrorInfo(); +assert.ok(errorValue); +const error = IErrorInfo._fromNative(errorValue); +errorValue.release(); +assert.equal(error.getGUID().toLowerCase(), ERROR_GUID); +assert.equal(error.getSource(), 'dynwinrt'); +assert.equal(error.getDescription(), 'generated COM error info'); +assert.equal(error.getHelpFile(), 'dynwinrt-help.chm'); +assert.equal(error.getHelpContext(), 42); +error.release(); +assert.equal(DynCom.getErrorInfo(), null); + +console.log('com-infrastructure ok'); diff --git a/tests/e2e/runners/com/property-store.mjs b/tests/e2e/runners/com/property-store.mjs new file mode 100644 index 00000000..a85a67b7 --- /dev/null +++ b/tests/e2e/runners/com/property-store.mjs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import assert from 'node:assert/strict'; +import { + DynCom, + DynComPropVariant, +} from '../../../../bindings/js/dist/com-unsafe.js'; +import { + createPROPERTYKEY, + IID_IPropertyStore, + IPropertyStore, +} from '../../e2e_generated/com/shell/com/IPropertyStore.js'; + +const CLSID_SHELL_LINK = '00021401-0000-0000-c000-000000000046'; +const VT_LPWSTR = 31; +const expected = 'Microsoft.DynWinRT.E2E.PropertyStore'; +const key = createPROPERTYKEY( + Buffer.from('55284c9f799f394ba8d0e1d42de1d5f305000000', 'hex'), +); + +DynCom.initialize(1); + +const native = DynCom.coCreateInstance(CLSID_SHELL_LINK, IID_IPropertyStore); +const store = IPropertyStore._fromNative(native); +native.release(); + +try { + const input = DynComPropVariant.string(expected); + try { + store.setValue(key, input); + } finally { + input.release(); + } + + const output = store.getValue(key); + try { + assert.equal(output.vartype, VT_LPWSTR); + assert.equal(output.kind, 'string'); + assert.equal(output.toStringValue(), expected); + } finally { + output.release(); + } + + store.commit(); +} finally { + store.release(); +} + +console.log('property-store ok'); diff --git a/tests/e2e/runners/com/sequential-stream-buffer.mjs b/tests/e2e/runners/com/sequential-stream-buffer.mjs index ab07f989..b5742000 100644 --- a/tests/e2e/runners/com/sequential-stream-buffer.mjs +++ b/tests/e2e/runners/com/sequential-stream-buffer.mjs @@ -4,9 +4,9 @@ import assert from 'node:assert/strict'; import { DynCom, DynComMethodSig, WinGuid } from '../../../../bindings/js/dist/com-unsafe.js'; import { - IID_ISequentialStream, - ISequentialStream, -} from '../../e2e_generated/com/stream/com/ISequentialStream.js'; + IID_IStream, + IStream, +} from '../../e2e_generated/com/stream/com/IStream.js'; import { IID_IWICImagingFactory, IWICImagingFactory, @@ -36,13 +36,22 @@ wicStream .method(16) .invoke(stream, [DynCom.pointer(expected), DynCom.u32(expected.length)]); -const sequential = ISequentialStream._fromNative(stream.cast(IID_ISequentialStream)); -const [hresult, actual] = sequential.read(Buffer.alloc(expected.length)); +const projectedStream = IStream._fromNative(stream.cast(IID_IStream)); +const stat = projectedStream.stat(1); +assert.equal(stat.name, null); +assert.equal(stat.storageType, 2); +assert.equal(stat.size, BigInt(expected.length)); +stat.release(); + +assert.equal(projectedStream.seek(0n, 0), 0n); +const [hresult, actual] = projectedStream.read(Buffer.alloc(expected.length)); assert.equal(hresult, 0); assert.deepEqual(actual, expected); -sequential.release(); +assert.throws(() => projectedStream.clone(), /0x80004001/); + +projectedStream.release(); stream.release(); factory.release(); -console.log('sequential-stream-buffer ok'); +console.log('istream-buffer ok'); diff --git a/tools/dynwinrt-codegen/src/codegen/com/ir.rs b/tools/dynwinrt-codegen/src/codegen/com/ir.rs index 7cc2751f..40ae59e9 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/ir.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/ir.rs @@ -119,11 +119,17 @@ pub(super) struct NativePodArchitectureLayout { pub(super) struct NativePodLayout { pub(super) namespace: String, pub(super) name: String, + pub(super) initializers: Vec, pub(super) x86: NativePodArchitectureLayout, pub(super) x64: NativePodArchitectureLayout, pub(super) arm64: NativePodArchitectureLayout, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub(super) enum NativePodInitializer { + SizeOfLayout { field: String }, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub(super) enum NativeUnionFieldType { Scalar(NativePodScalar), @@ -166,6 +172,7 @@ pub(super) enum ComType { Win32Bool, HResult, Guid, + GuidPointer, HString, Enum { namespace: String, @@ -178,6 +185,9 @@ pub(super) enum ComType { underlying: ComScalarRepr, }, RawPointer, + AllocatorPointer, + ConsumedAllocatorPointer, + InspectedAllocatorPointer, PointerAlias { namespace: String, name: String, @@ -201,6 +211,7 @@ pub(super) enum ComType { PropVariant, DispatchParams, ExcepInfo, + StatStg, ManagedInterface { iid: String, }, @@ -296,6 +307,9 @@ pub(super) enum ResultConversion { SafeArray, PropVariant, ExcepInfo, + StatStg, + MallocAllocation, + MallocReallocation, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -515,6 +529,10 @@ pub(super) fn dispatch_shape(typ: &ComType) -> Option { // depending on position, so they can collide with any other category // and are never safe overload-dispatch keys. ComType::RawPointer + | ComType::AllocatorPointer + | ComType::ConsumedAllocatorPointer + | ComType::InspectedAllocatorPointer + | ComType::GuidPointer | ComType::PointerAlias { .. } | ComType::NativePod { .. } | ComType::NativePodPointer { .. } @@ -524,6 +542,7 @@ pub(super) fn dispatch_shape(typ: &ComType) -> Option { | ComType::SafeArray { .. } | ComType::PropVariant | ComType::ExcepInfo + | ComType::StatStg | ComType::Bstr | ComType::CoTaskMemWideString | ComType::StringArray { .. } diff --git a/tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs index f215fe06..27886f28 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/render.rs @@ -552,7 +552,10 @@ fn wrap_param_arg_js(param: &ProjectedComParam, variable: &str) -> String { ComType::ManagedInterface { .. } => { format!("{variable} === null ? DynCom.nullComValue() : {wrapped}") } - ComType::RawPointer | ComType::PointerAlias { .. } => { + ComType::AllocatorPointer + | ComType::ConsumedAllocatorPointer + | ComType::InspectedAllocatorPointer => wrapped, + ComType::RawPointer | ComType::GuidPointer | ComType::PointerAlias { .. } => { format!("{variable} === null ? DynCom.pointer(null) : {wrapped}") } ComType::Bstr => format!("{variable} === null ? DynCom.nullBstr() : {wrapped}"), @@ -600,7 +603,10 @@ fn out_abi_type_js(method: &ProjectedComMethod, param_index: usize, typ: &ComTyp | ResultConversion::Variant | ResultConversion::SafeArray | ResultConversion::PropVariant - | ResultConversion::ExcepInfo, + | ResultConversion::ExcepInfo + | ResultConversion::StatStg + | ResultConversion::MallocAllocation + | ResultConversion::MallocReallocation, ) | None => abi_type_js(typ), } @@ -916,11 +922,28 @@ fn emit_method_js_named( } } } + let malloc_reallocation = method + .results + .iter() + .any(|result| result.conversion == ResultConversion::MallocReallocation); + if malloc_reallocation { + let size_surface = inputs + .iter() + .position(|(index, _)| *index == 1) + .expect("validated IMalloc::Realloc size input"); + let size_name = js_param_name(&method.params[1].name, size_surface); + out.push_str(&format!( + " const _mallocSize = BigInt({size_name});\n" + )); + } let args = method .params .iter() .enumerate() .filter_map(|(index, param)| { + if malloc_reallocation && index == 1 { + return Some("DynCom.usize(_mallocSize)".into()); + } for group in &method.shared_counts { match group { SharedCountPlan::StringInputScalarOutput { @@ -1936,6 +1959,10 @@ fn collect_pointer_aliases(meta: &ProjectedComInterface) -> Vec<(String, Pointer | ComType::Enum { .. } | ComType::ScalarAlias { .. } | ComType::RawPointer + | ComType::AllocatorPointer + | ComType::ConsumedAllocatorPointer + | ComType::InspectedAllocatorPointer + | ComType::GuidPointer | ComType::Bstr | ComType::NativePod { .. } | ComType::NativePodPointer { .. } @@ -1946,6 +1973,7 @@ fn collect_pointer_aliases(meta: &ProjectedComInterface) -> Vec<(String, Pointer | ComType::PropVariant | ComType::DispatchParams | ComType::ExcepInfo + | ComType::StatStg | ComType::ManagedInterface { .. } | ComType::CoTaskMemWideString | ComType::StringArray { .. } @@ -2031,6 +2059,10 @@ fn collect_native_pods(meta: &ProjectedComInterface) -> Vec Vec {} @@ -2120,6 +2153,14 @@ fn collect_runtime_types(meta: &ProjectedComInterface) -> Vec<&'static str> { ComType::ExcepInfo => { types.insert("DynComExcepInfo"); } + ComType::StatStg => { + types.insert("DynComStatStg"); + } + ComType::AllocatorPointer + | ComType::ConsumedAllocatorPointer + | ComType::InspectedAllocatorPointer => { + types.insert("DynComAllocation"); + } ComType::OwningArray { element, .. } => match element.as_ref() { ComType::Variant => { types.insert("DynComVariant"); @@ -2176,6 +2217,10 @@ fn collect_scalar_alias( | ComType::HString | ComType::Enum { .. } | ComType::RawPointer + | ComType::AllocatorPointer + | ComType::ConsumedAllocatorPointer + | ComType::InspectedAllocatorPointer + | ComType::GuidPointer | ComType::PointerAlias { .. } | ComType::Bstr | ComType::NativePod { .. } @@ -2187,6 +2232,7 @@ fn collect_scalar_alias( | ComType::PropVariant | ComType::DispatchParams | ComType::ExcepInfo + | ComType::StatStg | ComType::ManagedInterface { .. } | ComType::CoTaskMemWideString | ComType::StringArray { .. } => {} diff --git a/tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs b/tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs index eb46267b..013adc10 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/javascript/types.rs @@ -3,9 +3,9 @@ use super::super::ir::{ ComEnumUnderlying, ComPrimitive, ComScalarRepr, ComType, NativePodArchitectureLayout, - NativePodFieldType, NativePodLayout, NativePodScalar, NativeUnionArchitectureLayout, - NativeUnionFieldType, NativeUnionLayout, PointerAliasKind, ProjectedComResult, - ResultConversion, SafeArrayElement, StringEncoding, + NativePodFieldType, NativePodInitializer, NativePodLayout, NativePodScalar, + NativeUnionArchitectureLayout, NativeUnionFieldType, NativeUnionLayout, PointerAliasKind, + ProjectedComResult, ResultConversion, SafeArrayElement, StringEncoding, }; #[cfg(test)] use super::super::ir::{NativePodField, NativeUnionField}; @@ -31,10 +31,15 @@ pub(super) fn abi_type_js(typ: &ComType) -> String { ComType::NativeUsize => "DynCom.usizeType()".into(), ComType::Win32Bool | ComType::HResult => "DynCom.i32Type()".into(), ComType::Guid => "DynCom.guidType()".into(), + ComType::GuidPointer => "DynCom.pointerType()".into(), ComType::HString => "DynCom.hstringType()".into(), ComType::Enum { underlying, .. } => enum_abi_type_js(*underlying).into(), ComType::ScalarAlias { underlying, .. } => scalar_abi_type_js(*underlying).into(), - ComType::RawPointer | ComType::PointerAlias { .. } => "DynCom.pointerType()".into(), + ComType::RawPointer + | ComType::AllocatorPointer + | ComType::ConsumedAllocatorPointer + | ComType::InspectedAllocatorPointer + | ComType::PointerAlias { .. } => "DynCom.pointerType()".into(), ComType::Bstr => "DynCom.bstrType()".into(), ComType::NativePod { layout } => { format!("DynCom.nativeStructType({})", native_pod_layout_js(layout)) @@ -53,6 +58,7 @@ pub(super) fn abi_type_js(typ: &ComType) -> String { ComType::PropVariant => "DynCom.propVariantType()".into(), ComType::DispatchParams => "DynCom.dispatchParamsType()".into(), ComType::ExcepInfo => "DynCom.excepInfoType()".into(), + ComType::StatStg => "DynCom.statStgType()".into(), ComType::ManagedInterface { iid } => { format!("DynCom.interfaceType(WinGuid.parse('{iid}'))") } @@ -131,10 +137,14 @@ pub(super) fn type_dts(typ: &ComType) -> String { ComType::Win32Bool => "boolean".into(), ComType::HResult => "number".into(), ComType::Guid => "string".into(), + ComType::GuidPointer => "string".into(), ComType::HString => "string".into(), ComType::Enum { name, .. } => name.clone(), ComType::ScalarAlias { name, .. } => name.clone(), ComType::RawPointer => "Buffer | Uint8Array".into(), + ComType::AllocatorPointer + | ComType::ConsumedAllocatorPointer + | ComType::InspectedAllocatorPointer => "DynComAllocation".into(), ComType::PointerAlias { name, .. } => name.clone(), ComType::NativePod { layout } | ComType::NativePodPointer { layout } => layout.name.clone(), ComType::NativeUnionPointer { layout } => layout.name.clone(), @@ -145,6 +155,7 @@ pub(super) fn type_dts(typ: &ComType) -> String { ComType::PropVariant => "DynComPropVariant".into(), ComType::DispatchParams => "DynComDispatchParams".into(), ComType::ExcepInfo => "DynComExcepInfo".into(), + ComType::StatStg => "DynComStatStg".into(), ComType::ManagedInterface { .. } => "DynWinRtValue".into(), ComType::CoTaskMemWideString => "string".into(), ComType::StringArray { .. } => "string[]".into(), @@ -193,6 +204,10 @@ pub(super) fn result_type_dts(result: &ProjectedComResult) -> String { ResultConversion::SafeArray => "DynComSafeArray".into(), ResultConversion::PropVariant => "DynComPropVariant".into(), ResultConversion::ExcepInfo => "DynComExcepInfo".into(), + ResultConversion::StatStg => "DynComStatStg".into(), + ResultConversion::MallocAllocation | ResultConversion::MallocReallocation => { + "DynComAllocation | null".into() + } } } @@ -217,10 +232,20 @@ pub(super) fn wrap_arg_js(typ: &ComType, variable: &str) -> String { ComType::Win32Bool => format!("DynCom.i32({variable} ? 1 : 0)"), ComType::HResult => format!("DynCom.i32({variable})"), ComType::Guid => format!("DynCom.guid(WinGuid.parse({variable}))"), + ComType::GuidPointer => format!("DynCom.iidPointer(WinGuid.parse({variable}))"), ComType::HString => format!("DynCom.hstring({variable})"), ComType::Enum { underlying, .. } => wrap_enum_arg_js(*underlying, variable), ComType::ScalarAlias { underlying, .. } => wrap_scalar_arg_js(*underlying, variable), ComType::RawPointer => format!("DynCom.safeDataPointer({variable})"), + ComType::AllocatorPointer => { + format!("DynCom.mallocAllocationPointer(this._obj, {variable})") + } + ComType::ConsumedAllocatorPointer => { + format!("DynCom.takeMallocAllocationPointer(this._obj, {variable})") + } + ComType::InspectedAllocatorPointer => { + format!("DynCom.mallocInspectionPointer({variable})") + } ComType::Bstr => format!("DynCom.bstr({variable})"), ComType::PointerAlias { name, @@ -259,6 +284,7 @@ pub(super) fn wrap_arg_js(typ: &ComType, variable: &str) -> String { ComType::PropVariant => format!("DynCom.propVariant({variable})"), ComType::DispatchParams => format!("DynCom.dispatchParams({variable})"), ComType::ExcepInfo => unreachable!("EXCEPINFO is output-only"), + ComType::StatStg => unreachable!("STATSTG is output-only"), ComType::ManagedInterface { .. } => variable.to_string(), ComType::CoTaskMemWideString => { unreachable!("CoTaskMem string elements are output-only") @@ -340,6 +366,13 @@ pub(super) fn unwrap_result_js(result: &ProjectedComResult, expression: &str) -> ResultConversion::SafeArray => format!("DynCom.takeSafeArray({expression})"), ResultConversion::PropVariant => format!("DynCom.takePropVariant({expression})"), ResultConversion::ExcepInfo => format!("DynCom.takeExcepInfo({expression})"), + ResultConversion::StatStg => format!("DynCom.takeStatStg({expression})"), + ResultConversion::MallocAllocation => { + format!("DynCom.takeMallocAllocation(this._obj, {expression})") + } + ResultConversion::MallocReallocation => { + format!("DynCom.finishMallocReallocation(this._obj, pv, _mallocSize, {expression})") + } } } @@ -365,10 +398,15 @@ fn unwrap_value_js(typ: &ComType, expression: &str) -> String { ComType::Win32Bool => format!("(DynCom.toNumber({expression}) !== 0)"), ComType::HResult => format!("DynCom.toNumber({expression})"), ComType::Guid => format!("DynCom.toGuidString({expression})"), + ComType::GuidPointer => unreachable!("GUID pointer values are input-only"), ComType::HString => format!("{expression}.toString()"), ComType::Enum { underlying, .. } => unwrap_enum_js(*underlying, expression), ComType::ScalarAlias { underlying, .. } => unwrap_scalar_js(*underlying, expression), - ComType::RawPointer | ComType::PointerAlias { .. } => { + ComType::RawPointer + | ComType::AllocatorPointer + | ComType::ConsumedAllocatorPointer + | ComType::InspectedAllocatorPointer + | ComType::PointerAlias { .. } => { format!("DynCom.asPointerBigint({expression})") } ComType::Bstr => unreachable!("BSTR outputs require the BSTR result conversion"), @@ -385,6 +423,7 @@ fn unwrap_value_js(typ: &ComType, expression: &str) -> String { ComType::PropVariant => format!("DynCom.takePropVariant({expression})"), ComType::DispatchParams => unreachable!("DISPPARAMS is input-only"), ComType::ExcepInfo => format!("DynCom.takeExcepInfo({expression})"), + ComType::StatStg => format!("DynCom.takeStatStg({expression})"), ComType::ManagedInterface { .. } => expression.to_string(), ComType::CoTaskMemWideString => { unreachable!("CoTaskMem string elements are array-only") @@ -486,10 +525,26 @@ pub(super) fn native_pod_layout_js(layout: &NativePodLayout) -> String { } pub(super) fn native_pod_descriptor_js(layout: &NativePodLayout) -> String { + let initializers = layout + .initializers + .iter() + .map(|initializer| match initializer { + NativePodInitializer::SizeOfLayout { field } => { + format!("{{\"kind\":\"sizeOfLayout\",\"field\":\"{field}\"}}") + } + }) + .collect::>() + .join(","); + let initializers = if initializers.is_empty() { + String::new() + } else { + format!(",\"initializers\":[{initializers}]") + }; let descriptor = format!( - "{{\"name\":\"{}.{}\",\"x86\":{},\"x64\":{},\"arm64\":{}}}", + "{{\"name\":\"{}.{}\"{},\"x86\":{},\"x64\":{},\"arm64\":{}}}", layout.namespace, layout.name, + initializers, native_pod_architecture_json(&layout.x86), native_pod_architecture_json(&layout.x64), native_pod_architecture_json(&layout.arm64), @@ -744,6 +799,7 @@ mod tests { NativePodLayout { namespace: "Test".into(), name: "POD".into(), + initializers: Vec::new(), x86: architecture.clone(), x64: architecture.clone(), arm64: architecture, diff --git a/tools/dynwinrt-codegen/src/codegen/com/model/abi.rs b/tools/dynwinrt-codegen/src/codegen/com/model/abi.rs index c43a48e2..e1092c8b 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/model/abi.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/model/abi.rs @@ -163,6 +163,7 @@ pub(in crate::codegen::com) enum ComAbiType { PropVariant, DispatchParams, ExcepInfo, + StatStg, FunctionPointer(SignatureId), Unknown(UnsupportedReason), } diff --git a/tools/dynwinrt-codegen/src/codegen/com/model/metadata.rs b/tools/dynwinrt-codegen/src/codegen/com/model/metadata.rs index ee9ddf22..91789a80 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/model/metadata.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/model/metadata.rs @@ -135,16 +135,6 @@ pub(super) fn map_interface(meta: &ComInterfaceMeta) -> Result Result Option { fn map_method( model: &mut ComModel, - interface_iid: ComGuid, - interface_iid_text: &str, interface_namespace: &str, interface_name: &str, + interface_iid_text: &str, + interface_iid: ComGuid, method: &crate::com_metadata::MethodMeta, raw: &RawComMethod, ) -> Result { @@ -347,8 +337,8 @@ fn map_method( } map_param( model, - interface_namespace, - interface_name, + &raw.declaring_namespace, + &raw.declaring_interface, index, raw_param, raw, @@ -396,6 +386,10 @@ fn map_method( Some(RawExactMethodContractKind::UnsafePrivateData) => { unreachable!("unsafe private-data contracts fail before method mapping") } + Some(RawExactMethodContractKind::StatStg) => method, + Some(RawExactMethodContractKind::Malloc) => { + method.with_special_contract(ComMethodSpecialContract::Malloc) + } None => method, }, ) @@ -430,7 +424,20 @@ fn map_param( .then_some(RawParamDirection::Out) }) .unwrap_or(raw.direction); - let (abi_type, count) = if let Some(array) = &raw.native_array { + let (abi_type, count) = if raw_method.exact_contract.as_ref().is_some_and(|contract| { + contract.kind == RawExactMethodContractKind::StatStg + && contract.buffer_param_index == param_index + }) { + ( + insert_abi( + model, + Some(QualifiedName::new("Windows.Win32.System.Com", "STATSTG")?), + None, + ComAbiType::StatStg, + )?, + None, + ) + } else if let Some(array) = &raw.native_array { if effective_direction == RawParamDirection::InOut { return Err(ModelError::Unsupported(UnsupportedReason::Other( "in/out counted-buffer contents require a dedicated preserve-input storage plan" @@ -1310,6 +1317,7 @@ fn validate_pod_field_type( | ComAbiType::PropVariant | ComAbiType::DispatchParams | ComAbiType::ExcepInfo + | ComAbiType::StatStg | ComAbiType::FunctionPointer(_) | ComAbiType::Unknown(_) => Err(ModelError::Unsupported(UnsupportedReason::UnknownLayout)), } @@ -1377,6 +1385,7 @@ fn abi_size_alignment( | ComAbiType::PropVariant | ComAbiType::DispatchParams | ComAbiType::ExcepInfo + | ComAbiType::StatStg | ComAbiType::Unknown(_) => { return Err(ModelError::Unsupported(UnsupportedReason::UnknownLayout)); } @@ -1592,6 +1601,7 @@ fn buffer_element_ownership( | ComAbiType::PropVariant | ComAbiType::DispatchParams | ComAbiType::ExcepInfo + | ComAbiType::StatStg | ComAbiType::FunctionPointer(_) | ComAbiType::Unknown(_) => BufferElementOwnership::Unknown, }; @@ -1725,6 +1735,9 @@ fn map_ownership( ComAbiType::ExcepInfo if direction == Direction::Out => { Ok((ComOwnership::ExcepInfoOwned, Cleanup::ExcepInfoClear)) } + ComAbiType::StatStg if direction == Direction::Out => { + Ok((ComOwnership::StatStgOwned, Cleanup::StatStgClear)) + } ComAbiType::DispatchParams if direction == Direction::Out => Err( ModelError::Unsupported(UnsupportedReason::Other( "DISPPARAMS is input-only".into(), @@ -1748,6 +1761,7 @@ fn map_ownership( | ComAbiType::PropVariant | ComAbiType::DispatchParams | ComAbiType::ExcepInfo + | ComAbiType::StatStg if direction == Direction::InOut => { Err(ModelError::Unsupported(UnsupportedReason::Other( @@ -1781,6 +1795,7 @@ fn map_ownership( | ComAbiType::PropVariant | ComAbiType::DispatchParams | ComAbiType::ExcepInfo + | ComAbiType::StatStg | ComAbiType::FunctionPointer(_) | ComAbiType::Unknown(_) => { Err(ModelError::Unsupported(UnsupportedReason::UnknownOwnership)) diff --git a/tools/dynwinrt-codegen/src/codegen/com/model/method.rs b/tools/dynwinrt-codegen/src/codegen/com/model/method.rs index 40f2dc24..6b1ba450 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/model/method.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/model/method.rs @@ -34,6 +34,7 @@ impl ComReturnKind { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(in crate::codegen::com) enum ComMethodSpecialContract { FixedCapacityBytes { guid_param: ParamIndex }, + Malloc, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -144,6 +145,7 @@ impl ComMethodContract { constness: Constness::Mutable, .. } + | ComAbiType::StatStg ) { return Err(ModelError::InvalidContract(format!( diff --git a/tools/dynwinrt-codegen/src/codegen/com/model/ownership.rs b/tools/dynwinrt-codegen/src/codegen/com/model/ownership.rs index 9ee6103a..5ed1fc38 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/model/ownership.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/model/ownership.rs @@ -32,6 +32,7 @@ pub(in crate::codegen::com) enum ComOwnership { SafeArrayOwned, PropVariantOwned, ExcepInfoOwned, + StatStgOwned, LocalOwned, HandleOwned(HandleCleanup), CustomOwned(CleanupId), @@ -49,6 +50,7 @@ pub(in crate::codegen::com) enum Cleanup { SafeArrayDestroy, PropVariantClear, ExcepInfoClear, + StatStgClear, LocalFree, Handle(HandleCleanup), Custom(CleanupId), @@ -108,6 +110,7 @@ pub(super) fn validate_ownership_cleanup( | (ComOwnership::SafeArrayOwned, Cleanup::SafeArrayDestroy) | (ComOwnership::PropVariantOwned, Cleanup::PropVariantClear) | (ComOwnership::ExcepInfoOwned, Cleanup::ExcepInfoClear) + | (ComOwnership::StatStgOwned, Cleanup::StatStgClear) | (ComOwnership::LocalOwned, Cleanup::LocalFree) => true, (ComOwnership::HandleOwned(expected), Cleanup::Handle(actual)) => expected == actual, (ComOwnership::CustomOwned(expected), Cleanup::Custom(actual)) => expected == actual, diff --git a/tools/dynwinrt-codegen/src/codegen/com/project/mod.rs b/tools/dynwinrt-codegen/src/codegen/com/project/mod.rs index 93b6adf7..b5614efa 100644 --- a/tools/dynwinrt-codegen/src/codegen/com/project/mod.rs +++ b/tools/dynwinrt-codegen/src/codegen/com/project/mod.rs @@ -10,14 +10,14 @@ use crate::com_metadata::{ComCoclassMeta, ComInterfaceMeta}; use super::ir::{ ActivationPlan, BufferCountUnit as ProjectedBufferCountUnit, ComEnumUnderlying, ComParamDirection, ComPrimitive, ComReturnConvention, ComScalarRepr, ComType, DispatchShape, - NativePodArchitectureLayout, NativePodField, NativePodFieldType, NativePodLayout, - NativePodScalar, NativeUnionArchitectureLayout, NativeUnionField, NativeUnionFieldType, - NativeUnionLayout, OverloadDispatch, OverloadInfo, PointerAliasKind, ProjectedComCoclass, - ProjectedComEnum, ProjectedComEnumMember, ProjectedComInterface, ProjectedComMethod, - ProjectedComMethodKind, ProjectedComParam, ProjectedComResult, ProjectedEnumValue, - ProjectedInterfaceRef, ResultConversion, ResultSource, SafeArrayElement, SharedCountPlan, - StringBufferPlan, StringEncoding, TypedBufferPlan, TypedBufferRelation, TypedBufferSizing, - dispatch_shape, + NativePodArchitectureLayout, NativePodField, NativePodFieldType, NativePodInitializer, + NativePodLayout, NativePodScalar, NativeUnionArchitectureLayout, NativeUnionField, + NativeUnionFieldType, NativeUnionLayout, OverloadDispatch, OverloadInfo, PointerAliasKind, + ProjectedComCoclass, ProjectedComEnum, ProjectedComEnumMember, ProjectedComInterface, + ProjectedComMethod, ProjectedComMethodKind, ProjectedComParam, ProjectedComResult, + ProjectedEnumValue, ProjectedInterfaceRef, ResultConversion, ResultSource, SafeArrayElement, + SharedCountPlan, StringBufferPlan, StringEncoding, TypedBufferPlan, TypedBufferRelation, + TypedBufferSizing, dispatch_shape, }; use super::javascript::naming::camel_case; use super::model::ValidatedComInterface; @@ -232,6 +232,10 @@ fn validate_projected_surface_names(meta: &ProjectedComInterface) -> Result<(), | ComType::Guid | ComType::HString | ComType::RawPointer + | ComType::AllocatorPointer + | ComType::ConsumedAllocatorPointer + | ComType::InspectedAllocatorPointer + | ComType::GuidPointer | ComType::Bstr | ComType::Variant | ComType::VariantByValue @@ -239,6 +243,7 @@ fn validate_projected_surface_names(meta: &ProjectedComInterface) -> Result<(), | ComType::PropVariant | ComType::DispatchParams | ComType::ExcepInfo + | ComType::StatStg | ComType::ManagedInterface { .. } | ComType::CoTaskMemWideString | ComType::StringArray { .. } => {} @@ -275,6 +280,7 @@ fn diagnostic_compatibility( || semantic_error.contains("VARIANT") || semantic_error.contains("DISPPARAMS") || semantic_error.contains("EXCEPINFO") + || semantic_error.contains("STATSTG") || semantic_error.contains("IDispatch::Invoke") || semantic_error.contains("optional COM outputs") || semantic_error.contains("nested interface") @@ -494,9 +500,20 @@ fn project_method( &typed_buffers, )?; + let malloc_contract = matches!( + method.special_contract(), + Some(ComMethodSpecialContract::Malloc) + ); let mut params = Vec::with_capacity(method.params().len()); for (index, param) in method.params().iter().enumerate() { - let typ = project_param_type(semantic, param)?; + let mut typ = project_param_type(semantic, param)?; + if malloc_contract && matches!(typ, ComType::RawPointer) { + typ = match method.name() { + "Free" => ComType::ConsumedAllocatorPointer, + "DidAlloc" => ComType::InspectedAllocatorPointer, + _ => ComType::AllocatorPointer, + }; + } let typed_buffer = typed_buffers .iter() .find(|plan| plan.buffer_param_index == index); @@ -726,7 +743,7 @@ fn project_method( .. } ) - || !matches!(params[1].typ, ComType::RawPointer) + || !matches!(params[1].typ, ComType::GuidPointer) || !matches!( params[2].typ, ComType::Primitive(ComPrimitive::U32) @@ -779,6 +796,10 @@ fn project_method( matches!( typ, ComType::RawPointer + | ComType::AllocatorPointer + | ComType::ConsumedAllocatorPointer + | ComType::InspectedAllocatorPointer + | ComType::GuidPointer | ComType::PointerAlias { .. } | ComType::NativePodPointer { .. } | ComType::Bstr @@ -804,11 +825,15 @@ fn project_method( ComReturnConvention::Direct(typ) } ComReturnKind::DirectPointer(abi_type) => { - return Err(format!( - "{}: unsupported direct native return type {}", - context(), - semantic_type_name(semantic, abi_type) - )); + let typ = project_value_type(semantic, abi_type)?; + if !malloc_contract || !matches!(typ, ComType::RawPointer) { + return Err(format!( + "{}: unsupported direct native return type {}", + context(), + semantic_type_name(semantic, abi_type) + )); + } + ComReturnConvention::Direct(ComType::AllocatorPointer) } }; if matches!(kind, ProjectedComMethodKind::DispatchInvoke { .. }) @@ -838,7 +863,13 @@ fn project_method( results.push(ProjectedComResult { typ, source: ResultSource::DirectReturn, - conversion: ResultConversion::Value, + conversion: if malloc_contract && method.name() == "Alloc" { + ResultConversion::MallocAllocation + } else if malloc_contract && method.name() == "Realloc" { + ResultConversion::MallocReallocation + } else { + ResultConversion::Value + }, }); } for (index, (contract, param)) in method.params().iter().zip(¶ms).enumerate() { @@ -1019,6 +1050,8 @@ fn project_input_type( .map_err(|error| error.to_string())?; if matches!(pointee_definition.abi(), ComAbiType::ComInterface { .. }) { project_value_type(semantic, *pointee) + } else if matches!(pointee_definition.abi(), ComAbiType::Guid) { + Ok(ComType::GuidPointer) } else if matches!(pointee_definition.abi(), ComAbiType::NativeStruct(_)) { let ComType::NativePod { layout } = project_value_type(semantic, *pointee)? else { unreachable!("validated native struct projection") @@ -1049,6 +1082,8 @@ fn project_input_type( project_value_type(semantic, *pointee) } else if matches!(pointee_definition.abi(), ComAbiType::ExcepInfo) { Err("EXCEPINFO is output-only".into()) + } else if matches!(pointee_definition.abi(), ComAbiType::StatStg) { + Err("STATSTG is output-only".into()) } else { Ok(ComType::RawPointer) } @@ -1059,7 +1094,8 @@ fn project_input_type( ComAbiType::PropVariant | ComAbiType::SafeArray { .. } | ComAbiType::DispatchParams - | ComAbiType::ExcepInfo => Err(format!( + | ComAbiType::ExcepInfo + | ComAbiType::StatStg => Err(format!( "{} must be passed through its native pointer contract", semantic_type_definition_name(definition) )), @@ -1147,6 +1183,7 @@ fn project_output_type( "Automation outputs require VARIANT*, PROPVARIANT*, or SAFEARRAY** pointer metadata" .into(), ), + ComAbiType::StatStg => project_value_type(semantic, abi_type), _ => project_value_type(semantic, abi_type), } } @@ -1275,6 +1312,7 @@ fn project_value_type( ComAbiType::PropVariant => Ok(ComType::PropVariant), ComAbiType::DispatchParams => Ok(ComType::DispatchParams), ComAbiType::ExcepInfo => Ok(ComType::ExcepInfo), + ComAbiType::StatStg => Ok(ComType::StatStg), ComAbiType::NativeUnion(_) | ComAbiType::FunctionPointer(_) | ComAbiType::Unknown(_) => { Err(format!( "unsupported Classic-COM semantic type {}", @@ -1480,30 +1518,83 @@ fn project_native_pod_layout( .layout_definition(layout_id) .map_err(|error| error.to_string())?; let mut visiting = std::collections::HashSet::new(); + let x86 = project_native_pod_architecture( + semantic, + layouts.get(Architecture::X86), + Architecture::X86, + &mut visiting, + )?; + let x64 = project_native_pod_architecture( + semantic, + layouts.get(Architecture::X64), + Architecture::X64, + &mut visiting, + )?; + let arm64 = project_native_pod_architecture( + semantic, + layouts.get(Architecture::Arm64), + Architecture::Arm64, + &mut visiting, + )?; + let initializers = native_pod_initializers(namespace, name, &x86, &x64, &arm64)?; Ok(NativePodLayout { namespace: namespace.into(), name: name.into(), - x86: project_native_pod_architecture( - semantic, - layouts.get(Architecture::X86), - Architecture::X86, - &mut visiting, - )?, - x64: project_native_pod_architecture( - semantic, - layouts.get(Architecture::X64), - Architecture::X64, - &mut visiting, - )?, - arm64: project_native_pod_architecture( - semantic, - layouts.get(Architecture::Arm64), - Architecture::Arm64, - &mut visiting, - )?, + initializers, + x86, + x64, + arm64, }) } +fn native_pod_initializers( + namespace: &str, + name: &str, + x86: &NativePodArchitectureLayout, + x64: &NativePodArchitectureLayout, + arm64: &NativePodArchitectureLayout, +) -> Result, String> { + if namespace != "Windows.Win32.System.Com" + || !matches!(name, "BIND_OPTS" | "BIND_OPTS2" | "BIND_OPTS3") + { + return Ok(Vec::new()); + } + let expected = match name { + "BIND_OPTS" => [(16, 4), (16, 4), (16, 4)], + "BIND_OPTS2" => [(32, 4), (40, 8), (40, 8)], + "BIND_OPTS3" => [(36, 4), (48, 8), (48, 8)], + _ => unreachable!(), + }; + for (architecture, layout, (size, alignment)) in [ + ("x86", x86, expected[0]), + ("x64", x64, expected[1]), + ("ARM64", arm64, expected[2]), + ] { + if layout.size != size || layout.alignment != alignment { + return Err(format!( + "{namespace}.{name} {architecture} layout must be size {size}, alignment {alignment}; found size {}, alignment {}", + layout.size, layout.alignment + )); + } + let Some(field) = layout.fields.iter().find(|field| field.name == "cbStruct") else { + return Err(format!( + "{namespace}.{name} {architecture} layout is missing cbStruct" + )); + }; + if field.offset != 0 + || field.count != 1 + || field.typ != NativePodFieldType::Scalar(NativePodScalar::U32) + { + return Err(format!( + "{namespace}.{name} {architecture} cbStruct must be one u32 at offset 0" + )); + } + } + Ok(vec![NativePodInitializer::SizeOfLayout { + field: "cbStruct".into(), + }]) +} + fn project_native_pod_architecture( semantic: &SemanticComInterface, layout: &super::model::layout::NativeLayout, @@ -1618,6 +1709,7 @@ fn project_native_pod_field_type( | ComAbiType::PropVariant | ComAbiType::DispatchParams | ComAbiType::ExcepInfo + | ComAbiType::StatStg | ComAbiType::FunctionPointer(_) | ComAbiType::Unknown(_) => Err(format!( "unsupported nested native POD field {}", @@ -2370,6 +2462,9 @@ fn result_conversion( { Ok(ResultConversion::ExcepInfo) } + (ComOwnership::StatStgOwned, Cleanup::StatStgClear) if matches!(typ, ComType::StatStg) => { + Ok(ResultConversion::StatStg) + } (ownership, cleanup) => Err(format!( "{}: unsupported projected ownership {ownership:?} with cleanup {cleanup:?}", param.name() @@ -2892,6 +2987,7 @@ mod tests { layout: NativePodLayout { namespace: namespace.into(), name: "COLLISION".into(), + initializers: Vec::new(), x86: architecture.clone(), x64: architecture.clone(), arm64: architecture.clone(), @@ -2936,6 +3032,56 @@ mod tests { assert!(error.contains("Contoso.Two.COLLISION")); } + #[test] + fn bind_opts_initializer_requires_exact_cross_architecture_layout() { + let bind_opts = |size, alignment, offset| NativePodArchitectureLayout { + size, + alignment, + fields: vec![NativePodField { + name: "cbStruct".into(), + offset, + count: 1, + typ: NativePodFieldType::Scalar(NativePodScalar::U32), + }], + }; + let initializers = native_pod_initializers( + "Windows.Win32.System.Com", + "BIND_OPTS", + &bind_opts(16, 4, 0), + &bind_opts(16, 4, 0), + &bind_opts(16, 4, 0), + ) + .unwrap(); + assert_eq!( + initializers, + [NativePodInitializer::SizeOfLayout { + field: "cbStruct".into() + }] + ); + + let error = native_pod_initializers( + "Windows.Win32.System.Com", + "BIND_OPTS", + &bind_opts(16, 4, 4), + &bind_opts(16, 4, 0), + &bind_opts(16, 4, 0), + ) + .unwrap_err(); + assert!(error.contains("offset 0"), "{error}"); + + assert!( + native_pod_initializers( + "Contoso", + "BIND_OPTS", + &bind_opts(16, 4, 4), + &bind_opts(16, 4, 4), + &bind_opts(16, 4, 4), + ) + .unwrap() + .is_empty() + ); + } + #[test] fn specialized_buffer_rendering_rejects_uncomposable_plans() { let string = StringBufferPlan { diff --git a/tools/dynwinrt-codegen/src/com_metadata.rs b/tools/dynwinrt-codegen/src/com_metadata.rs index 477283c7..b117f62e 100644 --- a/tools/dynwinrt-codegen/src/com_metadata.rs +++ b/tools/dynwinrt-codegen/src/com_metadata.rs @@ -264,6 +264,8 @@ pub struct RawComMethod { pub enum RawExactMethodContractKind { FixedCapacityBytes, UnsafePrivateData, + StatStg, + Malloc, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1285,6 +1287,8 @@ fn apply_exact_method_contract( return; } } + RawExactMethodContractKind::StatStg => {} + RawExactMethodContractKind::Malloc => {} } } @@ -1314,6 +1318,62 @@ fn registered_exact_method_contract( "IMFAttributes::GetBlob documents a caller-allocated byte buffer with an input byte capacity and output actual byte count", "https://learn.microsoft.com/windows/win32/api/mfobjects/nf-mfobjects-imfattributes-getblob", ), + ("Windows.Win32.System.Com", "IStream", "Stat") => ( + RawExactMethodContractKind::StatStg, + 0, + 1, + None, + "IStream::Stat returns an owned STATSTG whose nested name is allocated with CoTaskMem", + "https://learn.microsoft.com/windows/win32/api/objidl/nf-objidl-istream-stat", + ), + ("Windows.Win32.System.Com", "IMalloc", "Alloc") => ( + RawExactMethodContractKind::Malloc, + 0, + 0, + None, + "IMalloc::Alloc returns allocator-owned memory that must be released with the same IMalloc::Free", + "https://learn.microsoft.com/windows/win32/api/objidl/nf-objidl-imalloc-alloc", + ), + ("Windows.Win32.System.Com", "IMalloc", "Realloc") => ( + RawExactMethodContractKind::Malloc, + 0, + 1, + None, + "IMalloc::Realloc consumes a nullable allocation address and returns memory owned by the same allocator", + "https://learn.microsoft.com/windows/win32/api/objidl/nf-objidl-imalloc-realloc", + ), + ("Windows.Win32.System.Com", "IMalloc", "Free") => ( + RawExactMethodContractKind::Malloc, + 0, + 0, + None, + "IMalloc::Free accepts only memory allocated by a compatible IMalloc instance", + "https://learn.microsoft.com/windows/win32/api/objidl/nf-objidl-imalloc-free", + ), + ("Windows.Win32.System.Com", "IMalloc", "GetSize") => ( + RawExactMethodContractKind::Malloc, + 0, + 0, + None, + "IMalloc::GetSize accepts a nullable allocation address owned by a compatible allocator", + "https://learn.microsoft.com/windows/win32/api/objidl/nf-objidl-imalloc-getsize", + ), + ("Windows.Win32.System.Com", "IMalloc", "DidAlloc") => ( + RawExactMethodContractKind::Malloc, + 0, + 0, + None, + "IMalloc::DidAlloc inspects a nullable allocation address without taking ownership", + "https://learn.microsoft.com/windows/win32/api/objidl/nf-objidl-imalloc-didalloc", + ), + ("Windows.Win32.System.Com", "IMalloc", "HeapMinimize") => ( + RawExactMethodContractKind::Malloc, + 0, + 0, + None, + "IMalloc::HeapMinimize has no parameters and no return value", + "https://learn.microsoft.com/windows/win32/api/objidl/nf-objidl-imalloc-heapminimize", + ), ("Windows.Win32.Graphics.Dxgi", "IDXGIObject", "GetPrivateData") => ( RawExactMethodContractKind::UnsafePrivateData, 2, @@ -1381,6 +1441,8 @@ fn registered_exact_method_contract( "ID3D11DeviceChild" | "ID3D11Device" => "Windows.Win32.Graphics.Direct3D11", "ID3D12Object" => "Windows.Win32.Graphics.Direct3D12", "IDMLObject" => "Windows.Win32.AI.MachineLearning.DirectML", + "IStream" => "Windows.Win32.System.Com", + "IMalloc" => "Windows.Win32.System.Com", _ => unreachable!("matched exact method interface"), }, declaring_interface: match interface { @@ -1392,6 +1454,8 @@ fn registered_exact_method_contract( "ID3D11Device" => "ID3D11Device", "ID3D12Object" => "ID3D12Object", "IDMLObject" => "IDMLObject", + "IStream" => "IStream", + "IMalloc" => "IMalloc", _ => unreachable!("matched exact method interface"), }, declaring_iid: match kind { @@ -1408,10 +1472,22 @@ fn registered_exact_method_contract( "IDMLObject" => "c8263aac-9e0c-4a2d-9b8e-007521a3317c", _ => unreachable!("matched exact private-data interface"), }, + RawExactMethodContractKind::StatStg => "0000000c-0000-0000-c000-000000000046", + RawExactMethodContractKind::Malloc => "00000002-0000-0000-c000-000000000046", }, method_name: match kind { RawExactMethodContractKind::FixedCapacityBytes => "GetBlob", RawExactMethodContractKind::UnsafePrivateData => "GetPrivateData", + RawExactMethodContractKind::StatStg => "Stat", + RawExactMethodContractKind::Malloc => match method { + "Alloc" => "Alloc", + "Realloc" => "Realloc", + "Free" => "Free", + "GetSize" => "GetSize", + "DidAlloc" => "DidAlloc", + "HeapMinimize" => "HeapMinimize", + _ => unreachable!("matched exact IMalloc method"), + }, }, vtable_index: match (interface, method) { ("IMFAttributes", "GetBlob") => 15, @@ -1421,6 +1497,13 @@ fn registered_exact_method_contract( ("ID3D11DeviceChild", "GetPrivateData") => 4, ("ID3D11Device", "GetPrivateData") => 34, ("ID3D12Object", "GetPrivateData") | ("IDMLObject", "GetPrivateData") => 3, + ("IStream", "Stat") => 12, + ("IMalloc", "Alloc") => 3, + ("IMalloc", "Realloc") => 4, + ("IMalloc", "Free") => 5, + ("IMalloc", "GetSize") => 6, + ("IMalloc", "DidAlloc") => 7, + ("IMalloc", "HeapMinimize") => 8, _ => unreachable!("matched exact method identity"), }, buffer_param_index: buffer, @@ -1561,6 +1644,17 @@ pub(crate) fn validate_exact_method_contract( if &expected != contract { return Err("exact method contract evidence does not match the registry".into()); } + if raw.declaring_namespace != contract.declaring_namespace + || raw.declaring_interface != contract.declaring_interface + || !raw + .declaring_iid + .eq_ignore_ascii_case(contract.declaring_iid) + { + return Err(format!( + "{}.{} declaring interface identity no longer matches exact contract evidence", + contract.declaring_interface, contract.method_name + )); + } if current_namespace == contract.declaring_namespace && current_interface == contract.declaring_interface && !current_iid.eq_ignore_ascii_case(contract.declaring_iid) @@ -1621,15 +1715,51 @@ pub(crate) fn validate_exact_method_contract( let optional = contract.declaring_interface != "IDXGIObject"; raw_private_data_shape(raw, optional) } + RawExactMethodContractKind::StatStg => { + raw_method_shape(raw) + == "Stat@12(pstatstg:out:required:noconstattr:Windows.Win32.System.Com.STATSTG[Struct]/ptr1/Mutable,grfStatFlag:in:required:noconstattr:u32/ptr0/Unspecified)->Windows.Win32.Foundation.HRESULT[Struct]/ptr0/Unspecified/underlying=i32/ptr0/Unspecified:plain_hresult:not_enumerator_next" + } + RawExactMethodContractKind::Malloc => { + let expected = match contract.method_name { + "Alloc" => { + "Alloc@3(cb:in:required:noconstattr:usize/ptr0/Unspecified)->void/ptr1/Mutable:plain_hresult:not_enumerator_next" + } + "Realloc" => { + "Realloc@4(pv:in:optional:noconstattr:void/ptr1/Mutable,cb:in:required:noconstattr:usize/ptr0/Unspecified)->void/ptr1/Mutable:plain_hresult:not_enumerator_next" + } + "Free" => { + "Free@5(pv:in:optional:noconstattr:void/ptr1/Mutable)->void/ptr0/Unspecified:plain_hresult:not_enumerator_next" + } + "GetSize" => { + "GetSize@6(pv:in:optional:noconstattr:void/ptr1/Mutable)->usize/ptr0/Unspecified:plain_hresult:not_enumerator_next" + } + "DidAlloc" => { + "DidAlloc@7(pv:in:optional:noconstattr:void/ptr1/Mutable)->i32/ptr0/Unspecified:plain_hresult:not_enumerator_next" + } + "HeapMinimize" => { + "HeapMinimize@8()->void/ptr0/Unspecified:plain_hresult:not_enumerator_next" + } + _ => return Err("unknown exact IMalloc method contract".into()), + }; + raw_method_shape(raw) == expected + } + }; + let indices_valid = match contract.kind { + RawExactMethodContractKind::FixedCapacityBytes + | RawExactMethodContractKind::UnsafePrivateData + | RawExactMethodContractKind::StatStg => { + contract.buffer_param_index < raw.params.len() + && contract.capacity_param_index < raw.params.len() + && contract + .actual_length_param_index + .is_none_or(|index| index < raw.params.len()) + } + RawExactMethodContractKind::Malloc => true, }; if !valid_shape || raw.semantic_hresult.is_some() || raw.enumerator_next.is_some() - || contract.buffer_param_index >= raw.params.len() - || contract.capacity_param_index >= raw.params.len() - || contract - .actual_length_param_index - .is_some_and(|index| index >= raw.params.len()) + || !indices_valid { return Err(format!( "{}.{} signature no longer matches exact contract evidence", diff --git a/tools/dynwinrt-codegen/tests/win32_com_test.rs b/tools/dynwinrt-codegen/tests/win32_com_test.rs index 1ac2a396..929b1a51 100644 --- a/tools/dynwinrt-codegen/tests/win32_com_test.rs +++ b/tools/dynwinrt-codegen/tests/win32_com_test.rs @@ -822,8 +822,9 @@ fn real_metadata_projects_complete_idispatch_with_automation_compounds() { assert!(!output.js.contains("_result.excepInfo")); assert!(!output.js.contains("_result.argErr")); assert!(!output.js.contains("nativeStructType")); + assert!(output.js.contains("DynCom.iidPointer(WinGuid.parse(riid))")); assert!(output.dts.contains( - "invoke(dispIdMember: number, riid: Buffer | Uint8Array, lcid: number, wFlags: DISPATCH_FLAGS, dispParams: DynComDispatchParams, options?: { result?: boolean; excepInfo?: boolean; argErr?: boolean }): { result?: DynComVariant };" + "invoke(dispIdMember: number, riid: string, lcid: number, wFlags: DISPATCH_FLAGS, dispParams: DynComDispatchParams, options?: { result?: boolean; excepInfo?: boolean; argErr?: boolean }): { result?: DynComVariant };" )); let invoke_raw_index = dispatch @@ -3825,6 +3826,120 @@ fn bstr_outputs_are_decoded_and_freed() { ); } +#[test] +fn com_p0_interfaces_generate_with_exact_pointer_and_ownership_contracts() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let malloc = + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.System.Com", "IMalloc") + .expect("IMalloc must exist"); + let malloc_output = com::generate_com_interface_files(&malloc, &win32_winmd()) + .expect("IMalloc generation should succeed"); + assert!( + malloc_output + .dts + .contains("alloc(cb: bigint): DynComAllocation | null;") + && malloc_output.dts.contains( + "realloc(pv: DynComAllocation | null, cb: bigint): DynComAllocation | null;" + ) + && malloc_output + .dts + .contains("free(pv: DynComAllocation | null): void;"), + "{}", + malloc_output.dts + ); + assert!( + malloc_output + .js + .contains("return DynCom.takeMallocAllocation(this._obj, _out);") + && malloc_output + .js + .contains("DynCom.mallocAllocationPointer(this._obj, pv)") + && malloc_output + .js + .contains("DynCom.mallocInspectionPointer(pv)") + && !malloc_output + .js + .contains("didAlloc(pv) {\n const _out = _IMalloc.method(7).invoke(this._obj, [DynCom.mallocAllocationPointer(this._obj, pv)])") + && malloc_output + .js + .contains("DynCom.takeMallocAllocationPointer(this._obj, pv)") + && malloc_output.js.contains("const _mallocSize = BigInt(cb);") + && malloc_output + .js + .contains("DynCom.finishMallocReallocation(this._obj, pv, _mallocSize, _out)"), + "{}", + malloc_output.js + ); + + let class_factory = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.Com", + "IClassFactory", + ) + .expect("IClassFactory must exist"); + let class_factory_output = com::generate_com_interface_files(&class_factory, &win32_winmd()) + .expect("IClassFactory generation should succeed"); + assert!( + class_factory_output.dts.contains( + "createInstance(pUnkOuter: DynWinRtValue | null, riid: string): DynWinRtValue;" + ) && class_factory_output + .js + .contains("return DynCom.adoptComPointer(_out, _iid);"), + "{}\n{}", + class_factory_output.dts, + class_factory_output.js + ); + + let create_error = com_metadata::parse_com_interface( + &win32_winmd(), + "Windows.Win32.System.Ole", + "ICreateErrorInfo", + ) + .expect("ICreateErrorInfo must exist"); + let create_error_output = com::generate_com_interface_files(&create_error, &win32_winmd()) + .expect("ICreateErrorInfo generation should succeed"); + assert!( + create_error_output + .dts + .contains("setGUID(rguid: string): void;") + && create_error_output + .js + .contains("DynCom.iidPointer(WinGuid.parse(rguid))"), + "{}\n{}", + create_error_output.dts, + create_error_output.js + ); +} + +#[test] +fn imalloc_exact_contract_fails_closed_on_metadata_drift() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let mut malloc = + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.System.Com", "IMalloc") + .expect("IMalloc must exist"); + let alloc = malloc + .raw_methods + .as_mut() + .expect("raw COM methods") + .iter_mut() + .find(|method| method.metadata_name == "Alloc") + .expect("IMalloc::Alloc"); + alloc.return_type.pointer_depth = 0; + let error = com::generate_com_interface_files(&malloc, &win32_winmd()).unwrap_err(); + assert!( + error.contains("IMalloc.Alloc signature no longer matches exact contract evidence"), + "{error}" + ); +} + #[test] fn unsigned_enum_values_preserve_their_value() { if !win32_available() { @@ -3943,9 +4058,61 @@ fn real_win32_pods_cover_value_pointer_out_and_inout_shapes() { let bind_ctx = com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.System.Com", "IBindCtx") .expect("IBindCtx must exist"); - let error = com::generate_com_interface_files(&bind_ctx, &win32_winmd()) - .expect_err("BIND_OPTS must fail until cbStruct initialization is modeled"); - assert!(error.contains("cbStruct"), "{error}"); + let bind_output = com::generate_com_interface_files(&bind_ctx, &win32_winmd()) + .expect("BIND_OPTS must project with an exact cbStruct initializer"); + assert!( + bind_output + .dts + .contains("export declare function createBIND_OPTS(bytes?: Buffer): BIND_OPTS;") + && bind_output + .dts + .contains("setBindOptions(pbindopts: BIND_OPTS): void;") + && bind_output + .dts + .contains("getBindOptions(pbindopts: BIND_OPTS): BIND_OPTS;"), + "{}", + bind_output.dts + ); + assert!( + bind_output + .js + .contains("\"initializers\":[{\"kind\":\"sizeOfLayout\",\"field\":\"cbStruct\"}]") + && bind_output + .js + .contains(".addInOut(DynCom.nativeStructType(_nativeLayout_BIND_OPTS))"), + "{}", + bind_output.js + ); + + let stream = + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.System.Com", "IStream") + .expect("IStream must exist"); + let stream_output = com::generate_com_interface_files(&stream, &win32_winmd()) + .expect("the complete IStream inheritance chain must project"); + assert!( + stream_output + .dts + .contains("stat(grfStatFlag: number): DynComStatStg;") + && stream_output + .dts + .contains("seek(dlibMove: bigint, origin: STREAM_SEEK): bigint;") + && stream_output.dts.contains("clone(): DynWinRtValue;"), + "{}", + stream_output.dts + ); + assert!( + stream_output + .js + .contains(".addMethodAt(3, 'Read', new DynComMethodSig().addCallerOutputBuffer") + && stream_output.js.contains( + ".addMethodAt(12, 'Stat', new DynComMethodSig().addOut(DynCom.statStgType())" + ) + && stream_output + .js + .contains("return DynCom.takeStatStg(_out);"), + "{}", + stream_output.js + ); let running_object_table = com_metadata::parse_com_interface( &win32_winmd(), @@ -3977,6 +4144,48 @@ fn real_win32_pods_cover_value_pointer_out_and_inout_shapes() { ); } +#[test] +fn istream_stat_exact_contract_fails_closed_on_metadata_drift() { + if !win32_available() { + eprintln!("Skipping: Win32 winmd not available"); + return; + } + + let mut stream = + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.System.Com", "IStream") + .expect("IStream must exist"); + let stat = stream + .raw_methods + .as_mut() + .expect("raw COM methods") + .iter_mut() + .find(|method| method.metadata_name == "Stat") + .expect("IStream::Stat"); + stat.params[0].name = "drifted".into(); + let error = com::generate_com_interface_files(&stream, &win32_winmd()).unwrap_err(); + assert!( + error.contains("IStream.Stat signature no longer matches exact contract evidence"), + "{error}" + ); + + let mut stream = + com_metadata::parse_com_interface(&win32_winmd(), "Windows.Win32.System.Com", "IStream") + .expect("IStream must exist"); + let stat = stream + .raw_methods + .as_mut() + .expect("raw COM methods") + .iter_mut() + .find(|method| method.metadata_name == "Stat") + .expect("IStream::Stat"); + stat.declaring_iid = "11111111-2222-3333-4444-555555555555".into(); + let error = com::generate_com_interface_files(&stream, &win32_winmd()).unwrap_err(); + assert!( + error.contains("IStream.Stat declaring interface identity no longer matches"), + "{error}" + ); +} + #[test] fn shelllink_other_methods_remain_unchanged_with_pod_support() { if !win32_available() {