Nicer C99 APIs with Designated Initializers

Article summary

While working on a library for property-based testing in C, I discovered a trick that can be used to make significantly nicer library interfaces in C99: “designated initializers”. As of C99, struct literals can have their fields set by name, in any order. The C99 standard explicitly updated the behavior for how fields in struct literals are handled:

6.7.8 point 21:

“If there are fewer initializers in a brace-enclosed list than there are elements or members of an aggregate, or fewer characters in a string literal used to initialize an array of known size than there are elements in the array, the remainder of the aggregate shall be initialized implicitly the same as objects that have static storage duration.” (emphasis mine)

Since memory with static storage duration is already initialized to zero, this means that in C99 we can finally depend on stack-allocated structs’ fields being set to zero, rather than garbage data from previous stack frames. This is a huge improvement! If pointers to structs are used as arguments for function calls, it also gives C99 a portable way of using optional and keyword arguments.

socket99 for BSD sockets

With this in mind, I made a wrapper for a particularly awkward library: the BSD sockets API. Its designers had to make a generic interface that could configure any kind of network connection, anticipating future networks’ needs. What they came up with is several functions, a subset of which need to be called in the right order, depending on the use case. Many of these take parameters cast to a generic socket struct type. (A union couldn’t be used, because it would need to know all possible types, and new ones are still being added.) There are a lot of details to get right when using them, and while checking for errors at every step along the way is crucial, it also makes relatively basic networking code quite verbose.

For many common use cases, only a little information is necessary. My library (socket99) takes a struct with a few keyword arguments set, and writes the result to an output struct — either a file descriptor, or details about the error. The library can make client or server connections on TCP, UDP, or Unix domain sockets (in either stream or datagram mode).

Here’s a couple example uses, from my library’s test code. First, a TCP client that connects to 127.0.0.1:8080 and sends “hello\n”:

bool tcp_client(void) {
    socket99_config cfg = {
        .host = "127.0.0.1",
        .port = 8080,
    };

    socket99_result res;
    bool ok = socket99_open(&cfg, &res);
    if (!ok) {
        printf("failed to open: status %d, errno %d\n",
            res.status, res.saved_errno);
        return false;
    }

    const char *msg = "hello\n";
    size_t msg_size = strlen(msg);

    ssize_t sent = send(res.fd, msg, msg_size, 0);
    bool pass = ((size_t)sent == msg_size);
    close(res.fd);
    return pass;
}

Second, a non-blocking TCP server that listens for incoming clients on 127.0.0.1:8080 and prints the first read from the first client, then exits:

bool tcp_server_nonblocking(void) {
    socket99_config cfg = {
        .host = "127.0.0.1",
        .port = 8080,
        .server = true,
        .nonblocking = true,
    };

    socket99_result res;
    bool ok = socket99_open(&cfg, &res);
    if (!ok) { return false; }

    struct pollfd fds[1];
    fds[0].fd = res.fd;
    fds[0].events = POLLIN;

    ssize_t received = 0;
    for (;;) {
        int poll_res = poll(fds, 1, 1000 /* msec */);
        if (poll_res > 0 && fds[0].revents & POLLIN) {
            struct sockaddr address;
            socklen_t addr_len;
            int client_fd = accept(res.fd, &address, &addr_len);
            if (client_fd == -1) {
                if (errno == EWOULDBLOCK) {
                    errno = 0;
                    continue;
                } else {
                    close(res.fd);
                    return false;
                }
            }

            received = read_and_print(client_fd);
            close(client_fd);
            if (received > 0) { break; }
        }
    }

    close(res.fd);
    return (received > 0);
}

Also, libraries using structs like this are still statically typed, so if the fields change between library versions, breaking changes will lead to errors at compile time rather than settings being silently ignored.
 

Conversation
  • Norcimo Five says:

    “bool ok = socket99_open(&cfg, &res);”

    I almost lost my marbles when I saw this line. Apparently your code snippet is not showing ampersands correctly. ;)

  • Comments are closed.