There has got to be a better way to deal with bitfields in C

I just encountered a problem involving this MacOS header file:

typedef struct {
	natural_t                     pad1;
	mach_msg_size_t               pad2;
	unsigned int                  pad3 : 24;
	mach_msg_descriptor_type_t    type : 8;
} mach_msg_type_descriptor_t;
xnu_static_assert_struct_size(mach_msg_type_descriptor_t, 12);

After translation, the code becomes:

// /home/cleong/.zvm/0.16.0/lib/libc/include/any-darwin-any/mach/message.h:296:32: warning: struct demoted to opaque type - has bitfield
pub const mach_msg_type_descriptor_t = opaque {};
comptime {
    if (!(@sizeOf(mach_msg_type_descriptor_t) == @as(c_ulong, 12))) @compileError("static assertion failed \"struct changed size unexpectedly\"");
}

So any file that happens to include this header is untranslatable. Demoting structs with bitfields to opaque is not a viable solution. It just breaks too much stuff.

1 Like

tracking issue: https://codeberg.org/ziglang/translate-c/issues/179

it’s just unimplemented

2 Likes

Layout of bitfields in C is compiler-dependent, so there isn’t general solution.

You can probably simulate this manually using packed struct or bitset after decoding what exactly the C code actually expects.

1 Like

C translation is given the target, which includes the ABI, which tells bitfield layout. So there is a solution that works perfectly fine.

Sorry, edited the post to be less harsh.

2 Likes

Yeah, the tracking bug describes the general solution. Lots of research and testing because start of the art elsewhere appears to be buggy/inaccurate.

Meanwhile until that work happens, you’re left with doing it manually and ungloriously to get past it for your use case, or contributing the fix, which would be blogworthy reputation building :-).

As someone who recently started contributing to translate-c, this seems like a neat issue to work on ngl. So I’m giving it a shot.

7 Likes

Would also love to see bitfield support in translate c. Current work around is to reimplement the struct I need in Zig, and insert if guards around the struct in C to replace it with another definition that has the right size.

Tbf, in the Zig ecosystem the solution should be “whatever Clang does”.

PS: C enums and bools technically have the same problem of the memory layout being implementation-defined behaviour. For bools since C99 it’s commonly 1 byte, but for enums some older MSVC versions used to pack small enum values into 16-bits while all other compilers used 32-bits (that’s why in some C/C++ headers you’ll sometimes see a special enum item with a value 0x7FFFFFFF which forced MSVC to use 32 bits for the underlying type).

PPS: …and AFAIK the memory layout of structs isn’t defined at all in the C standard, so theoretically it’s not possible to build a bindings shim without knowing the C compiler and build options. But thankfully in practice it still works just fine at least for the subset of popular C compilers (gcc vs clang vs msvc), otherwise my bindings-generation solution I created for the sokol headers wouldn’t actually work (I don’t support bitfields and a couple of other C/C++ features either though, but that’s ok because I also control the C API of the libraries I create the bindings for).

2 Likes

Switching C translation from @cImport as part of the language to a separate package came with some pain and ecosystem churn (some of which is still ongoing), but here we see one of the benefits: if you get such an implementation working, you and others too can immediately start using it in your project without waiting for it to be upstreamed.

3 Likes

It’s not whatever clang does - it’s determined by the ABI part of the target. That’s the purpose of it. Some people say “C doesn’t have a standard ABI” which is true in the sense that it’s not standard across compilers. So, we simply declare which C compiler’s ABI we are matching, and voilà! Stable ABI.

2 Likes

Are the Intel and Waterloo C compilers still around? Haven’t heard much of them in a while.

I just wrote a small program that uses zig cc to compile a C file for various targets then extract the contents of the symbols from the object files. It then display the bit patterns. Given the following C code:

struct some_struct {
    int a : 3;
    int b : 4;
    int c;
    int d : 1;
};

struct some_struct struct_a = { 1, 2, 0xA0B0C0D0, 1 };
int struct_a_size = sizeof(struct_a);

You get the following output:

[struct_a]
x86-linux: 12
10001000 00000000 00000000 00000000 00001011 00000011 00001101 00000101 10000000 00000000 00000000 00000000 
x86-windows: 12
10001000 00000000 00000000 00000000 00001011 00000011 00001101 00000101 10000000 00000000 00000000 00000000 
x86-macos: 12
10001000 00000000 00000000 00000000 00001011 00000011 00001101 00000101 10000000 00000000 00000000 00000000 
x86_64-linux: 12
10001000 00000000 00000000 00000000 00001011 00000011 00001101 00000101 10000000 00000000 00000000 00000000 
x86_64-linux-gnu: 12
10001000 00000000 00000000 00000000 00001011 00000011 00001101 00000101 10000000 00000000 00000000 00000000 
x86_64-windows: 12
10001000 00000000 00000000 00000000 00001011 00000011 00001101 00000101 10000000 00000000 00000000 00000000 
x86_64-macos: 12
10001000 00000000 00000000 00000000 00001011 00000011 00001101 00000101 10000000 00000000 00000000 00000000 
arm-linux: 12
10001000 00000000 00000000 00000000 00001011 00000011 00001101 00000101 10000000 00000000 00000000 00000000 
aarch64-linux: 12
10001000 00000000 00000000 00000000 00001011 00000011 00001101 00000101 10000000 00000000 00000000 00000000 
aarch64-windows: 12
10001000 00000000 00000000 00000000 00001011 00000011 00001101 00000101 10000000 00000000 00000000 00000000 
aarch64-macos: 12
10001000 00000000 00000000 00000000 00001011 00000011 00001101 00000101 10000000 00000000 00000000 00000000 
riscv32-linux: 12
10001000 00000000 00000000 00000000 00001011 00000011 00001101 00000101 10000000 00000000 00000000 00000000 
riscv64-linux: 12
10001000 00000000 00000000 00000000 00001011 00000011 00001101 00000101 10000000 00000000 00000000 00000000 
powerpc-linux: 12
00100100 00000000 00000000 00000000 00000101 00001101 00000011 00001011 00000001 00000000 00000000 00000000 
powerpc64-linux: 12
00100100 00000000 00000000 00000000 00000101 00001101 00000011 00001011 00000001 00000000 00000000 00000000 
powerpc64le-linux: 12
10001000 00000000 00000000 00000000 00001011 00000011 00001101 00000101 10000000 00000000 00000000 00000000 
mips-linux: 12
00100100 00000000 00000000 00000000 00000101 00001101 00000011 00001011 00000001 00000000 00000000 00000000 
mips64-linux: 12
00100100 00000000 00000000 00000000 00000101 00001101 00000011 00001011 00000001 00000000 00000000 00000000 

I hope it’d be helpful to someone trying to implement bit-fields support in translate-c. It handles ELF, COFF, and Mach-o currently. I’ll probably add WASM in the future if there’s a need.

I think it would be more interesting if it tested alternative compilers. The only tool I’m aware of for detecting abi breakage between compilers is https://github.com/Gankra/abi-cafe, but I don’t think this can check bitfields.

1 Like

In particular, the C compilers used on embedded targets (AVR, etc).

1 Like

I think we should handle the issue in two stages. In the first stage, we enable support on the major platforms. The main objective here is to stop C header files from triggering compile errors, to allow people to compile for MUSL (fails often because of timespec). The ability to access bitfields is secondary, just a way to lower the chance new problems popping up.

Support for embedded platforms would be done in the second stage. Basically, we pluck the low-hanging fruits first and leave the hard parts for later.

Instead of implementing the solution in translate-c, it probably makes sense to put it in std.meta, as people might wish to define such structs manually. Implementation would be more straight forward too.