3

Disclaimer - Since I have a working solution, this question perhaps crosses the line to code review, however I'm convinced I'm reinventing the wheel and a better solution exists.

Context

I am working with a low level communication protocol whereby I receive a byte[] as a serialized array of a known type. The data type will always be an unmanaged value type, typically UInt16, char etc.

Question

How can I (should I) generically convert from a byte[] to a T[] so as not to provide implementation for each case, or type specific converters?

Working Code

I have written an Extension Method, ToArray<T>, on byte[]:

public static T[] ToArray<T>(this byte[] input)
    where T: unmanaged
{
    // Use Reflection to find the appropiate MethodInfo from BitConverter
    var converterMethod =  (from method in typeof(BitConverter).GetMethods()
                            // Double redundant selection
                            where ((method.ReturnType == typeof(T)) && (method.Name == $"To{typeof(T).Name}"))
                            select method).FirstOrDefault();
            
    // Create a Function delegate from the MethodInfo, since all BitConverter.To methods share a signiture
    var converter = converterMethod.CreateDelegate(typeof(Func<byte[], int, T>));

    // Some meta variables regarding the target type
    int typeSize = Marshal.SizeOf<T>();
    int count = input.Length / typeSize;
            
    // Error Checking - Not yet implmented
    if (input.Length % typeSize != 0) throw new Exception();
            
    // Resulting array generation
    T[] result = new T[count];
    for(int i = 0; i < count; i++)
    {
        result[i] = (T)converter.DynamicInvoke(
            input.Slice(i * typeSize, typeSize), 0);
    }
    return result;
}

This also depends on another small extension, Slice<T>, on T[]:

public static T[] Slice<T>(this T[] array, int index, int count)
{
    T[] result = new T[count];
    for (int i = 0; i < count; i++) result[i] = array[index + i];
    return result;
}

Test Case

class Program
{
    static void Main(string[] args)
    {
        byte[] test = new byte[6]
        {
            0b_0001_0000, 0b_0010_0111, // 10,000 in Little Endian
            0b_0010_0000, 0b_0100_1110, // 20,000 in Little Endian
            0b_0011_0000, 0b_0111_0101, // 30,000 in Little Endian
        };

        UInt16[] results = test.ToArray<UInt16>();

        foreach (UInt16 result in results) Console.WriteLine(result);
    }
}

Output

10000
20000
30000
George Kerwood
  • 1,248
  • 8
  • 19
  • Do you actually need an array? You could do this with spans in 2 lines, and requires zero allocation/copy - you can coerce between blittable span types. Memory (span source) takes a *little* more work, but not much. – Marc Gravell Dec 12 '20 at 12:44
  • @MarcGravell Hello and thank you. I hadn't encountered spans before, looking briefly now I see how they replace my `Slice` extension. I'm afraid I don't see however how they would provide the generic conversion? – George Kerwood Dec 12 '20 at 12:50
  • see answer below – Marc Gravell Dec 12 '20 at 12:52

1 Answers1

2

Honestly, if this was me: I wouldn't get it as an array - I'd simply coerce between spans. An array is implicitly convertible to a span, so the input doesn't change. Span as an output is a different API, but very comparable in all ways except one (storage as a field).

Consider

public static Span<T> Coerce<T>(this byte[] input)
    where T: unmanaged
    => MemoryMarshal.Cast<byte, T>(input);

This is zero allocation and zero processing - it simply reinterprets the span over the existing data, which means it is fundamentally doing exactly what BitConverter does behind the scenes. There's also the concept of ReadOnlySpan<T> if the consumer needs to read but doesn't need to be able to write to the data. And spans allow you to work on portions of an array without needing to convey the bounds separately.

And if you can't use spans as the return, you can still use this approach for the code:

public static T[] Convert<T>(this byte[] input)
    where T: unmanaged
    => MemoryMarshal.Cast<byte, T>(input).ToArray();
Marc Gravell
  • 1,026,079
  • 266
  • 2,566
  • 2,900
  • That looks beautifully elegant, thank you! Really great stuff! – George Kerwood Dec 12 '20 at 12:55
  • 1
    @george (context: your question about arrays, now edited out) well, I added an extra bit at the bottom that might be what you want, but that is then an additional allocation / copy. That's the trade off if you want an array. You *can* create a custom `MemoryManager` to do this with zero copy, but that is more work - possibly 20 lines. – Marc Gravell Dec 12 '20 at 12:58
  • Thank you again, if I had more than one upvote to give, you'd get it! – George Kerwood Dec 12 '20 at 13:07