From 0c7c13470824b10eb051da4903b6c81d379db1cc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 2 Jun 2026 15:11:23 -0700 Subject: [PATCH 1/7] Normalize line endings in TestComponentCSharp 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. --- .../TestComponentCSharp.vcxproj.filters | 108 +++++++++--------- 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters b/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters index a2df1ce816..a72f764512 100644 --- a/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters +++ b/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters @@ -1,55 +1,55 @@ - - - - - accd3aa8-1ba0-4223-9bbe-0c431709210b - rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + accd3aa8-1ba0-4223-9bbe-0c431709210b + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 25b35ac5763b58e927ac66bae5f9a3c6bd787301 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 2 Jun 2026 15:00:54 -0700 Subject: [PATCH 2/7] Tests: add coverage for projected dictionary Keys/Values 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'. This is needed because no readily-available Windows SDK runtime class projects directly as 'IReadOnlyDictionary' (most map-like APIs return generic 'IMapView' interface values that go through a different runtime path). The new class projects as 'IReadOnlyDictionary' 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' and not 'ICollection', 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> --- .../FunctionalTests/Collections/Program.cs | 78 +++++ .../CustomReadOnlyDictionaryTest.cpp | 49 +++ .../CustomReadOnlyDictionaryTest.h | 28 ++ .../TestComponentCSharp.idl | 7 + .../TestComponentCSharp.vcxproj | 2 + .../TestComponentCSharp.vcxproj.filters | 2 + .../UnitTest/TestComponentCSharp_Tests.cs | 310 ++++++++++++++++++ 7 files changed, 476 insertions(+) create mode 100644 src/Tests/TestComponentCSharp/CustomReadOnlyDictionaryTest.cpp create mode 100644 src/Tests/TestComponentCSharp/CustomReadOnlyDictionaryTest.h diff --git a/src/Tests/FunctionalTests/Collections/Program.cs b/src/Tests/FunctionalTests/Collections/Program.cs index 082f641683..a53fdb5df8 100644 --- a/src/Tests/FunctionalTests/Collections/Program.cs +++ b/src/Tests/FunctionalTests/Collections/Program.cs @@ -150,6 +150,84 @@ } } +// Test 'Keys'/'Values' on a directly constructed runtime class that's projected via +// cswinrt's static_abi_methods path (uses the interop generator's '{Map}Methods.Keys/Values' +// statically resolved via 'UnsafeAccessor'). 'Windows.Foundation.Collections.StringMap' is a +// runtime class projected as 'IDictionary'; this exercises the trim/AOT path +// for both the cswinrt-generated 'Keys'/'Values' property and the interop-emitted static method. +var stringMapForKeysValues = new Windows.Foundation.Collections.StringMap +{ + ["one"] = "1", + ["two"] = "2", + ["three"] = "3" +}; + +System.Collections.Generic.ICollection stringMapKeys = stringMapForKeysValues.Keys; +System.Collections.Generic.ICollection stringMapValues = stringMapForKeysValues.Values; +if (stringMapKeys.Count != 3 || stringMapValues.Count != 3) +{ + return 101; +} +if (!stringMapKeys.Contains("one") || !stringMapKeys.Contains("two") || !stringMapKeys.Contains("three")) +{ + return 101; +} +if (!stringMapValues.Contains("1") || !stringMapValues.Contains("2") || !stringMapValues.Contains("3")) +{ + return 101; +} + +// Same scenario for 'PropertySet'/'ValueSet': different generic instantiation +// ('IDictionary') and so a different generated 'Methods' type. +var newPropertySet = new Windows.Foundation.Collections.PropertySet +{ + ["alpha"] = 1, + ["beta"] = "two" +}; + +System.Collections.Generic.ICollection newPropertySetKeys = newPropertySet.Keys; +System.Collections.Generic.ICollection newPropertySetValues = newPropertySet.Values; +if (newPropertySetKeys.Count != 2 || newPropertySetValues.Count != 2) +{ + return 101; +} +if (!newPropertySetKeys.Contains("alpha") || !newPropertySetKeys.Contains("beta")) +{ + return 101; +} + +var newValueSet = new Windows.Foundation.Collections.ValueSet +{ + ["x"] = 1.0, + ["y"] = 2.0 +}; + +if (newValueSet.Keys.Count != 2 || newValueSet.Values.Count != 2) +{ + return 101; +} + +// Test the parallel 'IReadOnlyDictionary' path through a runtime class that's projected +// as 'IReadOnlyDictionary'. This exercises the cswinrt-emitted projection +// (with the corrected 'IEnumerable' UnsafeAccessor return type) bound to the interop- +// emitted 'IReadOnlyDictionary'2Methods.Keys'/'Values' static methods, +// under trimming/AOT. +var customReadOnlyDictionary = new TestComponentCSharp.CustomReadOnlyDictionaryTest(); +System.Collections.Generic.IEnumerable customDictionaryKeys = customReadOnlyDictionary.Keys; +System.Collections.Generic.IEnumerable customDictionaryValues = customReadOnlyDictionary.Values; +if (customDictionaryKeys.Count() != 3 || customDictionaryValues.Count() != 3) +{ + return 101; +} +if (!customDictionaryKeys.Contains("apples") || !customDictionaryKeys.Contains("oranges") || !customDictionaryKeys.Contains("pears")) +{ + return 101; +} +if (!customDictionaryValues.Contains("1") || !customDictionaryValues.Contains("2") || !customDictionaryValues.Contains("3")) +{ + return 101; +} + var propertySet = Class.PropertySet; if (propertySet["beta"] is not string str || str != "second") { diff --git a/src/Tests/TestComponentCSharp/CustomReadOnlyDictionaryTest.cpp b/src/Tests/TestComponentCSharp/CustomReadOnlyDictionaryTest.cpp new file mode 100644 index 0000000000..caf2caccea --- /dev/null +++ b/src/Tests/TestComponentCSharp/CustomReadOnlyDictionaryTest.cpp @@ -0,0 +1,49 @@ +#include "pch.h" +#include "CustomReadOnlyDictionaryTest.h" +#include "CustomReadOnlyDictionaryTest.g.cpp" + +namespace winrt::TestComponentCSharp::implementation +{ + CustomReadOnlyDictionaryTest::CustomReadOnlyDictionaryTest() + { + // Default contents: a small set of string -> string entries suitable for 'Keys'/'Values' tests. + std::map initial{ + { L"apples", L"1" }, + { L"oranges", L"2" }, + { L"pears", L"3" } + }; + _mapView = winrt::single_threaded_map_view(std::move(initial)); + } + + CustomReadOnlyDictionaryTest::CustomReadOnlyDictionaryTest(winrt::Windows::Foundation::Collections::IMapView const& mapView) + { + _mapView = mapView; + } + + winrt::hstring CustomReadOnlyDictionaryTest::Lookup(winrt::hstring const& key) + { + return _mapView.Lookup(key); + } + + uint32_t CustomReadOnlyDictionaryTest::Size() + { + return _mapView.Size(); + } + + bool CustomReadOnlyDictionaryTest::HasKey(winrt::hstring const& key) + { + return _mapView.HasKey(key); + } + + void CustomReadOnlyDictionaryTest::Split( + winrt::Windows::Foundation::Collections::IMapView& first, + winrt::Windows::Foundation::Collections::IMapView& second) + { + _mapView.Split(first, second); + } + + winrt::Windows::Foundation::Collections::IIterator> CustomReadOnlyDictionaryTest::First() + { + return _mapView.First(); + } +} diff --git a/src/Tests/TestComponentCSharp/CustomReadOnlyDictionaryTest.h b/src/Tests/TestComponentCSharp/CustomReadOnlyDictionaryTest.h new file mode 100644 index 0000000000..d062da13aa --- /dev/null +++ b/src/Tests/TestComponentCSharp/CustomReadOnlyDictionaryTest.h @@ -0,0 +1,28 @@ +#pragma once +#include "CustomReadOnlyDictionaryTest.g.h" + +namespace winrt::TestComponentCSharp::implementation +{ + struct CustomReadOnlyDictionaryTest : CustomReadOnlyDictionaryTestT + { + CustomReadOnlyDictionaryTest(); + CustomReadOnlyDictionaryTest(winrt::Windows::Foundation::Collections::IMapView const& mapView); + + winrt::hstring Lookup(winrt::hstring const& key); + uint32_t Size(); + bool HasKey(winrt::hstring const& key); + void Split( + winrt::Windows::Foundation::Collections::IMapView& first, + winrt::Windows::Foundation::Collections::IMapView& second); + winrt::Windows::Foundation::Collections::IIterator> First(); + + winrt::Windows::Foundation::Collections::IMapView _mapView; + }; +} + +namespace winrt::TestComponentCSharp::factory_implementation +{ + struct CustomReadOnlyDictionaryTest : CustomReadOnlyDictionaryTestT + { + }; +} diff --git a/src/Tests/TestComponentCSharp/TestComponentCSharp.idl b/src/Tests/TestComponentCSharp/TestComponentCSharp.idl index 9645668d46..3c93d84344 100644 --- a/src/Tests/TestComponentCSharp/TestComponentCSharp.idl +++ b/src/Tests/TestComponentCSharp/TestComponentCSharp.idl @@ -601,6 +601,13 @@ namespace TestComponentCSharp static CustomIterableTest CreateWithCustomIterator(); } + [default_interface] + runtimeclass CustomReadOnlyDictionaryTest : Windows.Foundation.Collections.IMapView + { + CustomReadOnlyDictionaryTest(); + CustomReadOnlyDictionaryTest(Windows.Foundation.Collections.IMapView mapView); + } + // SupportedOSPlatform warning tests [contract(Windows.Foundation.UniversalApiContract, 10)] [attributeusage(target_all)] diff --git a/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj b/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj index 2013e28d1e..180eb5b9d8 100644 --- a/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj +++ b/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj @@ -89,6 +89,7 @@ + @@ -111,6 +112,7 @@ + Create diff --git a/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters b/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters index a72f764512..489731f499 100644 --- a/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters +++ b/src/Tests/TestComponentCSharp/TestComponentCSharp.vcxproj.filters @@ -25,6 +25,7 @@ + @@ -43,6 +44,7 @@ + diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index 2e4ee4737f..6c41bd7620 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -1662,6 +1662,316 @@ public void TestValueSetArrays() } } + [TestMethod] + public void TestStringMapKeysAndValues() + { + var stringMap = new Windows.Foundation.Collections.StringMap + { + ["foo"] = "bar", + ["hello"] = "world" + }; + + ICollection keys = stringMap.Keys; + ICollection values = stringMap.Values; + + Assert.IsNotNull(keys); + Assert.IsNotNull(values); + Assert.AreEqual(2, keys.Count); + Assert.AreEqual(2, values.Count); + + CollectionAssert.AreEquivalent(new[] { "foo", "hello" }, keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { "bar", "world" }, values.ToArray()); + + Assert.IsTrue(keys.Contains("foo")); + Assert.IsTrue(keys.Contains("hello")); + Assert.IsFalse(keys.Contains("missing")); + + Assert.IsTrue(values.Contains("bar")); + Assert.IsTrue(values.Contains("world")); + Assert.IsFalse(values.Contains("missing")); + + // 'Keys'/'Values' on a key/value collection are read-only views + Assert.IsTrue(keys.IsReadOnly); + Assert.IsTrue(values.IsReadOnly); + Assert.ThrowsExactly(() => keys.Add("nope")); + Assert.ThrowsExactly(() => values.Add("nope")); + Assert.ThrowsExactly(() => keys.Clear()); + Assert.ThrowsExactly(() => values.Clear()); + Assert.ThrowsExactly(() => keys.Remove("foo")); + Assert.ThrowsExactly(() => values.Remove("bar")); + + // 'CopyTo' on the key/value collections should produce the same set of items + string[] keyBuffer = new string[2]; + string[] valueBuffer = new string[2]; + keys.CopyTo(keyBuffer, 0); + values.CopyTo(valueBuffer, 0); + CollectionAssert.AreEquivalent(new[] { "foo", "hello" }, keyBuffer); + CollectionAssert.AreEquivalent(new[] { "bar", "world" }, valueBuffer); + } + + [TestMethod] + public void TestStringMapKeysAndValuesReflectMutation() + { + var stringMap = new Windows.Foundation.Collections.StringMap + { + ["foo"] = "bar" + }; + + ICollection keys = stringMap.Keys; + ICollection values = stringMap.Values; + + Assert.AreEqual(1, keys.Count); + Assert.AreEqual(1, values.Count); + + // Add new items + stringMap["hello"] = "world"; + + // The 'Keys'/'Values' collections are views over the underlying dictionary, + // so they should immediately reflect the mutation without needing a new lookup. + Assert.AreEqual(2, keys.Count); + Assert.AreEqual(2, values.Count); + Assert.IsTrue(keys.Contains("hello")); + Assert.IsTrue(values.Contains("world")); + + // Remove an existing item + Assert.IsTrue(stringMap.Remove("foo")); + + Assert.AreEqual(1, keys.Count); + Assert.AreEqual(1, values.Count); + Assert.IsFalse(keys.Contains("foo")); + Assert.IsFalse(values.Contains("bar")); + } + + [TestMethod] + public void TestStringMapKeysAndValuesRepeatedAccess() + { + var stringMap = new Windows.Foundation.Collections.StringMap + { + ["foo"] = "bar" + }; + + // Repeated access to 'Keys'/'Values' should always observe the current state and + // never throw. This is the basic guarantee callers rely on; whether each access + // returns the same wrapper instance or a fresh one is an implementation detail. + ICollection keys1 = stringMap.Keys; + ICollection keys2 = stringMap.Keys; + ICollection values1 = stringMap.Values; + ICollection values2 = stringMap.Values; + + Assert.AreEqual(1, keys1.Count); + Assert.AreEqual(1, keys2.Count); + Assert.AreEqual(1, values1.Count); + Assert.AreEqual(1, values2.Count); + CollectionAssert.AreEquivalent(keys1.ToArray(), keys2.ToArray()); + CollectionAssert.AreEquivalent(values1.ToArray(), values2.ToArray()); + } + + [TestMethod] + public void TestPropertySetKeysAndValues() + { + var propertySet = new Windows.Foundation.Collections.PropertySet + { + ["foo"] = "bar", + ["hello"] = 42 + }; + + ICollection keys = propertySet.Keys; + ICollection values = propertySet.Values; + + Assert.IsNotNull(keys); + Assert.IsNotNull(values); + Assert.AreEqual(2, keys.Count); + Assert.AreEqual(2, values.Count); + + CollectionAssert.AreEquivalent(new[] { "foo", "hello" }, keys.ToArray()); + CollectionAssert.AreEquivalent(new object[] { "bar", 42 }, values.ToArray()); + + // Cross-check that 'Keys' values can be used to index back into the dictionary + foreach (string key in keys) + { + Assert.IsTrue(propertySet.ContainsKey(key)); + Assert.IsTrue(values.Contains(propertySet[key])); + } + } + + [TestMethod] + public void TestPropertySetKeysAndValuesViaIDictionaryInterface() + { + // Cast to 'IDictionary' to force calls through the explicit + // interface implementation rather than directly on the projected runtime class. + IDictionary propertySet = new Windows.Foundation.Collections.PropertySet + { + ["alpha"] = 1L, + ["beta"] = "two", + ["gamma"] = true + }; + + ICollection keys = propertySet.Keys; + ICollection values = propertySet.Values; + + Assert.AreEqual(3, keys.Count); + Assert.AreEqual(3, values.Count); + + CollectionAssert.AreEquivalent(new[] { "alpha", "beta", "gamma" }, keys.ToArray()); + CollectionAssert.AreEquivalent(new object[] { 1L, "two", true }, values.ToArray()); + } + + [TestMethod] + public void TestValueSetKeysAndValues() + { + var valueSet = new Windows.Foundation.Collections.ValueSet + { + ["foo"] = "bar", + ["hello"] = 42 + }; + + ICollection keys = valueSet.Keys; + ICollection values = valueSet.Values; + + Assert.IsNotNull(keys); + Assert.IsNotNull(values); + Assert.AreEqual(2, keys.Count); + Assert.AreEqual(2, values.Count); + + CollectionAssert.AreEquivalent(new[] { "foo", "hello" }, keys.ToArray()); + CollectionAssert.AreEquivalent(new object[] { "bar", 42 }, values.ToArray()); + } + + [TestMethod] + public void TestValueSetKeysAndValuesAfterClear() + { + var valueSet = new Windows.Foundation.Collections.ValueSet + { + ["foo"] = "bar", + ["hello"] = 42 + }; + + ICollection keys = valueSet.Keys; + ICollection values = valueSet.Values; + + Assert.AreEqual(2, keys.Count); + Assert.AreEqual(2, values.Count); + + valueSet.Clear(); + + Assert.AreEqual(0, valueSet.Count); + Assert.AreEqual(0, keys.Count); + Assert.AreEqual(0, values.Count); + + using (IEnumerator e = keys.GetEnumerator()) + { + Assert.IsFalse(e.MoveNext()); + } + + using (IEnumerator e = values.GetEnumerator()) + { + Assert.IsFalse(e.MoveNext()); + } + } + + [TestMethod] + public void TestStringMapKeysAndValuesEmpty() + { + var stringMap = new Windows.Foundation.Collections.StringMap(); + + ICollection keys = stringMap.Keys; + ICollection values = stringMap.Values; + + Assert.IsNotNull(keys); + Assert.IsNotNull(values); + Assert.AreEqual(0, keys.Count); + Assert.AreEqual(0, values.Count); + + // Enumeration should produce nothing + Assert.IsFalse(keys.Any()); + Assert.IsFalse(values.Any()); + + // 'CopyTo' to a zero-length array should be a no-op (not throw) + keys.CopyTo([], 0); + values.CopyTo([], 0); + } + + [TestMethod] + public void TestCustomReadOnlyDictionaryKeysAndValues() + { + // 'CustomReadOnlyDictionaryTest' is a Windows Runtime class projected as + // 'IReadOnlyDictionary'. This exercises the cswinrt-generated + // 'Keys'/'Values' projection that uses the 'UnsafeAccessor' stub to call into + // the interop-emitted 'ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2Methods.Keys' + // (and 'Values') static methods. Unlike 'IDictionary', the 'IReadOnlyDictionary' + // 'Keys'/'Values' return 'IEnumerable' (not 'ICollection'); this also validates + // that the cswinrt-emitted 'UnsafeAccessor' return type matches the interop-emitted method. + var dictionary = new TestComponentCSharp.CustomReadOnlyDictionaryTest(); + + IEnumerable keys = dictionary.Keys; + IEnumerable values = dictionary.Values; + + Assert.IsNotNull(keys); + Assert.IsNotNull(values); + + CollectionAssert.AreEquivalent(new[] { "apples", "oranges", "pears" }, keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { "1", "2", "3" }, values.ToArray()); + + // Verify keys can be used to index back into the dictionary + foreach (string key in keys) + { + Assert.IsTrue(dictionary.ContainsKey(key)); + Assert.IsTrue(values.Contains(dictionary[key])); + } + } + + [TestMethod] + public void TestCustomReadOnlyDictionaryKeysAndValuesViaIReadOnlyDictionaryInterface() + { + // Cast to 'IReadOnlyDictionary' to force calls through the explicit + // interface implementation rather than directly on the projected runtime class. + IReadOnlyDictionary dictionary = new TestComponentCSharp.CustomReadOnlyDictionaryTest(); + + IEnumerable keys = dictionary.Keys; + IEnumerable values = dictionary.Values; + + Assert.AreEqual(3, keys.Count()); + Assert.AreEqual(3, values.Count()); + + CollectionAssert.AreEquivalent(new[] { "apples", "oranges", "pears" }, keys.ToArray()); + CollectionAssert.AreEquivalent(new[] { "1", "2", "3" }, values.ToArray()); + } + + [TestMethod] + public void TestCustomReadOnlyDictionaryKeysAndValuesEnumerationOnly() + { + // 'IReadOnlyDictionary.Keys' and '.Values' are 'IEnumerable' (not + // 'ICollection'), so they should not implicitly satisfy 'ICollection' callers. + // This pins down that contract — the cswinrt-emitted 'UnsafeAccessor' previously + // declared 'ICollection' as the return type by mistake, which would have caused + // a signature mismatch with the interop-emitted method and a 'MissingMethodException'. + var dictionary = new TestComponentCSharp.CustomReadOnlyDictionaryTest(); + + object keys = dictionary.Keys; + object values = dictionary.Values; + + Assert.IsNotNull(keys); + Assert.IsNotNull(values); + Assert.IsInstanceOfType>(keys); + Assert.IsInstanceOfType>(values); + Assert.IsNotInstanceOfType>(keys); + Assert.IsNotInstanceOfType>(values); + } + + [TestMethod] + public void TestCustomReadOnlyDictionaryKeysAndValuesEmpty() + { + // Construct with an empty backing map view so the projection is exercised + // with a zero-element dictionary. + var emptyDictionary = new Dictionary(); + IReadOnlyDictionary empty = emptyDictionary; + var dictionary = new TestComponentCSharp.CustomReadOnlyDictionaryTest(empty); + + Assert.AreEqual(0, dictionary.Count); + Assert.IsFalse(dictionary.Keys.Any()); + Assert.IsFalse(dictionary.Values.Any()); + } + [TestMethod] public void TestFactories() { From 1edfd50f189bf5a13fc01defa3fde3f0d94e6c49 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 2 Jun 2026 15:00:29 -0700 Subject: [PATCH 3/7] cswinrt: fix Keys/Values UnsafeAccessor return type for IReadOnlyDictionary projections The 'write_readonlydictionary_members_using_static_abi_methods' code path declared the Keys/Values UnsafeAccessor stubs with return type 'ICollection' copied from the 'IDictionary' template. This is wrong because: - 'IReadOnlyDictionary.Keys' returns 'IEnumerable', not 'ICollection' (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' and 'ReadOnlyDictionaryValueCollection' only implement 'IEnumerable' (not 'ICollection'). - 'UnsafeAccessor' is signature-sensitive: a stub with return type 'ICollection' would never bind to a method emitted with return type 'IEnumerable', resulting in a 'MissingMethodException' at the first call. Change both 'Keys' and 'Values' stubs to 'IEnumerable' to match the property shape and the runtime collection types. The parallel 'IDictionary' code path keeps 'ICollection' since that is the correct return type for that interface. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/cswinrt/code_writers.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cswinrt/code_writers.h b/src/cswinrt/code_writers.h index a780db203e..c512f74130 100644 --- a/src/cswinrt/code_writers.h +++ b/src/cswinrt/code_writers.h @@ -3530,10 +3530,10 @@ visibility, element, self, interop_method_name_prefix, objref_name); w.write(R"( [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "Keys")] -static extern ICollection<%> %Keys([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObjectReference objRef); +static extern IEnumerable<%> %Keys([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObjectReference objRef); [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "Values")] -static extern ICollection<%> %Values([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObjectReference objRef); +static extern IEnumerable<%> %Values([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObjectReference objRef); [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "Count")] static extern int %Count([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObjectReference objRef); From 8bc8fe78a324f2ab33d5fbd1f4ae7ea0d6f8e1c1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 2 Jun 2026 15:00:41 -0700 Subject: [PATCH 4/7] Interop generator: emit Keys/Values on dictionary 'Methods' types The cswinrt-generated projections for runtime classes whose interfaces map to 'IDictionary' or 'IReadOnlyDictionary' (e.g., 'PropertySet', 'ValueSet', 'StringMap') reference static helper methods 'ABI.System.Collections.Generic.<#corlib>IDictionary'2Methods.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(new NativeObject(thisReference)); This works because each generated 'NativeObject' inherits from the runtime base class 'WindowsRuntimeDictionary<...>' / 'WindowsRuntimeReadOnlyDictionary<...>', which already implement 'IDictionary' / 'IEnumerable>' 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> --- ...teropTypeDefinitionBuilder.IDictionary2.cs | 47 ++++++++++++++++ ...eDefinitionBuilder.IReadOnlyDictionary2.cs | 53 +++++++++++++++++++ .../Generation/InteropGenerator.Emit.cs | 18 ++++--- 3 files changed, 110 insertions(+), 8 deletions(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs index 5f6b3c5f35..c8ad2d7430 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs @@ -271,6 +271,7 @@ public static void IMapMethods( /// /// The for the type. /// The type returned by . + /// The type returned by . /// The instance to use. /// The emit state for this invocation. /// The interop module being built. @@ -278,6 +279,7 @@ public static void IMapMethods( public static void Methods( GenericInstanceTypeSignature dictionaryType, TypeDefinition mapMethodsType, + TypeDefinition nativeObjectType, InteropReferences interopReferences, InteropGeneratorEmitState emitState, ModuleDefinition module, @@ -414,6 +416,51 @@ public static void Methods( dictionaryMethodsType.Methods.Add(countMethod); + // Define the 'Keys' method as follows: + // + // public static ICollection<> Keys(WindowsRuntimeObjectReference thisReference) + // + // The 'NativeObject' instance derives from 'WindowsRuntimeDictionary<...>' which implements + // 'IDictionary', so it can be passed directly to the collection constructor. + MethodDefinition keysMethod = new( + name: "Keys"u8, + attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static, + signature: MethodSignature.CreateStatic( + returnType: interopReferences.ICollection1.MakeGenericReferenceType([keyType]), + parameterTypes: [interopReferences.WindowsRuntimeObjectReference.ToReferenceTypeSignature()])) + { + CilInstructions = + { + { Ldarg_0 }, + { Newobj, nativeObjectType.GetMethod(".ctor"u8) }, + { Newobj, interopReferences.DictionaryKeyCollection2_ctor(keyType, valueType) }, + { Ret } + } + }; + + dictionaryMethodsType.Methods.Add(keysMethod); + + // Define the 'Values' method as follows: + // + // public static ICollection<> Values(WindowsRuntimeObjectReference thisReference) + MethodDefinition valuesMethod = new( + name: "Values"u8, + attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static, + signature: MethodSignature.CreateStatic( + returnType: interopReferences.ICollection1.MakeGenericReferenceType([valueType]), + parameterTypes: [interopReferences.WindowsRuntimeObjectReference.ToReferenceTypeSignature()])) + { + CilInstructions = + { + { Ldarg_0 }, + { Newobj, nativeObjectType.GetMethod(".ctor"u8) }, + { Newobj, interopReferences.DictionaryValueCollection2_ctor(keyType, valueType) }, + { Ret } + } + }; + + dictionaryMethodsType.Methods.Add(valuesMethod); + // Define the 'ContainsKey' method as follows: // // public static bool ContainsKey(WindowsRuntimeObjectReference thisReference, key) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs index 063619b50a..ffdd45b5a2 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs @@ -144,6 +144,7 @@ public static void IMapViewMethods( /// /// The for the type. /// The type returned by . + /// The type returned by . /// The instance to use. /// The emit state for this invocation. /// The interop module being built. @@ -151,6 +152,7 @@ public static void IMapViewMethods( public static void Methods( GenericInstanceTypeSignature readOnlyDictionaryType, TypeDefinition mapViewMethodsType, + TypeDefinition nativeObjectType, InteropReferences interopReferences, InteropGeneratorEmitState emitState, ModuleDefinition module, @@ -220,6 +222,57 @@ public static void Methods( } }; + // Define the 'Keys' method as follows: + // + // public static IEnumerable<> Keys(WindowsRuntimeObjectReference thisReference) + // + // The 'NativeObject' instance derives from 'WindowsRuntimeReadOnlyDictionary<...>' which implements + // 'IEnumerable>', so it can be passed directly to the collection constructor. + MethodDefinition keysMethod = new( + name: "Keys"u8, + attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static, + signature: MethodSignature.CreateStatic( + returnType: interopReferences.IEnumerable1.MakeGenericReferenceType([keyType]), + parameterTypes: [interopReferences.WindowsRuntimeObjectReference.ToReferenceTypeSignature()])); + + readOnlyDictionaryMethodsType.Methods.Add(keysMethod); + + // Create a method body for the 'Keys' method + keysMethod.CilMethodBody = new CilMethodBody() + { + Instructions = + { + { Ldarg_0 }, + { Newobj, nativeObjectType.GetMethod(".ctor"u8) }, + { Newobj, interopReferences.ReadOnlyDictionaryKeyCollection2_ctor(keyType, valueType) }, + { Ret } + } + }; + + // Define the 'Values' method as follows: + // + // public static IEnumerable<> Values(WindowsRuntimeObjectReference thisReference) + MethodDefinition valuesMethod = new( + name: "Values"u8, + attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static, + signature: MethodSignature.CreateStatic( + returnType: interopReferences.IEnumerable1.MakeGenericReferenceType([valueType]), + parameterTypes: [interopReferences.WindowsRuntimeObjectReference.ToReferenceTypeSignature()])); + + readOnlyDictionaryMethodsType.Methods.Add(valuesMethod); + + // Create a method body for the 'Values' method + valuesMethod.CilMethodBody = new CilMethodBody() + { + Instructions = + { + { Ldarg_0 }, + { Newobj, nativeObjectType.GetMethod(".ctor"u8) }, + { Newobj, interopReferences.ReadOnlyDictionaryValueCollection2_ctor(keyType, valueType) }, + { Ret } + } + }; + // Define the 'ContainsKey' method as follows: // // public static bool ContainsKey(WindowsRuntimeObjectReference thisReference, key) diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs index 6817914a14..ceb4b3710e 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs @@ -1012,21 +1012,22 @@ private static void DefineIReadOnlyDictionaryTypes( module: module, mapViewMethodsType: out TypeDefinition mapViewMethodsType); - InteropTypeDefinitionBuilder.IReadOnlyDictionary2.Methods( + InteropTypeDefinitionBuilder.IReadOnlyDictionary2.NativeObject( readOnlyDictionaryType: typeSignature, mapViewMethodsType: mapViewMethodsType, interopReferences: interopReferences, emitState: emitState, module: module, - readOnlyDictionaryMethodsType: out TypeDefinition readOnlyDictionaryMethodsType); + out TypeDefinition nativeObjectType); - InteropTypeDefinitionBuilder.IReadOnlyDictionary2.NativeObject( + InteropTypeDefinitionBuilder.IReadOnlyDictionary2.Methods( readOnlyDictionaryType: typeSignature, mapViewMethodsType: mapViewMethodsType, + nativeObjectType: nativeObjectType, interopReferences: interopReferences, emitState: emitState, module: module, - out TypeDefinition nativeObjectType); + readOnlyDictionaryMethodsType: out TypeDefinition readOnlyDictionaryMethodsType); InteropTypeDefinitionBuilder.IReadOnlyDictionary2.ComWrappersCallbackType( readOnlyDictionaryType: typeSignature, @@ -1149,21 +1150,22 @@ private static void DefineIDictionaryTypes( module: module, mapMethodsType: out TypeDefinition mapMethodsType); - InteropTypeDefinitionBuilder.IDictionary2.Methods( + InteropTypeDefinitionBuilder.IDictionary2.NativeObject( dictionaryType: typeSignature, mapMethodsType: mapMethodsType, interopReferences: interopReferences, emitState: emitState, module: module, - dictionaryMethodsType: out TypeDefinition dictionaryMethodsType); + out TypeDefinition nativeObjectType); - InteropTypeDefinitionBuilder.IDictionary2.NativeObject( + InteropTypeDefinitionBuilder.IDictionary2.Methods( dictionaryType: typeSignature, mapMethodsType: mapMethodsType, + nativeObjectType: nativeObjectType, interopReferences: interopReferences, emitState: emitState, module: module, - out TypeDefinition nativeObjectType); + dictionaryMethodsType: out TypeDefinition dictionaryMethodsType); InteropTypeDefinitionBuilder.IDictionary2.ComWrappersCallbackType( dictionaryType: typeSignature, From 79a2faaad40465c38980e56d6a794f567a2e30f1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 3 Jun 2026 12:02:49 -0700 Subject: [PATCH 5/7] Optimize Keys/Values projection to avoid 'NativeObject' allocation 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 '=> (null, )' to '=> field ??= (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> --- .../UnitTest/TestComponentCSharp_Tests.cs | 12 ++++------ ...teropTypeDefinitionBuilder.IDictionary2.cs | 19 +++++++-------- ...eDefinitionBuilder.IReadOnlyDictionary2.cs | 22 +++++++++-------- .../Generation/InteropGenerator.Emit.cs | 18 +++++++------- src/cswinrt/code_writers.h | 24 +++++++++---------- 5 files changed, 46 insertions(+), 49 deletions(-) diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index 6c41bd7620..544d917c8a 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -1750,20 +1750,18 @@ public void TestStringMapKeysAndValuesRepeatedAccess() ["foo"] = "bar" }; - // Repeated access to 'Keys'/'Values' should always observe the current state and - // never throw. This is the basic guarantee callers rely on; whether each access - // returns the same wrapper instance or a fresh one is an implementation detail. + // The 'Keys' and 'Values' properties cache the wrapper collection (via the C# 14 + // 'field ??=' pattern) so repeated access returns the same instance, matching the + // caching behavior of the dictionary native object in 'WinRT.Runtime'. ICollection keys1 = stringMap.Keys; ICollection keys2 = stringMap.Keys; ICollection values1 = stringMap.Values; ICollection values2 = stringMap.Values; Assert.AreEqual(1, keys1.Count); - Assert.AreEqual(1, keys2.Count); Assert.AreEqual(1, values1.Count); - Assert.AreEqual(1, values2.Count); - CollectionAssert.AreEquivalent(keys1.ToArray(), keys2.ToArray()); - CollectionAssert.AreEquivalent(values1.ToArray(), values2.ToArray()); + Assert.AreSame(keys1, keys2); + Assert.AreSame(values1, values2); } [TestMethod] diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs index c8ad2d7430..bab44e0b97 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs @@ -271,7 +271,6 @@ public static void IMapMethods( /// /// The for the type. /// The type returned by . - /// The type returned by . /// The instance to use. /// The emit state for this invocation. /// The interop module being built. @@ -279,7 +278,6 @@ public static void IMapMethods( public static void Methods( GenericInstanceTypeSignature dictionaryType, TypeDefinition mapMethodsType, - TypeDefinition nativeObjectType, InteropReferences interopReferences, InteropGeneratorEmitState emitState, ModuleDefinition module, @@ -418,21 +416,22 @@ public static void Methods( // Define the 'Keys' method as follows: // - // public static ICollection<> Keys(WindowsRuntimeObjectReference thisReference) + // public static ICollection<> Keys(WindowsRuntimeObject windowsRuntimeObject) // - // The 'NativeObject' instance derives from 'WindowsRuntimeDictionary<...>' which implements - // 'IDictionary', so it can be passed directly to the collection constructor. + // The runtime instance passed in is the projected runtime class itself, which directly implements + // 'IDictionary'. The 'castclass' makes the IL verifiable; no extra object allocation + // is needed. MethodDefinition keysMethod = new( name: "Keys"u8, attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static, signature: MethodSignature.CreateStatic( returnType: interopReferences.ICollection1.MakeGenericReferenceType([keyType]), - parameterTypes: [interopReferences.WindowsRuntimeObjectReference.ToReferenceTypeSignature()])) + parameterTypes: [interopReferences.WindowsRuntimeObject.ToReferenceTypeSignature()])) { CilInstructions = { { Ldarg_0 }, - { Newobj, nativeObjectType.GetMethod(".ctor"u8) }, + { Castclass, dictionaryType.ToTypeDefOrRef() }, { Newobj, interopReferences.DictionaryKeyCollection2_ctor(keyType, valueType) }, { Ret } } @@ -442,18 +441,18 @@ public static void Methods( // Define the 'Values' method as follows: // - // public static ICollection<> Values(WindowsRuntimeObjectReference thisReference) + // public static ICollection<> Values(WindowsRuntimeObject windowsRuntimeObject) MethodDefinition valuesMethod = new( name: "Values"u8, attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static, signature: MethodSignature.CreateStatic( returnType: interopReferences.ICollection1.MakeGenericReferenceType([valueType]), - parameterTypes: [interopReferences.WindowsRuntimeObjectReference.ToReferenceTypeSignature()])) + parameterTypes: [interopReferences.WindowsRuntimeObject.ToReferenceTypeSignature()])) { CilInstructions = { { Ldarg_0 }, - { Newobj, nativeObjectType.GetMethod(".ctor"u8) }, + { Castclass, dictionaryType.ToTypeDefOrRef() }, { Newobj, interopReferences.DictionaryValueCollection2_ctor(keyType, valueType) }, { Ret } } diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs index ffdd45b5a2..ac81aac26e 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs @@ -144,7 +144,6 @@ public static void IMapViewMethods( /// /// The for the type. /// The type returned by . - /// The type returned by . /// The instance to use. /// The emit state for this invocation. /// The interop module being built. @@ -152,7 +151,6 @@ public static void IMapViewMethods( public static void Methods( GenericInstanceTypeSignature readOnlyDictionaryType, TypeDefinition mapViewMethodsType, - TypeDefinition nativeObjectType, InteropReferences interopReferences, InteropGeneratorEmitState emitState, ModuleDefinition module, @@ -224,16 +222,20 @@ public static void Methods( // Define the 'Keys' method as follows: // - // public static IEnumerable<> Keys(WindowsRuntimeObjectReference thisReference) + // public static IEnumerable<> Keys(WindowsRuntimeObject windowsRuntimeObject) // - // The 'NativeObject' instance derives from 'WindowsRuntimeReadOnlyDictionary<...>' which implements - // 'IEnumerable>', so it can be passed directly to the collection constructor. + // The runtime instance passed in is the projected runtime class itself, which directly implements + // 'IEnumerable>' (via 'IReadOnlyDictionary'). The 'castclass' + // makes the IL verifiable; no extra object allocation is needed. + ITypeDefOrRef keyValuePairEnumerableType = interopReferences.IEnumerable1.MakeGenericReferenceType([ + interopReferences.KeyValuePair2.MakeGenericValueType([keyType, valueType])]).ToTypeDefOrRef(); + MethodDefinition keysMethod = new( name: "Keys"u8, attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static, signature: MethodSignature.CreateStatic( returnType: interopReferences.IEnumerable1.MakeGenericReferenceType([keyType]), - parameterTypes: [interopReferences.WindowsRuntimeObjectReference.ToReferenceTypeSignature()])); + parameterTypes: [interopReferences.WindowsRuntimeObject.ToReferenceTypeSignature()])); readOnlyDictionaryMethodsType.Methods.Add(keysMethod); @@ -243,7 +245,7 @@ public static void Methods( Instructions = { { Ldarg_0 }, - { Newobj, nativeObjectType.GetMethod(".ctor"u8) }, + { Castclass, keyValuePairEnumerableType }, { Newobj, interopReferences.ReadOnlyDictionaryKeyCollection2_ctor(keyType, valueType) }, { Ret } } @@ -251,13 +253,13 @@ public static void Methods( // Define the 'Values' method as follows: // - // public static IEnumerable<> Values(WindowsRuntimeObjectReference thisReference) + // public static IEnumerable<> Values(WindowsRuntimeObject windowsRuntimeObject) MethodDefinition valuesMethod = new( name: "Values"u8, attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static, signature: MethodSignature.CreateStatic( returnType: interopReferences.IEnumerable1.MakeGenericReferenceType([valueType]), - parameterTypes: [interopReferences.WindowsRuntimeObjectReference.ToReferenceTypeSignature()])); + parameterTypes: [interopReferences.WindowsRuntimeObject.ToReferenceTypeSignature()])); readOnlyDictionaryMethodsType.Methods.Add(valuesMethod); @@ -267,7 +269,7 @@ public static void Methods( Instructions = { { Ldarg_0 }, - { Newobj, nativeObjectType.GetMethod(".ctor"u8) }, + { Castclass, keyValuePairEnumerableType }, { Newobj, interopReferences.ReadOnlyDictionaryValueCollection2_ctor(keyType, valueType) }, { Ret } } diff --git a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs index ceb4b3710e..6817914a14 100644 --- a/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs +++ b/src/WinRT.Interop.Generator/Generation/InteropGenerator.Emit.cs @@ -1012,22 +1012,21 @@ private static void DefineIReadOnlyDictionaryTypes( module: module, mapViewMethodsType: out TypeDefinition mapViewMethodsType); - InteropTypeDefinitionBuilder.IReadOnlyDictionary2.NativeObject( + InteropTypeDefinitionBuilder.IReadOnlyDictionary2.Methods( readOnlyDictionaryType: typeSignature, mapViewMethodsType: mapViewMethodsType, interopReferences: interopReferences, emitState: emitState, module: module, - out TypeDefinition nativeObjectType); + readOnlyDictionaryMethodsType: out TypeDefinition readOnlyDictionaryMethodsType); - InteropTypeDefinitionBuilder.IReadOnlyDictionary2.Methods( + InteropTypeDefinitionBuilder.IReadOnlyDictionary2.NativeObject( readOnlyDictionaryType: typeSignature, mapViewMethodsType: mapViewMethodsType, - nativeObjectType: nativeObjectType, interopReferences: interopReferences, emitState: emitState, module: module, - readOnlyDictionaryMethodsType: out TypeDefinition readOnlyDictionaryMethodsType); + out TypeDefinition nativeObjectType); InteropTypeDefinitionBuilder.IReadOnlyDictionary2.ComWrappersCallbackType( readOnlyDictionaryType: typeSignature, @@ -1150,22 +1149,21 @@ private static void DefineIDictionaryTypes( module: module, mapMethodsType: out TypeDefinition mapMethodsType); - InteropTypeDefinitionBuilder.IDictionary2.NativeObject( + InteropTypeDefinitionBuilder.IDictionary2.Methods( dictionaryType: typeSignature, mapMethodsType: mapMethodsType, interopReferences: interopReferences, emitState: emitState, module: module, - out TypeDefinition nativeObjectType); + dictionaryMethodsType: out TypeDefinition dictionaryMethodsType); - InteropTypeDefinitionBuilder.IDictionary2.Methods( + InteropTypeDefinitionBuilder.IDictionary2.NativeObject( dictionaryType: typeSignature, mapMethodsType: mapMethodsType, - nativeObjectType: nativeObjectType, interopReferences: interopReferences, emitState: emitState, module: module, - dictionaryMethodsType: out TypeDefinition dictionaryMethodsType); + out TypeDefinition nativeObjectType); InteropTypeDefinitionBuilder.IDictionary2.ComWrappersCallbackType( dictionaryType: typeSignature, diff --git a/src/cswinrt/code_writers.h b/src/cswinrt/code_writers.h index c512f74130..29b4b13b11 100644 --- a/src/cswinrt/code_writers.h +++ b/src/cswinrt/code_writers.h @@ -3530,10 +3530,10 @@ visibility, element, self, interop_method_name_prefix, objref_name); w.write(R"( [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "Keys")] -static extern IEnumerable<%> %Keys([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObjectReference objRef); +static extern IEnumerable<%> %Keys([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObject windowsRuntimeObject); [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "Values")] -static extern IEnumerable<%> %Values([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObjectReference objRef); +static extern IEnumerable<%> %Values([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObject windowsRuntimeObject); [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "Count")] static extern int %Count([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IReadOnlyDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObjectReference objRef); @@ -3556,15 +3556,15 @@ interop_method_name_prefix, key_interop_type_name, value_interop_type_name, key, ); w.write(R"( -%IEnumerable<%> %Keys => %Keys(null, %); -%IEnumerable<%> %Values => %Values(null, %); +%IEnumerable<%> %Keys => field ??= %Keys(null, this); +%IEnumerable<%> %Values => field ??= %Values(null, this); %int %Count => %Count(null, %); %% %this[% key] => %Item(null, %, key); %bool %ContainsKey(% key) => %ContainsKey(null, %, key); %bool %TryGetValue(% key, out % value) => %TryGetValue(null, %, key, out value); )", -visibility, key, self, interop_method_name_prefix, objref_name, -visibility, value, self, interop_method_name_prefix, objref_name, +visibility, key, self, interop_method_name_prefix, +visibility, value, self, interop_method_name_prefix, visibility, ireadonlycollection, interop_method_name_prefix, objref_name, visibility, value, self, key, interop_method_name_prefix, objref_name, visibility, self, key, interop_method_name_prefix, objref_name, @@ -3616,10 +3616,10 @@ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); w.write(R"( [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "Keys")] -static extern ICollection<%> %Keys([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObjectReference objRef); +static extern ICollection<%> %Keys([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObject windowsRuntimeObject); [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "Values")] -static extern ICollection<%> %Values([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObjectReference objRef); +static extern ICollection<%> %Values([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObject windowsRuntimeObject); [UnsafeAccessor(UnsafeAccessorKind.StaticMethod, Name = "Count")] static extern int %Count([UnsafeAccessorType("ABI.System.Collections.Generic.<#corlib>IDictionary'2<%|%>Methods, WinRT.Interop")] object _, WindowsRuntimeObjectReference objRef); @@ -3674,8 +3674,8 @@ interop_method_name_prefix, key_interop_type_name, value_interop_type_name, key, ); w.write(R"( -%ICollection<%> %Keys => %Keys(null, %); -%ICollection<%> %Values => %Values(null, %); +%ICollection<%> %Keys => field ??= %Keys(null, this); +%ICollection<%> %Values => field ??= %Values(null, this); %int %Count => %Count(null, %); %bool %IsReadOnly => false; %% %this[% key] @@ -3693,8 +3693,8 @@ set => %Item(null, %, key, value); %void %CopyTo(KeyValuePair<%, %>[] array, int arrayIndex) => %CopyTo(null, %, %, array, arrayIndex); bool ICollection>.Remove(KeyValuePair<%, %> item) => %Remove(null, %, item); )", -visibility, key, self, interop_method_name_prefix, objref_name, //Keys -visibility, value, self, interop_method_name_prefix, objref_name, // Values +visibility, key, self, interop_method_name_prefix, //Keys +visibility, value, self, interop_method_name_prefix, // Values visibility, icollection, interop_method_name_prefix, objref_name, // Count visibility, icollection, // IsReadOnly visibility, value, self, key, interop_method_name_prefix, objref_name, interop_method_name_prefix, objref_name, // Indexer From 5bb07751c82e584e3580ad473bcaa55fdf9ed61d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 3 Jun 2026 12:51:17 -0700 Subject: [PATCH 6/7] Remove redundant Castclass and update comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../UnitTest/TestComponentCSharp_Tests.cs | 72 +++++++++---------- ...teropTypeDefinitionBuilder.IDictionary2.cs | 11 ++- ...eDefinitionBuilder.IReadOnlyDictionary2.cs | 13 +--- 3 files changed, 44 insertions(+), 52 deletions(-) diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index 544d917c8a..1a6b1dda1f 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -1750,9 +1750,9 @@ public void TestStringMapKeysAndValuesRepeatedAccess() ["foo"] = "bar" }; - // The 'Keys' and 'Values' properties cache the wrapper collection (via the C# 14 - // 'field ??=' pattern) so repeated access returns the same instance, matching the - // caching behavior of the dictionary native object in 'WinRT.Runtime'. + // The 'Keys' and 'Values' properties cache the wrapper collection, + // so repeated access returns the same instance, matching the caching + // behavior of the dictionary native object in 'WinRT.Runtime'. ICollection keys1 = stringMap.Keys; ICollection keys2 = stringMap.Keys; ICollection values1 = stringMap.Values; @@ -4458,39 +4458,39 @@ private void TestSupportedOSPlatformWarnings() WarningStatic.WarningEvent += (object s, Int32 v) => { }; // warning CA1416 } - [TestMethod] - public void TestOverridable() - { - var obj = new OverridableTestClass(); - - // Test overridable property round-trip through native overrides interface - Assert.AreEqual(42, obj.CallOverridablePropertyGetter()); - obj.CallOverridablePropertySetter(99); - Assert.AreEqual(99, obj.CallOverridablePropertyGetter()); - - // Test overridable method round-trip through native overrides interface - Assert.IsFalse(obj.MethodWasCalled); - obj.CallOverridableMethod(); - Assert.IsTrue(obj.MethodWasCalled); - } - - class OverridableTestClass : WarningClass - { - private int _value = 42; - public bool MethodWasCalled { get; private set; } - - protected override int WarningOverridableProperty - { - get => _value; - set => _value = value; - } - - protected override void WarningOverridableMethod() - { - MethodWasCalled = true; - } - } - + [TestMethod] + public void TestOverridable() + { + var obj = new OverridableTestClass(); + + // Test overridable property round-trip through native overrides interface + Assert.AreEqual(42, obj.CallOverridablePropertyGetter()); + obj.CallOverridablePropertySetter(99); + Assert.AreEqual(99, obj.CallOverridablePropertyGetter()); + + // Test overridable method round-trip through native overrides interface + Assert.IsFalse(obj.MethodWasCalled); + obj.CallOverridableMethod(); + Assert.IsTrue(obj.MethodWasCalled); + } + + class OverridableTestClass : WarningClass + { + private int _value = 42; + public bool MethodWasCalled { get; private set; } + + protected override int WarningOverridableProperty + { + get => _value; + set => _value = value; + } + + protected override void WarningOverridableMethod() + { + MethodWasCalled = true; + } + } + [TestMethod] public void TestObjectFunctions() { diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs index bab44e0b97..220ec2d23a 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IDictionary2.cs @@ -416,11 +416,12 @@ public static void Methods( // Define the 'Keys' method as follows: // - // public static ICollection<> Keys(WindowsRuntimeObject windowsRuntimeObject) + // public static ICollection<> Keys(WindowsRuntimeObject thisObject) // // The runtime instance passed in is the projected runtime class itself, which directly implements - // 'IDictionary'. The 'castclass' makes the IL verifiable; no extra object allocation - // is needed. + // 'IDictionary'. This is strictly part of the contract, and we don't need to validate + // it at runtime (with a cast), in the same way as we don't do additional 'QueryInterface' calls on + // the various object references being passed around as arguments to interop methods. MethodDefinition keysMethod = new( name: "Keys"u8, attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static, @@ -431,7 +432,6 @@ public static void Methods( CilInstructions = { { Ldarg_0 }, - { Castclass, dictionaryType.ToTypeDefOrRef() }, { Newobj, interopReferences.DictionaryKeyCollection2_ctor(keyType, valueType) }, { Ret } } @@ -441,7 +441,7 @@ public static void Methods( // Define the 'Values' method as follows: // - // public static ICollection<> Values(WindowsRuntimeObject windowsRuntimeObject) + // public static ICollection<> Values(WindowsRuntimeObject thisObject) MethodDefinition valuesMethod = new( name: "Values"u8, attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static, @@ -452,7 +452,6 @@ public static void Methods( CilInstructions = { { Ldarg_0 }, - { Castclass, dictionaryType.ToTypeDefOrRef() }, { Newobj, interopReferences.DictionaryValueCollection2_ctor(keyType, valueType) }, { Ret } } diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs index ac81aac26e..6d8bd5aa70 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs @@ -222,14 +222,9 @@ public static void Methods( // Define the 'Keys' method as follows: // - // public static IEnumerable<> Keys(WindowsRuntimeObject windowsRuntimeObject) + // public static IEnumerable<> Keys(WindowsRuntimeObject thisObject) // - // The runtime instance passed in is the projected runtime class itself, which directly implements - // 'IEnumerable>' (via 'IReadOnlyDictionary'). The 'castclass' - // makes the IL verifiable; no extra object allocation is needed. - ITypeDefOrRef keyValuePairEnumerableType = interopReferences.IEnumerable1.MakeGenericReferenceType([ - interopReferences.KeyValuePair2.MakeGenericValueType([keyType, valueType])]).ToTypeDefOrRef(); - + // See additional notes in 'InteropTypeDefinitionBuilder.IDictionary2.Methods' (analogous logic). MethodDefinition keysMethod = new( name: "Keys"u8, attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static, @@ -245,7 +240,6 @@ public static void Methods( Instructions = { { Ldarg_0 }, - { Castclass, keyValuePairEnumerableType }, { Newobj, interopReferences.ReadOnlyDictionaryKeyCollection2_ctor(keyType, valueType) }, { Ret } } @@ -253,7 +247,7 @@ public static void Methods( // Define the 'Values' method as follows: // - // public static IEnumerable<> Values(WindowsRuntimeObject windowsRuntimeObject) + // public static IEnumerable<> Values(WindowsRuntimeObject thisObject) MethodDefinition valuesMethod = new( name: "Values"u8, attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static, @@ -269,7 +263,6 @@ public static void Methods( Instructions = { { Ldarg_0 }, - { Castclass, keyValuePairEnumerableType }, { Newobj, interopReferences.ReadOnlyDictionaryValueCollection2_ctor(keyType, valueType) }, { Ret } } From f11c721f12b9771d0f6c96352f1223a093da73af Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 3 Jun 2026 12:58:01 -0700 Subject: [PATCH 7/7] Inline 'Keys'/'Values' method body via 'CilInstructions' initializer 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> --- ...eDefinitionBuilder.IReadOnlyDictionary2.cs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs index 6d8bd5aa70..031deed436 100644 --- a/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs +++ b/src/WinRT.Interop.Generator/Builders/InteropTypeDefinitionBuilder.IReadOnlyDictionary2.cs @@ -230,14 +230,9 @@ public static void Methods( attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static, signature: MethodSignature.CreateStatic( returnType: interopReferences.IEnumerable1.MakeGenericReferenceType([keyType]), - parameterTypes: [interopReferences.WindowsRuntimeObject.ToReferenceTypeSignature()])); - - readOnlyDictionaryMethodsType.Methods.Add(keysMethod); - - // Create a method body for the 'Keys' method - keysMethod.CilMethodBody = new CilMethodBody() + parameterTypes: [interopReferences.WindowsRuntimeObject.ToReferenceTypeSignature()])) { - Instructions = + CilInstructions = { { Ldarg_0 }, { Newobj, interopReferences.ReadOnlyDictionaryKeyCollection2_ctor(keyType, valueType) }, @@ -245,6 +240,8 @@ public static void Methods( } }; + readOnlyDictionaryMethodsType.Methods.Add(keysMethod); + // Define the 'Values' method as follows: // // public static IEnumerable<> Values(WindowsRuntimeObject thisObject) @@ -253,14 +250,9 @@ public static void Methods( attributes: MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.Static, signature: MethodSignature.CreateStatic( returnType: interopReferences.IEnumerable1.MakeGenericReferenceType([valueType]), - parameterTypes: [interopReferences.WindowsRuntimeObject.ToReferenceTypeSignature()])); - - readOnlyDictionaryMethodsType.Methods.Add(valuesMethod); - - // Create a method body for the 'Values' method - valuesMethod.CilMethodBody = new CilMethodBody() + parameterTypes: [interopReferences.WindowsRuntimeObject.ToReferenceTypeSignature()])) { - Instructions = + CilInstructions = { { Ldarg_0 }, { Newobj, interopReferences.ReadOnlyDictionaryValueCollection2_ctor(keyType, valueType) }, @@ -268,6 +260,8 @@ public static void Methods( } }; + readOnlyDictionaryMethodsType.Methods.Add(valuesMethod); + // Define the 'ContainsKey' method as follows: // // public static bool ContainsKey(WindowsRuntimeObjectReference thisReference, key)