diff --git a/src/Tests/FunctionalTests/Collections/Program.cs b/src/Tests/FunctionalTests/Collections/Program.cs index a53fdb5df..39e19b3ad 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) => { diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index 78a2db97d..aee421ddf 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 7b23bcd00..357c23b87 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() {