Представление битовых полей объединения с использованием StrucLayout и FieldOffset в C#

Я понимаю, что для представления объединений в C# мне нужно использовать атрибут StructLayout[LayoutKind.Explicit)] и [FieldOffset(x)], чтобы указать смещение байта внутри объединения. Тем не менее, у меня есть следующий союз, который я хочу представить, и атрибут FieldOffset смещается только на размер байта.

union _myUnion
{
     unsigned int info;
     struct
     {
          unsigned int flag1:1 // bit 0
          unsigned int flag2:1 // bit 1
          unsigned int flag3:1 // bit 2
          unsigned int flag4:1 // bit 3
          unsigned int flag5:1 // bit 4
          unsigned int flag6:1 // bit 5
          .
          .
          .
          unsigned int flag31:1 // bit 31
     }
}

Как вы можете видеть для внутренней структуры в объединении, я не могу использовать FieldOffset, так как мне нужно что-то, что может немного сместиться.

Есть ли этому решение? Я пытаюсь вызвать функцию DLL, и одна из структур данных была определена как таковая, и у меня закончились идеи о том, как лучше всего представить эту объединенную структуру.

3 ответа

Нет необходимости в союзе там; одно поле + свойство для данных, 8 свойств, которые выполняют побитовые операции "сдвига", например:

public uint Value {get;set;}

public uint Flag2 {
   get { return Value >> 2; }
}

и т. д. Я бы тоже подумал, что ты хочешь здесь бул?

Обычно я бы сказал: не создавайте изменяемые структуры. PInvoke может (я не уверен) быть подходящим сценарием для этого, поэтому я его проигнорирую:)

Если значение действительно использует более 32 бит, рассмотрите возможность переключения вспомогательного поля на ulong.

Да, ты можешь это сделать. Вы находитесь на правильном пути, ответ заключается в использовании BitVector32 вместе с атрибутами FieldOffset и StructLayout. Однако при этом необходимо помнить о нескольких вещах:

  1. Вам необходимо явно указать размер переменных, которые будут содержать данные. На этот первый пункт очень важно обратить внимание. Например, в приведенном выше вопросе вы указываете информацию как unsigned int. Какой размер без знака int? 32 бита? 64 бита? Это зависит от версии ОС, в которой работает данная конкретная версия.NET (это может быть.NET Core, Mono или Win32/Win64).

  2. Что это за порядок байтов или порядок следования битов? Опять же, мы можем работать на любом типе оборудования (например, Mobile/Xamarin, а не только на ноутбуке или планшете) - поэтому вы не можете предполагать порядок следования битов Intel.

  3. Мы хотим избежать любого управления памятью, которое зависит от языка, или в языке POD (обычные старые данные) на языке C/C++. Это будет означать придерживаться только типов значений.

Я собираюсь сделать предположение, основанное на вашем вопросе и спецификации флагов 0-31, что sizeof (int) == 32.

Хитрость заключается в том, чтобы обеспечить следующее:

  1. Все данные выровнены по байту.
  2. Битовые поля и информационное поле выравниваются на одной границе байта.

Вот как мы это делаем:

[StructLayout(LayoutKind.Explicit, Size = 1, CharSet = CharSet.Ansi)]
public struct MyUnion
{
    #region Lifetime

    /// <summary>
    /// Ctor
    /// </summary>
    /// <param name="foo"></param>
    public MyUnion(int foo)
    {
        // allocate the bitfield
        info = new BitVector32(0);

        // initialize bitfield sections
        flag1 = BitVector32.CreateSection(1);
        flag2 = BitVector32.CreateSection(1, flag1);
        flag3 = BitVector32.CreateSection(1, flag2);
    }

    #endregion

    #region Bifield

    // Creates and initializes a BitVector32.
    [FieldOffset(0)]
    private BitVector32 info;

    #endregion

    #region Bitfield sections

    /// <summary>
    /// Section - Flag 1
    /// </summary>
    private static BitVector32.Section flag1;

    /// <summary>
    /// Section - Flag 2
    /// </summary>
    private static BitVector32.Section flag2;

    /// <summary>
    /// Section - Flag 3
    /// </summary>
    private static BitVector32.Section flag3;

    #endregion

    #region Properties

    /// <summary>
    /// Flag 1
    /// </summary>
    public bool Flag1
    {
        get { return info[flag1] != 0; }
        set { info[flag1] = value ? 1 : 0; }
    }

    /// <summary>
    /// Flag 2
    /// </summary>
    public bool Flag2
    {
        get { return info[flag2] != 0; }
        set { info[flag2] = value ? 1 : 0; }
    }

    /// <summary>
    /// Flag 1
    /// </summary>
    public bool Flag3
    {
        get { return info[flag3] != 0; }
        set { info[flag3] = value ? 1 : 0; }
    }

