# AWS Lambda Runtime for Zig ![Zig v0.16](https://img.shields.io/badge/Zig-v0.16-black?logo=zig&logoColor=F7A41D "Zig v0.16") [![MIT License](https://img.shields.io/github/license/by-nir/aws-lambda-zig)](/LICENSE) Write _AWS Lambda_ functions in Zig for blazing-fast invocations and cold starts. [🐣 Quick Start](#quick-start) · [📒 Documentation](#documentation) · [💽 Demos](#demos) ## Benchmark Zig makes it possible to build small, fast functions. Minimal [Hello World](#hello-world) demo: - ❄️ Cold invocation: `~11.5 ms` - ⚡ Warm invocation: between `0.9 ms` and `1.5 ms` - 💾 Max memory: `12 MB` - 📦 Function size: `362 KB` (zip, stripped)
Testing environment and settings
## Features - [x] Runtime API - [x] Runtime Metadata API - [ ] Extensions API - [ ] Telemetry API - [x] Response streaming - [x] CloudWatch & X-Ray integration - [ ] Lifecycle hooks - [x] Build system target configuration - [ ] Testing utilities ### Service Events _Feel free to open an issue for additional integrations, or better yet, open a pull request._ - [ ] Unified HTTP - [x] Lambda URLs - [ ] API Gateway - [ ] S3 - [ ] SQS - [ ] SNS - [ ] DynamoDB - [ ] Data Firehose ## Quick Start 1. Add a dependency to your project (replace `VERSION` with the desired version tag): ```console zig fetch --save git+https://github.com/by-nir/aws-lambda-zig#VERSION ``` 2. Configure the build script. 3. Implement a handler function (either an event handler or a streaming handler). 4. Build a handler executable for the preferred target architecture: ```console zig build --release -Darch=x86 zig build --release -Darch=arm ``` 5. Archive the executable into a zip: ```console zip -qj lambda.zip zig-out/bin/bootstrap ``` 6. Deploy the zip archive to a Lambda function: - Configure it with _Amazon Linux 2023_ or other **OS-only runtime**. - Use your preferred deployment method: the AWS console, CLI, SAM, or CI. ### Build Script ```zig const std = @import("std"); const lambda = @import("aws_lambda"); pub fn build(b: *std.Build) void { const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast, }); // Add an architecture configuration option and resolve a target query. const target = lambda.resolveTargetQuery(b, lambda.archOption(b)); // Import the runtime module const runtime = b.dependency("aws_lambda", .{}).module("lambda"); // Create the handler’s module const mod = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/main.zig"), .imports = &.{ .{ .name = "aws-lambda", .module = runtime }, }, // .link_libc = true, // If glibc is required // .strip = true, // If debug symbols aren’t required // .single_threaded = true, // For small Lambda (RAM ≤ 1,769 MB) }); // Compile an executable. const exe = b.addExecutable(.{ .name = "bootstrap", // The executable name must be "bootstrap"! .root_module = mod }); } ``` ### Event Handler ```zig const std = @import("std"); const lambda = @import("aws-lambda"); // Entry point for the Lambda function. pub fn main(init: std.process.Init) void { // Bind the handler to the runtime: lambda.handle(init, handler, .{}); } // Each event is processed separately by the handler function. // The function must have the following signature: fn handler( ctx: lambda.Context, // Metadata and utilities event: []const u8, // Raw event payload (JSON) ) ![]const u8 { return "Hello, world!"; } ``` ## Documentation ### Build This library provides a runtime module that handles the Lambda lifecycle and communication with the execution environment. To use it, follow these requirements: - Import the Lambda Runtime module provided by this library and wrap your handler function with it. - Build an executable named _bootstrap_ and archive it in a zip file. - Use the _Amazon Linux 2023_ runtime. #### Managed Target AWS Lambda supports two architectures: _x86_64_ and _arm64_ based on _Graviton2_. To build the handler correctly and get the best performance, the build target must be configured accordingly. The managed target resolver sets the optimal operating system, architecture, and CPU feature set. Call `lambda.resolveTargetQuery(*std.Build, arch)` to resolve the target for the given architecture (either `.x86` or `.arm`). To add a CLI configuration option, call `lambda.archOption(*std.Build)` and pass the result to `lambda.resolveTargetQuery`. It can then be set with `-Darch=x86` or `-Darch=arm` (it defaults to _x86_ when used manually). #### Example Build Script ```zig const std = @import("std"); const lambda = @import("aws_lambda"); pub fn build(b: *std.Build) void { const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseFast, }); // Add an architecture CLI option (or hard code either `.x86` or `.arm`) const arch: lambda.Arch = lambda.archOption(b); // Managed architecture target resolver const target = lambda.resolveTargetQuery(b, arch); // Import the runtime module const runtime = b.dependency("aws_lambda", .{}).module("lambda"); // Create the handler’s module const mod = b.createModule(.{ .target = target, .optimize = optimize, .root_source_file = b.path("src/main.zig"), .imports = &.{ .{ .name = "aws-lambda", .module = runtime }, }, // .link_libc = true, // If glibc is required // .strip = true, // If debug symbols aren’t required // .single_threaded = true, // For small Lambda (RAM ≤ 1,769 MB) }); // Compile an executable. const exe = b.addExecutable(.{ .name = "bootstrap", // The executable name must be "bootstrap"! .root_module = mod }); b.installArtifact(exe); } ``` ### Event Handler The event handler is the entry point for the Lambda function. The library provides a runtime that handles the event lifecycle and communication with the Lambda’s execution environment. With it, you can focus on implementing only the meaningful part of processing and responding to the event. Since the library manages the lifecycle, it expects the handler to have a specific signature. _Note that [response streaming](#response-streaming) has a dedicated lifecycle and handler signature._ ```zig const std = @import("std"); const lambda = @import("aws-lambda"); // Entry point for the Lambda function. // Each event is processed separately by the handler function. pub fn main(init: std.process.Init) void { // Bind the handler to the runtime: lambda.handle(init, handlerSync, .{}); // Alternatively, for asynchronous handlers: lambda.handleAsync(init, handlerAsync, .{}); } fn handlerSync( ctx: lambda.Context, // Metadata and utilities event: []const u8, // Raw event payload (JSON) ) ![]const u8 { // Process the `event` payload and return a response payload. return switch (event.len) { 0 => "Empty payload.", else => event, }; } fn handlerAsync( ctx: lambda.Context, // Metadata and utilities event: []const u8, // Raw event payload (JSON) ) !void { // Process the `event` payload... } ``` #### Errors & Logging When a handler returns an error, the runtime will log it to _CloudWatch_ and return an error response to the client. The runtime exposes a static logging function that can be used to manually log messages to _CloudWatch_. The function follows Zig’s standard logging conventions. ```zig const lambda = @import("aws-lambda"); lambda.log.err("This error is logged to {s}.", .{"CloudWatch"}); ``` > [!WARNING] > In release mode, only the _error_ level is preserved; other levels are removed at compile time. > This behavior may be overridden at build time. #### Handler Context The handler signature includes the parameter `ctx: lambda.Context`. It provides metadata and utilities to assist with processing the event. The following sections describe the context... #### Managed Resources Since the runtime manages the function and invocation lifecycle, it also owns the memory. The _handler context_ provides two allocators: | Allocator | Behavior | | --------- | -------- | | `ctx.gpa` | You own the memory and **must deallocate it** by the end of the invocation. | | `ctx.arena` | The memory is tied to the invocation’s lifetime. The runtime will deallocate it on your behalf after the invocation resolves. | | `ctx.io` | I/O interface for performing network and file operations. | #### Environment Variables The function's environment variables are mapped by the _handler context_. You can access them using the `env(key)` method: ```zig const foo_value = ctx.env("FOO_KEY") orelse "some_default_value"; ``` #### Invocation Metadata Per-invocation metadata is provided by the _handler context_ `ctx.request` field. It contains the following fields: | Field | Type | Description | | ----- | ---- | ----------- | | `request_id` | `[]const u8` | AWS request ID associated with the request. | | `xray_trace` | `[]const u8` | X-Ray trace ID. | | `invoked_arn` | `[]const u8` | The function ARN requested. It may be different in **each invoke** that executes the same version. | | `deadline_ms` | `u64` | Function execution deadline counted in milliseconds since the _Unix epoch_. | | `client_context` | `[]const u8` | Information about the client application and device when invoked through the AWS Mobile SDK. | | `cognito_identity` | `[]const u8` | Information about the Amazon Cognito identity provider when invoked through the AWS Mobile SDK. | #### Configuration Metadata Static config metadata is provided by the _handler context_ `ctx.config` field. It contains the following fields: | Field | Type | Description | | ----- | ---- | ----------- | | `aws_region` | `[]const u8` | AWS Region where the Lambda function is executed. | | `aws_access_id` | `[]const u8` | Access key obtained from the function's [execution role](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html). | | `aws_access_secret` | `[]const u8` | Access key obtained from the function's [execution role](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html). | | `aws_session_token` | `[]const u8` | Access key obtained from the function's [execution role](https://docs.aws.amazon.com/lambda/latest/dg/lambda-intro-execution-role.html). | | `func_name` | `[]const u8` | Name of the function. | | `func_version` | `[]const u8` | Version of the function being executed. | | `func_size` | `u16` | Amount of memory available to the function in MB. | | `func_init` | `InitType` | Initialization type of the function. | | `func_handler` | `[]const u8` | Handler location configured on the function. | | `log_group` | `[]const u8` | Name of the Amazon CloudWatch Logs group for the function. | | `log_stream` | `[]const u8` | Name of the Amazon CloudWatch Logs stream for the function. | #### Runtime Metadata To discover metadata about the runtime environment where the Lambda function runs, such as the Availability Zone ID, the _handler context_ provides a discovery method. ```zig const meta = try ctx.runtimeMetadata(); ``` The returned struct contains the following fields: | Field | Type | Description | | ----- | ---- | ----------- | | `availability_zone_id` | `[]const u8` | Availability Zone ID where the Lambda function is executed. | #### Force Termination Request that the Lambda execution environment terminate the runtime **AFTER** returning the response to the client. ```zig ctx.forceTerminateAfterResponse(); ``` > [!WARNING] > Use with caution! Only use this method when you assume the function won’t behave > as expected in the following invocation. ### Response Streaming The runtime supports streaming responses to the client; though implementing a streaming handler differs from the standard handler. ```zig const std = @import("std"); const lambda = @import("aws-lambda"); // Entry point for the Lambda function. pub fn main(init: std.process.Init) void { // Bind the handler to the runtime. lambda.handleStream(init, handler, .{}); } // Each event is processed separately by the handler function. // The function must have the following signature: fn handler( ctx: lambda.Context, // Metadata and utilities event: []const u8, // Raw event payload (JSON) stream: lambda.Stream, // Stream delegate ) !void { // Start streaming the response for a given content type. var buffer: [256]u8 = undefined; const writer = try stream.open(&buffer, "text/event-stream"); // Append to the streaming buffer. try writer.writeAll("data: Message"); try writer.print(" number {d}\n\n", .{1}); // Send the buffer to the client. try stream.publish(); // Wait for half a second. try ctx.io.sleep(.fromMilliseconds(500), .awake); // Send additional content to the client. try writer.writeAll("data: Message number 2\n\n"); try writer.writeAll("data: Message number 3\n\n"); try stream.publish(); // Optionally close the stream. try stream.close(); // Even after ending the stream we can still do more work in the handler... } ``` #### Stream Delegate Instead of returning the payload from the handler, a streaming handler uses the _stream delegate_. Once the content type is known, the handler should call `stream.open(&buffer, content_type)` to open the response stream. After that, it can append content incrementally. _Closing the stream is optional._ | Method | Description | | ------ | ----------- | | `stream.open(&buffer, content_type)` | Opens the response stream for a provided HTTP content type. | | `stream.openPrint(&buffer, content_type, fmt, args)` | Opens the response stream for a provided HTTP content type and initial body payload. The user MUST format the payload with proper HTTP semantics (or use a Event Encoder). | | `stream.publish()` | Send the partial response buffer to the client. | | `stream.close()` | Optionally conclude the response stream while continuing to process the event. | ## Demos ### Hello World Returns a short message. ```console zig build demo:hello --release -Darch=ARCH_OPTION ``` ### Debug ⚠️ _Deploy with caution! May expose sensitive data to the public._ Returns the raw payload as-is: ```console zig build demo:echo --release -Darch=ARCH_OPTION ``` Returns the function’s metadata, environment variables, and the event’s raw payload: ```console zig build demo:debug --release -Darch=ARCH_OPTION ``` ### Errors Immediately returns an error; the runtime logs it to _CloudWatch_: ```console zig build demo:fail --release -Darch=ARCH_OPTION ``` Returns an output larger than the Lambda limit; the runtime logs an error to _CloudWatch_: ```console zig build demo:oversize --release -Darch=ARCH_OPTION ``` Forces the Lambda function instance to terminate after returning a response: ```console zig build demo:terminate --release -Darch=ARCH_OPTION ``` ⚠️ Use with caution! _Only use this method when you assume the function won’t behave as expected in the following invocation._ ### Response Streaming 👉 _Be sure to configure the Lambda function with URL enabled and RESPONSE_STREAM invoke mode._ Streams a response to the client and continues execution after the response completes: ```console zig build demo:stream --release -Darch=ARCH_OPTION ``` ### Lambda URLs ⚠️ This is not a production-ready web server. Do not use it in production. Uses Lambda URLs in buffered invoke mode to serve dynamic web pages: ```console zig build demo:url_buffer --release -Darch=ARCH_OPTION ``` Uses Lambda URLs response streaming to serve dynamic updates: ```console zig build demo:url_stream --release -Darch=ARCH_OPTION ``` ## License The author and contributors are not responsible for issues or damages caused by the use of this software, any part of it, or its derivatives. See [LICENSE](/LICENSE) for the full terms of use. Disclaimer: **_AWS Lambda Runtime for Zig_ is not an official _Amazon Web Services_ software, nor is the author affiliated with _Amazon Web Services, Inc_.**