diff --git a/kits/zig/worker/.gitignore b/kits/zig/worker/.gitignore new file mode 100644 index 00000000..4a0641ed --- /dev/null +++ b/kits/zig/worker/.gitignore @@ -0,0 +1,2 @@ +zig-cache/ +zig-out/ \ 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..f20fd8a5 --- /dev/null +++ b/kits/zig/worker/README.md @@ -0,0 +1,11 @@ +# 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. + +## build + +To build example in ./examples + +``` +$ zig build -Dtarget="wasm32-wasi" +``` \ 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..3bfedee8 --- /dev/null +++ b/kits/zig/worker/build.zig @@ -0,0 +1,84 @@ +const std = @import("std"); + +const package_name = "worker"; +const package_path = "src/worker.zig"; + +const examples = [2][]const u8{ "main", "basic" }; + +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..4590b2ef --- /dev/null +++ b/kits/zig/worker/examples/basic.zig @@ -0,0 +1,7 @@ +const std = @import("std"); +const w = @import("worker"); + +pub fn main() !void { + var word = w.GetWord(); + std.debug.print("Hello from {s}!\n", .{ word }); +} \ No newline at end of file diff --git a/kits/zig/worker/examples/main.zig b/kits/zig/worker/examples/main.zig new file mode 100644 index 00000000..4590b2ef --- /dev/null +++ b/kits/zig/worker/examples/main.zig @@ -0,0 +1,7 @@ +const std = @import("std"); +const w = @import("worker"); + +pub fn main() !void { + var word = w.GetWord(); + std.debug.print("Hello from {s}!\n", .{ word }); +} \ 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..841651f5 --- /dev/null +++ b/kits/zig/worker/src/worker.zig @@ -0,0 +1,279 @@ +const std = @import("std"); +const io = std.io; +const http = std.http; + +pub fn GetWord() []const u8 { + return "lib"; +} + +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.StringHashMap([]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)) { + 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; + } + + // self.headers.init(allocator); // can we init thissomewhere else? + for (self.httpHeader.list.items) |item| { + std.debug.print("Fields: {!}\n", .{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 = .indent_2 }); + + 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 self.headers.put("hello", "world"); + 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(); + + // https://zig.news/kristoff/where-is-print-in-zig-57e9 + // std.debug.print("{s}", .{out}); + 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("json: {s}\n", .{m}); + return getInput(m); + } + } else |err| { + std.debug.print("error parsing json: {!}\n", .{err}); + } + + return Input{}; +} + +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.StringHashMap([]const u8).init(allocator), + }; + + var headers_map = parsed.value.object.get("headers").?.object; + + // can we maybe use an iterator here? + 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 = client.Request{ + .uri = in.url, + .method = in.method, + .headers = in.headers, + }; + + // req = req.WithContext(context.WithValue(req.Context(), CacheKey, cache)); + // req = req.WithContext(context.WithValue(req.Context(), ParamsKey, params)); + + return req; +} + +pub fn getWriterRequest() !@This() { + var in = readInput(allocator) catch |err| { + std.debug.print("error reading input: {s}\n", .{err}); + return std.os.exit(1); + }; + + var req = createRequest(&in, allocator) catch |err| { + std.debug.print("error creating request : {s}\n", .{err}); + return std.os.exit(1); + }; + + var w = Output{ + .headers = std.StringHashMap([]const u8).init(allocator), + }; + + return .{ + .req = req, + .w = w, + }; +} + +// works +// pub fn ServeFunc(f: anytype) void { +// f(); +// } + + +// works as function budy, must be comptime-known +// pub fn ServeFunc(comptime cool: fn () void) void { +// cool(); +// } + +// works as function pointer +pub fn ServeFunc(cool: *const fn (*http.Server.Response) void) void { + var r = try getWriterRequest(allocator); + cool(r.req); +} + + +// 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) || ((data[i + 1] & 0xC0) != 0x80)) { + return false; + } + i += 2; + } else if (byte < 0xF0) { + // 3-byte sequence + if ((i + 2 >= data.len) || ((data[i + 1] & 0xC0) != 0x80) || ((data[i + 2] & 0xC0) != 0x80)) { + return false; + } + i += 3; + } else if (byte < 0xF5) { + // 4-byte sequence + if ((i + 3 >= data.len) || ((data[i + 1] & 0xC0) != 0x80) || ((data[i + 2] & 0xC0) != 0x80) || ((data[i + 3] & 0xC0) != 0x80)) { + return false; + } + i += 4; + } else { + // Invalid UTF-8 byte + return false; + } + } + return true; +}