From 224c5065fc015db4972732eb1beefba453f94297 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 3 Jan 2026 18:20:10 -0800 Subject: [PATCH 01/13] Add IDictionaryAdapter and extension methods Introduces IDictionaryAdapter as a stateless adapter for IDictionary to expose Windows.Foundation.Collections.IMap functionality. Adds IDictionaryAdapterExtensions for string-keyed dictionaries to optimize lookup, removal, and key existence checks using ReadOnlySpan to avoid string allocations. --- .../IDictionaryAdapterExtensions.cs | 149 ++++++++++++++++++ .../IDictionaryAdapter{TKey, TValue}.cs | 139 ++++++++++++++++ 2 files changed, 288 insertions(+) create mode 100644 src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapterExtensions.cs create mode 100644 src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapter{TKey, TValue}.cs diff --git a/src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapterExtensions.cs b/src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapterExtensions.cs new file mode 100644 index 000000000..f42ba7d6a --- /dev/null +++ b/src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapterExtensions.cs @@ -0,0 +1,149 @@ +// 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, IDE0046 + +namespace WindowsRuntime.InteropServices; + +/// +/// Extensions for the type. +/// +[Obsolete(WindowsRuntimeConstants.PrivateImplementationDetailObsoleteMessage, + DiagnosticId = WindowsRuntimeConstants.PrivateImplementationDetailObsoleteDiagnosticId, + UrlFormat = WindowsRuntimeConstants.CsWinRTDiagnosticsUrlFormat)] +[EditorBrowsable(EditorBrowsableState.Never)] +public static class IDictionaryAdapterExtensions +{ + extension(IDictionaryAdapter) + { + /// + /// + /// This overload can be used to avoid a allocation on the caller side. + /// + public static TValue Lookup(IDictionary dictionary, ReadOnlySpan key) + { + bool found; + TValue? value; + + // Same logic as in 'IReadOnlyDictionaryAdapterExtensions.Lookup' for trying to avoid materializing the 'string' key + 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 KeyNotFoundException("Arg_KeyNotFoundWithKey") + { + HResult = WellKnownErrorCodes.E_BOUNDS + }; + } + + ThrowKeyNotFoundException(); + } + + return value!; + } + + /// + /// Determines whether the map contains the specified key. + /// + /// The wrapped instance. + /// The key to locate in the map. + /// Whether the key was found. + /// + /// This overload can be used to avoid a allocation on the caller side. + /// + /// /// + public static bool HasKey(IDictionary 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()); + } + + /// + /// + /// This overload can be used to avoid a allocation on the caller side. + /// + public static void Remove(IDictionary dictionary, ReadOnlySpan key) + { + bool removed; + + // Same logic as in 'IReadOnlyDictionaryAdapterExtensions.Lookup' for trying to avoid materializing the 'string' key. + // We don't handle 'FrozenDictionary' since instances of that type are immutable (removing would throw). + if (dictionary.GetType() == typeof(Dictionary) && + Unsafe.As>(dictionary).TryGetAlternateLookup(out Dictionary.AlternateLookup> lookup1)) + { + removed = lookup1.Remove(key); + } + else if (dictionary.GetType() == typeof(ConcurrentDictionary) && + Unsafe.As>(dictionary).TryGetAlternateLookup(out ConcurrentDictionary.AlternateLookup> lookup2)) + { + removed = lookup2.TryRemove(key, out _); + } + else + { + removed = dictionary.Remove(key.ToString()); + } + + if (!removed) + { + [DoesNotReturn] + static void ThrowKeyNotFoundException() + { + throw new KeyNotFoundException("Arg_KeyNotFoundWithKey") + { + HResult = WellKnownErrorCodes.E_BOUNDS + }; + } + + ThrowKeyNotFoundException(); + } + } + } +} \ No newline at end of file diff --git a/src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapter{TKey, TValue}.cs b/src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapter{TKey, TValue}.cs new file mode 100644 index 000000000..669e3e8d7 --- /dev/null +++ b/src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapter{TKey, TValue}.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +#pragma warning disable CS8714 + +namespace WindowsRuntime.InteropServices; + +/// +/// A stateless adapter for , to be exposed as Windows.Foundation.Collections.IMap<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 IDictionaryAdapter +{ + /// + /// Returns the item at the specified key in the map. + /// + /// The wrapped instance. + /// The key associated with the item to locate. + /// The value, if an item with the specified key exists. + /// + public static TValue Lookup(IDictionary dictionary, TKey key) + { + // Same logic as in 'IReadOnlyDictionaryAdapter.Lookup' + if (!dictionary.TryGetValue(key, out TValue? value)) + { + [DoesNotReturn] + static void ThrowKeyNotFoundException() + { + throw new KeyNotFoundException("Arg_KeyNotFoundWithKey") + { + HResult = WellKnownErrorCodes.E_BOUNDS + }; + } + + ThrowKeyNotFoundException(); + } + + return value; + } + + /// + /// Gets the number of items in the map. + /// + /// The wrapped instance. + /// The number of elements in the map. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint Size(IDictionary dictionary) + { + return (uint)dictionary.Count; + } + + /// + /// Returns an immutable view of the map. + /// + /// The wrapped instance. + /// The view of the map. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IReadOnlyDictionary GetView(IDictionary dictionary) + { + // Analogous implementation to 'IListAdapter.GetView', see additional notes there + return dictionary as IReadOnlyDictionary ?? new ReadOnlyDictionary(dictionary); + } + + /// + /// Inserts or replaces an item in the map. + /// + /// The wrapped instance. + /// The key associated with the item to insert. + /// The item to insert. + /// Whether an item with the specified key is an existing item that was replaced. + /// + public static bool Insert(IDictionary dictionary, TKey key, TValue value) + { + // If the target dictionary is a 'Dictionary' instance, we can optimize the + // insertion by avoiding the duplicate lookup. We only do this for exactly this type, as + // derived implementations could otherwise alter the standard indexer behavior. We don't + // need to worry about concurrent operations here: same result as using the indexer. + // The 'CollectionsMarshal' APIs already guarantee that concurrent operations might + // possibly throw or result in invalid results, but that type safety violations can't happen. + if (dictionary.GetType() == typeof(Dictionary)) + { + CollectionsMarshal.GetValueRefOrAddDefault( + dictionary: Unsafe.As>(dictionary), + key: key, + exists: out bool exists) = value; + + return exists; + } + + bool replacing = dictionary.ContainsKey(key); + + dictionary[key] = value; + + return replacing; + } + + /// + /// Removes an item from the map. + /// + /// The wrapped instance. + /// The key associated with the item to remove. + /// + public static void Remove(IDictionary dictionary, TKey key) + { + bool removed = dictionary.Remove(key); + + if (!removed) + { + [DoesNotReturn] + static void ThrowKeyNotFoundException() + { + throw new KeyNotFoundException("Arg_KeyNotFoundWithKey") + { + HResult = WellKnownErrorCodes.E_BOUNDS + }; + } + + ThrowKeyNotFoundException(); + } + } +} \ No newline at end of file From 16d6ee5db60a3c041120c94a9d2ef475aab0d10b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 3 Jan 2026 19:12:52 -0800 Subject: [PATCH 02/13] Add IDictionaryAdapter2 Lookup support for IReadOnlyDictionary2 Introduces support for generating the 'Lookup' method for IDictionaryAdapter in the IReadOnlyDictionary2 implementation. Adds new type and member references for IDictionaryAdapter2 and its Lookup method, and updates the method factory to use these references for non-string key types. --- ...teropTypeDefinitionBuilder.IDictionary2.cs | 13 +++++- ...initionFactory.IReadOnlyDictionary2Impl.cs | 2 +- .../References/InteropReferences.cs | 45 +++++++++++++++++++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs index 0ba64c060..b689ef26d 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs @@ -1104,6 +1104,17 @@ public static void ImplType( ModuleDefinition module, out TypeDefinition implType) { + TypeSignature keyType = dictionaryType.TypeArguments[0]; + TypeSignature valueType = dictionaryType.TypeArguments[1]; + + // Define the 'Lookup' method + MethodDefinition lookupMethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.Lookup( + readOnlyDictionaryType: dictionaryType, + lookupMethod: interopReferences.IReadOnlyDictionaryAdapter2Lookup(keyType, valueType), + interopReferences: interopReferences, + emitState: emitState, + module: module); + Impl( interfaceType: ComInterfaceType.InterfaceIsIInspectable, ns: InteropUtf8NameFactory.TypeNamespace(dictionaryType), @@ -1113,7 +1124,7 @@ public static void ImplType( interopReferences: interopReferences, module: module, implType: out implType, - vtableMethods: []); + vtableMethods: [lookupMethod]); // Track the type (it may be needed by COM interface entries for user-defined types) emitState.TrackTypeDefinition(implType, dictionaryType, "Impl"); diff --git a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs index 370114930..2bbb36d08 100644 --- a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs @@ -83,7 +83,7 @@ public static MethodDefinition Lookup( { adapterLookupMethod = SignatureComparer.IgnoreVersion.Equals(readOnlyDictionaryType.GenericType, interopReferences.IReadOnlyDictionary2) ? interopReferences.IReadOnlyDictionaryAdapterOfStringLookup(valueType) - : interopReferences.IListAdapterOfStringIndexOf(); // TODO + : interopReferences.IDictionaryAdapterOfStringLookup(valueType); } else { diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs index 6f2e06165..b2e6322d7 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -688,6 +688,16 @@ public InteropReferences( /// public TypeReference IReadOnlyListMethods1 => field ??= _windowsRuntimeModule.CreateTypeReference("WindowsRuntime.InteropServices"u8, "IReadOnlyListMethods`1"u8); + /// + /// Gets the for WindowsRuntime.InteropServices.IDictionaryAdapter<TKey, TValue>. + /// + public TypeReference IDictionaryAdapter2 => field ??= _windowsRuntimeModule.CreateTypeReference("WindowsRuntime.InteropServices"u8, "IDictionaryAdapter`2"u8); + + /// + /// Gets the for WindowsRuntime.InteropServices.IDictionaryAdapterExtensions. + /// + public TypeReference IDictionaryAdapterExtensions => field ??= _windowsRuntimeModule.CreateTypeReference("WindowsRuntime.InteropServices"u8, "IDictionaryAdapterExtensions"u8); + /// /// Gets the for WindowsRuntime.InteropServices.IDictionaryMethods. /// @@ -3642,6 +3652,41 @@ public MethodSpecification IDictionaryMethods2RemoveKeyValuePair(TypeSignature k .MakeGenericInstanceMethod(mapMethods.ToReferenceTypeSignature()); } + /// + /// Gets the for WindowsRuntime.InteropServices.IDictionaryAdapter<TKey, TValue>.Lookup. + /// + /// The input key type. + /// The input value type. + public MemberReference IDictionaryAdapter2Lookup(TypeSignature keyType, TypeSignature valueType) + { + return IDictionaryAdapter2 + .MakeGenericReferenceType(keyType, valueType) + .ToTypeDefOrRef() + .CreateMemberReference("Lookup"u8, MethodSignature.CreateStatic( + returnType: new GenericParameterSignature(GenericParameterType.Type, 1), + parameterTypes: [ + IDictionary2.MakeGenericReferenceType( + new GenericParameterSignature(GenericParameterType.Type, 0), + new GenericParameterSignature(GenericParameterType.Type, 1)), + keyType])); + } + + /// + /// Gets the for WindowsRuntime.InteropServices.IDictionaryAdapter<string, TValue>.Lookup. + /// + /// The input value type. + public MethodSpecification IDictionaryAdapterOfStringLookup(TypeSignature valueType) + { + return IDictionaryAdapterExtensions + .CreateMemberReference("Lookup"u8, MethodSignature.CreateStatic( + returnType: new GenericParameterSignature(GenericParameterType.Method, 0), + genericParameterCount: 1, + parameterTypes: [ + IDictionary2.MakeGenericReferenceType(_corLibTypeFactory.String, new GenericParameterSignature(GenericParameterType.Method, 0)), + ReadOnlySpanChar])) + .MakeGenericInstanceMethod(valueType); + } + /// /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryMethods<TKey, TValue>.Item. /// From 04ab56dc60066b4da4632a1e858142d0c255b28b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 3 Jan 2026 19:15:04 -0800 Subject: [PATCH 03/13] Add get_Size method to IDictionary2 interop builder Introduces the definition and vtable inclusion of the 'get_Size' method for IDictionary2 implementations in the interop type builder. Adds a new helper in InteropReferences to retrieve the MemberReference for IDictionaryAdapter2.Size, supporting correct method generation for COM interop. --- ...InteropTypeDefinitionBuilder.IDictionary2.cs | 11 ++++++++++- .../References/InteropReferences.cs | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs index b689ef26d..e663fd643 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs @@ -1115,6 +1115,13 @@ public static void ImplType( emitState: emitState, module: module); + // Define the 'get_Size' method + MethodDefinition sizeMethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.get_Size( + readOnlyDictionaryType: dictionaryType, + sizeMethod: interopReferences.IDictionaryAdapter2Size(keyType, valueType), + interopReferences: interopReferences, + module: module); + Impl( interfaceType: ComInterfaceType.InterfaceIsIInspectable, ns: InteropUtf8NameFactory.TypeNamespace(dictionaryType), @@ -1124,7 +1131,9 @@ public static void ImplType( interopReferences: interopReferences, module: module, implType: out implType, - vtableMethods: [lookupMethod]); + vtableMethods: [ + lookupMethod, + sizeMethod]); // Track the type (it may be needed by COM interface entries for user-defined types) emitState.TrackTypeDefinition(implType, dictionaryType, "Impl"); diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs index b2e6322d7..f4f8b0bbb 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -3687,6 +3687,23 @@ public MethodSpecification IDictionaryAdapterOfStringLookup(TypeSignature valueT .MakeGenericInstanceMethod(valueType); } + /// + /// Gets the for WindowsRuntime.InteropServices.IDictionaryAdapter<TKey, TValue>.Size. + /// + /// The input key type. + /// The input value type. + public MemberReference IDictionaryAdapter2Size(TypeSignature keyType, TypeSignature valueType) + { + return IDictionaryAdapter2 + .MakeGenericReferenceType(keyType, valueType) + .ToTypeDefOrRef() + .CreateMemberReference("Size"u8, MethodSignature.CreateStatic( + returnType: _corLibTypeFactory.UInt32, + parameterTypes: [IDictionary2.MakeGenericReferenceType( + new GenericParameterSignature(GenericParameterType.Type, 0), + new GenericParameterSignature(GenericParameterType.Type, 1))])); + } + /// /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryMethods<TKey, TValue>.Item. /// From 00dc4b1ca8d7823be927577f6ada5be5cb474a65 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 3 Jan 2026 19:21:23 -0800 Subject: [PATCH 04/13] Add HasKey method to IDictionary2 and fix parameter naming Introduces the HasKey method to the IDictionary2 implementation and updates related method signatures and references for consistency. Also corrects parameter naming from 'hasKeyMethod' to 'containsKeyMethod' and adds a new method reference for IDictionaryAdapterOfStringHasKey in InteropReferences. --- .../InteropTypeDefinitionBuilder.IDictionary2.cs | 11 ++++++++++- ...TypeDefinitionBuilder.IReadOnlyDictionary2.cs | 2 +- ...DefinitionFactory.IReadOnlyDictionary2Impl.cs | 8 ++++---- .../References/InteropReferences.cs | 16 ++++++++++++++++ 4 files changed, 31 insertions(+), 6 deletions(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs index e663fd643..39cdb2ebf 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs @@ -1122,6 +1122,14 @@ public static void ImplType( interopReferences: interopReferences, module: module); + // Define the 'HasKey' method + MethodDefinition hasKeymethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.HasKey( + readOnlyDictionaryType: dictionaryType, + containsKeyMethod: interopReferences.IDictionary2ContainsKey(keyType, valueType), + interopReferences: interopReferences, + emitState: emitState, + module: module); + Impl( interfaceType: ComInterfaceType.InterfaceIsIInspectable, ns: InteropUtf8NameFactory.TypeNamespace(dictionaryType), @@ -1133,7 +1141,8 @@ public static void ImplType( implType: out implType, vtableMethods: [ lookupMethod, - sizeMethod]); + sizeMethod, + hasKeymethod]); // Track the type (it may be needed by COM interface entries for user-defined types) emitState.TrackTypeDefinition(implType, dictionaryType, "Impl"); diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs index 9e39ca4d5..e4be3a1aa 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs @@ -604,7 +604,7 @@ public static void ImplType( // Define the 'HasKey' method MethodDefinition hasKeymethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.HasKey( readOnlyDictionaryType: readOnlyDictionaryType, - hasKeyMethod: interopReferences.IReadOnlyDictionary2ContainsKey(keyType, valueType), + containsKeyMethod: interopReferences.IReadOnlyDictionary2ContainsKey(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 2bbb36d08..1a71cc127 100644 --- a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.cs @@ -264,7 +264,7 @@ public static MethodDefinition get_Size( /// Creates a for the HasKey export method. /// /// The for the type. - /// The interface method to invoke on . + /// The interface method to invoke on . /// The instance to use. /// The emit state for this invocation. /// The interop module being built. @@ -273,7 +273,7 @@ public static MethodDefinition get_Size( /// public static MethodDefinition HasKey( GenericInstanceTypeSignature readOnlyDictionaryType, - MemberReference hasKeyMethod, + MemberReference containsKeyMethod, InteropReferences interopReferences, InteropGeneratorEmitState emitState, ModuleDefinition module) @@ -319,14 +319,14 @@ public static MethodDefinition HasKey( { MethodSpecification hasKeyMethodSpecification = SignatureComparer.IgnoreVersion.Equals(readOnlyDictionaryType.GenericType, interopReferences.IReadOnlyDictionary2) ? interopReferences.IReadOnlyDictionaryAdapterOfStringHasKey(valueType) - : interopReferences.IReadOnlyDictionaryAdapterOfStringHasKey(valueType); // TODO + : interopReferences.IDictionaryAdapterOfStringHasKey(valueType); callHasKeyMethod = new(Call, hasKeyMethodSpecification.Import(module)); } else { // Otherwise just use 'ContainsKey' method passed as input - callHasKeyMethod = new(Callvirt, hasKeyMethod.Import(module)); + callHasKeyMethod = new(Callvirt, containsKeyMethod.Import(module)); } // Create a method body for the 'HasKey' method diff --git a/src/WinRT.Interop.Generator/References/InteropReferences.cs b/src/WinRT.Interop.Generator/References/InteropReferences.cs index f4f8b0bbb..8437011e9 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -3704,6 +3704,22 @@ public MemberReference IDictionaryAdapter2Size(TypeSignature keyType, TypeSignat new GenericParameterSignature(GenericParameterType.Type, 1))])); } + /// + /// Gets the for WindowsRuntime.InteropServices.IDictionaryAdapter<string, TValue>.HasKey. + /// + /// The input value type. + public MethodSpecification IDictionaryAdapterOfStringHasKey(TypeSignature valueType) + { + return IDictionaryAdapterExtensions + .CreateMemberReference("HasKey"u8, MethodSignature.CreateStatic( + returnType: _corLibTypeFactory.Boolean, + genericParameterCount: 1, + parameterTypes: [ + IDictionary2.MakeGenericReferenceType(_corLibTypeFactory.String, new GenericParameterSignature(GenericParameterType.Method, 0)), + ReadOnlySpanChar])) + .MakeGenericInstanceMethod(valueType); + } + /// /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryMethods<TKey, TValue>.Item. /// From 2bebf8d4c08e5e400ce106d219ba3e7d2176b57e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Jan 2026 16:15:13 -0800 Subject: [PATCH 05/13] Add support for IDictionary.GetView interop Introduces a new interop method definition for the GetView method on IDictionary implementations. Updates type discovery to track IReadOnlyDictionary and ReadOnlyDictionary types needed for marshalling GetView results. Adds required references and method factories to support this functionality. --- ...teropTypeDefinitionBuilder.IDictionary2.cs | 10 +- .../InteropTypeDiscovery.Generics.cs | 15 ++ ...ethodDefinitionFactory.IDictionary2Impl.cs | 129 ++++++++++++++++++ .../References/InteropReferences.cs | 24 ++++ 4 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs index 39cdb2ebf..01756a3d4 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs @@ -1130,6 +1130,13 @@ public static void ImplType( emitState: emitState, module: module); + // Define the 'GetView' method + MethodDefinition getViewMethod = InteropMethodDefinitionFactory.IDictionary2Impl.GetView( + dictionaryType: dictionaryType, + interopReferences: interopReferences, + emitState: emitState, + module: module); + Impl( interfaceType: ComInterfaceType.InterfaceIsIInspectable, ns: InteropUtf8NameFactory.TypeNamespace(dictionaryType), @@ -1142,7 +1149,8 @@ public static void ImplType( vtableMethods: [ lookupMethod, sizeMethod, - hasKeymethod]); + hasKeymethod, + getViewMethod]); // Track the type (it may be needed by COM interface entries for user-defined types) emitState.TrackTypeDefinition(implType, dictionaryType, "Impl"); diff --git a/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs b/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs index 6153b4263..0a6dd709d 100644 --- a/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs +++ b/src/WinRT.Interop.Generator/Discovery/InteropTypeDiscovery.Generics.cs @@ -255,6 +255,21 @@ private static void TryTrackWindowsRuntimeGenericInterfaceTypeInstance( // This is for the same reason why we need the other special cases in this method: members are not analyzed. discoveryState.TrackKeyValuePairType(interopReferences.KeyValuePair2.MakeGenericValueType([.. typeSignature.TypeArguments])); + // Whenever we find an 'IDictionary' instantiation, we also need to track the corresponding + // 'IReadOnlyDictionary' instantiation. This is because that interface is needed to marshal + // the return value of the 'IMap.GetView' method ('IMapView'). Same as 'IVector.GetView' above. + discoveryState.TrackIReadOnlyDictionary2Type(interopReferences.IReadOnlyDictionary2.MakeGenericReferenceType([.. typeSignature.TypeArguments])); + + // We also need to track the constructed 'ReadOnlyDictionary' type, as that is used by + // 'IDictionaryAdapter.GetView' in case the input 'IDictionary' instance doesn't implement + // 'IReadOnlyDictionary' directly. Analogous to tracking 'ReadOnlyCollection' above. + TryTrackGenericTypeInstance( + typeSignature: interopReferences.ReadOnlyDictionary2.MakeGenericReferenceType([.. typeSignature.TypeArguments]), + args: args, + discoveryState: discoveryState, + interopReferences: interopReferences, + module: module); + // When we discover a constructed 'IDictionary' instantiation, we'll be generating a native object type during // the emit phase, which is used to marshal anonymous objects. This derives from 'WindowsRuntimeDictionary'. // For the 'Keys' and 'Values' properties, that base type will return instances of the 'DictionaryKeyCollection' diff --git a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs new file mode 100644 index 000000000..cb477d9c3 --- /dev/null +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs @@ -0,0 +1,129 @@ +// 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; + +namespace WindowsRuntime.InteropGenerator.Factories; + +/// +/// A factory for interop method definitions. +/// +internal static partial class InteropMethodDefinitionFactory +{ + /// + /// Helpers for impl types for interfaces. + /// + public static class IDictionary2Impl + { + /// + /// Creates a for the GetView export method. + /// + /// The for the type. + /// The instance to use. + /// The emit state for this invocation. + /// The interop module being built. + public static MethodDefinition GetView( + GenericInstanceTypeSignature dictionaryType, + InteropReferences interopReferences, + InteropGeneratorEmitState emitState, + ModuleDefinition module) + { + TypeSignature keyType = dictionaryType.TypeArguments[0]; + TypeSignature valueType = dictionaryType.TypeArguments[1]; + + // Define the 'GetView' method as follows: + // + // [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + // private static int GetView(void* thisPtr, void** result) + MethodDefinition getViewMethod = new( + name: "GetView"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()])) + { + CustomAttributes = { InteropCustomAttributeFactory.UnmanagedCallersOnly(interopReferences, module) } + }; + + // Declare the local variables: + // [0]: '' (for 'thisObject') + // [1]: 'int' (the 'HRESULT' to return) + CilLocalVariable loc_0_thisObject = new(dictionaryType.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)); + CilInstruction nop_convertToUnmanaged = new(Nop); + + // Create a method body for the 'GetView' method + getViewMethod.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(dictionaryType).Import(module) }, + { Stloc_0 }, + { Ldarg_1 }, + { Ldloc_0 }, + { Call, interopReferences.IDictionaryAdapter2GetView(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 the method for rewrite to marshal the result value + emitState.TrackRetValValueMethodRewrite( + retValType: interopReferences.IReadOnlyDictionary2.MakeGenericReferenceType(keyType, valueType), + method: getViewMethod, + marker: nop_convertToUnmanaged); + + return getViewMethod; + } + } +} \ 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 8437011e9..225d72d5c 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -813,6 +813,11 @@ public InteropReferences( /// public TypeReference DictionaryValueCollection2 => field ??= _windowsRuntimeModule.CreateTypeReference("WindowsRuntime"u8, "DictionaryValueCollection`2"u8); + /// + /// Gets the for . + /// + public TypeReference ReadOnlyDictionary2 => field ??= _corLibTypeFactory.CorLibScope.CreateTypeReference("System.Collections.ObjectModel"u8, "ReadOnlyDictionary`2"u8); + /// /// Gets the for WindowsRuntime.ReadOnlyDictionaryKeyCollection2<TKey, TValue>. /// @@ -3720,6 +3725,25 @@ public MethodSpecification IDictionaryAdapterOfStringHasKey(TypeSignature valueT .MakeGenericInstanceMethod(valueType); } + /// + /// Gets the for WindowsRuntime.InteropServices.IDictionaryAdapter<string, TValue>.GetView. + /// + /// The input key type. + /// The input value type. + public MemberReference IDictionaryAdapter2GetView(TypeSignature keyType, TypeSignature valueType) + { + return IDictionaryAdapter2 + .MakeGenericReferenceType(keyType, valueType) + .ToTypeDefOrRef() + .CreateMemberReference("GetView"u8, MethodSignature.CreateStatic( + returnType: IReadOnlyDictionary2.MakeGenericReferenceType( + new GenericParameterSignature(GenericParameterType.Type, 0), + new GenericParameterSignature(GenericParameterType.Type, 1)), + parameterTypes: [IDictionary2.MakeGenericReferenceType( + new GenericParameterSignature(GenericParameterType.Type, 0), + new GenericParameterSignature(GenericParameterType.Type, 1))])); + } + /// /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryMethods<TKey, TValue>.Item. /// From 07d3989b7bad2ed5f2118be4e818db1d8c71eac4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Jan 2026 16:15:17 -0800 Subject: [PATCH 06/13] Remove unused using directives in IDictionaryAdapter Eliminated unnecessary using statements for System.Collections and System.Diagnostics to clean up the code and improve maintainability. --- .../Collections/IDictionaryAdapter{TKey, TValue}.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapter{TKey, TValue}.cs b/src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapter{TKey, TValue}.cs index 669e3e8d7..c43e9ce11 100644 --- a/src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapter{TKey, TValue}.cs +++ b/src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapter{TKey, TValue}.cs @@ -2,11 +2,9 @@ // Licensed under the MIT License. using System; -using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.ComponentModel; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; From 3f38325dbb6b4f9dfbd06c0ac5c866059222fc7c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Jan 2026 16:43:44 -0800 Subject: [PATCH 07/13] Fix XML comment to reference correct method name Updated the XML comment to reference TryMarkUserDefinedType instead of the incorrect MarkUserDefinedType, improving documentation accuracy. --- .../Generation/InteropGeneratorDiscoveryState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WinRT.Interop.Generator/Generation/InteropGeneratorDiscoveryState.cs b/src/WinRT.Interop.Generator/Generation/InteropGeneratorDiscoveryState.cs index a0636c7e0..f2fcf13e1 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGeneratorDiscoveryState.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGeneratorDiscoveryState.cs @@ -66,7 +66,7 @@ internal sealed class InteropGeneratorDiscoveryState /// Backing field for . private readonly ConcurrentDictionary _szArrayTypes = new(SignatureComparer.IgnoreVersion); - /// Backing field to support . + /// Backing field to support . private readonly ConcurrentDictionary _markedUserDefinedTypes = new(SignatureComparer.IgnoreVersion); /// Backing field for . From 1ffcec8ff6b1ae819dda7d1985ae55bf2007ec04 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 4 Jan 2026 16:46:01 -0800 Subject: [PATCH 08/13] Add Insert method support for IDictionary2 interop Introduces the Insert method implementation for IDictionary2 interop types by defining the method in InteropMethodDefinitionFactory, wiring it into InteropTypeDefinitionBuilder, and adding the necessary reference in InteropReferences. This enables proper handling of Insert operations for projected IDictionary2 types. --- ...teropTypeDefinitionBuilder.IDictionary2.cs | 10 +- ...eDefinitionBuilder.IReadOnlyDictionary2.cs | 1 - ...ethodDefinitionFactory.IDictionary2Impl.cs | 118 ++++++++++++++++++ .../References/InteropReferences.cs | 19 +++ 4 files changed, 146 insertions(+), 2 deletions(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs index 01756a3d4..4194ffa54 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs @@ -1137,6 +1137,13 @@ public static void ImplType( emitState: emitState, module: module); + // Define the 'Insert' method + MethodDefinition insertMethod = InteropMethodDefinitionFactory.IDictionary2Impl.Insert( + dictionaryType: dictionaryType, + interopReferences: interopReferences, + emitState: emitState, + module: module); + Impl( interfaceType: ComInterfaceType.InterfaceIsIInspectable, ns: InteropUtf8NameFactory.TypeNamespace(dictionaryType), @@ -1150,7 +1157,8 @@ public static void ImplType( lookupMethod, sizeMethod, hasKeymethod, - getViewMethod]); + getViewMethod, + insertMethod]); // Track the type (it may be needed by COM interface entries for user-defined types) emitState.TrackTypeDefinition(implType, dictionaryType, "Impl"); diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs index e4be3a1aa..b6b7dda84 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using System.Runtime.InteropServices; using AsmResolver.DotNet; using AsmResolver.DotNet.Code.Cil; diff --git a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs index cb477d9c3..0f3c5a6df 100644 --- a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs @@ -125,5 +125,123 @@ public static MethodDefinition GetView( return getViewMethod; } + + /// + /// Creates a for the Insert export method. + /// + /// The for the type. + /// The instance to use. + /// The emit state for this invocation. + /// The interop module being built. + public static MethodDefinition Insert( + GenericInstanceTypeSignature dictionaryType, + InteropReferences interopReferences, + InteropGeneratorEmitState emitState, + ModuleDefinition module) + { + TypeSignature keyType = dictionaryType.TypeArguments[0]; + TypeSignature valueType = dictionaryType.TypeArguments[1]; + TypeSignature keyAbiType = keyType.GetAbiType(interopReferences); + TypeSignature valueAbiType = valueType.GetAbiType(interopReferences); + + // Define the 'Insert' method as follows: + // + // [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + // private static int Insert(void* thisPtr, key, value, bool* result) + MethodDefinition insertMethod = new( + name: "Insert"u8, + attributes: MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.Static, + signature: MethodSignature.CreateStatic( + returnType: module.CorLibTypeFactory.Int32, + parameterTypes: [ + module.CorLibTypeFactory.Void.MakePointerType(), + keyAbiType.Import(module), + valueAbiType.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(dictionaryType.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 nop_parameter2Rewrite = new(Nop); + CilInstruction ldloc_1_returnHResult = new(Ldloc_1); + CilInstruction call_catchStartMarshalException = new(Call, interopReferences.RestrictedErrorInfoExceptionMarshallerConvertToUnmanaged.Import(module)); + + // Create a method body for the 'Insert' method + insertMethod.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(dictionaryType).Import(module) }, + { Stloc_0 }, + { Ldarg_3 }, + { Ldloc_0 }, + { nop_parameter1Rewrite }, + { nop_parameter2Rewrite }, + { Call, interopReferences.IDictionaryAdapter2Insert(keyType, valueType).Import(module) }, + { Stind_I1 }, + { 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 two parameters for this method + emitState.TrackManagedParameterMethodRewrite( + parameterType: keyType, + method: insertMethod, + marker: nop_parameter1Rewrite, + parameterIndex: 1); + + emitState.TrackManagedParameterMethodRewrite( + parameterType: valueType, + method: insertMethod, + marker: nop_parameter2Rewrite, + parameterIndex: 2); + + return insertMethod; + } } } \ 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 225d72d5c..a943f81ea 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -3744,6 +3744,25 @@ public MemberReference IDictionaryAdapter2GetView(TypeSignature keyType, TypeSig new GenericParameterSignature(GenericParameterType.Type, 1))])); } + /// + /// Gets the for WindowsRuntime.InteropServices.IDictionaryAdapter<string, TValue>.Insert. + /// + /// The input key type. + /// The input value type. + public MemberReference IDictionaryAdapter2Insert(TypeSignature keyType, TypeSignature valueType) + { + return IDictionaryAdapter2 + .MakeGenericReferenceType(keyType, valueType) + .ToTypeDefOrRef() + .CreateMemberReference("Insert"u8, MethodSignature.CreateStatic( + returnType: _corLibTypeFactory.Boolean, + parameterTypes: [IDictionary2.MakeGenericReferenceType( + new GenericParameterSignature(GenericParameterType.Type, 0), + new GenericParameterSignature(GenericParameterType.Type, 1)), + new GenericParameterSignature(GenericParameterType.Type, 0), + new GenericParameterSignature(GenericParameterType.Type, 1)])); + } + /// /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryMethods<TKey, TValue>.Item. /// From 0889235e5fc6cf4596f8cffc9baaf1592123c603 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 5 Jan 2026 12:47:05 -0800 Subject: [PATCH 09/13] Add support for 'Remove' method in IDictionary2 interop Implements the 'Remove' method for IDictionary2 interop type definitions, including method creation in InteropMethodDefinitionFactory and reference resolution in InteropReferences. This enables proper COM interface generation for dictionary removal operations. --- ...teropTypeDefinitionBuilder.IDictionary2.cs | 10 +- ...ethodDefinitionFactory.IDictionary2Impl.cs | 104 ++++++++++++++++++ .../References/InteropReferences.cs | 34 ++++++ 3 files changed, 147 insertions(+), 1 deletion(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs index 4194ffa54..319b6c5c7 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs @@ -1144,6 +1144,13 @@ public static void ImplType( emitState: emitState, module: module); + // Define the 'Remove' method + MethodDefinition removeMethod = InteropMethodDefinitionFactory.IDictionary2Impl.Remove( + dictionaryType: dictionaryType, + interopReferences: interopReferences, + emitState: emitState, + module: module); + Impl( interfaceType: ComInterfaceType.InterfaceIsIInspectable, ns: InteropUtf8NameFactory.TypeNamespace(dictionaryType), @@ -1158,7 +1165,8 @@ public static void ImplType( sizeMethod, hasKeymethod, getViewMethod, - insertMethod]); + insertMethod, + removeMethod]); // Track the type (it may be needed by COM interface entries for user-defined types) emitState.TrackTypeDefinition(implType, dictionaryType, "Impl"); diff --git a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs index 0f3c5a6df..c5c4b2f63 100644 --- a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs @@ -243,5 +243,109 @@ public static MethodDefinition Insert( return insertMethod; } + + /// + /// Creates a for the Remove export method. + /// + /// The for the type. + /// The instance to use. + /// The emit state for this invocation. + /// The interop module being built. + public static MethodDefinition Remove( + GenericInstanceTypeSignature dictionaryType, + InteropReferences interopReferences, + InteropGeneratorEmitState emitState, + ModuleDefinition module) + { + TypeSignature keyType = dictionaryType.TypeArguments[0]; + TypeSignature valueType = dictionaryType.TypeArguments[1]; + + // Define the 'Remove' method as follows: + // + // [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + // private static int Remove(void* thisPtr, key) + MethodDefinition removeMethod = new( + name: "Remove"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)])) + { + CustomAttributes = { InteropCustomAttributeFactory.UnmanagedCallersOnly(interopReferences, module) } + }; + + // Declare the local variables: + // [0]: '' (for 'thisObject') + // [1]: 'int' (the 'HRESULT' to return) + CilLocalVariable loc_0_thisObject = new(dictionaryType.Import(module)); + CilLocalVariable loc_1_hresult = new(module.CorLibTypeFactory.Int32); + + // Labels for jumps + 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)); + + // Get the target 'Remove' method (we can optimize for 'string' types) + IMethodDescriptor adapterRemoveMethod = keyType.IsTypeOfString() + ? interopReferences.IDictionaryAdapterOfStringRemove(valueType) + : interopReferences.IDictionaryAdapter2Remove(keyType, valueType); + + // Create a method body for the 'Remove' method + removeMethod.CilMethodBody = new CilMethodBody() + { + LocalVariables = { loc_0_thisObject, loc_1_hresult }, + Instructions = + { + // '.try' code + { ldarg_0_tryStart }, + { Call, interopReferences.ComInterfaceDispatchGetInstance.MakeGenericInstanceMethod(dictionaryType).Import(module) }, + { Stloc_0 }, + { Ldloc_0 }, + { nop_parameter1Rewrite }, + { Call, adapterRemoveMethod.Import(module) }, + { 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: removeMethod, + marker: nop_parameter1Rewrite, + parameterIndex: 1); + + return removeMethod; + } } } \ 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 a943f81ea..67b51c00d 100644 --- a/src/WinRT.Interop.Generator/References/InteropReferences.cs +++ b/src/WinRT.Interop.Generator/References/InteropReferences.cs @@ -3763,6 +3763,40 @@ public MemberReference IDictionaryAdapter2Insert(TypeSignature keyType, TypeSign new GenericParameterSignature(GenericParameterType.Type, 1)])); } + /// + /// Gets the for WindowsRuntime.InteropServices.IDictionaryAdapter<string, TValue>.Remove. + /// + /// The input key type. + /// The input value type. + public MemberReference IDictionaryAdapter2Remove(TypeSignature keyType, TypeSignature valueType) + { + return IDictionaryAdapter2 + .MakeGenericReferenceType(keyType, valueType) + .ToTypeDefOrRef() + .CreateMemberReference("Remove"u8, MethodSignature.CreateStatic( + returnType: _corLibTypeFactory.Void, + parameterTypes: [IDictionary2.MakeGenericReferenceType( + new GenericParameterSignature(GenericParameterType.Type, 0), + new GenericParameterSignature(GenericParameterType.Type, 1)), + new GenericParameterSignature(GenericParameterType.Type, 0)])); + } + + /// + /// Gets the for WindowsRuntime.InteropServices.IDictionaryAdapter<string, TValue>.Remove. + /// + /// The input value type. + public MethodSpecification IDictionaryAdapterOfStringRemove(TypeSignature valueType) + { + return IDictionaryAdapterExtensions + .CreateMemberReference("Remove"u8, MethodSignature.CreateStatic( + returnType: _corLibTypeFactory.Void, + genericParameterCount: 1, + parameterTypes: [ + IDictionary2.MakeGenericReferenceType(_corLibTypeFactory.String, new GenericParameterSignature(GenericParameterType.Method, 0)), + ReadOnlySpanChar])) + .MakeGenericInstanceMethod(valueType); + } + /// /// Gets the for WindowsRuntime.InteropServices.IReadOnlyDictionaryMethods<TKey, TValue>.Item. /// From 562bbc13273e389d6465cc8b9422d23017086969 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 5 Jan 2026 12:55:30 -0800 Subject: [PATCH 10/13] Add Clear method to IDictionary2 interop implementation Introduces a Clear method to the IDictionary2 interop type builder and its method factory. This enables support for clearing dictionary instances in the generated interop code. --- ...teropTypeDefinitionBuilder.IDictionary2.cs | 9 ++- ...ethodDefinitionFactory.IDictionary2Impl.cs | 81 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs index 319b6c5c7..b6a5d49fb 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs @@ -1151,6 +1151,12 @@ public static void ImplType( emitState: emitState, module: module); + // Define the 'Clear' method + MethodDefinition clearMethod = InteropMethodDefinitionFactory.IDictionary2Impl.Clear( + dictionaryType: dictionaryType, + interopReferences: interopReferences, + module: module); + Impl( interfaceType: ComInterfaceType.InterfaceIsIInspectable, ns: InteropUtf8NameFactory.TypeNamespace(dictionaryType), @@ -1166,7 +1172,8 @@ public static void ImplType( hasKeymethod, getViewMethod, insertMethod, - removeMethod]); + removeMethod, + clearMethod]); // Track the type (it may be needed by COM interface entries for user-defined types) emitState.TrackTypeDefinition(implType, dictionaryType, "Impl"); diff --git a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs index c5c4b2f63..1dab64dfd 100644 --- a/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs +++ b/src/WinRT.Interop.Generator/Factories/InteropMethodDefinitionFactory.IDictionary2Impl.cs @@ -347,5 +347,86 @@ public static MethodDefinition Remove( return removeMethod; } + + /// + /// Creates a for the Clear export method. + /// + /// The for the type. + /// The instance to use. + /// The interop module being built. + public static MethodDefinition Clear( + GenericInstanceTypeSignature dictionaryType, + InteropReferences interopReferences, + ModuleDefinition module) + { + TypeSignature keyType = dictionaryType.TypeArguments[0]; + TypeSignature valueType = dictionaryType.TypeArguments[1]; + + // Define the 'Clear' method as follows: + // + // [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + // private static int Clear(void* thisPtr) + MethodDefinition clearMethod = new( + name: "Clear"u8, + attributes: MethodAttributes.Private | MethodAttributes.HideBySig | MethodAttributes.Static, + signature: MethodSignature.CreateStatic( + returnType: module.CorLibTypeFactory.Int32, + parameterTypes: [module.CorLibTypeFactory.Void.MakePointerType()])) + { + CustomAttributes = { InteropCustomAttributeFactory.UnmanagedCallersOnly(interopReferences, module) } + }; + + // Declare the local variables: + // [0]: '' (for 'thisObject') + // [1]: 'int' (the 'HRESULT' to return) + CilLocalVariable loc_0_thisObject = new(dictionaryType.Import(module)); + CilLocalVariable loc_1_hresult = new(module.CorLibTypeFactory.Int32); + + // Labels for jumps + 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 'Clear' method + clearMethod.CilMethodBody = new CilMethodBody() + { + LocalVariables = { loc_0_thisObject, loc_1_hresult }, + Instructions = + { + // '.try' code + { ldarg_0_tryStart }, + { Call, interopReferences.ComInterfaceDispatchGetInstance.MakeGenericInstanceMethod(dictionaryType).Import(module) }, + { Stloc_0 }, + { Ldloc_0 }, + { Call, interopReferences.ICollection1Clear(interopReferences.KeyValuePair2.MakeGenericValueType(keyType, valueType)).Import(module) }, + { 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 clearMethod; + } } } \ No newline at end of file From d07442b7bbe53eaaf7dec4c1bd54ecccf633910a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 5 Jan 2026 12:58:45 -0800 Subject: [PATCH 11/13] Enable vtable entry count validation Uncommented the check to enforce that all vtable entries are initialized by throwing an ArgumentOutOfRangeException if the counts do not match. --- .../Builders/InteropTypeDefinitionBuilder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.cs index bfb3127f7..4754ea72b 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.cs @@ -461,7 +461,7 @@ private static void Impl( } // Enforce that we did initialize all vtable entries - //ArgumentOutOfRangeException.ThrowIfNotEqual(vtableOffset, vftblType.Fields.Count, nameof(vtableMethods)); // TODO + ArgumentOutOfRangeException.ThrowIfNotEqual(vtableOffset, vftblType.Fields.Count, nameof(vtableMethods)); // Don't forget the 'ret' at the end of the static constructor _ = cctor.CilMethodBody.Instructions.Add(Ret); From baa22e1615a1f49085729a2cc275cc78e12b25c0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 5 Jan 2026 13:03:15 -0800 Subject: [PATCH 12/13] Fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../InteropServices/Collections/IDictionaryAdapterExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapterExtensions.cs b/src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapterExtensions.cs index f42ba7d6a..114a6bef3 100644 --- a/src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapterExtensions.cs +++ b/src/WinRT.Runtime2/InteropServices/Collections/IDictionaryAdapterExtensions.cs @@ -81,7 +81,7 @@ static void ThrowKeyNotFoundException() /// /// This overload can be used to avoid a allocation on the caller side. /// - /// /// + /// public static bool HasKey(IDictionary dictionary, ReadOnlySpan key) { // Same logic as in 'Lookup' above for trying to avoid materializing the 'string' key From ed367c15e0ba972317e3a20ebb97272ffc52b02e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 5 Jan 2026 13:03:59 -0800 Subject: [PATCH 13/13] Fix typo in variable name for HasKey method Renamed 'hasKeymethod' to 'hasKeyMethod' in both IDictionary2 and IReadOnlyDictionary2 interop builders for consistency and clarity. --- .../Builders/InteropTypeDefinitionBuilder.IDictionary2.cs | 4 ++-- .../InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs index b6a5d49fb..5ba83f59f 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs @@ -1123,7 +1123,7 @@ public static void ImplType( module: module); // Define the 'HasKey' method - MethodDefinition hasKeymethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.HasKey( + MethodDefinition hasKeyMethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.HasKey( readOnlyDictionaryType: dictionaryType, containsKeyMethod: interopReferences.IDictionary2ContainsKey(keyType, valueType), interopReferences: interopReferences, @@ -1169,7 +1169,7 @@ public static void ImplType( vtableMethods: [ lookupMethod, sizeMethod, - hasKeymethod, + hasKeyMethod, getViewMethod, insertMethod, removeMethod, diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs index b6b7dda84..22eddfb7c 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs @@ -601,7 +601,7 @@ public static void ImplType( module: module); // Define the 'HasKey' method - MethodDefinition hasKeymethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.HasKey( + MethodDefinition hasKeyMethod = InteropMethodDefinitionFactory.IReadOnlyDictionary2Impl.HasKey( readOnlyDictionaryType: readOnlyDictionaryType, containsKeyMethod: interopReferences.IReadOnlyDictionary2ContainsKey(keyType, valueType), interopReferences: interopReferences, @@ -627,7 +627,7 @@ 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)