From cb381a35ce96056cf276d9fcbc7eb0c301a3479b Mon Sep 17 00:00:00 2001 From: Christoph Voigt Date: Tue, 5 Sep 2023 04:41:55 +0200 Subject: [PATCH] feat: add Zig SDK and documentation (#196) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add support for zig (#144) * add wasi_exec_model reactor * use custom request/response entities * add params and env * add example zig-basic * add example zig-kv * add example zig-params * add example zig-envs * add zig docs to example README * add .gitignore to ignore zig build and cache directories * add utf8 validity check * add corrections to docs Co-authored-by: Rafael Fernández López * rename writeHeader to setStatus * rename s/data/body to be consistent with other languages * rename s/bash/shell-session Co-authored-by: Rafael Fernández López * add info for RequestAndOutput wrapper struct * fix read input until EOF --------- Co-authored-by: Rafael Fernández López --- .gitignore | 4 +- README.md | 2 +- docs/docs/features/all.md | 1 + docs/docs/features/dynamic-routes.md | 2 + docs/docs/features/environment-variables.md | 1 + docs/docs/features/http-requests.md | 1 + docs/docs/features/key-value.md | 2 + docs/docs/features/mount-folders.md | 1 + docs/docs/languages/zig.md | 345 ++++++++++++++++++++ docs/src/css/custom.css | 4 + docs/static/img/languages/zig.svg | 1 + examples/README.md | 1 + examples/zig-basic/README.md | 32 ++ examples/zig-basic/build.zig | 26 ++ examples/zig-basic/src/basic.zig | 57 ++++ examples/zig-kv/README.md | 33 ++ examples/zig-kv/build.zig | 26 ++ examples/zig-kv/src/worker-kv.zig | 46 +++ examples/zig-kv/zig-out/bin/worker-kv.toml | 6 + examples/zig-params/README.md | 33 ++ examples/zig-params/build.zig | 26 ++ examples/zig-params/src/worker-params.zig | 30 ++ kits/zig/worker/README.md | 49 +++ kits/zig/worker/src/worker.zig | 261 +++++++++++++++ 24 files changed, 988 insertions(+), 2 deletions(-) create mode 100644 docs/docs/languages/zig.md create mode 100644 docs/static/img/languages/zig.svg create mode 100644 examples/zig-basic/README.md create mode 100644 examples/zig-basic/build.zig create mode 100644 examples/zig-basic/src/basic.zig create mode 100644 examples/zig-kv/README.md create mode 100644 examples/zig-kv/build.zig create mode 100644 examples/zig-kv/src/worker-kv.zig create mode 100644 examples/zig-kv/zig-out/bin/worker-kv.toml create mode 100644 examples/zig-params/README.md create mode 100644 examples/zig-params/build.zig create mode 100644 examples/zig-params/src/worker-params.zig create mode 100644 kits/zig/worker/README.md create mode 100644 kits/zig/worker/src/worker.zig diff --git a/.gitignore b/.gitignore index 10e1fd84..d1c96cf2 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ target !tests/**/*.wasm examples/*.toml .DS_Store -.wws \ No newline at end of file +.wws +**/zig-cache +**/zig-out \ No newline at end of file diff --git a/README.md b/README.md index c71c3540..87cdbebc 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Wasm Workers Server focuses on simplicity. We want you to run workers (written i | Go | ✅ | No | [#95](https://github.com/vmware-labs/wasm-workers-server/issues/95) | | Ruby | ✅ | [Yes](https://workers.wasmlabs.dev/docs/languages/ruby#installation) | [#63](https://github.com/vmware-labs/wasm-workers-server/issues/63) | | Python | ✅ | [Yes](https://workers.wasmlabs.dev/docs/languages/python#installation) | [#63](https://github.com/vmware-labs/wasm-workers-server/issues/63) | -| Zig | 🚧 | No | [#144](https://github.com/vmware-labs/wasm-workers-server/issues/144) | +| Zig | ✅ | No | [#144](https://github.com/vmware-labs/wasm-workers-server/issues/144) | | PHP | 🚧 | No | [#100](https://github.com/vmware-labs/wasm-workers-server/issues/100) | To get more information about multi-language support in Wasm Workers Server, [check our documentation](https://workers.wasmlabs.dev/docs/languages/introduction). diff --git a/docs/docs/features/all.md b/docs/docs/features/all.md index 7b9b120a..c2a9ff66 100644 --- a/docs/docs/features/all.md +++ b/docs/docs/features/all.md @@ -36,3 +36,4 @@ The following table shows the language compatibility for the different worker fu | Go | ✅ | ✅ | ✅ | ✅ | ✅ | | Ruby | ✅ | ✅ | ✅ | ✅ | ❌ | | Python | ✅ | ✅ | ✅ | ✅ | ❌ | +| Zig | ✅ | ❌ | ✅ | ✅ | ❌ | diff --git a/docs/docs/features/dynamic-routes.md b/docs/docs/features/dynamic-routes.md index 41ea9872..7c0724b9 100644 --- a/docs/docs/features/dynamic-routes.md +++ b/docs/docs/features/dynamic-routes.md @@ -27,6 +27,7 @@ Check these guides to understand how to read parameters in the different support * [Dynamic routes in Python](../languages/python.md#dynamic-routes) * [Dynamic routes in Ruby](../languages/ruby.md#dynamic-routes) * [Dynamic routes in Go](../languages/go.md#dynamic-routes) +* [Dynamic routes in Zig](../languages/zig.md#dynamic-routes) ## Dynamic routes and folders @@ -64,3 +65,4 @@ In this case, the `./[resource]/[id]/show.js` worker replies to URLs like `/arti | Go | ✅ | | Ruby | ✅ | | Python | ✅ | +| Zig | ✅ | diff --git a/docs/docs/features/environment-variables.md b/docs/docs/features/environment-variables.md index aeefefc5..fea3fdfd 100644 --- a/docs/docs/features/environment-variables.md +++ b/docs/docs/features/environment-variables.md @@ -50,3 +50,4 @@ This feature allows you to configure environment variables dynamically. | Go | ✅ | | Ruby | ✅ | | Python | ✅ | +| Zig | ❌ | diff --git a/docs/docs/features/http-requests.md b/docs/docs/features/http-requests.md index 30695083..d1abda7a 100644 --- a/docs/docs/features/http-requests.md +++ b/docs/docs/features/http-requests.md @@ -44,3 +44,4 @@ Check these guides to perform HTTP requests in the different supported languages | Go | ✅ | | Ruby | ❌ | | Python | ❌ | +| Zig | ❌ | diff --git a/docs/docs/features/key-value.md b/docs/docs/features/key-value.md index de2bf483..b46e40bb 100644 --- a/docs/docs/features/key-value.md +++ b/docs/docs/features/key-value.md @@ -21,6 +21,7 @@ The worker may access all the data and perform changes over it. Then, a new K/V * [Add a K/V store to Python workers](../languages/python.md#add-a-key--value-store) * [Add a K/V store to Ruby workers](../languages/ruby.md#add-a-key--value-store) * [Add a K/V store to Go workers](../languages/go.md#add-a-key--value-store) +* [Add a K/V store to Zig workers](../languages/zig.md#add-a-key--value-store) ## Limitations @@ -35,3 +36,4 @@ A known limitation of the snapshot approach is the data override when concurrent | Go | ✅ | | Ruby | ✅ | | Python | ✅ | +| Zig | ✅ | diff --git a/docs/docs/features/mount-folders.md b/docs/docs/features/mount-folders.md index 10b169be..12f1c1d2 100644 --- a/docs/docs/features/mount-folders.md +++ b/docs/docs/features/mount-folders.md @@ -49,3 +49,4 @@ Note that those folders may include files that `wws` recognizes as workers (like | Go | ✅ | | Ruby | ✅ | | Python | ✅ | +| Zig | ✅ | diff --git a/docs/docs/languages/zig.md b/docs/docs/languages/zig.md new file mode 100644 index 00000000..c1a97eed --- /dev/null +++ b/docs/docs/languages/zig.md @@ -0,0 +1,345 @@ +--- +sidebar_position: 6 +--- + +# Zig + +Zig workers are tested with Zig version `0.11.0`. Then, they are loaded by Wasm Workers Server and start processing requests. + +## Your first Zig worker + +The recommended way to implement workers is by using the `worker.ServeFunc` function. + +In this example, the worker will get a request and print all the related information. + +1. Create a new Zig project: + + ```shell-session + zig init-exe + ``` + +2. Add Wasm Workers Server Zig dependency + + At this point in time Zigs Package manager is not yet available. We will therefore clone the repository to make the library locally available. + + ```shell-session + mkdir lib + wget -O ./lib/worker.zig https://raw.githubusercontent.com/vmware-labs/wasm-workers-server/main/kits/zig/worker/worker.zig + ``` + +3. Edit the `src/main.zig` to match the following contents: + + ```c title="worker.zig" + const std = @import("std"); + const worker = @import("worker"); + + fn requestFn(resp: *worker.Response, r: *worker.Request) void { + _ = r; + + _ = &resp.headers.append("x-generated-by", "wasm-workers-server"); + _ = &resp.writeAll("hello from zig"); + } + + pub fn main() !void { + worker.ServeFunc(requestFn); + } + ``` + +4. Additionally, you can now go further add all the information from the received `worker.Request`: + + ```c title="worker.zig" + const std = @import("std"); + const worker = @import("worker"); + + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + const allocator = arena.allocator(); + + fn requestFn(resp: *worker.Response, r: *worker.Request) void { + std.debug.print("Hello from function\n", .{ }); + + // // TODO: prepare to read request body and send it back + std.debug.print("+++ doing payload \n", .{ }); + + var payload: []const u8 = ""; + var reqBody = r.data; + + if (reqBody.len == 0) { + payload = "-"; + } else { + payload = reqBody; + } + + const s = + \\ + \\ + \\Wasm Workers Server + \\ + \\ + \\ + \\ + \\ + \\ + \\
+ \\

Hello from Wasm Workers Server 👋

+ \\
Replying to {s}
+            \\Method: {s}
+            \\User Agent: {s}
+            \\Payload: {s}
+ \\

+ \\This page was generated by a Zig⚡️ file running in WebAssembly. + \\

+ \\
+ \\ + ; + + var body = std.fmt.allocPrint(allocator, s, .{ r.url.path, r.method, "-", payload }) catch undefined; + + _ = &resp.headers.append("x-generated-by", "wasm-workers-server"); + _ = &resp.writeAll(body); + } + + pub fn main() !void { + worker.ServeFunc(requestFn); + } + ``` + +5. Compile the project + + ```shell-session + zig build-exe src/main.zig \ + --name worker \ + -mexec-model=reactor \ + -target wasm32-wasi \ + --mod worker::lib/worker.zig \ + --deps worker + ``` + + You can also use a build script to build the project with a simple `zig build`, please find some inspiration in our [zig examples](https://github.com/vmware-labs/wasm-workers-server/tree/main/examples/). + +6. Run your worker with `wws`. If you didn't download the `wws` server yet, check our [Getting Started](../get-started/quickstart.md) guide. + + ```shell-session + wws . + + ⚙️ Loading routes from: . + 🗺 Detected routes: + - http://127.0.0.1:8080/worker + => worker.wasm (name: default) + 🚀 Start serving requests at http://127.0.0.1:8080 + ``` + +7. Finally, open in your browser. + +## Add a Key / Value store + +Wasm Workers allows you to add a Key / Value store to your workers. Read more information about this feature in the [Key / Value store](../features/key-value.md) section. + +To add a KV store to your worker, follow these steps: + +1. Create a new Zig project: + + ```shell-session + zig init-exe + ``` + +2. Add Wasm Workers Server Zig dependency + + At this point in time Zigs Package manager is not yet available. We will therefore clone the repository to make the library locally available. + + ```shell-session + mkdir lib + wget -O ./lib/worker.zig https://raw.githubusercontent.com/vmware-labs/wasm-workers-server/main/kits/zig/worker/worker.zig ./lib + ``` + +1. Edit `src/main.zig` file with the following contents: + + ```c title="main.zig" + const std = @import("std"); + const worker = @import("worker"); + + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + const allocator = arena.allocator(); + + fn requestFn(resp: *worker.Response, r: *worker.Request) void { + var cache = r.context.cache; + var counter: i32 = 0; + + var v = cache.getOrPut("counter") catch undefined; + + if (!v.found_existing) { + v.value_ptr.* = "0"; + } else { + var counterValue = v.value_ptr.*; + var num = std.fmt.parseInt(i32, counterValue, 10) catch undefined; + counter = num + 1; + var num_s = std.fmt.allocPrint(allocator, "{d}", .{ counter }) catch undefined; + _ = cache.put("counter", num_s) catch undefined; + } + + const s = + \\ + \\ + \\ + \\Wasm Workers Server - KV example + \\ + \\ + \\ + \\ + \\

