ctypes.Structure bitfield packing for 64bit field

I need to communicate my python code to an API using a 21 bytes struct that uses bitfields, where one of its fields has 64 bits and starts in the middle of another byte. Is there any way I can reach this using ctypes.Structure? If not, I’d like to leave it as a suggestion to add support for it, maybe someone else could need it. I have looked at the documentation and it says bitfields only work with c_int, which makes me think there is no support for 64 bit fields, but I don’t know if I’m missing something else.

Below I give more details on the behavior I need and what I tried to achieve it using ctypes.Structure. In the end, I left some Python code I wrote (which I guess only works for little endian architectures) to work with the API without ctypes.Structure.

The struct looks like this (in C++):

#pragma pack(1)

struct BitfieldsStruct {
    uint8_t a : 4;
    uint8_t b : 4;
    bool c : 1;
    uint8_t d : 4;
    uint64_t e : 64;
    uint64_t f : 64;
    uint8_t g : 8;
    uint8_t h : 8;
    uint8_t i: 8;
    uint8_t j : 3;
};

The C++ client communicates flawlessly with the API, but I also need a Python client, and I haven’t been able to do it using ctypes.Structure. I have tried setting pack to 1 and also tried different combinations of field types (setting most of them to c_int or only the problematic ones). My first attempt looks like this:

import ctypes


class BitfieldsStruct(ctypes.Structure):
    _pack_ = 1

    _fields_ = [
        ("a", ctypes.c_uint8, 4),
        ("b", ctypes.c_uint8, 4),
        ("c", ctypes.c_bool, 1),
        ("d", ctypes.c_uint8, 4),
        ("e", ctypes.c_uint64, 64),
        ("f", ctypes.c_uint64, 64),
        ("g", ctypes.c_uint8, 8),
        ("h", ctypes.c_uint8, 8),
        ("i", ctypes.c_uint8, 8),
        ("j", ctypes.c_uint8, 3),
    ]

I have tried setting all of them; only ‘c’; only ‘e’; ‘e’ and ‘f’; and ‘c’, ‘d’, ‘e’ and ‘f’ to c_int, and got no improvements. The behavior I needed was a 21 bytes struct without any gaps (not even single bit gaps). The closest I got was a 22 bytes struct with a three bit gap (and all bits after gap were dislocated). Here is some C++ code to print the struct filled with ones:

#include <cstddef>
#include <cstdint>
#include <iostream>
#include <string>

#pragma pack(1)

struct BitfieldsStruct {
    uint8_t a : 4;
    uint8_t b : 4;
    bool c : 1;
    uint8_t d : 4;
    uint64_t e : 64;
    uint64_t f : 64;
    uint8_t g : 8;
    uint8_t h : 8;
    uint8_t i: 8;
    uint8_t j : 3;
};

union GeneralBS {
  BitfieldsStruct decoded;
  std::byte encoded[sizeof(BitfieldsStruct)];
};


int main() {

  GeneralBS exp;
  exp.decoded.a = ~0;
  exp.decoded.b = ~0;
  exp.decoded.c = true;
  exp.decoded.d = ~0;
  exp.decoded.e = ~0ll;
  exp.decoded.f = ~0ll;
  exp.decoded.g = ~0;
  exp.decoded.h = ~0;
  exp.decoded.i = ~0;
  exp.decoded.j = ~0;

  std::string outp = "";

  std::cout << "size " << sizeof(BitfieldsStruct) << std::endl;

  for(int i=0; i<sizeof(BitfieldsStruct); i++) {
    auto c = (unsigned int) exp.encoded[i];

    for(int j=0; j<8; j++) {
      outp += ((1 << (8-j-1)) & c) ? '1' : '0';
    }

		outp += ' ';
  }

  std::cout << outp << std::endl;
}

And this is its output:

size 21
11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111

Now here is the closest I got in Python:

import ctypes


