Bitfields.CSharp 0.3.2

dotnet add package Bitfields.CSharp --version 0.3.2                
NuGet\Install-Package Bitfields.CSharp -Version 0.3.2                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Bitfields.CSharp" Version="0.3.2" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Bitfields.CSharp --version 0.3.2                
#r "nuget: Bitfields.CSharp, 0.3.2"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Bitfields.CSharp as a Cake Addin
#addin nuget:?package=Bitfields.CSharp&version=0.3.2

// Install Bitfields.CSharp as a Cake Tool
#tool nuget:?package=Bitfields.CSharp&version=0.3.2                

🌻 Bitfields-CSharp

Build NuGet Version GitHub License Stars NuGet Downloads Ko-fi

A C# package that provides a source generator for generating bitfields from classes, structs, or custom types, which is useful for defining schemas when working with low-level environments or concepts (e.g. embedded or writing an emulator).

  • Efficient and safe code like you would write by hand.
  • No unsafe code or runtime dependencies.
  • Compile-time checks for fields, types, and bits bounds checking.
  • Supports most primitive and user-defined custom types.
  • Signed fields are treated as 2's complement data types.

🔧 Usage

Bitfields is available as a NuGet package. You can install it using the following command:

nuget install Bitfields.CSharp

Or using the .NET Core command-line interface:

dotnet add package Bitfields.CSharp

🚀 Getting Started

You're emulating the N1nt3nd0 GameChild and come across the Display Control Register (DISPCNT) which is an 8-bit register:

  Bit   Expl.
  0-1   BG Mode                    (0-7=Video Mode)
  2-3   Display BG (0-1)           (0=BGx Off, 1=BGx On)
  4     OBJ Character VRAM Mapping (0=Two dimensional, 1=One dimensional)
  5-7   Always 0x3                 Padding

In table form, the bits are as follows:

7 - 5 4 3 -2 1 - 0
Always 0x3 OBJ Display BG BG Mode

You can define the register as follows:

using System.Diagnostics;
using Bitfields.CSharp;

namespace Main;

[Bitfield(BitfieldType.Byte)]
public partial class DisplayControl
{
  // We specify gg mode occupies the first 2 bits (0-1) of the bitfield
  // using the `[Bits]` attribute.
  [Bits(2)]
  private byte bgMode;

  // Custom type fields must implement the `FromBits` and `ToBits`
  // methods and declare its size and its type using the `[Bits]`
  // attribute.
  [Bits(2, CustomFieldType.Byte)] 
  private DisplayMode displayMode;

  // We can omit the `[Bits]` attribute for non-custom types, the generator
  // will assume the number of bits is the size of the field type. Here,
  // its 1 bit for a `bool` type.
  private bool objCharVramMapping;

  // Suffixing a field with "_" marks it as a padding field which
  // are inaccessible. Padding fields are 0 by default, unless a default value
  // is provided.
  [Bits(3)] 
  private byte always0X3Padding_ = 0x3;
}

/// <summary>
///  Implement the `FromBits` and `ToBits` methods for the custom type.
/// </summary>
public partial struct DisplayMode
{
  public bool bg0On;
  public bool bg1On;

  public DisplayMode(bool bg0On, bool bg1On)
  {
    this.bg0On = bg0On;
    this.bg1On = bg1On;
  }

  /// <summary>
  /// Convert bits to the custom type.
  /// </summary>
  public static DisplayMode FromBits(byte bits)
  {
    return new DisplayMode
    {
      bg0On = (bits & 0b01) != 0,
      bg1On = (bits & 0b10) != 0,
    };
  }

  /// <summary>
  /// Convert the custom type into bits.
  /// </summary>
  public byte ToBits()
  {
    byte bits = 0;
    if (bg0On) bits |= 0b01;
    if (bg1On) bits |= 0b10;
    return bits;
  }
}

class Program
{
  static void Main(string[] args)
  {
    // Creating the display mode custom type.
    var displayMode = new DisplayMode
    {
      bg0On = true,
      bg1On = false
    };

    // Building the display control
    var displayControl = new DisplayControl.DisplayControlBuilder()
      .WithBgMode(0b1)
      .WithDisplayMode(displayMode)
      .WithObjCharVramMapping(true)
      .Build();

    // Converting into bits.
    var bits = displayControl.ToBits();
    Debug.Assert(bits == 0b01110101);
  }
}

