Remove unsafe code from CborHelpers, Crc32, and BlobUtilities#126269
Remove unsafe code from CborHelpers, Crc32, and BlobUtilities#126269
Conversation
|
Tagging subscribers to this area: @dotnet/area-system-reflection-metadata |
There was a problem hiding this comment.
Pull request overview
This PR replaces several Unsafe.* / MemoryMarshal.*-based unaligned read/write patterns with System.Buffers.Binary.BinaryPrimitives (and BitConverter bit-conversion helpers) to keep the implementations safe while preserving behavior.
Changes:
- System.Reflection.Metadata: replace unaligned integer writes with
BinaryPrimitives.Write*Endianand remove unsafe double bit reinterpretation inWriteDouble(non-NET path). - System.IO.Hashing: replace
Unsafe.ReadUnaligned/MemoryMarshal.Castpatterns in CRC32(C) intrinsic/scalar paths withBinaryPrimitives.Read*LittleEndian. - System.Formats.Cbor: simplify big-endian half/double read/write helpers using
BinaryPrimitives.Read/Write*BigEndian.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| src/libraries/System.Reflection.Metadata/src/System/Reflection/Internal/Utilities/BlobUtilities.cs | Switches unaligned integer writes to BinaryPrimitives and uses BitConverter.DoubleToInt64Bits for safe double bit extraction in non-NET builds. |
| src/libraries/System.IO.Hashing/src/System/IO/Hashing/Crc32ParameterSet.WellKnown.cs | Removes Unsafe/MemoryMarshal usage in CRC32(C) update loops, using BinaryPrimitives reads instead. |
| src/libraries/System.Formats.Cbor/src/System/Formats/Cbor/CborHelpers.netstandard.cs | Replaces endian-swap + MemoryMarshal patterns for half/double big-endian helpers with BinaryPrimitives calls. |
...braries/System.Reflection.Metadata/src/System/Reflection/Internal/Utilities/BlobUtilities.cs
Show resolved
Hide resolved
src/libraries/System.IO.Hashing/src/System/IO/Hashing/Crc32ParameterSet.WellKnown.cs
Show resolved
Hide resolved
src/libraries/System.Formats.Cbor/src/System/Formats/Cbor/CborHelpers.cs
Show resolved
Hide resolved
This comment has been minimized.
This comment has been minimized.
...braries/System.Reflection.Metadata/src/System/Reflection/Internal/Utilities/BlobUtilities.cs
Show resolved
Hide resolved
src/libraries/System.IO.Hashing/src/System/IO/Hashing/Crc32ParameterSet.WellKnown.cs
Outdated
Show resolved
Hide resolved
src/libraries/System.Formats.Cbor/src/System/Formats/Cbor/CborHelpers.cs
Show resolved
Hide resolved
- Revert Crc32ParameterSet.WellKnown.cs changes (bounds check concern) - BlobUtilities: use BitConverter.DoubleToInt64Bits on all TFMs - CborHelpers: remove ReadDoubleBigEndian/WriteDoubleBigEndian wrappers, inline BinaryPrimitives calls at call sites with #if NET polyfill. Remove WriteHalfBigEndian wrapper, call BinaryPrimitives directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This comment has been minimized.
This comment has been minimized.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/libraries/System.Formats.Cbor/src/System/Formats/Cbor/CborHelpers.netstandard.cs:104
- PR description indicates
CborHelpersreplacesUnsafe.*andMemoryMarshal.*usage withBinaryPrimitivesequivalents, butCborHelpers.netstandard.csstill usesMemoryMarshal.Read/Write(e.g.,ReadSingleBigEndian/WriteSingleBigEndian) and containsunsafepointer casts for float bit conversions. If the PR goal is to fully remove these APIs/unsafe code fromCborHelpers, consider updating the remaining helpers (where possible for the target TFMs) or clarifying the scope in the PR description.
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static ushort ReadHalfBigEndian(ReadOnlySpan<byte> source)
{
return BinaryPrimitives.ReadUInt16BigEndian(source);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static float ReadSingleBigEndian(ReadOnlySpan<byte> source)
{
return BitConverter.IsLittleEndian ?
Int32BitsToSingle(BinaryPrimitives.ReverseEndianness(MemoryMarshal.Read<int>(source))) :
MemoryMarshal.Read<float>(source);
}
...braries/System.Reflection.Metadata/src/System/Reflection/Internal/Utilities/BlobUtilities.cs
Show resolved
Hide resolved
...braries/System.Reflection.Metadata/src/System/Reflection/Internal/Utilities/BlobUtilities.cs
Show resolved
Hide resolved
This comment has been minimized.
This comment has been minimized.
| @@ -135,28 +117,6 @@ public static void WriteSingleBigEndian(Span<byte> destination, float value) | |||
| } | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
| extension(BinaryPrimitives) | |
| { | |
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |
| public static double ReadDoubleBigEndian(ReadOnlySpan<byte> source) | |
| { | |
| return BitConverter.Int64BitsToDouble(BinaryPrimitives.ReadInt64BigEndian(source); | |
| } | |
| } |
Can we convert all polyfills in this type to extension methods like this, so that the actual source can use all modern APIs directly without unnecessary wrappers?
| EnsureWriteCapacity(1 + sizeof(double)); | ||
| WriteInitialByte(new CborInitialByte(CborMajorType.Simple, CborAdditionalInfo.Additional64BitData)); | ||
| CborHelpers.WriteDoubleBigEndian(_buffer.AsSpan(_offset), value); | ||
| BinaryPrimitives.WriteInt64BigEndian(_buffer.AsSpan(_offset), BitConverter.DoubleToInt64Bits(value)); |
There was a problem hiding this comment.
| BinaryPrimitives.WriteInt64BigEndian(_buffer.AsSpan(_offset), BitConverter.DoubleToInt64Bits(value)); | |
| BinaryPrimitives.WriteDoubleBigEndian(_buffer.AsSpan(_offset), value); |
Address jkotas feedback: convert CborHelpers Single/Double wrappers to extension(BinaryPrimitives) polyfills so callers can use BinaryPrimitives.Read/WriteSingle/DoubleBigEndian directly on all TFMs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| @@ -27,21 +27,5 @@ public static string BuildStringFromIndefiniteLengthTextString<TState>(int lengt | |||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | |||
There was a problem hiding this comment.
Can we go further and delete this file completely?
| return BitConverter.IsLittleEndian ? | ||
| CborHelpers.Int32BitsToSingle(BinaryPrimitives.ReverseEndianness(MemoryMarshal.Read<int>(source))) : | ||
| MemoryMarshal.Read<float>(source); |
There was a problem hiding this comment.
| return BitConverter.IsLittleEndian ? | |
| CborHelpers.Int32BitsToSingle(BinaryPrimitives.ReverseEndianness(MemoryMarshal.Read<int>(source))) : | |
| MemoryMarshal.Read<float>(source); | |
| return CborHelpers.Int32BitsToSingle(BinaryPrimitives.ReadInt32BigEndian(source)); |
| public static void WriteSingleBigEndian(Span<byte> destination, float value) | ||
| { | ||
| MemoryMarshal.Write(destination, ref value); | ||
| if (BitConverter.IsLittleEndian) |
There was a problem hiding this comment.
| BinaryPrimitives.WriteSingleLittleEndian(buffer.AsSpan(start), value); |
| MemoryMarshal.Write(destination, ref value); | ||
| } | ||
| } | ||
| internal static uint SingleToUInt32Bits(float value) |
There was a problem hiding this comment.
Inline the unsafe casts into the BinaryPrimitives extensions directly?
This comment has been minimized.
This comment has been minimized.
…ves directly - CborHelpers: simplify ReadSingleBigEndian/WriteSingleBigEndian extensions to use ReadInt32BigEndian/WriteInt32BigEndian with inlined unsafe casts, removing MemoryMarshal.Read/Write and ReverseEndianness - BlobUtilities: use BinaryPrimitives.WriteSingle/DoubleLittleEndian on NET, keep existing fallback on netstandard2.0/net462 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
src/libraries/System.Formats.Cbor/src/System/Formats/Cbor/CborHelpers.cs
Show resolved
Hide resolved
This comment has been minimized.
This comment has been minimized.
Move all TFM-specific CborHelpers methods into one file with #if NET guards. Delete the netcoreapp-only partial class file. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| namespace System.Formats.Cbor | ||
| { | ||
| internal static partial class CborHelpers | ||
| internal static class CborHelpers |
There was a problem hiding this comment.
I did not mean to add a bunch of ifdefs to the common file.
I meant to:
- Delete CborHelpers.netcoreapp.cs
- Use the netcoreapp APIs directly throughout the code
- Add extensions to CborHelpers.netstandard.cs to polyfill the netcoreapp APIs
There was a problem hiding this comment.
Thanks, I did have that in mind, in my workflow I just ask AI to address the feedback and then change it. It sometimes proactively pushes it before I change it, but I assumed it's fine since the PR is in draft 🙂 Still, I appreciate the early feedback!
This comment has been minimized.
This comment has been minimized.
…n APIs on NET - Add extension(BitConverter) polyfills for SingleToInt32Bits, Int32BitsToSingle, SingleToUInt32Bits, UInt32BitsToSingle on netstandard - Update HalfHelpers and CborWriter.Simple to use BitConverter.* directly - Keep CborHelpers wrappers for UnixEpoch, BigInteger, decimal.GetBits, string.Create (different signatures per TFM, not pure polyfills) - Guard netstandard-only polyfills with #if !NET, add NET versions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (2)
src/libraries/System.Formats.Cbor/src/System/Formats/Cbor/CborHelpers.cs:11
using System.Buffers;andusing System.Runtime.InteropServices;are not used anywhere in this file, which will produce unused-using warnings (treated as errors in this repo). Please remove the unused using directives.
src/libraries/System.Formats.Cbor/src/System/Formats/Cbor/CborHelpers.cs:179- The
BinaryPrimitivesPolyfills/BitConverterPolyfillsblocks introduce newunsafecode and rely onextension(Type)syntax. This undermines the PR goal of removing unsafe code, and the extension-member syntax is not used elsewhere in the repo (making it risky/unclear for multi-target builds). SinceBinaryPrimitives.Read/WriteSingle/Double*Endianalready exist in System.Memory (used in netstandard builds), please remove these polyfills and call the existingBinaryPrimitivesAPIs directly; for bit conversions, keep the previous internal helper methods instead of extendingBitConverter.
| result = HalfHelpers.FloatToHalf(value); | ||
| return float.IsNaN(value) || CborHelpers.SingleToInt32Bits(HalfHelpers.HalfToFloat(result)) == CborHelpers.SingleToInt32Bits(value); | ||
| return float.IsNaN(value) || BitConverter.SingleToInt32Bits(HalfHelpers.HalfToFloat(result)) == BitConverter.SingleToInt32Bits(value); | ||
| } |
There was a problem hiding this comment.
On non-.NETCoreApp targets (netstandard2.0 / .NET Framework), BitConverter.SingleToInt32Bits is not available (the test helper for netstandard uses an unsafe bitcast for this reason). This change will break compilation unless you provide a valid polyfill; consider restoring the previous internal helper (e.g., CborHelpers.SingleToInt32Bits) or comparing bit patterns via BinaryPrimitives-based conversions instead.
| @@ -62,7 +62,7 @@ public static float HalfToFloat(ushort value) | |||
| return CreateSingle(sign, (byte)(exp + 0x70), sig << 13); | |||
|
|
|||
| static float CreateSingle(bool sign, byte exp, uint sig) | |||
| => CborHelpers.Int32BitsToSingle((int)(((sign ? 1U : 0U) << FloatSignShift) + ((uint)exp << FloatExponentShift) + sig)); | |||
| => BitConverter.Int32BitsToSingle((int)(((sign ? 1U : 0U) << FloatSignShift) + ((uint)exp << FloatExponentShift) + sig)); | |||
| } | |||
There was a problem hiding this comment.
BitConverter.UInt32BitsToSingle, BitConverter.Int32BitsToSingle, and BitConverter.SingleToUInt32Bits are not available on the netstandard2.0 / .NET Framework TFMs that this file targets, so these replacements will fail to compile without a robust polyfill. Please keep using the existing internal conversion helpers (previously on CborHelpers) or implement local bit-conversion helpers for these TFMs.
🤖 Copilot Code Review — PR #126269Note This review was generated by GitHub Copilot and has not been manually verified. Holistic AssessmentMotivation: Well-justified. Replacing manual Approach: Clean. The C# 14 Summary: ✅ LGTM. The changes are semantically equivalent, correctly constrained to internal helpers, and address all maintainer review feedback. No blocking issues found. Two minor observations below. Detailed Findings✅ Correctness — All BinaryPrimitives replacements are semantically equivalentVerified each transformation:
✅ Extension polyfill design — Correct and consistent with maintainer guidanceThe The remaining ✅ File consolidation — CborHelpers merge is correctDeleting ✅ No public API surface changesAll changed types are 💡 Unused
|
|
I'll split this PR into smaller ones since it now does more than I initially wanted (to remove unsafe) also it is getting harder to review. Also, I should probably rely less on AI on addressing feedback 😥 |
> [!NOTE] > This PR description was AI/Copilot-generated. Extracted from #126269 I wonder if we should move all Polyfils for NS/NETF to just one common place for all libraries. Currently, libs just define their own in-place. --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Note
This PR was generated with the assistance of GitHub Copilot.
Summary
Replace Unsafe.* and MemoryMarshal.* API usage with safe BinaryPrimitives equivalents in three libraries:
System.Formats.Cbor — CborHelpers.netstandard.cs
ReadHalfBigEndian: MemoryMarshal.Read + manual endian swap →BinaryPrimitives.ReadUInt16BigEndianWriteHalfBigEndian: MemoryMarshal.Write + manual endian swap →BinaryPrimitives.WriteUInt16BigEndianReadDoubleBigEndian: MemoryMarshal.Read + ReverseEndianness →BinaryPrimitives.ReadInt64BigEndianWriteDoubleBigEndian: MemoryMarshal.Write + ReverseEndianness →BinaryPrimitives.WriteInt64BigEndianSystem.IO.Hashing — Crc32ParameterSet.WellKnown.cs
UpdateScalarArm64,UpdateScalarArm: Unsafe.ReadUnaligned → BinaryPrimitives.ReadUInt64/32LittleEndianUpdateIntrinsic(SSE path): MemoryMarshal.Cast<byte, ulong/uint> → explicit for-loops with BinaryPrimitivesUpdateIntrinsic(ARM path): Unsafe.ReadUnaligned → BinaryPrimitives.ReadUInt64/32LittleEndianSystem.Reflection.Metadata — BlobUtilities.cs
BinaryPrimitives.Write*LittleEndian/BigEndianWriteDouble(#else branch): pointer cast*(ulong*)&value→BitConverter.DoubleToInt64BitsAll changes are behavioral equivalents — no public API changes. All existing tests pass.