dotnet / dotnet/runtime

[API Proposal]: Safe ROS<byte> <--> primitive conversion APIs.

Open
#127,606 12 comments 0 reactions 0 assignees View on GitHub
api-suggestion area-System.Memory
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

> [!NOTE]
> Updated proposal — original `BinaryPrimitives` "CurrentEndian" suggestion moved to **Alternative Designs**.

### Background and motivation

We plan to make `MemoryMarshal.[Try]Read/[Try]Write/Cast` and most `Unsafe.*` APIs caller-unsafe, which means that if you need to convert a `ReadOnlySpan` into a primitive, or read a primitive out of a `ReadOnlySpan`, you will need to find another option.

Today the only safe replacement requires a verbose endian split:

```cs
// unsafe:
bool success = MemoryMarshal.TryRead(span, out int val);

// safe (today):
bool success;
int val;
if (BitConverter.IsLittleEndian)
{
success = BinaryPrimitives.TryReadInt32LittleEndian(span, out val);
}
else
{
success = BinaryPrimitives.TryReadInt32BigEndian(span, out val);
}
```

Per discussion below, instead of adding per-type "CurrentEndian" `BinaryPrimitives` overloads, we propose introducing a **marker interface** `IBitwiseConvertible` which states "every bit pattern is a legal value of `TSelf`". That contract is exactly what makes reinterpreting `ReadOnlySpan` ↔ `ReadOnlySpan` safe, and lets us add **safe, generic** replacements for `MemoryMarshal.Read/Write/TryRead/TryWrite/Cast/AsBytes` constrained on it (no `where T : struct` escape hatch, no per-type overload explosion, no `Unsafe.*`).

