1. 17
  1. 9

    In C, this feature is called Bit Fields. It’s been part of the language since the 1980’s.

    1. 9

      Zig’s bitfields are a bit better, though. You can take the address of a bitfield. Andrew wrote a blog post about the feature a few years ago: https://andrewkelley.me/post/a-better-way-to-implement-bit-fields.html

      1. 3

        That’s an interesting design decision. You can’t pass the address of a bool bitfield to a function expecting a bool pointer, because the bit offset is part of the type.

        1. 2

          That’s true. But this allows any variable or struct member to be passed by reference, which is important for writing generic functions that are polymorphic over argument types.

        2. 2

          That’s a great link: a list of all the problems with C bitfields and how Zig fixes them.

        3. 3

          At first I thought this, and then I read the article and thought “oh, they mean like pascal sets” and then I read it again and realized that no they really are just describing an API that uses a struct of bitfields rather than an integer.

          Now obviously a struct of bitfields is superior to an untyped uint, but it lacks the flexibility that comes from getting confused about what int means what :D

          I think that the intended benefit of the API being an int is twofold: It is very easy to do things that result in structs being passed in memory even if they fit in registers (clang even has an attribute you can apply to a struct to force register passing), but you can very easily do set operations like unions, intersections, etc. Whether that trade off is worth it I think depends on the use case, but I think we can agree that it would be nicer if we could have set enums in C. There’s no reason for Zig not to have them as it isn’t confined to whatever can get through a standards body.

          1. 2

            Zig should extend the bitwise operations to work on structs and arrays of ints. That would eliminate the objection that you can’t do unions/intersections on bitfield structs.

            If you have this language extension in Zig, then Pascal style enum sets become a coding idiom, rather than a new builtin language feature. You could write a helper function (using comptime zig) that takes an enum type as an argument, finds the largest enum in the type using @typeinfo, then returns the appropriate array type to use as a set.

            There are many precedents for extended bitwise operators, all the way from APL in the 1960’s and Fortran 90 (where scalar operations are extended to work on arrays), to vector instructions in modern CPUs that would directly support this extension.

          2. 1

            From the article:

            mask |= WGPUColorWriteMask_Blue; // set blue bit

            This all works, people have been doing it for years in C, C++, Java, Rust, and more. In Zig, we can do better.

            1. 4

              Exactly, they say in C we use integers with various mask bits, and then propose a “superior” solution in zig. Which is to just use bitfields, as have existed in C, C++, [Object] Pascal, … forever.

              The point the I think @doug-moen is making is that the authors are claiming that the masked integer is an inherent requirement of C, etc and then that Zig can do better because of this special Zig only feature, pretending that the identical feature does not exist in C, and ignoring that maybe there are other reasons that this (terrible :D) style of API exists.

              1. 7

                Except bitfields in C are bristling with footguns, most importantly that the mapping to actual bits is implementation-dependent, which makes bitfields incompatible with any C API that defines bits using masks, or any hardware interface.

                From the GCC docs:

                The order of allocation of bit-fields within a unit (C90 6.5.2.1, C99 and C11 6.7.2.1).

                • Determined by ABI.

                The alignment of non-bit-field members of structures (C90 6.5.2.1, C99 and C11 6.7.2.1).

                • Determined by ABI.

                Having an actual useable implementation of bitfields is awesome for Zig.

                1. 2

                  Is Zig’s packing of bitfield independent of ABI?

                  And why does it matter unless the bitfield leaves your program’s address space? (Just about anything other than byte arrays leaving your address space is ABI dependent. Why single out bitfields?)

                  1. 1

                    It matters because if you’re interfacing with nearly any existing C API, which specifies bits using integers with masks, you cannot declare a C bit-field compatible with that API without knowing exactly how your compiler’s ABI allocates bits.

                    1. 2

                      Yes. How is that different from a struct?

                      1. 1

                        You know exactly how it’s laid out, because that’s defined by the platform ABI. There is no mystery here.

                        1. 1

                          Not a mystery, but highly platform-specific. So if you’re interfacing with anything using explicit bit masks, that makes it a really bad idea to use in cross-platform code, or in code you want to survive a change in CPU architecture. As a longtime Apple developer I’ve been through four big architecture changes in my career — 68000 to PowerPC to PPC 64-bit to x86 32/64 to ARM 32/64 — each with a different ABI. And over time I’ve had to write code compatible with Windows, Linux and ESP32.

                    2. 1

                      Yes, the platform defines the ABI for that platform and the compiler is expected to follow that ABI. So bit fields in C/C++ can (and are) used in plenty of APIs.

                      I get that you may like Zig, and prefer Zig’s approach to specifying things, but that doesn’t mean you get to spout incorrect information. The platform ABI defines how strict an are laid out. It defines the alignment and size of types. It defines the order of function arguments and where those arguments are placed. So claiming bitfields are footguns or that they are unusable because they .. follow the platform ABI? is like saying that function calls are footguns as the registers and stack slots used in a call are defined by the platform ABI.

                      If Zig is choosing to ignore the platform ABI, then Zig is the thing that is at fault if an API doesn’t work. If there is an API outside of Zig that uses a bit field then Zig will need some way to say that bits should be laid out in the manner specified by the platform ABI.

                      1. 2

                        I’m not actually a Zig fan (too low level for me) but I like its treatment of bitfields.

                        You seem to be missing my point. A typical API (or hardware interface) will define flags as, say, a 32-bit int, where a particular flag X is stored in the LSB. You can’t represent that using C bit-fields in a compiler- or platform-independent way, because you don’t know where the compiler will put a particular field. Each possible implementation will require a different bit-field declaration. In practice this is more trouble than it’s worth, which is why APIs like POSIX or OpenGL avoid bit-fields and use integers with mask constants instead.

                        Yes, if you declare your own bit-field based APIs they will work. They’re just not interoperable with code or hardware that uses a fixed definition of where the bits go.

                        1. 1

                          You can’t represent that using C bit-fields in a compiler- or platform-independent way, because you don’t know where the compiler will put a particular field.

                          No, that is all well defined as part of the platform specified ABI. You know exactly where they go, and exactly what the packing is. Otherwise the whole concept of having an API is broken. POSIX and GL use words and bit fields because many concepts benefit from masking, or to make interop with different languages easier - the API definition is literally just “here’s a N bit value”.

              2. 2

                This looks like a logic error:

                if (mask & (WGPUColorWriteMask_Alpha|WGPUColorWriteMask_Blue)) {
                    // alpha and blue are set..
                }
                

                Shouldn’t it be this?

                if ((mask & WGPUColorWriteMask_Alpha) && (mask & WGPUColorWriteMask_Blue)) {
                    // alpha and blue are set..
                }
                
                1. 2

                  No, because that requires that both flags are set. The equivalent condition to s & (A|B), which works no different, would be (s & A) || (s & B).

                  Notice the commonality in how the bitmasks are joined: always with some form of the disjunctive (“or”) operator, either bitwise | or boolean ||. In any case, the bitmask A or B must be applied to the operand s using the & bitwise “and” operator: s & A, s & B, s & (A|B).

                  The equivalent operation to your “correction” (s & A) && (s & B), using the technique of joining the bitmasks first, would be s & (A|B) == A|B. This checks that all of the bits are set, rather than that any of the bits are set.

                  Edit: I got confused 😅 You are right: the original code tests whether either alpha or blue is set. My initial comment above would have been applicable if the commented-out text had read, “// alpha OR blue is set..”. I think that’s as good a case as any for “tagged” bit-fields over “untagged” bit-masks.

                  Note for any lurkers who have read this far and are rather confused: You may want to read up on how bitmasks are used.

                  1. 2

                    Which makes either the comment or the code wrong. The comment says “and” not “or”. @smaddox was matching the code to the comment

                  2. 2

                    As others have pointed out, the comment is a bit misleading. But if you want to check if both are set, this would work:

                    if (mask & (WGPUColorWriteMask_Alpha | WGPUColorWriteMask_Blue) == (WGPUColorWriteMaskAlpha | WGPUColorWriteMask_Blue))
                    {
                        // alpha and blue are set ...
                    }
                    
                    1. 1

                      Wouldn’t the | operator join the two bit masks together to create a new intermediate with both bits set? It’s a matter of A == (B|C) versus (A==B) && (A==C) at this point.

                      1. 3

                        It does, but you get “or” instead of “and”. If either bit is set, the result is not zero.

                        1. 2

                          Correction: (A == B) && (A == C) is always false (0) when B != C, due to the transitive property of strict equality. You probably meant (A & B) && (A & C). See my other comment.