From d01b8c186036b80c58ee5f8d7a46129faa894caf Mon Sep 17 00:00:00 2001 From: Christoph Voigt Date: Fri, 11 Aug 2023 10:05:32 +0200 Subject: [PATCH] feat: add support for zig (#144) --- README.md | 2 +- kits/zig/worker/.gitignore | 4 + kits/zig/worker/README.md | 49 +++++ kits/zig/worker/build.zig | 85 ++++++++ kits/zig/worker/examples/basic.zig | 23 +++ kits/zig/worker/examples/main.zig | 38 ++++ kits/zig/worker/src/worker.zig | 319 +++++++++++++++++++++++++++++ 7 files changed, 519 insertions(+), 1 deletion(-) create mode 100644 kits/zig/worker/.gitignore create mode 100644 kits/zig/worker/README.md create mode 100644 kits/zig/worker/build.zig create mode 100644 kits/zig/worker/examples/basic.zig create mode 100644 kits/zig/worker/examples/main.zig create mode 100644 kits/zig/worker/src/worker.zig diff --git a/README.md b/README.md index c71c3540..2aacf6e9 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 | ✅ | Yes | [#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/kits/zig/worker/.gitignore b/kits/zig/worker/.gitignore new file mode 100644 index 00000000..8efc5ecd --- /dev/null +++ b/kits/zig/worker/.gitignore @@ -0,0 +1,4 @@ +zig-cache/ +zig-out/ +examples/json.zig +examples/output.zig \ No newline at end of file diff --git a/kits/zig/worker/README.md b/kits/zig/worker/README.md new file mode 100644 index 00000000..e673fb34 --- /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 example in ./examples + +```bash +$ zig build -Dtarget="wasm32-wasi" +``` + +To build a specific example + +```bash +$ zig build-exe examples/.zig -target wasm32-wasi +``` + +## testing + +from `./kits/zig/worker` execute + +```bash +$ 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/build.zig b/kits/zig/worker/build.zig new file mode 100644 index 00000000..40062d2f --- /dev/null +++ b/kits/zig/worker/build.zig @@ -0,0 +1,85 @@ +const std = @import("std"); + +const package_name = "worker"; +const package_path = "src/worker.zig"; + +const examples = [2][]const u8{ "main", "basic" }; +// const examples = [4][]const u8{ "main", "basic", "json", "output" }; + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{ }); + const optimize = b.standardOptimizeOption(.{}); + + const worker_module = b.createModule(.{ + .source_file = .{ .path = package_path }, + }); + + inline for (examples) |example| { + const exe = b.addExecutable(.{ + .name = example, + .root_source_file = .{ .path = "examples/" ++ example ++ ".zig" }, + .target = target, + .optimize = optimize, + }); + + exe.addModule("worker", worker_module); + + b.installArtifact(exe); + } + + // const exe = b.addExecutable(.{ + // .name = "worker", + // // 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 = "examples/main.zig" }, + // .target = target, + // .optimize = optimize, + // }); + + // exe.addModule("worker", worker_module); + // // exe.install(); + + // // This declares intent for the executable to be installed into the + // // standard location when the user invokes the "install" step (the default + // // step when running `zig build`). + // b.installArtifact(exe); + + // // This *creates* a Run step in the build graph, to be executed when another + // // step is evaluated that depends on it. The next line below will establish + // // such a dependency. + // const run_cmd = b.addRunArtifact(exe); + + // // By making the run step depend on the install step, it will be run from the + // // installation directory rather than directly from within the cache directory. + // // This is not necessary, however, if the application depends on other installed + // // files, this ensures they will be present and in the expected location. + // run_cmd.step.dependOn(b.getInstallStep()); + + // // This allows the user to pass arguments to the application in the build + // // command itself, like this: `zig build run -- arg1 arg2 etc` + // if (b.args) |args| { + // run_cmd.addArgs(args); + // } + + // // This creates a build step. It will be visible in the `zig build --help` menu, + // // and can be selected like this: `zig build run` + // // This will evaluate the `run` step rather than the default, which is "install". + // const run_step = b.step("run", "Run the app"); + // run_step.dependOn(&run_cmd.step); + + // // Creates a step for unit testing. This only builds the test executable + // // but does not run it. + // const unit_tests = b.addTest(.{ + // .root_source_file = .{ .path = "src/main.zig" }, + // .target = target, + // .optimize = optimize, + // }); + + // const run_unit_tests = b.addRunArtifact(unit_tests); + + // // Similar to creating the run step earlier, this exposes a `test` step to + // // the `zig build --help` menu, providing a way for the user to request + // // running the unit tests. + // const test_step = b.step("test", "Run unit tests"); + // test_step.dependOn(&run_unit_tests.step); +} diff --git a/kits/zig/worker/examples/basic.zig b/kits/zig/worker/examples/basic.zig new file mode 100644 index 00000000..b3d0955e --- /dev/null +++ b/kits/zig/worker/examples/basic.zig @@ -0,0 +1,23 @@ +const std = @import("std"); +const io = std.io; +const http = std.http; +const worker = @import("worker"); + +var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); +const allocator = arena.allocator(); + +// Not working with *http.Server.Response +// fn cool(resp: *http.Server.Response, r: *http.Client.Request) void { +fn requestFn(resp: *worker.Response, r: *http.Client.Request) void { + _ = r; + std.debug.print("Hello from function\n", .{ }); + + _ = &resp.httpHeader.append("content-type", "text/plain"); + _ = &resp.httpHeader.append("x-generated-by", "wasm-workers-server"); + + _ = &resp.writeAll("Zig World!"); +} + +pub fn main() !void { + worker.ServeFunc(requestFn); +} diff --git a/kits/zig/worker/examples/main.zig b/kits/zig/worker/examples/main.zig new file mode 100644 index 00000000..f5bdd342 --- /dev/null +++ b/kits/zig/worker/examples/main.zig @@ -0,0 +1,38 @@ +const std = @import("std"); +const io = std.io; +const http = std.http; +const worker = @import("worker"); + +var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); +const allocator = arena.allocator(); + +// Not working with *http.Server.Response +// fn cool(resp: *http.Server.Response, r: *http.Client.Request) void { +fn requestFn(resp: *worker.Response, r: *http.Client.Request) void { + _ = r; + 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.reader().readAllAlloc(allocator, 8192) catch unreachable; + // defer allocator.free(reqBody); + + // if (reqBody.len == 0) { + // payload = "-"; + // } else { + // payload = reqBody; + // } + // std.debug.print("+++ finished payload \n", .{ }); + + // var resp = r.response; + + _ = &resp.httpHeader.append("content-type", "text/plain"); + _ = &resp.httpHeader.append("x-generated-by", "wasm-workers-server"); + + _ = &resp.writeAll("Zig World!"); +} + +pub fn main() !void { + worker.ServeFunc(requestFn); +} diff --git a/kits/zig/worker/src/worker.zig b/kits/zig/worker/src/worker.zig new file mode 100644 index 00000000..a484528d --- /dev/null +++ b/kits/zig/worker/src/worker.zig @@ -0,0 +1,319 @@ +const std = @import("std"); +const io = std.io; +const http = std.http; + +// This is from ChatGPT - I have no clue whether this works, nor whats going on here :D +fn isValidUtf8(data: []const u8) bool { + var i: usize = 0; + while (i < data.len) { + const byte: u8 = data[i]; + if (byte < 0x80) { + // ASCII character + i += 1; + } else if (byte < 0xC2) { + // Invalid continuation byte + return false; + } else if (byte < 0xE0) { + // 2-byte sequence + if ((i + 1 >= data.len) or ((data[i + 1] & 0xC0) != 0x80)) { + return false; + } + i += 2; + } else if (byte < 0xF0) { + // 3-byte sequence + if ((i + 2 >= data.len) or ((data[i + 1] & 0xC0) != 0x80) or ((data[i + 2] & 0xC0) != 0x80)) { + return false; + } + i += 3; + } else if (byte < 0xF5) { + // 4-byte sequence + if ((i + 3 >= data.len) or ((data[i + 1] & 0xC0) != 0x80) or ((data[i + 2] & 0xC0) != 0x80) or ((data[i + 3] & 0xC0) != 0x80)) { + return false; + } + i += 4; + } else { + // Invalid UTF-8 byte + return false; + } + } + return true; +} + +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 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 writeHeader(self: *Self, statusCode: u16) void { + self.status = statusCode; + } + + pub fn write(self: *Self, data: []const u8) !u32 { + + if (isValidUtf8(data)) { + self.data = data; + } else { + self.base64 = true; + // is this correct? + const enc = std.base64.Base64Encoder.init(std.base64.url_safe_alphabet_chars, '='); + var dest: []u8 = undefined; + self.data = std.base64.Base64Encoder.encode(&enc, dest, data); + } + + if (self.status == 0) { + self.status = 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 cache.put("hello", "world"); + try w.objectField("kv"); + try w.write(try getCacheJsonObject(cache)); + + try w.endObject(); + const result = slice_stream.getWritten(); + + std.debug.print("output json: {s}\n", .{ result }); + + // I assume this works, as no stdout seems to be logged by wws, only stderr + // https://zig.news/kristoff/where-is-print-in-zig-57e9 + const stdout = std.io.getStdOut().writer(); + try stdout.print("{s}", .{result}); + + return self.data.len; + } +}; + +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()) |kv| { + try value.object.put(kv.key_ptr.*, std.json.Value{ .string = kv.value_ptr.*}); + } + + return value; +} + +pub fn readInput() !Input { + // https://www.openmymind.net/Reading-A-Json-Config-In-Zig/ + const in = std.io.getStdIn(); + var buf = std.io.bufferedReader(in.reader()); + var r = buf.reader(); + + var msg_buf: [4096]u8 = undefined; + + // delimiter "\n" might not be adequate? + if (r.readUntilDelimiterOrEof(&msg_buf, '\n')) |msg| { + if (msg) | m | { + std.debug.print("raw input json: {s}\n", .{m}); + return getInput(m); + } + } else |err| { + std.debug.print("error parsing json: {!}\n", .{err}); + } + + // TODO: proper return value + return undefined; +} + +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; + + // can we maybe use an iterator here? + // var i = headers_map.iterator(); + // while (i.next()) |kv| { + // try input.headers.put(kv.key_ptr.*.string, kv.value_ptr.*.string); + // } + + // std.debug.print("headers1: {!}", .{ input.headers }); + + for (headers_map.keys()) |key| { + var v = try headers_map.getOrPut(key); + if (v.found_existing) { + var value = v.value_ptr.*.string; + // std.debug.print("headers key: {s}, value: {s}\n", .{key, value}); + try input.headers.put(key, value); + } + } + + return input; +} + +pub fn createRequest(in: *Input) !http.Client.Request { + + // Create an HTTP client. + var client = http.Client{ .allocator = allocator }; + // Release all associated resources with the client. + defer client.deinit(); + // Parse the URI. + + var req = http.Client.Request{ + .client = &client, + .uri = try std.Uri.parseWithoutScheme(in.url), + .method = http.Method.GET, + .arena = arena, + .connection = null, + .headers = http.Headers.init(allocator), + .redirects_left = 0, + .handle_redirects = false, + .response = undefined, + }; + + // is it even necessary to copy headers from Input to Request struct? + var i = in.headers.iterator(); + while (i.next()) |kv| { + try req.headers.append(kv.key_ptr.*, kv.value_ptr.*); + } + + // req = req.WithContext(context.WithValue(req.Context(), CacheKey, cache)); + // req = req.WithContext(context.WithValue(req.Context(), ParamsKey, params)); + + return req; +} + +const RequestAndOutput = struct { + req: http.Client.Request, + output: Output, +}; + +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{ + .req = req, + .output = output, + }; +} + +pub const Response = struct { + data: []const u8, + httpHeader: http.Headers, + + pub fn writeAll(self: *Response, data: []const u8) !u32 { + self.data = data; + return self.data.len; + } +}; + +// Function parameter as function pointer +pub fn ServeFunc(requestFn: *const fn (*Response, *http.Client.Request) void) void { + var r = try getWriterRequest(); + var request = r.req; + var output = r.output; + + var response = Response{ .data = "", .httpHeader = http.Headers.init(allocator) }; + requestFn(&response, &request); + + // TODO: do we need request.headers in response? + // output.httpHeader = request.headers; + output.httpHeader = response.httpHeader; + + _ = output.write(response.data) catch |err| { + std.debug.print("error writing data: {!} \n", .{ err }); + }; + + std.debug.print("Done.\n", .{ }); +} + + +// Alternative ways for function parameter +// pub fn ServeFunc(requestFn: anytype) void { +// requestFn(); +// } + +// works as function budy, must be comptime-known +// pub fn ServeFunc(comptime requestFn: fn () void) void { +// requestFn(); +// }