🤔 What Other Features Does Bitfields Offer?

Bitfields offers a wide range of features to help you define and work with bitfields.

using System.Diagnostics;
using Bitfields.CSharp;

namespace Main;

// All fields in the bitfield must sum up to the number of bits of the bitfield type
// and the class or struct must be marked as partial.
[Bitfield(BitfieldType.ULong)]
public partial class Bitfield
{
  // Fields without bits specified default to the size of the field type.
  // 8 bits.
  private byte byteField;

  // A field can have specified bits, but the bits must be greater than zero
  // and fit in the field type.
  [Bits(4)] private byte smallByte; // A byte is 8 bits, so 4 bits is valid.

  // A field that is signed, will be sign-extended by the most significant 
  // bit of its type.
  private sbyte signedByte;

  // If you specify bits, the field will be sign-extended by the most significant
  // bit of the specified bits. In this case, the most significant bit of 4 bits.
  // Also signed fields are 2's complement, meaning this field with 4 bits has
  // the value range of `-8` to `7`. You can add more bits to increase this
  // range!
  [Bits(4)] 
  private sbyte smallSignedByte;

  // A field can be a bool type.
  private bool boolField;

  // A field can have a default value, which must fit in the field type.
  private byte fieldWithDefault = 0x1F;

  // A field can have a default value and specified bits. The default value
  // must fit in the specified bits or a compile error will occur.
  [Bits(4)]
  private byte fieldWithBitsDefault = 0xF;

  // Nested bitfields are supported, but must have their bits and
  // its type specified and implement the `static [CustomType] FromBits([type] bits)`
  // and `[type] ToBits()` methods.
  [Bits(3, CustomFieldType.Byte)] 
  private NestedBitfield nestedField;

  // Custom types are supported, but must have their bits and
  // type specified and implement the `static [CustomType] FromBits([type] bits)`
  // and `[type] ToBits()` methods.
  [Bits(3, CustomFieldType.Byte)] 
  private CustomType customType;

  // Custom enum types are supported. All custom types are assumed ot be a class
  // of struct, but enums must be specified using the `CustomFieldBase.Enum` param.
  // The enum must have an enum named `BfBase` and a extension class that implements
  // both the static `static [CustomEnumType] FromBits(this [CustomEnumType] _, byte val)`
  // and `static [type] ToBits(this [CustomEnumType] val)` methods.
  [Bits(3, CustomFieldType.Byte, CustomFieldBase.Enum)]
  private CustomEnumType customEnumType;
  
  // Fields suffixed with "_" are padding fields, which are inaccessible.
  [Bits(18)] 
  private int padding_ = 0x3;
}

[Bitfield(BitfieldType.Byte)]
public partial struct NestedBitfield
{
  private byte field;
}

// Custom types must have 2 methods, `static [CustomType] FromBits([type] bits)`
// and `[type] ToBits()` methods.
public struct CustomType
{
  private byte health;

  public CustomType(byte health)
  {
    this.health = health;
  }

  public static CustomType FromBits(byte bits)
  {
    return new CustomType(bits);
  }

  public byte ToBits()
  {
    return health;
  }
}

// Custom enum types must have an enum named `BfBase`, its value doesn't matter.
public enum CustomEnumType {
    BfBase = int.MaxValue,
    A = 1,
    B = 2,
    C = 3,
}

// Custom enum types must have a extension class that implements the `FromBits` and `ToBits`.
public static class CustomEnumTypeExtension
{
    public static CustomEnumType FromBits(this CustomEnumType _, byte val)
    {
        return val switch
        {
            1 => CustomEnumType.A,
            2 => CustomEnumType.B,
            3 => CustomEnumType.C,
            _ => CustomEnumType.A
        };
    }

    public static byte ToBits(this CustomEnumType val)
    {
        return (byte)val;
    }
}