Key / Value store in Zig

+ \\

Counter: {d}

+ \\

This page was generated by a Zig⚡️ file running in WebAssembly.

+ \\ + ; + + var body = std.fmt.allocPrint(allocator, s, .{ counter }) catch undefined; // add useragent + + _ = &resp.headers.append("x-generated-by", "wasm-workers-server"); + _ = &resp.writeAll(body); + } + + pub fn main() !void { + worker.ServeFunc(requestFn); + } + ``` + +5. Compile the project + + ```shell-session + zig build-exe src/main.zig \ + --name worker-kv \ + -mexec-model=reactor \ + -target wasm32-wasi \ + --mod worker::lib/worker.zig \ + --deps worker + ``` + + You can also use a build script to build the project with a simple `zig build`, please find some inspiration in our [zig examples](https://github.com/vmware-labs/wasm-workers-server/tree/main/examples/). + +1. Create a `worker-kv.toml` file with the following content. Note the name of the TOML file must match the name of the worker. In this case we have `worker-kv.wasm` and `worker-kv.toml` in the same folder: + + ```toml title="worker-kv.toml" + name = "workerkv" + version = "1" + + [data] + [data.kv] + namespace = "workerkv" + ``` + +1. Run your worker with `wws`. If you didn't download the `wws` server yet, check our [Getting Started](../get-started/quickstart.md) guide. + + ```shell-session + wws . + + ⚙️ Loading routes from: . + 🗺 Detected routes: + - http://127.0.0.1:8080/worker-kv + => worker-kv.wasm (name: default) + 🚀 Start serving requests at http://127.0.0.1:8080 + ``` + +1. Finally, open in your browser. + + +## Dynamic routes + +You can define [dynamic routes by adding route parameters to your worker files](../features/dynamic-routes.md) (like `[id].wasm`). To read them in Zig, follow these steps: + +1. Use the `worker.ParamsKey` context value to read in the passed in parameters: + + ```c title="main.zig" + const std = @import("std"); + const worker = @import("worker"); + + fn requestFn(resp: *worker.Response, r: *worker.Request) void { + var params = r.context.params; + + ... + } + ``` + +2. Then, you can read the values as follows: + + ```c title="main.zig" + const std = @import("std"); + const worker = @import("worker"); + + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + const allocator = arena.allocator(); + + fn requestFn(resp: *worker.Response, r: *worker.Request) void { + var params = r.context.params; + + var id: []const u8 = "the value is not available"; + + var v = params.get("id"); + + if (v) |val| { + id = val; + } + + const s = + \\Hey! The parameter is: {s} + ; + + var body = std.fmt.allocPrint(allocator, s, .{ id }) catch undefined; // add useragent + + _ = &resp.headers.append("x-generated-by", "wasm-workers-server"); + _ = &resp.writeAll(body); + } + + pub fn main() !void { + worker.ServeFunc(requestFn); + } + ``` + +3. Compile the project + + ```shell-session + zig build-exe src/main.zig \ + --name "[id]" \ + -mexec-model=reactor \ + -target wasm32-wasi \ + --mod worker::lib/worker.zig \ + --deps worker + ``` + + You can also use a build script to build the project with a simple `zig build`, please find some inspiration in our [zig examples](https://github.com/vmware-labs/wasm-workers-server/tree/main/examples/). + +4. Run your worker with `wws`. If you didn't download the `wws` server yet, check our [Getting Started](../get-started/quickstart.md) guide. + + ```shell-session + wws . + + ⚙️ Loading routes from: . + 🗺 Detected routes: + - http://127.0.0.1:8080/[id] + => worker-kv.wasm (name: default) + 🚀 Start serving requests at http://127.0.0.1:8080 + ``` + +5. Finally, open in your browser. + +## Other examples + +Find other examples in the [`/examples` directory](https://github.com/vmware-labs/wasm-workers-server/tree/main/examples/) of wasm-workers-server repository. + +## Contributors + +The Zig kit was originally authored by Christoph Voigt ([@voigt](https://github.com/voigt)). + +## Feature compatibility + +[Workers' features](../features/all.md) that are available in Zig: + +| [K/V Store](../features/key-value.md) | [Environment Variables](../features/environment-variables.md) | [Dynamic Routes](../features/dynamic-routes.md) | [Folders](../features/mount-folders.md) | [HTTP Requests](../features/http-requests.md) | +| --- | --- | --- | --- | --- | +| ✅ | ❌ | ✅ | ❓ | ❌ | diff --git a/docs/src/css/custom.css b/docs/src/css/custom.css index 08b1d584..2b079819 100644 --- a/docs/src/css/custom.css +++ b/docs/src/css/custom.css @@ -104,3 +104,7 @@ a.menu__link[href*="languages/rust"]::before { a.menu__link[href*="languages/go"]::before { background-image: url(/img/languages/go.svg); } + +a.menu__link[href*="languages/zig"]::before { + background-image: url(/img/languages/zig.svg); +} diff --git a/docs/static/img/languages/zig.svg b/docs/static/img/languages/zig.svg new file mode 100644 index 00000000..defec920 --- /dev/null +++ b/docs/static/img/languages/zig.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/examples/README.md b/examples/README.md index bfcfc0c1..5794dde8 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,3 +16,4 @@ Every example includes a `README.md` file with all the instructions. * [Python documentation](https://workers.wasmlabs.dev/docs/languages/python) * [Ruby documentation](https://workers.wasmlabs.dev/docs/languages/ruby) * [Go documentation](https://workers.wasmlabs.dev/docs/languages/go) +* [Zig documentation](https://workers.wasmlabs.dev/docs/languages/zig) diff --git a/examples/zig-basic/README.md b/examples/zig-basic/README.md new file mode 100644 index 00000000..c6f1305e --- /dev/null +++ b/examples/zig-basic/README.md @@ -0,0 +1,32 @@ +# Basic example + +Compile a Zig worker to WebAssembly and run it in Wasm Workers Server. + +## Prerequisites + +* Wasm Workers Server (wws): + + ```shell-session + curl -fsSL https://workers.wasmlabs.dev/install | bash + ``` + +* [Zig](https://ziglang.org/download/) `0.11.0` + +## Build + +All specific build confiugrations are in `build.zig` file. + +```shell-session +zig build +``` + +## Run + +```shell-session +wws ./zig-out/bin/ +``` + +## Resources + +* [Zig documentation](https://workers.wasmlabs.dev/docs/languages/zig) +* [Announcing Zig support for Wasm Workers Server](https://wasmlabs.dev/articles/Zig-support-on-wasm-workers-server/) diff --git a/examples/zig-basic/build.zig b/examples/zig-basic/build.zig new file mode 100644 index 00000000..a59f70d6 --- /dev/null +++ b/examples/zig-basic/build.zig @@ -0,0 +1,26 @@ +const std = @import("std"); + +const examples = [1][]const u8{ "basic" }; + +pub fn build(b: *std.Build) !void { + const target = try std.zig.CrossTarget.parse(.{ .arch_os_abi = "wasm32-wasi" }); + const optimize = b.standardOptimizeOption(.{}); + + const worker_module = b.createModule(.{ + .source_file = .{ .path = "../../kits/zig/worker/src/worker.zig" }, + }); + + inline for (examples) |example| { + const exe = b.addExecutable(.{ + .name = example, + .root_source_file = .{ .path = "src/" ++ example ++ ".zig" }, + .target = target, + .optimize = optimize, + }); + + exe.wasi_exec_model = .reactor; + exe.addModule("worker", worker_module); + + b.installArtifact(exe); + } +} diff --git a/examples/zig-basic/src/basic.zig b/examples/zig-basic/src/basic.zig new file mode 100644 index 00000000..77268184 --- /dev/null +++ b/examples/zig-basic/src/basic.zig @@ -0,0 +1,57 @@ +const std = @import("std"); +const worker = @import("worker"); + +var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); +const allocator = arena.allocator(); + +fn requestFn(resp: *worker.Response, r: *worker.Request) void { + + var payload: []const u8 = ""; + var reqBody = r.body; + + if (reqBody.len == 0) { + payload = "-"; + } else { + payload = reqBody; + } + + const s = + \\ + \\ + \\Wasm Workers Server + \\ + \\ + \\ + \\ + \\ + \\ + \\
+ \\