class BitfieldsStruct(ctypes.Structure):
    _pack_ = 1

    _fields_ = [
        ("a", ctypes.c_uint8, 4),
        ("b", ctypes.c_uint8, 4),
        ("c", ctypes.c_bool, 1),
        ("d", ctypes.c_uint8, 4),
        ("e", ctypes.c_uint64, 64),
        ("f", ctypes.c_uint64, 64),
        ("g", ctypes.c_uint8, 8),
        ("h", ctypes.c_uint8, 8),
        ("i", ctypes.c_uint8, 8),
        ("j", ctypes.c_uint8, 3),
    ]
            
if __name__ == '__main__':
    pack = BitfieldsStruct()
    pack.a = ~0
    pack.b = ~0
    pack.c = True
    pack.d = ~0
    pack.e = 0xFFFFFFFFFFFFFFFF
    pack.f = 0xFFFFFFFFFFFFFFFF
    pack.g = ~0
    pack.h = ~0
    pack.i = ~0
    pack.j = ~0

    res = bytes(pack)

    print("size", len(res))
    print(" ".join(bin(b) for b in res))

And its output:

size 22
0b11111111 0b11111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b111

The result has a 3-bit gap in the second byte.

Here is the Python code I wrote as a workaround:

from typing import Dict, List, Tuple


class PackedBitfields:
    _fields_: List[Tuple[str, int]]
    _bytemask = (1 << 8) - 1
    _field_sizes_ : Dict[str, int] = dict()

    def __init__(self):
        self.__setattr__("_fields_", self._fields_)


    def __setattr__(self, attrname, value) -> None:
        if attrname == "_fields_":
            for field in value:
                self._field_sizes_[field[0]] = field[1]
            for field in value:
                self.__setattr__(field[0], 0)
            super().__setattr__(attrname, value)
        elif attrname in self._field_sizes_:
            fs = self._field_sizes_[attrname]
            fieldmask = (1 << fs) - 1
            maskedval = value & fieldmask
            super().__setattr__(attrname, maskedval)
        else:
            super().__setattr__(attrname, value)

    def __bytes__(self) -> bytes:
        currbyte_rem = 8
        currbyte = 0

        res: List[int] = []

        for field in self._fields_:
            remval = getattr(self, field[0])
            field_remsize = field[1]

            while(field_remsize > 0):
                if(currbyte_rem <= field_remsize):
                    curropmask = (self._bytemask) >> (8 - currbyte_rem)
                    maskedval = (remval & curropmask) << (8 - currbyte_rem)
                    currbyte |= maskedval
                    res.append(currbyte)
                    field_remsize -= currbyte_rem
                    remval >>= currbyte_rem
                    currbyte_rem = 8
                    currbyte = 0
                    continue
                else:
                    curropmask = (self._bytemask) >> (8 - field_remsize)
                    maskedval = (remval & curropmask) << (8 - currbyte_rem)
                    currbyte |= maskedval
                    currbyte_rem -= field_remsize
                    field_remsize = 0
                    remval = 0

        if(currbyte_rem < 8):
            res.append(currbyte)

        return bytes(res)




class BFStruct(PackedBitfields):
    _fields_ = [
        ("a", 4),
        ("b", 4),
        ("c", 1),
        ("d", 4),
        ("e", 64),
        ("f", 64),
        ("g", 8),
        ("h", 8),
        ("i", 8),
        ("j", 3),
    ]

And its output (when plugged in that example code):

size 21
0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111 0b11111111

I’m using ubuntu 22.04 amd64; Python 3.8.18 and g++ 11.4.0.

ctypes is following the C standard here, and that disallows the kind of overlapping between bitfields of different types you are apparently describing. I don’t know why your C++ compiler does this, but I don’t think there is any way to convince ctypes to do this nor should this be added as an option. There are other libraries (although I can’t name any of the top of my head) or you can easily write a short piece of code that does this yourself (which is what I have done whenever I needed to. For example you can use a third party bitarray library to help you).

