diff --git a/src/flexilib-interface.zig b/src/flexilib-interface.zig new file mode 100644 index 0000000..43160e9 --- /dev/null +++ b/src/flexilib-interface.zig @@ -0,0 +1,140 @@ +const std = @import("std"); + +// C interfaces between main and libraries +pub const Header = extern struct { + name_ptr: [*]u8, + name_len: usize, + + value_ptr: [*]u8, + value_len: usize, +}; +pub const Response = extern struct { + ptr: [*]u8, + len: usize, + + headers: [*]Header, + headers_len: usize, +}; + +pub const Request = extern struct { + method: [*:0]u8, + method_len: usize, + + content: [*]u8, + content_len: usize, + + headers: [*]Header, + headers_len: usize, +}; + +// If the library is Zig, we can use these helpers +threadlocal var allocator: ?*std.mem.Allocator = null; + +const log = std.log.scoped(.interface); + +pub const ZigRequest = struct { + method: [:0]u8, + content: []u8, + headers: []Header, +}; + +pub const ZigHeader = struct { + name: []u8, + value: []u8, +}; + +pub const ZigResponse = struct { + body: *std.ArrayList(u8), + headers: *std.StringHashMap([]const u8), +}; + +pub const ZigRequestHandler = *const fn (std.mem.Allocator, ZigRequest, ZigResponse) anyerror!void; + +/// This function is optional and can be exported by zig libraries for +/// initialization. If exported, it will be called once in the beginning of +/// a request and will be provided a pointer to std.mem.Allocator, which is +/// useful for reusing the parent allocator. If you're planning on using +/// the handleRequest helper below, you must use zigInit or otherwise +/// set the interface allocator in your own version of zigInit +pub fn zigInit(parent_allocator: *anyopaque) callconv(.C) void { + allocator = @ptrCast(@alignCast(parent_allocator)); +} + +pub fn toZigHeader(header: Header) ZigHeader { + return .{ + .name = header.name_ptr[0..header.name_len], + .value = header.value_ptr[0..header.value_len], + }; +} + +/// Converts a StringHashMap to the structure necessary for passing through the +/// C boundary. This will be called automatically for you via the handleRequest function +/// and is also used by the main processing loop to coerce request headers +fn toHeaders(alloc: std.mem.Allocator, headers: std.StringHashMap([]const u8)) ![*]Header { + var header_array = try std.ArrayList(Header).initCapacity(alloc, headers.count()); + var iterator = headers.iterator(); + while (iterator.next()) |kv| { + header_array.appendAssumeCapacity(.{ + .name_ptr = @constCast(kv.key_ptr.*).ptr, + .name_len = kv.key_ptr.*.len, + + .value_ptr = @constCast(kv.value_ptr.*).ptr, + .value_len = kv.value_ptr.*.len, + }); + } + return header_array.items.ptr; +} + +/// handles a request, implementing the C interface to communicate between the +/// main program and a zig library. Most importantly, it will catch/report +/// errors appropriately and allow zig code to use standard Zig error semantics +pub fn handleRequest(request: *Request, zigRequestHandler: ZigRequestHandler) ?*Response { + // TODO: implement another library in C or Rust or something to show + // that anything using a C ABI can be successful + var alloc = if (allocator) |a| a.* else { + log.err("zigInit not called prior to handle_request. This is a coding error", .{}); + return null; + }; + + // setup response body + var response = std.ArrayList(u8).init(alloc); + + // setup headers + var headers = std.StringHashMap([]const u8).init(alloc); + zigRequestHandler( + alloc, + .{ + .method = request.method[0..request.method_len :0], + .content = request.content[0..request.content_len], + .headers = request.headers[0..request.headers_len], + }, + .{ + .body = &response, + .headers = &headers, + }, + ) catch |e| { + log.err("Unexpected error processing request: {any}", .{e}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + return null; + }; + + // Marshall data back for handling by server + + var rc = alloc.create(Response) catch { + log.err("Could not allocate memory for response object. This may be fatal", .{}); + return null; + }; + rc.ptr = response.items.ptr; + rc.len = response.items.len; + rc.headers = toHeaders(alloc, headers) catch |e| { + log.err("Unexpected error processing request: {any}", .{e}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + return null; + }; + rc.headers_len = headers.count(); + return rc; +} diff --git a/src/flexilib.zig b/src/flexilib.zig new file mode 100644 index 0000000..39215f3 --- /dev/null +++ b/src/flexilib.zig @@ -0,0 +1,107 @@ +const std = @import("std"); +const interface = @import("flexilib-interface.zig"); // TODO: pull in flexilib directly +const testing = std.testing; + +const log = std.log.scoped(.@"main-lib"); + +const client_handler = @import("flexilib_handler"); + +// The main program will look for exports during the request lifecycle: +// zigInit (optional): called at the beginning of a request, includes pointer to an allocator +// handle_request: called with request data, expects response data +// request_deinit (optional): called at the end of a request to allow resource cleanup +// +// Setup for these is aided by the interface library as shown below + +// zigInit is an optional export called at the beginning of a request. It will +// be passed an allocator (which...shh...is an arena allocator). Since the +// interface library provides a request handler that requires a built-in allocator, +// if you are using the interface's handleRequest function as shown above, +// you will need to also include this export. To customize, just do something +// like this: +// +// export fn zigInit(parent_allocator: *anyopaque) callconv(.C) void { +// // your code here, just include the next line +// interface.zigInit(parent_allocator); +// } +// +comptime { + @export(interface.zigInit, .{ .name = "zigInit", .linkage = .Strong }); +} + +/// handle_request will be called on a single request, but due to the preservation +/// of restrictions imposed by the calling interface, it should generally be more +/// useful to call into the interface library to let it do the conversion work +/// on your behalf +export fn handle_request(request: *interface.Request) callconv(.C) ?*interface.Response { + // The interface library provides a handleRequest function that will handle + // marshalling data back and forth from the C format used for the interface + // to a more Zig friendly format. It also allows usage of zig errors. To + // use, pass in the request and the zig function used to handle the request + // (here called "handleRequest"). The function signature must be: + // + // fn (std.mem.Allocator, interface.ZigRequest, interface.ZigResponse) !void + // + return interface.handleRequest(request, handleRequest); +} + +// request_deinit is an optional export and will be called a the end of the +// request. Useful for deallocating memory. Since this is zig code and the +// allocator used is an arena allocator, all allocated memory will be automatically +// cleaned up by the main program at the end of a request +// +// export fn request_deinit() void { +// } + +// ************************************************************************ +// Boilerplate ^^, Custom code vv +// ************************************************************************ +// +// handleRequest function here is the last line of boilerplate and the +// entry to a request +fn handleRequest(allocator: std.mem.Allocator, request: interface.ZigRequest, response: interface.ZigResponse) !void { + // setup + var response_writer = response.body.writer(); + try response_writer.writeAll(try client_handler.handler(allocator, request.content, .{})); + // real work + for (request.headers) |h| { + const header = interface.toZigHeader(h); + // std.debug.print("\n{s}: {s}\n", .{ header.name, header.value }); + if (std.ascii.eqlIgnoreCase(header.name, "host") and std.mem.startsWith(u8, header.value, "iam")) { + try response_writer.print("iam response", .{}); + return; + } + if (std.ascii.eqlIgnoreCase(header.name, "x-slow")) { + std.time.sleep(std.time.ns_per_ms * (std.fmt.parseInt(usize, header.value, 10) catch 1000)); + try response_writer.print("i am slow\n\n", .{}); + return; + } + } + try response.headers.put("X-custom-foo", "bar"); + log.info("handlerequest header count {d}", .{response.headers.count()}); +} +// Need to figure out how tests would work +test "handle_request" { + var arena = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena.deinit(); + var aa = arena.allocator(); + interface.zigInit(&aa); + var headers: []interface.Header = @constCast(&[_]interface.Header{.{ + .name_ptr = @ptrCast(@constCast("GET".ptr)), + .name_len = 3, + .value_ptr = @ptrCast(@constCast("GET".ptr)), + .value_len = 3, + }}); + var req = interface.Request{ + .method = @ptrCast(@constCast("GET".ptr)), + .method_len = 3, + .content = @ptrCast(@constCast("GET".ptr)), + .content_len = 3, + .headers = headers.ptr, + .headers_len = 1, + }; + const response = handle_request(&req).?; + try testing.expectEqualStrings(" 1", response.ptr[0..response.len]); + try testing.expectEqualStrings("X-custom-foo", response.headers[0].name_ptr[0..response.headers[0].name_len]); + try testing.expectEqualStrings("bar", response.headers[0].value_ptr[0..response.headers[0].value_len]); +} diff --git a/src/flexilib_build.zig b/src/flexilib_build.zig new file mode 100644 index 0000000..60fb56f --- /dev/null +++ b/src/flexilib_build.zig @@ -0,0 +1,47 @@ +const std = @import("std"); +const builtin = @import("builtin"); + +/// flexilib will create a dynamic library for use with flexilib. +/// Flexilib will need to get the exe compiled as a library +/// For flexilib, we will need the main file to have a pub fn named +/// "handler". If it is not called that, a pub const handler = ... can be +/// used instead +pub fn configureBuild(b: *std.build.Builder, exe: *std.Build.Step.Compile, build_root_src: []const u8) !void { + const package_step = b.step("flexilib", "Create a flexilib dynamic library"); + + // const exe = b.addExecutable(.{ + // .name = "universal-lambda-example", + // // 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 = "src/main.zig" }, + // .target = target, + // .optimize = optimize, + // }); + // + const lib = b.addSharedLibrary(.{ + .name = exe.name, + .root_source_file = .{ .path = b.pathJoin(&[_][]const u8{ build_root_src, "flexilib.zig" }) }, + .target = exe.target, + .optimize = exe.optimize, + }); + + // We will not free this, as the rest of the build system will use it. + // This should be ok because our allocator is, I believe, an arena + var module_dependencies = try b.allocator.alloc(std.Build.ModuleDependency, exe.modules.count()); + var iterator = exe.modules.iterator(); + + var i: usize = 0; + while (iterator.next()) |entry| : (i += 1) { + module_dependencies[i] = .{ + .name = entry.key_ptr.*, + .module = entry.value_ptr.*, + }; + lib.addModule(entry.key_ptr.*, entry.value_ptr.*); + } + lib.addAnonymousModule("flexilib_handler", .{ + // Source file can be anywhere on disk, does not need to be a subdirectory + .source_file = exe.root_src.?, + .dependencies = module_dependencies, + }); + package_step.dependOn(&b.addInstallArtifact(lib, .{}).step); +} diff --git a/src/lambdabuild.zig b/src/lambda_build.zig similarity index 100% rename from src/lambdabuild.zig rename to src/lambda_build.zig diff --git a/src/universal_lambda_build.zig b/src/universal_lambda_build.zig index 1ac3eaf..ef28b6b 100644 --- a/src/universal_lambda_build.zig +++ b/src/universal_lambda_build.zig @@ -25,7 +25,7 @@ pub fn configureBuild(b: *std.Build, exe: *std.Build.Step.Compile) !void { }); // Add steps - try @import("lambdabuild.zig").configureBuild(b, exe); + try @import("lambda_build.zig").configureBuild(b, exe); try @import("standalone_server_build.zig").configureBuild(b, exe); try @import("flexilib_build.zig").configureBuild(b, exe, file_location);