Skip to content

Commit 8ea13ae

Browse files
committed
Add Stream wrappers for memory and text-based types
Polyfills StringStream, ReadOnlyMemoryStream, WritableMemoryStream and ReadOnlySequenceStream from dotnet/runtime#126669. These types are approved for net11 but are not in the current net11 preview, so they are gated on `#if FeatureMemory` and remain active on net11. When a net11 SDK ships the real types, ApiBuilderTests.StreamWrapperBclDetectionTests fails with the exact change to make in each file: flip the guard to `#if FeatureMemory && !NET11_0_OR_GREATER` and add a TypeForwardedTo under `#if NET11_0_OR_GREATER`. The same steps are documented atop each source file. - ReadOnlyMemoryStream/WritableMemoryStream derive from MemoryStream and delegate to the MemoryStream(byte[],int,int,bool,bool) constructor, giving read-only / fixed-capacity semantics with GetBuffer throwing and TryGetBuffer returning false, without relying on MemoryStream internals unavailable on older TFMs. - StringStream (non-seekable, never emits a BOM) and ReadOnlySequenceStream (seekable) derive from Stream. Tests run on every framework where FeatureMemory is available; verified compiling across all 22 TFMs and passing on net11.0 and net48.
1 parent d89c73d commit 8ea13ae

99 files changed

Lines changed: 11704 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/ApiBuilderTests/BuildApiTest.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ public Task RunWithRoslyn()
2323
WriteHelper("Utf16", writer, ref count);
2424
WriteType(nameof(TaskCompletionSource), writer, ref count);
2525
WriteType(nameof(UnreachableException), writer, ref count);
26+
WriteType("StringStream", writer, ref count);
27+
WriteType("ReadOnlyMemoryStream", writer, ref count);
28+
WriteType("WritableMemoryStream", writer, ref count);
29+
WriteType("ReadOnlySequenceStream", writer, ref count);
2630

2731
count += Directory.EnumerateFiles(polyfillDir, "*Attribute.cs", SearchOption.AllDirectories).Count();
2832
// Index and Range
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
using System.Text;
2+
3+
/// <summary>
4+
/// The Stream wrapper types from dotnet/runtime#126669 (StringStream, ReadOnlyMemoryStream,
5+
/// WritableMemoryStream, ReadOnlySequenceStream) were approved/merged for net11 but are NOT in the
6+
/// current net11 preview/RC. Polyfill therefore ships them as ACTIVE on net11 too.
7+
///
8+
/// This project (ApiBuilderTests) targets net11.0 and does NOT compile the polyfill source into
9+
/// itself, so it keeps building once the BCL gains these types. This test runs on the net11 runtime
10+
/// and fails the moment the real types appear, telling you exactly which guards to flip.
11+
/// </summary>
12+
public class StreamWrapperBclDetectionTests
13+
{
14+
static readonly (string FullName, string File)[] pending =
15+
[
16+
("System.IO.StringStream", "StringStream.cs"),
17+
("System.IO.ReadOnlyMemoryStream", "ReadOnlyMemoryStream.cs"),
18+
("System.IO.WritableMemoryStream", "WritableMemoryStream.cs"),
19+
("System.Buffers.ReadOnlySequenceStream", "ReadOnlySequenceStream.cs"),
20+
];
21+
22+
[Test]
23+
public void StreamWrapperPolyfillsStillNeededOnNet11()
24+
{
25+
var present = new List<(string FullName, string File, string Assembly)>();
26+
foreach (var (fullName, file) in pending)
27+
{
28+
var type = FindBclType(fullName);
29+
if (type != null)
30+
{
31+
present.Add((fullName, file, type.Assembly.GetName().Name!));
32+
}
33+
}
34+
35+
if (present.Count == 0)
36+
{
37+
return;
38+
}
39+
40+
var builder = new StringBuilder();
41+
builder.AppendLine($"{present.Count} Stream wrapper type(s) from dotnet/runtime#126669 are now provided by the BCL");
42+
builder.AppendLine($"on this runtime ({System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}).");
43+
builder.AppendLine("The matching Polyfill source now collides with the real type and must be retired for net11.");
44+
builder.AppendLine("For EACH type below, edit the listed file under src/Polyfill/:");
45+
builder.AppendLine();
46+
47+
foreach (var (fullName, file, assembly) in present)
48+
{
49+
builder.AppendLine($" {fullName} (now in {assembly})");
50+
builder.AppendLine($" file: src/Polyfill/{file}");
51+
builder.AppendLine(" 1. change `#if FeatureMemory` to `#if FeatureMemory && !NET11_0_OR_GREATER`");
52+
builder.AppendLine(" 2. append after the final #endif:");
53+
builder.AppendLine(" #if NET11_0_OR_GREATER");
54+
builder.AppendLine($" [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof({fullName}))]");
55+
builder.AppendLine(" #endif");
56+
builder.AppendLine();
57+
}
58+
59+
builder.AppendLine("Then re-run ApiBuilderTests in Debug to regenerate Split + api_list, and drop any now-redundant");
60+
builder.AppendLine("usage from src/Consume/Consume.cs.");
61+
62+
throw new(builder.ToString());
63+
}
64+
65+
static System.Type? FindBclType(string fullName)
66+
{
67+
foreach (var assembly in new[]
68+
{
69+
"System.Private.CoreLib",
70+
"System.Runtime",
71+
"System.Memory",
72+
"mscorlib"
73+
})
74+
{
75+
var type = System.Type.GetType($"{fullName}, {assembly}", throwOnError: false);
76+
if (type is { IsPublic: true })
77+
{
78+
return type;
79+
}
80+
}
81+
82+
return null;
83+
}
84+
}

