The power of ⚡Zap⚡ for Zig
Ed Yu (@edyu on Github and
@edyu on Twitter)
January.23.2024
Introduction
Zig is a modern systems programming language and although it claims to a be a better C, many people who initially didn't need systems programming were attracted to it due to the simplicity of its syntax compared to alternatives such as C++ or Rust.
However, due to the power of the language, some of the syntaxes are not obvious for those first coming into the language. I was actually one such person.
Today we will explore an awesome Zig project called ⚡Zap⚡. From a high level, Zap is web-server. However, it's not a web-server fully implemented in Zig. It's a wrapper on top of a C web-server (library) called facil.io.
Why Zap
There are many reasons I like Zap but I'll list the top 3:
- Zap is designed by Rene, who's probably one of the most practical engineers I know.
- Zap is extremely fast.
- Zap is very simple.
Let me explain each of these reasons:
Rene originally started Zap for his own use at work so it was designed to make his work easier. He made many decisions along the way and I believe he made many correct decisions.
He will not implement something just because every other webserver has it nor would he implement something in the same way that other web-servers implemented the feature.
Zap is extremely fast because facil.io is extremely fast. You can check out the full benchmark here.
Of course, a benchmark doesn't tell the full story and can be easily manipulated but it at least shows that bottleneck is definitely not in Zap or Zig itself.
Zap is simple because Rene didn't implement anything he didn't need and when he did need a feature, he tried to implement it in a straightforward manner. His code is a pleasure to read and I can honestly say that he was probably the primary reason I was able to learn Zig.
Zap also has one of the best examples of any projects on github. Just by looking at the example, you should be able to get up to speed quickly.
Why Not Zap
Of course, Zap is not for everyone and here are three reasons why you may not want to use it:
- If you have NIH Syndrome, Zap is NOT implemented in Zig.
- If you are on Windows or you need HTTP/2 or HTTP/3 because facil.io only implemented HTTP/1.0 and HTTP/1.1.
- You need to write a lot of code even for simple things in Zap compared to most other web frameworks because Zap is a web-server not a web framework.
For any new language, there is a tendency for early adopters of that language to reimplement everything in the new shiny language. Even if the language is not new anymore, there will still be plenty of people creating a new framework in that language.
This is certainly not the case for Zap because every feature that is exposed is used by Rene himself.
He also made it very clear that Zap will not reimplement everything in facil.io using Zig. Rene also tries to minimize any changes to the upstream facil.io. Unfortunately, it also means that Zap won't run on Windows.
Lastly, understand that even the simplest language and simplest wrapper would introduce artifacts that complicate usage due to impedence mismatch.
For example, to get a parameter that was passed in the URL, you have to do the following in Zap:
if (r.getParamStr(allocator, "my-param", false)) |maybe_str| {
if (maybe_str) |*s| {
defer s.deinit();
std.log.info("Param my-param = {s}", .{s.str});
} else {
std.log.info("Param my-param not found!", .{});
}
}
There is also no authentication (other than basic HTTP authentication), authorization (other than HTTP Bearer authorization), or database built-in so you'll end up writing a lot of code to implement these yourself.
In some private tests I've done myself, you end up writing about 5 times as much code in Zig and Zap than if you use something like Python FastAPI, or even comparing to another extremely young web-framework such as Julia Oxygen.jl. So whether that's worth the trade-off is up to you.
Fortunately, as more people start using Zap, more features are being added. For example, Zap recently finally has TLS added. The built-in Mustache template engine is also recently improved as well.
Zig 0.11 vs master (0.12)
By default, Zap is on Zig 0.11 so if you can, please use that instead.
However, Zig by default exposes master, which is currently pre-release 0.12.0, which would not build the Zap project properly.
Build using Zig 0.11
You don't necessarily need to build Zap although it would make referring to the examples and the source code easier.
To build Zap, you need to do the following:
1. git clone git@github.com:zigzap/zap.git
2. cd zap
3. zig build
If you want to build all the examples:
zig build all
If you only need to build a specific example such as hello
:
zig build hello
Zig master (0.12)
To build Zap on Zig master, you need to do the following:
1. git clone git@github.com:zigzap/zap.git
2. cd zap
3. git switch zig-0.12.0
3. zig build
The other steps are the same as 0.11.
Setup for Zig 0.11
For your Zap project, you need to have the following in your build.zig.zon
:
.{
.name = "wtf-zig-zap",
.version = "0.0.1",
.dependencies = .{
// zap v0.5.0
.zap = .{
.url = "https://github.com/zigzap/zap/archive/refs/tags/v0.5.0.tar.gz",
.hash = "1220aabff84ad1d800f5657d6a49cb90dab3799765811ada27faf527be45dd315a4d",
}
}
}
For build.zig
, you need the following:
const exe = b.addExecutable(.{
.name = "wtf-zig-zap",
// In this case the main source file is merely a path, however, in more
// complicated build scripts, this could be a generated file.
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const zap = b.dependency("zap", .{
.target = target,
.optimize = optimize,
});
exe.addModule("zap", zap.module("zap"));
exe.linkLibrary(zap.artifact("facil.io"));
Setup for Zig master (0.12)
Because all official Zap releases are based on Zig 0.11, for Zig master, you'll need to either build your own package or use a package I built.
.{
.name = "wtf-zig-zap",
.version = "0.0.1",
// note the extra paths field for zig 0.12
.paths = .{"."},
.dependencies = .{
// zap v0.4.0
.zap = .{
// note this is a package built from my forked repository
.url = "https://github.com/edyu/zap/archive/refs/tags/v0.4.0.tar.gz",
// the hash is also different
.hash = "12203e381b737b077759d3c63a1752fe79bb35dd50d1122a329a3f7b4504156d5595",
}
}
}
The build.zig
has some more changes due to Zig 0.12:
const exe = b.addExecutable(.{
.name = "wtf-zig-zap",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const zap = b.dependency("zap", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("zap", zap.module("zap"));
exe.linkLibrary(zap.artifact("facil.io"));
Hello World
This is basically the same example as hello
in the official examples:
The main entry to your code is the callback on_request
which you specify in zap.HttpListener.init()
.
In this example, there are on_request_verbose
and on_request_minimal
to illustrate how you get the path
and query
from the zap.Request
:
You start the server by using calling zap.start
.
The workers (.workers
) do not share memory so if you store states in your Zap server in memory, you'll have multiple copies of the states. Therefore, I recommend to start with .workers = 1
until you know what that means or that you make your server state-free and only use a shared database for states.
For threads, you use .threads
; you can have more than 1 and note that if all the threads hang, your server will hang as well.
const std = @import("std");
const zap = @import("zap");
fn on_request_verbose(r: zap.Request) void {
if (r.path) |the_path| {
std.debug.print("PATH: {s}\n", .{the_path});
}
if (r.query) |the_query| {
std.debug.print("QUERY: {s}\n", .{the_query});
}
r.sendBody("<html><body><h1>Hello from ZAP!!!</h1></body></html>") catch return;
}
fn on_request_minimal(r: zap.Request) void {
r.sendBody("<html><body><h1>Hello from ZAP!!!</h1></body></html>") catch return;
}
pub fn main() !void {
var listener = zap.HttpListener.init(.{
.port = 3000,
// .on_request = on_request_minimal,
.on_request = on_request_verbose,
.log = true,
.max_clients = 100000,
});
try listener.listen();
std.debug.print("Listening on 0.0.0.0:3000\n", .{});
// start worker threads
zap.start(.{
// if all threads hang, your server will hang
.threads = 2,
// workers share memory so do not share states if you have multiple workers
.workers = 1,
});
}
To run this:
zig build run
You can now go to localhost:3000
on your browser to see your server in action!
Static Files
You can specify a folder for static files with .public_folder
, which will be served by the server directly.
const std = @import("std");
const zap = @import("zap");
fn on_request(r: zap.Request) void {
r.setStatus(.not_found);
r.sendBody("<html><body><h1>404 - File not found</h1></body></html>") catch return;
}
pub fn main() !void {
var listener = zap.HttpListener.init(.{
.port = 3000,
.on_request = on_request,
.log = true,
.public_folder = "public",
.max_clients = 100000,
});
try listener.listen();
std.debug.print("Listening on 0.0.0.0:3000\n", .{});
// start worker threads
zap.start(.{
// if all threads hang, your server will hang
.threads = 2,
// workers share memory so do not share states if you have multiple workers
.workers = 1,
});
}
If you put a file such as zap.png
in the public
directory, you can access the file with localhost:3000/zap.png
.
Bonus
This is not specific to Zap but I found that it's invaluable to keep track of memory leaks during development.
Zig makes it very easy to keeping track of memory leaks. Just wrap your server code as follows:
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{
.thread_safe = true,
}){};
{
// use this allocator for all your memory allocation
var allocator = gpa.allocator();
var listener = zap.HttpListener.init(.{
.port = 3000,
.on_request = on_request,
.log = true,
.public_folder = "public",
.max_clients = 100000,
});
try listener.listen();
std.debug.print("Listening on 0.0.0.0:3000\n", .{});
// start worker threads
zap.start(.{
// if all threads hang, your server will hang
.threads = 2,
// workers share memory so do not share states if you have multiple workers
.workers = 1,
});
}
// all defers should have run by now
std.debug.print("\n\nSTOPPED!\n\n", .{});
// we'll arrive here after zap.stop()
const leaked = gpa.detectLeaks();
std.debug.print("Leaks detected: {}\n", .{leaked});
}
Now, whenever you Ctrl-C
out of your server, it will report whether you have memory leaks in your application.
Until Next Time
Once again, I recommend reading the examples because there is pretty much an example of everything to get you started such as sending a file, using a template engine, and basic routing.
As I wrote earlier, there is no modern authentication built-in so if you want to use cookies for authentication and/or OAuth, you need to implement it yourself. I'll likely follow up next time with an oauth implementation in Zap.
The End
You can find the code for the article here.
Zap is here and the patched facil.io is here.
Facil.io is here and the code is here.
The examples are here.
Top comments (6)
Hey, zap author here. Thanks for the great article. Just a few things I'd like to point out.
zap.Auth
for the part of authentication / authorization that zap supports. It's low-level support but you don't have to start from scratch.build_all.sh
: you can also build all withzig build all
.Yo, I just pushed getParamSlice() and getParamSlices() to master, will release + docupdate soon:
The differences to the functions accepting an allocator are:
true
This is totally acceptable for many use-cases.
Thank you for taking the time to break it down, it seems easy indeed! I learned C++ a few years back and it was definitely challenging, but my knowledge of this programming language really helps in this case. Will be giving it a go.
Regards from Geneva,
Matthieu Bordil
CTO Egga
Thank you for the kind words, Matthieu. I'm a watch guy so seeing that you are from Geneva makes me want to visit. :) Enjoy your journey in Zig; I certainly learned a lot. Also when you have chance, look into Odin as well. I will likely write an article contrasting Zig with Odin next month. I like them both and they excel at different areas.
thanks for writing this up @edyu and also thank you for your early patches to work with zig's master branch 0.12.x.
Thank you. I'm debating whether to patch some more as there are more stuff that broke even now with new Zig updates. :)