Using clib in ZIG for a simple socket HTTP Server

Feb 16, 2024

I thought that implementing sockets using the standard clib library rather than a Zig library would be a good “hello world” project. It’s a good way to learn about ZIG while also seeing how well it integrates with C code. The code itself is basically a copy of this tutorial. As such I will focus on zig language features and assume the reader is familiar with the socket interface.

Zig allows direct importing of C headers through a special import directive @cImport. Zig then translates these headers into Zig interfaces with C types.

const std = @import("std");
const c = @cImport({
   @cInclude("sys/socket.h");
   ...
});

This enables us to access constants like AF_INET from the C code.

    const socket_fd = c.socket(c.AF_INET, c.SOCK_STREAM, 0);
    if (socket_fd < 0) {
        std.log.err("Could not open servers socket.", .{});
        std.os.exit(1);
    }
    defer _ = c.close(socket_fd);

In this snippet, we utilized one of the convenient features of the Zig language - the defer keyword. It executes the subsequent expression unconditionally when the variable goes out of scope. T

At the same time this code snippet also showed me that zig is still an unstable language as I was greeted with the following compiler error:

        std.log.err("Could not open servers socket.", .{"oops"});  
        // bin/zig_install/lib/std/fmt.zig:202:18: 
        // error: unused argument in 'error: Could not set options
        //                                                 '
        //    1 => @compileError("unused argument in '" ++ fmt ++ "'"),     

As you can see the compiler error only references to a line number within the standard library. this leads to frustrating commenting out of code to even figure out which part of the code is causing the compiler error.

    const in_any = c.struct_in_addr{
        .s_addr = c.INADDR_ANY,
    };
    const serv_addr = c.sockaddr_in{
        .sin_family = c.AF_INET,
        .sin_port = c.htons(port),
        .sin_addr = in_any,
        .sin_zero = std.mem.zeroes([8]u8),
    };

Initialization works more or less the same way as in C. However, one nice touch is that a trailing comma in the initialization list will cause the formatting tool (zig-fmt) to format it in a multiline style. When the trailing comma is removed, it will be formatted in an inline style. I really like how the language itself is designed with tooling in mind.

After binding and listening to a socket we can accept incoming connections.

 var cli_adr = std.mem.zeroes(c.sockaddr_in);
var clilen: c.socklen_t = @sizeOf(@TypeOf(cli_adr));
var new_fd = c.accept(socket_fd, @ptrCast(&cli_adr), @ptrCast(&clilen));
defer _ = c.close(new_fd);

Notice how casting the pointer with @ptrCast does not require us to specify the type to cast to. Instead, the Zig language looks at the interface of the function we want to call and automatically infers the type of pointer we want to cast to.

Another thing to be careful about is ensuring that we actually create a variable for clilen. If we were to declare it as a constant, we could not cast it to a C-pointer that could potentially be changed within the C code.

With that in mind you can find the entire code here and compile it with the following command:

zig build-exe main.zig -lc