From d6ed6a219b31a58526721f96195c80061d41ce54 Mon Sep 17 00:00:00 2001 From: Andrew Clayton Date: Wed, 2 Aug 2023 17:03:48 +0100 Subject: Initial commit libunit-wasm and example C and Rust WebAssembly modules for NGINX Unit. Co-developed-by: Timo Stark Co-developed-by: Liam Crilly Signed-off-by: Andrew Clayton --- examples/.gitignore | 1 + examples/c/Makefile | 40 ++++++ examples/c/echo-request-raw.c | 187 +++++++++++++++++++++++++ examples/c/luw-echo-request.c | 98 +++++++++++++ examples/c/luw-upload-reflector.c | 101 ++++++++++++++ examples/c/unit-wasm-raw.c | 46 ++++++ examples/c/unit-wasm-raw.h | 87 ++++++++++++ examples/c/upload-reflector-raw.c | 223 ++++++++++++++++++++++++++++++ examples/docker/README.md | 31 +++++ examples/docker/demo-wasm.Dockerfile | 20 +++ examples/docker/unit-wasm.Dockerfile | 40 ++++++ examples/docker/wasm-conf.json | 76 ++++++++++ examples/rust/.gitignore | 3 + examples/rust/Makefile | 17 +++ examples/rust/echo-request/Cargo.toml | 12 ++ examples/rust/echo-request/src/lib.rs | 200 +++++++++++++++++++++++++++ examples/rust/upload-reflector/Cargo.toml | 12 ++ examples/rust/upload-reflector/src/lib.rs | 131 ++++++++++++++++++ 18 files changed, 1325 insertions(+) create mode 100644 examples/.gitignore create mode 100644 examples/c/Makefile create mode 100644 examples/c/echo-request-raw.c create mode 100644 examples/c/luw-echo-request.c create mode 100644 examples/c/luw-upload-reflector.c create mode 100644 examples/c/unit-wasm-raw.c create mode 100644 examples/c/unit-wasm-raw.h create mode 100644 examples/c/upload-reflector-raw.c create mode 100644 examples/docker/README.md create mode 100644 examples/docker/demo-wasm.Dockerfile create mode 100644 examples/docker/unit-wasm.Dockerfile create mode 100644 examples/docker/wasm-conf.json create mode 100644 examples/rust/.gitignore create mode 100644 examples/rust/Makefile create mode 100644 examples/rust/echo-request/Cargo.toml create mode 100644 examples/rust/echo-request/src/lib.rs create mode 100644 examples/rust/upload-reflector/Cargo.toml create mode 100644 examples/rust/upload-reflector/src/lib.rs (limited to 'examples') diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..19e1bce --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1 @@ +*.wasm diff --git a/examples/c/Makefile b/examples/c/Makefile new file mode 100644 index 0000000..1b10269 --- /dev/null +++ b/examples/c/Makefile @@ -0,0 +1,40 @@ +include ../../shared.mk + +CFLAGS += -I../../src/c/include +LIBS = -L../../src/c -lunit-wasm + +SDIR = examples/c + +LUW_SRCDIR = ../../src/c + +luw_deps = $(LUW_SRCDIR)/libunit-wasm.a \ + $(LUW_SRCDIR)/include/unit/unit-wasm.h + +examples: examples-luw + +examples-luw: luw-echo-request.wasm luw-upload-reflector.wasm + +examples-raw: echo-request-raw.wasm upload-reflector-raw.wasm + +luw-echo-request.wasm: luw-echo-request.c $(luw_deps) + $(PP_CCLNK) $(SDIR)/$@ + $(v)$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS) + +luw-upload-reflector.wasm: luw-upload-reflector.c $(luw_deps) + $(PP_CCLNK) $(SDIR)/$@ + $(v)$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LIBS) + +unit-wasm-raw.o: unit-wasm-raw.c unit-wasm-raw.h + $(PP_CC) $(SDIR)/$@ + $(v)$(CC) $(CFLAGS) -c $< + +echo-request-raw.wasm: echo-request-raw.c unit-wasm-raw.o + $(PP_CCLNK) $(SDIR)/$@ + $(v)$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< unit-wasm-raw.o + +upload-reflector-raw.wasm: upload-reflector-raw.c unit-wasm-raw.o + $(PP_CCLNK) $(SDIR)/$@ + $(v)$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< unit-wasm-raw.o + +clean: + rm -f *.wasm *.o *.gch diff --git a/examples/c/echo-request-raw.c b/examples/c/echo-request-raw.c new file mode 100644 index 0000000..2071597 --- /dev/null +++ b/examples/c/echo-request-raw.c @@ -0,0 +1,187 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * Copyright (C) Andrew Clayton + * Copyright (C) F5, Inc. + */ + +/* + * echo-request-raw.c - Raw example of writing a WASM module for use with Unit + * + * Download the wasi-sysroot tarball from https://github.com/WebAssembly/wasi-sdk/releases + */ + +#define _GNU_SOURCE + +#include +#include +#include +#include + +#include "unit-wasm-raw.h" + +static u8 *request_buf; + +__attribute__((import_module("env"), import_name("nxt_wasm_get_init_mem_size"))) +u32 nxt_wasm_get_init_mem_size(void); +__attribute__((import_module("env"), import_name("nxt_wasm_response_end"))) +void nxt_wasm_response_end(void); +__attribute__((import_module("env"), import_name("nxt_wasm_send_response"))) +void nxt_wasm_send_response(u32 offset); + +__attribute__((export_name("wasm_module_end_handler"))) +void wasm_module_end_handler(void) +{ + free(request_buf); +} + +__attribute__((export_name("wasm_module_init_handler"))) +void wasm_module_init_handler(void) +{ + request_buf = malloc(nxt_wasm_get_init_mem_size()); +} + +__attribute__((export_name("wasm_free_handler"))) +void wasm_free_handler(u32 addr) +{ + free((void *)addr); +} + +__attribute__((export_name("wasm_malloc_handler"))) +u32 wasm_malloc_handler(size_t size) +{ + return (u32)malloc(size); +} + +static int echo_request(u8 *addr) +{ + u8 *p; + const char *method; + struct req *req; + struct resp *resp; + struct hdr_field *hf; + struct hdr_field *hf_end; + static const int resp_offs = 4096; + + printf("==[WASM RESP]== %s:\n", __func__); + + /* + * For convenience, we will return our headers at the start + * of the shared memory so leave a little space (resp_offs) + * before storing the main response. + * + * send_headers() will return the start of the shared memory, + * echo_request() will return the start of the shared memory + * plus resp_offs. + */ + resp = (struct resp *)(addr + resp_offs); + + req = (struct req *)request_buf; + +#define BUF_ADD(name, member) \ + do { \ + p = mempcpy(p, name, strlen(name)); \ + p = mempcpy(p, (u8 *)req + req->member##_offs, req->member##_len); \ + p = mempcpy(p, "\n", 1); \ + } while (0) + +#define BUF_ADD_HF() \ + do { \ + p = mempcpy(p, (u8 *)req + hf->name_offs, hf->name_len); \ + p = mempcpy(p, " = ", 3); \ + p = mempcpy(p, (u8 *)req + hf->value_offs, hf->value_len); \ + p = mempcpy(p, "\n", 1); \ + } while (0) + + p = resp->data; + + p = mempcpy(p, "Welcome to WebAssembly on Unit!\n\n", 33); + + p = mempcpy(p, "[Request Info]\n", 15); + BUF_ADD("REQUEST_PATH = ", path); + BUF_ADD("METHOD = ", method); + BUF_ADD("VERSION = ", version); + BUF_ADD("QUERY = ", query); + BUF_ADD("REMOTE = ", remote); + BUF_ADD("LOCAL_ADDR = ", local_addr); + BUF_ADD("LOCAL_PORT = ", local_port); + BUF_ADD("SERVER_NAME = ", server_name); + + p = mempcpy(p, "\n[Request Headers]\n", 19); + hf_end = req->fields + req->nr_fields; + for (hf = req->fields; hf < hf_end; hf++) + BUF_ADD_HF(); + + method = (char *)req + req->method_offs; + if (memcmp(method, "POST", req->method_len) == 0 || + memcmp(method, "PUT", req->method_len) == 0) { + p = mempcpy(p, "\n[", 2); + p = mempcpy(p, method, req->method_len); + p = mempcpy(p, " data]\n", 7); + p = mempcpy(p, (u8 *)req + req->content_offs, req->content_len); + p = mempcpy(p, "\n", 1); + } + + p = memcpy(p, "\0", 1); + + resp->size = p - resp->data; + + send_headers(addr, "text/plain", resp->size); + + nxt_wasm_send_response(resp_offs); + /* Tell Unit no more data to send */ + nxt_wasm_response_end(); + + return 0; +} + +__attribute__((export_name("wasm_request_handler"))) +int wasm_request_handler(u8 *addr) +{ + struct req *req = (struct req *)addr; + struct req *rb = (struct req *)request_buf; + + printf("==[WASM REQ]== %s:\n", __func__); + + /* + * This function _may_ be called multiple times during a single + * request if there is a large amount of data to transfer. + * + * In this simple demo, we are only expecting it to be called + * once per request. + * + * Some useful request meta data: + * + * req->content_len contains the overall size of the POST/PUT + * data. + * req->content_sent shows how much of the body content has been + * in _this_ request. + * req->total_content_sent shows how much of it has been sent in + * total. + * req->content_offs is the offset in the passed in memory where + * the body content starts. + * + * For new requests req->request_size shows the total size of + * _this_ request, incl the req structure itself. + * For continuation requests, req->request_size is just the amount + * of new content, i.e req->content_sent + * + * When req->content_len == req->total_content_sent, that's the end + * of that request. + */ + + printf("==[WASM REQ]== req->request_size : %u\n", req->request_size); + memcpy(request_buf, addr, req->request_size); + + rb = (struct req *)request_buf; + printf("==[WASM REQ]== rb@%p\n", rb); + printf("==[WASM REQ]== request_buf@%p\n", request_buf); + printf("==[WASM REQ]== rb->content_offs : %u\n", rb->content_offs); + printf("==[WASM REQ]== rb->content_len : %u\n", rb->content_len); + printf("==[WASM REQ]== rb->content_sent : %u\n", rb->content_sent); + printf("==[WASM REQ]== rb->request_size : %u\n", rb->request_size); + + echo_request(addr); + + return 0; +} diff --git a/examples/c/luw-echo-request.c b/examples/c/luw-echo-request.c new file mode 100644 index 0000000..5655c65 --- /dev/null +++ b/examples/c/luw-echo-request.c @@ -0,0 +1,98 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * examples/c/luw-echo-request.c - Example of writing a WASM module for use + * with Unit using libunit-wasm + * + * Copyright (C) Andrew Clayton + * Copyright (C) F5, Inc. + */ + +#define _XOPEN_SOURCE 500 + +#include +#include +#include +#include + +#include "unit/unit-wasm.h" + +static u8 *request_buf; + +__luw_export_name("luw_module_end_handler") +void luw_module_end_handler(void) +{ + free(request_buf); +} + +__luw_export_name("luw_module_init_handler") +void luw_module_init_handler(void) +{ + request_buf = malloc(luw_mem_get_init_size()); +} + +static bool hdr_iter_func(luw_ctx_t *ctx, const char *name, const char *value, + void *user_data __luw_unused) +{ + luw_mem_writep(ctx, "%s = %s\n", name, value); + + return true; +} + +__luw_export_name("luw_request_handler") +int luw_request_handler(u8 *addr) +{ + luw_ctx_t ctx; + char clen[32]; + const char *method; + + luw_init_ctx(&ctx, addr, 4096 /* Response offset */); + /* Take a copy of the request and use that */ + luw_set_req_buf(&ctx, &request_buf, LUW_SRB_NONE); + +#define BUF_ADD(fmt, member) \ + luw_mem_writep(&ctx, fmt, luw_get_http_##member(&ctx)); + + luw_mem_writep(&ctx, + " *** Welcome to WebAssembly on Unit! " + "[libunit-wasm (%d.%d.%d/%#0.8x)] ***\n\n", + LUW_VERSION_MAJOR, LUW_VERSION_MINOR, LUW_VERSION_PATCH, + LUW_VERSION_NUMBER); + + luw_mem_writep(&ctx, "[Request Info]\n"); + BUF_ADD("REQUEST_PATH = %s\n", path); + BUF_ADD("METHOD = %s\n", method); + BUF_ADD("VERSION = %s\n", version); + BUF_ADD("QUERY = %s\n", query); + BUF_ADD("REMOTE = %s\n", remote); + BUF_ADD("LOCAL_ADDR = %s\n", local_addr); + BUF_ADD("LOCAL_PORT = %s\n", local_port); + BUF_ADD("SERVER_NAME = %s\n", server_name); + + luw_mem_writep(&ctx, "\n[Request Headers]\n"); + + luw_http_hdr_iter(&ctx, hdr_iter_func, NULL); + + method = luw_get_http_method(&ctx); + if (memcmp(method, "POST", strlen(method)) == 0 || + memcmp(method, "PUT", strlen(method)) == 0) { + luw_mem_writep(&ctx, "\n[%s data]\n", method); + luw_mem_writep_data(&ctx, luw_get_http_content(&ctx), + luw_get_http_content_len(&ctx)); + luw_mem_writep(&ctx, "\n"); + } + + luw_http_init_headers(&ctx, 2, 0); + + snprintf(clen, sizeof(clen), "%lu", luw_get_response_data_size(&ctx)); + luw_http_add_header(&ctx, 0, "Content-Type", "text/plain"); + luw_http_add_header(&ctx, 1, "Content-Length", clen); + + luw_http_send_headers(&ctx); + + luw_http_send_response(&ctx); + /* Tell Unit no more data to send */ + luw_http_response_end(); + + return 0; +} diff --git a/examples/c/luw-upload-reflector.c b/examples/c/luw-upload-reflector.c new file mode 100644 index 0000000..95bc514 --- /dev/null +++ b/examples/c/luw-upload-reflector.c @@ -0,0 +1,101 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * examples/c/luw-upload-reflector.c - Example of writing a WASM module for + * use with Unit using libunit-wasm + * + * Copyright (C) Andrew Clayton + * Copyright (C) F5, Inc. + */ + +#define _XOPEN_SOURCE 500 + +#include +#include + +#include "unit/unit-wasm.h" + +static luw_ctx_t ctx; + +static size_t total_response_sent; + +static u8 *request_buf; + +/* + * While these first two _handlers_ aren't technically required, they + * could be combined or the code could just go in upload_reflector(), + * they demonstrate their use in ensuring the module is in the right + * state for a new request. + */ +__luw_export_name("luw_response_end_handler") +void luw_response_end_handler(void) +{ + total_response_sent = 0; +} + +__luw_export_name("luw_request_end_handler") +void luw_request_end_handler(void) +{ + if (!request_buf) + return; + + free(request_buf); + request_buf = NULL; +} + +static int upload_reflector(luw_ctx_t *ctx) +{ + size_t write_bytes; + + /* Send headers */ + if (total_response_sent == 0) { + static const char *defct = "application/octet-stream"; + const char *ct = luw_http_hdr_get_value(ctx, "Content-Type"); + char clen[32]; + + snprintf(clen, sizeof(clen), "%lu", + luw_get_http_content_len(ctx)); + + luw_http_init_headers(ctx, 2, 0); + luw_http_add_header(ctx, 0, "Content-Type", ct ? ct : defct); + luw_http_add_header(ctx, 1, "Content-Length", clen); + luw_http_send_headers(ctx); + } + + write_bytes = luw_mem_fill_buf_from_req(ctx, total_response_sent); + total_response_sent += write_bytes; + + luw_http_send_response(ctx); + + if (total_response_sent == luw_get_http_content_len(ctx)) { + /* Tell Unit no more data to send */ + luw_http_response_end(); + } + + return 0; +} + +__luw_export_name("luw_request_handler") +int luw_request_handler(u8 *addr) +{ + if (!request_buf) { + luw_init_ctx(&ctx, addr, 0 /* Response offset */); + /* + * Take a copy of the request and use that, we do this + * in APPEND mode so we can build up request_buf from + * multiple requests. + * + * Just allocate memory for the total amount of data we + * expect to get, this includes the request structure + * itself as well as any body content. + */ + luw_set_req_buf(&ctx, &request_buf, + LUW_SRB_APPEND|LUW_SRB_ALLOC|LUW_SRB_FULL_SIZE); + } else { + luw_req_buf_append(&ctx, addr); + } + + upload_reflector(&ctx); + + return 0; +} diff --git a/examples/c/unit-wasm-raw.c b/examples/c/unit-wasm-raw.c new file mode 100644 index 0000000..42ebcbf --- /dev/null +++ b/examples/c/unit-wasm-raw.c @@ -0,0 +1,46 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * Copyright (C) Andrew Clayton + * Copyright (C) F5, Inc. + */ + +#define _GNU_SOURCE + +#include +#include + +#include "unit-wasm-raw.h" + +__attribute__((import_module("env"), import_name("nxt_wasm_send_headers"))) +void nxt_wasm_send_headers(u32 offset); + +void send_headers(u8 *addr, const char *ct, size_t len) +{ + struct resp_hdr *rh; + char clen[32]; + u8 *p; + static const u32 hdr_offs = 0; + + rh = (struct resp_hdr *)addr; + +#define SET_HDR_FIELD(idx, name, val) \ + do { \ + rh->fields[idx].name_offs = p - addr; \ + rh->fields[idx].name_len = strlen(name); \ + p = mempcpy(p, name, rh->fields[idx].name_len); \ + rh->fields[idx].value_offs = p - addr; \ + rh->fields[idx].value_len = strlen(val); \ + p = mempcpy(p, val, rh->fields[idx].value_len); \ + } while (0) + + rh->nr_fields = 2; + p = addr + sizeof(struct resp_hdr) + + (rh->nr_fields * sizeof(struct hdr_field)); + + SET_HDR_FIELD(0, "Content-Type", ct); + snprintf(clen, sizeof(clen), "%lu", len); + SET_HDR_FIELD(1, "Content-Length", clen); + + nxt_wasm_send_headers(hdr_offs); +} diff --git a/examples/c/unit-wasm-raw.h b/examples/c/unit-wasm-raw.h new file mode 100644 index 0000000..6fd9d35 --- /dev/null +++ b/examples/c/unit-wasm-raw.h @@ -0,0 +1,87 @@ +#ifndef _UNIT_WASM_H_ +#define _UNIT_WASM_H_ + +#include +#include + +typedef uint64_t u64; +typedef int64_t s64; +typedef uint32_t u32; +typedef int32_t s32; +typedef uint16_t u16; +typedef int16_t s16; +typedef uint8_t u8; +typedef int8_t s8; + +#ifndef __unused +#define __unused __attribute__((unused)) +#endif +#ifndef __maybe_unused +#define __maybe_unused __unused +#endif +#ifndef __always_unused +#define __always_unused __unused +#endif + +struct hdr_field { + u32 name_offs; + u32 name_len; + u32 value_offs; + u32 value_len; +}; + +struct req { + u32 method_offs; + u32 method_len; + u32 version_offs; + u32 version_len; + u32 path_offs; + u32 path_len; + u32 query_offs; + u32 query_len; + u32 remote_offs; + u32 remote_len; + u32 local_addr_offs; + u32 local_addr_len; + u32 local_port_offs; + u32 local_port_len; + u32 server_name_offs; + u32 server_name_len; + + u32 content_offs; + u32 content_len; + u32 content_sent; + u32 total_content_sent; + + u32 request_size; + + u32 nr_fields; + + u32 tls; + + struct hdr_field fields[]; +}; + +struct resp { + u32 size; + + u8 data[]; +}; + +struct resp_hdr { + u32 nr_fields; + + struct hdr_field fields[]; +}; + +extern void wasm_module_end_handler(void); +extern void wasm_module_init_handler(void); +extern void wasm_response_end_handler(void); +extern void wasm_request_end_handler(void); +extern void wasm_free_handler(u32 addr); +extern u32 wasm_malloc_handler(size_t size); +extern int wasm_request_handler(u8 *addr); + +extern void send_headers(u8 *addr, const char *ct, size_t len); + +#endif /* _UNIT_WASM_H_ */ diff --git a/examples/c/upload-reflector-raw.c b/examples/c/upload-reflector-raw.c new file mode 100644 index 0000000..3da4f8d --- /dev/null +++ b/examples/c/upload-reflector-raw.c @@ -0,0 +1,223 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * Copyright (C) Andrew Clayton + * Copyright (C) F5, Inc. + */ + +/* + * upload-reflector-raw.c - Raw example of writing a WASM module for use with + * Unit + * + * Download the wasi-sysroot tarball from https://github.com/WebAssembly/wasi-sdk/releases + */ + +#define _GNU_SOURCE + +#include +#include +#include + +#include "unit-wasm-raw.h" + +static size_t total_response_sent; + +static u8 *request_buf; + +__attribute__((import_module("env"), import_name("nxt_wasm_get_init_mem_size"))) +u32 nxt_wasm_get_init_mem_size(void); +__attribute__((import_module("env"), import_name("nxt_wasm_response_end"))) +void nxt_wasm_response_end(void); +__attribute__((import_module("env"), import_name("nxt_wasm_send_response"))) +void nxt_wasm_send_response(u32 offset); + +__attribute__((export_name("wasm_response_end_handler"))) +void wasm_response_end_handler(void) +{ + total_response_sent = 0; +} + +__attribute__((export_name("wasm_request_end_handler"))) +void wasm_request_end_handler(void) +{ + if (!request_buf) + return; + + free(request_buf); + request_buf = NULL; +} + +__attribute__((export_name("wasm_free_handler"))) +void wasm_free_handler(u32 addr) +{ + free((void *)addr); +} + +__attribute__((export_name("wasm_malloc_handler"))) +u32 wasm_malloc_handler(size_t size) +{ + return (u32)malloc(size); +} + +static int upload_reflector(u8 *addr) +{ + size_t mem_size = nxt_wasm_get_init_mem_size(); + size_t rsize = sizeof(struct resp); + size_t write_bytes; + struct req *req; + struct resp *resp; + + printf("==[WASM RESP]== %s:\n", __func__); + + resp = (struct resp *)addr; + req = (struct req *)request_buf; + + printf("==[WASM RESP]== resp@%p\n", resp); + printf("==[WASM RESP]== req@%p\n", req); + printf("==[WASM RESP]== req->content_len : %u\n", req->content_len); + + resp = (struct resp *)addr; + + /* Send headers */ + if (total_response_sent == 0) { + const char *field; + struct hdr_field *f; + struct hdr_field *f_end; + char ct[256]; + + /* Try to set the Content-Type */ + f_end = req->fields + req->nr_fields; + for (f = req->fields; f < f_end; f++) { + field = (const char *)(u8 *)req + f->name_offs; + + if (strncasecmp(field, "Content-Type", 12) == 0) { + snprintf(ct, sizeof(ct), "%.*s", f->value_len, + (u8 *)req + f->value_offs); + break; + } + + field = NULL; + } + if (!field) + sprintf(ct, "application/octet-stream"); + + send_headers(addr, ct, req->content_len); + } + + write_bytes = req->content_sent; + if (write_bytes > mem_size - rsize) + write_bytes = mem_size - rsize; + + printf("==[WASM RESP]== write_bytes : %lu\n", write_bytes); + printf("==[WASM RESP]== req->content_len : %u\n", req->content_len); + printf("==[WASM RESP]== total_response_sent : %lu\n", + total_response_sent); + + printf("==[WASM RESP]== Copying (%lu) bytes of data from [%p+%lx] to " + "[%p]\n", write_bytes, req, + req->content_offs + total_response_sent, resp->data); + memcpy(resp->data, + (u8 *)req + req->content_offs + total_response_sent, + write_bytes); + + total_response_sent += write_bytes; + resp->size = write_bytes; + printf("==[WASM RESP]== resp->size : %u\n", resp->size); + + nxt_wasm_send_response(0); + + if (total_response_sent == req->content_len) { + printf("==[WASM RESP]== All data sent. Cleaning up...\n"); + total_response_sent = 0; + + free(request_buf); + request_buf = NULL; + + /* Tell Unit no more data to send */ + nxt_wasm_response_end(); + } + + return 0; +} + +__attribute__((export_name("wasm_request_handler"))) +int wasm_request_handler(u8 *addr) +{ + struct req *req = (struct req *)addr; + struct req *rb = (struct req *)request_buf; + + printf("==[WASM REQ]== %s:\n", __func__); + + /* + * This function _may_ be called multiple times during a single + * request if there is a large amount of data to transfer. + * + * Some useful request meta data: + * + * req->content_len contains the overall size of the POST/PUT + * data. + * req->content_sent shows how much of the body content has been + * in _this_ request. + * req->total_content_sent shows how much of it has been sent in + * total. + * req->content_offs is the offset in the passed in memory where + * the body content starts. + * + * For new requests req->request_size shows the total size of + * _this_ request, incl the req structure itself. + * For continuation requests, req->request_size is just the amount + * of new content, i.e req->content_sent + * + * When req->content_len == req->total_content_sent, that's the end + * of that request. + */ + + if (!request_buf) { + /* + * Just allocate memory for the total amount of data we + * expect to get, this includes the request structure + * itself as well as any body content. + */ + printf("==[WASM REQ]== malloc(%u)\n", + req->content_offs + req->content_len); + request_buf = malloc(req->content_offs + req->content_len); + + /* + * Regardless of how much memory we allocated above, here + * we only want to copy the amount of data we actually + * received in this request. + */ + printf("==[WASM REQ]== req->request_size : %u\n", + req->request_size); + memcpy(request_buf, addr, req->request_size); + + rb = (struct req *)request_buf; + printf("==[WASM REQ]== rb@%p\n", rb); + printf("==[WASM REQ]== request_buf@%p\n", request_buf); + printf("==[WASM REQ]== rb->content_offs : %u\n", + rb->content_offs); + printf("==[WASM REQ]== rb->content_len : %u\n", + rb->content_len); + printf("==[WASM REQ]== rb->content_sent : %u\n", + rb->content_sent); + printf("==[WASM REQ]== rb->request_size : %u\n", + rb->request_size); + } else { + memcpy(request_buf + rb->request_size, addr + req->content_offs, + req->request_size); + + printf("==[WASM REQ +]== req->content_offs : %u\n", + req->content_offs); + printf("==[WASM REQ +]== req->content_sent : %u\n", + req->content_sent); + printf("==[WASM REQ +]== req->request_size : %u\n", + req->request_size); + + rb->content_sent = req->content_sent; + rb->total_content_sent = req->total_content_sent; + } + + upload_reflector(addr); + + return 0; +} diff --git a/examples/docker/README.md b/examples/docker/README.md new file mode 100644 index 0000000..61d2740 --- /dev/null +++ b/examples/docker/README.md @@ -0,0 +1,31 @@ +Unit-Wasm demo +============== + +## Build the docker images + +From the repository root, run + +```shell +$ make docker +``` + +This builds two docker images. + +### 1. unit:wasm + +This image is based on the Docker Official Images for Unit 1.30 with a fresh +build of unitd and the experimental Wasm module. Wasmtime is included as a +shared object. + +### 2. unit:demo-wasm + +This image is based on the new `unit:wasm` image created above. It includes +a demo application written in C and compiled to wasm. + +## Run the demo + +```shell +$ docker run -d -p 9000:80 unit:demo-wasm +$ curl localhost:9000 +$ curl localhost:9000/echo +``` diff --git a/examples/docker/demo-wasm.Dockerfile b/examples/docker/demo-wasm.Dockerfile new file mode 100644 index 0000000..2385211 --- /dev/null +++ b/examples/docker/demo-wasm.Dockerfile @@ -0,0 +1,20 @@ +FROM unit:wasm AS build +WORKDIR /demo + +# Get all the build tools we need +# +RUN apt update && apt install -y wget build-essential clang llvm lld +RUN cd /usr/lib/llvm-11/lib/clang/11.0.1 && wget -O- https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-20/libclang_rt.builtins-wasm32-wasi-20.0.tar.gz | tar zxvf - +RUN wget -O- https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-20/wasi-sysroot-20.0.tar.gz | tar zxfv - + +# Copy-in the demo application source code and build into a .wasm module +# +ADD ${PWD} /demo/ +RUN make WASI_SYSROOT=/demo/wasi-sysroot examples + +# Copy the .wasm modules and Unit configuration to the final Docker image +# that will run the demo application. +# +FROM unit:wasm +COPY --from=build /demo/examples/c/*.wasm /demo/ +ADD examples/docker/wasm-conf.json /docker-entrypoint.d diff --git a/examples/docker/unit-wasm.Dockerfile b/examples/docker/unit-wasm.Dockerfile new file mode 100644 index 0000000..b7b47a5 --- /dev/null +++ b/examples/docker/unit-wasm.Dockerfile @@ -0,0 +1,40 @@ +# Start with the minimal Docker Official Image so we can use the same defaults +# +FROM unit:minimal AS build +WORKDIR /src + +# Get all the build tools we need, including Wasmtime +# +#RUN apt update && apt install -y wget git build-essential clang lld libpcre2-dev libssl-dev +RUN apt update && apt install -y wget git build-essential libpcre2-dev libssl-dev +RUN wget -O- https://github.com/bytecodealliance/wasmtime/releases/download/v11.0.0/wasmtime-v11.0.0-$(arch)-linux-c-api.tar.xz \ + | tar Jxfv - && \ + mkdir /usr/lib/wasmtime && \ + cp /src/wasmtime-v11.0.0-$(arch)-linux-c-api/lib/* /usr/lib/wasmtime + +# Build NGINX JavaScript (njs) so that we have a feature-complete Unit +# +RUN git clone https://github.com/nginx/njs.git && \ + cd njs && \ + ./configure --no-libxml2 --no-zlib && \ + make + +# Build Unit with the Wasm module, copying the configure arguments from the +# official image. +# +RUN git clone https://github.com/nginx/unit.git && \ + cd unit && \ + wget -O- https://github.com/nginx/unit/pull/902.patch | patch -p1 && \ + ./configure $(unitd --version 2>&1 | tr ' ' '\n' | grep ^-- | grep -v opt=) \ + --cc-opt="-I/src/njs/src -I/src/njs/build" --ld-opt=-L/src/njs/build && \ + ./configure wasm --include-path=/src/wasmtime-v11.0.0-$(arch)-linux-c-api/include \ + --lib-path=/usr/lib/wasmtime --rpath && \ + make + +# Create a clean final image by copying over only Wasmtime, the new unitd +# binary, and the Wasm module. +# +FROM unit:minimal +COPY --from=build /src/unit/build/sbin/unitd /usr/sbin +COPY --from=build /src/unit/build/lib/unit/modules/wasm.unit.so /usr/lib/unit/modules +COPY --from=build /usr/lib/wasmtime/*.so /usr/lib/wasmtime/ diff --git a/examples/docker/wasm-conf.json b/examples/docker/wasm-conf.json new file mode 100644 index 0000000..5ed173e --- /dev/null +++ b/examples/docker/wasm-conf.json @@ -0,0 +1,76 @@ +{ + "access_log": "/dev/stdout", + "settings": { + "http": { + "log_route": true, + "max_body_size": 1073741824 + } + }, + + "listeners": { + "*:80": { + "pass": "routes" + } + }, + + "routes": [ + { + "match": { + "uri": "/echo*" + }, + "action": { + "pass": "applications/luw-echo-request" + } + }, + { + "match": { + "uri": "/upload*" + }, + "action": { + "pass": "applications/luw-upload-reflector" + } + }, + { + "match": { + "headers": { + "accept": "*text/html*" + } + }, + "action": { + "share": "/usr/share/unit/welcome/welcome.html" + } + }, + { + "action": { + "share": "/usr/share/unit/welcome/welcome.md" + } + } + ], + + "applications": { + "luw-echo-request": { + "type": "wasm", + "module": "/demo/luw-echo-request.wasm", + "request_handler": "luw_request_handler", + "malloc_handler": "luw_malloc_handler", + "free_handler": "luw_free_handler", + "module_init_handler": "luw_module_init_handler", + "module_end_handler": "luw_module_end_handler", + "access": { + "filesystem": [ + "/tmp", + "/var/tmp" + ] + } + }, + "luw-upload-reflector": { + "type": "wasm", + "module": "/demo/luw-upload-reflector.wasm", + "request_handler": "luw_request_handler", + "malloc_handler": "luw_malloc_handler", + "free_handler": "luw_free_handler", + "request_end_handler": "luw_request_end_handler", + "response_end_handler": "luw_response_end_handler" + } + } +} diff --git a/examples/rust/.gitignore b/examples/rust/.gitignore new file mode 100644 index 0000000..559d663 --- /dev/null +++ b/examples/rust/.gitignore @@ -0,0 +1,3 @@ +Cargo.lock + +target/ diff --git a/examples/rust/Makefile b/examples/rust/Makefile new file mode 100644 index 0000000..b41bc33 --- /dev/null +++ b/examples/rust/Makefile @@ -0,0 +1,17 @@ +include ../../shared.mk + +SDIR = examples/rust + +examples: rust-echo-request rust-upload-reflector + +rust-echo-request: echo-request/Cargo.toml echo-request/src/lib.rs + $(PP_GEN) $(SDIR)/echo-request/target/wasm32-wasi/ + $(v)cd echo-request; cargo build --target=wasm32-wasi + +rust-upload-reflector: upload-reflector/Cargo.toml upload-reflector/src/lib.rs + $(PP_GEN) $(SDIR)/upload-reflector/target/wasm32-wasi/ + $(v)cd upload-reflector; cargo build --target=wasm32-wasi + +clean: + rm -f echo-request/Cargo.lock upload-reflector/Cargo.lock + rm -rf echo-request/target upload-reflector/target diff --git a/examples/rust/echo-request/Cargo.toml b/examples/rust/echo-request/Cargo.toml new file mode 100644 index 0000000..256242e --- /dev/null +++ b/examples/rust/echo-request/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rust-echo-request" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +unit-wasm = { path = "../../../src/rust", version = "0.1.0-beta" } + +[lib] +crate-type = ["cdylib"] diff --git a/examples/rust/echo-request/src/lib.rs b/examples/rust/echo-request/src/lib.rs new file mode 100644 index 0000000..4802cff --- /dev/null +++ b/examples/rust/echo-request/src/lib.rs @@ -0,0 +1,200 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * Copyright (C) Andrew Clayton + * Copyright (C) Timo Stark + * Copyright (C) F5, Inc. + */ + +// Include RAW FFI Bindings. +// @todo: Replace this with the new native Rust API +use unit_wasm::ffi::*; + +use std::ffi::CStr; +use std::os::raw::c_char; +use std::os::raw::c_void; +use std::ptr; + +// Buffer of some size to store the copy of the request +static mut REQUEST_BUF: *mut u8 = ptr::null_mut(); + +#[no_mangle] +pub extern "C" fn luw_module_end_handler() { + unsafe { + luw_free(REQUEST_BUF as *mut c_void); + } +} + +#[no_mangle] +pub extern "C" fn luw_module_init_handler() { + unsafe { + REQUEST_BUF = luw_malloc(luw_mem_get_init_size().try_into().unwrap()) + as *mut u8; + } +} + +pub extern "C" fn hdr_iter_func( + ctx: *mut luw_ctx_t, + name: *const c_char, + value: *const c_char, + _data: *mut c_void, +) -> bool { + unsafe { + luw_mem_writep( + ctx, + "%s = %s\n\0".as_ptr() as *const c_char, + name, + value, + ); + } + + return true; +} + +#[no_mangle] +pub extern "C" fn luw_request_handler(addr: *mut u8) -> i32 { + // Need a initalization + // + // It sucks that rust needs this, this is supposed to be + // an opaque structure and the structure is 0-initialised + // in luw_init_ctx(); + let mut ctx_: luw_ctx_t = luw_ctx_t { + addr: ptr::null_mut(), + mem: ptr::null_mut(), + req: ptr::null_mut(), + resp: ptr::null_mut(), + resp_hdr: ptr::null_mut(), + resp_offset: 0, + req_buf: ptr::null_mut(), + hdrp: ptr::null_mut(), + reqp: ptr::null_mut(), + }; + let ctx: *mut luw_ctx_t = &mut ctx_; + + unsafe { + // Initialise the context structure. + // + // addr is the address of the previously allocated memory shared + // between the module and unit. + // + // The response data will be stored @ addr + offset (of 4096 bytes). + // This will leave some space for the response headers. + luw_init_ctx(ctx, addr, 4096); + + // Allocate memory to store the request and copy the request data. + luw_set_req_buf(ctx, &mut REQUEST_BUF, luw_srb_flags_t_LUW_SRB_NONE); + + // Define the Response Body Text. + + luw_mem_writep( + ctx, + " * Welcome to WebAssembly in Rust on Unit! \ + [libunit-wasm (%d.%d.%d/%#0.8x)] \ + *\n\n\0" + .as_ptr() as *const c_char, + LUW_VERSION_MAJOR, + LUW_VERSION_MINOR, + LUW_VERSION_PATCH, + LUW_VERSION_NUMBER, + ); + + luw_mem_writep(ctx, "[Request Info]\n\0".as_ptr() as *const c_char); + + luw_mem_writep( + ctx, + "REQUEST_PATH = %s\n\0".as_ptr() as *const c_char, + luw_get_http_path(ctx) as *const c_char, + ); + luw_mem_writep( + ctx, + "METHOD = %s\n\0".as_ptr() as *const c_char, + luw_get_http_method(ctx) as *const c_char, + ); + luw_mem_writep( + ctx, + "VERSION = %s\n\0".as_ptr() as *const c_char, + luw_get_http_version(ctx) as *const c_char, + ); + luw_mem_writep( + ctx, + "QUERY = %s\n\0".as_ptr() as *const c_char, + luw_get_http_query(ctx) as *const c_char, + ); + luw_mem_writep( + ctx, + "REMOTE = %s\n\0".as_ptr() as *const c_char, + luw_get_http_remote(ctx) as *const c_char, + ); + luw_mem_writep( + ctx, + "LOCAL_ADDR = %s\n\0".as_ptr() as *const c_char, + luw_get_http_local_addr(ctx) as *const c_char, + ); + luw_mem_writep( + ctx, + "LOCAL_PORT = %s\n\0".as_ptr() as *const c_char, + luw_get_http_local_port(ctx) as *const c_char, + ); + luw_mem_writep( + ctx, + "SERVER_NAME = %s\n\0".as_ptr() as *const c_char, + luw_get_http_server_name(ctx) as *const c_char, + ); + + luw_mem_writep( + ctx, + "\n[Request Headers]\n\0".as_ptr() as *const c_char, + ); + + luw_http_hdr_iter(ctx, Some(hdr_iter_func), ptr::null_mut()); + + let method = CStr::from_ptr(luw_get_http_method(ctx)).to_str().unwrap(); + if method == "POST" || method == "PUT" { + luw_mem_writep( + ctx, + "\n[%s data]\n\0".as_ptr() as *const c_char, + luw_get_http_method(ctx) as *const c_char, + ); + luw_mem_writep_data( + ctx, + luw_get_http_content(ctx), + luw_get_http_content_len(ctx), + ); + luw_mem_writep(ctx, "\n\0".as_ptr() as *const c_char); + } + + let content_len = format!("{}\0", luw_get_response_data_size(ctx)); + + // Init Response Headers + // + // Needs the context, number of headers about to add as well as + // the offset where to store the headers. In this case we are + // storing the response headers at the beginning of our shared + // memory at offset 0. + + luw_http_init_headers(ctx, 2, 0); + luw_http_add_header( + ctx, + 0, + "Content-Type\0".as_ptr() as *const c_char, + "text/plain\0".as_ptr() as *const c_char, + ); + luw_http_add_header( + ctx, + 1, + "Content-Length\0".as_ptr() as *const c_char, + content_len.as_ptr() as *const c_char, + ); + + // This calls nxt_wasm_send_headers() in Unit + luw_http_send_headers(ctx); + + // This calls nxt_wasm_send_response() in Unit + luw_http_send_response(ctx); + + // This calls nxt_wasm_response_end() in Unit + luw_http_response_end(); + } + + return 0; +} diff --git a/examples/rust/upload-reflector/Cargo.toml b/examples/rust/upload-reflector/Cargo.toml new file mode 100644 index 0000000..dc9d23f --- /dev/null +++ b/examples/rust/upload-reflector/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "rust-upload-reflector" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +unit-wasm = { path = "../../../src/rust", version = "0.1.0-beta" } + +[lib] +crate-type = ["cdylib"] diff --git a/examples/rust/upload-reflector/src/lib.rs b/examples/rust/upload-reflector/src/lib.rs new file mode 100644 index 0000000..9893d5a --- /dev/null +++ b/examples/rust/upload-reflector/src/lib.rs @@ -0,0 +1,131 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * Copyright (C) Andrew Clayton + * Copyright (C) Timo Stark + * Copyright (C) F5, Inc. + */ + +// Include RAW FFI Bindings. +// @todo: Replace this with the new native Rust API +use unit_wasm::ffi::*; + +use std::os::raw::c_char; +use std::os::raw::c_void; +use std::ptr; + +static mut CTX: luw_ctx_t = luw_ctx_t { + addr: ptr::null_mut(), + mem: ptr::null_mut(), + req: ptr::null_mut(), + resp: ptr::null_mut(), + resp_hdr: ptr::null_mut(), + resp_offset: 0, + req_buf: ptr::null_mut(), + hdrp: ptr::null_mut(), + reqp: ptr::null_mut(), +}; + +static mut TOTAL_RESPONSE_SENT: usize = 0; + +// Buffer of some size to store the copy of the request +static mut REQUEST_BUF: *mut u8 = ptr::null_mut(); + +#[no_mangle] +pub extern "C" fn luw_response_end_handler() { + unsafe { + TOTAL_RESPONSE_SENT = 0; + } +} + +#[no_mangle] +pub extern "C" fn luw_request_end_handler() { + unsafe { + if REQUEST_BUF.is_null() { + return; + } + + luw_free(REQUEST_BUF as *mut c_void); + REQUEST_BUF = ptr::null_mut(); + } +} + +pub fn upload_reflector(ctx: *mut luw_ctx_t) -> i32 { + let write_bytes: usize; + + unsafe { + // Send headers + if TOTAL_RESPONSE_SENT == 0 { + let content_len = format!("{}\0", luw_get_http_content_len(ctx)); + let defct = "application/octet-stream\0".as_ptr() as *const c_char; + let mut ct = luw_http_hdr_get_value( + ctx, + "Content-Type\0".as_ptr() as *const c_char, + ); + + if ct == ptr::null_mut() { + ct = defct; + } + + luw_http_init_headers(ctx, 2, 0); + luw_http_add_header( + ctx, + 0, + "Content-Type\0".as_ptr() as *const c_char, + ct, + ); + luw_http_add_header( + ctx, + 1, + "Content-Length\0".as_ptr() as *const c_char, + content_len.as_ptr() as *const c_char, + ); + luw_http_send_headers(ctx); + } + + write_bytes = luw_mem_fill_buf_from_req(ctx, TOTAL_RESPONSE_SENT); + TOTAL_RESPONSE_SENT += write_bytes; + + luw_http_send_response(ctx); + + if TOTAL_RESPONSE_SENT == luw_get_http_content_len(ctx) { + // Tell Unit no more data to send + luw_http_response_end(); + } + } + + return 0; +} + +#[no_mangle] +pub extern "C" fn luw_request_handler(addr: *mut u8) -> i32 { + unsafe { + let ctx: *mut luw_ctx_t = &mut CTX; + + if REQUEST_BUF.is_null() { + luw_init_ctx(ctx, addr, 0 /* Response offset */); + /* + * Take a copy of the request and use that, we do this + * in APPEND mode so we can build up request_buf from + * multiple requests. + * + * Just allocate memory for the total amount of data we + * expect to get, this includes the request structure + * itself as well as any body content. + */ + luw_set_req_buf( + ctx, + &mut REQUEST_BUF, + luw_srb_flags_t_LUW_SRB_APPEND + | luw_srb_flags_t_LUW_SRB_ALLOC + | luw_srb_flags_t_LUW_SRB_FULL_SIZE, + ); + } else { + luw_req_buf_append(ctx, addr); + } + + upload_reflector(ctx); + } + + return 0; +} -- cgit