Skip to content

Use readonly/scoped with existing ref structs that accept spans #71497

@stephentoub

Description

@stephentoub

We have a handful of public ref struct types that have instance methods that accept span arguments. Callers then run into problems when trying to use these methods with variables of mixed lifetimes, e.g.

using System;

public class C
{
    public bool M(ref Reader r)
    {
        Span<char> span = stackalloc char[] { 'a' };
        return r.Equals(span);
    }
}

public ref struct Reader
{
    public bool Equals(ReadOnlySpan<char> span) => true;
}

This will fail with the warnings:

error CS8352: Cannot use variable 'span' in this context because it may expose referenced variables outside of their declaration scope
error CS8350: This combination of arguments to 'Reader.Equals(ReadOnlySpan<char>)' is disallowed because it may expose variables referenced by parameter 'span' outside of their declaration scope

because the r.Equals(span) call could theoretically store that span into r, in which case the caller of M would then have a Reader storing a Span<char> that's pointing to invalid memory.

We have a handful of members that get tripped up by this:

namespace System.Buffers
{
    public ref struct SequenceReader<T> where T : unmanaged, IEquatable<T>
    {
        public long AdvancePastAny(ReadOnlySpan<T> values);
        public bool IsNext(ReadOnlySpan<T> next, bool advancePast = false);
        public bool TryAdvanceToAny(ReadOnlySpan<T> delimiters, bool advancePastDelimiter = true);
        public bool TryReadTo(out ReadOnlySpan<T> span, ReadOnlySpan<T> delimiter, bool advancePastDelimiter = true);
        public bool TryReadToAny(out ReadOnlySequence<T> sequence, ReadOnlySpan<T> delimiters, bool advancePastDelimiter = true);
        public bool TryReadToAny(out ReadOnlySpan<T> span, ReadOnlySpan<T> delimiters, bool advancePastDelimiter = true);
    }
}

namespace System.Text.Json
{
    public ref struct Utf8JsonReader
    {
        public bool ValueTextEquals(ReadOnlySpan<byte> utf8Text);
        public bool ValueTextEquals(ReadOnlySpan<char> text);
    }
}

We have two levers we can pull to address these:

  1. Mark the member as readonly. This only works if a) the member actually doesn't modify instance state and b) there aren't other ref parameters / ref structs into which something could be stored.
  2. Mark the parameter as scoped. This is new in C# 11 and promises that the parameter won't be stored or returned.

I propose the following:

namespace System.Buffers
{
    public ref struct SequenceReader<T> where T : unmanaged, IEquatable<T>
    {
        public long AdvancePastAny(**scoped** ReadOnlySpan<T> values);
        public bool IsNext(**scoped** ReadOnlySpan<T> next, bool advancePast = false);
        public bool TryAdvanceToAny(**scoped** ReadOnlySpan<T> delimiters, bool advancePastDelimiter = true);
        public bool TryReadTo(out ReadOnlySpan<T> span, **scoped** ReadOnlySpan<T> delimiter, bool advancePastDelimiter = true);
        public bool TryReadToAny(out ReadOnlySequence<T> sequence, **scoped** ReadOnlySpan<T> delimiters, bool advancePastDelimiter = true);
        public bool TryReadToAny(out ReadOnlySpan<T> span, **scoped** ReadOnlySpan<T> delimiters, bool advancePastDelimiter = true);
    }
}

namespace System.Text.Json
{
    public ref struct Utf8JsonReader
    {
        public **readonly** bool ValueTextEquals(ReadOnlySpan<byte> utf8Text);
        public **readonly** bool ValueTextEquals(ReadOnlySpan<char> text);
    }
}

Metadata

Metadata

Assignees

Labels

api-ready-for-reviewAPI is ready for review, it is NOT ready for implementationarea-System.Buffersarea-System.Text.JsonblockingMarks issues that we want to fast track in order to unblock other important work

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions