From ffde180ff2b86bfb84bd0ab2df0108a32598bd66 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 30 Jun 2026 15:25:23 -0700 Subject: [PATCH 1/2] Add unit tests for collection expressions targeting read-only collection interfaces Collection expressions targeting a read-only collection interface (IEnumerable, IReadOnlyList, IReadOnlyCollection) lower to compiler synthesized backing types (<>z__ReadOnlyArray and <>z__ReadOnlySingleElementList) that are marshalled across the WinRT ABI as CCWs. Add CoreCLR unit tests covering these scenarios, validating that the CCW for the synthesized type exposes IIterable, IVectorView, and IVector (not just IIterable), for both the multi-element and single-element backing types. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../UnitTest/TestComponentCSharp_Tests.cs | 71 +++++++++++++++++++ src/Tests/UnitTest/TestComponent_Tests.cs | 12 ++++ 2 files changed, 83 insertions(+) diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index 78a2db97de..aee421ddf6 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -2603,6 +2603,77 @@ public void TestBindable() Assert.AreEqual(6, observable.Observation); } + [TestMethod] + public unsafe void TestCollectionExpressionReadOnlyInterfaceMarshalling() + { + // Collection expressions targeting a read-only collection interface ('IEnumerable', + // 'IReadOnlyList', 'IReadOnlyCollection') lower to compiler synthesized backing types + // ('<>z__ReadOnlyArray' for multiple elements and '<>z__ReadOnlySingleElementList' for + // a single element). Marshalling them across the WinRT ABI builds a CCW for the synthesized type. + IEnumerable enumerable = [10, 20, 30]; + Assert.AreEqual(60, SumViaIterator(enumerable)); + + IReadOnlyList readOnlyList = [10, 20, 30]; + Assert.AreEqual(60, SumViaIterator(readOnlyList)); + + IReadOnlyCollection readOnlyCollection = [10, 20, 30]; + Assert.AreEqual(60, SumViaIterator(readOnlyCollection)); + + // Single element uses the '<>z__ReadOnlySingleElementList' backing type + IEnumerable singleElement = [42]; + Assert.AreEqual(42, SumViaIterator(singleElement)); + + // The synthesized backing type implements 'IEnumerable', 'IReadOnlyList', and + // 'IList', so its CCW exposes 'IIterable', 'IVectorView', and 'IVector' + AssertCcwExposesCollectionInterfaces([10, 20, 30]); + AssertCcwExposesCollectionInterfaces([42]); + + // Values must be sequential from 0 because the native bindable setter validates them + IReadOnlyList bindable = [0, 1, 2]; + TestObject.BindableIterableProperty = bindable; + Assert.AreEqual(bindable, TestObject.BindableIterableProperty); + + int SumViaIterator(IEnumerable values) + { + int sum = 0; + var iterator = TestObject.GetIteratorForCollection(values); + while (iterator.MoveNext()) + { + sum += iterator.Current; + } + + return sum; + } + + static void AssertCcwExposesCollectionInterfaces(IReadOnlyList source) + { + Guid iidIIterableInt = new("81A643FB-F51C-5565-83C4-F96425777B66"); + Guid iidIVectorViewInt = new("8D720CDF-3934-5D3F-9A55-40E8063B086A"); + Guid iidIVectorInt = new("B939AF5B-B45D-5489-9149-61442C1905FE"); + + void* ccw = WindowsRuntimeMarshal.ConvertToUnmanaged(source); + + try + { + AssertHasInterface(ccw, in iidIIterableInt); + AssertHasInterface(ccw, in iidIVectorViewInt); + AssertHasInterface(ccw, in iidIVectorInt); + } + finally + { + _ = Marshal.Release((nint)ccw); + } + + static void AssertHasInterface(void* ccw, in Guid iid) + { + Marshal.ThrowExceptionForHR(Marshal.QueryInterface((nint)ccw, in iid, out nint interfaceCcw)); + Assert.AreNotEqual(IntPtr.Zero, interfaceCcw); + + _ = Marshal.Release(interfaceCcw); + } + } + } + [TestMethod] public void TestClassGeneric() { diff --git a/src/Tests/UnitTest/TestComponent_Tests.cs b/src/Tests/UnitTest/TestComponent_Tests.cs index 7b23bcd003..357c23b872 100644 --- a/src/Tests/UnitTest/TestComponent_Tests.cs +++ b/src/Tests/UnitTest/TestComponent_Tests.cs @@ -883,6 +883,18 @@ public void Collections_ReadOnly_List_Call() }); } + [TestMethod] + public void Collections_ReadOnly_List_Call_CollectionExpression() + { + // Collection expressions typed as 'IReadOnlyList' use a compiler synthesized + // backing type that marshals to native as a CCW exposing 'IVectorView'. + Tests.Collection6Call((IReadOnlyList a, out IReadOnlyList b) => + { + b = [.. a]; + return [.. a]; + }); + } + [TestMethod] public void TestComposable() { From 6faef2f37fa28dcf050fc1e8ccdd491be21ca468 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 30 Jun 2026 15:25:24 -0700 Subject: [PATCH 2/2] Add trim/AOT tests for collection expressions targeting read-only collection interfaces Mirror the collection-expression read-only collection interface scenarios in the Collections functional test so the interop generator's CCW discovery for the compiler synthesized backing types (<>z__ReadOnlyArray and <>z__ReadOnlySingleElementList) is exercised under trimming and Native AOT, including validating that the synthesized type's CCW exposes IIterable, IVectorView, and IVector. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../FunctionalTests/Collections/Program.cs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/src/Tests/FunctionalTests/Collections/Program.cs b/src/Tests/FunctionalTests/Collections/Program.cs index a53fdb5df8..39e19b3add 100644 --- a/src/Tests/FunctionalTests/Collections/Program.cs +++ b/src/Tests/FunctionalTests/Collections/Program.cs @@ -630,12 +630,112 @@ return 101; } +// Collection expressions targeting a read-only collection interface ('IEnumerable', +// 'IReadOnlyList', 'IReadOnlyCollection') lower to compiler synthesized backing types +// ('<>z__ReadOnlyArray' for multiple elements and '<>z__ReadOnlySingleElementList' for a +// single element). Marshalling them across the WinRT ABI builds a CCW for the synthesized type. +IEnumerable collectionExpressionEnumerable = [10, 20, 30]; +if (SumViaIterator(instance, collectionExpressionEnumerable) != 60) +{ + return 106; +} + +IReadOnlyList collectionExpressionReadOnlyList = [10, 20, 30]; +if (SumViaIterator(instance, collectionExpressionReadOnlyList) != 60) +{ + return 107; +} + +IReadOnlyCollection collectionExpressionReadOnlyCollection = [10, 20, 30]; +if (SumViaIterator(instance, collectionExpressionReadOnlyCollection) != 60) +{ + return 108; +} + +// Single element uses the '<>z__ReadOnlySingleElementList' backing type +IEnumerable collectionExpressionSingleElement = [42]; +if (SumViaIterator(instance, collectionExpressionSingleElement) != 42) +{ + return 109; +} + +// The synthesized backing type implements 'IEnumerable', 'IReadOnlyList', and 'IList', +// so its CCW exposes 'IIterable', 'IVectorView', and 'IVector' +if (!CcwExposesCollectionInterfaces([10, 20, 30])) +{ + return 110; +} + +if (!CcwExposesCollectionInterfaces([42])) +{ + return 111; +} + +// Values must be sequential from 0 because the native bindable setter validates them +IReadOnlyList collectionExpressionBindable = [0, 1, 2]; +instance.BindableIterableProperty = collectionExpressionBindable; +if (collectionExpressionBindable != instance.BindableIterableProperty) +{ + return 112; +} + +// 'IReadOnlyList' marshals back to native as a CCW exposing 'IVectorView' +instance2.Collection6Call((IReadOnlyList a, out IReadOnlyList b) => +{ + b = [.. a]; + return [.. a]; +}); + return 100; static bool SequencesEqual(IEnumerable x, params IEnumerable[] list) => list.All((y) => x.SequenceEqual(y)); static bool AllEqual(T[] x, params T[][] list) => list.All((y) => x.SequenceEqual(y)); +static int SumViaIterator(Class target, IEnumerable values) +{ + int sum = 0; + var iterator = target.GetIteratorForCollection(values); + while (iterator.MoveNext()) + { + sum += iterator.Current; + } + + return sum; +} + +static unsafe bool CcwExposesCollectionInterfaces(IReadOnlyList source) +{ + Guid iidIIterableInt = new("81A643FB-F51C-5565-83C4-F96425777B66"); + Guid iidIVectorViewInt = new("8D720CDF-3934-5D3F-9A55-40E8063B086A"); + Guid iidIVectorInt = new("B939AF5B-B45D-5489-9149-61442C1905FE"); + + void* ccw = WindowsRuntimeMarshal.ConvertToUnmanaged(source); + + try + { + return HasInterface(ccw, in iidIIterableInt) + && HasInterface(ccw, in iidIVectorViewInt) + && HasInterface(ccw, in iidIVectorInt); + } + finally + { + _ = Marshal.Release((nint)ccw); + } + + static unsafe bool HasInterface(void* ccw, in Guid iid) + { + if (Marshal.QueryInterface((nint)ccw, in iid, out nint interfaceCcw) != 0 || interfaceCcw == IntPtr.Zero) + { + return false; + } + + _ = Marshal.Release(interfaceCcw); + + return true; + } +} + static Func ActionToFunction(Action action) => (a1, a2) => {