It is similar in spirit to the proposed `IBitwiseEquatable` (#75642).

### API Proposal

```diff
namespace System.Numerics
{
+ public partial interface IBitwiseConvertible
+ where TSelf : System.Numerics.IBitwiseConvertible?
+ {
+ }
}
```

Implemented by every CLR primitive (integral + floating-point). `bool` is intentionally **excluded** — not every byte pattern is a legal `bool` value.

```diff
+public readonly struct SByte : IBitwiseConvertible
+public readonly struct Byte : IBitwiseConvertible
+public readonly struct Int16 : IBitwiseConvertible
+public readonly struct UInt16 : IBitwiseConvertible
+public readonly struct Int32 : IBitwiseConvertible
+public readonly struct UInt32 : IBitwiseConvertible
+public readonly struct Int64 : IBitwiseConvertible
+public readonly struct UInt64 : IBitwiseConvertible
+public readonly struct IntPtr : IBitwiseConvertible
+public readonly struct UIntPtr : IBitwiseConvertible
+public readonly struct Int128 : IBitwiseConvertible
+public readonly struct UInt128 : IBitwiseConvertible
+public readonly struct Char : IBitwiseConvertible
+public readonly struct Half : IBitwiseConvertible
+public readonly struct BFloat16 : IBitwiseConvertible
+public readonly struct Single : IBitwiseConvertible
+public readonly struct Double : IBitwiseConvertible
```

Companion safe `MemoryMarshal` helpers enabled by the marker:

```diff
namespace System.Runtime.InteropServices
{
public static partial class MemoryMarshal
{
+ public static System.ReadOnlySpan AsBytes(System.ReadOnlySpan span)
+ where T : System.Numerics.IBitwiseConvertible { throw null; }
+ public static System.Span AsBytes(System.Span span)
+ where T : System.Numerics.IBitwiseConvertible { throw null; }
+
+ public static System.ReadOnlySpan BitCast(System.ReadOnlySpan span)
+ where TFrom : System.Numerics.IBitwiseConvertible
+ where TTo : System.Numerics.IBitwiseConvertible { throw null; }
+ public static System.Span BitCast(System.Span span)
+ where TFrom : System.Numerics.IBitwiseConvertible
+ where TTo : System.Numerics.IBitwiseConvertible { throw null; }
+
+ public static T Read(System.ReadOnlySpan source)
+ where T : System.Numerics.IBitwiseConvertible { throw null; }
+ public static bool TryRead(System.ReadOnlySpan source, out T value)
+ where T : System.Numerics.IBitwiseConvertible { throw null; }
+ public static void Write(System.Span destination, T value)
+ where T : System.Numerics.IBitwiseConvertible { }
+ public static bool TryWrite(System.Span destination, T value)
+ where T : System.Numerics.IBitwiseConvertible { throw null; }
}
}
```

### API Usage

```cs
// Safe, generic, no Unsafe / `where T : struct`:
bool ok = MemoryMarshal.TryRead(span, out int v);
ReadOnlySpan bytes = MemoryMarshal.AsBytes(floats);
ReadOnlySpan u16 = MemoryMarshal.BitCast(chars);
```

### Alternative Designs

1. **Add per-type "CurrentEndian" overloads to `BinaryPrimitives`** (the original proposal):

```diff
namespace System.Buffers.Binary
{
public static partial class BinaryPrimitives
{
+ public static System.Numerics.BFloat16 ReadBFloat16(System.ReadOnlySpan source) { throw null; }
+ public static double ReadDouble(System.ReadOnlySpan source) { throw null; }
+ public static System.Half ReadHalf(System.ReadOnlySpan source) { throw null; }
+ public static short ReadInt16(System.ReadOnlySpan source) { throw null; }
+ public static int ReadInt32(System.ReadOnlySpan source) { throw null; }
+ public static long ReadInt64(System.ReadOnlySpan source) { throw null; }
+ public static System.Int128 ReadInt128(System.ReadOnlySpan source) { throw null; }
+ public static nint ReadIntPtr(System.ReadOnlySpan source) { throw null; }
+ public static float ReadSingle(System.ReadOnlySpan source) { throw null; }
+ public static ushort ReadUInt16(System.ReadOnlySpan source) { throw null; }
+ public static uint ReadUInt32(System.ReadOnlySpan source) { throw null; }
+ public static ulong ReadUInt64(System.ReadOnlySpan source) { throw null; }
+ public static System.UInt128 ReadUInt128(System.ReadOnlySpan source) { throw null; }
+ public static nuint ReadUIntPtr(System.ReadOnlySpan source) { throw null; }
+ public static bool TryReadBFloat16(System.ReadOnlySpan source, out System.Numerics.BFloat16 value) { throw null; }
+ public static bool TryReadDouble(System.ReadOnlySpan source, out double value) { throw null; }
+ public static bool TryReadHalf(System.ReadOnlySpan source, out System.Half value) { throw null; }
+ public static bool TryReadInt16(System.ReadOnlySpan source, out short value) { throw null; }
+ public static bool TryReadInt32(System.ReadOnlySpan source, out int value) { throw null; }
+ public static bool TryReadInt64(System.ReadOnlySpan source, out long value) { throw null; }
+ public static bool TryReadInt128(System.ReadOnlySpan source, out System.Int128 value) { throw null; }
+ public static bool TryReadIntPtr(System.ReadOnlySpan source, out nint value) { throw null; }
+ public static bool TryReadSingle(System.ReadOnlySpan source, out float value) { throw null; }
+ public static bool TryReadUInt16(System.ReadOnlySpan source, out ushort value) { throw null; }
+ public static bool TryReadUInt32(System.ReadOnlySpan source, out uint value) { throw null; }
+ public static bool TryReadUInt64(System.ReadOnlySpan source, out ulong value) { throw null; }
+ public static bool TryReadUInt128(System.ReadOnlySpan source, out System.UInt128 value) { throw null; }
+ public static bool TryReadUIntPtr(System.ReadOnlySpan source, out nuint value) { throw null; }
+ public static bool TryWriteBFloat16(System.Span destination, System.Numerics.BFloat16 value) { throw null; }
+ public static bool TryWriteDouble(System.Span destination, double value) { throw null; }
+ public static bool TryWriteHalf(System.Span destination, System.Half value) { throw null; }
+ public static bool TryWriteInt16(System.Span destination, short value) { throw null; }
+ public static bool TryWriteInt32(System.Span destination, int value) { throw null; }
+ public static bool TryWriteInt64(System.Span destination, long value) { throw null; }
+ public static bool TryWriteInt128(System.Span destination, System.Int128 value) { throw null; }
+ public static bool TryWriteIntPtr(System.Span destination, nint value) { throw null; }
+ public static bool TryWriteSingle(System.Span destination, float value) { throw null; }
+ public static bool TryWriteUInt16(System.Span destination, ushort value) { throw null; }
+ public static bool TryWriteUInt32(System.Span destination, uint value) { throw null; }
+ public static bool TryWriteUInt64(System.Span destination, ulong value) { throw null; }
+ public static bool TryWriteUInt128(System.Span destination, System.UInt128 value) { throw null; }
+ public static bool TryWriteUIntPtr(System.Span destination, nuint value) { throw null; }
+ public static void WriteBFloat16(System.Span destination, System.Numerics.BFloat16 value) { }
+ public static void WriteDouble(System.Span destination, double value) { }
+ public static void WriteHalf(System.Span destination, System.Half value) { }
+ public static void WriteInt16(System.Span destination, short value) { }
+ public static void WriteInt32(System.Span destination, int value) { }
+ public static void WriteInt64(System.Span destination, long value) { }
+ public static void WriteInt128(System.Span destination, System.Int128 value) { }
+ public static void WriteIntPtr(System.Span destination, nint value) { }
+ public static void WriteSingle(System.Span destination, float value) { }
+ public static void WriteUInt16(System.Span destination, ushort value) { }
+ public static void WriteUInt32(System.Span destination, uint value) { }
+ public static void WriteUInt64(System.Span destination, ulong value) { }
+ public static void WriteUInt128(System.Span destination, System.UInt128 value) { }
+ public static void WriteUIntPtr(System.Span destination, nuint value) { }
}
}
```

Or **Extend `BitConverter`** with `TryRead`/`Read` overloads for all primitives.

### Risks

No response

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.