Skip to content

Commit

Permalink
Add new methods for the people not using streams
Browse files Browse the repository at this point in the history
  • Loading branch information
Kermalis committed Aug 31, 2022
1 parent 9b04ae5 commit 46a72c9
Show file tree
Hide file tree
Showing 7 changed files with 193 additions and 23 deletions.
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,11 @@ For example, classes and structs in C# cannot have ignored members when marshall
The `EndianBinaryPrimitives` static class which resembles `System.Buffers.Binary.BinaryPrimitives` is an API that converts to/from data types using `Span<T>`/`ReadOnlySpan<T>` with specific endianness, rather than streams.

----
## Changelog For v2.0.0.0
Be sure to check the comment at https://github.com/Kermalis/EndianBinaryIO/pull/28!
## Changelog For v2.0.1
Check the comment on [the release page](https://github.com/Kermalis/EndianBinaryIO/releases/tag/v2.0.1)!

## Changelog For v2.0.0
Check the comment on [the release page](https://github.com/Kermalis/EndianBinaryIO/releases/tag/v2.0.0)!

----
## 🚀 Usage:
Expand Down
18 changes: 8 additions & 10 deletions Source/EndianBinaryIO.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<Title>EndianBinaryIO</Title>
<PackageId>EndianBinaryIO</PackageId>
<AssemblyName>EndianBinaryIO</AssemblyName>
<Version>2.0.0</Version>
<Version>2.0.1</Version>
<RepositoryUrl>https://github.com/Kermalis/EndianBinaryIO</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<Description>This .NET library provides a simple API to read/write bytes from/to streams and spans using user-specified endianness.
Expand All @@ -28,15 +28,13 @@ Project URL and Samples ― https://github.com/Kermalis/EndianBinaryIO</Descript
<PackageTags>Serialization;Reflection;Endianness;LittleEndian;BigEndian;EndianBinaryIO</PackageTags>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageLicenseFile>LICENSE.md</PackageLicenseFile>
<PackageReleaseNotes>* Rewritten with Span&lt;T&gt; and performance in mind. No allocations unless absolutely necessary
* The compiler will now inline certain methods. For example, ReadEnum&lt;TEnum&gt;() will only include code that will be executed for the given enum size. So passing a TEnum that is the size of a byte will condense down to just a ReadByte() call with no size/type checks
* Implemented reading and writing for Half, DateOnly, TimeOnly, Vector2, Vector3, Vector4, Quaternion, and Matrix4x4
* Removed bloated overloads (with array offset/count, alternate Encoding/BooleanSize, null termination, etc.). The reader/writer now respects its state (such as whether to use ASCII, and which BooleanSize to use) which you can change at any time
* decimal int order now matches with .net APIs
* Removed EndianBitConverter in favor of EndianBinaryPrimitives, which has similar API while using modern programming like Span&lt;T&gt;
* API uses nullable notations
* You can now ReadObject() and WriteObject() with primitives and other supported types like DateTime, Vector3, etc.
* Removed Encoding usage. The whole thing was very complicated before, and it barely functioned. Now you have ASCII and .net (UTF16-LE) support by default, and can add your own requirements either by extension methods or inheriting the reader/writer</PackageReleaseNotes>
<PackageReleaseNotes>Version 2.0.1 Changelog:
* Added TrimNullTerminators(ref char[] chars) and TrimNullTerminators(ref Span&lt;char&gt; chars) to EndianBinaryPrimitives which will remove all '\0' from the end
* Added ReadSBytes(ReadOnlySpan&lt;byte&gt; src, Span&lt;sbyte&gt; dest) and WriteSBytes(Span&lt;byte&gt; dest, ReadOnlySpan&lt;sbyte&gt; src) to EndianBinaryPrimitives
* Added heavily optimized enum methods to EndianBinaryPrimitives that use the same optimization techniques as the ones in EndianBinaryReader and EndianBinaryWriter
* Added PeekBytes(Span&lt;byte&gt; dest) to EndianBinaryReader

No breaking changes from v2.0.0</PackageReleaseNotes>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
Expand Down
158 changes: 158 additions & 0 deletions Source/EndianBinaryPrimitives.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System;
using System.Buffers.Binary;
using System.Numerics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Kermalis.EndianBinaryIO
{
Expand All @@ -19,8 +21,33 @@ public static int GetBytesForBooleanSize(BooleanSize boolSize)
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static void TrimNullTerminators(ref char[] chars)
{
int i = Array.IndexOf(chars, '\0');
if (i != -1)
{
Array.Resize(ref chars, i);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static void TrimNullTerminators(ref Span<char> chars)
{
int i = chars.IndexOf('\0');
if (i != -1)
{
chars = chars.Slice(0, i);
}
}

#region Read

[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static void ReadSBytes(ReadOnlySpan<byte> src, Span<sbyte> dest)
{
src.CopyTo(MemoryMarshal.Cast<sbyte, byte>(dest));
}

public static short ReadInt16(ReadOnlySpan<byte> src, Endianness endianness)
{
return endianness == Endianness.LittleEndian
Expand Down Expand Up @@ -196,6 +223,68 @@ public static void ReadBooleans(ReadOnlySpan<byte> src, Span<bool> dest, Endiann
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static TEnum ReadEnum<TEnum>(ReadOnlySpan<byte> src, Endianness endianness) where TEnum : unmanaged, Enum
{
int size = Unsafe.SizeOf<TEnum>();
if (size == 1)
{
byte b = src[0];
return Unsafe.As<byte, TEnum>(ref b);
}
if (size == 2)
{
ushort s = ReadUInt16(src, endianness);
return Unsafe.As<ushort, TEnum>(ref s);
}
if (size == 4)
{
uint i = ReadUInt32(src, endianness);
return Unsafe.As<uint, TEnum>(ref i);
}
ulong l = ReadUInt64(src, endianness);
return Unsafe.As<ulong, TEnum>(ref l);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static void ReadEnums<TEnum>(ReadOnlySpan<byte> src, Span<TEnum> dest, Endianness endianness) where TEnum : unmanaged, Enum
{
int size = Unsafe.SizeOf<TEnum>();
if (size == 1)
{
src.CopyTo(MemoryMarshal.Cast<TEnum, byte>(dest));
}
else if (size == 2)
{
ReadUInt16s(src, MemoryMarshal.Cast<TEnum, ushort>(dest), endianness);
}
else if (size == 4)
{
ReadUInt32s(src, MemoryMarshal.Cast<TEnum, uint>(dest), endianness);
}
else
{
ReadUInt64s(src, MemoryMarshal.Cast<TEnum, ulong>(dest), endianness);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static object ReadEnum(ReadOnlySpan<byte> src, Endianness endianness, Type enumType)
{
// Type.IsEnum is also false for base Enum type, so don't worry about it
Type underlyingType = Enum.GetUnderlyingType(enumType);
switch (Type.GetTypeCode(underlyingType))
{
case TypeCode.SByte: return Enum.ToObject(enumType, (sbyte)src[0]);
case TypeCode.Byte: return Enum.ToObject(enumType, src[0]);
case TypeCode.Int16: return Enum.ToObject(enumType, ReadInt16(src, endianness));
case TypeCode.UInt16: return Enum.ToObject(enumType, ReadUInt16(src, endianness));
case TypeCode.Int32: return Enum.ToObject(enumType, ReadInt32(src, endianness));
case TypeCode.UInt32: return Enum.ToObject(enumType, ReadUInt32(src, endianness));
case TypeCode.Int64: return Enum.ToObject(enumType, ReadInt64(src, endianness));
case TypeCode.UInt64: return Enum.ToObject(enumType, ReadUInt64(src, endianness));
}
throw new ArgumentOutOfRangeException(nameof(enumType), enumType, null);
}

public static DateTime ReadDateTime(ReadOnlySpan<byte> src, Endianness endianness)
{
return DateTime.FromBinary(ReadInt64(src, endianness));
Expand Down Expand Up @@ -330,6 +419,12 @@ public static void ReadMatrix4x4s(ReadOnlySpan<byte> src, Span<Matrix4x4> dest,

#region Write

[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static void WriteSBytes(Span<byte> dest, ReadOnlySpan<sbyte> src)
{
MemoryMarshal.Cast<sbyte, byte>(src).CopyTo(dest);
}

public static void WriteInt16(Span<byte> dest, short value, Endianness endianness)
{
if (endianness == Endianness.LittleEndian)
Expand Down Expand Up @@ -553,6 +648,69 @@ public static void WriteBooleans(Span<byte> dest, ReadOnlySpan<bool> src, Endian
}
}

[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static void WriteEnum<TEnum>(Span<byte> dest, TEnum value, Endianness endianness) where TEnum : unmanaged, Enum
{
int size = Unsafe.SizeOf<TEnum>();
if (size == 1)
{
dest[0] = Unsafe.As<TEnum, byte>(ref value);
}
else if (size == 2)
{
WriteUInt16(dest, Unsafe.As<TEnum, ushort>(ref value), endianness);
}
else if (size == 4)
{
WriteUInt32(dest, Unsafe.As<TEnum, uint>(ref value), endianness);
}
else
{
WriteUInt64(dest, Unsafe.As<TEnum, ulong>(ref value), endianness);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static void WriteEnums<TEnum>(Span<byte> dest, ReadOnlySpan<TEnum> src, Endianness endianness) where TEnum : unmanaged, Enum
{
int size = Unsafe.SizeOf<TEnum>();
if (size == 1)
{
MemoryMarshal.Cast<TEnum, byte>(src).CopyTo(dest);
}
else if (size == 2)
{
WriteUInt16s(dest, MemoryMarshal.Cast<TEnum, ushort>(src), endianness);
}
else if (size == 4)
{
WriteUInt32s(dest, MemoryMarshal.Cast<TEnum, uint>(src), endianness);
}
else
{
WriteUInt64s(dest, MemoryMarshal.Cast<TEnum, ulong>(src), endianness);
}
}
// #13 - Allow writing the abstract "Enum" type
// For example, EndianBinaryPrimitives.WriteEnum((Enum)Enum.Parse(enumType, value))
// Don't allow writing Enum[] though, since there is no way to read that
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static void WriteEnum(Span<byte> dest, Enum value, Endianness endianness)
{
Type underlyingType = Enum.GetUnderlyingType(value.GetType());
ref byte data = ref Utils.GetRawData(value); // Use memory tricks to skip object header of generic Enum
switch (Type.GetTypeCode(underlyingType))
{
case TypeCode.SByte:
case TypeCode.Byte: dest[0] = data; break;
case TypeCode.Int16:
case TypeCode.UInt16: WriteUInt16(dest, Unsafe.As<byte, ushort>(ref data), endianness); break;
case TypeCode.Int32:
case TypeCode.UInt32: WriteUInt32(dest, Unsafe.As<byte, uint>(ref data), endianness); break;
case TypeCode.Int64:
case TypeCode.UInt64: WriteUInt64(dest, Unsafe.As<byte, ulong>(ref data), endianness); break;
}
}

public static void WriteDateTime(Span<byte> dest, DateTime value, Endianness endianness)
{
WriteInt64(dest, value.ToBinary(), endianness);
Expand Down
10 changes: 9 additions & 1 deletion Source/EndianBinaryReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ public byte PeekByte()
Stream.Position = offset;
return buffer[0];
}
public void PeekBytes(Span<byte> dest)
{
long offset = Stream.Position;

ReadBytes(dest);

Stream.Position = offset;
}

public sbyte ReadSByte()
{
Expand Down Expand Up @@ -412,7 +420,7 @@ public char[] ReadChars_TrimNullTerminators(int charCount)
{
char[] chars = new char[charCount];
ReadChars(chars);
Utils.TrimNullTerminators(ref chars);
EndianBinaryPrimitives.TrimNullTerminators(ref chars);
return chars;
}
public string ReadString_Count(int charCount)
Expand Down
2 changes: 1 addition & 1 deletion Source/EndianBinaryWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ public void WriteEnums<TEnum>(ReadOnlySpan<TEnum> values) where TEnum : unmanage
}
}
// #13 - Allow writing the abstract "Enum" type
// For example, writer.Write((Enum)Enum.Parse(enumType, value))
// For example, writer.WriteEnum((Enum)Enum.Parse(enumType, value))
// Don't allow writing Enum[] though, since there is no way to read that
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public void WriteEnum(Enum value)
Expand Down
12 changes: 3 additions & 9 deletions Source/Utils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,13 @@ private sealed class RawData
{
public byte Data;
}
public static ref byte GetRawData(object value)
// This is a copy of what Enum uses internally
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static ref byte GetRawData<T>(T value) where T : class
{
return ref Unsafe.As<RawData>(value).Data; // Skip object header
}

public static void TrimNullTerminators(ref char[] chars)
{
int i = Array.IndexOf(chars, '\0');
if (i != -1)
{
Array.Resize(ref chars, i);
}
}
private static bool TryConvertToInt32(object? obj, out int value)
{
try
Expand Down
9 changes: 9 additions & 0 deletions Testing/BasicTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,5 +192,14 @@ public void WriteManually()

Assert.True(bytes.SequenceEqual(_bytes));
}

[Fact]
public void SpanIsProperlyTrimmed()
{
Span<char> test = stackalloc char[] { 'K', 'e', 'r', 'm', 'a', 'l', 'i', 's', '\0', '\0', };
EndianBinaryPrimitives.TrimNullTerminators(ref test);

Assert.True(test.SequenceEqual("Kermalis"));
}
}
}

0 comments on commit 46a72c9

Please sign in to comment.