Zig NEWS

Cover image for Zig Zap -- WTF is ⚡Zap⚡
Ed Yu
Ed Yu

Posted on • Updated on

Zig Zap -- WTF is ⚡Zap⚡

The power of ⚡Zap⚡ for Zig


Ed Yu (@edyu on Github and
@edyu on Twitter)
January.23.2024


Zig Logo

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:

  1. Zap is designed by Rene, who's probably one of the most practical engineers I know.
  2. Zap is extremely fast.
  3. 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 Request/Second

Zap Transfer/Second

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:

  1. If you have NIH Syndrome, Zap is NOT implemented in Zig.
  2. 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.
  3. 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.

Not Invented Here

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!", .{});
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

If you want to build all the examples:

zig build all
Enter fullscreen mode Exit fullscreen mode

If you only need to build a specific example such as hello:

zig build hello
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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",
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

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",
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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"));
Enter fullscreen mode Exit fullscreen mode

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,
    });
}
Enter fullscreen mode Exit fullscreen mode

To run this:

zig build run
Enter fullscreen mode Exit fullscreen mode

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,
    });
}
Enter fullscreen mode Exit fullscreen mode

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});
}
Enter fullscreen mode Exit fullscreen mode

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.

The Zap discord is here.

Zig Logo

Top comments (6)

Collapse
 
renerocksai profile image
Rene Schallner

Hey, zap author here. Thanks for the great article. Just a few things I'd like to point out.

  • zap has autodoc docs
  • zig master: yes, maybe I can enable tagged releases for that in the future.
  • see 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 with zig build all.
  • the getParamStr() is ugly because it shouldn't need an allocator. The added "convenience" to be allowed to allocate -> use returned string after exit of request handler, came with that prize. I am thinking of changing that.
Collapse
 
renerocksai profile image
Rene Schallner • Edited

Yo, I just pushed getParamSlice() and getParamSlices() to master, will release + docupdate soon:

// ========================================================
// Access RAW params from querystring
// ========================================================

// let's get param "one" by name
if (r.getParamSlice("one")) |value| {
    std.log.info("Param one = {s}", .{value});
} else {
    std.log.info("Param one not found!", .{});
}

var arg_it = r.getParamSlices();
while (arg_it.next()) |param| {
    std.log.info("ParamStr `{s}` is `{s}`", .{ param.name, param.value });
}
Enter fullscreen mode Exit fullscreen mode

The differences to the functions accepting an allocator are:

  • they just return slices into the query string
  • params are not decoded and type-tagged:
    • "hello+world" will not be turned into "hello world"
    • "true" will not be turned into true
  • the strings vanish on return of the request function, so dupe() them if you need to hold on to them.

This is totally acceptable for many use-cases.

Collapse
 
grassdev profile image
grassdev

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

Collapse
 
edyu profile image
Ed Yu

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.

Collapse
 
aquamo4k profile image
Aquamo4k

thanks for writing this up @edyu and also thank you for your early patches to work with zig's master branch 0.12.x.

Collapse
 
edyu profile image
Ed Yu

Thank you. I'm debating whether to patch some more as there are more stuff that broke even now with new Zig updates. :)