After the recent announcement regarding WASI support in Workers, I decided to see what it would take to get code written in Zig to run as a Worker, and it turned out to be trivial. This post documents the process I followed as a new user of Zig. It’s so exciting to see how Cloudflare Workers is a polyglot platform allowing you to write programs in the language you love, or the language you’re learning!
Hello, World!
I’m not a Zig expert by any means, and to keep things entirely honest I’ve only just started looking into the language, but we all have to start somewhere. So, if my Zig code isn’t perfect please bear with me. My goal was to build a real, small program using Zig and deploy it on Cloudflare Workers. And to see how fast I can go from a blank screen to production code.
My goal for this wasn’t ambitious, just read some text from stdin and print it to stdout with line numbers, like running cat -n
. But it does show just how easy the Workers paradigm is. This Zig program works identically on the command-line on my laptop and as an HTTP API deployed on Cloudflare Workers.
Here’s my code. It reads a line from stdin and outputs the same line prefixed with a line number. It terminates when there’s no more input.
const std = @import("std");
pub fn main() anyerror!void {
// setup allocator
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer std.debug.assert(!gpa.deinit());
const allocator = gpa.allocator();
// setup streams
const stdout = std.io.getStdOut().writer();
const in = std.io.getStdIn();
var reader = std.io.bufferedReader(in.reader()).reader();
var counter: u32 = 1;
// read input line by line
while (try reader.readUntilDelimiterOrEofAlloc(allocator, '\n', std.math.maxInt(usize))) |line| {
defer allocator.free(line);
try stdout.print("{d}\t{s}\n", .{counter, line});
counter = counter + 1;
}
}
To build Zig code, you create a build.zig
file that defines how to build your project. For this trivial case I just opted to build an executable from the sources
const std = @import("std");
pub fn build(b: *std.build.Builder) void {
const target = b.standardTargetOptions(.{});
const mode = b.standardReleaseOptions();
const exe = b.addExecutable("print-with-line-numbers", "src/main.zig");
exe.setTarget(target);
exe.setBuildMode(mode);
exe.install();
}
By running zig build
the compiler will run and output a binary under zig-out/bin
$ zig build
$ ls zig-out/bin
print-with-line-numbers
$ echo "Hello\nWorld" | ./zig-out/bin/print-with-line-numbers
1 Hello
2 World
WASI
The next step is to get this running on Workers, but first I need to compile it into WASM with WASI support.
Thankfully, this comes out of the box with recent versions of Zig, so you can just tell the compiler to build your executable using the wasm32-wasi
target, which will produce a file that can be run on any WASI-compatible WebAssembly runtime, such as wasmtime.
This same .wasm file can be run in wasmtime and deployed directly to Cloudflare Workers. This makes building, testing and deploying seamless.
$ zig build -Dtarget=wasm32-wasi
$ ls zig-out/bin
print-with-line-numbers.wasm
$ echo "Hello\nWorld" | wasmtime ./zig-out/bin/print-with-line-numbers.wasm
1 Hello
2 World
Zig on Workers
With our binary ready to go, the last piece is to get it running on Cloudflare Workers using wrangler2. That is as simple as publishing the .wasm file on workers.dev. If you don’t have a workers.dev account, you can follow the tutorial on our getting started guide that will get you from code to deployment within minutes!
In fact, once I signed up for my account, all I needed to do was complete the first two steps, install wrangler and login.
$ npx wrangler@wasm login
Attempting to login via OAuth...
Opening a link in your default browser: https://dash.cloudflare.com/oauth2/auth
Successfully logged in.
Then, I ran the following command to publish my worker:
$ npx wrangler@wasm publish --name print-with-line-numbers --compatibility-date=2022-07-07 zig-out/bin/print-with-line-numbers.wasm
Uploaded print-with-line-numbers (3.04 sec)
Published print-with-line-numbers (6.28 sec)
print-with-line-numbers.workers.dev
With that step completed, the worker is ready to run and can be invoked by calling the URL printed from the output above.
echo "Hello\nWorld" | curl https://print-with-line-numbers.workers.dev -X POST --data-binary @-
1 Hello
2 World
Success!
Conclusion
What impressed me the most here was just how easy this process was.
First, I had a binary compiled for the architecture of my laptop, then I compiled the code into WebAssembly by just passing a flag to the compiler, and finally I had this running on workers without having to change any code.
Granted, this program was not very complicated and does not do anything other than read from STDIN and write to STDOUT, but it gives me confidence of what is possible, especially as technology like WASI matures.