Browse Source
* Add failing tests for SAFEARRAY marshalling of COM providers SafeArrayRef.CreateFromObjects sizes the SAFEARRAY to the pooled buffer it rents rather than to the input, and only fills a slot when the object already has a live COM wrapper. Cover both with tests driven through SafeArrayMarshaller, which is how the UIA interop actually reaches this code. Two of the three tests fail: a two-element input produces a 16-element array of nulls. The string test passes and is the control. Each provider test checks the length before it reads the entries. A wrongly sized array holds slots that were never written, and dereferencing those takes down the test host with an access violation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RYYMWsTkmkKjptFL88xjAg * Fix ISelectionProvider.GetSelection() never reaching the UIA client SafeArrayRef.CreateFromObjects had two defects, and between them every VT_UNKNOWN SAFEARRAY the automation interop produced was empty. It sized the array to the buffer it rented from ArrayPool rather than to the input, so a one-element selection became a 16-element array. The rented buffer isn't cleared, so stale pointers from an earlier rent could leak in and later be released by SafeArrayDestroy. It also filled a slot only when ComWrappers.TryGetComInstance succeeded. That call resolves a managed object that wraps a native COM instance; for a purely managed object it always fails, so no slot was ever filled and UIA received an array of nulls. Wrappers now come from ComInterfaceMarshaller<T>, which is what the rest of the interop uses. Sharing the instance matters: UI Automation identifies elements by IUnknown pointer, so a wrapper from a second ComWrappers would read as a different element. The element type isn't recoverable from the values, so SafeArrayMarshaller<T> passes it through a new generic TryCreate<T> overload. The object branch of the non-generic TryCreate is gone with it. Nothing reached it: the only other caller is ComVariant.Create, and no property value is an array of providers. This fixes ISelectionProvider.GetSelection() and, by the same route, ITableProvider.GetRowHeaders/GetColumnHeaders, ITableItemProvider, and ITextProvider.GetSelection/GetVisibleRanges. Fixes #22150 Claude-Session: https://claude.ai/code/session_01RYYMWsTkmkKjptFL88xjAg Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>pull/22169/head
committed by
GitHub
4 changed files with 156 additions and 57 deletions
@ -0,0 +1,83 @@ |
|||
using System; |
|||
using System.Runtime.CompilerServices; |
|||
using System.Runtime.InteropServices; |
|||
using System.Runtime.InteropServices.Marshalling; |
|||
using Avalonia.Win32.Automation.Marshalling; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.IntegrationTests.Win32.Automation; |
|||
|
|||
public unsafe class SafeArrayRefTests |
|||
{ |
|||
[Fact] |
|||
public void ConvertToUnmanaged_Sizes_String_Array_To_Input() |
|||
{ |
|||
var safeArray = SafeArrayMarshaller<string>.ConvertToUnmanaged(["foo", "bar", "baz"]); |
|||
|
|||
Assert.Equal(3, GetLength(safeArray)); |
|||
|
|||
SafeArrayMarshaller<string>.Free(safeArray); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ConvertToUnmanaged_Sizes_Provider_Array_To_Input() |
|||
{ |
|||
ITestProvider[] providers = [new TestProvider(1), new TestProvider(2)]; |
|||
|
|||
var safeArray = SafeArrayMarshaller<ITestProvider>.ConvertToUnmanaged(providers); |
|||
|
|||
Assert.Equal(providers.Length, GetLength(safeArray)); |
|||
|
|||
SafeArrayMarshaller<ITestProvider>.Free(safeArray); |
|||
} |
|||
|
|||
[Fact] |
|||
public void ConvertToUnmanaged_Fills_Provider_Array_With_Com_Wrappers() |
|||
{ |
|||
ITestProvider[] providers = [new TestProvider(1), new TestProvider(2)]; |
|||
|
|||
var safeArray = SafeArrayMarshaller<ITestProvider>.ConvertToUnmanaged(providers); |
|||
|
|||
// Check the length before reading the entries. A wrongly sized array holds slots that were
|
|||
// never written, and dereferencing those below would take down the test host.
|
|||
Assert.Equal(providers.Length, GetLength(safeArray)); |
|||
Assert.All(GetEntries(safeArray), x => Assert.NotEqual(IntPtr.Zero, x)); |
|||
|
|||
var roundTripped = SafeArrayMarshaller<ITestProvider>.ConvertToManaged(safeArray); |
|||
Assert.NotNull(roundTripped); |
|||
Assert.Equal([1, 2], Array.ConvertAll(roundTripped, x => x.GetValue())); |
|||
|
|||
SafeArrayMarshaller<ITestProvider>.Free(safeArray); |
|||
} |
|||
|
|||
private static int GetLength(SafeArrayRef safeArray) |
|||
{ |
|||
var ptr = (SafeArrayRef.SAFEARRAY*)Unsafe.As<SafeArrayRef, IntPtr>(ref safeArray); |
|||
return (int)ptr->rgsabound[0].cElements; |
|||
} |
|||
|
|||
private static IntPtr[] GetEntries(SafeArrayRef safeArray) |
|||
{ |
|||
var ptr = (SafeArrayRef.SAFEARRAY*)Unsafe.As<SafeArrayRef, IntPtr>(ref safeArray); |
|||
var result = new IntPtr[(int)ptr->rgsabound[0].cElements]; |
|||
new Span<IntPtr>(ptr->pvData, result.Length).CopyTo(result); |
|||
return result; |
|||
} |
|||
} |
|||
|
|||
[GeneratedComInterface] |
|||
[Guid("6ADEBBF3-6C63-4C1B-9C4F-9EE7C3B8A6D1")] |
|||
internal partial interface ITestProvider |
|||
{ |
|||
int GetValue(); |
|||
} |
|||
|
|||
[GeneratedComClass] |
|||
internal partial class TestProvider : ITestProvider |
|||
{ |
|||
private readonly int _value; |
|||
|
|||
public TestProvider(int value) => _value = value; |
|||
|
|||
public int GetValue() => _value; |
|||
} |
|||
Loading…
Reference in new issue