Bit flag ergonomics in Zig

Hi! I guess packed structs of bools are the way to do bit flags in Zig.

For some things this is fine and readable, but some things look a bit ugly compared to how I’d write it in C. For example:

pub const Result = packed struct(u8) {
    active: bool,
    submit: bool,
    change: bool,
    _: u5 = 0,

    pub const none: Result = @bitCast(@as(u8, 0));
};

pub fn functionReturningResult() Result {
    // .. longer function impl here
   
    var res = Result.none;
    res.active = true;
    return res;
}

In C, the last 3 lines of the functionReturningResult() would be something like return RES_ACTIVE;.

Alternatively, I could default the fields to false and then do something like return .{ .active = true }; but that’s a little busy too.

Any alternatives or nicer ways of doing the above? FWIW, I’m trying to avoid comptime tricks with this one, as don’t want to throw off ZLS.

1 Like

For packed structs that are being used for a bit-flags, it is most often helpful to provide a default value for all fields, often the none equivalent.

pub const Result = packed struct(u8) {
    active: bool = false,
    submit: bool = false,
    change: bool = false,
    _: u5 = 0,

    pub const none: Result = @bitCast(@as(u8, 0));
};

pub fn functionReturningResult() Result {
    // .. longer function impl here
   return .{ .active = true };
}
6 Likes

If you have no fear of potential difference between field order and MSB/LSB order, you could try something like:

return @bitCast(@as(u8, 1) << @bitOffsetOf(Result, "active"));

I personally would not call this nicer than just allowing the fields to default-initialise false and using struct initialisation syntax.
(Especially when you need to bitwise OR multiple flags together to construct your result, which struct initialisation syntax handles very nicely!)

1 Like

I did craft this, which is similar:

pub const Result = packed struct(u8) {
    active: bool = false,
    submit: bool = false,
    change: bool = false,
    _: u5 = 0,

    pub const none: Result = @bitCast(@as(u8, 0));
    pub fn set(field: anytype) Result {
        var opts = Result.none;
        @field(opts, @tagName(field)) = true;
        return opts;
    }
};

which one could use like:

return Result.set(.active);

For another type, I also added function for casting to int which reduces the amount of @bitCast spam a little bit. These work like below:

pub fn inputKeyDown(self: *Context, key: Key) void {
    self.key_pressed = @bitCast(self.key_pressed.int() | key.int());
    self.key_down = @bitCast(self.key_down.int() | key.int());
}

no fear of potential difference between field order and MSB/LSB order

I was under the impression that the field order and bit order can be expected to be sensible. At least if not, it’d make packed structs as bit-fields a lot less useful for interop with C APIs for example.

I’m assuming this is the case, but just to be sure since none of the examples so far have done it, are there cases where more than 1 flag in this Result type is set (i.e. Result{ .active = true, .submit = true, .change = false } or similar)?

If so, and you’re just searching for a shorthand for single-flag instances, then this would work:

pub const Result = packed struct(u8) {
    active: bool = false,
    submit: bool = false,
    change: bool = false,
    _: u5 = 0,

    pub const none: Result = .{};
    pub const only_active: Result = .{ .active = true };
    pub const only_submit: Result = .{ .submit = true };
    pub const only_change: Result = .{ .change = true };
};

pub fn functionReturningResult() Result {
    // ..
   return .only_active;
}

If not, then enum(u8) would be a better choice.

7 Likes

In this case, I often use std.enums.EnumFieldStruct, but this is not packed struct.

For instance:

const ResultKind = enum(u8) { active, submit, change };
const Result = std.enums.EnumFieldStruct(ResultKind, bool, false);

By default, all of the field is initialized as the false value.
And This is srtust type, so you can initialize partially.

pub fn main() !void {
    const r1 = Result{};
    const r2 = Result{ .active = true };

    std.debug.print("r1: {}, r2: {}\n", .{r1, r2});
}
$ zig build run
r1: .{ .active = false, .submit = false, .change = false }, r2: .{ .active = true, .submit = false, .change = false }

If you want to pass a value as the bit mask to the C FFI routine, you can use std.enums.EnumSet.

const PackedResult = std.enums.EnumSet(ResultKind);

pub fn main() void {
    const r = Result{ submit = true, change = true };
    const pack = PackedResult.init(r);
    std.debug.print("packed value: {}", .{ pack.bits.mask });
}
$ zig build run
packed value: 6

It notes that ResultKind has to be a sequential values if you use std.enums.EnumSet.
This type provides a bit mask by position of the enum, but not values.

const ResultKind = enum(u8) { active = 1 << 0, submit << 1, change = 1 << 7 };
const PackedResult = std.enums.EnumSet(ResultKind);

pub fn main() void {
   const f = PackedResult.init(Result{ .submit = true, .change = true });
   std.debug.print("packed value: {}", .{f});
}
$ zig build run
packed value: 6
2 Likes

