Live data from Hacker News

Packed structs in Zig make bit/flag sets trivial

devlog.hexops.com

101–110 of 147 posts

Re: Packed structs in Zig make bit/flag sets trivial

#101

Earlier quoted context omitted.

The only real problem with C (and C++) bitfields is that the standard doesn’t say anything about how they’re laid out in memory, which unfortunately makes them mostly useless unless you either don’t care about layout or you’re targeting one specific compiler and that compiler documents its choices and doesn’t break them in a few years. Sign-extension of 1-bit fields also messes people up all the time, but that’s “jus…

That is a really big problem in some applications. I wish the C committee would come out and say "use the layout that GCC 11 .x.x does" to make these a lot more usable.

I can't remember if GCC has actually documented their layout choices. Ideally we would pick something documented =)

Re: Packed structs in Zig make bit/flag sets trivial

#102
post #44

> This all works, people have been doing it for years in C, C++, Java, Rust, and more. In Zig, we can do better. We can also do better in those other languages, too. For example, in Rust, I can use a crate like `bitfield` which gives me a macro with which I can write bitfield! { pub struct Color(u32); red, set_red: 0; green, set_green: 1; blue, set_blue: 2; alpha, set_alpha: 3; } Don't get me wrong: it's cool that fu…

Is it something I'd ever want to rely on third-party functionality for?

As long as the compiled code is just as efficient as it would be had it been built-in to the language, I don't see the issue? Bitfield ops are very low-level constructs that are only useful in very specific project types. Their portable usage can be tricky, it's not something a majority of coders should reach for.

That's the luxury of a standard build system: essential but rarely used features can be left out of the core language / lib because adding them back in is just a crate import away.

Re: Packed structs in Zig make bit/flag sets trivial

#103

Earlier quoted context omitted.

Yes it would need to be: if ((mask & (WGPUColorWriteMask_Alpha|WGPUColorWriteMask_Blue)) == (WGPUColorWriteMask_Alpha|WGPUColorWriteMask_Blue)) { ... } ...which is quite a mouthful.

I’ve definitely desired an operator that combines these before. I’ve settled on &== as the best syntax in C-style languages, with a &== b being equivalent to a & b == b (but with a single evaluation of b). if (mask &== WGPUColorWriteMask_Alpha|WGPUColorWriteMask_Blue) { … } I find only two very minor problems with it: firstly, that it’s not commutative (that is, a &== b is not equivalent to b &== a ). Secondly, that…

Faszinating: I also thought about exactly '&==' in the past. It seems to be natural choice for this.

But then, continuing that thinking, if you have '&==', you'd also need '&!=' -- that looks really confusing.

Can't we turn that into a '==0' test somehow? Maybe '^&', because '((a ^ b) & b) == 0' is equivalent to '(a & b) == b'. And, as somehow said: '~&' also works: '(~a & b) == 0' is also equivalent to '(a & b) == b'.

    if ((a ~& b) == 0) { ... }
Wait -- we can swap that into '&~' and we're back in C. However, it reverses the logical order, with the test bits first:

    if ((b & ~a) == 0) { ... }

    if (((WGPUColorWriteMask_Alpha|WGPUColorWriteMask_Blue) & ~mask) == 0) {
        ...
    }
So we're back -- this is standard C now. But I find it incomprehensible.

Re: Packed structs in Zig make bit/flag sets trivial

#104

Earlier quoted context omitted.

Yes it would need to be: if ((mask & (WGPUColorWriteMask_Alpha|WGPUColorWriteMask_Blue)) == (WGPUColorWriteMask_Alpha|WGPUColorWriteMask_Blue)) { ... } ...which is quite a mouthful.

I’ve definitely desired an operator that combines these before. I’ve settled on &== as the best syntax in C-style languages, with a &== b being equivalent to a & b == b (but with a single evaluation of b). if (mask &== WGPUColorWriteMask_Alpha|WGPUColorWriteMask_Blue) { … } I find only two very minor problems with it: firstly, that it’s not commutative (that is, a &== b is not equivalent to b &== a ). Secondly, that…

You could also play the game of pretending that the scalar is an object and allow for syntactical sugar like:

    if (mask.contains(WGPUColorWriteMask_Alpha) || mask.contains(WGPUColorWriteMask_Blue)) { ... }

Re: Packed structs in Zig make bit/flag sets trivial

#105
post #24
post #6

Earlier quoted context omitted.

Admittedly I didn't know C had bit fields like that, so I didn't cover them. I've never seen them used in practice and I've used quite a lot of C libraries. I wonder why that is? One major difference appears to be that C bitfields memory layout is compiler-dependant. The other major difference is Zig's arbitrary-bit-width integer types just leading to less footguns I would speculate

Far as I can tell 'don't use bit fields' is something students learn from their CS professors along with don't use goto, multiple returns, continue statements, pass structs by value, and always convert to big endian when sending data over a wire. My experience is C compilers have ways packing and defining the order of bitfields and structs.

Yeah no, bitfields are notoriously implementation defined.

In the embedded world you often have to deal with vendor specific toolchains, and with their finicky compilers, you'd be surprised the weirdness you run into when using bitfields.

You will learn to question everything not specifically defined by the c standard.

Re: Packed structs in Zig make bit/flag sets trivial

