zzz Framework

repository·main·Indexed 20 days ago

https://github.com/tardy-org/zzz

A high-performance, modular Zig framework for building networked HTTP/HTTPS services. Designed for extreme efficiency and low memory usage, zzz is suitable for high-traffic servers, embedded systems, and bare-metal environments. It is built on the Tardy asynchronous runtime and supports various I/O backends including io_uring, epoll, kqueue, and poll across Linux, Mac, and Windows. Features include a layered router with middleware support, TLS via secsock, and memory pooling to operate in configurations as low as 256 kB RAM.

Tokens
4.2K
Snippets
11
Records
13
Agent score
73%

What's inside zzz

  1. What is zzz?

    main

    zzz is a high-performance, reliable framework for writing networked services in Zig. It supports both HTTP and HTTPS and is designed with modularity and portability in mind.

    Key Characteristics

    • Performance: Optimized via startup allocation and reduced thread contention. It is designed to compete with high-performance servers like gnet while maintaining a significantly lower memory footprint.
    • Modularity: Allows swapping implementations for various components. Consumers can provide their own async implementations, making it suitable for standard servers, embedded systems, or bare-metal domains.
    • Platform Support: Supports Linux, Mac, and Windows. Linux is the recommended target for deployments.
    • Async Runtime: Built on top of the Tardy asynchronous runtime.
  2. Core features of zzz

    main

    zzz provides several advanced networking and routing features:

    • Modular Asynchronous Implementation: Supports various backends depending on the OS:
      • io_uring for Linux (>= 5.1.0)
      • epoll for Linux (>= 2.5.45)
      • kqueue for BSD & Mac
      • poll for Linux, Mac, and Windows
    • Layered Router: Includes support for Middleware.
    • Concurrency Models: Supports both single-threaded and multi-threaded modes.
    • TLS Support: Uses secsock for secure connections.
    • Memory Management: Utilizes memory pooling to minimize allocations and allow for minimal RAM configurations (e.g., as low as 256 kB).
  3. Implement TLS/HTTPS support in zzz

    main

    zzz provides TLS functionality through the secsock library, which is decoupled from the I/O layer for portability. To implement HTTPS, you must wrap a standard Socket into a SecureSocket using a secsock.BearSSL instance and then pass that secure socket to the http.Server.serve method.

    Key Requirements:

    • Certificate and Key: You must provide both a certificate and a private key. Because the PEM format allows multiple items in a single file, you must explicitly provide the identifiers for the certificate (e.g., "CERTIFICATE") and the private key (e.g., "EC PRIVATE KEY").
    • Server Configuration: When calling server.serve, pass the SecureSocket via the .secure field in the options object.

    Note: TLS support is currently considered a rough, evolving area of the project and may undergo significant changes in future development cycles.

    // ... setup socket and bearssl ...
    var bearssl: secsock.BearSSL = .init(init.gpa);
    defer bearssl.deinit();
    
    try bearssl.add_cert_chain(
        "CERTIFICATE",
        @embedFile("certs/cert.pem"),
        "EC PRIVATE KEY",
        @embedFile("certs/key.pem"),
    );
    
    const secure = try bearssl.to_secure_socket(socket, .server);
    
    // ... pass to server ...
    try server.serve(rt, p.router, .{ .secure = p.socket });
  4. Add zzz to your build.zig

    main

    After fetching the dependency, add it to your build.zig file to make the zzz module available to your executable.

    const zzz = b.dependency("zzz", .{
        .target = target,
        .optimize = optimize,
    }).module("zzz");
    
    exe.root_module.addImport(zzz);
  5. Install zzz via Zig fetch

    main

    You can install zzz using the Zig package manager. Choose the command based on whether you want the latest stable release or the development version.

    Latest Release (v0.3.2)

    Recommended for most users. Uses tardy v0.3.2, secsock v0.1.2, and Zig 0.16.0.

    Development Version

    Use this if you are working with tardy/main, secsock/main, and Zig 0.17.0-dev.1454+5faa79730.

    Note: zzz is currently in alpha and is subject to rapid changes.

    # Install latest release
    zig fetch --save 'git+https://github.com/tardy-org/zzz#v0.3.2'
    
    # Install development version
    zig fetch --save 'git+https://github.com/tardy-org/zzz?ref=main#commit_hash'
  6. Full TLS/HTTPS Server Example

    main

    This complete example demonstrates initializing a Tardy instance, setting up a Router with middleware, creating a TCP Socket, wrapping it with secsock.BearSSL, and serving it via http.Server using the .secure option.

    const Tardy = tardy.Tardy(.auto);
    
    fn root_handler(ctx: *const Context, _: void) !Respond {
        const body = "<!DOCTYPE html><html><body><h1>Hello, World!</h1></body></html>";
        return ctx.response.apply(.{
            .status = .OK,
            .mime = .HTML,
            .body = body[0..],
        });
    }
    
    pub fn main(init: std.process.Init) !void {
        const host: []const u8 = "0.0.0.0";
        const port: u16 = 9862;
    
        var t: Tardy = try .init(init.gpa, init.io, .{ .threading = .auto });
        defer t.deinit();
    
        var router: Router = try .init(init.gpa, &.{ 
            Route.init("/").get({}, root_handler).layer(),
        }, .{});
        defer router.deinit(init.gpa);
    
        var socket: Socket = try .init(init.io, .{ .tcp = .{ .host = host, .port = port } });
        defer socket.close_blocking();
        try socket.bind();
        try socket.listen(1024);
    
        var bearssl: secsock.BearSSL = .init(init.gpa);
        defer bearssl.deinit();
    
        try bearssl.add_cert_chain(
            "CERTIFICATE",
            @embedFile("certs/cert.pem"),
            "EC PRIVATE KEY",
            @embedFile("certs/key.pem"),
        );
        const secure = try bearssl.to_secure_socket(socket, .server);
    
        const EntryParams = struct {
            router: *const Router,
            socket: SecureSocket,
        };
        const params: EntryParams = .{ .router = &router, .socket = secure };
    
        try t.entry(
            params,
            struct {
                fn entry(rt: *Runtime, p: EntryParams) !void {
                    var server: Server = .init(.{ .stack_size = .max });
                    try server.serve(rt, p.router, .{ .secure = p.socket });
                }
            }.entry,
        );
    }
  7. Create a basic HTTP server with zzz

    main

    To serve HTTP responses, you need to initialize the tardy.Tardy runtime, set up an http.Router with routes, and bind a tardy.net.Socket to a host and port. The server is then started within the t.entry callback, which provides access to the Runtime.

    const std = @import("std");
    const zzz = @import("zzz");
    const http = zzz.http;
    const tardy = zzz.tardy;
    const Runtime = tardy.Runtime;
    const Socket = tardy.net.Socket;
    const Server = http.Server;
    const Router = http.Router;
    const Context = http.Context;
    const Route = Router.Route;
    const Respond = http.Respond;
    
    const log = std.log.scoped(.@"examples/basic");
    
    const Tardy = tardy.Tardy(.auto);
    
    fn base_handler(ctx: *const Context, _: void) !Respond {
        return ctx.response.apply(.{
            .status = .OK,
            .mime = http.Mime.HTML,
            .body = "Hello, world!",
        });
    }
    
    pub fn main(init: std.process.Init) !void {
        const host: []const u8 = "0.0.0.0";
        const port: u16 = 9862;
    
        var t: Tardy = try .init(init.gpa, init.io, .{ .threading = .auto });
        defer t.deinit();
    
        var router: Router = try .init(init.gpa, &{
            Route.init("/").get({}, base_handler).layer(),
        }, .{});
        defer router.deinit(init.gpa);
    
        // create socket for tardy
        var socket: Socket = try .init(init.io, .{
            .tcp = .{ .host = host, .port = port },
        });
        defer socket.close_blocking();
        try socket.bind();
        try socket.listen(4096);
    
        const EntryParams = struct {
            router: *const Router,
            socket: Socket,
        };
        const params: EntryParams = .{ .router = &router, .socket = socket };
    
        try t.entry(
            params,
            struct {
                fn entry(rt: *Runtime, p: EntryParams) !void {
                    var server: Server = .init(.{
                        .stack_size = .@"4MiB",
                        .socket_buffer_bytes = 1024 * 2,
                        .keepalive_count_max = null,
                        .connection_count_max = 1024,
                    });
                    try server.serve(rt, p.router, .{ .normal = p.socket });
                }
            }.entry,
        );
    }
  8. Use secsock.BearSSL to create a SecureSocket

    main

    The secsock.BearSSL type is used to manage TLS certificates and transform standard sockets into secure ones. Use add_cert_chain to load your credentials and to_secure_socket to wrap your existing socket.

    add_cert_chain parameters:

    • cert_name: The PEM identifier for the certificate (e.g., "CERTIFICATE").
    • cert_data: The actual certificate data (can be provided via @embedFile or a buffer).
    • key_name: The PEM identifier for the private key (e.g., "EC PRIVATE KEY").
    • key_data: The actual private key data.

    to_secure_socket parameters:

    • socket: The underlying tardy.net.Socket.
    • mode: The TLS mode (e.g., .server).
    var bearssl: secsock.BearSSL = .init(init.gpa);
    // Load certs
    try bearssl.add_cert_chain(
        "CERTIFICATE",
        @embedFile("certs/cert.pem"),
        "EC PRIVATE KEY",
        @embedFile("certs/key.pem"),
    );
    // Wrap socket
    const secure = try bearssl.to_secure_socket(socket, .server);
  9. Configure the `Server.Config` options

    main

    The Config struct allows you to tune the performance and limits of the HTTP server. Key configuration options include:

    • stack_size: The stack size for coroutines. Default is .@"1MiB". Increase this if using many middlewares or heavy stack usage.
    • connection_count_max: Maximum concurrent connections per runtime. If null, there is no limit. Connections exceeding this are dropped.
    • keepalive_count_max: Maximum number of request-response cycles allowed per keep-alive connection. If null, there is no limit.
    • connection_arena_bytes_retain: Amount of memory retained after an arena is cleared. Higher values increase speed at the cost of memory. Default is 1024.
    • list_recv_bytes_retain: Amount of space on the recv_buffer retained after every send. Default is 1024.
    • list_recv_bytes_max: Maximum size of the Recv buffer. Default is 2MB.
    • socket_buffer_bytes: Size of the buffer used for socket interaction. Default is 1024.
    • capture_count_max: Maximum number of captures in a route. Default is 8.
    • request_bytes_max: Maximum size of a request in bytes. Default is 2MB.
    • request_uri_bytes_max: Maximum size of the request URI in bytes. Default is 2KB.
    pub const Config = struct {
        stack_size: Coroutine.Stack = .@"1MiB",
        connection_count_max: ?u32 = null,
        keepalive_count_max: ?u16 = null,
        connection_arena_bytes_retain: u32 = 1024,
        list_recv_bytes_retain: u32 = 1024,
        list_recv_bytes_max: u32 = 1024 * 1024 * 2,
        socket_buffer_bytes: u32 = 1024,
        capture_count_max: u16 = 8,
        request_bytes_max: u32 = 1024 * 1024 * 2,
        request_uri_bytes_max: u32 = 1024 * 2,
    };
  10. Initialize and serve an HTTP server with `Server`

    main

    To run an HTTP server, initialize a Server instance with a Config object and call serve. You must provide a Runtime, a Router for request routing, and a SocketKind (either .normal or .secure) to specify the security mode.

    Note that serve is an asynchronous operation that spawns the main server loop within the provided Runtime.

    const server = Server.init(.{
        .connection_count_max = 1024,
        // ... other Config fields
    });
    
    try server.serve(rt, router, .{ .normal = socket });