class Program
{
  static void Main(string[] args)
  {
    // Usage:
    // Creates a new bitfield using a builder pattern, unset fields default to 0 
    // or their provided default value.
    var bitfield = new Bitfield.BitfieldBuilder()
      .WithByteField(5)
      .WithSmallByte(0xF)
      .WithCustomType(CustomType.FromBits(0x3))
      .WithCustomEnumType(CustomEnumType.A)
      .WithSignedByte(-5)
      .WithSmallSignedByte(0xF)
      .Build();

    // var bitfield = new Bitfield(); // Bitfield with defaults
    // var bitfield = new Bitfield(withoutDefaults: true); // Bitfield without defaults

    // var builder = new Bitfield.BitfieldBuilder(); // Builder with defaults
    // var builder = new Bitfield.BitfieldBuilder(withoutDefaults: true); // Builder without defaults

    // var builder = bitfield.ToBuilder(); // Builder from existing bitfield

    // Accessing fields:
    var byteField = bitfield.GetByteField();
    Debug.Assert(byteField == 5);
    var smallSignedByte = bitfield.GetSmallSignedByte(); // Signed-types are sign-extended by the MSB.
    Debug.Assert(smallSignedByte == -1);
    var enumType = bitfield.GetCustomEnumType();
    Debug.Assert(enumType == CustomEnumType.A);

    // Converting into bits:
    var bits = bitfield.ToBits();

    // Converting from bits:
    var bitfieldFromBits = Bitfield.FromBits(0x3); // Converts from bits
    // var fromBitsWithDefaults = Bitfield.FromBitsWithDefaults(0x3); // Converts from bits with defaults

    // Constants:
    Debug.Assert(Bitfield.BYTE_FIELD_BITS == 8); // Number of bits of the field.
    Debug.Assert(Bitfield.BOOL_FIELD_OFFSET == 0); // The offset of the field in the bitfield.
  }
}

Bitfield Types

A bitfield can represent unsigned types (Byte, UShort, Char, UInt, ULong). The field bits of a bitfield must add up to the number of bits of the bitfield type.

using Bitfields.CSharp;

[Bitfield(BitfieldType.Byte)]
public partial class BitfieldByte
{
  private byte field;
}

[Bitfield(BitfieldType.UShort)]
public partial class BitfieldUShort
{
  private ushort field;
}

[Bitfield(BitfieldType.ULong)]
public partial class BitfieldULong
{
  private ulong field;
}

Bitfield Field Types

A bitfield field can be any unsigned (byte, short, char, uint, ulong), signed type (sbyte, short, int, long), or a custom type that implements the methods FromBits and ToBits.

Signed types are treated as 2's complement data types, meaning the most significant represents the sign bit. For example, if you had a field with 5 bits, the value range would be -16 to 15. The more bits you include, the larger the value range.

A field can have a default value which must fit in the field type and the specified bits.

using System.Diagnostics;
using Bitfields.CSharp;

namespace Main;

[Bitfield(BitfieldType.UInt)]
public partial class Bitfield
{
  [Bits(8)]
  private byte a = 0xFF;
  [Bits(8)] 
  private sbyte b = -127;
  [Bits(4)] 
  private sbyte cSignExtended = 9;
  [Bits(8, CustomFieldType.Byte)]
  private CustomType customType = CustomType.FromBits(0x11);
  [Bits(2, CustomFieldType.Byte, CustomFieldBase.Enum)]
  private CustomEnumType customEnumType = CustomEnumType.A;
  [Bits(2)]
  private byte padding_ = 0;
}

public class CustomType
{
  private byte val;

  private CustomType(byte val)
  {
    this.val = val;
  }
  
  public static CustomType FromBits(byte val)
  {
    return new CustomType(val);
  }

  public byte ToBits()
  {
    return val;
  }
}

// Custom enum types must have an enum named `BfBase`, its value doesn't matter.
public enum CustomEnumType {
    BfBase = int.MaxValue,
    A = 1,
    B = 2,
    C = 3,
}

// Custom enum types must have a extension class that implements the `FromBits` and `ToBits`.
public static class CustomEnumTypeExtension
{
    public static CustomEnumType FromBits(this CustomEnumType _, byte val)
    {
        return val switch
        {
            1 => CustomEnumType.A,
            2 => CustomEnumType.B,
            3 => CustomEnumType.C,
            _ => CustomEnumType.A
        };
    }

    public static byte ToBits(this CustomEnumType val)
    {
        return (byte)val;
    }
}

class Program
{
  static void Main(string[] args)
  {
    var bitfield = new Bitfield();
    
    Debug.Assert(bitfield.GetA() == 0xFF);
    Debug.Assert(bitfield.GetB() == -127);
    Debug.Assert(bitfield.GetCSignExtended() == -7);
    Debug.Assert(bitfield.GetCustomType().ToBits() == CustomType.FromBits(0x11).ToBits());
    Debug.Assert(bitfield.GetCustomEnumType() == CustomEnumType.A);
  }
}