There is also the struct module in the standard library. It may be come handy in this situation :slight_smile:

1 Like

Not really, it can’t do sub byte precision either.

You can use struct for convenience. You’ll become very accustomed to working with bitwise operators after one or two examples.

data = struct.pack(
    '!BBQQBBB',
    (s.a << 4) | (s.b & 0b1111),
    ((int(s.c) & 0x1) << 7) | (s.d << 3 & 0xFF) | (s.e >> 61 & 0b111),
    (s.e << 3 & 0xFFFFFFFFFFFFFFFF) | (s.f >> 61 & 0b111),
    (s.f << 3 & 0xFFFFFFFFFFFFFFFF) | (s.g >> 5 & 0b111),
    (s.g << 3 & 0xFF) | (s.h >> 5 & 0b111),
    (s.h << 3 & 0xFF) | (s.i >> 5 & 0b111),
    (s.i << 3 & 0xFF) | (s.j & 0b111)
)
Complete code
import ctypes
import struct


# Define the BitfieldsStruct class
class BitfieldsStruct(ctypes.Structure):
    _fields_ = [
        ("a", ctypes.c_uint8, 4),
        ("b", ctypes.c_uint8, 4),
        ("c", ctypes.c_bool, 1),
        ("d", ctypes.c_uint8, 4),
        ("e", ctypes.c_uint64, 64),
        ("f", ctypes.c_uint64, 64),
        ("g", ctypes.c_uint8, 8),
        ("h", ctypes.c_uint8, 8),
        ("i", ctypes.c_uint8, 8),
        ("j", ctypes.c_uint8, 3),
    ]


# Create an instance of BitfieldsStruct
s = BitfieldsStruct()
s.a = ~0
s.b = ~0
s.c = True
s.d = ~0
s.e = 0xFFFFFFFFFFFFFFFF
s.f = 0xFFFFFFFFFFFFFFFF
s.g = ~0
s.h = ~0
s.i = ~0
s.j = ~0

# Pack the values into bytes
data = struct.pack(
    '!BBQQBBB',
    (s.a << 4) | (s.b & 0b1111),
    ((int(s.c) & 0x1) << 7) | (s.d << 3 & 0xFF) | (s.e >> 61 & 0b111),
    (s.e << 3 & 0xFFFFFFFFFFFFFFFF) | (s.f >> 61 & 0b111),
    (s.f << 3 & 0xFFFFFFFFFFFFFFFF) | (s.g >> 5 & 0b111),
    (s.g << 3 & 0xFF) | (s.h >> 5 & 0b111),
    (s.h << 3 & 0xFF) | (s.i >> 5 & 0b111),
    (s.i << 3 & 0xFF) | (s.j & 0b111)
)

# Print the packed data
print("Packed data:", data)
print("Length:", len(data))
print(" ".join(bin(byte) for byte in data))

Shouldn’t this be:

s.a | (s.b << 4)  

…, since b would be the upper eight bits and a is already the lower 4 bits?

Here is a small simple test in C for clarification:

#include <stdbool.h>

union
{
   struct {
           uint8_t a:  4; // 0 ... 3 bits
           uint8_t b:  4; // 4 .. 7 bits
           bool    c:  1; // bit 8
           uint8_t d:  4; // 9 ... 12 bits
           uint8_t e:  4; // 13 .. 16 bits
           uint8_t f:  4; // 17 .. 20 bits
   
   } fields;
   
   uint64_t val; 
   
} MSG;

uint8_t temp1 = 0; // Used to read in field `a`
uint8_t temp2 = 0; // Used to read in field `b`
uint8_t temp3 = 0; // Used to OR bits `a` and `b`

int main(void) 
{   
    MSG.val = 0xFA7A;      // Defines entire variable
    asm( "nop" );
    asm( "nop" );    
    temp1 = MSG.fields.a;  // Read value of field `a` = `A`
    temp2 = MSG.fields.b;  // Read value of field `b` = `7`
    
    temp3 = MSG.fields.a | (MSG.fields.b << 4);  //  = 0x7A

while(1)
    {       
        asm( "nop" );
        asm( "nop" );
        asm( "nop" );
    }
    
    return 0;
}

