Hi everyone,
I have been using Python for a while and when trying to do things in Zig, I often find myself in some sort of analysis paralysis.
In this case, I am trying to implement a very basic HTTP/1.1 server as part of my learning process and to satisfy my curiosity. While implementing the routing and responses, I found myself in the following situation:
const Header = struct {
name: []const u8,
value: []const u8,
};
const Response = struct {
start_line: []const u8,
headers: []const Header,
message_body: []const u8,
};
fn router(r: Request) !void {
var response;
if (std.mem.eql(u8, r.path, "/bla")) {
// version 1
var header_list = std.ArrayList(Header).init(allocator);
const content_length = try std.fmt.allocPrint(allocator, "{}", .{r.start_line.request_target.len - 6});
try header_list.append(Header{ .name = "Content-Type", .value = "text/plain" });
try header_list.append(Header{ .name = "Content-Length", .value = content_length });
response = Response{
.start_line = .{ .status_code = 200, .reason_phrase = "OK" },
.headers = header_list.toOwnedSlice(),
.message_body = r.start_line.request_target[6..],
};
// version 2
response = route(allocator, r)
}
_ = try stream_out.print("{}\n", .{response});
}
fn route(a: Allocator, r: Request) !Response {
var header_list = std.ArrayList(Header).init(a);
const content_length = try std.fmt.allocPrint(a, "{}", .{r.start_line.request_target.len - 6});
try header_list.append(Header{ .name = "Content-Type", .value = "text/plain" });
try header_list.append(Header{ .name = "Content-Length", .value = content_length });
return Response{
.start_line = .{ .status_code = 200, .reason_phrase = "OK" },
.headers = header_list.toOwnedSlice(),
.message_body = r.start_line.request_target[6..],
};
}
The code above is not exactly what I have, except for the function body, which is almost the same.
I feel a bit stuck because I am trying to find the best solution, but nothing really convinces me. So maybe you could advice me on how to proceed. This is not just a technical but also a philosophical question.
After all my reading, I see the following options:
-
Move the
_ = try stream_out.print("{}\n", .{response});
into theroute()
. This fixes the lifetime issues since I never need to accessresponse
outside ofroute()
. However, it feels a bit ugly, because later on I might want to add extra headers, which would require managing them within my “business logic.” It makes things trivial, but I don’t really learn anything new this way. -
Use an arena allocator. This would be the lazy approach, as I wouldn’t need manage memory as long as I don’t run out of it. Potentially inefficient. I’m not at the point where I’m creating so many resources that I might run out of memory, but that is still something I don’t learn.
-
Allocate the entire
Response
structure and then release everything. This seems inefficient, because I’d be allocating for constants that are stored in global registry. -
Use “bookkeeping”, which I only read about yesterday: have a structure that records all the pointers I need to free.
Any thoughts? Feel free to share links to anything you consider insightful.
P.S. I know there have been a few similar questions here on the forum. I’ll probably re-read them again later today.