Constructing a Bitfield

A bitfield can be constructed using its Bitfield() and Bitfield(bool withoutDefaults) constructors. The former initializes the bitfield with default values, while the latter initializes the bitfield without default values, except for padding fields which always keep their default value or 0.

A bitfield can also be constructed using a fluent builder pattern using the new <Bitfield>Builder() or new <Bitfield>Builder(bool withoutDefaults) constructors. They operate the same as the Bitfield() and Bitfield(bool withoutDefaults) constructors.

using System.Diagnostics;
using Bitfields.CSharp;

namespace Main;

[Bitfield(BitfieldType.UInt)]
public partial class Bitfield
{
  [Bits(8)]
  private byte a = 0x12;
  [Bits(8)] 
  private byte b = 0x34;
  [Bits(8)]
  private byte c = 0x56;
  [Bits(8)]
  private byte d_ = 0x78; // Padding field
}

class Program
{
  static void Main(string[] args)
  {
    var bitfield = new Bitfield();
    Debug.Assert(bitfield.GetA() == 0x12);
    Debug.Assert(bitfield.GetB() == 0x34);
    Debug.Assert(bitfield.GetC() == 0x56);
    Debug.Assert(bitfield.ToBits() == 0x78563412);

    var bitfieldWithoutDefaults = new Bitfield(withoutDefaults: true);
    Debug.Assert(bitfieldWithoutDefaults.GetA() == 0);
    Debug.Assert(bitfieldWithoutDefaults.GetB() == 0);
    Debug.Assert(bitfieldWithoutDefaults.GetC() == 0);
    Debug.Assert(bitfieldWithoutDefaults.ToBits() == 0x78000000);

    var customBitfield = new Bitfield.BitfieldBuilder()
      .WithA(0x12)
      .WithB(0x34)
      .WithC(0x56)
      .Build();
    Debug.Assert(customBitfield.GetA() == 0x12);
    Debug.Assert(customBitfield.GetB() == 0x34);
    Debug.Assert(customBitfield.GetC() == 0x56);
    Debug.Assert(customBitfield.ToBits() == 0x78563412);
  }
}

To Builder

A constructed bitfield can be converted back to a builder using the ToBuilder() method.

using Bitfields.CSharp;

namespace Main;

[Bitfield(BitfieldType.UInt)]
public partial class Bitfield
{
  [Bits(8)]
  private byte a = 0x12;
  [Bits(8)] 
  private byte b = 0x34;
  [Bits(8)]
  private byte c = 0x56;
  [Bits(8)]
  private byte d_ = 0x78; // Padding field
}

class Program
{
  static void Main(string[] args)
  {
    var bitfield = new Bitfield();

    var builder = bitfield.ToBuilder();
  }
}

Bitfield Conversions

A bitfield can be converted from bits using the FromBits() or FromBitsWithoutDefaults() methods. The former ignores default values, while the latter respects them. Padding fields are always 0 or their default value. The bitfield can also be converted to bits using the ToBits() function.

using System.Diagnostics;
using Bitfields.CSharp;

namespace Main;

[Bitfield(BitfieldType.UInt)]
public partial class Bitfield
{
  [Bits(8)]
  private byte a = 0x12;
  [Bits(8, CustomFieldType.Byte)] 
  private CustomType b;
  private byte c;
  private byte d_ = 0x78;
}

public class CustomType
{
  private byte val;

  private CustomType(byte val)
  {
    this.val = val;
  }
  
  // Only takes the first bit of the val.
  public static CustomType FromBits(byte val)
  {
    return new CustomType((byte)(val & 1));
  }

  public byte ToBits()
  {
    return val;
  }
}

class Program
{
  static void Main(string[] args)
  {
    var bitfield = Bitfield.FromBits(0x11223344);
    Debug.Assert(bitfield.GetA() == 0x44);
    Debug.Assert(bitfield.GetB().ToBits() == 1);
    Debug.Assert(bitfield.GetC() == 0x22);
    Debug.Assert(bitfield.ToBits() == 0x78220144);
    
    var bitfieldRespectDefaults = Bitfield.FromBitsWithDefaults(0x11223344);
    Debug.Assert(bitfieldRespectDefaults.GetA() == 0x12); // Default value respected
    Debug.Assert(bitfieldRespectDefaults.GetB().ToBits() == 1);
    Debug.Assert(bitfieldRespectDefaults.GetC() == 0x22);
    Debug.Assert(bitfieldRespectDefaults.ToBits() == 0x78220112);
  }
}

