I’m sure you can find dozens of other blog posts explaining how to use comptime in Zig1. This one won’t be any more useful; it’s just mine.

Protocol

The Netlink2 protocol is a control plane for Linux networking. If you haven’t used it directly, know that tools like systemd and iproute2 use Netlink to create network devices, assign IP addresses to them, and define routes in the routing table.

The basic structure of a Netlink message is

-------------------
| struct nlmsghdr |
-------------------
| struct _        |
-------------------
| struct nlattr[] |
-------------------

That middle structure varies based on the message, whose type is specified in nlmsghdr. It will be an ifinfomsg for creating a network device and rtgenmsg for listing them. So there is a common structure with a middle element that can vary, which means you cannot model the entire message with a single structure (note that the size of the middle element is variable). Oh and the attributes are TLV3 structures.

There are many libraries out there for working with Netlink; they vary widely from giving the programmer a high-level API such as

int rtnl_link_alloc_cache(struct nl_sock *sk, int family, struct nl_cache **result)

all the way to

void *mnl_nlmsg_put_extra_header(struct nlmsghdr *nlh, size_t size);
void mnl_attr_put_u8(struct nlmsghdr *nlh, uint16_t type, uint8_t data);

(I know this comparison won’t make sense without being familiar with those libraries.)

For basic use, the high-level APIs are fine. But if you work on Linux networking software, you’ll inevitably run into some limitations. Notice how the list function only accepts a family arg as a filter, but in fact there are additional attributes you can use to filter. In other words, for some cases you want the flexibility of that low-level API.

Generics

Here is a basic need for generics. In Rust, you could define this using a generic data type4 as

struct Nlmsg<I> {
    hdr: nlmsghdr,
    middle: I,
    attrs: Vec<Attr>
}

impl<I> Nlmsg<I> {
    fn method(self) {
    }
}

let msg: Nlmsg<ifinfomsg> = ...;

I would inevitably add a trait to I to define parsing and serialization methods.

Zig does not have generics as a distinct part of the language, but it does support them via the comptime5 feature. The short of it is that you write functions that are executed at compile-time to create new types.

pub fn Nlmsg(comptime T: type) type {
    return struct {
        hdr: nlmsghdr,
        middle: T,
        attrs: []Attr,

        const Self = @This();

        fn method(self: Self) {
        }
    };
}

const CreateLinkRequest = Nlmsg(ifinfomsg);

var msg: CreateLinkRequest = ...;

This isn’t as sophisticated as generics in Rust, since traits offer a powerful way of specifying generic function argument and struct field types, but so far it seems much simpler. I’m going to wait to give any more of an opinion until I’ve had more time with the language.

A comptime type can be used to create new types, but it can also be used for generic functions. Whereas libmnl6 has to define mnl_attr_put_* for each integer type, in Zig you can write

fn attr_put_int(self: *Self, type_: u14, comptime Int: type, val: Int)

and invoke it as

msg.attr_put_int(type_, u8, 4);
msg.attr_put_int(type_, u16, 1000);

Again, this isn’t an added capability compared to Rust or any other language with generics, it’s just a different way of thinking about them. It’s a fantastic and simple improvement over void pointers in C for this sort of thing.

Further Reading

I’m putting together a library over at https://git.0x1b.me/netlink using this idea. I’ve got the basics together and am reimplementing parts of iproute2 as a test bed.