Need a workaround to access ReadOnlySpan<T> inside a function that returns an IEnumerable

Viewed 2330

while dealing with native code interop I decided it was the time to learn and try new Span features of the C# language. Although with many trials, everything was going perfectly fine until I come to the final stage of my very long function for which I insert below just a minimal reproducible sample:

    [DllImport(dll, SetLastError = true)]
    internal static extern void GetNativeData(out byte lpBuffer, int size, out bytesRead);

    ReadOnlySpan<T> ReadArray<T>(ReadOnlySpan<byte> buf, int Length) where T : unmanaged
    {
        var size = Length * Unsafe.SizeOf<T>();
        if (buf.Length < size)
            buf = new byte[size];
            GetNativeData(out MemoryMarshal.GetReference(buf), size, out int read));
            Dh.CreateError(ReadMemoryErr);
        
        return MemoryMarshal.Cast<byte, T>(buf.Slice(0, size));
    }

    static IEnumerable<MyClass> GetResult()
    {
        // Here I allocate a buffer
        Span<byte> buf = new byte[1000];

        // After a long serie of calls to unmanaged DLL functions I end up with something like this:
        ReadOnlySpan<uint> uintRes = data.ReadArray<uint>(buf, 10);
        ReadOnlySpan<ushort> shortRes = data.ReadArray<ushort>(buf, 10);
        for (int i = 0; i < uintRes.Length; i++)
        {
           // Any access to spans inside this loop result in Error CS4013
           string r = GetFunRes(uintRes[i]);
           IntPtr r2 = GetFunRes2(shortRes[i]);
           yield return new MyClass() { Prop1 = r, Prop2 = r2 };
        }
    }

The error I get is

Error CS4013: Instance of type 'Span' cannot be used inside a nested function, query expression, iterator block or async method

Now, I have read that there are workarounds for this. The articles only shows usage for async methods, but it also states that applies to iterators. Unfortunately I have not been able to make this work. I only need to read specific elements of the span and then, yield a result that does not contains any element or reference to the span. It's just that whatever I try to, as soon as I try to access something the compiler fails.

I have read about Memory<T>, may be that this one could work, but I have some concerns because I have read that the performance is dramatically reduced. People also recommend Span at the first place. I hope I can find a solution because otherwise I will have to restart from scratch my project and rewrite everything because now is tightly tied to Span.

Thanks in advance for your help

@00110001:

var uintRes = data.ReadArray<uint>(buf, 10).ToArray();

I know that this will work, but I suppose that it will make Span usage useless along with the new generics unmanaged features I am trying to take advantage of (ReadArray<T>). If I am not wrong, calling ToArray() it's the same of the old Marshaling style where a new copy is created for each call:

internal static extern void GetNativeData(out uint[] lpBuffer, int size, out bytesRead);

@Ian Kemp This is how I tried the workaround mentioned in the article:

int len = uintRes.Length;
for (int i = 0; i < len; i++)
{
   var res = ParseData(i);
   if (res == ExpectedResult())
      yield return res;
}
MyClass ParseData(int index)
{
     // CS8175: Cannot use ref local 'uintRes, shortRes' inside an anonymous method, lambda expression, or query expression
     string r = GetFunRes(uintRes[index]);
     IntPtr r2 = GetFunRes2(shortRes[index]);
     return new MyClass() { Prop1 = r, Prop2 = r2 };
}
1 Answers

I am going to mark the question as closed because I found the answer myself, by reading this article. Basically, in order to use IEnumerable with yield, Memory<T> is needed because Span<T> is a ref struct that is allocated on the stack and can't be used across yield boundaries. On the other side, Memory<T> can reside on heap while still providing Span<T> access. The author of the article shows the maximum that can be done with pure Span and the sample code required in order to enable IEnumerable with LINQ along with the benchmarks of the different implementations. IEnumerable+memory<T> is very slow compared to the other solutions, but for anyone who still want LINQ, I recommend to look at NetFabric.Hyperlinq, an alternative version of LINQ developed from the same author of the article in order to fill that gap. I am not sure if even with this solution will still persist a big difference with for/foreach loops combined with pure Span, but I suppose it mainly depends from the number of calls, type of queries etc (see LinqBenchmarks to see how each query performs). Thanks

P.s. I noticed that sometimes the server hosting the article is unreachable, therefore I include here a web archive capture to ensure that it will be always reachable in the future.

Related