Skip to content

Fix missing Keys/Values on projected dictionary types#2427

Open
Sergio0694 wants to merge 7 commits into
staging/3.0from
user/sergiopedri/fix-keys-values-codegen
Open

Fix missing Keys/Values on projected dictionary types#2427
Sergio0694 wants to merge 7 commits into
staging/3.0from
user/sergiopedri/fix-keys-values-codegen

Conversation

@Sergio0694
Copy link
Copy Markdown
Member

Summary

Fixes a latent bug in projected dictionary types where the first access to Keys or Values on a runtime class projected as IDictionary<K, V> or IReadOnlyDictionary<K, V> (e.g., Windows.Foundation.Collections.PropertySet, ValueSet, StringMap) would throw MissingMethodException because the UnsafeAccessor stub emitted by cswinrt.exe referenced a static method that the interop generator never actually emitted.

Motivation

cswinrt.exe's write_*_using_static_abi_methods paths declare [UnsafeAccessor(...)] static extern ... Keys/Values(...) stubs that target ABI.System.Collections.Generic.<#corlib>IDictionary'2<K|V>Methods.Keys (and Values) in the interop sidecar (WinRT.Interop.dll). The interop generator's InteropTypeDefinitionBuilder.IDictionary2.Methods and InteropTypeDefinitionBuilder.IReadOnlyDictionary2.Methods only emit Item, Count, ContainsKey, TryGetValue (plus mutation surface for the mutable variant) — Keys and Values were missing. The UnsafeAccessor binder lazy-resolves on first call, so this only fails at runtime, on the first Keys or Values access.

While investigating, the IReadOnlyDictionary UnsafeAccessor stubs were also discovered to declare the wrong return type (ICollection<T>, copy-pasted from the IDictionary template). IReadOnlyDictionary<TKey, TValue>.Keys/.Values return IEnumerable<T>, and ReadOnlyDictionaryKeyCollection<TKey, TValue>/ReadOnlyDictionaryValueCollection<TKey, TValue> only implement IEnumerable<T>. UnsafeAccessor is signature-sensitive, so even if the interop generator emitted these methods with the correct IEnumerable<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 in TestComponentCSharp_Tests.cs covering the IDictionary path via StringMap / PropertySet / ValueSet (enumeration, count, contains, read-only contract, mutation reflection, repeated access, empty case, explicit cast); new C++ runtime class TestComponentCSharp.CustomReadOnlyDictionaryTest implementing IMapView<String, String> to provide direct coverage for the IReadOnlyDictionary path; new unit tests using it (direct, explicit cast, negative-type assertion pinning the IEnumerable<T> (not ICollection<T>) contract, empty case); functional/AOT tests in FunctionalTests/Collections/Program.cs mirroring both paths.
  • src/cswinrt/code_writers.h: fix write_readonlydictionary_members_using_static_abi_methods to declare the Keys/Values UnsafeAccessor stubs with return type IEnumerable<T> (matching the property body and the runtime collection types). The IDictionary path is unchanged because its ICollection<T> return type is already correct.
  • src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs and InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs: emit the missing Keys and Values static methods on the generated Methods types. Each method constructs the appropriate runtime collection wrapper around a fresh NativeObject built from the supplied WindowsRuntimeObjectReference (e.g., new DictionaryKeyCollection<TKey, TValue>(new NativeObject(thisReference))). The NativeObject inherits from WindowsRuntimeDictionary<...> / WindowsRuntimeReadOnlyDictionary<...>, which implement the interface required by the collection constructors.
  • src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs: reorder the emission so NativeObject runs before Methods for both IDictionary2 and IReadOnlyDictionary2 (the new Keys/Values method bodies depend on nativeObjectType). The NativeObject builder does not depend on the Methods type, so this reordering is safe.

Sergio0694 and others added 4 commits June 2, 2026 15:11
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>
@Sergio0694 Sergio0694 added bug Something isn't working AOT CsWinRT 3.0 labels Jun 2, 2026
@Sergio0694 Sergio0694 requested a review from manodasanW June 2, 2026 22:17
@Sergio0694 Sergio0694 marked this pull request as draft June 2, 2026 23:44
Sergio0694 and others added 2 commits June 3, 2026 12:02
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.
@Sergio0694 Sergio0694 marked this pull request as ready for review June 3, 2026 19:54
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AOT bug Something isn't working CsWinRT 3.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant