From ec28bb2311e3f7dd36a7e06109f1d9b92a3a2bd6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 30 Dec 2025 14:06:42 -0800 Subject: [PATCH 01/16] Remove IMapViewVftbl struct definition Deleted the IMapViewVftbl struct from InteropServices Vtables as part of code cleanup or refactoring. This may indicate a change in how IMapView interfaces are handled or a removal of unused interop code. --- .../InteropServices/Vtables/IMapViewVftbl.cs | 29 ------------------- 1 file changed, 29 deletions(-) delete mode 100644 src/WinRT.Runtime2/InteropServices/Vtables/IMapViewVftbl.cs diff --git a/src/WinRT.Runtime2/InteropServices/Vtables/IMapViewVftbl.cs b/src/WinRT.Runtime2/InteropServices/Vtables/IMapViewVftbl.cs deleted file mode 100644 index 0fb57d8e6..000000000 --- a/src/WinRT.Runtime2/InteropServices/Vtables/IMapViewVftbl.cs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Runtime.InteropServices; -using Windows.Foundation; - -namespace WindowsRuntime.InteropServices; - -/// -/// Binding type for the IMapView<K, V> interface vtable. -/// -/// -[StructLayout(LayoutKind.Sequential)] -internal unsafe struct IMapViewVftbl -{ - public delegate* unmanaged[MemberFunction] QueryInterface; - public delegate* unmanaged[MemberFunction] AddRef; - public delegate* unmanaged[MemberFunction] Release; - public delegate* unmanaged[MemberFunction] GetIids; - public delegate* unmanaged[MemberFunction] GetRuntimeClassName; - public delegate* unmanaged[MemberFunction] GetTrustLevel; - - // See notes in 'IVectorViewVftbl' regarding ABI mismatches for the by-value parameters below - public delegate* unmanaged[MemberFunction] Lookup; - public delegate* unmanaged[MemberFunction] get_Size; - public delegate* unmanaged[MemberFunction] HasKey; - public delegate* unmanaged[MemberFunction] Split; -} \ No newline at end of file From 94398f7a7bb72b6a93b225dc653068a3afd0defe Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 1 Jan 2026 20:58:01 -0800 Subject: [PATCH 02/16] Add IReadOnlyDictionarySplitAdapter implementation Introduces IReadOnlyDictionarySplitAdapter, an adapter for IReadOnlyDictionary that maintains keys in sorted order and supports efficient splitting. This class is intended for internal use and is marked obsolete, providing binary search-based lookups and split functionality for Windows Runtime interop scenarios. --- ...nlyDictionarySplitAdapter{TKey, TValue}.cs | 216 ++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionarySplitAdapter{TKey, TValue}.cs diff --git a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionarySplitAdapter{TKey, TValue}.cs b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionarySplitAdapter{TKey, TValue}.cs new file mode 100644 index 000000000..11bd1a16f --- /dev/null +++ b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionarySplitAdapter{TKey, TValue}.cs @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace WindowsRuntime.InteropServices; + +/// +/// An adapter for with keys sorted in ascending order. +/// +/// The type of keys in the dictionary. +/// The type of values in the dictionary. +/// +[Obsolete(WindowsRuntimeConstants.PrivateImplementationDetailObsoleteMessage, + DiagnosticId = WindowsRuntimeConstants.PrivateImplementationDetailObsoleteDiagnosticId, + UrlFormat = WindowsRuntimeConstants.CsWinRTDiagnosticsUrlFormat)] +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed class IReadOnlyDictionarySplitAdapter : IReadOnlyDictionary +{ + /// + /// The array of key-value pairs stored in the adapter. + /// + /// + /// These items are sorted with . + /// + private readonly KeyValuePair[] _items; + + /// + /// The index of the first item in the array that can be used by this instance. + /// + private readonly int _firstItemIndex; + + /// + /// The index of the last item in the array that can be used by this instance. + /// + private readonly int _lastItemIndex; + + /// + /// The instance, if initialized. + /// + private ReadOnlyDictionaryKeyCollection? _keys; + + /// + /// The instance, if initialized. + /// + private ReadOnlyDictionaryValueCollection? _values; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The array of key-value pairs to store in the adapter. + /// The index of the first item in the array that can be used by this instance. + /// The index of the last item in the array that can be used by this instance. + private IReadOnlyDictionarySplitAdapter(KeyValuePair[] items, int firstItemIndex, int lastItemIndex) + { + _items = items; + _firstItemIndex = firstItemIndex; + _lastItemIndex = lastItemIndex; + } + + /// + /// Creates a new instance with the specified parameters. + /// + /// The source instance. + internal IReadOnlyDictionarySplitAdapter(IReadOnlyDictionary dictionary) + { + ArgumentNullException.ThrowIfNull(dictionary); + + _firstItemIndex = 0; + _lastItemIndex = dictionary.Count - 1; + _items = CreateKeyValueArray(dictionary); + } + + /// + public IEnumerable Keys => _keys ??= new ReadOnlyDictionaryKeyCollection(this); + + /// + public IEnumerable Values => _values ??= new ReadOnlyDictionaryValueCollection(this); + + /// + public int Count => _lastItemIndex - _firstItemIndex + 1; + + /// + public TValue this[TKey key] + { + get + { + // Try to lookup the key, and throw the right exception if it's not found. + // We don't need to adjust the 'HRESULT' here, it'll be done separately. + if (!TryGetValue(key, out TValue? value)) + { + [DoesNotReturn] + static void ThrowKeyNotFoundException() => throw new InvalidOperationException("Arg_KeyNotFoundWithKey"); + + ThrowKeyNotFoundException(); + } + + return value!; + } + } + + /// + /// Splits the dictionary into two instances, each representing a sorted half. + /// + /// One half of the original dictionary. + /// The second half of the original dictionary. + public void Split( + [NotNullIfNotNull(nameof(second))] out IReadOnlyDictionary? first, + [NotNullIfNotNull(nameof(first))] out IReadOnlyDictionary? second) + { + if (Count < 2) + { + first = null; + second = null; + + return; + } + + int pivot = (int)(uint)(((ulong)(uint)_firstItemIndex + (uint)_lastItemIndex) / 2UL); + + first = new IReadOnlyDictionarySplitAdapter(_items, _firstItemIndex, pivot); + second = new IReadOnlyDictionarySplitAdapter(_items, pivot + 1, _lastItemIndex); + } + + /// + public bool ContainsKey(TKey key) + { + return TryGetValue(key, out _); + } + + /// + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + KeyValuePair searchKey = new(key, default!); + + // Do a binary search for the input key (the array is sorted upon construction) + int index = Array.BinarySearch( + array: _items, + index: _firstItemIndex, + length: Count, + value: searchKey, + comparer: KeyValuePairComparer.Instance); + + // If the key was not found, just stop here + if (index < 0) + { + value = default; + + return false; + } + + // Lookup the found pair in the array and return the value + value = _items[index].Value; + + return true; + } + + /// + public IEnumerator> GetEnumerator() + { + return new ArraySegment>(_items, _firstItemIndex, Count).GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Prepares the sorted array of key-value pairs from an input dictionary. + /// + /// The source instance. + /// The resulting sorted array of key-value pairs from an input dictionary. + private static KeyValuePair[] CreateKeyValueArray(IReadOnlyDictionary dictionary) + { + KeyValuePair[] array = new KeyValuePair[dictionary.Count]; + + // Get all key-value pairs from the dictionary (we avoid 'ToArray' to avoid using LINQ) + using (IEnumerator> enumerator = dictionary.GetEnumerator()) + { + int i = 0; + + while (enumerator.MoveNext()) + { + array[i++] = enumerator.Current; + } + } + + // Sort the items based on their keys + Array.Sort(array, KeyValuePairComparer.Instance); + + return array; + } + + /// + /// A comparer for that only checks . + /// + private sealed class KeyValuePairComparer : IComparer> + { + /// + /// The singleton instance. + /// + public static readonly KeyValuePairComparer Instance = new(); + + /// + public int Compare(KeyValuePair x, KeyValuePair y) + { + return Comparer.Default.Compare(x.Key, y.Key); + } + } +} \ No newline at end of file From 7a4a11785be1bdcc606b2165c8128d3f05bf52a0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 1 Jan 2026 20:58:12 -0800 Subject: [PATCH 03/16] Add IReadOnlyDictionaryAdapter for IMapView support Introduces a static adapter class to expose IReadOnlyDictionary as Windows.Foundation.Collections.IMapView. Provides Lookup, Size, and Split methods for compatibility with WinRT map view interfaces. --- ...ReadOnlyDictionaryAdapter{TKey, TValue}.cs | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapter{TKey, TValue}.cs diff --git a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapter{TKey, TValue}.cs b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapter{TKey, TValue}.cs new file mode 100644 index 000000000..bb03cefe6 --- /dev/null +++ b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapter{TKey, TValue}.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace WindowsRuntime.InteropServices; + +/// +/// A stateless adapter for , to be exposed as Windows.Foundation.Collections.IMapView<K, V>. +/// +/// The type of keys in the dictionary. +/// The type of values in the dictionary. +/// +[Obsolete(WindowsRuntimeConstants.PrivateImplementationDetailObsoleteMessage, + DiagnosticId = WindowsRuntimeConstants.PrivateImplementationDetailObsoleteDiagnosticId, + UrlFormat = WindowsRuntimeConstants.CsWinRTDiagnosticsUrlFormat)] +[EditorBrowsable(EditorBrowsableState.Never)] +public static class IReadOnlyDictionaryAdapter +{ + /// + /// Returns the item at the specified key in the map view. + /// + /// The wrapped instance. + /// The key to locate in the map view. + /// The value, if an item with the specified key exists. + /// + public static TValue Lookup(IReadOnlyDictionary dictionary, TKey key) + { + // Try to lookup the key, and throw the right exception if it's not found. + // This is more efficient than using the indexer and then catching the + // exception if the lookup fails, adjusting the 'HRESUT', and re-throwing. + if (!dictionary.TryGetValue(key, out TValue? value)) + { + [DoesNotReturn] + static void ThrowKeyNotFoundException() + { + throw new InvalidOperationException("Arg_KeyNotFoundWithKey") + { + HResult = WellKnownErrorCodes.E_BOUNDS + }; + } + + ThrowKeyNotFoundException(); + } + + return value; + } + + /// + /// Gets the number of elements in the map. + /// + /// The wrapped instance. + /// The number of elements in the map. + /// + public static uint Size(IReadOnlyDictionary dictionary) + { + return (uint)dictionary.Count; + } + + /// + /// Splits the map view into two views. + /// + /// The wrapped instance. + /// One half of the original map. + /// The second half of the original map. + /// + public static void Split( + IReadOnlyDictionary dictionary, + [NotNullIfNotNull(nameof(second))] out IReadOnlyDictionary? first, + [NotNullIfNotNull(nameof(first))] out IReadOnlyDictionary? second) + { + // If the input dictionary doesn't have enough items, just set both halves to 'null' + if (dictionary.Count < 2) + { + first = null; + second = null; + + return; + } + + // Get the split adapter, if we don't have one already. We can reuse the data + // from the first one we create, so we don't re-allocate and re-sort the full + // set of key-value pairs from the original input dictionary. + if (dictionary is not IReadOnlyDictionarySplitAdapter splitAdapter) + { + splitAdapter = new IReadOnlyDictionarySplitAdapter(dictionary); + } + + splitAdapter.Split(out first, out second); + } +} \ No newline at end of file From c7854e9c4e26df4365b4786b1ca298b69e010fec Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 2 Jan 2026 12:26:38 -0800 Subject: [PATCH 04/16] Add IReadOnlyDictionaryAdapterExtensions for optimized lookup Introduces IReadOnlyDictionaryAdapterExtensions with a Lookup extension method for IReadOnlyDictionary that accepts ReadOnlySpan keys. This method optimizes lookups by avoiding string allocations when possible, using alternate lookup mechanisms for Dictionary, ConcurrentDictionary, and FrozenDictionary types. --- .../IReadOnlyDictionaryAdapterExtensions.cs | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapterExtensions.cs diff --git a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapterExtensions.cs b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapterExtensions.cs new file mode 100644 index 000000000..3c01aa58f --- /dev/null +++ b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapterExtensions.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Collections.Frozen; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + +#pragma warning disable IDE0045 + +namespace WindowsRuntime.InteropServices; + +/// +/// Extensions for the type. +/// +[Obsolete(WindowsRuntimeConstants.PrivateImplementationDetailObsoleteMessage, + DiagnosticId = WindowsRuntimeConstants.PrivateImplementationDetailObsoleteDiagnosticId, + UrlFormat = WindowsRuntimeConstants.CsWinRTDiagnosticsUrlFormat)] +[EditorBrowsable(EditorBrowsableState.Never)] +public static class IReadOnlyDictionaryAdapterExtensions +{ + extension(IReadOnlyDictionaryAdapter) + { + /// + /// + /// This overload can be used to avoid a allocation on the caller side. + /// + public static TValue Lookup(IReadOnlyDictionary dictionary, ReadOnlySpan key) + { + bool found; + TValue? value; + + // Try to lookup via an alternate comparer, if we can get one. This allows us to avoid materializing + // the 'string' value for the key, whenever possible. In practice, these cases should pretty much + // cover almost all scenarios. Custom dictionaries are rare. We can't use an 'is' check for + // 'Dictionary', because we could have some + // user-defined type that reimplemented 'IReadOnlyDictionary.TryGetValue' with a + // different implementation that's not guaranteed to match what the alternate lookup does. So in + // those cases, we have to fallback to that method instead, hence the explicit type checks here. + // This is not needed for 'FrozenDictionary', as only the BCL can instantiate it. + if (dictionary.GetType() == typeof(Dictionary) && + Unsafe.As>(dictionary).TryGetAlternateLookup(out Dictionary.AlternateLookup> lookup1)) + { + found = lookup1.TryGetValue(key, out value); + } + else if (dictionary.GetType() == typeof(ConcurrentDictionary) && + Unsafe.As>(dictionary).TryGetAlternateLookup(out ConcurrentDictionary.AlternateLookup> lookup2)) + { + found = lookup2.TryGetValue(key, out value); + } + else if (dictionary is FrozenDictionary candidate3 && + candidate3.TryGetAlternateLookup(out FrozenDictionary.AlternateLookup> lookup3)) + { + found = lookup3.TryGetValue(key, out value); + } + else + { + found = dictionary.TryGetValue(key.ToString(), out value); + } + + // Throw the correct exception if the lookup failed + if (!found) + { + [DoesNotReturn] + static void ThrowKeyNotFoundException() + { + throw new InvalidOperationException("Arg_KeyNotFoundWithKey") + { + HResult = WellKnownErrorCodes.E_BOUNDS + }; + } + + ThrowKeyNotFoundException(); + } + + return value!; + } + } +} \ No newline at end of file From 0ddec04f4dacbf9974c3f4fcb80190f5e7cc13fa Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 2 Jan 2026 12:41:12 -0800 Subject: [PATCH 05/16] Add Lookup method for IReadOnlyDictionary2 interop Introduces a new Lookup method implementation for IReadOnlyDictionary2 interop types, including a dedicated factory and supporting references. Updates the builder to use this method in vtable construction and refines parameter rewrite tracking in IReadOnlyList1Impl. --- ...eDefinitionBuilder.IReadOnlyDictionary2.cs | 9 +- ...initionFactory.IReadOnlyDictionary2Impl.cs | 141 ++++++++++++++++++ ...hodDefinitionFactory.IReadOnlyList1Impl.cs | 2 +- .../References/InteropReferences.cs | 27 ++++ 4 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs index f820a6c93..320fc647a 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs @@ -582,6 +582,13 @@ public static void ImplType( ModuleDefinition module, out TypeDefinition implType) { + // Define the 'Lookup' method + MethodDefinition getAtMethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.Lookup( + readOnlyDictionaryType: readOnlyDictionaryType, + interopReferences: interopReferences, + emitState: emitState, + module: module); + Impl( interfaceType: ComInterfaceType.InterfaceIsIInspectable, ns: InteropUtf8NameFactory.TypeNamespace(readOnlyDictionaryType), @@ -591,7 +598,7 @@ public static void ImplType( interopReferences: interopReferences, module: module, implType: out implType, - vtableMethods: []); + vtableMethods: [getAtMethod]); // Track the type (it may be needed by COM interface entries for user-defined types) emitState.TrackTypeDefinition(implType, readOnlyDictionaryType, "Impl"); diff --git a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs new file mode 100644 index 000000000..ffd3814c1 --- /dev/null +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using AsmResolver.DotNet; +using AsmResolver.DotNet.Code.Cil; +using AsmResolver.DotNet.Signatures; +using AsmResolver.PE.DotNet.Cil; +using AsmResolver.PE.DotNet.Metadata.Tables; +using WindowsRuntime.InteropGenerator.Generation; +using WindowsRuntime.InteropGenerator.References; +using static AsmResolver.PE.DotNet.Cil.CilOpCodes; + +#pragma warning disable IDE1006 + +namespace WindowsRuntime.InteropGenerator.Factories; + +/// +/// A factory for interop method definitions. +/// +internal static partial class InteropMethodDefinitionFactory +{ + /// + /// Helpers for impl types for interfaces. + /// + public static class IReadOnlyDictionary2Impl + { + /// + /// Creates a for the Lookup export method. + /// + /// The for the type. + /// The instance to use. + /// The emit state for this invocation. + /// The interop module being built. + public static MethodDefinition Lookup( + GenericInstanceTypeSignature readOnlyDictionaryType, + InteropReferences interopReferences, + InteropGeneratorEmitState emitState, + ModuleDefinition module) + { + TypeSignature keyType = readOnlyDictionaryType.TypeArguments[0]; + TypeSignature valueType = readOnlyDictionaryType.TypeArguments[1]; + + // Define the 'Lookup' method as follows: + // + // [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + // private static int Lookup(void* thisPtr, key, * result) + MethodDefinition lookupMethod = new( + name: "Lookup"u8, + attributes: MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.Static, + signature: MethodSignature.CreateStatic( + returnType: module.CorLibTypeFactory.Int32, + parameterTypes: [ + module.CorLibTypeFactory.Void.MakePointerType(), + keyType.GetAbiType(interopReferences).Import(module), + valueType.GetAbiType(interopReferences).Import(module).MakePointerType()])) + { + CustomAttributes = { InteropCustomAttributeFactory.UnmanagedCallersOnly(interopReferences, module) } + }; + + // Declare the local variables: + // [0]: '' (for 'thisObject') + // [1]: 'int' (the 'HRESULT' to return) + CilLocalVariable loc_0_thisObject = new(readOnlyDictionaryType.Import(module)); + CilLocalVariable loc_1_hresult = new(module.CorLibTypeFactory.Int32); + + // Labels for jumps + CilInstruction nop_beforeTry = new(Nop); + CilInstruction ldarg_0_tryStart = new(Ldarg_0); + CilInstruction nop_parameter1Rewrite = new(Nop); + CilInstruction ldloc_1_returnHResult = new(Ldloc_1); + CilInstruction call_catchStartMarshalException = new(Call, interopReferences.RestrictedErrorInfoExceptionMarshallerConvertToUnmanaged.Import(module)); + CilInstruction nop_convertToUnmanaged = new(Nop); + + // Create a method body for the 'Lookup' method + lookupMethod.CilMethodBody = new CilMethodBody() + { + LocalVariables = { loc_0_thisObject, loc_1_hresult }, + Instructions = + { + // Return 'E_POINTER' if the argument is 'null' + { Ldarg_2 }, + { Ldc_I4_0 }, + { Conv_U }, + { Bne_Un_S, nop_beforeTry.CreateLabel() }, + { Ldc_I4, unchecked((int)0x80004003) }, + { Ret }, + { nop_beforeTry }, + + // '.try' code + { ldarg_0_tryStart }, + { Call, interopReferences.ComInterfaceDispatchGetInstance.MakeGenericInstanceMethod(readOnlyDictionaryType).Import(module) }, + { Stloc_0 }, + { Ldarg_2 }, + { Ldloc_0 }, + { nop_parameter1Rewrite }, + { Call, interopReferences.IReadOnlyDictionaryAdapter2Lookup(keyType, valueType).Import(module) }, + { nop_convertToUnmanaged }, + { Ldc_I4_0 }, + { Stloc_1 }, + { Leave_S, ldloc_1_returnHResult.CreateLabel() }, + + // '.catch' code + { call_catchStartMarshalException }, + { Stloc_1 }, + { Leave_S, ldloc_1_returnHResult.CreateLabel() }, + + // Return the 'HRESULT' from location [1] + { ldloc_1_returnHResult }, + { Ret } + }, + ExceptionHandlers = + { + new CilExceptionHandler + { + HandlerType = CilExceptionHandlerType.Exception, + TryStart = ldarg_0_tryStart.CreateLabel(), + TryEnd = call_catchStartMarshalException.CreateLabel(), + HandlerStart = call_catchStartMarshalException.CreateLabel(), + HandlerEnd = ldloc_1_returnHResult.CreateLabel(), + ExceptionType = interopReferences.Exception.Import(module) + } + } + }; + + // Track rewriting the parameter for this method + emitState.TrackManagedParameterMethodRewrite( + parameterType: keyType, + method: lookupMethod, + marker: nop_parameter1Rewrite, + parameterIndex: 1); + + // Track the method for rewrite to marshal the result value + emitState.TrackRetValValueMethodRewrite( + retValType: valueType, + method: lookupMethod, + marker: nop_convertToUnmanaged); + + return lookupMethod; + } + } +} \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyList1Impl.cs b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyList1Impl.cs index 82a02a032..fe8600b85 100644 --- a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyList1Impl.cs +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyList1Impl.cs @@ -356,7 +356,7 @@ public static MethodDefinition IndexOf( ? interopReferences.ReadOnlySpanChar : elementType; - // Track rewriting the two parameters for this method + // Track rewriting the parameter for this method emitState.TrackManagedParameterMethodRewrite( parameterType: parameterType, method: indexOfImplMethod, diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs index eb97ffed1..dc448cdfa 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -698,6 +698,16 @@ public InteropReferences( /// public TypeReference IDictionaryMethods2 => field ??= _windowsRuntimeModule.CreateTypeReference("WindowsRuntime.InteropServices"u8, "IDictionaryMethods`2"u8); + /// + /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryAdapter<TKey, TValue>. + /// + public TypeReference IReadOnlyDictionaryAdapter2 => field ??= _windowsRuntimeModule.CreateTypeReference("WindowsRuntime.InteropServices"u8, "IReadOnlyDictionaryAdapter`2"u8); + + /// + /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryAdapterExtensions. + /// + public TypeReference IReadOnlyDictionaryAdapterExtensions => field ??= _windowsRuntimeModule.CreateTypeReference("WindowsRuntime.InteropServices"u8, "IReadOnlyDictionaryAdapterExtensions"u8); + /// /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryMethods. /// @@ -3688,6 +3698,23 @@ public MethodSpecification IReadOnlyDictionaryMethods2TryGetValue(TypeSignature .MakeGenericInstanceMethod(mapViewMethods.ToReferenceTypeSignature()); } + /// + /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryAdapter<TKey, TValue>.Lookup. + /// + /// The input key type. + /// The input value type. + public MemberReference IReadOnlyDictionaryAdapter2Lookup(TypeSignature keyType, TypeSignature valueType) + { + return IReadOnlyDictionaryAdapter2 + .MakeGenericReferenceType(keyType, valueType) + .ToTypeDefOrRef() + .CreateMemberReference("Lookup"u8, MethodSignature.CreateStatic( + returnType: new GenericParameterSignature(GenericParameterType.Type, 1), + parameterTypes: [ + IReadOnlyDictionary2.MakeGenericReferenceType(keyType, valueType), + keyType])); + } + /// /// Gets the for . /// From 99938364395bf19ca0b9949e2ccd52faa65df0c9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 2 Jan 2026 13:39:19 -0800 Subject: [PATCH 06/16] Add Import extension for IMethodDescriptor Introduced an Import extension method for IMethodDescriptor to allow importing method descriptors into a module using the default importer. This enhances consistency and usability alongside existing import extensions. --- .../Extensions/ImportExtensions.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/WinRT.Interop.Generator/Extensions/ImportExtensions.cs b/src/WinRT.Interop.Generator/Extensions/ImportExtensions.cs index 77a1f99a0..7203b47b3 100644 --- a/src/WinRT.Interop.Generator/Extensions/ImportExtensions.cs +++ b/src/WinRT.Interop.Generator/Extensions/ImportExtensions.cs @@ -33,6 +33,17 @@ public static IMethodDefOrRef Import(this IMethodDefOrRef methodDefOrRef, Module return (IMethodDefOrRef)methodDefOrRef.ImportWith(module.DefaultImporter); } + /// + /// Imports a method descriptor or reference into a module using the default reference importer. + /// + /// The instance to import. + /// The module to import into. + /// The imported . + public static IMethodDescriptor Import(this IMethodDescriptor methodDescriptor, ModuleDefinition module) + { + return (IMethodDescriptor)methodDescriptor.ImportWith(module.DefaultImporter); + } + /// /// Imports a type signature into a module using the default reference importer. /// From 0746a61a4bf8d1c4c3be2cfb9287a96f1ac14d1b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 2 Jan 2026 13:39:39 -0800 Subject: [PATCH 07/16] Refactor Lookup method for IReadOnlyDictionary2 interop Refactored the Lookup method generation to accept a lookupMethod parameter, enabling optimized handling for string key types and supporting both IReadOnlyDictionary and IDictionary interfaces. Updated related builder and reference code to support this change and added remarks for clarity. --- ...eDefinitionBuilder.IReadOnlyDictionary2.cs | 5 +++ ...initionFactory.IReadOnlyDictionary2Impl.cs | 39 +++++++++++++++---- ...hodDefinitionFactory.IReadOnlyList1Impl.cs | 3 ++ .../References/InteropReferences.cs | 16 ++++++++ 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs index 320fc647a..ed792de50 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Runtime.InteropServices; using AsmResolver.DotNet; using AsmResolver.DotNet.Code.Cil; @@ -582,9 +583,13 @@ public static void ImplType( ModuleDefinition module, out TypeDefinition implType) { + TypeSignature keyType = readOnlyDictionaryType.TypeArguments[0]; + TypeSignature valueType = readOnlyDictionaryType.TypeArguments[1]; + // Define the 'Lookup' method MethodDefinition getAtMethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.Lookup( readOnlyDictionaryType: readOnlyDictionaryType, + lookupMethod: interopReferences.IReadOnlyDictionaryAdapter2Lookup(keyType, valueType), interopReferences: interopReferences, emitState: emitState, module: module); diff --git a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs index ffd3814c1..de1745a22 100644 --- a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs @@ -28,11 +28,16 @@ public static class IReadOnlyDictionary2Impl /// Creates a for the Lookup export method. /// /// The for the type. + /// The interface method to invoke on . /// The instance to use. /// The emit state for this invocation. /// The interop module being built. + /// + /// This method can also be used to define the Lookup method for interfaces. + /// public static MethodDefinition Lookup( GenericInstanceTypeSignature readOnlyDictionaryType, + MemberReference lookupMethod, InteropReferences interopReferences, InteropGeneratorEmitState emitState, ModuleDefinition module) @@ -44,7 +49,7 @@ public static MethodDefinition Lookup( // // [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] // private static int Lookup(void* thisPtr, key, * result) - MethodDefinition lookupMethod = new( + MethodDefinition lookupImplMethod = new( name: "Lookup"u8, attributes: MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.Static, signature: MethodSignature.CreateStatic( @@ -71,8 +76,23 @@ public static MethodDefinition Lookup( CilInstruction call_catchStartMarshalException = new(Call, interopReferences.RestrictedErrorInfoExceptionMarshallerConvertToUnmanaged.Import(module)); CilInstruction nop_convertToUnmanaged = new(Nop); + IMethodDescriptor adapterLookupMethod; + + // Get the target 'Lookup' method (we can optimize for 'string' types) + if (keyType.IsTypeOfString()) + { + adapterLookupMethod = SignatureComparer.IgnoreVersion.Equals(readOnlyDictionaryType.GenericType, interopReferences.IReadOnlyDictionary2) + ? interopReferences.IReadOnlyDictionaryAdapterOfStringLookup(valueType) + : interopReferences.IListAdapterOfStringIndexOf(); // TODO + } + else + { + // Otherwise use the provided method directly (it will always be valid) + adapterLookupMethod = lookupMethod; + } + // Create a method body for the 'Lookup' method - lookupMethod.CilMethodBody = new CilMethodBody() + lookupImplMethod.CilMethodBody = new CilMethodBody() { LocalVariables = { loc_0_thisObject, loc_1_hresult }, Instructions = @@ -93,7 +113,7 @@ public static MethodDefinition Lookup( { Ldarg_2 }, { Ldloc_0 }, { nop_parameter1Rewrite }, - { Call, interopReferences.IReadOnlyDictionaryAdapter2Lookup(keyType, valueType).Import(module) }, + { Call, adapterLookupMethod.Import(module) }, { nop_convertToUnmanaged }, { Ldc_I4_0 }, { Stloc_1 }, @@ -122,20 +142,25 @@ public static MethodDefinition Lookup( } }; + // If the key type is 'string', we use 'ReadOnlySpan' to avoid an allocation + TypeSignature parameterType = keyType.IsTypeOfString() + ? interopReferences.ReadOnlySpanChar + : keyType; + // Track rewriting the parameter for this method emitState.TrackManagedParameterMethodRewrite( - parameterType: keyType, - method: lookupMethod, + parameterType: parameterType, + method: lookupImplMethod, marker: nop_parameter1Rewrite, parameterIndex: 1); // Track the method for rewrite to marshal the result value emitState.TrackRetValValueMethodRewrite( retValType: valueType, - method: lookupMethod, + method: lookupImplMethod, marker: nop_convertToUnmanaged); - return lookupMethod; + return lookupImplMethod; } } } \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyList1Impl.cs b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyList1Impl.cs index fe8600b85..a70f3fcd9 100644 --- a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyList1Impl.cs +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyList1Impl.cs @@ -239,6 +239,9 @@ public static MethodDefinition get_Size( /// The instance to use. /// The emit state for this invocation. /// The interop module being built. + /// + /// This method can also be used to define the IndexOf method for interfaces. + /// public static MethodDefinition IndexOf( GenericInstanceTypeSignature readOnlyListType, MemberReference indexOfMethod, diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs index dc448cdfa..5b1f1ff85 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -3715,6 +3715,22 @@ public MemberReference IReadOnlyDictionaryAdapter2Lookup(TypeSignature keyType, keyType])); } + /// + /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryAdapter<string, TValue>.Lookup. + /// + /// The input value type. + public MethodSpecification IReadOnlyDictionaryAdapterOfStringLookup(TypeSignature valueType) + { + return IReadOnlyDictionaryAdapterExtensions + .CreateMemberReference("Lookup"u8, MethodSignature.CreateStatic( + returnType: new GenericParameterSignature(GenericParameterType.Method, 0), + genericParameterCount: 1, + parameterTypes: [ + IReadOnlyDictionary2.MakeGenericReferenceType(_corLibTypeFactory.String, new GenericParameterSignature(GenericParameterType.Method, 0)), + ReadOnlySpanChar])) + .MakeGenericInstanceMethod(valueType); + } + /// /// Gets the for . /// From de3db635c886426e3568d5e928ae1ba743e9ded1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 2 Jan 2026 13:46:11 -0800 Subject: [PATCH 08/16] Add get_Size method to IReadOnlyDictionary2 interop Introduces the get_Size method implementation for IReadOnlyDictionary2 interop types. Updates the builder to include get_Size in the vtable, adds a factory method to generate the method definition, and provides a reference accessor for the Size property in InteropReferences. --- ...eDefinitionBuilder.IReadOnlyDictionary2.cs | 11 ++- ...initionFactory.IReadOnlyDictionary2Impl.cs | 97 +++++++++++++++++++ .../References/InteropReferences.cs | 17 ++++ 3 files changed, 124 insertions(+), 1 deletion(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs index ed792de50..25cc31bac 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs @@ -594,6 +594,13 @@ public static void ImplType( emitState: emitState, module: module); + // Define the 'get_Size' method + MethodDefinition sizeMethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.get_Size( + readOnlyDictionaryType: readOnlyDictionaryType, + sizeMethod: interopReferences.IReadOnlyDictionaryAdapter2Size(keyType, valueType), + interopReferences: interopReferences, + module: module); + Impl( interfaceType: ComInterfaceType.InterfaceIsIInspectable, ns: InteropUtf8NameFactory.TypeNamespace(readOnlyDictionaryType), @@ -603,7 +610,9 @@ public static void ImplType( interopReferences: interopReferences, module: module, implType: out implType, - vtableMethods: [getAtMethod]); + vtableMethods: [ + getAtMethod, + sizeMethod]); // Track the type (it may be needed by COM interface entries for user-defined types) emitState.TrackTypeDefinition(implType, readOnlyDictionaryType, "Impl"); diff --git a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs index de1745a22..e405b721c 100644 --- a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs @@ -162,5 +162,102 @@ public static MethodDefinition Lookup( return lookupImplMethod; } + + /// + /// Creates a for the get_Size export method. + /// + /// The for the type. + /// The interface method to invoke on . + /// The instance to use. + /// The interop module being built. + /// + /// This method can also be used to define the get_Size method for interfaces. + /// + public static MethodDefinition get_Size( + GenericInstanceTypeSignature readOnlyDictionaryType, + MemberReference sizeMethod, + InteropReferences interopReferences, + ModuleDefinition module) + { + // Define the 'get_Size' method as follows: + // + // [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + // private static int get_Size(void* thisPtr, uint* result) + MethodDefinition sizeImplMethod = new( + name: "get_Size"u8, + attributes: MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.Static, + signature: MethodSignature.CreateStatic( + returnType: module.CorLibTypeFactory.Int32, + parameterTypes: [ + module.CorLibTypeFactory.Void.MakePointerType(), + module.CorLibTypeFactory.UInt32.MakePointerType()])) + { + CustomAttributes = { InteropCustomAttributeFactory.UnmanagedCallersOnly(interopReferences, module) } + }; + + // Declare the local variables: + // [0]: '' (for 'thisObject') + // [1]: 'int' (the 'HRESULT' to return) + CilLocalVariable loc_0_thisObject = new(readOnlyDictionaryType.Import(module)); + CilLocalVariable loc_1_hresult = new(module.CorLibTypeFactory.Int32); + + // Labels for jumps + CilInstruction nop_beforeTry = new(Nop); + CilInstruction ldarg_0_tryStart = new(Ldarg_0); + CilInstruction ldloc_1_returnHResult = new(Ldloc_1); + CilInstruction call_catchStartMarshalException = new(Call, interopReferences.RestrictedErrorInfoExceptionMarshallerConvertToUnmanaged.Import(module)); + + // Create a method body for the 'get_Size' method + sizeImplMethod.CilMethodBody = new CilMethodBody() + { + LocalVariables = { loc_0_thisObject, loc_1_hresult }, + Instructions = + { + // Return 'E_POINTER' if the argument is 'null' + { Ldarg_1 }, + { Ldc_I4_0 }, + { Conv_U }, + { Bne_Un_S, nop_beforeTry.CreateLabel() }, + { Ldc_I4, unchecked((int)0x80004003) }, + { Ret }, + { nop_beforeTry }, + + // '.try' code + { ldarg_0_tryStart }, + { Call, interopReferences.ComInterfaceDispatchGetInstance.MakeGenericInstanceMethod(readOnlyDictionaryType).Import(module) }, + { Stloc_0 }, + { Ldarg_1 }, + { Ldloc_0 }, + { Call, sizeMethod.Import(module) }, + { Stind_I4 }, + { Ldc_I4_0 }, + { Stloc_1 }, + { Leave_S, ldloc_1_returnHResult.CreateLabel() }, + + // '.catch' code + { call_catchStartMarshalException }, + { Stloc_1 }, + { Leave_S, ldloc_1_returnHResult.CreateLabel() }, + + // Return the 'HRESULT' from location [1] + { ldloc_1_returnHResult }, + { Ret } + }, + ExceptionHandlers = + { + new CilExceptionHandler + { + HandlerType = CilExceptionHandlerType.Exception, + TryStart = ldarg_0_tryStart.CreateLabel(), + TryEnd = call_catchStartMarshalException.CreateLabel(), + HandlerStart = call_catchStartMarshalException.CreateLabel(), + HandlerEnd = ldloc_1_returnHResult.CreateLabel(), + ExceptionType = interopReferences.Exception.Import(module) + } + } + }; + + return sizeImplMethod; + } } } \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs index 5b1f1ff85..23425f796 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -3731,6 +3731,23 @@ public MethodSpecification IReadOnlyDictionaryAdapterOfStringLookup(TypeSignatur .MakeGenericInstanceMethod(valueType); } + /// + /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryAdapter<TKey, TValue>.Size. + /// + /// The input key type. + /// The input value type. + public MemberReference IReadOnlyDictionaryAdapter2Size(TypeSignature keyType, TypeSignature valueType) + { + return IReadOnlyDictionaryAdapter2 + .MakeGenericReferenceType(keyType, valueType) + .ToTypeDefOrRef() + .CreateMemberReference("Size"u8, MethodSignature.CreateStatic( + returnType: _corLibTypeFactory.UInt32, + parameterTypes: [IReadOnlyDictionary2.MakeGenericReferenceType( + new GenericParameterSignature(GenericParameterType.Type, 0), + new GenericParameterSignature(GenericParameterType.Type, 1))])); + } + /// /// Gets the for . /// From e5816bed349ae126716a36db26a81efbc74e8f69 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 2 Jan 2026 14:55:22 -0800 Subject: [PATCH 09/16] Add HasKey support for IReadOnlyDictionary2 interop Introduces a HasKey method for IReadOnlyDictionary2 interop types, including method generation in the builder and factory, and a fast-path extension for string keys using ReadOnlySpan. This improves performance and feature parity for COM interop scenarios involving IReadOnlyDictionary. --- ...eDefinitionBuilder.IReadOnlyDictionary2.cs | 11 +- ...initionFactory.IReadOnlyDictionary2Impl.cs | 141 ++++++++++++++++++ .../References/InteropReferences.cs | 16 ++ .../IReadOnlyDictionaryAdapterExtensions.cs | 36 ++++- 4 files changed, 202 insertions(+), 2 deletions(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs index 25cc31bac..3bfdea804 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs @@ -601,6 +601,14 @@ public static void ImplType( interopReferences: interopReferences, module: module); + // Define the 'HasKey' method + MethodDefinition hasKeymethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.HasKey( + readOnlyDictionaryType: readOnlyDictionaryType, + hasKeyMethod: interopReferences.IReadOnlyDictionary2ContainsKey(keyType, valueType), + interopReferences: interopReferences, + emitState: emitState, + module: module); + Impl( interfaceType: ComInterfaceType.InterfaceIsIInspectable, ns: InteropUtf8NameFactory.TypeNamespace(readOnlyDictionaryType), @@ -612,7 +620,8 @@ public static void ImplType( implType: out implType, vtableMethods: [ getAtMethod, - sizeMethod]); + sizeMethod, + hasKeymethod]); // Track the type (it may be needed by COM interface entries for user-defined types) emitState.TrackTypeDefinition(implType, readOnlyDictionaryType, "Impl"); diff --git a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs index e405b721c..718521606 100644 --- a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs @@ -259,5 +259,146 @@ public static MethodDefinition get_Size( return sizeImplMethod; } + + /// + /// Creates a for the HasKey export method. + /// + /// The for the type. + /// The interface method to invoke on . + /// The instance to use. + /// The emit state for this invocation. + /// The interop module being built. + /// + /// This method can also be used to define the HasKey method for interfaces. + /// + public static MethodDefinition HasKey( + GenericInstanceTypeSignature readOnlyDictionaryType, + MemberReference hasKeyMethod, + InteropReferences interopReferences, + InteropGeneratorEmitState emitState, + ModuleDefinition module) + { + TypeSignature keyType = readOnlyDictionaryType.TypeArguments[0]; + TypeSignature valueType = readOnlyDictionaryType.TypeArguments[1]; + + // Define the 'HasKey' method as follows: + // + // [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + // private static int HasKey(void* thisPtr, key, bool* result) + MethodDefinition hasKeyImplMethod = new( + name: "HasKey"u8, + attributes: MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.Static, + signature: MethodSignature.CreateStatic( + returnType: module.CorLibTypeFactory.Int32, + parameterTypes: [ + module.CorLibTypeFactory.Void.MakePointerType(), + keyType.GetAbiType(interopReferences).Import(module), + module.CorLibTypeFactory.Boolean.MakePointerType()])) + { + CustomAttributes = { InteropCustomAttributeFactory.UnmanagedCallersOnly(interopReferences, module) } + }; + + // Declare the local variables: + // [0]: '' (for 'thisObject') + // [1]: 'int' (the 'HRESULT' to return) + CilLocalVariable loc_0_thisObject = new(readOnlyDictionaryType.Import(module)); + CilLocalVariable loc_1_hresult = new(module.CorLibTypeFactory.Int32); + + // Labels for jumps + CilInstruction nop_beforeTry = new(Nop); + CilInstruction ldarg_0_tryStart = new(Ldarg_0); + CilInstruction nop_parameter1Rewrite = new(Nop); + CilInstruction ldloc_1_returnHResult = new(Ldloc_1); + CilInstruction call_catchStartMarshalException = new(Call, interopReferences.RestrictedErrorInfoExceptionMarshallerConvertToUnmanaged.Import(module)); + CilInstruction nop_convertToUnmanaged = new(Nop); + CilInstruction callHasKeyMethod; + + // Get the target 'HasKey' method (we can optimize for 'string' types). + // We prepare the full instruction, as we need 'callvirt' in some cases. + if (keyType.IsTypeOfString()) + { + MethodSpecification hasKeyMethodSpecification = SignatureComparer.IgnoreVersion.Equals(readOnlyDictionaryType.GenericType, interopReferences.IReadOnlyDictionary2) + ? interopReferences.IReadOnlyDictionaryAdapterOfStringHasKey(valueType) + : interopReferences.IReadOnlyDictionaryAdapterOfStringHasKey(valueType); // TODO + + callHasKeyMethod = new(Call, hasKeyMethodSpecification.Import(module)); + } + else + { + // Otherwise just use 'ContainsKey' method passed as input + callHasKeyMethod = new(Callvirt, hasKeyMethod.Import(module)); + } + + // Create a method body for the 'HasKey' method + hasKeyImplMethod.CilMethodBody = new CilMethodBody() + { + LocalVariables = { loc_0_thisObject, loc_1_hresult }, + Instructions = + { + // Return 'E_POINTER' if the argument is 'null' + { Ldarg_2 }, + { Ldc_I4_0 }, + { Conv_U }, + { Bne_Un_S, nop_beforeTry.CreateLabel() }, + { Ldc_I4, unchecked((int)0x80004003) }, + { Ret }, + { nop_beforeTry }, + + // '.try' code + { ldarg_0_tryStart }, + { Call, interopReferences.ComInterfaceDispatchGetInstance.MakeGenericInstanceMethod(readOnlyDictionaryType).Import(module) }, + { Stloc_0 }, + { Ldarg_2 }, + { Ldloc_0 }, + { nop_parameter1Rewrite }, + { callHasKeyMethod }, + { nop_convertToUnmanaged }, + { Ldc_I4_0 }, + { Stloc_1 }, + { Leave_S, ldloc_1_returnHResult.CreateLabel() }, + + // '.catch' code + { call_catchStartMarshalException }, + { Stloc_1 }, + { Leave_S, ldloc_1_returnHResult.CreateLabel() }, + + // Return the 'HRESULT' from location [1] + { ldloc_1_returnHResult }, + { Ret } + }, + ExceptionHandlers = + { + new CilExceptionHandler + { + HandlerType = CilExceptionHandlerType.Exception, + TryStart = ldarg_0_tryStart.CreateLabel(), + TryEnd = call_catchStartMarshalException.CreateLabel(), + HandlerStart = call_catchStartMarshalException.CreateLabel(), + HandlerEnd = ldloc_1_returnHResult.CreateLabel(), + ExceptionType = interopReferences.Exception.Import(module) + } + } + }; + + // If the key type is 'string', we use 'ReadOnlySpan' to avoid an allocation + TypeSignature parameterType = keyType.IsTypeOfString() + ? interopReferences.ReadOnlySpanChar + : keyType; + + // Track rewriting the parameter for this method + emitState.TrackManagedParameterMethodRewrite( + parameterType: parameterType, + method: hasKeyImplMethod, + marker: nop_parameter1Rewrite, + parameterIndex: 1); + + // Track the method for rewrite to marshal the result value + emitState.TrackRetValValueMethodRewrite( + retValType: module.CorLibTypeFactory.Boolean, + method: hasKeyImplMethod, + marker: nop_convertToUnmanaged); + + return hasKeyImplMethod; + } } } \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs index 23425f796..d0919d286 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -3748,6 +3748,22 @@ public MemberReference IReadOnlyDictionaryAdapter2Size(TypeSignature keyType, Ty new GenericParameterSignature(GenericParameterType.Type, 1))])); } + /// + /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryAdapter<string, TValue>.HasKey. + /// + /// The input value type. + public MethodSpecification IReadOnlyDictionaryAdapterOfStringHasKey(TypeSignature valueType) + { + return IReadOnlyDictionaryAdapterExtensions + .CreateMemberReference("HasKey"u8, MethodSignature.CreateStatic( + returnType: _corLibTypeFactory.Boolean, + genericParameterCount: 1, + parameterTypes: [ + IReadOnlyDictionary2.MakeGenericReferenceType(_corLibTypeFactory.String, new GenericParameterSignature(GenericParameterType.Method, 0)), + ReadOnlySpanChar])) + .MakeGenericInstanceMethod(valueType); + } + /// /// Gets the for . /// diff --git a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapterExtensions.cs b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapterExtensions.cs index 3c01aa58f..e290e1de9 100644 --- a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapterExtensions.cs +++ b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapterExtensions.cs @@ -9,7 +9,7 @@ using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; -#pragma warning disable IDE0045 +#pragma warning disable IDE0045, IDE0046 namespace WindowsRuntime.InteropServices; @@ -78,5 +78,39 @@ static void ThrowKeyNotFoundException() return value!; } + + /// + /// Determines whether the map view contains the specified key. + /// + /// The wrapped instance. + /// The key to locate in the map view. + /// Whether the key was found. + /// + /// This overload can be used to avoid a allocation on the caller side. + /// + /// /// + public static bool HasKey(IReadOnlyDictionary dictionary, ReadOnlySpan key) + { + // Same logic as in 'Lookup' above for trying to avoid materializing the 'string' key + if (dictionary.GetType() == typeof(Dictionary) && + Unsafe.As>(dictionary).TryGetAlternateLookup(out Dictionary.AlternateLookup> lookup1)) + { + return lookup1.ContainsKey(key); + } + + if (dictionary.GetType() == typeof(ConcurrentDictionary) && + Unsafe.As>(dictionary).TryGetAlternateLookup(out ConcurrentDictionary.AlternateLookup> lookup2)) + { + return lookup2.ContainsKey(key); + } + + if (dictionary is FrozenDictionary candidate3 && + candidate3.TryGetAlternateLookup(out FrozenDictionary.AlternateLookup> lookup3)) + { + return lookup3.ContainsKey(key); + } + + return dictionary.ContainsKey(key.ToString()); + } } } \ No newline at end of file From bd535e5e885b054a08fe9380906a73ced13829c9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 2 Jan 2026 16:31:14 -0800 Subject: [PATCH 10/16] Add Split method to IReadOnlyDictionary2 interop Introduces the Split method to the IReadOnlyDictionary2 interop implementation, including its definition, method body, and reference resolution. This enables support for splitting IReadOnlyDictionary2 instances in the generated interop code. --- ...eDefinitionBuilder.IReadOnlyDictionary2.cs | 10 +- ...initionFactory.IReadOnlyDictionary2Impl.cs | 121 ++++++++++++++++++ .../References/InteropReferences.cs | 22 ++++ 3 files changed, 152 insertions(+), 1 deletion(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs index 3bfdea804..9e39ca4d5 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs @@ -609,6 +609,13 @@ public static void ImplType( emitState: emitState, module: module); + // Define the 'Split' method + MethodDefinition splitMethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.Split( + readOnlyDictionaryType: readOnlyDictionaryType, + interopReferences: interopReferences, + emitState: emitState, + module: module); + Impl( interfaceType: ComInterfaceType.InterfaceIsIInspectable, ns: InteropUtf8NameFactory.TypeNamespace(readOnlyDictionaryType), @@ -621,7 +628,8 @@ public static void ImplType( vtableMethods: [ getAtMethod, sizeMethod, - hasKeymethod]); + hasKeymethod, + splitMethod]); // Track the type (it may be needed by COM interface entries for user-defined types) emitState.TrackTypeDefinition(implType, readOnlyDictionaryType, "Impl"); diff --git a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs index 718521606..370114930 100644 --- a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs @@ -400,5 +400,126 @@ public static MethodDefinition HasKey( return hasKeyImplMethod; } + + /// + /// Creates a for the Split export method. + /// + /// The for the type. + /// The instance to use. + /// The emit state for this invocation. + /// The interop module being built. + public static MethodDefinition Split( + GenericInstanceTypeSignature readOnlyDictionaryType, + InteropReferences interopReferences, + InteropGeneratorEmitState emitState, + ModuleDefinition module) + { + TypeSignature keyType = readOnlyDictionaryType.TypeArguments[0]; + TypeSignature valueType = readOnlyDictionaryType.TypeArguments[1]; + + // Define the 'Split' method as follows: + // + // [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + // private static int Split(void* thisPtr, void** first, void** second) + MethodDefinition splitMethod = new( + name: "Split"u8, + attributes: MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.Static, + signature: MethodSignature.CreateStatic( + returnType: module.CorLibTypeFactory.Int32, + parameterTypes: [ + module.CorLibTypeFactory.Void.MakePointerType(), + module.CorLibTypeFactory.Void.MakePointerType().MakePointerType(), + module.CorLibTypeFactory.Void.MakePointerType().MakePointerType()])) + { + CustomAttributes = { InteropCustomAttributeFactory.UnmanagedCallersOnly(interopReferences, module) } + }; + + // Declare the local variables: + // [0]: '' (for 'thisObject') + // [1]: '' (for 'firstObject') + // [2]: '' (for 'secondObject') + // [3]: 'int' (the 'HRESULT' to return) + CilLocalVariable loc_0_thisObject = new(readOnlyDictionaryType.Import(module)); + CilLocalVariable loc_1_firstObject = new(readOnlyDictionaryType.Import(module)); + CilLocalVariable loc_2_secondObject = new(readOnlyDictionaryType.Import(module)); + CilLocalVariable loc_3_hresult = new(module.CorLibTypeFactory.Int32); + + // Labels for jumps + CilInstruction nop_beforeTry = new(Nop); + CilInstruction ldarg_0_tryStart = new(Ldarg_0); + CilInstruction ldloc_3_returnHResult = new(Ldloc_3); + CilInstruction call_catchStartMarshalException = new(Call, interopReferences.RestrictedErrorInfoExceptionMarshallerConvertToUnmanaged.Import(module)); + CilInstruction nop_firstObject_convertToUnmanaged = new(Nop); + CilInstruction nop_secondObject_convertToUnmanaged = new(Nop); + + // Create a method body for the 'Split' method + splitMethod.CilMethodBody = new CilMethodBody() + { + LocalVariables = { loc_0_thisObject, loc_1_firstObject, loc_2_secondObject, loc_3_hresult }, + Instructions = + { + // Return 'E_POINTER' if the argument is 'null' + { Ldarg_2 }, + { Ldc_I4_0 }, + { Conv_U }, + { Bne_Un_S, nop_beforeTry.CreateLabel() }, + { Ldc_I4, unchecked((int)0x80004003) }, + { Ret }, + { nop_beforeTry }, + + // '.try' code + { ldarg_0_tryStart }, + { Call, interopReferences.ComInterfaceDispatchGetInstance.MakeGenericInstanceMethod(readOnlyDictionaryType).Import(module) }, + { Stloc_0 }, + { Ldloc_0 }, + { Ldloca_S, loc_1_firstObject }, + { Ldloca_S, loc_2_secondObject }, + { Call, interopReferences.IReadOnlyDictionaryAdapter2Split(keyType, valueType).Import(module) }, + { Ldarg_1 }, + { Ldloc_1 }, + { nop_firstObject_convertToUnmanaged }, + { Ldarg_2 }, + { Ldloc_2 }, + { nop_secondObject_convertToUnmanaged }, + { Ldc_I4_0 }, + { Stloc_3 }, + { Leave_S, ldloc_3_returnHResult.CreateLabel() }, + + // '.catch' code + { call_catchStartMarshalException }, + { Stloc_3 }, + { Leave_S, ldloc_3_returnHResult.CreateLabel() }, + + // Return the 'HRESULT' from location [1] + { ldloc_3_returnHResult }, + { Ret } + }, + ExceptionHandlers = + { + new CilExceptionHandler + { + HandlerType = CilExceptionHandlerType.Exception, + TryStart = ldarg_0_tryStart.CreateLabel(), + TryEnd = call_catchStartMarshalException.CreateLabel(), + HandlerStart = call_catchStartMarshalException.CreateLabel(), + HandlerEnd = ldloc_3_returnHResult.CreateLabel(), + ExceptionType = interopReferences.Exception.Import(module) + } + } + }; + + // Track the method for rewrite to marshal the two result values + emitState.TrackRetValValueMethodRewrite( + retValType: readOnlyDictionaryType, + method: splitMethod, + marker: nop_firstObject_convertToUnmanaged); + + emitState.TrackRetValValueMethodRewrite( + retValType: readOnlyDictionaryType, + method: splitMethod, + marker: nop_secondObject_convertToUnmanaged); + + return splitMethod; + } } } \ No newline at end of file diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs index d0919d286..72b0beda6 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -3764,6 +3764,28 @@ public MethodSpecification IReadOnlyDictionaryAdapterOfStringHasKey(TypeSignatur .MakeGenericInstanceMethod(valueType); } + /// + /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryAdapter<TKey, TValue>.Split. + /// + /// The input key type. + /// The input value type. + public MemberReference IReadOnlyDictionaryAdapter2Split(TypeSignature keyType, TypeSignature valueType) + { + TypeSignature readOnlyDictionaryType = IReadOnlyDictionary2.MakeGenericReferenceType( + new GenericParameterSignature(GenericParameterType.Type, 0), + new GenericParameterSignature(GenericParameterType.Type, 1)); + + return IReadOnlyDictionaryAdapter2 + .MakeGenericReferenceType(keyType, valueType) + .ToTypeDefOrRef() + .CreateMemberReference("Split"u8, MethodSignature.CreateStatic( + returnType: _corLibTypeFactory.Void, + parameterTypes: [ + readOnlyDictionaryType, + readOnlyDictionaryType.MakeByReferenceType(), + readOnlyDictionaryType.MakeByReferenceType()])); + } + /// /// Gets the for . /// From 4d1165066d87b0b22f7278eb40e713b647aa241a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 2 Jan 2026 20:36:21 -0800 Subject: [PATCH 11/16] Track IReadOnlyDictionarySplitAdapter2 generic type Adds handling for IReadOnlyDictionarySplitAdapter in InteropTypeDiscovery to support types returned by IMapView.Split. Also introduces a TypeReference for IReadOnlyDictionarySplitAdapter2 in InteropReferences. --- .../Discovery/InteropTypeDiscovery.Generics.cs | 8 ++++++++ .../References/InteropReferences.cs | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs b/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs index ba291ee87..6153b4263 100644 --- a/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs +++ b/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs @@ -298,6 +298,14 @@ private static void TryTrackWindowsRuntimeGenericInterfaceTypeInstance( discoveryState: discoveryState, interopReferences: interopReferences, module: module); + + // Handle 'IReadOnlyDictionarySplitAdapter', which is returned by the 'IMapView.Split' method + TryTrackGenericTypeInstance( + typeSignature: interopReferences.IReadOnlyDictionarySplitAdapter2.MakeGenericReferenceType([.. typeSignature.TypeArguments]), + args: args, + discoveryState: discoveryState, + interopReferences: interopReferences, + module: module); } else if (SignatureComparer.IgnoreVersion.Equals(typeSignature.GenericType, interopReferences.IObservableVector1)) { diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs index 72b0beda6..6f2e06165 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -708,6 +708,11 @@ public InteropReferences( /// public TypeReference IReadOnlyDictionaryAdapterExtensions => field ??= _windowsRuntimeModule.CreateTypeReference("WindowsRuntime.InteropServices"u8, "IReadOnlyDictionaryAdapterExtensions"u8); + /// + /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionarySplitAdapter2<TKey, TValue>. + /// + public TypeReference IReadOnlyDictionarySplitAdapter2 => field ??= _windowsRuntimeModule.CreateTypeReference("WindowsRuntime.InteropServices"u8, "IReadOnlyDictionarySplitAdapter`2"u8); + /// /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryMethods. /// From caeb9b5dd1a2df3bc6f8d563255c7cbc8ecfa484 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 2 Jan 2026 21:09:58 -0800 Subject: [PATCH 12/16] Throw KeyNotFoundException for missing dictionary keys Replaces InvalidOperationException with KeyNotFoundException when a key is not found in IReadOnlyDictionary adapters. This change improves exception accuracy and aligns with .NET conventions. --- .../Collections/IReadOnlyDictionaryAdapterExtensions.cs | 2 +- .../Collections/IReadOnlyDictionaryAdapter{TKey, TValue}.cs | 2 +- .../IReadOnlyDictionarySplitAdapter{TKey, TValue}.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapterExtensions.cs b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapterExtensions.cs index e290e1de9..353642cf3 100644 --- a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapterExtensions.cs +++ b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapterExtensions.cs @@ -67,7 +67,7 @@ public static TValue Lookup(IReadOnlyDictionary dictionary, Read [DoesNotReturn] static void ThrowKeyNotFoundException() { - throw new InvalidOperationException("Arg_KeyNotFoundWithKey") + throw new KeyNotFoundException("Arg_KeyNotFoundWithKey") { HResult = WellKnownErrorCodes.E_BOUNDS }; diff --git a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapter{TKey, TValue}.cs b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapter{TKey, TValue}.cs index bb03cefe6..eb2e5dad8 100644 --- a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapter{TKey, TValue}.cs +++ b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapter{TKey, TValue}.cs @@ -37,7 +37,7 @@ public static TValue Lookup(IReadOnlyDictionary dictionary, TKey k [DoesNotReturn] static void ThrowKeyNotFoundException() { - throw new InvalidOperationException("Arg_KeyNotFoundWithKey") + throw new KeyNotFoundException("Arg_KeyNotFoundWithKey") { HResult = WellKnownErrorCodes.E_BOUNDS }; diff --git a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionarySplitAdapter{TKey, TValue}.cs b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionarySplitAdapter{TKey, TValue}.cs index 11bd1a16f..556d311a6 100644 --- a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionarySplitAdapter{TKey, TValue}.cs +++ b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionarySplitAdapter{TKey, TValue}.cs @@ -94,7 +94,7 @@ public TValue this[TKey key] if (!TryGetValue(key, out TValue? value)) { [DoesNotReturn] - static void ThrowKeyNotFoundException() => throw new InvalidOperationException("Arg_KeyNotFoundWithKey"); + static void ThrowKeyNotFoundException() => throw new KeyNotFoundException("Arg_KeyNotFoundWithKey"); ThrowKeyNotFoundException(); } From bdcf309be5096fb50cb68bc0d1801d0a139dd7d6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 3 Jan 2026 11:49:49 -0800 Subject: [PATCH 13/16] Rename getAtMethod to lookupMethod for clarity Renamed the variable 'getAtMethod' to 'lookupMethod' to better reflect its purpose and improve code readability in the IReadOnlyDictionary2 interop builder. --- .../InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs index 9e39ca4d5..051019283 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs @@ -587,7 +587,7 @@ public static void ImplType( TypeSignature valueType = readOnlyDictionaryType.TypeArguments[1]; // Define the 'Lookup' method - MethodDefinition getAtMethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.Lookup( + MethodDefinition lookupMethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.Lookup( readOnlyDictionaryType: readOnlyDictionaryType, lookupMethod: interopReferences.IReadOnlyDictionaryAdapter2Lookup(keyType, valueType), interopReferences: interopReferences, @@ -626,7 +626,7 @@ public static void ImplType( module: module, implType: out implType, vtableMethods: [ - getAtMethod, + lookupMethod, sizeMethod, hasKeymethod, splitMethod]); From 53c47d824d938c394f27543bf266a0e05f78c194 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 3 Jan 2026 18:21:35 -0800 Subject: [PATCH 14/16] Add AggressiveInlining to Size method Applied the MethodImplOptions.AggressiveInlining attribute to the Size method in IReadOnlyDictionaryAdapter to improve performance by suggesting inlining to the compiler. --- .../Collections/IReadOnlyDictionaryAdapter{TKey, TValue}.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapter{TKey, TValue}.cs b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapter{TKey, TValue}.cs index eb2e5dad8..be32c223b 100644 --- a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapter{TKey, TValue}.cs +++ b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionaryAdapter{TKey, TValue}.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; namespace WindowsRuntime.InteropServices; @@ -55,6 +56,7 @@ static void ThrowKeyNotFoundException() /// The wrapped instance. /// The number of elements in the map. /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static uint Size(IReadOnlyDictionary dictionary) { return (uint)dictionary.Count; From 505d765c7685c77f9cdf53391c3e3825e9f12abf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 3 Jan 2026 18:27:01 -0800 Subject: [PATCH 15/16] Fix generic parameter usage in Lookup method reference Corrects the generic parameter signatures used in the IReadOnlyDictionary2 reference for the Lookup method, ensuring the correct type parameters are passed instead of previously used variables. --- src/WinRT.Interop.Generator/References/InteropReferences.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs index 6f2e06165..283687dbb 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -3716,7 +3716,9 @@ public MemberReference IReadOnlyDictionaryAdapter2Lookup(TypeSignature keyType, .CreateMemberReference("Lookup"u8, MethodSignature.CreateStatic( returnType: new GenericParameterSignature(GenericParameterType.Type, 1), parameterTypes: [ - IReadOnlyDictionary2.MakeGenericReferenceType(keyType, valueType), + IReadOnlyDictionary2.MakeGenericReferenceType( + new GenericParameterSignature(GenericParameterType.Type, 0), + new GenericParameterSignature(GenericParameterType.Type, 1)), keyType])); } From cb0df7838390f6f81e5a3b8dc06496a92b64e655 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 9 Jan 2026 10:48:32 -0800 Subject: [PATCH 16/16] Track ArraySegment Enumerator in interop type discovery Adds special handling for ArraySegment.Enumerator returned by IReadOnlyDictionarySplitAdapter.GetEnumerator() in cswinrtgen. Updates InteropReferences to expose ArraySegment1Enumerator and documents the need to keep implementation in sync with code generator logic. --- .../Discovery/InteropTypeDiscovery.Generics.cs | 8 ++++++++ .../References/InteropReferences.cs | 10 ++++++++++ .../InteropServices/Collections/IListAdapter{T}.cs | 3 +++ .../IReadOnlyDictionarySplitAdapter{TKey, TValue}.cs | 2 ++ 4 files changed, 23 insertions(+) diff --git a/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs b/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs index 6153b4263..b699cdb41 100644 --- a/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs +++ b/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs @@ -306,6 +306,14 @@ private static void TryTrackWindowsRuntimeGenericInterfaceTypeInstance( discoveryState: discoveryState, interopReferences: interopReferences, module: module); + + // Handle 'ArraySegment.Enumerator', which is returned by 'IReadOnlyDictionarySplitAdapter.GetEnumerator()' + TryTrackGenericTypeInstance( + typeSignature: interopReferences.ArraySegment1Enumerator.MakeGenericValueType(interopReferences.KeyValuePair2.MakeGenericValueType([.. typeSignature.TypeArguments])), + args: args, + discoveryState: discoveryState, + interopReferences: interopReferences, + module: module); } else if (SignatureComparer.IgnoreVersion.Equals(typeSignature.GenericType, interopReferences.IObservableVector1)) { diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs index 283687dbb..9360c5240 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -133,6 +133,16 @@ public InteropReferences( /// public TypeReference Array => field ??= _corLibTypeFactory.CorLibScope.CreateTypeReference("System"u8, "Array"u8); + /// + /// Gets the for . + /// + public TypeReference ArraySegment1 => field ??= _corLibTypeFactory.CorLibScope.CreateTypeReference("System"u8, "ArraySegment`1"u8); + + /// + /// Gets the for . + /// + public TypeReference ArraySegment1Enumerator => field ??= ArraySegment1.CreateTypeReference("Enumerator"u8); + /// /// Gets the for . /// diff --git a/src/WinRT.Runtime2/InteropServices/Collections/IListAdapter{T}.cs b/src/WinRT.Runtime2/InteropServices/Collections/IListAdapter{T}.cs index 497d1dd36..e11851411 100644 --- a/src/WinRT.Runtime2/InteropServices/Collections/IListAdapter{T}.cs +++ b/src/WinRT.Runtime2/InteropServices/Collections/IListAdapter{T}.cs @@ -73,6 +73,9 @@ public static IReadOnlyList GetView(IList list) // 'IReadOnlyList'), this allows us to not allocate anything. That is, when native // code calls 'GetView()', it would get back the same CCW instance, just through a // 'QueryInterface' call for 'IVectorView' instead. + // + // The returned 'ReadOnlyCollection' type requires special handling in 'cswinrtgen' to + // be tracked correctly. If this implementation is changed, it needs to be kept in sync. return list as IReadOnlyList ?? new ReadOnlyCollection(list); } diff --git a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionarySplitAdapter{TKey, TValue}.cs b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionarySplitAdapter{TKey, TValue}.cs index 556d311a6..aeb2ebd97 100644 --- a/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionarySplitAdapter{TKey, TValue}.cs +++ b/src/WinRT.Runtime2/InteropServices/Collections/IReadOnlyDictionarySplitAdapter{TKey, TValue}.cs @@ -162,6 +162,8 @@ public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) /// public IEnumerator> GetEnumerator() { + // The returned 'ArraySegment>.Enumerator' type requires special handling in + // 'cswinrtgen' to be tracked correctly. If this implementation is changed, it needs to be kept in sync. return new ArraySegment>(_items, _firstItemIndex, Count).GetEnumerator(); }