Hello from Wasm Workers Server 👋

+ \\
Replying to {s}
+        \\Method: {s}
+        \\User Agent: {s}
+        \\Payload: {s}
+ \\

+ \\This page was generated by a Zig⚡️ file running in WebAssembly. + \\

+ \\
+ \\ + ; + + var body = std.fmt.allocPrint(allocator, s, .{ r.url.path, r.method, "-", payload }) catch undefined; // add useragent + + _ = &resp.headers.append("x-generated-by", "wasm-workers-server"); + _ = &resp.writeAll(body); +} + +pub fn main() !void { + worker.ServeFunc(requestFn); +} diff --git a/examples/zig-kv/README.md b/examples/zig-kv/README.md new file mode 100644 index 00000000..2230a277 --- /dev/null +++ b/examples/zig-kv/README.md @@ -0,0 +1,33 @@ +# Key/Value example + +Compile a Zig worker to WebAssembly and run it in Wasm Workers Server. + +## Prerequisites + +* Wasm Workers Server (wws): + + ```shell-session + curl -fsSL https://workers.wasmlabs.dev/install | bash + ``` + +* [Zig](https://ziglang.org/download/) `0.11.0` + +## Build + +All specific build confiugrations are in `build.zig` file. + +```shell-session +zig build +``` + +## Run + +```shell-session +wws ./zig-out/bin/ +``` + +## Resources + +* [Key / Value store](https://workers.wasmlabs.dev/docs/features/key-value) +* [Zig documentation](https://workers.wasmlabs.dev/docs/languages/zig) +* [Announcing Zig support for Wasm Workers Server](https://wasmlabs.dev/articles/Zig-support-on-wasm-workers-server/) diff --git a/examples/zig-kv/build.zig b/examples/zig-kv/build.zig new file mode 100644 index 00000000..8f5b624a --- /dev/null +++ b/examples/zig-kv/build.zig @@ -0,0 +1,26 @@ +const std = @import("std"); + +const examples = [1][]const u8{ "worker-kv" }; + +pub fn build(b: *std.Build) !void { + const target = try std.zig.CrossTarget.parse(.{ .arch_os_abi = "wasm32-wasi" }); + const optimize = b.standardOptimizeOption(.{}); + + const worker_module = b.createModule(.{ + .source_file = .{ .path = "../../kits/zig/worker/src/worker.zig" }, + }); + + inline for (examples) |example| { + const exe = b.addExecutable(.{ + .name = example, + .root_source_file = .{ .path = "src/" ++ example ++ ".zig" }, + .target = target, + .optimize = optimize, + }); + + exe.wasi_exec_model = .reactor; + exe.addModule("worker", worker_module); + + b.installArtifact(exe); + } +} diff --git a/examples/zig-kv/src/worker-kv.zig b/examples/zig-kv/src/worker-kv.zig new file mode 100644 index 00000000..3f1bcd78 --- /dev/null +++ b/examples/zig-kv/src/worker-kv.zig @@ -0,0 +1,46 @@ +const std = @import("std"); +const worker = @import("worker"); + +var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); +const allocator = arena.allocator(); + +fn requestFn(resp: *worker.Response, r: *worker.Request) void { + var cache = r.context.cache; + var counter: i32 = 0; + + var v = cache.getOrPut("counter") catch undefined; + + if (!v.found_existing) { + v.value_ptr.* = "0"; + } else { + var counterValue = v.value_ptr.*; + var num = std.fmt.parseInt(i32, counterValue, 10) catch undefined; + counter = num + 1; + var num_s = std.fmt.allocPrint(allocator, "{d}", .{ counter }) catch undefined; + _ = cache.put("counter", num_s) catch undefined; + } + + const s = + \\ + \\ + \\ + \\Wasm Workers Server - KV example + \\ + \\ + \\ + \\ + \\

