Reflect the tag type of a tagged union?

I’m deserializing a byte stream that could contain a fixed set of different message types.

I’m using a union for this. When deserializing, I first identify the type of message in the buffer, then I call the specific deserialization function for the message type I identify.

  1. How can I write this without having to explicitly create the union tag type? Specifically, can I write the following code without defining InContentTag?
  2. Do you think attempting to remove what feels like redundant code will be too detrimental to readability?
pub const InContentTag = enum {
    expedited,
    normal,
    segment,
    abort,
    emergency,
};

/// MailboxIn Content for CoE.
pub const InContent = union(InContentTag) {
    expedited: server.Expedited,
    normal: server.Normal,
    segment: server.Segment,
    abort: server.Abort,
    emergency: server.Emergency,

    // TODO: implement remaining CoE content types

    pub fn deserialize(buf: []const u8) !InContent {
        switch (try identify(buf)) {
            .expedited => return InContent{ .expedited = try server.Expedited.deserialize(buf) },
            .normal => return InContent{ .normal = try server.Normal.deserialize(buf) },
            .segment => return InContent{ .segment = try server.Segment.deserialize(buf) },
            .abort => return InContent{ .abort = try server.Abort.deserialize(buf) },
            .emergency => return InContent{ .emergency = try server.Emergency.deserialize(buf) },
        }
    }

    /// Identify what kind of CoE content is in MailboxIn
    pub fn identify(buf: []const u8) !InContentTag {
        var fbs = std.io.fixedBufferStream(buf);
        const reader = fbs.reader();
        const mbx_header = try wire.packFromECatReader(mailbox.Header, reader);

        switch (mbx_header.type) {
            .CoE => {},
            else => return error.WrongMbxProtocol,
        }
        const header = try wire.packFromECatReader(Header, reader);

        switch (header.service) {
            .tx_pdo => return error.NotImplemented,
            .rx_pdo => return error.NotImplemented,
            .tx_pdo_remote_request => return error.NotImplemented,
            .rx_pdo_remote_request => return error.NotImplemented,
            .sdo_info => return error.NotImplemented,

            .sdo_request => {
                const sdo_header = try wire.packFromECatReader(server.SDOHeader, reader);
                return switch (sdo_header.command) {
                    .abort_transfer_request => .abort,
                    else => error.InvalidSDORequest,
                };
            },
            .sdo_response => {
                const sdo_header = try wire.packFromECatReader(server.SDOHeader, reader);
                switch (sdo_header.command) {
                    .upload_segment_response => return .segment,
                    .download_segment_response => return .segment,
                    .initiate_upload_response => switch (sdo_header.transfer_type) {
                        .normal => return .normal,
                        .expedited => return .expedited,
                    },
                    .initiate_download_response => return .expedited,
                    .abort_transfer_request => return .abort,
                    _ => return error.InvalidSDOResponseSDOHeader,
                }
            },
            .emergency => return .emergency,
            _ => return error.InvalidCoEService,
        }
    }
};

I know that unions can infer their enum tag type with

pub const InContent = union (enum) { ...}

figured it out:

pub const InContent = union(enum) {
...
     fn identify(buf: []const u8) !@typeInfo(InContent).Union.tag_type.? {

Frankly nuts that I can do this!!! :slight_smile:

You’re looking for std.meta.Tag:

    const U = union(enum) {
        a: u8,
        b: f16,

        fn is(self: @This(), tag: std.meta.Tag(@This())) bool {
            return self == tag;
        }
    };

    const u = U{ .a = 42 };
    std.debug.print("{}\n", .{u.is(.a)}); // true
    std.debug.print("{}\n", .{u.is(.b)}); // false

I think that the impact on readability is mostly a matter of personal preference here. It does make the code less redundant and puts everything in one place, making it easier to make changes in the future.

2 Likes

yeah, I think in the future I will eventually eliminate these separate identify functions because I am deserializing some areas of the buffer twice, which is a waste. But premature optimization is the root of all evil so if it works ship it!

1 Like

When dealing with this kind of stuff, I quite often (if not always) use an array of function pointers instead of switch (or a chain of else ifs :slight_smile: ), and tag value is an index for this table. I do not have a quick example in Zig by hand right now, in C it usually something like this:

typedef int (*irz_decode_frame)(...);
...
static irz_decode_frame __irz_decode_frame[256] = {
          [1] = __irz_decode_frame_001,
         [21] = __irz_decode_frame_021,
....
irz_decode_frame decode_frame;
decode_frame = __irz_decode_frame[frame_type];
if (NULL == decode_frame) {...
err = decode_frame(...);

This way you organize “routing” by data (some table), not by code (switch or whatever).