Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework Zig SDK #2

Open
wants to merge 23 commits into
base: main
Choose a base branch
from
Open

Conversation

sea-grass
Copy link

Note: See previous discussion/code review in the PR from the archived repository: vmware-labs#264


Summary

This PR reworks the Zig SDK to provide more control to the user w.r.t. dynamic allocation (fixes vmware-labs#218) and also provides facilities to write the worker config directly from the Zig build.zig, so Zig developers can run e.g. zig build; wws zig-out root and be up and running with wws in a matter of seconds.


TODO

  • Rewrite worker Zig SDK to wws Zig SDK
  • Add zig example using the wws SDK
  • Rewrite existing Zig examples using the wws SDK
  • Add example using zig-router
  • Add example demonstrating dynamic allocation (of headers, KV, response body)
  • Add example with folder mounts
  • Update Zig docs
  • Add benchmarks between current SDK (kits/zig/worker) and new SDK (in my initial testing, the new method is way faster)
  • Figure out the suggested way to get Zig developers to add wws as a dependency (see outstanding questions below)

Details

A summary of changes, so far:

  • Zig 0.11 -> Zig. 0.12 (this is important, as it includes updates to the Zig package manager)
  • All zig examples will be in examples/zig-examples to ease maintenance of Zig example workers and also demonstrate composition of multiple wws workers in a single server
  • Renamed the Zig SDK module from worker to wws
  • The wws module exposes some build-time functions to build a Zig worker and associated config
  • The wws module exposes functions to read the request object from stdin (or any reader) and write the request object to stdout (or any writer)

I believe these changes align with the goals of a worker:

  • Easier to develop: Since the worker's main function is the request handler (no need to defer to a handleFn, unless it makes sense to split code that way for your use case), the code stays small and focused.
  • Easier to test: The wws request parser lets you alternatively provide a string as request input (no need to depend on stdin) and the wws response writer lets you provide your own writer (does not depend on stdout), so it's easy for developers to test individual parts of their worker. Additionally, the request parser requires the developer to provide their own allocator, so it's easy to ensure there are no memory leaks.
  • Easier to deploy: The wws module provides helpers for compiling wasm and writing the associated worker config, so there are less steps to getting a wws server up and running.

Overall, with the new API surface, developing wws workers with Zig looks like this:

  1. Add wws as a dependency
  2. In your build.zig, use const worker = try wws.addWorker(...) to build your Zig program as a WASM module
  3. In your build.zig, use worker.addToWriteFiles(b, write_files) to copy the worker and its associated config to a WWS server directory
  4. In your root source file (i.e. src/main.zig), use wws.parseStream(allocator, parse_config) to decode the request
  5. Construct a wws.Response
  6. Use wws.writeResponse(response, writer) to write the response

Minimal Zig worker now looks like this (click to expand)

// build.zig.zon
.{
  // ...
  .dependencies = .{
    .wws = .{
      .path = "../path/to/kits/zig/wws",
    },
  },
}
// build.zig
const std = @import("std");
const wws = @import("wws");

pub fn build(b: *std.Build) !void {
  const wws_dep = b.dependency("wws", .{});

  const wf = b.addWriteFiles();

  const worker = try wws.addWorker(.{
    .name = "example",
    .root_source_file = .{ .path = "src/main.zig" },
    .wws = wws_dep,
  });

  worker.addToWriteFiles(b, wf);

  const install = b.addInstallDirectory(.{
    .source_dir = wf.getDirectory(),
    .install_dir = .prefix,
    .install_subdir = "root",
  });

  b.getInstallStep().dependOn(&install.step);
// main.zig
const std = @import("std");
const wws = @import("wws");

pub fn main() !void {
  var gpa = std.heap.GeneralPurposeAllocator(.{}){};
  // can switch on the result to check for memory leaks
  defer _ = gpa.deinit();
  const allocator = gpa.allocator();

  // By default, will read from stdin
  const parse_result = try wws.parseStream(allocator, .{});
  const request = parse_result.value;

  const response = wws.Response{
    // Simply echo the request url
    .data = request.url,
  };

  const stdout = std.io.getStdOut();
  // Serializes the response as a json object and writes to the provided writer
  try wws.writeResponse(response, stdout.writer());
}

To build/run the server:

$ zig build
$ tree zig-out/root
zig-out/root/
├── example.toml
└── example.wasm
$ wws zig-out/root
⚙️  Preparing the project from: zig-out/root
⚙️  Loading routes from: zig-out/root
⏳ Loading workers from 1 routes...
✅ Workers loaded in 29.292541ms.
    - http://127.0.0.1:8080/example
      => zig-out/root/example.wasm
🚀 Start serving requests at http://127.0.0.1:8080

Outstanding questions:

  • Should the Zig kit be moved to a separate repo? Zig's package manager lets you specify dependencies in the local filesystem or a remote tar archive, but I'm not sure if it lets you specify a "relative root" for the Zig package in that tarball. An alternative would be to move the build.zig to the root of this repository, but that may be confusing.

This commit introduces `kits/zig/wws`, with the intention of replacing `kits/zig/worker`.
The design of the new SDK improves upon the previous one by exposing utilities for parsing wws requests and serializing wws responses, to give the user more control over memory while still keeping the SDK easy to use.
The name was changed to make it more ergonomic within the Zig ecosystem (the developer will pull in a `wws` dependency to interface with wws, rather than a `worker` dependency).
The dependency `zig-router` doesn't yet support the latest Zig. Once that library is updated, we can uncomment this example.

`std.fs.Dir.openFileWasi` has been removed, so we can just use `std.fs.Dir.openFile`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow the Zig SDK to return arbitrarily big responses
1 participant