Having a hard time understanding io_uring for TCP server

Hi,

This is my first time using io_uring, and I’ve been learning Zig for past few days. I’m trying to create a TCP server as a little pet project (and to get more familiar with Zig + Systems Programming). However, I’ve been stumped by io_uring.

const std = @import("std");
const posix = std.posix;
const request = @import("request.zig");
const linux = std.os.linux;

const Response = @import("response.zig").Response;
const Address = std.net.Address;

const EventType = enum(u8) { ACCEPT, READ, WRITE };

const Client = struct {
    allocator: *std.mem.Allocator,
    client_socket: posix.socket_t,
    buffer: []u8,

    fn init(allocator: *std.mem.Allocator, socket: posix.socket_t) !Client {
        const buffer = try allocator.alloc(u8, 4096);
        @memset(buffer, 0); //done to make buffer readable

        return .{
            .allocator = allocator,
            .client_socket = socket,
            .buffer = buffer,
        };
    }

    fn deinit(self: *Client, allocator: *std.mem.Allocator) void {
        allocator.free(self.buffer);
        allocator.destroy(self);
    }
};

const Event = struct {
    ptr: ?*Client,
    event_type: EventType,

    fn init(ptr: ?*Client, event_type: EventType) Event {
        return .{ .ptr = ptr, .event_type = event_type };
    }
};

fn addAcceptRequest(ring: *linux.IoUring, listener: posix.socket_t, allocator: *std.mem.Allocator) !void {
    const event = try allocator.create(Event);
    event.* = Event.init(null, .ACCEPT);

    var address: posix.sockaddr = undefined;
    var address_len: posix.socklen_t = @sizeOf(posix.sockaddr);

    const user_data: usize = @intFromPtr(event);
    std.debug.print("ACCEPT - user_data: {any}\n", .{user_data});
    _ = try ring.accept(user_data, listener, &address, &address_len, 0);
    const num_submitted = try ring.submit();
    std.debug.print("ACCEPT - num_submitted: {any}\n", .{num_submitted});
}

fn addReadRequest(ring: *linux.IoUring, client_socket: posix.socket_t, allocator: *std.mem.Allocator) !void {
    const client = try allocator.create(Client);
    client.* = try Client.init(allocator, client_socket);

    const event = try allocator.create(Event);
    event.* = Event.init(client, .READ);

    const read_buffer = linux.IoUring.ReadBuffer{ .buffer = client.buffer[0..] };
    const user_data: usize = @intFromPtr(event);

    _ = try ring.read(user_data, client.client_socket, read_buffer, 0);
    const num_submitted = try ring.submit();
    std.debug.print("READ - num_submitted: {any}\n", .{num_submitted});
}

fn handleRequest(client: *Client) void {
    _ = request.parse_request(client.buffer) catch |err| {
        std.log.err("Invalid request when parsing: {any}", .{err});
        return;
    };

    std.debug.print("\nClient Request\n{s}\n\n", .{client.buffer}); //view contents of buffer after reading/parsing is done
}

pub fn main() !void {
    const host = [4]u8{ 127, 0, 0, 1 };
    const port = 3000;

    const addr = Address.initIp4(host, port);

    {
        var buffer: [64]u8 = undefined;
        const stderr = std.debug.lockStderrWriter(&buffer);
        defer std.debug.unlockStderrWriter();

        try stderr.print("Listening at http://", .{});
        try addr.in.format(stderr);
        try stderr.print("\n", .{});
        try stderr.flush();
    }

    const sock_fd = try posix.socket(addr.any.family, posix.SOCK.STREAM | posix.SOCK.NONBLOCK, posix.IPPROTO.TCP);

    try posix.setsockopt(sock_fd, posix.SOL.SOCKET, posix.SO.REUSEADDR, &std.mem.toBytes(@as(c_int, 1)));

    try posix.bind(sock_fd, &addr.any, addr.getOsSockLen());
    try posix.listen(sock_fd, 1024);

    var ring = try linux.IoUring.init(256, 0);

    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    var allocator = gpa.allocator();

    try addAcceptRequest(&ring, sock_fd, &allocator);

    while (true) {
        //TODO: add threading here
        var cqe = try ring.copy_cqe();
        std.debug.print("cqe: {any}\n", .{cqe});
        std.debug.print("user_data: {any}\n", .{cqe.user_data});
        const event: *Event = @ptrFromInt(cqe.user_data);
        std.debug.print("event: {any}\n", .{event.*});

        switch (event.event_type) {
            .ACCEPT => {
                try addAcceptRequest(&ring, sock_fd, &allocator);
                try addReadRequest(&ring, cqe.res, &allocator);
            },
            .READ => {
                std.debug.print("HELLO!!!", .{});
                handleRequest(event.ptr.?);
            },
            .WRITE => {},
        }

        ring.cqe_seen(&cqe);
    }
}

Here’s my current code, in its entirety. When running it, I run into the following output:

Listening at http://127.0.0.1:3000
ACCEPT - user_data: 133904333537280
ACCEPT - num_submitted: 1
cqe: .{ .user_data = 133904333537280, .res = 5, .flags = 0 }
user_data: 133904333537280
event: .{ .ptr = null, .event_type = .ACCEPT }
ACCEPT - user_data: 133904333537296
ACCEPT - num_submitted: 1
READ - num_submitted: 1
cqe: .{ .user_data = 133904333537296, .res = 6, .flags = 0 }
user_data: 133904333537296
event: .{ .ptr = null, .event_type = .ACCEPT }
ACCEPT - user_data: 133904333537328
ACCEPT - num_submitted: 1
READ - num_submitted: 1
cqe: .{ .user_data = 0, .res = 0, .flags = 0 }
user_data: 0
thread 195698 panic: cast causes pointer to be null
/home/w/projects/tcp-server/src/main.zig:162:31: 0x11449b3 in main   (main.zig)
        const event: *Event = @ptrFromInt(cqe.user_data);

I’ve been heavily following along with this code of a similar implementation, but in C (I learned C in university, but am not a massive fan).

Any help, advice, or resources would be greatly appreciated!

1 Like

Pointers to io uring ops have to stay stable until completion.

try ring.accept(user_data, listener, &address, &address_len, 0);

The address and address_len go out of scope when the function exits here for example.

You should also not call cqe_seen

/// If you use copy_cqes() or copy_cqe() you must not call cqe_seen() or cq_advance().

The code also has bunch of bad memory handling (you leak memory), but I won’t go over all that.

5 Likes

Thank you!

I’m aware of the terrible memory handling, I was just trying to figure out this problem first before tackling memory-related issues.

I’ve managed to get it working now!

take a look