src/Consume/Consume.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,39 @@ void Base64UrlUsage()
258258
}
259259
#endif
260260

261+
#if FeatureMemory
262+
void StreamWrappersUsage()
263+
{
264+
var bytes = new byte[10];
265+
266+
using (Stream stringStream = new StringStream("text", Encoding.UTF8))
267+
{
268+
stringStream.Read(bytes, 0, bytes.Length);
269+
}
270+
271+
using (var stringStream = new StringStream("text".AsMemory(), Encoding.UTF8))
272+
{
273+
Encoding encoding = stringStream.Encoding;
274+
stringStream.ReadByte();
275+
}
276+
277+
using (MemoryStream readOnly = new ReadOnlyMemoryStream(new ReadOnlyMemory<byte>(bytes)))
278+
{
279+
readOnly.Read(bytes, 0, bytes.Length);
280+
}
281+
282+
using (MemoryStream writable = new WritableMemoryStream(new Memory<byte>(bytes)))
283+
{
284+
writable.Write(bytes, 0, bytes.Length);
285+
}
286+
287+
using (Stream sequenceStream = new System.Buffers.ReadOnlySequenceStream(new System.Buffers.ReadOnlySequence<byte>(bytes)))
288+
{
289+
sequenceStream.Read(bytes, 0, bytes.Length);
290+
}
291+
}
292+
#endif
293+
261294
#if FeatureMemory && !RefsBclMemory
262295
void Base64Usage()
263296
{
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#nullable enable
2+
3+
// === Polyfill target window =====================================================================
4+
// System.IO.ReadOnlyMemoryStream was approved and merged into dotnet/runtime for net11
5+
// (https://github.com/dotnet/runtime/pull/126669) but is NOT in the current net11 preview/RC SDK,
6+
// so this polyfill is intentionally ACTIVE on net11 as well.
7+
// When you upgrade to a net11 SDK that actually ships this type, the build will collide (CS0436)
8+
// and the ApiBuilderTests "StreamWrapperBclDetectionTests" test FAILS with these exact steps.
9+
// To retire this polyfill for net11:
10+
// 1. change `#if FeatureMemory` below to `#if FeatureMemory && !NET11_0_OR_GREATER`
11+
// 2. add at the bottom of this file (after the final #endif):
12+
// #if NET11_0_OR_GREATER
13+
// [assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.IO.ReadOnlyMemoryStream))]
14+
// #endif
15+
// 3. re-run ApiBuilderTests in Debug to regenerate Split + api_list.
16+
// ================================================================================================
17+
#if FeatureMemory
18+
19+
#pragma warning disable
20+
21+
namespace System.IO;
22+
23+
using System.Diagnostics;
24+
using System.Diagnostics.CodeAnalysis;
25+
using System.Runtime.InteropServices;
26+
27+
/// <summary>
28+
/// Provides a seekable, read-only <see cref="MemoryStream"/> over a <see cref="ReadOnlyMemory{Byte}"/>.
29+
/// </summary>
30+
/// <remarks>
31+
/// The stream cannot be written to. <see cref="MemoryStream.CanWrite"/> always returns <see langword="false"/>.
32+
/// <see cref="MemoryStream.GetBuffer"/> throws and <see cref="MemoryStream.TryGetBuffer"/> returns <see langword="false"/>.
33+
/// When the supplied memory is not backed by an array (for example native memory) the polyfill copies it;
34+
/// the BCL always wraps the memory without copying.
35+
/// </remarks>
36+
[ExcludeFromCodeCoverage]
37+
[DebuggerNonUserCode]
38+
#if PolyUseEmbeddedAttribute
39+
[global::Microsoft.CodeAnalysis.EmbeddedAttribute]
40+
#endif
41+
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.readonlymemorystream?view=net-11.0
42+
#if PolyPublic
43+
public
44+
#endif
45+
sealed class ReadOnlyMemoryStream :
46+
MemoryStream
47+
{
48+
/// <summary>
49+
/// Initializes a new instance of the <see cref="ReadOnlyMemoryStream"/> class over the specified <see cref="ReadOnlyMemory{Byte}"/>.
50+
/// </summary>
51+
//Link: https://learn.microsoft.com/en-us/dotnet/api/system.io.readonlymemorystream.-ctor?view=net-11.0
52+
public ReadOnlyMemoryStream(ReadOnlyMemory<byte> source)
53+
: base(GetArray(source, out var offset, out var count), offset, count, writable: false, publiclyVisible: false)
54+
{
55+
}
56+
57+
static byte[] GetArray(ReadOnlyMemory<byte> source, out int offset, out int count)
58+
{
59+
if (MemoryMarshal.TryGetArray(source, out var segment) &&
60+
segment.Array != null)
61+
{
62+
offset = segment.Offset;
63+
count = segment.Count;
64+
return segment.Array;
65+
}
66+
67+
var array = source.ToArray();
68+
offset = 0;
69+
count = array.Length;
70+
return array;
71+
}
72+
}
73+
74+
#endif

0 commit comments

Comments
 (0)