From 748b1ef36591748a9abcd72aa930b932d85fc03f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Mar 2026 08:37:17 -0800 Subject: [PATCH 01/18] Add pinned IBuffer and span read/write Introduce WindowsRuntimePinnedMemoryBuffer (IBuffer) to wrap a pinned pointer without owning the memory and support invalidation to prevent use-after-free. Add Span/ReadOnlySpan-based Read and Write overrides in WindowsRuntimeManagedStreamAdapter that pin spans, wrap them with the pinned buffer, and drive WinRT async operations. Add helper ThrowIfBufferIsInvalidated and an exception message constant for invalidated-buffer errors. Methods are attributed with SupportedOSPlatform and include comments explaining intent and safety considerations. --- .../WindowsRuntimePinnedMemoryBuffer.cs | 106 ++++++++++++++++++ ...anagedStreamAdapter.Implementation.Read.cs | 49 ++++++++ ...nagedStreamAdapter.Implementation.Write.cs | 43 +++++++ .../WindowsRuntimeExceptionExtensions.cs | 20 ++++ .../WindowsRuntimeExceptionMessages.cs | 2 + 5 files changed, 220 insertions(+) create mode 100644 src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs diff --git a/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs b/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs new file mode 100644 index 000000000..150118b13 --- /dev/null +++ b/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Threading; +using Windows.Storage.Streams; + +namespace WindowsRuntime.InteropServices; + +/// +/// Provides a managed implementation of the interface backed by a pinned +/// pointer to memory. This buffer does not own the underlying memory and can be invalidated to +/// prevent further access once the memory it points to is no longer guaranteed to be pinned. +/// +[WindowsRuntimeManagedOnlyType] +internal sealed unsafe class WindowsRuntimePinnedMemoryBuffer : IBuffer +{ + /// + /// The pointer to the pinned memory (stored as to support operations). + /// + private nint _data; + + /// + /// The number of bytes that can be read or written in the buffer. + /// + private int _length; + + /// + /// The capacity of the buffer. + /// + private readonly int _capacity; + + /// + /// Creates a instance with the specified parameters. + /// + /// The pointer to the pinned memory. + /// The number of bytes. + /// The maximum number of bytes the buffer can hold. + /// This constructor doesn't validate any of its parameters. + public WindowsRuntimePinnedMemoryBuffer(byte* data, int length, int capacity) + { + Debug.Assert(data is not null); + Debug.Assert(length >= 0); + Debug.Assert(capacity >= 0); + Debug.Assert(capacity >= length); + + _data = (nint)data; + _length = length; + _capacity = capacity; + } + + /// + public uint Capacity + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (uint)_capacity; + } + + /// + public uint Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (uint)_length; + set + { + ArgumentOutOfRangeException.ThrowIfBufferLengthExceedsCapacity(value, Capacity); + + _length = unchecked((int)value); + } + } + + /// + /// Thrown if the buffer has been invalidated. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public byte* Buffer() + { + byte* data = (byte*)Volatile.Read(ref _data); + + InvalidOperationException.ThrowIfBufferIsInvalidated(data); + + return data; + } + + /// + /// Invalidates the buffer, preventing any further access to the underlying memory. + /// + /// + /// + /// After calling this method, any attempt to call will throw + /// an . This is used to prevent use-after-free + /// scenarios when the buffer wraps memory that is only temporarily pinned (e.g. a span). + /// + /// + /// This type intentionally does not implement to perform this + /// invalidation. This is because would end up in the CCW interface + /// list for this implementation, which is not desirable since this type + /// is only meant to be used from the managed side in a specific, controlled context. + /// + /// + public void Invalidate() + { + Volatile.Write(ref _data, 0); + } +} diff --git a/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Read.cs b/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Read.cs index 92e38ed29..7f8db1dff 100644 --- a/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Read.cs +++ b/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Read.cs @@ -194,6 +194,55 @@ StreamReadAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallb } /// + [SupportedOSPlatform("windows10.0.10240.0")] + public override unsafe int Read(Span buffer) + { + ObjectDisposedException.ThrowIfStreamIsDisposed(_windowsRuntimeStream); + NotSupportedException.ThrowIfStreamCannotRead(_canRead); + + if (buffer.IsEmpty) + { + return 0; + } + + IInputStream windowsRuntimeStream = (IInputStream)EnsureNotDisposed(); + + // Pin the span so that it stays at the same address while the async I/O operation is in progress. + // We create a 'WindowsRuntimePinnedMemoryBuffer' wrapping the pinned pointer, then invalidate it + // in the 'finally' block to ensure no one can access the pointer after the span goes out of scope. + fixed (byte* pinnedData = buffer) + { + WindowsRuntimePinnedMemoryBuffer pinnedMemoryBuffer = new(pinnedData, length: 0, capacity: buffer.Length); + + try + { + IAsyncOperationWithProgress asyncReadOperation = windowsRuntimeStream.ReadAsync( + buffer: pinnedMemoryBuffer, + count: unchecked((uint)buffer.Length), + options: InputStreamOptions.Partial); + + // See the large comment in the 'Read(byte[], int, int)' method about why we use + // a custom 'IAsyncResult' implementation instead of 'ReadAsync' + 'AsTask' here. + StreamReadAsyncResult asyncResult = new( + asyncReadOperation, + pinnedMemoryBuffer, + userCompletionCallback: null, + userAsyncStateInfo: null, + processCompletedOperationInCallback: false); + + int numberOfBytesRead = EndRead(asyncResult); + + return numberOfBytesRead; + } + finally + { + pinnedMemoryBuffer.Invalidate(); + } + } + } + + /// + [SupportedOSPlatform("windows10.0.10240.0")] public override int ReadByte() { byte result = 0; diff --git a/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs b/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs index c52e7ee3d..1281ca632 100644 --- a/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs +++ b/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs @@ -122,12 +122,55 @@ public override void Write(byte[] buffer, int offset, int count) } /// + [SupportedOSPlatform("windows10.0.10240.0")] public override void WriteByte(byte value) { // We don't need to call 'EnsureNotDisposed', see notes in 'ReadByte' Write(new ReadOnlySpan(in value)); } + /// + [SupportedOSPlatform("windows10.0.10240.0")] + public override unsafe void Write(ReadOnlySpan buffer) + { + ObjectDisposedException.ThrowIfStreamIsDisposed(_windowsRuntimeStream); + NotSupportedException.ThrowIfStreamCannotWrite(_canWrite); + + if (buffer.IsEmpty) + { + return; + } + + IOutputStream windowsRuntimeStream = (IOutputStream)EnsureNotDisposed(); + + // Pin the span so that it stays at the same address while the async I/O operation is in progress. + // We create a 'WindowsRuntimePinnedMemoryBuffer' wrapping the pinned pointer, then invalidate it + // in the 'finally' block to ensure no one can access the pointer after the span goes out of scope. + fixed (byte* pinnedData = buffer) + { + WindowsRuntimePinnedMemoryBuffer pinnedMemoryBuffer = new(pinnedData, length: buffer.Length, capacity: buffer.Length); + + try + { + // See the large comment in 'Read(byte[], int, int)' about why we use a custom 'IAsyncResult' + // implementation instead of 'WriteAsync' + 'AsTask' here (same deadlock concerns apply). + IAsyncOperationWithProgress asyncWriteOperation = windowsRuntimeStream.WriteAsync(pinnedMemoryBuffer); + + StreamWriteAsyncResult asyncResult = new( + asyncWriteOperation, + userCompletionCallback: null, + userAsyncStateInfo: null, + processCompletedOperationInCallback: false); + + EndWrite(asyncResult); + } + finally + { + pinnedMemoryBuffer.Invalidate(); + } + } + } + /// [SupportedOSPlatform("windows10.0.10240.0")] public override void Flush() diff --git a/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionExtensions.cs b/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionExtensions.cs index aabe7a73b..7d84b588d 100644 --- a/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionExtensions.cs +++ b/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionExtensions.cs @@ -226,6 +226,26 @@ static void ThrowInvalidOperationException() } } + /// + /// Throws an if the buffer has been invalidated. + /// + /// The pointer to the buffer data. + /// Thrown if is . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + [StackTraceHidden] + public static unsafe void ThrowIfBufferIsInvalidated(byte* data) + { + [DoesNotReturn] + [StackTraceHidden] + static void ThrowInvalidOperationException() + => throw new InvalidOperationException(WindowsRuntimeExceptionMessages.InvalidOperation_CannotAccessInvalidatedBuffer); + + if (data is null) + { + ThrowInvalidOperationException(); + } + } + /// /// Creates an indicating that the method cannot be called in the current state. /// diff --git a/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionMessages.cs b/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionMessages.cs index 14e1125b4..653c36dee 100644 --- a/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionMessages.cs +++ b/src/WinRT.Runtime2/Properties/WindowsRuntimeExceptionMessages.cs @@ -88,6 +88,8 @@ internal static class WindowsRuntimeExceptionMessages public const string ArgumentOutOfRange_IO_CannotSeekToNegativePosition = "Cannot seek to an absolute stream position that is negative."; + public const string InvalidOperation_CannotAccessInvalidatedBuffer = "Cannot access the underlying data of this buffer because it has been invalidated."; + public const string InvalidOperation_CannotCallThisMethodInCurrentState = "The state of this object does not permit invoking this method."; public const string InvalidOperation_CannotChangeBufferSizeOfStreamAdapter = "Cannot convert the specified Windows Runtime stream to a managed System.IO.Stream object with the specified buffer size because this Windows Runtime stream has been previously converted to a managed Stream object with a different buffer size. Ensure that the 'bufferSize' argument matches the existing buffer or use the '{0}'-overload without the 'bufferSize' argument to convert the specified Windows Runtime stream to a Stream object with the same buffer size as previously."; From aadeb7ed17cabdad2d740f6d9175dc2be1881a8c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Mar 2026 09:40:48 -0800 Subject: [PATCH 02/18] Add Memory-based async ReadAsync/WriteAsync overloads Implement Memory/ReadOnlyMemory async overloads for ReadAsync and WriteAsync on WindowsRuntimeManagedStreamAdapter. Adds fast paths for array-backed Memory via MemoryMarshal.TryGetArray and slow paths that pin memory with MemoryHandle and use a WindowsRuntimePinnedMemoryBuffer for WinRT I/O. Includes disposal/cancellation checks, result copying for reads (EnsureResultsInUserBuffer), exception propagation, and cleanup (Invalidate). Also adds using directives for System.Buffers and System.Runtime.InteropServices and marks the APIs with SupportedOSPlatform attribute. --- ...anagedStreamAdapter.Implementation.Read.cs | 76 +++++++++++++++++++ ...nagedStreamAdapter.Implementation.Write.cs | 52 +++++++++++++ 2 files changed, 128 insertions(+) diff --git a/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Read.cs b/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Read.cs index 7f8db1dff..a295fd82d 100644 --- a/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Read.cs +++ b/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Read.cs @@ -2,7 +2,9 @@ // Licensed under the MIT License. using System; +using System.Buffers; using System.Diagnostics; +using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; @@ -124,6 +126,80 @@ async Task ReadCoreAsync(byte[] buffer, int offset, int count, Cancellation return ReadCoreAsync(buffer, offset, count, cancellationToken); } + /// + [SupportedOSPlatform("windows10.0.10240.0")] + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIfStreamIsDisposed(_windowsRuntimeStream); + NotSupportedException.ThrowIfStreamCannotRead(_canRead); + + // If already cancelled, stop early + cancellationToken.ThrowIfCancellationRequested(); + + if (buffer.IsEmpty) + { + return new(0); + } + + // Fast path: if the memory is backed by an array, use the existing array-based overload directly + if (MemoryMarshal.TryGetArray((ReadOnlyMemory)buffer, out ArraySegment segment)) + { + return new(ReadAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken)); + } + + // Helper to perform the actual asynchronous read operation with pinned memory + async ValueTask ReadPinnedMemoryAsync(Memory buffer, CancellationToken cancellationToken) + { + using MemoryHandle handle = buffer.Pin(); + + WindowsRuntimePinnedMemoryBuffer pinnedMemoryBuffer; + + // An explicit unsafe block is needed here because the 'async unsafe' modifier is not supported + // by the language (CS4004: "Cannot await in an unsafe context"), so we scope the pointer access + // to just the buffer initialization, which is the only expression that requires unsafe context. + unsafe + { + pinnedMemoryBuffer = new((byte*)handle.Pointer, length: 0, capacity: buffer.Length); + } + + try + { + IInputStream windowsRuntimeStream = (IInputStream)EnsureNotDisposed(); + + IAsyncOperationWithProgress asyncReadOperation = windowsRuntimeStream.ReadAsync( + buffer: pinnedMemoryBuffer, + count: pinnedMemoryBuffer.Capacity, + options: InputStreamOptions.Partial); + + IBuffer? resultBuffer = await asyncReadOperation.AsTask(cancellationToken).ConfigureAwait(false); + + if (resultBuffer is null) + { + return 0; + } + + WindowsRuntimeIOHelpers.EnsureResultsInUserBuffer(pinnedMemoryBuffer, resultBuffer); + + Debug.Assert(resultBuffer.Length <= unchecked(int.MaxValue)); + + return unchecked((int)resultBuffer.Length); + } + catch (Exception exception) + { + WindowsRuntimeIOHelpers.GetExceptionDispatchInfo(exception).Throw(); + + return 0; + } + finally + { + pinnedMemoryBuffer.Invalidate(); + } + } + + // Slow path: pin the memory and use a pinned memory buffer for the async read operation + return ReadPinnedMemoryAsync(buffer, cancellationToken); + } + /// [SupportedOSPlatform("windows10.0.10240.0")] public override int Read(byte[] buffer, int offset, int count) diff --git a/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs b/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs index 1281ca632..b6a58309d 100644 --- a/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs +++ b/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs @@ -2,6 +2,8 @@ // Licensed under the MIT License. using System; +using System.Buffers; +using System.Runtime.InteropServices; using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; @@ -110,6 +112,56 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati return windowsRuntimeStream.WriteAsync(asyncWriteBuffer).AsTask(cancellationToken); } + /// + [SupportedOSPlatform("windows10.0.10240.0")] + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIfStreamIsDisposed(_windowsRuntimeStream); + NotSupportedException.ThrowIfStreamCannotWrite(_canWrite); + + // If already cancelled, stop early + cancellationToken.ThrowIfCancellationRequested(); + + if (buffer.IsEmpty) + { + return default; + } + + // Fast path: if the memory is backed by an array, use the existing array-based overload directly + if (MemoryMarshal.TryGetArray(buffer, out ArraySegment segment)) + { + return new(WriteAsync(segment.Array!, segment.Offset, segment.Count, cancellationToken)); + } + + // Helper to perform the actual asynchronous write operation with pinned memory + async ValueTask WritePinnedMemoryAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + using MemoryHandle handle = buffer.Pin(); + + WindowsRuntimePinnedMemoryBuffer pinnedMemoryBuffer; + + // See notes in 'ReadPinnedMemoryAsync' for why we need an 'unsafe' block here + unsafe + { + pinnedMemoryBuffer = new((byte*)handle.Pointer, length: buffer.Length, capacity: buffer.Length); + } + + try + { + IOutputStream windowsRuntimeStream = (IOutputStream)EnsureNotDisposed(); + + _ = await windowsRuntimeStream.WriteAsync(pinnedMemoryBuffer).AsTask(cancellationToken).ConfigureAwait(false); + } + finally + { + pinnedMemoryBuffer.Invalidate(); + } + } + + // Slow path: pin the memory and use a pinned memory buffer for the async write operation + return WritePinnedMemoryAsync(buffer, cancellationToken); + } + /// [SupportedOSPlatform("windows10.0.10240.0")] public override void Write(byte[] buffer, int offset, int count) From 5d68dc6d6461ea9072d2f62a37c688ee062b5250 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Mar 2026 09:51:57 -0800 Subject: [PATCH 03/18] Add Span/Memory stream adapter tests Add coverage for Span/Memory-based Stream APIs on the WinRT InMemoryRandomAccessStream adapter. Changes include: adding System.Threading to Program.cs and a runtime functional test that exercises Write(ReadOnlySpan), Read(Span), WriteAsync(ReadOnlyMemory), ReadAsync(Memory), ReadByte/WriteByte, empty-span/memory ops, and cancellation behavior. Expand unit tests (TestComponentCSharp_Tests.cs) with many TestMethod cases for sync/async span and memory reads/writes, partial/empty reads, cancellation checks, byte/span interoperability, round-trip scenarios, and an UnmanagedMemoryManager (MemoryManager) to exercise the pinned/unmanaged memory code paths. Also add System.Buffers using and integrate the unmanaged memory tests to validate pinning and slow-path behavior. --- src/Tests/FunctionalTests/Async/Program.cs | 104 +++++ .../UnitTest/TestComponentCSharp_Tests.cs | 360 ++++++++++++++++++ 2 files changed, 464 insertions(+) diff --git a/src/Tests/FunctionalTests/Async/Program.cs b/src/Tests/FunctionalTests/Async/Program.cs index e85942f68..17d3a2703 100644 --- a/src/Tests/FunctionalTests/Async/Program.cs +++ b/src/Tests/FunctionalTests/Async/Program.cs @@ -1,6 +1,7 @@ using System; using System.IO; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using TestComponentCSharp; using Windows.Foundation; @@ -162,6 +163,109 @@ return 117; } +// Test stream adapter span/memory overrides using InMemoryRandomAccessStream +{ + var random = new Random(42); + byte[] data = new byte[256]; + random.NextBytes(data); + + using var adaptedStream = new InMemoryRandomAccessStream().AsStream(); + + // Test Write(ReadOnlySpan) and Read(Span) + adaptedStream.Write(new ReadOnlySpan(data)); + adaptedStream.Seek(0, SeekOrigin.Begin); + + Span spanRead = new byte[256]; + int spanBytesRead = adaptedStream.Read(spanRead); + + if (spanBytesRead != 256) + { + return 118; + } + + for (int i = 0; i < data.Length; i++) + { + if (data[i] != spanRead[i]) + { + return 119; + } + } + + // Test WriteAsync(ReadOnlyMemory) and ReadAsync(Memory) + adaptedStream.Seek(0, SeekOrigin.Begin); + await adaptedStream.WriteAsync(new ReadOnlyMemory(data)); + adaptedStream.Seek(0, SeekOrigin.Begin); + + Memory memoryRead = new byte[256]; + int memoryBytesRead = await adaptedStream.ReadAsync(memoryRead); + + if (memoryBytesRead != 256) + { + return 120; + } + + for (int i = 0; i < data.Length; i++) + { + if (data[i] != memoryRead.Span[i]) + { + return 121; + } + } + + // Test ReadByte/WriteByte (which delegate to span overrides) + adaptedStream.Seek(0, SeekOrigin.Begin); + adaptedStream.WriteByte(0xAB); + adaptedStream.WriteByte(0xCD); + adaptedStream.Seek(0, SeekOrigin.Begin); + + if (adaptedStream.ReadByte() != 0xAB) + { + return 122; + } + + if (adaptedStream.ReadByte() != 0xCD) + { + return 123; + } + + // Test empty span/memory operations + if (adaptedStream.Read(Span.Empty) != 0) + { + return 124; + } + + adaptedStream.Write(ReadOnlySpan.Empty); + + if (await adaptedStream.ReadAsync(Memory.Empty) != 0) + { + return 125; + } + + await adaptedStream.WriteAsync(ReadOnlyMemory.Empty); + + // Test cancellation for memory-based async operations + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + try + { + await adaptedStream.ReadAsync(new byte[256].AsMemory(), cts.Token); + return 126; + } + catch (OperationCanceledException) + { + } + + try + { + await adaptedStream.WriteAsync(new byte[256].AsMemory(), cts.Token); + return 127; + } + catch (OperationCanceledException) + { + } +} + return 100; static async Task InvokeAddAsync(Class instance, int lhs, int rhs) diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index 13470b8e0..be148931d 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -1,4 +1,5 @@ using System; +using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -842,6 +843,365 @@ public void TestBuffer() Assert.IsTrue(arr1[1] == arr2[1]); } + [TestMethod] + public void TestStreamReadSpan() + { + var random = new Random(42); + byte[] data = new byte[256]; + random.NextBytes(data); + + using var stream = new InMemoryRandomAccessStream().AsStream(); + stream.Write(data, 0, data.Length); + stream.Seek(0, SeekOrigin.Begin); + + Span read = new byte[256]; + int bytesRead = stream.Read(read); + + Assert.AreEqual(256, bytesRead); + CollectionAssert.AreEqual(data, read.ToArray()); + } + + [TestMethod] + public void TestStreamReadSpanPartial() + { + var random = new Random(42); + byte[] data = new byte[256]; + random.NextBytes(data); + + using var stream = new InMemoryRandomAccessStream().AsStream(); + stream.Write(data, 0, data.Length); + stream.Seek(0, SeekOrigin.Begin); + + Span read = new byte[64]; + int bytesRead = stream.Read(read); + + Assert.AreEqual(64, bytesRead); + CollectionAssert.AreEqual(data[..64], read.ToArray()); + } + + [TestMethod] + public void TestStreamReadSpanEmpty() + { + using var stream = new InMemoryRandomAccessStream().AsStream(); + int bytesRead = stream.Read(Span.Empty); + Assert.AreEqual(0, bytesRead); + } + + [TestMethod] + public void TestStreamWriteSpan() + { + var random = new Random(42); + byte[] data = new byte[256]; + random.NextBytes(data); + + using var stream = new InMemoryRandomAccessStream().AsStream(); + stream.Write(new ReadOnlySpan(data)); + stream.Seek(0, SeekOrigin.Begin); + + byte[] read = new byte[256]; + stream.Read(read, 0, read.Length); + CollectionAssert.AreEqual(data, read); + } + + [TestMethod] + public void TestStreamWriteSpanEmpty() + { + using var stream = new InMemoryRandomAccessStream().AsStream(); + stream.Write(ReadOnlySpan.Empty); + Assert.AreEqual(0L, stream.Length); + } + + [TestMethod] + public void TestStreamReadAsyncMemory() + { + async Task TestAsync() + { + var random = new Random(42); + byte[] data = new byte[256]; + random.NextBytes(data); + + using var stream = new InMemoryRandomAccessStream().AsStream(); + await stream.WriteAsync(data, 0, data.Length); + stream.Seek(0, SeekOrigin.Begin); + + Memory read = new byte[256]; + int bytesRead = await stream.ReadAsync(read); + + Assert.AreEqual(256, bytesRead); + CollectionAssert.AreEqual(data, read.ToArray()); + } + + Assert.IsTrue(TestAsync().Wait(5000)); + } + + [TestMethod] + public void TestStreamReadAsyncMemoryPartial() + { + async Task TestAsync() + { + var random = new Random(42); + byte[] data = new byte[256]; + random.NextBytes(data); + + using var stream = new InMemoryRandomAccessStream().AsStream(); + await stream.WriteAsync(data, 0, data.Length); + stream.Seek(0, SeekOrigin.Begin); + + Memory read = new byte[64]; + int bytesRead = await stream.ReadAsync(read); + + Assert.AreEqual(64, bytesRead); + CollectionAssert.AreEqual(data[..64], read.ToArray()); + } + + Assert.IsTrue(TestAsync().Wait(5000)); + } + + [TestMethod] + public void TestStreamReadAsyncMemoryEmpty() + { + async Task TestAsync() + { + using var stream = new InMemoryRandomAccessStream().AsStream(); + int bytesRead = await stream.ReadAsync(Memory.Empty); + Assert.AreEqual(0, bytesRead); + } + + Assert.IsTrue(TestAsync().Wait(5000)); + } + + [TestMethod] + public void TestStreamWriteAsyncMemory() + { + async Task TestAsync() + { + var random = new Random(42); + byte[] data = new byte[256]; + random.NextBytes(data); + + using var stream = new InMemoryRandomAccessStream().AsStream(); + await stream.WriteAsync(new ReadOnlyMemory(data)); + stream.Seek(0, SeekOrigin.Begin); + + byte[] read = new byte[256]; + await stream.ReadAsync(read, 0, read.Length); + CollectionAssert.AreEqual(data, read); + } + + Assert.IsTrue(TestAsync().Wait(5000)); + } + + [TestMethod] + public void TestStreamWriteAsyncMemoryEmpty() + { + async Task TestAsync() + { + using var stream = new InMemoryRandomAccessStream().AsStream(); + await stream.WriteAsync(ReadOnlyMemory.Empty); + Assert.AreEqual(0L, stream.Length); + } + + Assert.IsTrue(TestAsync().Wait(5000)); + } + + [TestMethod] + public void TestStreamReadAsyncMemoryWithCancellation() + { + using var stream = new InMemoryRandomAccessStream().AsStream(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + Memory buffer = new byte[256]; + Assert.ThrowsExactly(() => stream.ReadAsync(buffer, cts.Token).AsTask().GetAwaiter().GetResult()); + } + + [TestMethod] + public void TestStreamWriteAsyncMemoryWithCancellation() + { + using var stream = new InMemoryRandomAccessStream().AsStream(); + using var cts = new CancellationTokenSource(); + cts.Cancel(); + + ReadOnlyMemory buffer = new byte[256]; + Assert.ThrowsExactly(() => stream.WriteAsync(buffer, cts.Token).AsTask().GetAwaiter().GetResult()); + } + + [TestMethod] + public void TestStreamReadByteAfterSpanWrite() + { + using var stream = new InMemoryRandomAccessStream().AsStream(); + stream.Write(new ReadOnlySpan([0xAB, 0xCD])); + stream.Seek(0, SeekOrigin.Begin); + + Assert.AreEqual(0xAB, stream.ReadByte()); + Assert.AreEqual(0xCD, stream.ReadByte()); + Assert.AreEqual(-1, stream.ReadByte()); + } + + [TestMethod] + public void TestStreamWriteByteAndReadSpan() + { + using var stream = new InMemoryRandomAccessStream().AsStream(); + stream.WriteByte(0xAB); + stream.WriteByte(0xCD); + stream.Seek(0, SeekOrigin.Begin); + + Span read = new byte[2]; + int bytesRead = stream.Read(read); + + Assert.AreEqual(2, bytesRead); + Assert.AreEqual((byte)0xAB, read[0]); + Assert.AreEqual((byte)0xCD, read[1]); + } + + [TestMethod] + public void TestStreamSpanAndMemoryRoundTrip() + { + async Task TestAsync() + { + var random = new Random(42); + byte[] data = new byte[1024]; + random.NextBytes(data); + + using var stream = new InMemoryRandomAccessStream().AsStream(); + + // Write via span (sync) + stream.Write(new ReadOnlySpan(data, 0, 512)); + + // Write via memory (async) + await stream.WriteAsync(new ReadOnlyMemory(data, 512, 512)); + + stream.Seek(0, SeekOrigin.Begin); + + // Read via memory (async) + Memory readFirst = new byte[512]; + int bytesRead1 = await stream.ReadAsync(readFirst); + Assert.AreEqual(512, bytesRead1); + + // Read via span (sync) + Span readSecond = new byte[512]; + int bytesRead2 = stream.Read(readSecond); + Assert.AreEqual(512, bytesRead2); + + // Verify round-trip + CollectionAssert.AreEqual(data[..512], readFirst.ToArray()); + CollectionAssert.AreEqual(data[512..], readSecond.ToArray()); + } + + Assert.IsTrue(TestAsync().Wait(5000)); + } + + /// + /// A backed by unmanaged memory, used to test the pinned memory + /// code path in and + /// (i.e. the slow + /// path when + /// returns ). + /// + unsafe class UnmanagedMemoryManager : System.Buffers.MemoryManager + { + private byte* _pointer; + private readonly int _length; + + public UnmanagedMemoryManager(int length) + { + _pointer = (byte*)NativeMemory.AllocZeroed((nuint)length); + _length = length; + } + + public override Span GetSpan() => new(_pointer, _length); + + public override MemoryHandle Pin(int elementIndex = 0) => new(_pointer + elementIndex); + + public override void Unpin() { } + + protected override void Dispose(bool disposing) + { + if (_pointer is not null) + { + NativeMemory.Free(_pointer); + _pointer = null; + } + } + } + + [TestMethod] + public void TestStreamReadAsyncUnmanagedMemory() + { + async Task TestAsync() + { + var random = new Random(42); + byte[] data = new byte[256]; + random.NextBytes(data); + + using var stream = new InMemoryRandomAccessStream().AsStream(); + await stream.WriteAsync(data, 0, data.Length); + stream.Seek(0, SeekOrigin.Begin); + + using var manager = new UnmanagedMemoryManager(256); + Memory read = manager.Memory; + int bytesRead = await stream.ReadAsync(read); + + Assert.AreEqual(256, bytesRead); + CollectionAssert.AreEqual(data, read.ToArray()); + } + + Assert.IsTrue(TestAsync().Wait(5000)); + } + + [TestMethod] + public void TestStreamWriteAsyncUnmanagedMemory() + { + async Task TestAsync() + { + var random = new Random(42); + byte[] data = new byte[256]; + random.NextBytes(data); + + using var manager = new UnmanagedMemoryManager(256); + data.AsSpan().CopyTo(manager.Memory.Span); + + using var stream = new InMemoryRandomAccessStream().AsStream(); + await stream.WriteAsync((ReadOnlyMemory)manager.Memory); + stream.Seek(0, SeekOrigin.Begin); + + byte[] read = new byte[256]; + await stream.ReadAsync(read, 0, read.Length); + CollectionAssert.AreEqual(data, read); + } + + Assert.IsTrue(TestAsync().Wait(5000)); + } + + [TestMethod] + public void TestStreamUnmanagedMemoryRoundTrip() + { + async Task TestAsync() + { + var random = new Random(42); + byte[] data = new byte[512]; + random.NextBytes(data); + + using var stream = new InMemoryRandomAccessStream().AsStream(); + + // Write via unmanaged memory (exercises pinned memory buffer path) + using var writeManager = new UnmanagedMemoryManager(512); + data.AsSpan().CopyTo(writeManager.Memory.Span); + await stream.WriteAsync((ReadOnlyMemory)writeManager.Memory); + + stream.Seek(0, SeekOrigin.Begin); + + // Read via unmanaged memory (exercises pinned memory buffer path) + using var readManager = new UnmanagedMemoryManager(512); + int bytesRead = await stream.ReadAsync(readManager.Memory); + + Assert.AreEqual(512, bytesRead); + CollectionAssert.AreEqual(data, readManager.Memory.ToArray()); + } + + Assert.IsTrue(TestAsync().Wait(5000)); + } + #endif async Task TestStorageFileAsync() From 3f4aabdab7cf620b208e989bc36ce74b19983e88 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Mar 2026 11:45:33 -0800 Subject: [PATCH 04/18] Add early empty-input checks to array-based stream overrides Add consistent count == 0 early return checks to the four array-based Read/Write stream overrides in WindowsRuntimeManagedStreamAdapter, matching the existing pattern in the span/memory-based overrides. Also move argument validation into Write(byte[], int, int) to match Read(byte[], int, int). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...eManagedStreamAdapter.Implementation.Read.cs | 10 ++++++++++ ...ManagedStreamAdapter.Implementation.Write.cs | 17 ++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Read.cs b/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Read.cs index a295fd82d..f45d39ce7 100644 --- a/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Read.cs +++ b/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Read.cs @@ -80,6 +80,11 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, Cancel // If already cancelled, stop early cancellationToken.ThrowIfCancellationRequested(); + if (count == 0) + { + return Task.FromResult(0); + } + // Helper to perform the actual asynchronous read operation async Task ReadCoreAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { @@ -211,6 +216,11 @@ public override int Read(byte[] buffer, int offset, int count) ObjectDisposedException.ThrowIfStreamIsDisposed(_windowsRuntimeStream); NotSupportedException.ThrowIfStreamCannotRead(_canRead); + if (count == 0) + { + return 0; + } + // Helper to do a sync-over-async read operation StreamReadAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state, bool usedByBlockingWrapper) { diff --git a/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs b/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs index b6a58309d..edf8d283e 100644 --- a/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs +++ b/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs @@ -103,6 +103,11 @@ public override Task WriteAsync(byte[] buffer, int offset, int count, Cancellati // If already cancelled, stop early cancellationToken.ThrowIfCancellationRequested(); + if (count == 0) + { + return Task.CompletedTask; + } + IOutputStream windowsRuntimeStream = (IOutputStream)EnsureNotDisposed(); IBuffer asyncWriteBuffer = buffer.AsBuffer(offset, count); @@ -166,7 +171,17 @@ async ValueTask WritePinnedMemoryAsync(ReadOnlyMemory buffer, Cancellation [SupportedOSPlatform("windows10.0.10240.0")] public override void Write(byte[] buffer, int offset, int count) { - // Arguments validation and disposal validation are done in 'BeginWrite' + ArgumentNullException.ThrowIfNull(buffer); + ArgumentOutOfRangeException.ThrowIfNegative(offset); + ArgumentOutOfRangeException.ThrowIfNegative(count); + ArgumentException.ThrowIfInsufficientArrayElementsAfterOffset(buffer.Length, offset, count); + ObjectDisposedException.ThrowIfStreamIsDisposed(_windowsRuntimeStream); + NotSupportedException.ThrowIfStreamCannotWrite(_canWrite); + + if (count == 0) + { + return; + } StreamWriteAsyncResult asyncResult = BeginWrite(buffer, offset, count, null, null, usedByBlockingWrapper: true); From 28e66e363c574478e70802eaa22ab6a10909f1fc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Mar 2026 12:01:23 -0800 Subject: [PATCH 05/18] Refactor WindowsRuntimeManagedStreamAdapter.BeginWrite Add pragma to disable CS1573 and refactor BeginWrite overloads: the public BeginWrite now forwards to a private BeginWrite implementation (with usedByBlockingWrapper=false). The concrete implementation was moved to the bottom of the file, consolidated duplicated validation/commented logic, and adds XML doc for the usedByBlockingWrapper parameter. Behavior is preserved; the change reduces duplication and centralizes the async write logic. --- ...nagedStreamAdapter.Implementation.Write.cs | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs b/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs index edf8d283e..aa5101c0c 100644 --- a/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs +++ b/src/WinRT.Runtime2/InteropServices/Streams/Adapters/WindowsRuntimeManagedStreamAdapter.Implementation.Write.cs @@ -12,6 +12,8 @@ using Windows.Storage.Buffers; using Windows.Storage.Streams; +#pragma warning disable CS1573 + namespace WindowsRuntime.InteropServices; /// @@ -20,13 +22,6 @@ internal partial class WindowsRuntimeManagedStreamAdapter /// [SupportedOSPlatform("windows10.0.10240.0")] public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) - { - return BeginWrite(buffer, offset, count, callback, state, usedByBlockingWrapper: false); - } - - /// - [SupportedOSPlatform("windows10.0.10240.0")] - private StreamWriteAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state, bool usedByBlockingWrapper) { ArgumentNullException.ThrowIfNull(buffer); ArgumentOutOfRangeException.ThrowIfNegative(offset); @@ -35,20 +30,7 @@ private StreamWriteAsyncResult BeginWrite(byte[] buffer, int offset, int count, ObjectDisposedException.ThrowIfStreamIsDisposed(_windowsRuntimeStream); NotSupportedException.ThrowIfStreamCannotWrite(_canWrite); - IOutputStream windowsRuntimeStream = (IOutputStream)EnsureNotDisposed(); - - IBuffer asyncWriteBuffer = buffer.AsBuffer(offset, count); - - // See the large comment in the 'BeginRead' method about why we are not using the - // 'WriteAsync' method, and instead using a custom implementation of 'IAsyncResult'. - IAsyncOperationWithProgress asyncWriteOperation = windowsRuntimeStream.WriteAsync(asyncWriteBuffer); - - // See additional notes in the 'Read' method about how CCW objects for this result are managed - return new StreamWriteAsyncResult( - asyncWriteOperation, - callback, - state, - processCompletedOperationInCallback: !usedByBlockingWrapper); + return BeginWrite(buffer, offset, count, callback, state, usedByBlockingWrapper: false); } /// @@ -298,4 +280,28 @@ public override Task FlushAsync(CancellationToken cancellationToken) return windowsRuntimeStream.FlushAsync().AsTask(cancellationToken); } + + /// + /// Indicates whether this method is being called by a method doing sync-over-async on the result. + [SupportedOSPlatform("windows10.0.10240.0")] + private StreamWriteAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state, bool usedByBlockingWrapper) + { + // This method doesn't do validation, to avoid repeating it in the 'Write' method that calls this one. + // It is only called by that method and by 'BeginWrite', so the validation there should be kept in sync. + + IOutputStream windowsRuntimeStream = (IOutputStream)EnsureNotDisposed(); + + IBuffer asyncWriteBuffer = buffer.AsBuffer(offset, count); + + // See the large comment in the 'BeginRead' method about why we are not using the + // 'WriteAsync' method, and instead using a custom implementation of 'IAsyncResult'. + IAsyncOperationWithProgress asyncWriteOperation = windowsRuntimeStream.WriteAsync(asyncWriteBuffer); + + // See additional notes in the 'Read' method about how CCW objects for this result are managed + return new StreamWriteAsyncResult( + asyncWriteOperation, + callback, + state, + processCompletedOperationInCallback: !usedByBlockingWrapper); + } } \ No newline at end of file From b2753b5dab9db8c92127b131baa92e42ec1acff0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Mar 2026 15:08:09 -0800 Subject: [PATCH 06/18] Use SequenceEqual for buffer comparisons Replace manual index-based loops with SequenceEqual checks when comparing data to spanRead and memoryRead.Span in src/Tests/FunctionalTests/Async/Program.cs. This simplifies the test code, improves readability, and preserves the existing return codes (119 and 121) on mismatch. --- src/Tests/FunctionalTests/Async/Program.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/Tests/FunctionalTests/Async/Program.cs b/src/Tests/FunctionalTests/Async/Program.cs index 17d3a2703..056c8d846 100644 --- a/src/Tests/FunctionalTests/Async/Program.cs +++ b/src/Tests/FunctionalTests/Async/Program.cs @@ -183,12 +183,9 @@ return 118; } - for (int i = 0; i < data.Length; i++) + if (!data.SequenceEqual(spanRead)) { - if (data[i] != spanRead[i]) - { - return 119; - } + return 119; } // Test WriteAsync(ReadOnlyMemory) and ReadAsync(Memory) @@ -204,12 +201,9 @@ return 120; } - for (int i = 0; i < data.Length; i++) + if (!data.SequenceEqual(memoryRead.Span)) { - if (data[i] != memoryRead.Span[i]) - { - return 121; - } + return 121; } // Test ReadByte/WriteByte (which delegate to span overrides) From e11d715f858883fb381d0662b1e2fb7c5b91a993 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Mar 2026 15:26:33 -0800 Subject: [PATCH 07/18] Add WindowsRuntimePinnedMemoryBuffer support to buffer helpers Update TryGetManagedSpanForCapacity and add TryGetManagedData helper in WindowsRuntimeBufferHelpers to handle WindowsRuntimePinnedMemoryBuffer alongside the existing array-backed buffer types. Simplify WindowsRuntimeBufferMarshal.TryGetDataUnsafe to use the new helper. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Buffers/WindowsRuntimeBufferHelpers.cs | 47 ++++++++++++++++++- .../WindowsRuntimeBufferMarshal.cs | 14 +----- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimeBufferHelpers.cs b/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimeBufferHelpers.cs index 35a1e503c..9231cbcd8 100644 --- a/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimeBufferHelpers.cs +++ b/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimeBufferHelpers.cs @@ -48,7 +48,7 @@ public static unsafe Span GetSpanForCapacity(IBuffer buffer) /// /// The returned value has a length equal to , not . /// - public static bool TryGetManagedSpanForCapacity(IBuffer buffer, out Span span) + public static unsafe bool TryGetManagedSpanForCapacity(IBuffer buffer, out Span span) { // If the buffer is backed by a managed array, return it if (buffer is WindowsRuntimeExternalArrayBuffer externalArrayBuffer) @@ -66,6 +66,14 @@ public static bool TryGetManagedSpanForCapacity(IBuffer buffer, out Span s return true; } + // Also handle pinned memory buffers (pointer-based, not array-backed) + if (buffer is WindowsRuntimePinnedMemoryBuffer pinnedMemoryBuffer) + { + span = new(pinnedMemoryBuffer.Buffer(), checked((int)buffer.Capacity)); + + return true; + } + span = default; return false; @@ -102,6 +110,43 @@ public static bool TryGetManagedArray(IBuffer buffer, [NotNullWhen(true)] out by return false; } + /// + /// Tries to get a pointer to the underlying data for the specified buffer, only if it is a known managed buffer implementation. + /// + /// The input instance. + /// The underlying data, if retrieved. + /// Whether could be retrieved. + public static unsafe bool TryGetManagedData(IBuffer buffer, out byte* data) + { + // If the buffer is backed by a managed array, get the data pointer + if (buffer is WindowsRuntimeExternalArrayBuffer externalArrayBuffer) + { + data = externalArrayBuffer.Buffer(); + + return true; + } + + // Same as above for pinned arrays as well + if (buffer is WindowsRuntimePinnedArrayBuffer pinnedArrayBuffer) + { + data = pinnedArrayBuffer.Buffer(); + + return true; + } + + // Also handle pinned memory buffers (pointer-based, not array-backed) + if (buffer is WindowsRuntimePinnedMemoryBuffer pinnedMemoryBuffer) + { + data = pinnedMemoryBuffer.Buffer(); + + return true; + } + + data = null; + + return false; + } + /// /// Tries to get the underlying data for the specified buffer, only if backed by native memory. /// diff --git a/src/WinRT.Runtime2/InteropServices/WindowsRuntimeBufferMarshal.cs b/src/WinRT.Runtime2/InteropServices/WindowsRuntimeBufferMarshal.cs index 83f3ccda5..ff0756aa5 100644 --- a/src/WinRT.Runtime2/InteropServices/WindowsRuntimeBufferMarshal.cs +++ b/src/WinRT.Runtime2/InteropServices/WindowsRuntimeBufferMarshal.cs @@ -39,19 +39,9 @@ public static unsafe bool TryGetDataUnsafe([NotNullWhen(true)] IBuffer? buffer, return true; } - // Also handle a managed instance of the external array buffer type from 'WinRT.Runtime.dll' - if (buffer is WindowsRuntimeExternalArrayBuffer externalArrayBuffer) - { - data = externalArrayBuffer.Buffer(); - - return true; - } - - // Same as above, but for pinned array buffers as well - if (buffer is WindowsRuntimePinnedArrayBuffer pinnedArrayBuffer) + // Also handle managed buffer implementations from 'WinRT.Runtime.dll' + if (WindowsRuntimeBufferHelpers.TryGetManagedData(buffer, out data)) { - data = pinnedArrayBuffer.Buffer(); - return true; } From f0b3ffed5bcf3a8de644b6574bc6e79bb14a8933 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Mar 2026 15:33:49 -0800 Subject: [PATCH 08/18] Use volatile field modifier instead of Volatile class for _data Replace Volatile.Read/Write calls with a volatile nint field declaration, simplifying the read/write sites to plain field access. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Buffers/WindowsRuntimePinnedMemoryBuffer.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs b/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs index 150118b13..ab8905f64 100644 --- a/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs +++ b/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs @@ -4,7 +4,6 @@ using System; using System.Diagnostics; using System.Runtime.CompilerServices; -using System.Threading; using Windows.Storage.Streams; namespace WindowsRuntime.InteropServices; @@ -18,9 +17,9 @@ namespace WindowsRuntime.InteropServices; internal sealed unsafe class WindowsRuntimePinnedMemoryBuffer : IBuffer { /// - /// The pointer to the pinned memory (stored as to support operations). + /// The pointer to the pinned memory. /// - private nint _data; + private volatile nint _data; /// /// The number of bytes that can be read or written in the buffer. @@ -76,7 +75,7 @@ public uint Length [MethodImpl(MethodImplOptions.AggressiveInlining)] public byte* Buffer() { - byte* data = (byte*)Volatile.Read(ref _data); + byte* data = (byte*)_data; InvalidOperationException.ThrowIfBufferIsInvalidated(data); @@ -101,6 +100,6 @@ public uint Length /// public void Invalidate() { - Volatile.Write(ref _data, 0); + _data = 0; } } From 40d774eeaa1d7403b77b0162eb5b56483c7207d8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Mar 2026 15:37:40 -0800 Subject: [PATCH 09/18] Add GetSpanForCapacity to WindowsRuntimePinnedMemoryBuffer Add a GetSpanForCapacity() method consistent with the other managed buffer types. Update the call site in WindowsRuntimeBufferHelpers to use it instead of manually constructing a Span from Buffer() and Capacity, which also avoids the checked uint-to-int conversion since the capacity is stored as int. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Buffers/WindowsRuntimeBufferHelpers.cs | 4 ++-- .../Buffers/WindowsRuntimePinnedMemoryBuffer.cs | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimeBufferHelpers.cs b/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimeBufferHelpers.cs index 9231cbcd8..a259bca33 100644 --- a/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimeBufferHelpers.cs +++ b/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimeBufferHelpers.cs @@ -48,7 +48,7 @@ public static unsafe Span GetSpanForCapacity(IBuffer buffer) /// /// The returned value has a length equal to , not . /// - public static unsafe bool TryGetManagedSpanForCapacity(IBuffer buffer, out Span span) + public static bool TryGetManagedSpanForCapacity(IBuffer buffer, out Span span) { // If the buffer is backed by a managed array, return it if (buffer is WindowsRuntimeExternalArrayBuffer externalArrayBuffer) @@ -69,7 +69,7 @@ public static unsafe bool TryGetManagedSpanForCapacity(IBuffer buffer, out Span< // Also handle pinned memory buffers (pointer-based, not array-backed) if (buffer is WindowsRuntimePinnedMemoryBuffer pinnedMemoryBuffer) { - span = new(pinnedMemoryBuffer.Buffer(), checked((int)buffer.Capacity)); + span = pinnedMemoryBuffer.GetSpanForCapacity(); return true; } diff --git a/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs b/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs index ab8905f64..6903d735a 100644 --- a/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs +++ b/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs @@ -82,6 +82,18 @@ public uint Length return data; } + /// + /// Thrown if the buffer has been invalidated. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span GetSpanForCapacity() + { + byte* data = (byte*)_data; + + InvalidOperationException.ThrowIfBufferIsInvalidated(data); + + return new(data, _capacity); + } + /// /// Invalidates the buffer, preventing any further access to the underlying memory. /// From 176b021e7027630e3a1c1f3737862e01d1380491 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Mar 2026 15:43:38 -0800 Subject: [PATCH 10/18] Use default to reset _data in Invalidate Replace the explicit literal 0 with the default literal when clearing the _data field in Invalidate(). This is a stylistic/clarity change that uses language-appropriate default initialization for the pointer/field and does not alter runtime behavior. --- .../InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs b/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs index 6903d735a..61dd4648e 100644 --- a/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs +++ b/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs @@ -112,6 +112,6 @@ public Span GetSpanForCapacity() /// public void Invalidate() { - _data = 0; + _data = default; } } From 2b1d8791be31090c6683caf12c6bdfc6549a0255 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Mar 2026 16:45:32 -0800 Subject: [PATCH 11/18] Discard ReadAsync result with discard assignment In src/Tests/FunctionalTests/Async/Program.cs, explicitly assign the awaited adaptedStream.ReadAsync call to the discard variable (`_`) so the returned value is intentionally ignored. This clarifies intent and silences warnings about unused/ignored return values from the async read. --- src/Tests/FunctionalTests/Async/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tests/FunctionalTests/Async/Program.cs b/src/Tests/FunctionalTests/Async/Program.cs index 056c8d846..ae4bcf1fc 100644 --- a/src/Tests/FunctionalTests/Async/Program.cs +++ b/src/Tests/FunctionalTests/Async/Program.cs @@ -243,7 +243,7 @@ try { - await adaptedStream.ReadAsync(new byte[256].AsMemory(), cts.Token); + _ = await adaptedStream.ReadAsync(new byte[256].AsMemory(), cts.Token); return 126; } catch (OperationCanceledException) From 5a1b96ee8c31d87fc3c29d36ed17559da7a6f6fc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 6 Mar 2026 20:15:14 -0800 Subject: [PATCH 12/18] Fix stream cancellation tests to accept derived exception types Use explicit try/catch for OperationCanceledException instead of ThrowsExactly, since the actual exception may be TaskCanceledException (a subclass) depending on whether the CsWinRT adapter or the base Stream implementation handles the call. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../UnitTest/TestComponentCSharp_Tests.cs | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index be148931d..1958a50b2 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -1012,7 +1012,18 @@ public void TestStreamReadAsyncMemoryWithCancellation() cts.Cancel(); Memory buffer = new byte[256]; - Assert.ThrowsExactly(() => stream.ReadAsync(buffer, cts.Token).AsTask().GetAwaiter().GetResult()); + bool threwCancellation = false; + + try + { + stream.ReadAsync(buffer, cts.Token).AsTask().GetAwaiter().GetResult(); + } + catch (OperationCanceledException) + { + threwCancellation = true; + } + + Assert.IsTrue(threwCancellation); } [TestMethod] @@ -1023,7 +1034,18 @@ public void TestStreamWriteAsyncMemoryWithCancellation() cts.Cancel(); ReadOnlyMemory buffer = new byte[256]; - Assert.ThrowsExactly(() => stream.WriteAsync(buffer, cts.Token).AsTask().GetAwaiter().GetResult()); + bool threwCancellation = false; + + try + { + stream.WriteAsync(buffer, cts.Token).AsTask().GetAwaiter().GetResult(); + } + catch (OperationCanceledException) + { + threwCancellation = true; + } + + Assert.IsTrue(threwCancellation); } [TestMethod] From 2bb4807981adb0e34474523723277c34ae79f4bc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 7 Mar 2026 19:32:23 -0800 Subject: [PATCH 13/18] Add CCW support for WindowsRuntimePinnedMemoryBuffer Add the missing ABI CCW (COM Callable Wrapper) support file for WindowsRuntimePinnedMemoryBuffer, following the same pattern as WindowsRuntimeExternalArrayBuffer and WindowsRuntimePinnedArrayBuffer. This includes TypeMapAssociation registration, interface entries for IBuffer, IBufferByteAccess, IStringable, IWeakReferenceSource, IMarshal, IAgileObject, IInspectable, and IUnknown, plus the custom marshaller attribute and IBufferByteAccess vtable implementation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../WindowsRuntimePinnedMemoryBuffer.cs | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) create mode 100644 src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs diff --git a/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs b/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs new file mode 100644 index 000000000..f0a036f5d --- /dev/null +++ b/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs @@ -0,0 +1,155 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.ComponentModel; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using WindowsRuntime; +using WindowsRuntime.InteropServices; +using WindowsRuntime.InteropServices.Marshalling; +using static System.Runtime.InteropServices.ComWrappers; + +#pragma warning disable CS0723, IDE0008, IDE0046, IDE1006 + +[assembly: TypeMapAssociation( + source: typeof(WindowsRuntimePinnedMemoryBuffer), + proxy: typeof(ABI.WindowsRuntime.InteropServices.WindowsRuntimePinnedMemoryBuffer))] + +namespace ABI.WindowsRuntime.InteropServices; + +/// +/// ABI type for . +/// +[WindowsRuntimeClassName("Windows.Storage.Streams.IBuffer")] +[WindowsRuntimePinnedMemoryBufferComWrappersMarshaller] +file static class WindowsRuntimePinnedMemoryBuffer; + +/// +/// The set of values for . +/// +file struct WindowsRuntimePinnedMemoryBufferInterfaceEntries +{ + public ComInterfaceEntry IBuffer; + public ComInterfaceEntry IBufferByteAccess; + public ComInterfaceEntry IStringable; + public ComInterfaceEntry IWeakReferenceSource; + public ComInterfaceEntry IMarshal; + public ComInterfaceEntry IAgileObject; + public ComInterfaceEntry IInspectable; + public ComInterfaceEntry IUnknown; +} + +/// +/// The implementation of . +/// +file static class WindowsRuntimePinnedMemoryBufferInterfaceEntriesImpl +{ + /// + /// The value for . + /// + [FixedAddressValueType] + public static readonly WindowsRuntimePinnedMemoryBufferInterfaceEntries Entries; + + /// + /// Initializes . + /// + static WindowsRuntimePinnedMemoryBufferInterfaceEntriesImpl() + { + Entries.IBuffer.IID = WellKnownWindowsInterfaceIIDs.IID_IBuffer; + Entries.IBuffer.Vtable = Windows.Storage.Streams.IBufferImpl.Vtable; + Entries.IBufferByteAccess.IID = WellKnownWindowsInterfaceIIDs.IID_IBufferByteAccess; + Entries.IBufferByteAccess.Vtable = WindowsRuntimePinnedMemoryBufferByteAccessImpl.Vtable; + Entries.IStringable.IID = WellKnownWindowsInterfaceIIDs.IID_IStringable; + Entries.IStringable.Vtable = IStringableImpl.Vtable; + Entries.IWeakReferenceSource.IID = WellKnownWindowsInterfaceIIDs.IID_IWeakReferenceSource; + Entries.IWeakReferenceSource.Vtable = IWeakReferenceSourceImpl.Vtable; + Entries.IMarshal.IID = WellKnownWindowsInterfaceIIDs.IID_IMarshal; + Entries.IMarshal.Vtable = IMarshalImpl.RoBufferVtable; + Entries.IAgileObject.IID = WellKnownWindowsInterfaceIIDs.IID_IAgileObject; + Entries.IAgileObject.Vtable = IAgileObjectImpl.Vtable; + Entries.IInspectable.IID = WellKnownWindowsInterfaceIIDs.IID_IInspectable; + Entries.IInspectable.Vtable = IInspectableImpl.Vtable; + Entries.IUnknown.IID = WellKnownWindowsInterfaceIIDs.IID_IUnknown; + Entries.IUnknown.Vtable = IUnknownImpl.Vtable; + } +} + +/// +/// A custom implementation for . +/// +[Obsolete(WindowsRuntimeConstants.PrivateImplementationDetailObsoleteMessage, + DiagnosticId = WindowsRuntimeConstants.PrivateImplementationDetailObsoleteDiagnosticId, + UrlFormat = WindowsRuntimeConstants.CsWinRTDiagnosticsUrlFormat)] +[EditorBrowsable(EditorBrowsableState.Never)] +public sealed unsafe class WindowsRuntimePinnedMemoryBufferComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute +{ + /// + public override void* GetOrCreateComInterfaceForObject(object value) + { + // No reference tracking is needed, see notes in the marshaller attribute for 'WindowsRuntimePinnedArrayBuffer' + return (void*)WindowsRuntimeComWrappers.Default.GetOrCreateComInterfaceForObject(value, CreateComInterfaceFlags.None); + } + + /// + public override ComInterfaceEntry* ComputeVtables(out int count) + { + count = sizeof(WindowsRuntimePinnedMemoryBufferInterfaceEntries) / sizeof(ComInterfaceEntry); + + return (ComInterfaceEntry*)Unsafe.AsPointer(in WindowsRuntimePinnedMemoryBufferInterfaceEntriesImpl.Entries); + } +} + +/// +/// The native implementation of IBufferByteAccess for . +/// +file static unsafe class WindowsRuntimePinnedMemoryBufferByteAccessImpl +{ + /// + /// The value for the implementation. + /// + [FixedAddressValueType] + private static readonly IBufferByteAccessVftbl Vftbl; + + /// + /// Initializes . + /// + static WindowsRuntimePinnedMemoryBufferByteAccessImpl() + { + *(IInspectableVftbl*)Unsafe.AsPointer(ref Vftbl) = *(IInspectableVftbl*)IInspectableImpl.Vtable; + + Vftbl.Buffer = &Buffer; + } + + /// + /// Gets a pointer to the implementation. + /// + public static nint Vtable + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => (nint)Unsafe.AsPointer(in Vftbl); + } + + /// + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvMemberFunction)])] + private static HRESULT Buffer(void* thisPtr, byte** value) + { + if (value is null) + { + return WellKnownErrorCodes.E_POINTER; + } + + try + { + var thisObject = ComInterfaceDispatch.GetInstance((ComInterfaceDispatch*)thisPtr); + + *value = thisObject.Buffer(); + + return WellKnownErrorCodes.S_OK; + } + catch (Exception ex) + { + return RestrictedErrorInfoExceptionMarshaller.ConvertToUnmanaged(ex); + } + } +} From 7424bf5489a333496ebeed548c3122903c923be4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 7 Mar 2026 19:41:53 -0800 Subject: [PATCH 14/18] Add buffer CCW test coverage for all managed buffer types Add tests that exercise CCW (COM Callable Wrapper) interop for both WindowsRuntimeExternalArrayBuffer and WindowsRuntimePinnedArrayBuffer. The existing stream adapter span/memory tests already exercise WindowsRuntimePinnedMemoryBuffer through the pinned memory code path. Managed tests: - Extend TestCCWMarshaler to also verify PinnedArrayBuffer CCW creation and IMarshal QI (via WindowsRuntimeBuffer.Create()) - Add TestWriteBufferPinnedArrayBuffer that writes a PinnedArrayBuffer to a native InMemoryRandomAccessStream and reads the data back Functional tests: - Add explicit CCW creation and IBuffer QI test for PinnedArrayBuffer - Add write-to-native-stream tests for both ExternalArrayBuffer and PinnedArrayBuffer to verify end-to-end CCW interop Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Tests/FunctionalTests/Async/Program.cs | 48 +++++++++++++++++++ .../UnitTest/TestComponentCSharp_Tests.cs | 35 ++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/Tests/FunctionalTests/Async/Program.cs b/src/Tests/FunctionalTests/Async/Program.cs index ae4bcf1fc..6a113a03f 100644 --- a/src/Tests/FunctionalTests/Async/Program.cs +++ b/src/Tests/FunctionalTests/Async/Program.cs @@ -112,6 +112,7 @@ return 111; } + // Test WindowsRuntimeExternalArrayBuffer CCW (created via AsBuffer()) var arr = new byte[100]; var buffer = arr.AsBuffer(); ptr = WindowsRuntimeMarshal.ConvertToUnmanaged(buffer); @@ -126,6 +127,20 @@ return 113; } + // Test WindowsRuntimePinnedArrayBuffer CCW (created via WindowsRuntimeBuffer.Create()) + var pinnedBuffer = WindowsRuntimeBuffer.Create(100); + ptr = WindowsRuntimeMarshal.ConvertToUnmanaged(pinnedBuffer); + if (ptr is null) + { + return 128; + } + + if (Marshal.QueryInterface((nint)ptr, typeof(IBuffer).GUID, out ptr2) != 0 || + ptr2 == IntPtr.Zero) + { + return 129; + } + var asyncOperation = randomAccessStream.ReadAsync(buffer, 50, InputStreamOptions.Partial); ptr = WindowsRuntimeMarshal.ConvertToUnmanaged(asyncOperation); if (ptr is null) @@ -260,6 +275,39 @@ } } +// Test writing each managed buffer type to a native WinRT stream (exercises CCW interop) +{ + byte[] testData = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]; + + // Test WindowsRuntimeExternalArrayBuffer (from AsBuffer()) written to native stream + using var stream1 = new InMemoryRandomAccessStream(); + IBuffer externalArrayBuffer = testData.AsBuffer(); + await stream1.WriteAsync(externalArrayBuffer); + stream1.Seek(0); + + byte[] read1 = new byte[8]; + IBuffer readBuffer1 = read1.AsBuffer(); + await stream1.ReadAsync(readBuffer1, 8, InputStreamOptions.None); + if (!testData.SequenceEqual(read1)) + { + return 130; + } + + // Test WindowsRuntimePinnedArrayBuffer (from WindowsRuntimeBuffer.Create()) written to native stream + using var stream2 = new InMemoryRandomAccessStream(); + IBuffer pinnedArrayBuffer = WindowsRuntimeBuffer.Create(testData); + await stream2.WriteAsync(pinnedArrayBuffer); + stream2.Seek(0); + + byte[] read2 = new byte[8]; + IBuffer readBuffer2 = read2.AsBuffer(); + await stream2.ReadAsync(readBuffer2, 8, InputStreamOptions.None); + if (!testData.SequenceEqual(read2)) + { + return 131; + } +} + return 100; static async Task InvokeAddAsync(Class instance, int lhs, int rhs) diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index 1958a50b2..846079d97 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -1270,6 +1270,34 @@ public void TestWriteBuffer() Assert.IsTrue(InvokeWriteBufferAsync().Wait(1000)); } + async Task InvokeWriteBufferPinnedArrayAsync() + { + var random = new Random(42); + byte[] data = new byte[256]; + random.NextBytes(data); + + // WindowsRuntimeBuffer.Create() creates a WindowsRuntimePinnedArrayBuffer, + // which exercises a separate CCW code path than AsBuffer() (which creates + // a WindowsRuntimeExternalArrayBuffer). This test verifies that the pinned + // array buffer CCW is correctly used when writing to a native WinRT stream. + using var stream = new InMemoryRandomAccessStream(); + IBuffer buffer = WindowsRuntimeBuffer.Create(data); + await stream.WriteAsync(buffer); + + stream.Seek(0); + + byte[] readData = new byte[256]; + IBuffer readBuffer = readData.AsBuffer(); + await stream.ReadAsync(readBuffer, 256, InputStreamOptions.None); + CollectionAssert.AreEqual(data, readData); + } + + [TestMethod] + public void TestWriteBufferPinnedArrayBuffer() + { + Assert.IsTrue(InvokeWriteBufferPinnedArrayAsync().Wait(5000)); + } + [TestMethod] public unsafe void TestUri() { @@ -2238,11 +2266,18 @@ public unsafe void TestCCWMarshaler() Marshal.ThrowExceptionForHR(Marshal.QueryInterface((IntPtr)ccw.GetThisPtrUnsafe(), in IID_IMarshal, out var marshalCCW)); Assert.AreNotEqual(IntPtr.Zero, marshalCCW); + // Test WindowsRuntimeExternalArrayBuffer CCW (created via AsBuffer()) var array = new byte[] { 0x01 }; var buff = array.AsBuffer(); using WindowsRuntimeObjectReferenceValue ccw2 = WindowsRuntimeInterfaceMarshaller.ConvertToUnmanaged(buff, typeof(IBuffer).GUID); Marshal.ThrowExceptionForHR(Marshal.QueryInterface((IntPtr)ccw2.GetThisPtrUnsafe(), in IID_IMarshal, out var marshalCCW2)); Assert.AreNotEqual(IntPtr.Zero, marshalCCW2); + + // Test WindowsRuntimePinnedArrayBuffer CCW (created via WindowsRuntimeBuffer.Create()) + var pinnedBuff = WindowsRuntimeBuffer.Create(new byte[] { 0x01 }); + using WindowsRuntimeObjectReferenceValue ccw3 = WindowsRuntimeInterfaceMarshaller.ConvertToUnmanaged(pinnedBuff, typeof(IBuffer).GUID); + Marshal.ThrowExceptionForHR(Marshal.QueryInterface((IntPtr)ccw3.GetThisPtrUnsafe(), in IID_IMarshal, out var marshalCCW3)); + Assert.AreNotEqual(IntPtr.Zero, marshalCCW3); } [TestMethod] From 106b402439254e6a272fd78842ede5ce2a3ec7df Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 7 Mar 2026 20:10:51 -0800 Subject: [PATCH 15/18] Make buffer CCW marshaller attribute types file-scoped Make the WindowsRuntimeComWrappersMarshallerAttribute derivatives for all three managed buffer types file-scoped instead of public, since they are only referenced within their own file. This also removes the now-unnecessary [Obsolete] and [EditorBrowsable] attributes and the unused System.ComponentModel using directive. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Buffers/WindowsRuntimeExternalArrayBuffer.cs | 7 +------ .../Buffers/WindowsRuntimePinnedArrayBuffer.cs | 7 +------ .../Buffers/WindowsRuntimePinnedMemoryBuffer.cs | 7 +------ 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimeExternalArrayBuffer.cs b/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimeExternalArrayBuffer.cs index c1908daa7..3ea7bdf7d 100644 --- a/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimeExternalArrayBuffer.cs +++ b/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimeExternalArrayBuffer.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.ComponentModel; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using WindowsRuntime; @@ -78,11 +77,7 @@ static WindowsRuntimeExternalArrayBufferInterfaceEntriesImpl() /// /// A custom implementation for . /// -[Obsolete(WindowsRuntimeConstants.PrivateImplementationDetailObsoleteMessage, - DiagnosticId = WindowsRuntimeConstants.PrivateImplementationDetailObsoleteDiagnosticId, - UrlFormat = WindowsRuntimeConstants.CsWinRTDiagnosticsUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -public sealed unsafe class WindowsRuntimeExternalArrayBufferComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute +file sealed unsafe class WindowsRuntimeExternalArrayBufferComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute { /// public override void* GetOrCreateComInterfaceForObject(object value) diff --git a/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimePinnedArrayBuffer.cs b/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimePinnedArrayBuffer.cs index 46dfaab91..2aea75ad9 100644 --- a/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimePinnedArrayBuffer.cs +++ b/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimePinnedArrayBuffer.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.ComponentModel; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using WindowsRuntime; @@ -78,11 +77,7 @@ static WindowsRuntimePinnedArrayBufferInterfaceEntriesImpl() /// /// A custom implementation for . /// -[Obsolete(WindowsRuntimeConstants.PrivateImplementationDetailObsoleteMessage, - DiagnosticId = WindowsRuntimeConstants.PrivateImplementationDetailObsoleteDiagnosticId, - UrlFormat = WindowsRuntimeConstants.CsWinRTDiagnosticsUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -public sealed unsafe class WindowsRuntimePinnedArrayBufferComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute +file sealed unsafe class WindowsRuntimePinnedArrayBufferComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute { /// public override void* GetOrCreateComInterfaceForObject(object value) diff --git a/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs b/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs index f0a036f5d..da1268cde 100644 --- a/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs +++ b/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.ComponentModel; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using WindowsRuntime; @@ -78,11 +77,7 @@ static WindowsRuntimePinnedMemoryBufferInterfaceEntriesImpl() /// /// A custom implementation for . /// -[Obsolete(WindowsRuntimeConstants.PrivateImplementationDetailObsoleteMessage, - DiagnosticId = WindowsRuntimeConstants.PrivateImplementationDetailObsoleteDiagnosticId, - UrlFormat = WindowsRuntimeConstants.CsWinRTDiagnosticsUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -public sealed unsafe class WindowsRuntimePinnedMemoryBufferComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute +file sealed unsafe class WindowsRuntimePinnedMemoryBufferComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute { /// public override void* GetOrCreateComInterfaceForObject(object value) From 070941c4178d61d4ff3e60ca942c4d8bc70b72be Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 7 Mar 2026 20:11:12 -0800 Subject: [PATCH 16/18] Add functional test for WindowsRuntimePinnedMemoryBuffer CCW Add a test case that writes data to a native InMemoryRandomAccessStream via the stream adapter's span-based Write override, which internally creates a WindowsRuntimePinnedMemoryBuffer and passes it as an IBuffer CCW to the native WinRT stream's WriteAsync. The data is then read back directly from the native stream to verify correctness. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Tests/FunctionalTests/Async/Program.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Tests/FunctionalTests/Async/Program.cs b/src/Tests/FunctionalTests/Async/Program.cs index 6a113a03f..fa43cc064 100644 --- a/src/Tests/FunctionalTests/Async/Program.cs +++ b/src/Tests/FunctionalTests/Async/Program.cs @@ -306,6 +306,24 @@ { return 131; } + + // Test WindowsRuntimePinnedMemoryBuffer (created internally by the stream adapter when + // using span/memory-based Write, which pins the data and wraps it in a PinnedMemoryBuffer + // before passing it as an IBuffer CCW to the native WinRT stream's WriteAsync) + using var stream3 = new InMemoryRandomAccessStream(); + using var adaptedStream3 = stream3.AsStream(); + adaptedStream3.Write(new ReadOnlySpan(testData)); + adaptedStream3.Dispose(); + + stream3.Seek(0); + + byte[] read3 = new byte[8]; + IBuffer readBuffer3 = read3.AsBuffer(); + await stream3.ReadAsync(readBuffer3, 8, InputStreamOptions.None); + if (!testData.SequenceEqual(read3)) + { + return 132; + } } return 100; From 319516daad5dc282955a5f9a9388b1c3e8a20fec Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 7 Mar 2026 20:11:30 -0800 Subject: [PATCH 17/18] Refactor TestWriteBufferPinnedArrayBuffer to use local method Use a local async method instead of a separate private helper method, matching the pattern used by the other stream adapter tests in this file (e.g. TestStreamReadAsyncMemory, TestStreamWriteAsyncMemory). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../UnitTest/TestComponentCSharp_Tests.cs | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs index 846079d97..8edcc8415 100644 --- a/src/Tests/UnitTest/TestComponentCSharp_Tests.cs +++ b/src/Tests/UnitTest/TestComponentCSharp_Tests.cs @@ -1270,32 +1270,32 @@ public void TestWriteBuffer() Assert.IsTrue(InvokeWriteBufferAsync().Wait(1000)); } - async Task InvokeWriteBufferPinnedArrayAsync() + [TestMethod] + public void TestWriteBufferPinnedArrayBuffer() { - var random = new Random(42); - byte[] data = new byte[256]; - random.NextBytes(data); + async Task TestAsync() + { + var random = new Random(42); + byte[] data = new byte[256]; + random.NextBytes(data); - // WindowsRuntimeBuffer.Create() creates a WindowsRuntimePinnedArrayBuffer, - // which exercises a separate CCW code path than AsBuffer() (which creates - // a WindowsRuntimeExternalArrayBuffer). This test verifies that the pinned - // array buffer CCW is correctly used when writing to a native WinRT stream. - using var stream = new InMemoryRandomAccessStream(); - IBuffer buffer = WindowsRuntimeBuffer.Create(data); - await stream.WriteAsync(buffer); + // WindowsRuntimeBuffer.Create() creates a WindowsRuntimePinnedArrayBuffer, + // which exercises a separate CCW code path than AsBuffer() (which creates + // a WindowsRuntimeExternalArrayBuffer). This test verifies that the pinned + // array buffer CCW is correctly used when writing to a native WinRT stream. + using var stream = new InMemoryRandomAccessStream(); + IBuffer buffer = WindowsRuntimeBuffer.Create(data); + await stream.WriteAsync(buffer); - stream.Seek(0); + stream.Seek(0); - byte[] readData = new byte[256]; - IBuffer readBuffer = readData.AsBuffer(); - await stream.ReadAsync(readBuffer, 256, InputStreamOptions.None); - CollectionAssert.AreEqual(data, readData); - } + byte[] readData = new byte[256]; + IBuffer readBuffer = readData.AsBuffer(); + await stream.ReadAsync(readBuffer, 256, InputStreamOptions.None); + CollectionAssert.AreEqual(data, readData); + } - [TestMethod] - public void TestWriteBufferPinnedArrayBuffer() - { - Assert.IsTrue(InvokeWriteBufferPinnedArrayAsync().Wait(5000)); + Assert.IsTrue(TestAsync().Wait(5000)); } [TestMethod] From 73fc9888815b05dbb56016e38dc25ecd66874bd6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 8 Mar 2026 12:52:57 -0700 Subject: [PATCH 18/18] Use byte* for pinned buffer pointer Change the pinned memory field from nint to byte* and update usages accordingly. Removed redundant pointer casts in the constructor, Buffer(), and GetSpanForCapacity(), and set the field to null in Invalidate(). These changes improve type safety and clarity when working with the unsafe pinned buffer pointer. --- .../Buffers/WindowsRuntimePinnedMemoryBuffer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs b/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs index 61dd4648e..3b485243a 100644 --- a/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs +++ b/src/WinRT.Runtime2/InteropServices/Buffers/WindowsRuntimePinnedMemoryBuffer.cs @@ -19,7 +19,7 @@ internal sealed unsafe class WindowsRuntimePinnedMemoryBuffer : IBuffer /// /// The pointer to the pinned memory. /// - private volatile nint _data; + private volatile byte* _data; /// /// The number of bytes that can be read or written in the buffer. @@ -45,7 +45,7 @@ public WindowsRuntimePinnedMemoryBuffer(byte* data, int length, int capacity) Debug.Assert(capacity >= 0); Debug.Assert(capacity >= length); - _data = (nint)data; + _data = data; _length = length; _capacity = capacity; } @@ -75,7 +75,7 @@ public uint Length [MethodImpl(MethodImplOptions.AggressiveInlining)] public byte* Buffer() { - byte* data = (byte*)_data; + byte* data = _data; InvalidOperationException.ThrowIfBufferIsInvalidated(data); @@ -87,7 +87,7 @@ public uint Length [MethodImpl(MethodImplOptions.AggressiveInlining)] public Span GetSpanForCapacity() { - byte* data = (byte*)_data; + byte* data = _data; InvalidOperationException.ThrowIfBufferIsInvalidated(data); @@ -112,6 +112,6 @@ public Span GetSpanForCapacity() /// public void Invalidate() { - _data = default; + _data = null; } }