Key / Value store in Zig

+ \\

Counter: {d}

+ \\

This page was generated by a Zig⚡️ file running in WebAssembly.

+ \\ + ; + + var body = std.fmt.allocPrint(allocator, s, .{ counter }) catch undefined; // add useragent + + _ = &resp.headers.append("x-generated-by", "wasm-workers-server"); + _ = &resp.writeAll(body); +} + +pub fn main() !void { + worker.ServeFunc(requestFn); +} diff --git a/examples/zig-kv/zig-out/bin/worker-kv.toml b/examples/zig-kv/zig-out/bin/worker-kv.toml new file mode 100644 index 00000000..d1987525 --- /dev/null +++ b/examples/zig-kv/zig-out/bin/worker-kv.toml @@ -0,0 +1,6 @@ +name = "workerkv" +version = "1" + +[data] +[data.kv] +namespace = "workerkv" \ No newline at end of file diff --git a/examples/zig-params/README.md b/examples/zig-params/README.md new file mode 100644 index 00000000..220ee921 --- /dev/null +++ b/examples/zig-params/README.md @@ -0,0 +1,33 @@ +# Params example + +Compile a Zig worker to WebAssembly and run it in Wasm Workers Server. + +## Prerequisites + +* Wasm Workers Server (wws): + + ```shell-session + curl -fsSL https://workers.wasmlabs.dev/install | bash + ``` + +* [Zig](https://ziglang.org/download/) `0.11.0` + +## Build + +All specific build confiugrations are in `build.zig` file. + +```shell-session +zig build +``` + +## Run + +```shell-session +wws ./zig-out/bin/ +``` + +## Resources + +* [Dynamic routes](https://workers.wasmlabs.dev/docs/features/dynamic-routes) +* [Zig documentation](https://workers.wasmlabs.dev/docs/languages/zig) +* [Announcing Zig support for Wasm Workers Server](https://wasmlabs.dev/articles/Zig-support-on-wasm-workers-server/) diff --git a/examples/zig-params/build.zig b/examples/zig-params/build.zig new file mode 100644 index 00000000..657dc469 --- /dev/null +++ b/examples/zig-params/build.zig @@ -0,0 +1,26 @@ +const std = @import("std"); + +const examples = [1][]const u8{ "worker-params" }; + +pub fn build(b: *std.Build) !void { + const target = try std.zig.CrossTarget.parse(.{ .arch_os_abi = "wasm32-wasi" }); + const optimize = b.standardOptimizeOption(.{}); + + const worker_module = b.createModule(.{ + .source_file = .{ .path = "../../kits/zig/worker/src/worker.zig" }, + }); + + inline for (examples) |example| { + const exe = b.addExecutable(.{ + .name = example, + .root_source_file = .{ .path = "src/" ++ example ++ ".zig" }, + .target = target, + .optimize = optimize, + }); + + exe.wasi_exec_model = .reactor; + exe.addModule("worker", worker_module); + + b.installArtifact(exe); + } +} diff --git a/examples/zig-params/src/worker-params.zig b/examples/zig-params/src/worker-params.zig new file mode 100644 index 00000000..a571d28b --- /dev/null +++ b/examples/zig-params/src/worker-params.zig @@ -0,0 +1,30 @@ +const std = @import("std"); +const worker = @import("worker"); + +var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); +const allocator = arena.allocator(); + +fn requestFn(resp: *worker.Response, r: *worker.Request) void { + var params = r.context.params; + + var id: []const u8 = "the value is not available"; + + var v = params.get("id"); + + if (v) |val| { + id = val; + } + + const s = + \\Hey! The parameter is: {s} + ; + + var body = std.fmt.allocPrint(allocator, s, .{ id }) catch undefined; // add useragent + + _ = &resp.headers.append("x-generated-by", "wasm-workers-server"); + _ = &resp.writeAll(body); +} + +pub fn main() !void { + worker.ServeFunc(requestFn); +} diff --git a/kits/zig/worker/README.md b/kits/zig/worker/README.md new file mode 100644 index 00000000..403fd470 --- /dev/null +++ b/kits/zig/worker/README.md @@ -0,0 +1,49 @@ +# Zig kit + +This folder contains the Zig kit or SDK for Wasm Workers Server. Currently, it uses the regular STDIN / STDOUT approach to receive the request and provide the response. + +> *Note: this assumes Zig `0.11.0`* + +## Build + +To build all examples in ./examples + +```shell-session +$ zig build -Dtarget="wasm32-wasi" +``` + +To build a specific example: + +```shell-session +$ zig build-exe examples/.zig -target wasm32-wasi +``` + +## Testing + +At `./kits/zig/worker` execute: + +```shell-session +$ zig build -Dtarget="wasm32-wasi" +$ wws ./zig-out/bin/ +``` + +## sockaddr issue + +Using `http.Server.Response` was unsuccessful and lead to following error: + +``` +$ worker git:(144_-_add_support_for_zig) ✗ zig build -Dtarget="wasm32-wasi" +zig build-exe main Debug wasm32-wasi: error: the following command failed with 1 compilation errors: +/Users/c.voigt/.asdf/installs/zig/0.11.0/zig build-exe /Users/c.voigt/go/src/github.com/voigt/wasm-workers-server/kits/zig/worker/examples/main.zig --cache-dir /Users/c.voigt/go/src/github.com/voigt/wasm-workers-server/kits/zig/worker/zig-cache --global-cache-dir /Users/c.voigt/.cache/zig --name main -target wasm32-wasi -mcpu generic --mod worker::/Users/c.voigt/go/src/github.com/voigt/wasm-workers-server/kits/zig/worker/src/worker.zig --deps worker --listen=- +Build Summary: 6/9 steps succeeded; 1 failed (disable with --summary none) +install transitive failure +└─ install main transitive failure + └─ zig build-exe main Debug wasm32-wasi 1 errors +/Users/c.voigt/.asdf/installs/zig/0.11.0/lib/std/os.zig:182:28: error: root struct of file 'os.wasi' has no member named 'sockaddr' +pub const sockaddr = system.sockaddr; + ~~~~~~^~~~~~~~~ +referenced by: + Address: /Users/c.voigt/.asdf/installs/zig/0.11.0/lib/std/net.zig:18:12 + Address: /Users/c.voigt/.asdf/installs/zig/0.11.0/lib/std/net.zig:17:28 + remaining reference traces hidden; use '-freference-trace' to see all reference traces +``` \ No newline at end of file diff --git a/kits/zig/worker/src/worker.zig b/kits/zig/worker/src/worker.zig new file mode 100644 index 00000000..197e1be3 --- /dev/null +++ b/kits/zig/worker/src/worker.zig @@ -0,0 +1,261 @@ +const std = @import("std"); +const io = std.io; +const http = std.http; + +var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); +const allocator = arena.allocator(); + +pub var cache = std.StringHashMap([]const u8).init(allocator); +pub var params = std.StringHashMap([]const u8).init(allocator); + +pub const Request = struct { + url: std.Uri, + method: []const u8, // TODO: change to http.Method enum + headers: http.Headers, + body: []const u8, + context: Context, +}; + +pub const Response = struct { + body: []const u8, + headers: http.Headers, + request: Request, + + pub fn writeAll(res: *Response, data: []const u8) !u32 { + res.body = data; + return res.body.len; + } +}; + +// Note: as Zig does not support multiple return types, we use this struct +// to wrap both the request and the output to keep code a bit more clean +const RequestAndOutput = struct { + request: Request, + output: Output, +}; + +pub const Input = struct { + url: []const u8, + method: []const u8, + headers: std.StringArrayHashMap([]const u8), + body: []const u8, +}; + +pub const Output = struct { + data: []const u8, + headers: std.StringArrayHashMap([]const u8), + status: u16, + base64: bool, + + httpHeader: http.Headers, + + const Self = @This(); + + pub fn init() Self { + return .{ + .data = "", + .headers = std.StringArrayHashMap([]const u8).init(allocator), + .status = 0, + .base64 = false, + .httpHeader = http.Headers.init(allocator), + }; + } + + pub fn header(self: *Self) http.Headers { + if (self.httpHeader == undefined) { + self.httpHeader = http.Headers.init(allocator); + } + + return self.httpHeader; + } + + pub fn setStatus(self: *Self, statusCode: u16) void { + self.status = statusCode; + } + + pub fn write(self: *Self, data: []const u8) !u32 { + if (std.unicode.utf8ValidateSlice(data)) { + self.data = data; + } else { + self.base64 = true; + self.data = base64Encode(data); + } + + if (self.status == 0) { + self.setStatus(200); + } + + for (self.httpHeader.list.items) |item| { + try self.headers.put(item.name, item.value); + } + + // prepare writer for json + var out_buf: [1024]u8 = undefined; + var slice_stream = std.io.fixedBufferStream(&out_buf); + const out = slice_stream.writer(); + var w = std.json.writeStream(out, .{ .whitespace = .minified }); + + slice_stream.reset(); + try w.beginObject(); + + try w.objectField("data"); + try w.write(self.data); + + try w.objectField("status"); + try w.write(self.status); + + try w.objectField("base64"); + try w.write(self.base64); + + try w.objectField("headers"); + try w.write(try getHeadersJsonObject(self.headers)); + + try w.objectField("kv"); + try w.write(try getCacheJsonObject(cache)); + + try w.endObject(); + const result = slice_stream.getWritten(); + + // std.debug.print("\noutput json: {s}\n\n", .{ result }); + + const stdout = std.io.getStdOut().writer(); + try stdout.print("{s}", .{ result }); + + return self.data.len; + } +}; + +fn base64Encode(data: []const u8) []const u8 { + // This initializing Base64Encoder throws weird error if not wrapped in function (maybe Zig bug?) + var enc = std.base64.Base64Encoder.init(std.base64.standard_alphabet_chars, '='); + var data_len = enc.calcSize(data.len); + var buf: [128]u8 = undefined; + return enc.encode(buf[0..data_len], data); +} + +fn getHeadersJsonObject(s: std.StringArrayHashMap([]const u8)) !std.json.Value { + var value = std.json.Value{ .object = std.json.ObjectMap.init(allocator) }; + + var i = s.iterator(); + while (i.next()) |kv| { + try value.object.put(kv.key_ptr.*, std.json.Value{ .string = kv.value_ptr.*}); + } + + return value; +} + +fn getCacheJsonObject(s: std.StringHashMap([]const u8)) !std.json.Value { + var value = std.json.Value{ .object = std.json.ObjectMap.init(allocator) }; + + var i = s.iterator(); + while (i.next()) |entry| { + try value.object.put(entry.key_ptr.*, std.json.Value{ .string = entry.value_ptr.*}); + } + + return value; +} + +pub fn readInput() !Input { + const in = std.io.getStdIn(); + var buf = std.io.bufferedReader(in.reader()); + var r = buf.reader(); + + var msg = try r.readAllAlloc(allocator, std.math.maxInt(u32)); + return getInput(msg); +} + +fn getInput(s: []const u8) !Input { + var parsed = try std.json.parseFromSlice(std.json.Value, allocator, s, .{}); + defer parsed.deinit(); + + var input = Input{ + .url = parsed.value.object.get("url").?.string, + .method = parsed.value.object.get("method").?.string, + .body = parsed.value.object.get("body").?.string, + .headers = std.StringArrayHashMap([]const u8).init(allocator), + }; + + var headers_map = parsed.value.object.get("headers").?.object; + var headersIterator = headers_map.iterator(); + while (headersIterator.next()) |entry| { + try input.headers.put(entry.key_ptr.*, entry.value_ptr.*.string); + } + + var kv = parsed.value.object.get("kv").?.object; + var kvIterator = kv.iterator(); + while (kvIterator.next()) |entry| { + try cache.put(entry.key_ptr.*, entry.value_ptr.*.string); + } + + var p = parsed.value.object.get("params").?.object; + var paramsIterator = p.iterator(); + while (paramsIterator.next()) |entry| { + try params.put(entry.key_ptr.*, entry.value_ptr.*.string); + } + + return input; +} + +pub fn createRequest(in: *Input) !Request { + var req = Request{ + .url = try std.Uri.parseWithoutScheme(in.url), + .method = in.method, + .headers = http.Headers.init(allocator), + .body = in.body, + .context = Context.init(), + }; + + var i = in.headers.iterator(); + while (i.next()) |kv| { + try req.headers.append(kv.key_ptr.*, kv.value_ptr.*); + } + + return req; +} + +pub fn getWriterRequest() !RequestAndOutput { + var in = readInput() catch |err| { + std.debug.print("error reading input: {!}\n", .{err}); + return std.os.exit(1); + }; + + var req = createRequest(&in) catch |err| { + std.debug.print("error creating request : {!}\n", .{err}); + return std.os.exit(1); + }; + + var output = Output.init(); + + return RequestAndOutput{ + .request = req, + .output = output, + }; +} + +pub const Context = struct { + cache: *std.StringHashMap([]const u8), + params: *std.StringHashMap([]const u8), + + pub fn init() Context { + return .{ + .cache = &cache, + .params = ¶ms, + }; + } +}; + +pub fn ServeFunc(requestFn: *const fn (*Response, *Request) void) void { + var r = try getWriterRequest(); + var request = r.request; + var output = r.output; + + var response = Response{ .body = "", .headers = http.Headers.init(allocator), .request = request, }; + + requestFn(&response, &request); + + output.httpHeader = response.headers; + + _ = output.write(response.body) catch |err| { + std.debug.print("error writing data: {!} \n", .{ err }); + }; +}