Simple HTTP Server

Hello everyone! I am a beginner learning Zig. I’m attempting to write a simple HTTP server to help me get started. I found a library called libxev that I like and discovered an example on GitHub to use as a starting point. However, I stumbled when I tried to split the Client struct into another file. There seems to be a circular dependency with the type ClientPool. I’m not sure how to resolve this issue in Zig. Any suggestions would be greatly appreciated. Thank you.
link: xev-http/src/main.zig at master · dylanblokhuis/xev-http · GitHub

Hi @chrischtel Welcome to ziggit

I’ve tried to split into client.zig , server.zig and main.zig.
In version 0.13.0, Build process is successful.

client.zig
const std = @import("std");
const xev = @import("xev");

pub const CompletionPool = std.heap.MemoryPoolExtra(xev.Completion, .{});
pub const ClientPool = std.heap.MemoryPoolExtra(Client, .{});

pub const Client = struct {
    id: u32,
    socket: xev.TCP,
    loop: *xev.Loop,
    arena: std.heap.ArenaAllocator,
    client_pool: *ClientPool,
    completion_pool: *CompletionPool,
    read_buf: [4096]u8 = undefined,

    const Self = @This();

    pub fn work(self: *Self) void {
        const c_read = self.completion_pool.create() catch unreachable;
        self.socket.read(self.loop, c_read, .{ .slice = &self.read_buf }, Client, self, Client.readCallback);
    }

    pub fn readCallback(
        self_: ?*Client,
        l: *xev.Loop,
        c: *xev.Completion,
        s: xev.TCP,
        buf: xev.ReadBuffer,
        r: xev.TCP.ReadError!usize,
    ) xev.CallbackAction {
        const self = self_.?;
        const n = r catch |err| {
            std.log.err("read error {any}", .{err});
            s.shutdown(l, c, Client, self, shutdownCallback);
            return .disarm;
        };
        const data = buf.slice[0..n];

        std.log.info("{s}", .{data});

        const httpOk =
            \\HTTP/1.1 200 OK
            \\Content-Type: text/plain
            \\Server: xev-http
            \\Content-Length: {d}
            \\Connection: close
            \\
            \\{s}
        ;

        const content_str =
            \\Hello, World! {d}
        ;

        const content = std.fmt.allocPrint(self.arena.allocator(), content_str, .{self.id}) catch unreachable;
        const res = std.fmt.allocPrint(self.arena.allocator(), httpOk, .{ content.len, content }) catch unreachable;

        self.socket.write(self.loop, c, .{ .slice = res }, Client, self, writeCallback);

        return .disarm;
    }

    fn writeCallback(
        self_: ?*Client,
        l: *xev.Loop,
        c: *xev.Completion,
        s: xev.TCP,
        buf: xev.WriteBuffer,
        r: xev.TCP.WriteError!usize,
    ) xev.CallbackAction {
        _ = buf; // autofix
        _ = r catch unreachable;

        const self = self_.?;
        s.shutdown(l, c, Client, self, shutdownCallback);

        return .disarm;
    }

    fn shutdownCallback(
        self_: ?*Client,
        l: *xev.Loop,
        c: *xev.Completion,
        s: xev.TCP,
        r: xev.TCP.ShutdownError!void,
    ) xev.CallbackAction {
        _ = r catch {};

        const self = self_.?;
        s.close(l, c, Client, self, closeCallback);
        return .disarm;
    }

    fn closeCallback(
        self_: ?*Client,
        l: *xev.Loop,
        c: *xev.Completion,
        socket: xev.TCP,
        r: xev.TCP.CloseError!void,
    ) xev.CallbackAction {
        _ = l;
        _ = r catch unreachable;
        _ = socket;

        var self = self_.?;
        self.arena.deinit();
        self.completion_pool.destroy(c);
        self.client_pool.destroy(self);
        return .disarm;
    }

    pub fn destroy(self: *Self) void {
        self.arena.deinit();
        self.client_pool.destroy(self);
    }
};
server.zig
const std = @import("std");
const xev = @import("xev");

const Allocator = std.mem.Allocator;

const clients = @import("client.zig");
const Client = clients.Client;
const CompletionPool = clients.CompletionPool;
const ClientPool = clients.ClientPool;

pub const Server = struct {
    loop: *xev.Loop,
    gpa: Allocator,
    completion_pool: *CompletionPool,
    client_pool: *ClientPool,
    conns: u32 = 0,

    pub fn acceptCallback(
        self_: ?*Server,
        l: *xev.Loop,
        // we ignore the completion, to keep the accept loop going for new connections
        _: *xev.Completion,
        r: xev.TCP.AcceptError!xev.TCP,
    ) xev.CallbackAction {
        const self = self_.?;
        var client = self.client_pool.create() catch unreachable;
        client.* = Client{
            .id = self.conns,
            .loop = l,
            .socket = r catch unreachable,
            .arena = std.heap.ArenaAllocator.init(self.gpa),
            .client_pool = self.client_pool,
            .completion_pool = self.completion_pool,
        };
        client.work();

        self.conns += 1;

        return .rearm;
    }
};
main.zig
const std = @import("std");
const xev = @import("xev");

const clients = @import("client.zig");
const Client = clients.Client;
const CompletionPool = clients.CompletionPool;
const ClientPool = clients.ClientPool;

const Server = @import("server.zig").Server;

const net = std.net;
const Allocator = std.mem.Allocator;

pub fn main() !void {
    var thread_pool = xev.ThreadPool.init(.{});
    defer thread_pool.deinit();
    defer thread_pool.shutdown();

    var loop = try xev.Loop.init(.{
        .entries = 4096,
        .thread_pool = &thread_pool,
    });
    defer loop.deinit();

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const alloc = gpa.allocator();

    const port = 3000;
    const addr = try net.Address.parseIp4("0.0.0.0", port);
    var socket = try xev.TCP.init(addr);

    std.log.info("Listening on port {}", .{port});

    try socket.bind(addr);
    try socket.listen(std.os.linux.SOMAXCONN);

    var completion_pool = CompletionPool.init(alloc);
    defer completion_pool.deinit();

    var client_pool = ClientPool.init(alloc);
    defer client_pool.deinit();

    const c = try completion_pool.create();
    var server = Server{
        .loop = &loop,
        .gpa = alloc,
        .completion_pool = &completion_pool,
        .client_pool = &client_pool,
    };

    socket.accept(&loop, c, Server, &server, Server.acceptCallback);
    try loop.run(.until_done);
}

For 0.13.0, build.zig has changed a little.

    const exe = b.addExecutable(.{
        .name = "xev-http",
-       .root_source_file = .{ .path = "src/main.zig" },
+       .root_source_file = .{ .cwd_relative = "src/main.zig" },
        .target = target,
        .optimize = optimize,
        // .use_llvm = false,
        // .use_lld = false,
    });
2 Likes

Oh cool, thanks!

This is not the correct fix! Please use instead:

    const exe = b.addExecutable(.{
        .name = "xev-http",
-       .root_source_file = .{ .path = "src/main.zig" },
+       .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
        // .use_llvm = false,
        // .use_lld = false,
    });
3 Likes

Thank you for your follow up.

Hi, this an off-topic question, but I don’t think it deserves it’s own topic.

What is the difference between b.path and .{ .path = … } ?

path field of std.Build.LazyPath is deprecated with version 0.13

https://ziglang.org/documentation/0.12.0/std/#std.Build.LazyPath

https://ziglang.org/download/0.12.0/release-notes.html#introduce-bpath-deprecate-LazyPathrelative