Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions src/Tests/FunctionalTests/Collections/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -630,12 +630,112 @@
return 101;
}

// Collection expressions targeting a read-only collection interface ('IEnumerable<T>',
// 'IReadOnlyList<T>', 'IReadOnlyCollection<T>') lower to compiler synthesized backing types
// ('<>z__ReadOnlyArray<T>' for multiple elements and '<>z__ReadOnlySingleElementList<T>' for a
// single element). Marshalling them across the WinRT ABI builds a CCW for the synthesized type.
IEnumerable<int> collectionExpressionEnumerable = [10, 20, 30];
if (SumViaIterator(instance, collectionExpressionEnumerable) != 60)
{
return 106;
}

IReadOnlyList<int> collectionExpressionReadOnlyList = [10, 20, 30];
if (SumViaIterator(instance, collectionExpressionReadOnlyList) != 60)
{
return 107;
}

IReadOnlyCollection<int> collectionExpressionReadOnlyCollection = [10, 20, 30];
if (SumViaIterator(instance, collectionExpressionReadOnlyCollection) != 60)
{
return 108;
}

// Single element uses the '<>z__ReadOnlySingleElementList<int>' backing type
IEnumerable<int> collectionExpressionSingleElement = [42];
if (SumViaIterator(instance, collectionExpressionSingleElement) != 42)
{
return 109;
}

// The synthesized backing type implements 'IEnumerable<int>', 'IReadOnlyList<int>', and 'IList<int>',
// so its CCW exposes 'IIterable<int>', 'IVectorView<int>', and 'IVector<int>'
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<int> collectionExpressionBindable = [0, 1, 2];
instance.BindableIterableProperty = collectionExpressionBindable;
if (collectionExpressionBindable != instance.BindableIterableProperty)
{
return 112;
}

// 'IReadOnlyList<string>' marshals back to native as a CCW exposing 'IVectorView<string>'
instance2.Collection6Call((IReadOnlyList<string> a, out IReadOnlyList<string> b) =>
{
b = [.. a];
return [.. a];
});

return 100;

static bool SequencesEqual<T>(IEnumerable<T> x, params IEnumerable<T>[] list) => list.All((y) => x.SequenceEqual(y));

static bool AllEqual<T>(T[] x, params T[][] list) => list.All((y) => x.SequenceEqual(y));

static int SumViaIterator(Class target, IEnumerable<int> values)
{
int sum = 0;
var iterator = target.GetIteratorForCollection(values);
while (iterator.MoveNext())
{
sum += iterator.Current;
}

return sum;
}

static unsafe bool CcwExposesCollectionInterfaces(IReadOnlyList<int> 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<TA1, TA2, TA1> ActionToFunction<TA1, TA2>(Action<TA1, TA2> action) =>
(a1, a2) =>
{
Expand Down
71 changes: 71 additions & 0 deletions src/Tests/UnitTest/TestComponentCSharp_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>',
// 'IReadOnlyList<T>', 'IReadOnlyCollection<T>') lower to compiler synthesized backing types
// ('<>z__ReadOnlyArray<T>' for multiple elements and '<>z__ReadOnlySingleElementList<T>' for
// a single element). Marshalling them across the WinRT ABI builds a CCW for the synthesized type.
IEnumerable<int> enumerable = [10, 20, 30];
Assert.AreEqual(60, SumViaIterator(enumerable));

IReadOnlyList<int> readOnlyList = [10, 20, 30];
Assert.AreEqual(60, SumViaIterator(readOnlyList));

IReadOnlyCollection<int> readOnlyCollection = [10, 20, 30];
Assert.AreEqual(60, SumViaIterator(readOnlyCollection));

// Single element uses the '<>z__ReadOnlySingleElementList<int>' backing type
IEnumerable<int> singleElement = [42];
Assert.AreEqual(42, SumViaIterator(singleElement));

// The synthesized backing type implements 'IEnumerable<int>', 'IReadOnlyList<int>', and
// 'IList<int>', so its CCW exposes 'IIterable<int>', 'IVectorView<int>', and 'IVector<int>'
AssertCcwExposesCollectionInterfaces([10, 20, 30]);
AssertCcwExposesCollectionInterfaces([42]);

// Values must be sequential from 0 because the native bindable setter validates them
IReadOnlyList<int> bindable = [0, 1, 2];
TestObject.BindableIterableProperty = bindable;
Assert.AreEqual(bindable, TestObject.BindableIterableProperty);

int SumViaIterator(IEnumerable<int> values)
{
int sum = 0;
var iterator = TestObject.GetIteratorForCollection(values);
while (iterator.MoveNext())
{
sum += iterator.Current;
}

return sum;
}

static void AssertCcwExposesCollectionInterfaces(IReadOnlyList<int> 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()
{
Expand Down
12 changes: 12 additions & 0 deletions src/Tests/UnitTest/TestComponent_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -883,6 +883,18 @@ public void Collections_ReadOnly_List_Call()
});
}

[TestMethod]
public void Collections_ReadOnly_List_Call_CollectionExpression()
{
// Collection expressions typed as 'IReadOnlyList<string>' use a compiler synthesized
// backing type that marshals to native as a CCW exposing 'IVectorView<string>'.
Tests.Collection6Call((IReadOnlyList<string> a, out IReadOnlyList<string> b) =>
{
b = [.. a];
return [.. a];
});
}

[TestMethod]
public void TestComposable()
{
Expand Down