FWIW I just use regular namespaced consts in my bit-twiddling-heavy emulator code, e.g.:

…which then lets me write code like this:

if ((chn.control & (CTRL.MODE | CTRL.RESET | CTRL.CONST_FOLLOWS)) == CTRL.MODE_TIMER) {
    ...
}

In some situations the actual bit positions are also generated via comptime code from generic parameters, this was basically when I stopped considering packed structs, e.g.:

3 Likes

Thanks everyone for comments! I guess I’ve settled on just packed structs for now, although although @floooh 's suggestion of just namespaced ints is a strong challenger. I kinda like the way this stuff is done in C where no attempt is made to hide the bits. :slight_smile:

I had used an pub const none: Result = @bitCast(@as(u8, 0)) but I think in this case default initializing all the fields to zero has some benefits. It’d be nice if Zig supported the JavaScript-style object initialization which makes this possible: {...initValue, submit: true}.

1 Like

You can also use a function that returns a modified version of its argument:

pub const Result = packed struct(u8) {
    active: bool,
    submit: bool,
    change: bool,
    _: u5 = 0,

    // you can also comptime-generate this
    pub const Modifier = struct {
        active: ?bool = null,
        submit: ?bool = null,
        change: ?bool = null,
    };

    pub fn copyWith(result: Result, modifier: Modifier) Result {
        var tmp = result;
        inline for (comptime std.meta.fieldNames(Modifier)) |field| {
            if (@field(modifier, field)) |value|
                @field(tmp, field) = value;
        }
        return tmp;
    }

    pub const none: Result = @bitCast(@as(u8, 0));
};

pub fn functionReturningResult() Result {
    // .. longer function impl here
   
    return res.copyWith(.{.active = true});
}

Note that the optimizer might struggle more with this, but it has generated perfect assembly in my tests.

1 Like

Yeah I use these tricks even in comptime recursive. Works perfectly.

const SearchState = struct {
    us: Color,
    is_root: bool,

    fn new(comptime us: Color) SearchState {
        return .{ .us = us, .is_root = true };
    }

    fn flip(comptime ss: SearchState) SearchState {
        return .{ .us = ss.us.opp(), .is_root = false };
    }
};

Or create a simplified initDefault similar to std.enums.EnumArray (but not using optionals):

const std = @import("std");

const mixin = struct {
    pub fn initDefault(comptime T: type, comptime Value: type) fn (
        comptime default: Value,
        init_values: anytype,
    ) T {
        return struct {
            fn initDefault(
                comptime default: Value,
                init_values: anytype,
            ) T {
                const A = @TypeOf(init_values);
                var result: T = undefined;
                inline for (std.meta.fields(T)) |f| {
                    if (f.type == Value) {
                        @field(result, f.name) =
                            if (@hasField(A, f.name))
                                @field(init_values, f.name)
                            else
                                default;
                    } else {
                        if (f.defaultValue()) |val| {
                            @field(result, f.name) = val;
                        } else {
                            @compileError("uninitialized field: " ++ f.name);
                        }
                    }
                }
                return result;
            }
        }.initDefault;
    }
};

pub const Result = packed struct(u8) {
    active: bool,
    submit: bool,
    change: bool,
    _: u5 = 0,

    pub const initDefault = mixin.initDefault(@This(), bool);
    pub const none: Result = .initDefault(false, .{});
    pub const all: Result = .initDefault(true, .{});
};

pub fn main() !void {
    const res: Result = .initDefault(false, .{ .submit = true });
    std.debug.print("res: {}\n", .{res});
}

Here is also the version that uses optionals (but I don’t really like it because then I would rather make the fields on Result all optional, which might be the best anyway (especially if the flags are completely independent)):

const FieldEnum = std.meta.FieldEnum;
const EnumFieldStruct = std.enums.EnumFieldStruct;

pub fn GenInitDefault(comptime T: type, comptime Value: type) type {
    return struct {
        fn initDefault(
            comptime default: ?Value,
            init_values: EnumFieldStruct(FieldEnum(T), Value, default),
        ) T {
            var result: T = undefined;
            inline for (std.meta.fields(T)) |f| {
                if (f.type == Value)
                    @field(result, f.name) = @field(init_values, f.name);
            }
            return result;
        }
    };
}

pub const Result = packed struct(u8) {
    ...
    pub const initDefault = GenInitDefault(@This(), bool).initDefault;
};
1 Like

I think it’s great, and not too busy for me anyway. I’m very happy to have bitfields built into the language.

2 Likes

In the end, this is what I settled on too. I started liking that in this variant the “empty case” becomes just return .{}; which looks like the empty set. Same for input arguments: callFunc(.{}) vs callFunc(Result.none).

1 Like