runtime: [API Proposal] Expose missing BinaryPrimitives APIs

System.Buffers.Binary.BinaryPrimitives exposes many APIs but there are few notable ones missing for core number types. We should extend the type to ensure that it supports the basic operations for all basic number types.

namespace System.Buffers.Binary;

public static partial class BinaryPrimitives
{
    // Read*BigEndian and Read*LittleEndian

    public static nint ReadIntPtrBigEndian(ReadOnlySpan<byte> source);
    public static nint ReadIntPtrLittleEndian(ReadOnlySpan<byte> source);

    public static Int128 ReadInt128BigEndian(ReadOnlySpan<byte> source);
    public static Int128 ReadInt128LittleEndian(ReadOnlySpan<byte> source);

    public static nuint ReadUIntPtrBigEndian(ReadOnlySpan<byte> source);
    public static nuint ReadUIntPtrLittleEndian(ReadOnlySpan<byte> source);

    public static UInt128 ReadUInt128BigEndian(ReadOnlySpan<byte> source);
    public static UInt128 ReadUInt128LittleEndian(ReadOnlySpan<byte> source);

    // ReverseEndianness

    public static nint ReverseEndianness(nint value);
    public static Int128 ReverseEndianness(Int128 value);
    public static nuint ReverseEndianness(nuint value);
    public static UInt128 ReverseEndianness(UInt128 value);

    // TryRead*BigEndian and TryRead*LittleEndian

    public static bool TryReadIntPtrBigEndian(ReadOnlySpan<byte> source, out nint value);
    public static bool TryReadIntPtrLittleEndian(ReadOnlySpan<byte> source, out nint value);

    public static bool TryReadInt128BigEndian(ReadOnlySpan<byte> source, out Int128 value);
    public static bool TryReadInt128LittleEndian(ReadOnlySpan<byte> source, out Int128 value);

    public static bool TryReadUIntPtrBigEndian(ReadOnlySpan<byte> source, out nuint value);
    public static bool TryReadUIntPtrLittleEndian(ReadOnlySpan<byte> source, out nuint value);

    public static bool TryReadUInt128BigEndian(ReadOnlySpan<byte> source, out UInt128 value);
    public static bool TryReadUInt128LittleEndian(ReadOnlySpan<byte> source, out UInt128 value);

    // TryWrite*BigEndian and TryWrite*LittleEndian

    public static bool TryWriteIntPtrBigEndian(ReadOnlySpan<byte> destination, nint value);
    public static bool TryWriteIntPtrLittleEndian(ReadOnlySpan<byte> destination, nint value);

    public static bool TryWriteInt128BigEndian(ReadOnlySpan<byte> destination, Int128 value);
    public static bool TryWriteInt128LittleEndian(ReadOnlySpan<byte> destination, Int128 value);

    public static bool TryWriteUIntPtrBigEndian(ReadOnlySpan<byte> destination, nuint value);
    public static bool TryWriteUIntPtrLittleEndian(ReadOnlySpan<byte> destination, nuint value);

    public static bool TryWriteUInt128BigEndian(ReadOnlySpan<byte> destination, UInt128 value);
    public static bool TryWriteUInt128LittleEndian(ReadOnlySpan<byte> destination, UInt128 value);

    // Write*BigEndian and Write*LittleEndian

    public static void WriteIntPtrBigEndian(ReadOnlySpan<byte> destination, nint value);
    public static void WriteIntPtrLittleEndian(ReadOnlySpan<byte> destination, nint value);

    public static void WriteInt128BigEndian(ReadOnlySpan<byte> destination, Int128 value);
    public static void WriteInt128LittleEndian(ReadOnlySpan<byte> destination, Int128 value);

    public static void WriteUIntPtrBigEndian(ReadOnlySpan<byte> destination, nuint value);
    public static void WriteUIntPtrLittleEndian(ReadOnlySpan<byte> destination, nuint value);

    public static void WriteUInt128BigEndian(ReadOnlySpan<byte> destination, UInt128 value);
    public static void WriteUInt128LittleEndian(ReadOnlySpan<byte> destination, UInt128 value);
}

Additional Notes

Also missing are some APIs around char, Half, float, and double. However, there are concerns around char not being a “core” numeric type and around possible normalization issues for the various floating-point types, so they aren’t included.

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 33 (33 by maintainers)

Most upvoted comments

I’m handling:

    public static nint ReverseEndianness(nint value);
    public static Int128 ReverseEndianness(Int128 value);
    public static nuint ReverseEndianness(nuint value);
    public static UInt128 ReverseEndianness(UInt128 value);

as part of #75901.

thinking that the following would not suffer from normalization issues?

Yes, however I do not think you would want to write this code this way, for the structure in your example at least.

would that mean void ReverseEndianness(ref readonly float value, out float result) would be a “safer” signature at all?

The signature of an API optimized for in-place swapping that you are trying to do here would be void ReverseEndianness(ref float value) => Unsafe.As<float,uint>(ref float) = ReverseEndianess(Unsafe.As<float,uint>(ref float)). You can implement this as a one-liner helper yourself if you have a system designed around in-place swapping.

I want to clarify that I’m not opposed to these APIs. (Or at least the char-based ones, which is the area I understand the best.)

If there’s a valid power-user scenario for them, I don’t want to stand in the way of enabling that. But from a code review perspective, if I were to see a PR come through which introduced a call to one of these APIs, it would immediately raise a red flag for me. “This call site is suspicious and the caller is probably doing something very subtly incorrect, and I should scrutinize this code very closely.”

There’s always some slight paranoia I feel whenever I see an API that lends itself to that, even if there are legitimate power user scenarios. 😃

A use case I typically have for a float/double ReverseEndianness is when I’m reading in a bunch of structured data at once, and then want to correct it later if I’m running on a big endian system:

struct DataBlock
{
    public int Field1;
    public float Field2;
    public float Field3;
}

// ...
var blocks = new DataBlock[count];
stream.ReadExactly(MemoryMarshal.AsBytes(blocks.AsSpan()));

if (!BitConverter.IsLittleEndian)
{
    for (int i = 0; i < blocks.Length; i++)
    {
        blocks[i].Field1 = BinaryPrimitives.ReverseEndianness(blocks[i].Field1);
        blocks[i].Field2 = BinaryPrimitives.ReverseEndianness(blocks[i].Field2);
        blocks[i].Field3 = BinaryPrimitives.ReverseEndianness(blocks[i].Field3);
    }
}

What would be the optimal alternative here? Changing the structure to use int/uint and exposing properties that use Int32BitsToSingle/SingleToInt32Bits?

It’s a case that does have endianness considerations in that there is explicitly both UTF-16 LE and UTF-16 BE so this will be “useful” for manual lexing/parsing scenarios.

Copying chars to/from byte streams is not the correct way to perform UTF-16 conversion. We have existing APIs in the framework that do this the correct way. I really don’t want people to think the APIs being proposed here are a blessed (or “fast”) way to do this.