#106
post #95

Earlier quoted context omitted.

Bit fields in C are notoriously non-portable (exact physical layout depends on the compiler). Is Zig any better (apart from the fact that there is only one Zig compiler for now)?

Bit fields are portable since ages, since C89. Just pack it, use the smallest base type and don't leave holes. We were using them in perl5 forever, and this compiles on more platforms with more compilers then you know. Just use -mms-bitfields on mingw and use unsigned short instead of just unsigned. E.g. https://www.nntp.perl.org/group/perl.perl5.porters/2008/02/m... for tricks.

> Bit fields are portable since ages, since C89.

Wrong. N1256 (ISO C99 spec), for instance, explicitly states in 6.7.2.1.10:

  The order of allocation of bit-fields within a unit
  (high-order to low-order or low-order to high-order)
  is implementation-defined.
Look it up.

Re: Packed structs in Zig make bit/flag sets trivial

#107

This is a nice syntax, but the functionality seems essentially the same as bit fields in C. Does this provide anything additional?

The in-memory representation of bit fields is implementation-defined. Therefore, if you're calling into an external API that takes a uint32_t like in the example without an explicit remapping, you may or may not like the results. In practice, everything you're likely to come across will be little endian nowadays, and the ABI you're using will most likely order your struct from top to bottom in memory, so they will lo…

> In practice, everything you're likely to come across will be little endian nowadays

The internet?

Re: Packed structs in Zig make bit/flag sets trivial

#108

This is a nice syntax, but the functionality seems essentially the same as bit fields in C. Does this provide anything additional?

Bit fields in C are notoriously non-portable (exact physical layout depends on the compiler). Is Zig any better (apart from the fact that there is only one Zig compiler for now)?

> Bit fields in C are notoriously non-portable (exact physical layout depends on the compiler).

The word "non-portable" is being thrown here too cheaply. By the definition used here, technically every C program that puts two integers consecutively on a struct is non portable, either. Not just due to alignment but due to endianess, etc.

Re: Packed structs in Zig make bit/flag sets trivial

#109

Earlier quoted context omitted.

I’ve definitely desired an operator that combines these before. I’ve settled on &== as the best syntax in C-style languages, with a &== b being equivalent to a & b == b (but with a single evaluation of b). if (mask &== WGPUColorWriteMask_Alpha|WGPUColorWriteMask_Blue) { … } I find only two very minor problems with it: firstly, that it’s not commutative (that is, a &== b is not equivalent to b &== a ). Secondly, that…

Faszinating: I also thought about exactly '&==' in the past. It seems to be natural choice for this. But then, continuing that thinking, if you have '&==', you'd also need '&!=' -- that looks really confusing. Can't we turn that into a '==0' test somehow? Maybe '^&', because '((a ^ b) & b) == 0' is equivalent to '(a & b) == b'. And, as somehow said: '~&' also works: '(~a & b) == 0' is also equivalent to '(a & b) == b…

Now I’m trying to decide if you make it better or worse by replacing the == 0 with simple logical negation:

  if (!(b & ~a)) { … }
  if (!((WGPUColorWriteMask_Alpha|WGPUColorWriteMask_Blue) & ~mask)) { … }

  if (!(~a & b)) { … }
  if (!(~mask & (WGPUColorWriteMask_Alpha|WGPUColorWriteMask_Blue))) { … }
Actually, that latter makes a little more intuitive sense to me because of how ! and ~ are both negation¹, so they kinda cancel out and leave just the masking. Kinda.

—⁂—

¹ Fun fact: Rust uses ! for both logical and bitwise negation, backed by core::ops::Not, with bool → bool and {integer} → {integer}, since bool is a proper type and there’s no boolean coercion anywhere—so you would have to stick with `== 0` in Rust, though in practice you’d probably go all typey with the bitflags crate’s macro to generate good types with some handy extra methods, and write `mask.contains(WgpuColorWriteMask::Alpha | WgpuColorWriteMask::Blue)`.

Re: Packed structs in Zig make bit/flag sets trivial

#110

Earlier quoted context omitted.

I’ve definitely desired an operator that combines these before. I’ve settled on &== as the best syntax in C-style languages, with a &== b being equivalent to a & b == b (but with a single evaluation of b). if (mask &== WGPUColorWriteMask_Alpha|WGPUColorWriteMask_Blue) { … } I find only two very minor problems with it: firstly, that it’s not commutative (that is, a &== b is not equivalent to b &== a ). Secondly, that…

You could also play the game of pretending that the scalar is an object and allow for syntactical sugar like: if (mask.contains(WGPUColorWriteMask_Alpha) || mask.contains(WGPUColorWriteMask_Blue)) { ... }

Fun fact: Rust’s bitflags macro crate generates just this, https://docs.rs/bitflags/latest/bitflags/example_generated/s....

  // To check for either flag:
  if mask.contains(WgpuColorWriteMask::Alpha) || mask.contains(WgpuColorWriteMask::Blue) { … }
  if mask.intersects(WgpuColorWriteMask::Alpha | WgpuColorWriteMask::Blue) { … }

  // To check for both flags:
  if mask.contains(WgpuColorWriteMask::Alpha | WgpuColorWriteMask::Blue) { … }
Post reply on HN