From edaf32f9c14a24529924b3b6af38bbd55872606a Mon Sep 17 00:00:00 2001 From: RejectKid Date: Wed, 22 Jul 2026 01:09:29 -0400 Subject: [PATCH 1/2] Add opt-in unsafe generated accessors --- .../BitsKitBenchmark.GeneratedAccessors.cs | 114 +++++++ .../GeneratedAccessorBenchmarkModels.cs | 24 ++ BitsKit.Generator/Enums.cs | 6 + BitsKit.Generator/Models/BitFieldModel.cs | 123 ++++++++ BitsKit.Generator/Models/BooleanFieldModel.cs | 6 + BitsKit.Generator/Models/EnumFieldModel.cs | 6 + .../Models/IntegralFieldModel.cs | 6 + BitsKit.Generator/TypeSymbolProcessor.cs | 13 + BitsKit.Tests/GeneratorTests.Models.cs | 52 +++ BitsKit.Tests/UnsafeAccessTests.cs | 202 ++++++++++++ BitsKit/BitFields/BitObjectAccessMode.cs | 23 ++ BitsKit/BitFields/BitObjectAttribute.cs | 9 + BitsKit/Primitives/BitPrimitives.cs | 16 +- BitsKit/Primitives/UnsafeBitPrimitives.cs | 297 ++++++++++++++++++ CHANGELOG.md | 5 + README.md | 22 +- 16 files changed, 915 insertions(+), 9 deletions(-) create mode 100644 BitsKit.Tests/UnsafeAccessTests.cs create mode 100644 BitsKit/BitFields/BitObjectAccessMode.cs create mode 100644 BitsKit/Primitives/UnsafeBitPrimitives.cs diff --git a/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs b/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs index 50a6759..0b98e8b 100644 --- a/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs +++ b/BitsKit.Benchmarks/BitsKitBenchmark.GeneratedAccessors.cs @@ -12,6 +12,8 @@ public partial class BitsKitBenchmark private readonly GeneratedAccessorLsbModel[] _generatedAccessorSetModels = CreateGeneratedAccessorModels(); private readonly GeneratedAccessorMemoryModel[] _generatedAccessorMemoryModels = CreateGeneratedAccessorMemoryModels(); private readonly GeneratedAccessorAlignedMemoryModel[] _generatedAccessorAlignedMemoryModels = CreateGeneratedAccessorAlignedMemoryModels(); + private readonly GeneratedAccessorCheckedAccessModel[] _generatedAccessorCheckedAccessModels = CreateGeneratedAccessorCheckedAccessModels(); + private readonly GeneratedAccessorUnsafeAccessModel[] _generatedAccessorUnsafeAccessModels = CreateGeneratedAccessorUnsafeAccessModels(); private readonly byte[][] _generatedAccessorAlignedSpanBuffers = CreateGeneratedAccessorAlignedSpanBuffers(); private readonly GeneratedAccessorInlineArrayModel[] _generatedAccessorInlineArrayModels = CreateGeneratedAccessorInlineArrayModels(); private readonly GeneratedAccessorAlignedInlineArrayModel[] _generatedAccessorAlignedInlineArrayModels = CreateGeneratedAccessorAlignedInlineArrayModels(); @@ -292,6 +294,82 @@ public ulong GeneratedAccessorSetAlignedMemoryUInt64LSB() return BitConverter.ToUInt64(_generatedAccessorAlignedMemoryModels[0].UInt64BackingField.Span); } + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "CheckedAccess", "Memory", "Get", "20", "LSB")] + public uint GeneratedAccessorAccessComparisonCheckedGetMemory20LSB() + { + uint sum = 0; + for (int i = 0; i < AccessorOperations; i++) + sum += _generatedAccessorCheckedAccessModels[i & AccessorModelMask].Value20; + return sum; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "UnsafeAccess", "Memory", "Get", "20", "LSB")] + public uint GeneratedAccessorAccessComparisonUnsafeGetMemory20LSB() + { + uint sum = 0; + for (int i = 0; i < AccessorOperations; i++) + sum += _generatedAccessorUnsafeAccessModels[i & AccessorModelMask].Value20; + return sum; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "CheckedAccess", "Memory", "Set", "20", "LSB")] + public uint GeneratedAccessorAccessComparisonCheckedSetMemory20LSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorCheckedAccessModels[i & AccessorModelMask].Value20 = (uint)i; + return BitConverter.ToUInt32(_generatedAccessorCheckedAccessModels[0].Value20BackingField.Span); + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "UnsafeAccess", "Memory", "Set", "20", "LSB")] + public uint GeneratedAccessorAccessComparisonUnsafeSetMemory20LSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorUnsafeAccessModels[i & AccessorModelMask].Value20 = (uint)i; + return BitConverter.ToUInt32(_generatedAccessorUnsafeAccessModels[0].Value20BackingField.Span); + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "CheckedAccess", "Memory", "Boolean", "Get", "LSB")] + public int GeneratedAccessorAccessComparisonCheckedGetMemoryBooleanLSB() + { + int count = 0; + for (int i = 0; i < AccessorOperations; i++) + count += _generatedAccessorCheckedAccessModels[i & AccessorModelMask].Flag ? 1 : 0; + return count; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "UnsafeAccess", "Memory", "Boolean", "Get", "LSB")] + public int GeneratedAccessorAccessComparisonUnsafeGetMemoryBooleanLSB() + { + int count = 0; + for (int i = 0; i < AccessorOperations; i++) + count += _generatedAccessorUnsafeAccessModels[i & AccessorModelMask].Flag ? 1 : 0; + return count; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "CheckedAccess", "Memory", "Boolean", "Set", "LSB")] + public byte GeneratedAccessorAccessComparisonCheckedSetMemoryBooleanLSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorCheckedAccessModels[i & AccessorModelMask].Flag = (i & 1) != 0; + return _generatedAccessorCheckedAccessModels[0].BooleanBackingField.Span[0]; + } + + [Benchmark(OperationsPerInvoke = AccessorOperations)] + [BenchmarkCategory("GeneratedAccessor", "UnsafeAccess", "Memory", "Boolean", "Set", "LSB")] + public byte GeneratedAccessorAccessComparisonUnsafeSetMemoryBooleanLSB() + { + for (int i = 0; i < AccessorOperations; i++) + _generatedAccessorUnsafeAccessModels[i & AccessorModelMask].Flag = (i & 1) != 0; + return _generatedAccessorUnsafeAccessModels[0].BooleanBackingField.Span[0]; + } + [Benchmark(OperationsPerInvoke = AccessorOperations)] [BenchmarkCategory("GeneratedAccessor", "Span", "Aligned", "Get", "UInt32", "LSB")] public uint GeneratedAccessorGetAlignedSpanUInt32LSB() @@ -462,6 +540,42 @@ private static GeneratedAccessorAlignedMemoryModel[] CreateGeneratedAccessorAlig return models; } + private static GeneratedAccessorCheckedAccessModel[] CreateGeneratedAccessorCheckedAccessModels() + { + var models = new GeneratedAccessorCheckedAccessModel[AccessorModelCount]; + + for (int i = 0; i < models.Length; i++) + { + byte[] value20 = new byte[16]; + byte[] boolean = new byte[16]; + uint value = unchecked((uint)i * 0x9E3779B9u); + BitConverter.TryWriteBytes(value20, value); + boolean[0] = (byte)value; + models[i].Value20BackingField = value20; + models[i].BooleanBackingField = boolean; + } + + return models; + } + + private static GeneratedAccessorUnsafeAccessModel[] CreateGeneratedAccessorUnsafeAccessModels() + { + var models = new GeneratedAccessorUnsafeAccessModel[AccessorModelCount]; + + for (int i = 0; i < models.Length; i++) + { + byte[] value20 = new byte[16]; + byte[] boolean = new byte[16]; + uint value = unchecked((uint)i * 0x9E3779B9u); + BitConverter.TryWriteBytes(value20, value); + boolean[0] = (byte)value; + models[i].Value20BackingField = value20; + models[i].BooleanBackingField = boolean; + } + + return models; + } + private static byte[][] CreateGeneratedAccessorAlignedSpanBuffers() { var buffers = new byte[AccessorModelCount][]; diff --git a/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs b/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs index ce6edad..6da7698 100644 --- a/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs +++ b/BitsKit.Benchmarks/GeneratedAccessorBenchmarkModels.cs @@ -88,6 +88,30 @@ public partial struct GeneratedAccessorAlignedMemoryModel public Memory UInt64BackingField; } +[BitObject(BitOrder.LeastSignificant)] +public partial struct GeneratedAccessorCheckedAccessModel +{ + [BitField(3)] + [BitField("Value20", 20, BitFieldType.UInt32)] + public Memory Value20BackingField; + + [BitField(5)] + [BooleanField("Flag")] + public Memory BooleanBackingField; +} + +[BitObject(BitOrder.LeastSignificant, AccessMode = BitObjectAccessMode.Unsafe)] +public partial struct GeneratedAccessorUnsafeAccessModel +{ + [BitField(3)] + [BitField("Value20", 20, BitFieldType.UInt32)] + public Memory Value20BackingField; + + [BitField(5)] + [BooleanField("Flag")] + public Memory BooleanBackingField; +} + [BitObject(BitOrder.LeastSignificant)] public ref partial struct GeneratedAccessorAlignedSpanModel { diff --git a/BitsKit.Generator/Enums.cs b/BitsKit.Generator/Enums.cs index cd10e63..9b98d5f 100644 --- a/BitsKit.Generator/Enums.cs +++ b/BitsKit.Generator/Enums.cs @@ -55,6 +55,12 @@ internal enum BitOrder MostSignificant } +internal enum BitObjectAccessMode +{ + Checked, + Unsafe +} + /// /// An enumeration listing all of the options to modify bit-field generation /// diff --git a/BitsKit.Generator/Models/BitFieldModel.cs b/BitsKit.Generator/Models/BitFieldModel.cs index 26adf9a..d0d4559 100644 --- a/BitsKit.Generator/Models/BitFieldModel.cs +++ b/BitsKit.Generator/Models/BitFieldModel.cs @@ -18,6 +18,7 @@ internal abstract record BitFieldModel public BitFieldModifiers Modifiers { get; } private readonly bool _containingTypeIsStruct; + protected bool UsesUnsafeAccess { get; } public BitFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeSymbol) { @@ -28,6 +29,7 @@ public BitFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeSymbo _containingTypeIsStruct = typeSymbol.IsStruct; BitOrder = typeSymbol.DefaultBitOrder; + UsesUnsafeAccess = typeSymbol.AccessMode == BitObjectAccessMode.Unsafe; } for (int i = 0; i < attributeData.NamedArguments.Length; i++) @@ -181,6 +183,127 @@ BackingFieldType.Span or _ => throw new NotSupportedException() }; + /// + /// Creates an unchecked read through a raw reference for opted-in byte-addressable storage. + /// + protected bool TryGetUnsafeReadExpression(out string expression) + { + expression = string.Empty; + if (UsesFasterCheckedStorageSpecialization()) + return false; + + if (!TryGetUnsafeStorageReference(writable: false, out string source)) + return false; + + expression = $"UnsafeBitPrimitives.Read{FieldType!.Value.ToIntegralName()}{BitOrder.ToShortName()}" + + $"({source}, {BitOffset}, {BitCount})"; + return true; + } + + /// + /// Creates an unchecked write through a raw reference for opted-in byte-addressable storage. + /// + protected bool TryGetUnsafeWriteExpression(string valueExpression, out string expression) + { + expression = string.Empty; + if (UsesFasterCheckedStorageSpecialization()) + return false; + + if (!TryGetUnsafeStorageReference(writable: true, out string destination)) + return false; + + expression = $"UnsafeBitPrimitives.Write{FieldType!.Value.ToIntegralName()}{BitOrder.ToShortName()}" + + $"({destination}, {BitOffset}, unchecked(({FieldType.Value})({valueExpression})), {BitCount})"; + return true; + } + + /// + /// Creates an unchecked single-bit read through a raw reference. + /// + protected bool TryGetUnsafeBooleanReadExpression(out string expression) + { + expression = string.Empty; + if (!TryGetUnsafeStorageTarget(writable: false, out string source)) + return false; + + int byteOffset = BitOffset >> 3; + int bitInByte = BitOffset & 7; + int mask = 1 << (BitOrder == BitOrder.MostSignificant ? 7 - bitInByte : bitInByte); + string target = byteOffset == 0 + ? source + : $"System.Runtime.CompilerServices.Unsafe.Add(ref {source}, {byteOffset})"; + expression = $"({target} & 0x{mask:X2}) != 0"; + return true; + } + + /// + /// Creates an unchecked single-bit write through a raw reference. + /// + protected bool TryGetUnsafeBooleanWriteTemplate(out string template) + { + template = string.Empty; + if (!TryGetUnsafeStorageTarget(writable: true, out string destination)) + return false; + + int byteOffset = BitOffset >> 3; + int bitInByte = BitOffset & 7; + int mask = 1 << (BitOrder == BitOrder.MostSignificant ? 7 - bitInByte : bitInByte); + string target = byteOffset == 0 + ? destination + : $"System.Runtime.CompilerServices.Unsafe.Add(ref {destination}, {byteOffset})"; + template = + "{0} {1}\n" + + "{{\n" + + $" ref Byte target = ref {target};\n" + + " if (value)\n" + + $" target |= 0x{mask:X2};\n" + + " else\n" + + $" target &= 0x{255 ^ mask:X2};\n" + + "}}"; + return true; + } + + private bool TryGetUnsafeStorageReference(bool writable, out string reference) + { + reference = string.Empty; + if (!TryGetUnsafeStorageTarget(writable, out string target)) + return false; + + reference = $"ref {target}"; + return true; + } + + private bool TryGetUnsafeStorageTarget(bool writable, out string target) + { + target = string.Empty; + if (!UsesUnsafeAccess || BackingFieldType == BackingFieldType.Integral) + return false; + + string source = BackingField.TypeString == "byte[]" + ? writable ? "((Span){4})" : "((ReadOnlySpan){4})" + : writable ? SetterSource() : GetterSource(); + target = $"MemoryMarshal.GetReference({source})"; + return true; + } + + private bool UsesFasterCheckedStorageSpecialization() + { + int width = FieldType switch + { + BitFieldType.SByte or BitFieldType.Byte => 8, + BitFieldType.Int16 or BitFieldType.UInt16 => 16, + BitFieldType.Int32 or BitFieldType.UInt32 => 32, + BitFieldType.Int64 or BitFieldType.UInt64 => 64, + _ => 0 + }; + + return UsesUnsafeAccess && + BackingFieldType is BackingFieldType.Memory or BackingFieldType.Span or BackingFieldType.InlineArray && + width != 0 && + BitCount == width && + (BitOffset & 7) == 0; + } + /// /// Creates a direct endian-aware read for byte-aligned, full-width byte storage. /// diff --git a/BitsKit.Generator/Models/BooleanFieldModel.cs b/BitsKit.Generator/Models/BooleanFieldModel.cs index 3883eef..0a6b8a2 100644 --- a/BitsKit.Generator/Models/BooleanFieldModel.cs +++ b/BitsKit.Generator/Models/BooleanFieldModel.cs @@ -30,6 +30,9 @@ public BooleanFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeS protected override string GetGetterTemplate() { + if (TryGetUnsafeBooleanReadExpression(out string unsafeExpression)) + return "{0} {1} => " + unsafeExpression + ";"; + if (TryGetDirectStorageBooleanReadExpression(out string expression) || TryGetDirectIntegralBooleanReadExpression(out expression)) return "{0} {1} => " + expression + ";"; @@ -43,6 +46,9 @@ protected override string GetGetterTemplate() protected override string GetSetterTemplate() { + if (TryGetUnsafeBooleanWriteTemplate(out string unsafeTemplate)) + return unsafeTemplate; + if (TryGetDirectStorageBooleanWriteTemplate(out string template)) return template; diff --git a/BitsKit.Generator/Models/EnumFieldModel.cs b/BitsKit.Generator/Models/EnumFieldModel.cs index 00edd2c..dbe74c8 100644 --- a/BitsKit.Generator/Models/EnumFieldModel.cs +++ b/BitsKit.Generator/Models/EnumFieldModel.cs @@ -50,6 +50,9 @@ public EnumFieldModel(AttributeData attributeData, TypeSymbolProcessor? typeSymb protected override string GetGetterTemplate() { + if (TryGetUnsafeReadExpression(out string unsafeExpression)) + return $"{{0}} {{1}} => ({ReturnType})({unsafeExpression});"; + if (TryGetDirectFixedWidthReadTemplate(out string template)) return template; @@ -62,6 +65,9 @@ protected override string GetGetterTemplate() protected override string GetSetterTemplate() { + if (TryGetUnsafeWriteExpression("value", out string unsafeExpression)) + return "{0} {1} => " + unsafeExpression + ";"; + if (TryGetDirectFixedWidthWriteTemplate("value", out string template)) return template; diff --git a/BitsKit.Generator/Models/IntegralFieldModel.cs b/BitsKit.Generator/Models/IntegralFieldModel.cs index cd12996..40ebb93 100644 --- a/BitsKit.Generator/Models/IntegralFieldModel.cs +++ b/BitsKit.Generator/Models/IntegralFieldModel.cs @@ -42,6 +42,9 @@ public IntegralFieldModel(AttributeData attributeData, TypeSymbolProcessor? type protected override string GetGetterTemplate() { + if (TryGetUnsafeReadExpression(out string unsafeExpression)) + return "{0} {1} => " + unsafeExpression + ";"; + if (TryGetDirectFixedWidthReadTemplate(out string template)) return template; @@ -62,6 +65,9 @@ protected override string GetGetterTemplate() protected override string GetSetterTemplate() { + if (TryGetUnsafeWriteExpression("value", out string unsafeExpression)) + return "{0} {1} => " + unsafeExpression + ";"; + if (TryGetDirectFixedWidthWriteTemplate("value", out string template)) return template; diff --git a/BitsKit.Generator/TypeSymbolProcessor.cs b/BitsKit.Generator/TypeSymbolProcessor.cs index c5032bd..cbf8ba8 100644 --- a/BitsKit.Generator/TypeSymbolProcessor.cs +++ b/BitsKit.Generator/TypeSymbolProcessor.cs @@ -12,6 +12,7 @@ internal sealed record TypeSymbolProcessor public string? Namespace { get; } public BitOrder DefaultBitOrder { get; } + public BitObjectAccessMode AccessMode { get; } public bool IsStruct { get; } public bool IsInlineArray { get; } @@ -34,12 +35,24 @@ public TypeSymbolProcessor(INamedTypeSymbol typeSymbol, AttributeData attribute) if (string.IsNullOrWhiteSpace(Namespace)) Namespace = null; DefaultBitOrder = (BitOrder)attribute.ConstructorArguments[0].Value!; + AccessMode = GetAccessMode(attribute); IsStruct = typeSymbol.TypeKind == TypeKind.Struct; IsInlineArray = HasInlineArrayAttribute(typeSymbol); Fields = EnumerateFields(typeSymbol); } + private static BitObjectAccessMode GetAccessMode(AttributeData attribute) + { + foreach (KeyValuePair argument in attribute.NamedArguments) + { + if (argument.Key == "AccessMode" && argument.Value.Value is int value) + return (BitObjectAccessMode)value; + } + + return BitObjectAccessMode.Checked; + } + public void GenerateCSharpSource(StringBuilder sb) { sb.AppendIndentedLine(1, diff --git a/BitsKit.Tests/GeneratorTests.Models.cs b/BitsKit.Tests/GeneratorTests.Models.cs index 1c48137..6c8afec 100644 --- a/BitsKit.Tests/GeneratorTests.Models.cs +++ b/BitsKit.Tests/GeneratorTests.Models.cs @@ -314,6 +314,49 @@ public partial struct SpecializedMemoryAccessorStruct public Memory BigEndianBacking48; } +[BitObject(BitOrder.LeastSignificant, AccessMode = BitObjectAccessMode.Unsafe)] +public partial struct UnsafeMemoryAccessorStruct +{ + [BitField(3)] + [BitField("Value", 20, BitFieldType.UInt32)] + public Memory Backing; + + [BitField(5)] + [BitField("SignedValue", 13, BitFieldType.Int32, ReverseBitOrder = true)] + public Memory SignedBacking; + + [BitField(6)] + [BooleanField("Flag")] + public Memory BooleanBacking; + + [BitField("AlignedValue", 32, BitFieldType.UInt32)] + public Memory AlignedBacking; +} + +[BitObject(BitOrder.LeastSignificant, AccessMode = BitObjectAccessMode.Unsafe)] +public partial struct UnsafeArrayAccessorStruct +{ + [BitField(3)] + [BitField("Value", 20, BitFieldType.UInt32)] + public byte[] Backing; +} + +[BitObject(BitOrder.LeastSignificant, AccessMode = BitObjectAccessMode.Unsafe)] +public ref partial struct UnsafeSpanAccessorStruct +{ + [BitField(3)] + [BitField("Value", 20, BitFieldType.UInt32)] + public Span Backing; +} + +[BitObject(BitOrder.LeastSignificant, AccessMode = BitObjectAccessMode.Unsafe)] +public unsafe partial struct UnsafeFixedAccessorStruct +{ + [BitField(3)] + [BitField("Value", 20, BitFieldType.UInt32)] + public fixed byte Backing[16]; +} + [BitObject(BitOrder.LeastSignificant)] public ref partial struct SpecializedSpanAccessorStruct { @@ -331,6 +374,15 @@ public ref partial struct SpecializedSpanAccessorStruct #if NET8_0_OR_GREATER +[BitObject(BitOrder.LeastSignificant, AccessMode = BitObjectAccessMode.Unsafe)] +[InlineArray(16)] +public partial struct UnsafeInlineArrayAccessorStruct +{ + [BitField(3)] + [BitField("Value", 20, BitFieldType.UInt32)] + private byte _element; +} + [BitObject(BitOrder.LeastSignificant)] [InlineArray(4)] public partial struct OptimizedInlineArrayAccessorStruct diff --git a/BitsKit.Tests/UnsafeAccessTests.cs b/BitsKit.Tests/UnsafeAccessTests.cs new file mode 100644 index 0000000..c36c5e2 --- /dev/null +++ b/BitsKit.Tests/UnsafeAccessTests.cs @@ -0,0 +1,202 @@ +using System; +using System.Linq; +using BitsKit.Primitives; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace BitsKit.Tests; + +[TestClass] +public class UnsafeAccessTests +{ + [TestMethod] + public void UnsafePrimitivesMatchReferenceOperationsForValidPaddedBuffers() + { + var random = new Random(0x51A7E); + int[] widths = [8, 16, 32, 64]; + + foreach (int width in widths) + { + foreach (int bitOffset in Enumerable.Range(0, 8)) + { + foreach (int bitCount in new[] { 1, Math.Max(1, width / 2), width }) + { + for (int iteration = 0; iteration < 50; iteration++) + { + var original = new byte[32]; + random.NextBytes(original); + + foreach (bool mostSignificant in new[] { false, true }) + { + ulong expectedRead = mostSignificant + ? Helpers.ReadBitsMSB(original, bitOffset, bitCount) + : Helpers.ReadBitsLSB(original, bitOffset, bitCount); + var readBuffer = (byte[])original.Clone(); + ulong actualRead = ReadUnsafe( + ref readBuffer[0], + bitOffset, + bitCount, + width, + mostSignificant); + Assert.AreEqual(expectedRead, actualRead); + + ulong value = unchecked((ulong)random.NextInt64()); + var expectedWrite = (byte[])original.Clone(); + var actualWrite = (byte[])original.Clone(); + if (mostSignificant) + Helpers.WriteBitsMSB(expectedWrite, bitOffset, value, bitCount); + else + Helpers.WriteBitsLSB(expectedWrite, bitOffset, value, bitCount); + + WriteUnsafe( + ref actualWrite[0], + bitOffset, + value, + bitCount, + width, + mostSignificant); + CollectionAssert.AreEqual(expectedWrite, actualWrite); + } + } + } + } + } + } + + [TestMethod] + public void UnsafeGeneratedMemoryAccessorsMatchReferenceOperations() + { + var random = new Random(0xB175); + + for (int iteration = 0; iteration < 1_000; iteration++) + { + var backing = new byte[16]; + var signedBacking = new byte[16]; + var booleanBacking = new byte[16]; + var alignedBacking = new byte[16]; + random.NextBytes(backing); + random.NextBytes(signedBacking); + random.NextBytes(booleanBacking); + random.NextBytes(alignedBacking); + + var model = new UnsafeMemoryAccessorStruct + { + Backing = backing, + SignedBacking = signedBacking, + BooleanBacking = booleanBacking, + AlignedBacking = alignedBacking + }; + + Assert.AreEqual((uint)Helpers.ReadBitsLSB(backing, 3, 20), model.Value); + + int signedValue = unchecked((int)Helpers.ReadBitsMSB(signedBacking, 5, 13)); + signedValue = (signedValue << 19) >> 19; + Assert.AreEqual(signedValue, model.SignedValue); + + Assert.AreEqual(Helpers.ReadBitsLSB(booleanBacking, 6, 1) != 0, model.Flag); + Assert.AreEqual((uint)Helpers.ReadBitsLSB(alignedBacking, 0, 32), model.AlignedValue); + + uint next = unchecked((uint)random.NextInt64()); + var expected = (byte[])backing.Clone(); + Helpers.WriteBitsLSB(expected, 3, next, 20); + model.Value = next; + CollectionAssert.AreEqual(expected, backing); + + bool nextFlag = (iteration & 1) != 0; + var expectedBoolean = (byte[])booleanBacking.Clone(); + Helpers.WriteBitsLSB(expectedBoolean, 6, nextFlag ? 1UL : 0UL, 1); + model.Flag = nextFlag; + CollectionAssert.AreEqual(expectedBoolean, booleanBacking); + } + } + + [TestMethod] + public void CheckedGeneratedAccessorsRemainCheckedByDefault() + { + var model = new OptimizedMemoryAccessorStruct { Backing = Array.Empty() }; + Assert.ThrowsExactly(() => _ = model.Value); + Assert.ThrowsExactly(() => model.Value = 1); + } + + [TestMethod] + public unsafe void UnsafeGenerationSupportsEveryByteAddressableBackingKind() + { + const uint Expected = 0xABCDE; + + var arrayModel = new UnsafeArrayAccessorStruct { Backing = new byte[16] }; + arrayModel.Value = Expected; + Assert.AreEqual(Expected, arrayModel.Value); + + Span spanBuffer = stackalloc byte[16]; + var spanModel = new UnsafeSpanAccessorStruct { Backing = spanBuffer }; + spanModel.Value = Expected; + Assert.AreEqual(Expected, spanModel.Value); + + var fixedModel = new UnsafeFixedAccessorStruct(); + fixedModel.Value = Expected; + Assert.AreEqual(Expected, fixedModel.Value); + + var inlineModel = new UnsafeInlineArrayAccessorStruct(); + inlineModel.Value = Expected; + Assert.AreEqual(Expected, inlineModel.Value); + } + + private static ulong ReadUnsafe( + ref byte source, + int bitOffset, + int bitCount, + int width, + bool mostSignificant) + { + return (width, mostSignificant) switch + { + (8, false) => UnsafeBitPrimitives.ReadUInt8LSB(ref source, bitOffset, bitCount), + (8, true) => UnsafeBitPrimitives.ReadUInt8MSB(ref source, bitOffset, bitCount), + (16, false) => UnsafeBitPrimitives.ReadUInt16LSB(ref source, bitOffset, bitCount), + (16, true) => UnsafeBitPrimitives.ReadUInt16MSB(ref source, bitOffset, bitCount), + (32, false) => UnsafeBitPrimitives.ReadUInt32LSB(ref source, bitOffset, bitCount), + (32, true) => UnsafeBitPrimitives.ReadUInt32MSB(ref source, bitOffset, bitCount), + (64, false) => UnsafeBitPrimitives.ReadUInt64LSB(ref source, bitOffset, bitCount), + (64, true) => UnsafeBitPrimitives.ReadUInt64MSB(ref source, bitOffset, bitCount), + _ => throw new ArgumentOutOfRangeException(nameof(width)) + }; + } + + private static void WriteUnsafe( + ref byte destination, + int bitOffset, + ulong value, + int bitCount, + int width, + bool mostSignificant) + { + switch (width, mostSignificant) + { + case (8, false): + UnsafeBitPrimitives.WriteUInt8LSB(ref destination, bitOffset, (byte)value, bitCount); + break; + case (8, true): + UnsafeBitPrimitives.WriteUInt8MSB(ref destination, bitOffset, (byte)value, bitCount); + break; + case (16, false): + UnsafeBitPrimitives.WriteUInt16LSB(ref destination, bitOffset, (ushort)value, bitCount); + break; + case (16, true): + UnsafeBitPrimitives.WriteUInt16MSB(ref destination, bitOffset, (ushort)value, bitCount); + break; + case (32, false): + UnsafeBitPrimitives.WriteUInt32LSB(ref destination, bitOffset, (uint)value, bitCount); + break; + case (32, true): + UnsafeBitPrimitives.WriteUInt32MSB(ref destination, bitOffset, (uint)value, bitCount); + break; + case (64, false): + UnsafeBitPrimitives.WriteUInt64LSB(ref destination, bitOffset, value, bitCount); + break; + case (64, true): + UnsafeBitPrimitives.WriteUInt64MSB(ref destination, bitOffset, value, bitCount); + break; + default: + throw new ArgumentOutOfRangeException(nameof(width)); + } + } +} diff --git a/BitsKit/BitFields/BitObjectAccessMode.cs b/BitsKit/BitFields/BitObjectAccessMode.cs new file mode 100644 index 0000000..079e04e --- /dev/null +++ b/BitsKit/BitFields/BitObjectAccessMode.cs @@ -0,0 +1,23 @@ +namespace BitsKit.BitFields; + +/// +/// Controls how generated bit-field accessors validate byte-addressable backing storage. +/// +public enum BitObjectAccessMode +{ + /// + /// Generated accessors retain bounds checks and throw when backing storage is too small. + /// + Checked, + + /// + /// Generated accessors may skip bounds checks for byte-addressable backing storage when the + /// unchecked path is faster than the checked specialization. + /// + /// + /// The caller must guarantee that every backing buffer is non-empty and large enough for + /// the generated access width. Violating that contract can read or modify memory outside + /// the declared buffer and may cause data corruption, information disclosure, or process failure. + /// + Unsafe +} diff --git a/BitsKit/BitFields/BitObjectAttribute.cs b/BitsKit/BitFields/BitObjectAttribute.cs index 5843ca8..1efb2bd 100644 --- a/BitsKit/BitFields/BitObjectAttribute.cs +++ b/BitsKit/BitFields/BitObjectAttribute.cs @@ -10,4 +10,13 @@ public sealed class BitObjectAttribute(BitOrder defaultBitOrder) : Attribute /// Defines the default bit order for the object /// public BitOrder DefaultOrder { get; } = defaultBitOrder; + + /// + /// Controls whether generated accessors may use unchecked byte-addressable storage operations. + /// + /// + /// must only be used when every instance is guaranteed + /// to provide non-empty backing storage large enough for each generated access width. + /// + public BitObjectAccessMode AccessMode { get; set; } = BitObjectAccessMode.Checked; } diff --git a/BitsKit/Primitives/BitPrimitives.cs b/BitsKit/Primitives/BitPrimitives.cs index 310ddf7..fdce3c8 100644 --- a/BitsKit/Primitives/BitPrimitives.cs +++ b/BitsKit/Primitives/BitPrimitives.cs @@ -30,7 +30,7 @@ private static bool ValidateArgs(int availableBits, int bitOffset, int bitCount, [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int GetMask(int size) => (1 << size) - 1; - private static uint ReadValue32(uint source, int bitOffset, int bitCount, BitOrder bitOrder) + internal static uint ReadValue32(uint source, int bitOffset, int bitCount, BitOrder bitOrder) { if (bitCount == 0) return 0; @@ -41,7 +41,7 @@ private static uint ReadValue32(uint source, int bitOffset, int bitCount, BitOrd return BinaryPrimitives.ReverseEndianness(source) << bitOffset >> (32 - bitCount); } - private static ulong ReadValue64(ulong source, int bitOffset, int bitCount, BitOrder bitOrder) + internal static ulong ReadValue64(ulong source, int bitOffset, int bitCount, BitOrder bitOrder) { if (bitCount == 0) return 0; @@ -52,7 +52,7 @@ private static ulong ReadValue64(ulong source, int bitOffset, int bitCount, BitO return BinaryPrimitives.ReverseEndianness(source) << bitOffset >> (64 - bitCount); } - private static UInt128 ReadValue128(UInt128 source, int bitOffset, int bitCount, BitOrder bitOrder) + internal static UInt128 ReadValue128(UInt128 source, int bitOffset, int bitCount, BitOrder bitOrder) { if (bitCount == 0) return 0; @@ -64,7 +64,7 @@ private static UInt128 ReadValue128(UInt128 source, int bitOffset, int bitCount, } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void WriteValue8(ref byte destination, int bitShift, int value, int bitCount) + internal static void WriteValue8(ref byte destination, int bitShift, int value, int bitCount) { if (bitCount == 0) return; @@ -72,7 +72,7 @@ private static void WriteValue8(ref byte destination, int bitShift, int value, i destination = (byte)((destination & ~(GetMask(bitCount) << bitShift)) | ((value & GetMask(bitCount)) << bitShift)); } - private static void WriteValue16(ref ushort destination, int bitOffset, int value, int bitCount, BitOrder bitOrder) + internal static void WriteValue16(ref ushort destination, int bitOffset, int value, int bitCount, BitOrder bitOrder) { if (bitCount == 0) return; @@ -101,7 +101,7 @@ private static void WriteValue16(ref ushort destination, int bitOffset, int valu } } - private static void WriteValue32(ref uint destination, int bitOffset, uint value, int bitCount, BitOrder bitOrder) + internal static void WriteValue32(ref uint destination, int bitOffset, uint value, int bitCount, BitOrder bitOrder) { if (bitCount == 0) return; @@ -130,7 +130,7 @@ private static void WriteValue32(ref uint destination, int bitOffset, uint value destination |= value; } - private static void WriteValue64(ref ulong destination, int bitOffset, ulong value, int bitCount, BitOrder bitOrder) + internal static void WriteValue64(ref ulong destination, int bitOffset, ulong value, int bitCount, BitOrder bitOrder) { if (bitCount == 0) return; @@ -159,7 +159,7 @@ private static void WriteValue64(ref ulong destination, int bitOffset, ulong val destination |= value; } - private static void WriteValue128(ref ulong destination, int bitOffset, ulong value, int bitCount, BitOrder bitOrder) + internal static void WriteValue128(ref ulong destination, int bitOffset, ulong value, int bitCount, BitOrder bitOrder) { // benchmarking shows that decomposing into ulong writes is faster diff --git a/BitsKit/Primitives/UnsafeBitPrimitives.cs b/BitsKit/Primitives/UnsafeBitPrimitives.cs new file mode 100644 index 0000000..f1cd6e0 --- /dev/null +++ b/BitsKit/Primitives/UnsafeBitPrimitives.cs @@ -0,0 +1,297 @@ +namespace BitsKit.Primitives; + +/// +/// Provides unchecked bit access over a raw byte reference. +/// +/// +/// These methods perform no bounds or argument validation. The caller must guarantee that every +/// source or destination reference points to enough accessible memory for the requested operation. +/// Invalid arguments can corrupt memory, disclose data, or terminate the process. Prefer +/// unless measurement proves these methods necessary. +/// +public static class UnsafeBitPrimitives +{ + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ReadBitLSB(ref byte source, int bitOffset) => + ((Unsafe.Add(ref source, bitOffset >> 3) >> (bitOffset & 7)) & 1) != 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static bool ReadBitMSB(ref byte source, int bitOffset) => + ((Unsafe.Add(ref source, bitOffset >> 3) >> (7 - (bitOffset & 7))) & 1) != 0; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static sbyte ReadInt8LSB(ref byte source, int bitOffset, int bitCount) => + unchecked((sbyte)SignExtend(ReadUnsigned(ref source, bitOffset, bitCount, 8, BitOrder.LeastSignificant), bitCount)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static sbyte ReadInt8MSB(ref byte source, int bitOffset, int bitCount) => + unchecked((sbyte)SignExtend(ReadUnsigned(ref source, bitOffset, bitCount, 8, BitOrder.MostSignificant), bitCount)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt8LSB(ref byte source, int bitOffset, int bitCount) => + unchecked((byte)ReadUnsigned(ref source, bitOffset, bitCount, 8, BitOrder.LeastSignificant)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static byte ReadUInt8MSB(ref byte source, int bitOffset, int bitCount) => + unchecked((byte)ReadUnsigned(ref source, bitOffset, bitCount, 8, BitOrder.MostSignificant)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short ReadInt16LSB(ref byte source, int bitOffset, int bitCount) => + unchecked((short)SignExtend(ReadUnsigned(ref source, bitOffset, bitCount, 16, BitOrder.LeastSignificant), bitCount)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static short ReadInt16MSB(ref byte source, int bitOffset, int bitCount) => + unchecked((short)SignExtend(ReadUnsigned(ref source, bitOffset, bitCount, 16, BitOrder.MostSignificant), bitCount)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReadUInt16LSB(ref byte source, int bitOffset, int bitCount) => + unchecked((ushort)ReadUnsigned(ref source, bitOffset, bitCount, 16, BitOrder.LeastSignificant)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ushort ReadUInt16MSB(ref byte source, int bitOffset, int bitCount) => + unchecked((ushort)ReadUnsigned(ref source, bitOffset, bitCount, 16, BitOrder.MostSignificant)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReadInt32LSB(ref byte source, int bitOffset, int bitCount) => + unchecked((int)SignExtend(ReadUnsigned(ref source, bitOffset, bitCount, 32, BitOrder.LeastSignificant), bitCount)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int ReadInt32MSB(ref byte source, int bitOffset, int bitCount) => + unchecked((int)SignExtend(ReadUnsigned(ref source, bitOffset, bitCount, 32, BitOrder.MostSignificant), bitCount)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt32LSB(ref byte source, int bitOffset, int bitCount) => + unchecked((uint)ReadUnsigned(ref source, bitOffset, bitCount, 32, BitOrder.LeastSignificant)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static uint ReadUInt32MSB(ref byte source, int bitOffset, int bitCount) => + unchecked((uint)ReadUnsigned(ref source, bitOffset, bitCount, 32, BitOrder.MostSignificant)); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long ReadInt64LSB(ref byte source, int bitOffset, int bitCount) => + SignExtend(ReadUnsigned(ref source, bitOffset, bitCount, 64, BitOrder.LeastSignificant), bitCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static long ReadInt64MSB(ref byte source, int bitOffset, int bitCount) => + SignExtend(ReadUnsigned(ref source, bitOffset, bitCount, 64, BitOrder.MostSignificant), bitCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong ReadUInt64LSB(ref byte source, int bitOffset, int bitCount) => + ReadUnsigned(ref source, bitOffset, bitCount, 64, BitOrder.LeastSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static ulong ReadUInt64MSB(ref byte source, int bitOffset, int bitCount) => + ReadUnsigned(ref source, bitOffset, bitCount, 64, BitOrder.MostSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint ReadIntPtrLSB(ref byte source, int bitOffset, int bitCount) => + IntPtr.Size == 8 ? (nint)ReadInt64LSB(ref source, bitOffset, bitCount) : ReadInt32LSB(ref source, bitOffset, bitCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nint ReadIntPtrMSB(ref byte source, int bitOffset, int bitCount) => + IntPtr.Size == 8 ? (nint)ReadInt64MSB(ref source, bitOffset, bitCount) : ReadInt32MSB(ref source, bitOffset, bitCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nuint ReadUIntPtrLSB(ref byte source, int bitOffset, int bitCount) => + IntPtr.Size == 8 ? (nuint)ReadUInt64LSB(ref source, bitOffset, bitCount) : ReadUInt32LSB(ref source, bitOffset, bitCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static nuint ReadUIntPtrMSB(ref byte source, int bitOffset, int bitCount) => + IntPtr.Size == 8 ? (nuint)ReadUInt64MSB(ref source, bitOffset, bitCount) : ReadUInt32MSB(ref source, bitOffset, bitCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteBitLSB(ref byte destination, int bitOffset, bool value) => + WriteBit(ref destination, bitOffset, value, BitOrder.LeastSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteBitMSB(ref byte destination, int bitOffset, bool value) => + WriteBit(ref destination, bitOffset, value, BitOrder.MostSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteInt8LSB(ref byte destination, int bitOffset, sbyte value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, unchecked((byte)value), bitCount, 8, BitOrder.LeastSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteInt8MSB(ref byte destination, int bitOffset, sbyte value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, unchecked((byte)value), bitCount, 8, BitOrder.MostSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt8LSB(ref byte destination, int bitOffset, byte value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, value, bitCount, 8, BitOrder.LeastSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt8MSB(ref byte destination, int bitOffset, byte value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, value, bitCount, 8, BitOrder.MostSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteInt16LSB(ref byte destination, int bitOffset, short value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, unchecked((ushort)value), bitCount, 16, BitOrder.LeastSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteInt16MSB(ref byte destination, int bitOffset, short value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, unchecked((ushort)value), bitCount, 16, BitOrder.MostSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt16LSB(ref byte destination, int bitOffset, ushort value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, value, bitCount, 16, BitOrder.LeastSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt16MSB(ref byte destination, int bitOffset, ushort value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, value, bitCount, 16, BitOrder.MostSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteInt32LSB(ref byte destination, int bitOffset, int value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, unchecked((uint)value), bitCount, 32, BitOrder.LeastSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteInt32MSB(ref byte destination, int bitOffset, int value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, unchecked((uint)value), bitCount, 32, BitOrder.MostSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt32LSB(ref byte destination, int bitOffset, uint value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, value, bitCount, 32, BitOrder.LeastSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt32MSB(ref byte destination, int bitOffset, uint value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, value, bitCount, 32, BitOrder.MostSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteInt64LSB(ref byte destination, int bitOffset, long value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, unchecked((ulong)value), bitCount, 64, BitOrder.LeastSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteInt64MSB(ref byte destination, int bitOffset, long value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, unchecked((ulong)value), bitCount, 64, BitOrder.MostSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt64LSB(ref byte destination, int bitOffset, ulong value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, value, bitCount, 64, BitOrder.LeastSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUInt64MSB(ref byte destination, int bitOffset, ulong value, int bitCount) => + WriteUnsigned(ref destination, bitOffset, value, bitCount, 64, BitOrder.MostSignificant); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteIntPtrLSB(ref byte destination, int bitOffset, nint value, int bitCount) + { + if (IntPtr.Size == 8) + WriteInt64LSB(ref destination, bitOffset, value, bitCount); + else + WriteInt32LSB(ref destination, bitOffset, (int)value, bitCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteIntPtrMSB(ref byte destination, int bitOffset, nint value, int bitCount) + { + if (IntPtr.Size == 8) + WriteInt64MSB(ref destination, bitOffset, value, bitCount); + else + WriteInt32MSB(ref destination, bitOffset, (int)value, bitCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUIntPtrLSB(ref byte destination, int bitOffset, nuint value, int bitCount) + { + if (IntPtr.Size == 8) + WriteUInt64LSB(ref destination, bitOffset, value, bitCount); + else + WriteUInt32LSB(ref destination, bitOffset, (uint)value, bitCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void WriteUIntPtrMSB(ref byte destination, int bitOffset, nuint value, int bitCount) + { + if (IntPtr.Size == 8) + WriteUInt64MSB(ref destination, bitOffset, value, bitCount); + else + WriteUInt32MSB(ref destination, bitOffset, (uint)value, bitCount); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static ulong ReadUnsigned(ref byte source, int bitOffset, int bitCount, int maxBits, BitOrder bitOrder) + { + if (bitCount == 0) + return 0; + + ref byte first = ref Unsafe.Add(ref source, bitOffset >> 3); + int bitInByte = bitOffset & 7; + + if (maxBits <= 16) + { + uint value = Unsafe.ReadUnaligned(ref first); + return BitPrimitives.ReadValue32(value, bitInByte, bitCount, bitOrder); + } + + if (maxBits == 32) + { + ulong value = Unsafe.ReadUnaligned(ref first); + return bitCount + bitInByte <= 32 + ? BitPrimitives.ReadValue32(unchecked((uint)value), bitInByte, bitCount, bitOrder) + : BitPrimitives.ReadValue64(value, bitInByte, bitCount, bitOrder); + } + + UInt128 wideValue = Unsafe.ReadUnaligned(ref first); + return bitCount + bitInByte > 64 + ? unchecked((ulong)BitPrimitives.ReadValue128(wideValue, bitInByte, bitCount, bitOrder)) + : BitPrimitives.ReadValue64(unchecked((ulong)wideValue), bitInByte, bitCount, bitOrder); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long SignExtend(ulong value, int bitCount) => + bitCount == 0 ? 0 : unchecked((long)(value << (64 - bitCount))) >> (64 - bitCount); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteBit(ref byte destination, int bitOffset, bool value, BitOrder bitOrder) + { + ref byte target = ref Unsafe.Add(ref destination, bitOffset >> 3); + int bitInByte = bitOffset & 7; + int mask = 1 << (bitOrder == BitOrder.MostSignificant ? 7 - bitInByte : bitInByte); + target = value ? unchecked((byte)(target | mask)) : unchecked((byte)(target & ~mask)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void WriteUnsigned( + ref byte destination, + int bitOffset, + ulong value, + int bitCount, + int maxBits, + BitOrder bitOrder) + { + ref byte first = ref Unsafe.Add(ref destination, bitOffset >> 3); + int bitInByte = bitOffset & 7; + + if (maxBits == 8) + { + if (bitCount + bitInByte > 8) + BitPrimitives.WriteValue16(ref Unsafe.As(ref first), bitInByte, unchecked((int)value), bitCount, bitOrder); + else + BitPrimitives.WriteValue8(ref first, bitOrder == BitOrder.MostSignificant ? 8 - bitCount - bitInByte : bitInByte, unchecked((int)value), bitCount); + return; + } + + if (maxBits == 16) + { + if (bitCount + bitInByte > 16) + BitPrimitives.WriteValue32(ref Unsafe.As(ref first), bitInByte, unchecked((uint)value), bitCount, bitOrder); + else + BitPrimitives.WriteValue16(ref Unsafe.As(ref first), bitInByte, unchecked((int)value), bitCount, bitOrder); + return; + } + + if (maxBits == 32) + { + if (bitCount + bitInByte > 32) + BitPrimitives.WriteValue64(ref Unsafe.As(ref first), bitInByte, value, bitCount, bitOrder); + else + BitPrimitives.WriteValue32(ref Unsafe.As(ref first), bitInByte, unchecked((uint)value), bitCount, bitOrder); + return; + } + + ref ulong target = ref Unsafe.As(ref first); + if (bitCount + bitInByte > 64) + BitPrimitives.WriteValue128(ref target, bitInByte, value, bitCount, bitOrder); + else + BitPrimitives.WriteValue64(ref target, bitInByte, value, bitCount, bitOrder); + } +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 56850eb..68b34b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ Notable changes to the community-maintained fork are documented here. This proje ## Unreleased +### Added + +- An explicit `BitObjectAccessMode.Unsafe` generator opt-in can remove byte-storage bounds checks for callers that guarantee padded, valid backing buffers; checked generation remains the default. +- `UnsafeBitPrimitives` exposes the raw-reference operations used by unsafe generated accessors. + ## 1.5.0 - 2026-07-21 ### Added diff --git a/README.md b/README.md index 6d42432..55e2b7c 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,27 @@ Enum bit-fields are supported by the `[EnumFieldAttribute]` helper attribute. Th #### Modifiers The `BitFieldModifiers` enum allows alterations to the way that the source generator produces the bit-fields. By default all bit-fields are generated as a *public read/write* or *public readonly* properties relative to their backing field's accessibility. The `Modifiers` field allows control over this and provides the ability to change a bit-field's accessibility and if it is `readonly`, `init only` (.NET 6.0) and/or `required` (.NET 7.0). -For valid fixed-width integral backing fields, generated LSB and MSB getters and setters use direct masks, shifts, and byte-order operations. Memory-backed fields, native integers, and invalid ranges continue through `BitPrimitives` to preserve their established semantics. +For valid fixed-width integral backing fields, generated LSB and MSB getters and setters use direct masks, shifts, and byte-order operations. Eligible memory, span, array, and byte inline-array layouts also use specialized generated accessors. Other layouts continue through `BitPrimitives` to preserve their established semantics. + +#### Unsafe Access Mode + +Generated accessors are bounds-checked by default. Applications that control every backing buffer and have measured a meaningful benefit can opt a bit-object into unchecked byte access: + +```c# +[BitObject( + BitOrder.LeastSignificantBit, + AccessMode = BitObjectAccessMode.Unsafe)] +public partial struct TrustedPacket +{ + [BitField(3)] + [BitField("Value", 20, BitFieldType.UInt32)] + private Memory _buffer; +} +``` + +Unsafe access mode affects byte-addressable backing fields only; integral backing fields are unchanged. It permits the generator to remove length and argument validation and use raw references through `UnsafeBitPrimitives`. Already-specialized layouts retain their checked intrinsic when benchmarks show it is faster. For unchecked fields, the caller must keep backing storage alive, non-empty, and large enough from the field's starting byte for the generated load or store: 1 byte for Boolean, 4 bytes for 8/16-bit fields, 8 bytes for 32-bit fields, and 16 bytes for 64-bit fields. These access widths can exceed the bytes occupied by the logical bit-field. + +**Warning:** An undersized, empty, invalid, or concurrently moved backing buffer can cause out-of-bounds reads or writes, data corruption, information disclosure, or process failure. Unsafe mode is never inferred and should not be enabled merely because it benchmarks faster. Checked access remains the supported default for untrusted or variable-sized input. **Note:** Currently both the getter and setter share the same accessibility therefore you cannot have public bit-fields with private setters. From a5571edd7c61a1625e6211d188469395d866e2db Mon Sep 17 00:00:00 2001 From: RejectKid Date: Wed, 22 Jul 2026 09:18:08 -0400 Subject: [PATCH 2/2] Expand fork and unsafe mode documentation --- README.md | 88 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 84 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 55e2b7c..12e13d4 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,44 @@ BitsKit is a lightweight C# library that provides efficient bit-level reading, w All features support integral and memory types, as well as targeting both, Little Endian (LE) Least Significant Bit (LSB) and Big Endian (BE) Most Significant Bit (MSB). +Install the maintained package: + +```shell +dotnet add package RejectKid.BitsKit +``` + +Existing source code continues to use the original namespaces, for example `BitsKit.BitFields` and `BitsKit.Primitives`. Do not reference both the original `BitsKit` package and `RejectKid.BitsKit` in the same project because they provide the same assembly and namespaces. + ## Features +- [Changes in this fork](#changes-in-this-fork) - [BitPrimitives](#bitprimitives) - [Bit Fields](#bit-fields) - [IO Classes](#io-classes) - [Utility Methods](#utility-methods) - [Benchmarks](#benchmarks) +## Changes in this fork + +This fork preserves the original public API while maintaining it for current .NET and C# versions. New APIs, including non-seekable stream writing and unsafe generated access, are additive. The detailed version-by-version history is in the [changelog](CHANGELOG.md). + +| Area | Maintained fork changes | +| --- | --- | +| Source generator | Restored `BITSKIT003`, fixed inline-array setters for modern compilers, removed retained Roslyn state, and expanded generator regression coverage. | +| Generated-code performance | Specialized integral, memory, span, array, fixed-buffer, Boolean, inline-array, aligned, and common-width accessors instead of routing every operation through general primitives. | +| Streams | Correct partial-read and EOF behavior, pooled reader/writer buffering, large stream positions, consistent disposal errors, and forward-only/non-seekable `BitStreamWriter` output. | +| Runtime targets | Targets `netstandard2.1`, .NET 8, and .NET 10; the end-of-life .NET 6 and .NET 7 assets were removed. | +| Validation | Cross-platform CI, CodeQL, modern tests, package validation, release provenance, scheduled benchmarks, and an original-fork performance regression workflow. | +| Distribution | Automated tagged GitHub releases and the maintained [`RejectKid.BitsKit`](https://www.nuget.org/packages/RejectKid.BitsKit) package. | +| Optional unsafe access | Generated byte-storage accessors can explicitly skip safety checks for trusted, sufficiently padded buffers; checked behavior remains the default. | + +Migrating from the original package normally requires only changing the package reference: + +```xml + +``` + +No namespace changes are required. Applications that still target .NET 6 or .NET 7 can consume the `netstandard2.1` asset when their target supports it, but those runtimes are no longer built or tested directly by this project. + ## Usage ### BitPrimitives @@ -115,21 +146,70 @@ For valid fixed-width integral backing fields, generated LSB and MSB getters and #### Unsafe Access Mode -Generated accessors are bounds-checked by default. Applications that control every backing buffer and have measured a meaningful benefit can opt a bit-object into unchecked byte access: +Generated accessors are bounds-checked by default. No changes are required for existing bit objects: + +```c# +[BitObject(BitOrder.LeastSignificantBit)] +public partial struct CheckedPacket +{ + [BitField(3)] + [BitField("Value", 20, BitFieldType.UInt32)] + private Memory _buffer; +} +``` + +Applications that control every backing buffer and have measured a meaningful benefit can opt an entire bit object into unchecked byte access. Validate the buffer once at the trust boundary, before storing it in the bit object: ```c# [BitObject( BitOrder.LeastSignificantBit, AccessMode = BitObjectAccessMode.Unsafe)] -public partial struct TrustedPacket +public partial class TrustedPacket { [BitField(3)] [BitField("Value", 20, BitFieldType.UInt32)] private Memory _buffer; + + public TrustedPacket(Memory buffer) + { + // A generated UInt32 unsafe accessor can touch an 8-byte window. + if (buffer.Length < 8) + throw new ArgumentException("The buffer must contain at least 8 bytes.", nameof(buffer)); + + _buffer = buffer; + } } + +var storage = new byte[8]; +var packet = new TrustedPacket(storage); +packet.Value = 0xABCDE; +Console.WriteLine(packet.Value); // 703710 +``` + +Unsafe access mode affects byte-addressable backing fields only; integral backing fields are unchanged. It permits the generator to remove length and argument validation and use raw references through `UnsafeBitPrimitives`. Already-specialized layouts retain their checked intrinsic when benchmarks show it is faster. + +The minimum accessible window, measured from the byte containing the field's first bit, is based on the declared `BitFieldType`, not only the logical field size: + +| Generated field type | Required accessible window | +| --- | ---: | +| `Boolean` | 1 byte | +| `SByte`, `Byte`, `Int16`, `UInt16` | 4 bytes | +| `Int32`, `UInt32` | 8 bytes | +| `Int64`, `UInt64` | 16 bytes | + +For example, a 20-bit `UInt32` field still requires an accessible 8-byte window. Account for the field's starting byte as well: if it begins in byte 5, the backing storage must remain accessible through byte 12. These access widths can exceed the bytes occupied by the logical bit-field. + +`UnsafeBitPrimitives` can also be called directly after performing the same validation: + +```c# +Span buffer = stackalloc byte[8]; +ref byte first = ref MemoryMarshal.GetReference(buffer); + +UnsafeBitPrimitives.WriteUInt32LSB(ref first, bitOffset: 3, value: 0xABCDE, bitCount: 20); +uint value = UnsafeBitPrimitives.ReadUInt32LSB(ref first, bitOffset: 3, bitCount: 20); ``` -Unsafe access mode affects byte-addressable backing fields only; integral backing fields are unchanged. It permits the generator to remove length and argument validation and use raw references through `UnsafeBitPrimitives`. Already-specialized layouts retain their checked intrinsic when benchmarks show it is faster. For unchecked fields, the caller must keep backing storage alive, non-empty, and large enough from the field's starting byte for the generated load or store: 1 byte for Boolean, 4 bytes for 8/16-bit fields, 8 bytes for 32-bit fields, and 16 bytes for 64-bit fields. These access widths can exceed the bytes occupied by the logical bit-field. +The direct methods do not validate buffer length, bit offset, or bit count. Prefer generated checked accessors or `BitPrimitives` unless the caller owns the complete memory-safety contract. **Warning:** An undersized, empty, invalid, or concurrently moved backing buffer can cause out-of-bounds reads or writes, data corruption, information disclosure, or process failure. Unsafe mode is never inferred and should not be enabled merely because it benchmarks faster. Checked access remains the supported default for untrusted or variable-sized input. @@ -148,7 +228,7 @@ struct S unsigned char b4 : 2; // 2 bits for b4 - next (and final) bits in the 2nd byte }; ``` -Converted to it's BitsKit representation: +Converted to its BitsKit representation: ```c# [BitObject(BitOrder.LeastSignificantBit)] public partial struct S