The same principle applied to other bits and bit fields.

That would result in bbbbaaaa. I am left shifting all bits.
aaaa bbbb c dddd eeeee...
aaaabbbb cddddeee ee...
…but how C++ does it depends on the implementation. I only showed an example. My advice is to avoid packing bits in Python; abstract away the usage of C++ struct from Python.

I just replicated the code in C and got the same result of C++. Here is the C code:

#include <stdint.h>
#include <stdio.h>
#pragma pack(1)

typedef unsigned char bool;
typedef unsigned char byte;

typedef struct  {
    uint8_t a : 4;
    uint8_t b : 4;
    bool c : 1;
    uint8_t d : 4;
    uint64_t e : 64;
    uint64_t f : 64;
    uint8_t g : 8;
    uint8_t h : 8;
    uint8_t i: 8;
    uint8_t j : 3;
} BitfieldsStruct;

typedef union {
  BitfieldsStruct decoded;
  byte encoded[sizeof(BitfieldsStruct)];
} GeneralBS;


int main() {

  GeneralBS exp;
  exp.decoded.a = ~0;
  exp.decoded.b = ~0;
  exp.decoded.c = 1;
  exp.decoded.d = ~0;
  exp.decoded.e = ~0ll;
  exp.decoded.f = ~0ll;
  exp.decoded.g = ~0;
  exp.decoded.h = ~0;
  exp.decoded.i = ~0;
  exp.decoded.j = ~0;

  char outp[10];
  outp[8] = ' ';
  outp[9] = 0;

  printf("(in C) size %lu \n", sizeof(BitfieldsStruct));

  for(int i=0; i<sizeof(BitfieldsStruct); i++) {
    byte c = exp.encoded[i];

    for(int j=0; j<8; j++) {
      outp[j] = ((1 << (8-j-1)) & c) ? '1' : '0';
    }

    printf("%s", outp);
  }
  printf("\n");

  return 0;
}

And here its output:

(in C) size 21 
11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 

And if I change the value of the field ‘e’ to 0ll, it outputs this:

(in C) size 21 
11111111 00011111 00000000 00000000 00000000 00000000 00000000 00000000 00000000 11100000 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 

Aha, then your C compiler is doing something weird. Which one are you using?

(in C) size 22
11111111 00111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 01111111

is what I get with gcc.

I’m using gcc lol. This is the output of gcc --version:

gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0

Aha, interesting, apparently this is a difference between OSes (on windows I get 22 bytes). This means you shouldn’t use this as an interchange format. But I guess ctypes should correct it’s behavior here to match up with the ABI? We should find some actual documentation of the ABI for bit fields then. And apparently I am misremembering the C standard forbidding this? Gotta check that.

Yeah, I should probably use something more portable like protobuf.

About the C standard, it seems like bit packing in structs is implementation specific. I don’t know if this is the correct doc, but I’m referring to https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf. In section 6.7.2.1, paragraph 5 states that the types allowed for bit-fields are implementation-defined:

A bit-field shall have a type that is a qualified or unqualified version of _Bool, signed int, unsigned int, or some other implementation-defined type. It is implementation-defined whether atomic types are permitted.

And then what I understood from paragraph 11 of the same section is that allowing insertion of part of a field in the remaining bits of a unit is implementation-defined:

An implementation may allocate any addressable storage unit large enough to hold a bit-field. If enough space remains, a bit-field that immediately follows another bit-field in a structure shall be packed into adjacent bits of the same unit. If insufficient space remains, whether a bit-field that does not fit is put into the next unit or overlaps adjacent units is implementation-defined. The order of allocation of bit-fields within a unit (high-order to low-order or low-order to high-order) is implementation-defined. The alignment of the addressable storage unit is unspecified.