    #endregion

    #region ToString

    /// <summary>
    /// Allows us to represent this in human readable form
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return $"Name: {nameof(MyUnion)}{Environment.NewLine}Flag1: {Flag1}: Flag2: {Flag2} Flag3: {Flag3}  {Environment.NewLine}BitVector32: {info}{Environment.NewLine}";
    }

    #endregion
}

Обратите особое внимание на конструктор. По определению, вы не можете определить конструктор по умолчанию для структур в C#. Однако нам нужен какой-то способ убедиться, что объект BitVector32 и его разделы правильно инициализированы перед использованием. Мы достигаем этого, требуя конструктор, который принимает фиктивный целочисленный параметр, и инициализируем объект следующим образом:

    /// <summary>
    /// Main entry point
    /// </summary>
    /// <param name="args"></param>
    static void Main(string[] args)
    {
        // brew up one of these...
        var myUnion = new MyUnion(0)
        {
            Flag2 = true
        };

Кстати, вы никоим образом не ограничены отдельными битовыми полями - вы можете определить битовое поле любого размера. Например, если бы я изменил ваш пример на:

union _myUnion
{
    unsigned int info;
    struct
    {
        unsigned int flag1 : 3 // bit 0-2
        unsigned int flag2 : 1 // bit 3
        unsigned int flag3 : 4 // bit 4-7
            .
            .
            .
        unsigned int flag31 : 1 // bit 31
    }
}

Я бы просто изменил свой класс на:

[StructLayout(LayoutKind.Explicit, Size = 1, CharSet = CharSet.Ansi)]
public struct MyUnion2
{
    #region Lifetime

    /// <summary>
    /// Ctor
    /// </summary>
    /// <param name="foo"></param>
    public MyUnion2(int foo)
    {
        // allocate the bitfield
        info = new BitVector32(0);

        // initialize bitfield sections
        flag1 = BitVector32.CreateSection(0x07);
        flag2 = BitVector32.CreateSection(1, flag1);
        flag3 = BitVector32.CreateSection(0x0f, flag2);
    }

    #endregion

    #region Bifield

    // Creates and initializes a BitVector32.
    [FieldOffset(0)]
    private BitVector32 info;

    #endregion

    #region Bitfield sections

    /// <summary>
    /// Section - Flag1
    /// </summary>
    private static BitVector32.Section flag1;

    /// <summary>
    /// Section - Flag2
    /// </summary>
    private static BitVector32.Section flag2;

    /// <summary>
    /// Section - Flag3
    /// </summary>
    private static BitVector32.Section flag3;

    #endregion

    #region Properties

    /// <summary>
    /// Flag 1
    /// </summary>
    public int Flag1
    {
        get { return info[flag1]; }
        set { info[flag1] = value; }
    }

    /// <summary>
    /// Flag 2
    /// </summary>
    public bool Flag2
    {
        get { return info[flag2] != 0; }
        set { info[flag2] = value ? 1 : 0; }
    }

    /// <summary>
    /// Flag 1
    /// </summary>
    public int Flag3
    {
        get { return info[flag3]; }
        set { info[flag3] = value; }
    }

    #endregion

    #region ToString

    /// <summary>
    /// Allows us to represent this in human readable form
    /// </summary>
    /// <returns></returns>
    public override string ToString()
    {
        return $"Name: {nameof(MyUnion2)}{Environment.NewLine}Flag1: {Flag1}: Flag2: {Flag2} Flag3: {Flag3}  {Environment.NewLine}BitVector32: {info}{Environment.NewLine}";
    }

    #endregion
}

Последнее слово об этой теме... должно быть очевидно, что это должно быть сделано только в том случае, если вы абсолютно ДОЛЖНЫ сделать это. Понятно, что для этого требуются специальные знания вашей среды ОС, языка, на котором вы работаете, соглашения о вызовах и множества других хрупких требований.

В любом другом контексте здесь так много запахов кода, что это явно кричит о непереносимости. Но из контекста вашего вопроса я бы предположил, что все дело в том, что вам нужно работать близко к аппаратному обеспечению и нуждаться в такой точности.

Пусть покупатель будет бдителен!

Наиболее элегантным решением для этого является использование флагов enum

Лучше использовать Uint вместо Int

[Flags]
public enum MyEnum
    : uint
{
    None=0,
    Flag1=1,
    Flag2=1<<1,
    Flag3=1<<2,
    // etc
    Flag32=1<<31
}

После этого вы можете использовать int как enum и как uint

MyEnum value=MyEnum.Flag1|MyEnum.Flag2;
uint uintValue=(uint)value;
// uintValue=3

Это будет обычно маршал от PInvoke

Другие вопросы по тегам