Search code examples
c#.net-coreclr

Passing both stackalloc'ed Span<T> and by-ref struct as arguments


I am using a SequenceReader<T> to copy contents from a ReadOnlySequence<T> into different Span<T>. However, I get a compiler error when I try to encapsulate the copy logic into a separate function and want to call it with a Span<T> allocated on the stack.

var sequence = new ReadOnlySequence<byte>(new byte[20]); // Source data to copy

Span<byte> heap = new byte[10]; // Target location 1
Span<byte> stack = stackalloc byte[10]; // Target location 2

var reader = new SequenceReader<byte>(sequence);

TryReadInto(ref reader, heap); // This is fine
TryReadInto(ref reader, stack); // Gives compile time error CS8350

Since the instance method TryCopyTo on SequenceReader<T> does not advance the reader, I have created a higher level function that takes care of that:

[MethodImpl(MethodImplOptions.AggressiveInlining)]
static bool TryReadInto(ref SequenceReader<byte> reader, Span<byte> destination)
{
    if (reader.TryCopyTo(destination)) {
        reader.Advance(destination.Length);
        return true;
    }

    return false;
}

As long as the destination is heap allocated everything works fine. But when the destination is allocated using stackalloc the compiler complains with a CS8350: This combination of arguments to is disallowed because it may expose variables referenced by parameter outside of their declaration scope.

When I do not pass the reader as reference that compiler error goes away, but it is required for the reader to be passed as reference since SequenceReader<T> is a mutable struct and the position is advanced within the function.

I get the compiler error. Parameter reader could be allocated in an earlier stack frame and as a ref-like struct would be able to have a Span<T> as a field. The method body could make the second (stack-allocated) parameter destination escape it's own stack frame, thus a function call with both a ref-parameter and a stack-allocated Span<T> is illegal in C#.

However, how am I able to encapsulate my logic then? SequenceReader<T> is a closed type so I can't just add an instance method. When I just inline the function call manually I do not get compiler errors. I guess the compiler sees that the reader is allocated in the same stack frame as the stack-allocated Span<byte>. Passing the reader by value is not an options since SequenceReader<T> is a mutable struct.


Solution

  • You have seen the problem the compiler is trying to prevent.

    If I translate what you described in question into C#, the code might look like this:

    // This is a struct lives on stack
    ref struct SomeStructOnStack
    {
        public Span<byte> Something;
    }
    
    // This method saves "stack" to the "obj"
    static void SomeMethod(ref SomeStructOnStack obj, Span<byte> stack)
    {
        obj = new SomeStructOnStack
        {
            Something = stack
        };
    }
    
    // This method accepts byref "obj" from its caller
    // In such case, "stack" will escape its scope
    // This is what CS8350 is for
    void Test(ref SomeStructOnStack obj)
    {
        Span<byte> stack = stackalloc byte[20];
        SomeMethod(ref obj, stack);
    }
    

    I do agree with you when the ref struct is in the same stack frame where stack is, we shouldn't see the compiler error. But this is not currently supported.

    To workaround this issue, you could create a copy of the Span<byte> like this:

    Span<byte> stack = stackalloc byte[20];
    
    unsafe
    {
        fixed (byte* pStack = stack)
        {
            var copy = new Span<byte>(pStack, stack.Length);
            TryReadInto(ref reader, copy);
        }
    }