Field Order

By default, fields are ordered from the least significant bit (lsb) to the most significant bit (msb). The order can be changed by specifying the second BitOrder arg on the bitfield attribute. There are two field orderings, BitOrder.Lsb and BitOrder.Msb.

using System.Diagnostics;
using Bitfields.CSharp;

namespace Main;

[Bitfield(BitfieldType.UInt, BitOrder.Msb)]
public partial class Bitfield
{
  [Bits(8)] 
  private byte a = 0x12;
  [Bits(8)] 
  private byte b = 0x34;
  [Bits(8)]
  private byte c = 0x56;
  [Bits(8)]
  private byte d = 0x78;
}

class Program
{
  static void Main(string[] args)
  {
    var bitfield = new Bitfield();
    Debug.Assert(bitfield.GetA() == 0x12);
    Debug.Assert(bitfield.GetB() == 0x34);
    Debug.Assert(bitfield.GetC() == 0x56);
    Debug.Assert(bitfield.GetD() == 0x78);
    var bits = bitfield.ToBits();
    
    //                      .- a
    //                      |  .- b
    //                      |  |  .- c
    //                      |  |  |  .- d
    Debug.Assert(bits == 0x12_34_56_78);
    Debug.Assert(Bitfield.A_OFFSET == 24); // Offset of the field in the bitfield.
  }
}

Padding Fields

Fields suffixed with an underscore _ are padding fields, which are inaccessible. Meaning the field is always 0/false or a default value. They are useful for padding the bits of a bitfield.

using System.Diagnostics;
using Bitfields.CSharp;

namespace Main;

[Bitfield(BitfieldType.UShort)]
public partial class Bitfield
{
  [Bits(8)] 
  private byte a;
  [Bits(8)] 
  private byte padding_ = 0xFF;
}

class Program
{
  static void Main(string[] args)
  {
    var bitfield = new Bitfield();
    Debug.Assert(bitfield.GetA() == 0);
    // Padding field is inaccessible, so no direct access or setting.
    Debug.Assert(bitfield.ToBits() == 0xFF00);
  }
}

Field Constants

Fields have constants generated for their number of bits and offset in the bitfield.

using System.Diagnostics;
using Bitfields.CSharp;

namespace Main;

[Bitfield(BitfieldType.UInt)]
public partial class Bitfield
{
  [Bits(8)] 
  private byte a = 0x12;
  [Bits(8)] 
  private byte b = 0x34;
  [Bits(8)]
  private byte c = 0x56;
  [Bits(8)]
  private byte d = 0x78;
}

class Program
{
  static void Main(string[] args)
  {
    Debug.Assert(Bitfield.A_OFFSET == 0); // Number of bits of the a field.
    Debug.Assert(Bitfield.A_BITS == 8); // The offset of the a field in the bitfield.
    Debug.Assert(Bitfield.B_OFFSET == 8); // Number of bits of the b field.
    Debug.Assert(Bitfield.B_BITS == 8); // The offset of the b field in the bitfield.
    Debug.Assert(Bitfield.C_OFFSET == 16); // Number of bits of the c field.
    Debug.Assert(Bitfield.C_BITS == 8); // The offset of the c field in the bitfield.
    Debug.Assert(Bitfield.D_OFFSET == 24); // Number of bits of the d field.
    Debug.Assert(Bitfield.D_BITS == 8); // The offset of the d field in the bitfield.
  }
}

⚖️ License

Distributed under the MIT License. See LICENSE for more information.

🤝 Contributing

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the MIT license, shall be licensed as above, without any additional terms or conditions.

🥶 Employment Disclaimer

As of Jan 2025, I am a Google employee; "Bitfields" is my own work, not affiliated with Google, its subsidiaries, nor endorsing any Google-owned products or tools. "Bitfields" was written without any proprietary knowledge, tools, or resources of Google.

Other Versions

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
0.3.2 76 2/1/2025
0.3.1 65 1/31/2025
0.3.0 67 1/30/2025
0.2.0 68 1/28/2025
0.1.2 48 1/24/2025
0.1.1 52 1/24/2025
0.1.0 46 1/24/2025

- Add badges to readme