Fix missing Keys/Values on projected dictionary types#2427
Open
Sergio0694 wants to merge 7 commits into
Open
Conversation
Normalize end-of-line characters in src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters. This change only affects whitespace/EOLs and makes no functional changes to the project file.
Adds unit tests and AOT functional tests for the cswinrt-generated 'Keys'/ 'Values' projections that go through the 'UnsafeAccessor' stub paths fixed in the previous two commits. IDictionary path (tests use 'StringMap', 'PropertySet', and 'ValueSet'): - TestStringMapKeysAndValues: enumeration, count, contains, read-only contract (IsReadOnly, Add/Clear/Remove throw NotSupportedException), CopyTo - TestStringMapKeysAndValuesReflectMutation: keys/values views observe the underlying dictionary mutations (add/remove) - TestStringMapKeysAndValuesRepeatedAccess: repeated property access is safe and consistent - TestStringMapKeysAndValuesEmpty: zero-element dictionary case - TestPropertySetKeysAndValues / TestValueSetKeysAndValues: same scenarios for the 'string, object' generic instantiation - TestPropertySetKeysAndValuesViaIDictionaryInterface: explicit cast path - TestValueSetKeysAndValuesAfterClear: keys/values reflect 'Clear()' IReadOnlyDictionary path: Adds a new C++ runtime class 'TestComponentCSharp.CustomReadOnlyDictionaryTest' that implements 'Windows.Foundation.Collections.IMapView<String, String>'. This is needed because no readily-available Windows SDK runtime class projects directly as 'IReadOnlyDictionary<K, V>' (most map-like APIs return generic 'IMapView<K, V>' interface values that go through a different runtime path). The new class projects as 'IReadOnlyDictionary<string, string>' and exercises the cswinrt static_abi_methods path for the read-only case. - TestCustomReadOnlyDictionaryKeysAndValues: enumeration and indexer cross-check - TestCustomReadOnlyDictionaryKeysAndValuesViaIReadOnlyDictionaryInterface: explicit cast path - TestCustomReadOnlyDictionaryKeysAndValuesEnumerationOnly: asserts that 'Keys'/'Values' are 'IEnumerable<T>' and not 'ICollection<T>', pinning the exact contract violated by the prior UnsafeAccessor bug - TestCustomReadOnlyDictionaryKeysAndValuesEmpty: zero-element case using a CCW-marshalled managed dictionary Functional/AOT tests in 'FunctionalTests/Collections/Program.cs' mirror the two paths under trimming and Native AOT publishing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ionary projections The 'write_readonlydictionary_members_using_static_abi_methods' code path declared the Keys/Values UnsafeAccessor stubs with return type 'ICollection<T>' copied from the 'IDictionary' template. This is wrong because: - 'IReadOnlyDictionary<TKey, TValue>.Keys' returns 'IEnumerable<TKey>', not 'ICollection<TKey>' (and likewise for 'Values'). The cswinrt-emitted property body already returns 'IEnumerable<%>', so the stub was inconsistent with its own caller. - The runtime collection types 'ReadOnlyDictionaryKeyCollection<TKey, TValue>' and 'ReadOnlyDictionaryValueCollection<TKey, TValue>' only implement 'IEnumerable<T>' (not 'ICollection<T>'). - 'UnsafeAccessor' is signature-sensitive: a stub with return type 'ICollection<T>' would never bind to a method emitted with return type 'IEnumerable<T>', resulting in a 'MissingMethodException' at the first call. Change both 'Keys' and 'Values' stubs to 'IEnumerable<T>' to match the property shape and the runtime collection types. The parallel 'IDictionary' code path keeps 'ICollection<T>' since that is the correct return type for that interface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The cswinrt-generated projections for runtime classes whose interfaces map to 'IDictionary<K, V>' or 'IReadOnlyDictionary<K, V>' (e.g., 'PropertySet', 'ValueSet', 'StringMap') reference static helper methods 'ABI.System.Collections.Generic.<#corlib>IDictionary'2<K|V>Methods.Keys' (and 'Values') via 'UnsafeAccessor'. These methods were never actually emitted by the interop generator -- only 'Item', 'Count', 'ContainsKey', 'TryGetValue' (plus 'Add', 'Remove', 'Clear', 'Contains', 'CopyTo' for the mutable variant) were emitted. The first 'Keys' or 'Values' access on any such projected class would therefore throw 'MissingMethodException'. Add the missing 'Keys'/'Values' static methods in both 'InteropTypeDefinitionBuilder.IDictionary2.cs' and 'InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs'. The implementation constructs the appropriate runtime collection wrapper around a fresh 'NativeObject' built from the supplied 'WindowsRuntimeObjectReference': new DictionaryKeyCollection<TKey, TValue>(new NativeObject(thisReference)); This works because each generated 'NativeObject' inherits from the runtime base class 'WindowsRuntimeDictionary<...>' / 'WindowsRuntimeReadOnlyDictionary<...>', which already implement 'IDictionary<TKey, TValue>' / 'IEnumerable<KeyValuePair<TKey, TValue>>' respectively -- exactly the parameter types accepted by the collection constructors. To make 'nativeObjectType' available to the 'Methods' builder, the emission order in 'InteropGenerator.Emit.cs' is reordered so that 'NativeObject' runs before 'Methods' (for both 'IDictionary2' and 'IReadOnlyDictionary2'). The 'NativeObject' builder does not depend on the 'Methods' type, so this is safe. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The cswinrt-generated 'Keys'/'Values' projections for dictionary types had two
inefficiencies in the previous commit:
1. The interop-emitted static helper built a fresh 'NativeObject' wrapping
the 'WindowsRuntimeObjectReference' just to pass it to the runtime
collection constructor. This was wasteful: the projected runtime class
instance ('this' from the property body) already implements the dictionary
interface, so it can be passed directly.
2. Each 'Keys'/'Values' property access re-invoked the helper and allocated a
new collection wrapper, unlike the dictionary native object types in
'WinRT.Runtime' which cache the wrapper.
This commit:
- Changes the cswinrt-emitted 'UnsafeAccessor' stub parameter for 'Keys' and
'Values' from 'WindowsRuntimeObjectReference objRef' to
'WindowsRuntimeObject windowsRuntimeObject'.
- Changes the cswinrt-emitted property bodies from
'=> <stub>(null, <objref>)' to '=> field ??= <stub>(null, this);',
mirroring the field caching used in 'WindowsRuntimeDictionary' /
'WindowsRuntimeReadOnlyDictionary' (which also use plain '??=' since the
worst-case race is one extra wrapper allocation).
- Changes the interop-generator-emitted static method parameter to
'WindowsRuntimeObject' and the body to 'ldarg.0; castclass IFACE; newobj
Collection..ctor; ret' (verifiable IL, no extra allocations).
- Removes the now-unused 'nativeObjectType' parameter from the 'Methods'
builder of both 'InteropTypeDefinitionBuilder.IDictionary2' and
'InteropTypeDefinitionBuilder.IReadOnlyDictionary2', and restores the
original emission order in 'InteropGenerator.Emit.cs' (Methods can now run
before NativeObject again).
- Updates 'TestStringMapKeysAndValuesRepeatedAccess' to assert the new
caching contract via 'Assert.AreSame'.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Simplify interop IL generation by removing unnecessary Castclass instructions in IDictionary/IReadOnlyDictionary builders (the runtime object already implements the required interfaces). Update related comments to use a consistent parameter name (thisObject) and reference analogous logic. Also tweak unit test comments/formatting (remove mention of C# 14 'field ??=' pattern and reflow whitespace) — no behavioral changes intended.
For consistency with the parallel 'IDictionary2.Methods' implementation (and with how most other 'MethodDefinition' instances in the project are constructed), use the 'CilInstructions' object initializer for the 'Keys' and 'Values' methods on 'InteropTypeDefinitionBuilder.IReadOnlyDictionary2.Methods' instead of assigning 'CilMethodBody' separately afterwards. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes a latent bug in projected dictionary types where the first access to
KeysorValueson a runtime class projected asIDictionary<K, V>orIReadOnlyDictionary<K, V>(e.g.,Windows.Foundation.Collections.PropertySet,ValueSet,StringMap) would throwMissingMethodExceptionbecause theUnsafeAccessorstub emitted bycswinrt.exereferenced a static method that the interop generator never actually emitted.Motivation
cswinrt.exe'swrite_*_using_static_abi_methodspaths declare[UnsafeAccessor(...)] static extern ... Keys/Values(...)stubs that targetABI.System.Collections.Generic.<#corlib>IDictionary'2<K|V>Methods.Keys(andValues) in the interop sidecar (WinRT.Interop.dll). The interop generator'sInteropTypeDefinitionBuilder.IDictionary2.MethodsandInteropTypeDefinitionBuilder.IReadOnlyDictionary2.Methodsonly emitItem,Count,ContainsKey,TryGetValue(plus mutation surface for the mutable variant) —KeysandValueswere missing. TheUnsafeAccessorbinder lazy-resolves on first call, so this only fails at runtime, on the firstKeysorValuesaccess.While investigating, the
IReadOnlyDictionaryUnsafeAccessor stubs were also discovered to declare the wrong return type (ICollection<T>, copy-pasted from theIDictionarytemplate).IReadOnlyDictionary<TKey, TValue>.Keys/.ValuesreturnIEnumerable<T>, andReadOnlyDictionaryKeyCollection<TKey, TValue>/ReadOnlyDictionaryValueCollection<TKey, TValue>only implementIEnumerable<T>.UnsafeAccessoris signature-sensitive, so even if the interop generator emitted these methods with the correctIEnumerable<T>return type, the existing stub would never bind.Changes
Commits are ordered so the test commit precedes the fixes — CI on this PR (with tests-only applied) should reproduce the failure before the fix commits land.
src/Tests/: new unit tests inTestComponentCSharp_Tests.cscovering theIDictionarypath viaStringMap/PropertySet/ValueSet(enumeration, count, contains, read-only contract, mutation reflection, repeated access, empty case, explicit cast); new C++ runtime classTestComponentCSharp.CustomReadOnlyDictionaryTestimplementingIMapView<String, String>to provide direct coverage for theIReadOnlyDictionarypath; new unit tests using it (direct, explicit cast, negative-type assertion pinning theIEnumerable<T>(notICollection<T>) contract, empty case); functional/AOT tests inFunctionalTests/Collections/Program.csmirroring both paths.src/cswinrt/code_writers.h: fixwrite_readonlydictionary_members_using_static_abi_methodsto declare theKeys/ValuesUnsafeAccessor stubs with return typeIEnumerable<T>(matching the property body and the runtime collection types). TheIDictionarypath is unchanged because itsICollection<T>return type is already correct.src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.csandInteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs: emit the missingKeysandValuesstatic methods on the generatedMethodstypes. Each method constructs the appropriate runtime collection wrapper around a freshNativeObjectbuilt from the suppliedWindowsRuntimeObjectReference(e.g.,new DictionaryKeyCollection<TKey, TValue>(new NativeObject(thisReference))). TheNativeObjectinherits fromWindowsRuntimeDictionary<...>/WindowsRuntimeReadOnlyDictionary<...>, which implement the interface required by the collection constructors.src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs: reorder the emission soNativeObjectruns beforeMethodsfor bothIDictionary2andIReadOnlyDictionary2(the newKeys/Valuesmethod bodies depend onnativeObjectType). TheNativeObjectbuilder does not depend on theMethodstype, so this reordering is safe.