diff options
48 files changed, 4307 insertions, 0 deletions
diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..45ec515 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.c diff=cpp +*.h diff=cpp diff --git a/.github/workflows/build_tests.yaml b/.github/workflows/build_tests.yaml new file mode 100644 index 0000000..f77df2c --- /dev/null +++ b/.github/workflows/build_tests.yaml @@ -0,0 +1,41 @@ +name: Builds + +on: + push: + branches: preview + paths: + - Makefile + - 'examples/**' + - 'src/**' + - '.github/workflows/build_tests.yaml' + pull_request: + branches: preview + paths: + - Makefile + - 'examples/**' + - 'src/**' + - '.github/workflows/build_tests.yaml' + +jobs: + # GitHub Currently only supports running directly on Ubuntu, + # for any other Linux we need to use a container. + + fedora-rawhide: + runs-on: ubuntu-latest + + container: + image: fedora:rawhide + + steps: + - name: Install tools/deps + run: | + dnf -y install git wget clang llvm compiler-rt lld make bindgen-cli cargo rust rust-std-static-wasm32-unknown-unknown rust-std-static-wasm32-wasi + wget -O- https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-20/libclang_rt.builtins-wasm32-wasi-20.0.tar.gz | tar --strip-components=1 -xvzf - -C $(dirname $(rpm -ql compiler-rt | grep lib/libclang_rt.builtins-x86_64.a)) + wget -O- https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-20/wasi-sysroot-20.0.tar.gz | tar -xvzf - -C ${RUNNER_TEMP} + + - uses: actions/checkout@v3 + with: + fetch-depth: "0" + + - name: make + run: make WASI_SYSROOT=${RUNNER_TEMP}/wasi-sysroot V=1 E=1 all diff --git a/.github/workflows/check-whitespace.yaml b/.github/workflows/check-whitespace.yaml new file mode 100644 index 0000000..3f99a99 --- /dev/null +++ b/.github/workflows/check-whitespace.yaml @@ -0,0 +1,49 @@ +name: check-whitespace + +# Get the repo with the commits(+1) in the series. +# Process `git log --check` output to extract just the check errors. +# Add a comment to the pull request with the check errors. + +on: + pull_request: + types: [ opened, synchronize ] + +jobs: + check-whitespace: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: git log --check + id: check_out + run: | + log= + commit= + while read dash etc + do + case "${dash}" in + "---") + commit="${etc}" + ;; + "") + ;; + *) + if test -n "${commit}" + then + log="${log}\n${commit}" + echo "" + echo "--- ${commit}" + fi + commit= + log="${log}\n${dash} ${etc}" + echo "${dash} ${etc}" + ;; + esac + done <<< $(git log --check --pretty=format:"--- %h %s" ${{github.event.pull_request.base.sha}}..) + + if test -n "${log}" + then + exit 2 + fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..50996ec --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.swp +*~ + +*.o +*.gch + +tags diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..12f259b --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,2 @@ +max_width = 80 +#binop_separator = "Back" diff --git a/API-C.md b/API-C.md new file mode 100644 index 0000000..fa04acb --- /dev/null +++ b/API-C.md @@ -0,0 +1,962 @@ +# libunit-wasm C API + +C Library for creating WebAssembly modules for use with NGINX Unit. + +```C +#include <unit/unit-wasm.h> +``` + +1. [libunit-wasm C API](#libunit-wasm-c-api) +2. [Macros](#macros) + * [Version](#version) + * [Misc](#misc) +3. [Types](#types) +4. [Enums](#enums) +5. [Structs](#structs) +6. [Function Handlers](#function-handlers) + * [Optional](#optional) + - [luw_module_init_handler](#luw_module_init_handler) + - [luw_module_end_handler](#luw-_module_end_handler) + - [luw_request_init_handler](#luw_request_init_handler) + - [luw_request_end_handler](#luw_request_end_handler) + - [luw_response_end_handler](#luw_response_end_handler) + * [Required](#required) + - [luw_request_handler](#luw_request_handler) + - [luw_free_handler](#luw_free_handler) + - [luw_malloc_handler](#luw_malloc_handler) +7. [Functions](#functions) + * [luw_init_ctx](#luw_init_ctx) + * [luw_set_req_buf](#luw_set_req_buf) + * [luw_get_http_path](#luw_get_http_path) + * [luw_get_http_method](#luw_get_http_method) + * [luw_get_http_version](#luw_get_http_version) + * [luw_get_http_query](#luw_get_http_query) + * [luw_get_http_remote](#luw_get_http_remote) + * [luw_get_http_local_addr](#luw_get_http_local_addr) + * [luw_get_http_local_port](#luw_get_http_local_port) + * [luw_get_http_server_name](#luw_get_http_server_name) + * [luw_get_http_content](#luw_get_http_content) + * [luw_get_http_content_len](#luw_get_http_content_len) + * [luw_get_http_content_sent](#luw_get_http_content_sent) + * [luw_http_is_tls](#luw_http_is_tls) + * [luw_http_hdr_iter](#luw_http_hdr_iter) + * [luw_http_hdr_get_value](#luw_http_hdr_get_value) + * [luw_get_response_data_size](#luw_get_response_data_size) + * [luw_mem_writep](#luw_mem_writep) + * [luw_mem_writep_data](#luw_mem_writep_data) + * [luw_req_buf_append](#luw_req_buf_append) + * [luw_mem_fill_buf_from_req](#luw_mem_fill_buf_from_req) + * [luw_mem_reset](#luw_mem_reset) + * [luw_http_send_response](#luw_http_send_response) + * [luw_http_init_headers](#luw_http_init_headers) + * [luw_http_add_header](#luw_http_add_header) + * [luw_http_send_headers](#luw_http_send_headers) + * [luw_http_response_end](#luw_http_response_end) + * [luw_mem_get_init_size](#luw_mem_get_init_size) + * [luw_foreach_http_hdr](#luw_foreach_http_hdr) +8. [Misc. Functions](#misc-functions) + * [luw_malloc](#luw_malloc) + * [luw_free](#luw_free) + +## Macros + +### Version + +```C +#define LUW_VERSION_MAJOR M +#define LUW_VERSION_MINOR m +#define LUW_VERSION_PATCH p +``` + +```C +/* Version number in hex 0xMMmmpp00 */ +#define LUW_VERSION_NUMBER \ + ( (LUW_VERSION_MAJOR << 24) | \ + (LUW_VERSION_MINOR << 16) | \ + (LUW_VERSION_PATCH << 8) ) +``` + +### Misc + +```C +#define __luw_export_name(name) __attribute__((export_name(name))) +``` + +```C +#define __luw_unused __attribute__((unused)) +#define __luw_maybe_unused __luw_unused +``` + +```C +#define luw_foreach_http_hdr(ctx, iter, name, value) \ + for (iter = ctx.req->fields, \ + name = (const char *)ctx.req + iter->name_off; \ + (iter < (ctx.req->fields + ctx.req->nr_fields)) && \ + (value = (const char *)ctx.req + iter->value_off); \ + iter++, name = (const char *)ctx.req + iter->name_off) +``` + +## Types + +```C +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; +``` + +## Enums + +```C +typedef enum { + LUW_SRB_NONE = 0x00, + LUW_SRB_APPEND = 0x01, + LUW_SRB_ALLOC = 0x02, + LUW_SRB_FULL_SIZE = 0x04, +} luw_srb_flags_t; +#define LUW_SRB_FLAGS_ALL \ + (LUW_SRB_NONE|LUW_SRB_APPEND|LUW_SRB_ALLOC|LUW_SRB_FULL_SIZE +``` + +## Structs + +```C +struct luw_hdr_field { + u32 name_off; + u32 name_len; + u32 value_off; + u32 value_len; +}; +``` + +```C +struct luw_req { + u32 method_off; + u32 method_len; + u32 version_off; + u32 version_len; + u32 path_off; + u32 path_len; + u32 query_off; + u32 query_len; + u32 remote_off; + u32 remote_len; + u32 local_addr_off; + u32 local_addr_len; + u32 local_port_off; + u32 local_port_len; + u32 server_name_off; + u32 server_name_len; + + u32 content_off; + u32 content_len; + u32 content_sent; + u32 total_content_sent; + + u32 request_size; + + u32 nr_fields; + + u32 tls; + + struct luw_hdr_field fields[]; +}; +``` + +```C +struct luw_resp { + u32 size; + + u8 data[]; +}; +``` + +```C +struct luw_resp_hdr { + u32 nr_fields; + + struct luw_hdr_field fields[]; +}; +``` + +```C +typedef struct { + /* pointer to the shared memory */ + u8 *addr; + + /* points to the end of ctx->resp->data */ + u8 *mem; + + /* struct luw_req representation of the shared memory */ + struct luw_req *req; + + /* struct luw_resp representation of the shared memory */ + struct luw_resp *resp; + + /* struct luw_resp_hdr representation of the shared memory */ + struct luw_resp_hdr *resp_hdr; + + /* offset to where the struct resp starts in the shared memory */ + size_t resp_offset; + + /* points to the external buffer used for a copy of the request */ + u8 *req_buf; + + /* points to the end of the fields array in struct luw_resp_hdr */ + u8 *hdrp; + + /* points to the end of ctx->req_buf */ + u8 *reqp; +} luw_ctx_t; +``` + +```C +typedef struct luw_hdr_field luw_http_hdr_iter_t; +``` + +## Function Handlers + +These functions are exported from the WebAssembly module and are called from +the WebAssembly runtime (the Unit WebAssembly language module in this case). + +There are two types of handlers; required & optional. + +luw_request_handler(), luw_malloc_handler() & luw_free_handler() are required +with the rest being optional. + +libunit-wasm includes exports for these handlers and some default +implementations. + +These functions are defined as _weak_ symbols and so if a developer writes +their own function of the same name, that will take precedence. + +However, developers are under no obligation to use these and can create their +own with any (valid) names they like. + +Whatever names developers choose, they are specified in the Unit config. + +## Required + +#### luw_request_handler + +```C +__attribute__((export_name("luw_request_handler"), __weak__)) +int luw_request_handler(u8 *addr); +``` + +This is called by Unit during a request. It may be called multiple times for +a single HTTP request if there is more request data than the available memory +for host <--> module communications. + +You will need to provide your own implementation of this function. + +It receives the base address of the shared memory. Essentially what is +returned by luw_malloc_handler(). + +This memory will contain a *struct luw_req*. + +It returns an int, this is currently ignored but will likely be used to +indicate a HTTP status code. + +#### luw_malloc_handler + +```C +__attribute__((export_name("luw_malloc_handler"), __weak__)) +u32 luw_malloc_handler(size_t size); +``` + +This is called by Unit when it loads the WebAssembly language module. This +provides the shared memory used for host <--> module communications. + +It receives the desired size of the memory, which is currently +NXT_WASM_MEM_SIZE + NXT_WASM_PAGE_SIZE. + +However calls to luw_mem_get_init_size() will return just NXT_WASM_MEM_SIZE +(which is currently 32MiB). The extra NXT_WASM_PAGE_SIZE is to cater for +structure sizes in the response so developers can generally assume they have +the full NXT_WASM_MEM_SIZE for their data. + +A default implementation of this function is provided ready for use that +calls malloc(3). + +#### luw_free_handler + +```C +__attribute__((export_name("luw_free_handler"), __weak__)) +void luw_free_handler(u32 addr); +``` + +This is called by Unit when it shuts down the WebAssembly language module and +free's the memory previously allocated by luw_malloc_handler(). + +It receives the address of the memory to free. + +An implementation of this function is provided ready for use that calls +free(3), in which case it receives the address that was previously returned +by luw_malloc_handler(). + +### Optional + +#### luw_module_init_handler + +```C +__attribute__((export_name("luw_module_init_handler"), __weak__)) +void luw_module_init_handler(void); +``` + +This is called by Unit when it loads the WebAssembly language module. + +A default dummy function is provided. If this handler is not required, there +is no need to specify it in the Unit config. + +#### luw_module_end_handler + +```C +__attribute__((export_name("luw_module_end_handler"), __weak__)) +void luw_module_end_handler(void); +``` + +This is called by Unit when it shuts down the WebAssembly language module. + +A default dummy function is provided. If this handler is not required, there +is no need to specify it in the Unit config. + +#### luw_request_init_handler + +```C +__attribute__((export_name("luw_request_init_handler"), __weak__)) +void luw_request_init_handler(void); +``` + +This is called by Unit at the start of nxt_wasm_request_handler(), i.e at the +start of a new request. + +A default dummy function is provided. If this handler is not required, there +is no need to specify it in the Unit config. + +#### luw_request_end_handler + +```C +__attribute__((export_name("luw_request_end_handler"), __weak__)) +void luw_request_end_handler(void); +``` + +This is called by Unit at the end of nxt_wasm_request_handler(), i.e at the +end of a request. + +A default dummy function is provided. If this handler is not required, there +is no need to specify it in the Unit config. + +#### luw_response_end_handler + +```C +__attribute__((export_name("luw_response_end_handler"), __weak__)) +void luw_response_end_handler(void); +``` + +This is called by Unit after luw_http_response_end() has been called. + +A default dummy function is provided. If this handler is not required, there +is no need to specify it in the Unit config. + +## Functions + +### luw_init_ctx + +```C +void luw_init_ctx(luw_ctx_t *ctx, u8 *addr, size_t offset); +``` + +This function sets up a *luw_ctx_t* context structure, this contains stuff +required all throughout the API. It's a typedef for opaqueness and you should +not in general be concerned with its contents. + +It take a pointer to a stack allocated luw_ctx_t, this will be zeroed and +have various members initialised. + +**addr** is a pointer to the shared memory as passed into luw_request_handler(). + +**offset** is where in the shared memory it should start writing the response. + +#### A quick word about memory + +The way the Unit WebAssembly language module (the host/runtime) and the +WebAssembly module you want to write (the guest) communicate is via a chunk +of shared memory. + +This shared memory is simply the modules (guest) address space from which we +can allocate a chunk. How this memory is laid out varies on how the module +is built. + +With clang/linker flags of -Wl,--stack-first -Wl,-z,stack-size=$((8*1024*1024)) +we get a memory layout something like + +``` + |----------------------------------------------------------------------| + | | | | + | <-- Stack | Global Data | Heap --> | + | | | | + |----------------------------------------------------------------------| + 0 0x800000 0x100000000 + + WebAssembly Module Linear Memory / Process Memory Layout +``` + +(The above is assuming _--target=wasm32-wasi_, i.e 32bit) + +A chunk of memory from the heap is allocated at Unit WebAssembly language +module startup. + +We currently use this same chunk of memory for both requests and responses. +This means that depending on what you're doing, you'll want to take a copy +of the request (and remember luw_request_handler() may be called multiple +times for a single http request). + +That will be covered in more detail by the next function, luw_set_req_buf(). + +Now back to _offset_, it may be convenient to put the response headers at the +beginning of this memory and then put the response after it, rather than +doing the headers and then doing the response as separate steps, if the +headers depends on some aspect of the response, its size for example and +Content-Length. + +Example + +```C +luw_ctx_t ctx; +/* ... */ +luw_init_ctx(&ctx, addr, 4096 /* Response offset */); +``` + +### luw_set_req_buf + +```C +int luw_set_req_buf(luw_ctx_t *ctx, u8 **buf, unsigned long flags); +``` + +This function is used to take a copy of the request buffer (as discussed +above). + +This takes a previously initialised (with luw_init_ctx()) luw_ctx_t. + +**buf** is a buffer where the request data will written. + +**flags** can be some combination (OR'd) of the following + +**LUW_SRB_NONE** + +No specific action to be performed. It will simply copy the request data +into the specified buffer. + +**LUW_SRB_APPEND** + +Sets up append mode whereby multiple successive requests will be appended +to the specified buffer. + +The first request will have all its metadata copied. Subsequent requests +will _only_ have the actual body data appended. + +**LUW_SRB_ALLOC** + +Allocate memory for the specified buffer. + +**LUW_SRB_FULL_SIZE** + +Used in conjunction with *LUW_SRB_ALLOC*. By default only +*ctx->req->request_size* is allocated. If this flag is present it says to +allocate memory for the _entire_ request that will eventually be sent. + +Example + +```C +static u8 *request_buf; +*/ ... */ +int luw_request_handler(u8 *addr) +{ + if (!request_buf) { + luw_init_ctx(&ctx, addr, 0); + /* + * 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); + } + + /* operate on the request (ctx) */ + + return 0; +} +``` + +That example is taken from the [luw-upload-reflector.c](https://github.com/nginx/unit-wasm/blob/master/examples/c/luw-upload-reflector.c) demo module. For a +simpler example see [luw-echo-request.c](https://github.com/nginx/unit-wasm/blob/master/examples/c/luw-echo-request.c) + +### luw_get_http_path + +```C +const char *luw_get_http_path(const luw_ctx_t *ctx); +``` + +This function returns a pointer to the HTTP request path. + +E.g + +Given a request of +``` +http://localhost:8080/echo/?q=a +``` +this function will return +``` +/echo/?q=a +``` + +### luw_get_http_method + +```C +const char *luw_get_http_method(const luw_ctx_t *ctx); +``` + +This function returns a pointer to the HTTP method. + +E.g + +``` +GET +``` + +### luw_get_http_version + +```C +const char *luw_get_http_version(const luw_ctx_t *ctx); +``` + +This function returns a pointer to the HTTP version. + +E.g + +``` +1.1 +``` + +### luw_get_http_query + +```C +const char *luw_get_http_query(const luw_ctx_t *ctx); +``` + +This function returns a pointer to the query string (empty string for no query +string). + +E.g + +Given a request of +``` +http://localhost:8080/echo/?q=a +``` +this function will return +``` +q=a +``` + +### luw_get_http_remote + +```C +const char *luw_get_http_remote(const luw_ctx_t *ctx); +``` + +This function returns a pointer to the remote/client/peer address. + +E.g + +``` +2001:db8::f00 +``` + +### luw_get_http_local_addr + +```C +const char *luw_get_http_local_addr(const luw_ctx_t *ctx); +``` + +This function returns a pointer to the local/server address. + +E.g + +``` +2001:db8::1 +``` + +### luw_get_http_local_port + +```C +const char *luw_get_http_local_port(const luw_ctx_t *ctx); +``` + +This function returns a pointer to the local/server port. + +E.g + +``` +443 +``` + +### luw_get_http_server_name + +```C +const char *luw_get_http_server_name(const luw_ctx_t *ctx); +``` + +This function returns a pointer to the local/server name. + +E.g + +``` +www.example.com +``` + +### luw_get_http_content + +```C +const u8 *luw_get_http_content(const luw_ctx_t *ctx); +``` + +This function returns a pointer to the start of the request body. + +### luw_get_http_content_len + +```C +size_t luw_get_http_content_len(const luw_ctx_t *ctx); +``` + +This function returns the size of the overall content. I.e Content-Length. + + +### luw_get_http_content_sent + +```C +size_t luw_get_http_content_sent(const luw_ctx_t *ctx); +``` + +This function returns the length of the content that was sent to the +WebAssembly module in _this_ request. Remember, a single HTTP request may be +split over several calls to luw_request_handler(). + +### luw_http_is_tls + +```C +bool luw_http_is_tls(const luw_ctx_t *ctx); +``` + +This function returns _true_ if the connection to Unit was made over TLS. + +### luw_http_hdr_iter + +```C +void luw_http_hdr_iter(luw_ctx_t *ctx, + bool (*luw_http_hdr_iter_func)(luw_ctx_t *ctx, + const char *name, + const char *value, + void *data), + void *user_data) +``` + +This function allows to iterate over the HTTP headers. For each header it +will call the given luw_http_hdr_iter_func() function whose prototype is + +```C +bool luw_http_hdr_iter_func(luw_ctx_t *ctx, + const char *name, const char *value, void *data); +``` + +You may call this function whatever you like. For each header it will be +passed the *luw_ctx_t*, the header name, its value and a user specified +pointer if any, can be NULL. + +Returning _true_ from this function will cause the iteration process to +continue, returning _false_ will terminate it. + +Example + +```C +static bool hdr_iter_func(luw_ctx_t *ctx, const char *name, const char *value, + void *user_data __luw_unused) +{ + /* Do something with name & value */ + + /* Continue iteration or return false to stop */ + return true; +} + +/* ... * + +luw_http_hdr_iter(&ctx, hdr_iter_func, NULL); +``` + +### luw_http_hdr_get_value + +```C +const char *luw_http_hdr_get_value(luw_ctx_t *ctx, const char *hdr); +``` + +Given a HTTP header _hdr_ this function will look it up in the request and +return its value if found, otherwise _NULL_. + +The lookup is done case insensitively. + +### luw_get_response_data_size + +```C +size_t luw_get_response_data_size(const luw_ctx_t *ctx); +``` + +This function returns the size of the response data written to memory. + +### luw_mem_writep + +```C +__attribute__((__format__(printf, 2, 3))) +int luw_mem_writep(luw_ctx_t *ctx, const char *fmt, ...); +``` + +This function is a cross between vasprintf(3) and mempcpy(3). + +It takes a format argument and zero or more arguments that will be +substituted into the format string. + +It then appends this formatted string to the memory. Note this string will +_not_ be nul terminated. Unit does not expect this response data to be nul +terminated and we track the size of the response and return that to Unit. + +This function returns -1 on error or the length of the string written. + +### luw_mem_writep_data + +```C +size_t luw_mem_writep_data(luw_ctx_t *ctx, const u8 *src, size_t size); +``` + +This function just appends _size_ bytes from _src_ to the response. + +It returns the new size of the response. + +### luw_req_buf_append + +```C +void luw_req_buf_append(luw_ctx_t *ctx, const u8 *src); +``` + +This function appends the request data contained in _src_ to the previously +setup *request_buffer* with luw_set_req_buf(). + +This function would be used after an initial request to append the data from +subsequent requests to the request_buffer. + +Example + +```C +int luw_request_handler(u8 *addr) +{ + if (!request_buf) { + luw_init_ctx(&ctx, addr, 0); + /* + * 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); + } + + /* Do something with the request (ctx) */ + + return 0; +} +``` + +### luw_mem_fill_buf_from_req + +```C +size_t luw_mem_fill_buf_from_req(luw_ctx_t *ctx, size_t from); +``` + +This is a convenience function to fill the response buffer with data from +the request buffer. + +_from_ is basically the offset in the request_buffer where to start copying +data from. + +Example + +```C +/* ... */ +write_bytes = luw_mem_fill_buf_from_req(ctx, total_response_sent); +total_response_sent += write_bytes; +/* ... */ +``` + +This is taken from the [luw-upload-reflector.c](https://github.com/nginx/unit-wasm/blob/master/examples/c/luw-upload-reflector.c) demo module. + +In this case we build up a request_buffer on each call of +luw_request_handler(), so total_response_sent grows each time by how much data +was sent in _that_ request. + +Here are are sending data back to the client after each time we receive it to +demonstrate the interleaving of requests and responses from the WebAssembly +module during a single http request. + +This function returns the number of bytes written to the response buffer. + +### luw_mem_reset + +```C +void luw_mem_reset(luw_ctx_t *ctx); +``` + +This function resets the response buffer size and the number of response +headers back to 0. + +### luw_http_send_response + +```C +void luw_http_send_response(const luw_ctx_t *ctx); +``` + +This function calls into Unit to send the response buffer back. + +### luw_http_init_headers + +```C +void luw_http_init_headers(luw_ctx_t *ctx, size_t nr, size_t offset); +``` + +This function is used in the preparation of sending back response headers. + +_nr_ is the number of headers we are sending. + +_offset_ is the offset into the response buffer where we are placing these +headers. This will usually be 0. + +Example + +```C +luw_http_init_headers(ctx, 2, 0); +``` + +### luw_http_add_header + +```C +void luw_http_add_header(luw_ctx_t *ctx, u16 idx, const char *name, + const char *value); +``` + +This function is used to add a header to the response. + +_idx_ is the index (starting at 0) of the header we are adding. + +_name_ is the name of the header. + +_value_ is the value of the header. + +Example + +```C +char clen[32]; +/* ... */ +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 + +```C +void luw_http_send_headers(const luw_ctx_t *ctx); +``` + +This function calls into Unit and triggers the sending of the response +headers. + +### luw_http_response_end + +```C +void luw_http_response_end(void); +``` + +This function calls into Unit and tells it this is the end of the response +which will trigger Unit to send it to the client. + +### luw_mem_get_init_size + +```C +u32 luw_mem_get_init_size(void); +``` + +This function calls into Unit to get the size of the shared memory. This is +the amount of memory you should assume you have for creating responses. +Remember you can create multiple responses before calling +luw_http_response_end(). + +### luw_foreach_http_hdr + +```C +void luw_foreach_http_hdr(luw_ctx_t ctx, luw_http_hdr_iter_t *iter, + const char *name, const char *value) +``` + +Defined as a macro, this is used to iterate over the HTTP header fields. + +It takes a _luw_ctx_t *_ and a _luw_http_hdr_iter_t *_ and returns pointers +to the field name and value. + +Example + +```C +luw_ctx_t ctx; +luw_http_hdr_iter_t *iter; +const char *name; +const char *value; +/* ... */ +luw_foreach_http_hdr(ctx, iter, name, value) { + printf("Field name : %s, field value : %s\n", name, value); + /* do something else with name & value */ +} +``` + +## Misc. Functions + +The following functions are convenience wrappers for the Rust bindings and +should **not** be used directly. + +### luw_malloc + +```C +void *luw_malloc(size_t size); +``` + +Straight wrapper for malloc(3). + +### luw_free + +```C +void luw_free(void *ptr); +``` + +Straight wrapper for free(3). diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..aea287f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the moderation team at nginx-oss-community@f5.com. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, +available at <https://www.contributor-covenant.org/version/1/4/code-of-conduct.html> + +For answers to common questions about this code of conduct, see +<https://www.contributor-covenant.org/faq> diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..19ead6e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,83 @@ +# Contributing Guidelines + +The following is a set of guidelines for contributing to unit-wasm. We do +appreciate that you are considering contributing! + +## Table Of Contents + +- [Getting Started](#getting-started) +- [Ask a Question](#ask-a-question) +- [Contributing](#contributing) +- [Git Style Guide](#git-style-guide) + + +## Getting Started + +Check out the [README](README.md). + + +## Ask a Question + +Please open an [issue](https://github.com/nginx/unit-wasm/issues/new) on +GitHub with the label `question`. You can also ask a question on +[Slack](https://nginxcommunity.slack.com) or the NGINX Unit mailing list, +unit@nginx.org (subscribe +[here](https://mailman.nginx.org/mailman3/lists/unit.nginx.org/)). + + +## Contributing + +### Report a Bug + +Ensure the bug was not already reported by searching on GitHub under +[Issues](https://github.com/nginx/unit-wasm/issues). + +If the bug is a potential security vulnerability, please report using our +[security policy](https://unit.nginx.org/troubleshooting/#getting-support). + +To report a non-security bug, open an +[issue](https://github.com/nginx/unit-wasm/issues/new) on GitHub with the +label `bug`. Be sure to include a title and clear description, as much +relevant information as possible, and a code sample or an executable test +case showing the expected behavior that doesn't occur. + + +### Suggest an Enhancement + +To suggest an enhancement, open an +[issue](https://github.com/nginx/unit/issues/new) on GitHub with the label +`enhancement`. Please do this before implementing a new feature to discuss +the feature first. + + +### Open a Pull Request + +Clone the repo, create a branch, and submit a PR when your changes are tested +and ready for review. Again, if you'd like to implement a new feature, please +consider creating a feature request issue first to start a discussion about +the feature. + + +## Git Style Guide + +- Split your work into multiple commits is necessary. Each commit should make + one logical change. I.e don't mix code re-formatting with a fix in the same + commit. + +- Subject lines should be short (around 50 characters, not a hard rule) and + concisely describe the change. + +- The commit message body should be limited to 72 character lines. + +- You can use subject line prefixes for commits that affect a specific + portion of the code; examples include "libunit-wasm:" and "rust-bindings:". + +- Reference issues and PRs at the end of the commit messages, e.g if the + commit remedies a GitHub issue add a tag like + + Closes: <https://github.com/nginx/unit-wasm/issues/NNN> + + If the commit fixes an issue introduced in a previous commit use the "Fixes" + tag to reference it, e.g + + Fixes: abbrev commit id ("Commit subject line") diff --git a/HOWTO.md b/HOWTO.md new file mode 100644 index 0000000..c2d2ae7 --- /dev/null +++ b/HOWTO.md @@ -0,0 +1,203 @@ +Trying it out +============= + +For a quick and simple 'hello world' experience, you can use docker with + +```shell +$ make docker +``` + +which will create two images: + + 1. `unit:wasm` (based on the Docker Official image, with Wasm Module) + 2. `unit:demo-wasm` (based on the Wasm image, with demo application) + +Manual build instructions below. + +## Prerequisites and Assumptions + +You will need: + * Modern Linux platform (might work on others, not yet tested). + * Ability to build Unit from source. + If you haven't done this before, please first run through the +[Building From Source how-to guide](https://unit.nginx.org/howto/source/). + * Additional build tools (required for the demo Wasm Module) + - clang + - llvm + - lld + +## Building the Wasm Language Module + +0. Do a test build of Unit from source ([see docs](https://unit.nginx.org/howto/source/)) with this PR/patch applied. The following steps assume you're +starting in the `unit` directory and used `./configure --prefix=$PWD/build`. + +2. Download and extract the Wasmtime C API (newer versions may or may not +work). Notice that we use `$(arch)` to substitute-in the appropriate CPU +architecture. This works for **x86_64** and **aarch64** (ARM) platforms. +``` +wget -O- https://github.com/bytecodealliance/wasmtime/releases/download/v11.0.0/wasmtime-v11.0.0-$(arch)-linux-c-api.tar.xz | tar Jxfv - +``` + +3. Configure the Wasm Language Module for Unit +``` +./configure wasm --include-path=$PWD/wasmtime-v11.0.0-$(arch)-linux-c-api/include \ + --lib-path=$PWD/wasmtime-v11.0.0-$(arch)-linux-c-api/lib --rpath +``` + +4. Build the Wasm Language Module +``` +make +``` + +5. Test that **unitd** Can Load the Language Module + +Run `unitd` in the foreground (attached to the console) to check that Unit +can discover and load the `wasm` Language Module at startup. You should see +console output similar to this: +``` +$ $PWD/build/sbin/unitd --no-daemon --log /dev/stderr +2023/06/15 11:29:31 [info] 1#1 unit 1.31.0 started +2023/06/15 11:29:31 [info] 43#43 discovery started +2023/06/15 11:29:31 [notice] 43#43 module: wasm 0.1 "/path/to/modules/wasm.unit.so" +``` + +## Building the demo application + +From a suitable directory... + +Clone the [unit-wasm](https://github.com/nginx/unit-wasm) repository + +```shell +$ git clone https://github.com/nginx/unit-wasm.git +``` + +Download and extract the wasi-sysroot from the [WASI SDK](https://github.com/WebAssembly/wasi-sdk) + +```shell +wget -O- https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-20/wasi-sysroot-20.0.tar.gz | tar zxfv - +``` + +Next Compile the C demo Wasm Modules to `.wasm` files. This requires at least +the following; make and clang, llvm, compiler-rt, and lld from LLVM 8.0+ + +```shell +$ cd unit-wasm +$ make WASI_SYSROOT=../wasi-sysroot examples +``` + +If the above fails like + +``` +wasm-ld: error: cannot open /usr/lib/llvm-11/lib/clang/11.0.1/lib/wasi/libclang_rt.builtins-wasm32.a: No such file or directory +clang: error: linker command failed with exit code 1 (use -v to see invocation) +``` +Then you need to download the wasm32 clang runtime and copy it into the +location mentioned in the error message. + +E.g + +In the above case we would untar +*libclang_rt.builtins-wasm32-wasi-20.0.tar.gz* into +*/usr/lib/llvm-11/lib/clang/11.0.1/* + +On Fedora this would be more like */usr/lib64/clang/16/* + +Adjust the tar '-C ...' option accordingly below... + +```shell +wget -O- https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-20/libclang_rt.builtins-wasm32-wasi-20.0.tar.gz | sudo tar -xvzf - -C /usr/lib/llvm-11/lib/clang/11.0.1 +``` + +Then try again... + +If everything built OK then you should have the following two WASM modules + +``` +examples/c/luw-echo-request.wasm +examples/c/luw-upload-reflector.wasm +``` + +## Configure Unit to run the demo application + +```json + { + "listeners": { + "[::1]:8080": { + "pass": "routes" + } + }, + + "settings": { + "http": { + "max_body_size": 1073741824 + } + }, + + "routes": [ + { + "match": { + "uri": "/echo*" + }, + "action": { + "pass": "applications/luw-echo-request" + } + }, + { + "match": { + "uri": "/upload*" + }, + "action": { + "pass": "applications/luw-upload-reflector" + } + } + ], + + "applications": { + "luw-echo-request": { + "type": "wasm", + "module": "/path/to/unit-wasm/examples/c/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", + "/foo/bar" + ] + } + }, + "luw-upload-reflector": { + "type": "wasm", + "module": "/path/to/unit-wasm/examples/c/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" + } + } +} + +``` + +Apply the above configuration to the **/config** URI of Unit's Control API. +With the JSON in a file, you can use the CLI to apply it. +``` +cat conf.json | tools/unitc /config +``` + +The following messages should then appear in the Unit log file (or console if +running with `--no-daemon`). +``` +2023/07/26 13:28:14 [info] 182585#182585 "luw-echo-request" prototype started +2023/07/26 13:28:14 [info] 182590#182590 "luw-echo-request" application started +2023/07/26 13:28:14 [info] 182591#182591 "luw-upload-reflector" prototype started +2023/07/26 13:28:14 [info] 182596#182596 "luw-upload-reflector" application started +``` + +Now make a request to the demo application. +``` +curl http://localhost:8080/echo +``` @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..1a56cb0 --- /dev/null +++ b/Makefile @@ -0,0 +1,73 @@ +MAKE_OPTS = --no-print-directory + +.PHONY: libunit-wasm +libunit-wasm: + @echo "Building: libunit-wasm" + @$(MAKE) $(MAKE_OPTS) -C src/c + +.PHONY: examples +examples: libunit-wasm + @echo "Building: examples" + @$(MAKE) $(MAKE_OPTS) -C examples/c examples-luw + +.PHONY: examples-raw +examples-raw: libunit-wasm + @echo "Building: raw examples" + @$(MAKE) $(MAKE_OPTS) -C examples/c examples-raw + +.PHONY: rust +rust: libunit-wasm + @echo "Building: libunit-wasm-rust" + @$(MAKE) $(MAKE_OPTS) -C src/rust + +.PHONY: examples-rust +examples-rust: rust + @echo "Building: rust examples" + @$(MAKE) $(MAKE_OPTS) -C examples/rust + +.PHONY: all +all: libunit-wasm examples examples-raw rust examples-rust + +.PHONY: docker +docker: + docker build -t unit:wasm -f examples/docker/unit-wasm.Dockerfile . + docker build -t unit:demo-wasm -f examples/docker/demo-wasm.Dockerfile . + +.PHONY: clean +clean: + @echo "Cleaning: libunit-wasm" + @$(MAKE) $(MAKE_OPTS) -C src/c clean + @echo "Cleaning: rust" + @$(MAKE) $(MAKE_OPTS) -C src/rust clean + @echo "Cleaning: examples" + @$(MAKE) $(MAKE_OPTS) -C examples/c clean + @echo "Cleaning: rust examples" + @$(MAKE) $(MAKE_OPTS) -C examples/rust clean + +.PHONY: tags +tags: + @echo "Generating ctags..." + @ctags -R src/ examples/ + +.PHONY: help +help: + @echo "Available Targets:" + @echo " default / " + @echo " libunit-wasm - Builds libunit-wasm C library" + @echo " examples - Builds the above as well as C examples" + @echo " examples-raw - Builds raw (non libunit-wasm) C examples" + @echo " rust - Builds the libunit-wasm rust crate" + @echo " examples-rust _ Builds the above and rust examples" + @echo " all - Builds all the above" + @echo " docker - Builds demo docker images" + @echo " clean - Removes auto generated artifacts" + @echo " tags - Generate ctags" + @echo + @echo "Variables:" + @echo " make CC= - Specify compiler to use" + @echo " Defaults to clang" + @echo " make WASI_SYSROOT= - Specify the path to the WASI sysroot" + @echo " Defaults to /usr/wasm32-wasi" + @echo " make V=1 - Enables verbose output" + @echo " make D=1 - Enables debug builds (-O0)" + @echo " make E=1 - Enables Werror" diff --git a/README.md b/README.md new file mode 100644 index 0000000..6292a0d --- /dev/null +++ b/README.md @@ -0,0 +1,389 @@ +# C & Rust Library & Examples for Building WebAssembly Modules for NGINX Unit + +This provides a C library (lbunit-wasm) and a Rust crate based on that library +to aid in the creation of WebAssembly modules in C and Rust. + +It also has some demo WebAssembly modules written in C and Rust. + +1. [C & Rust Library & Examples for Building WebAssembly Modules for NGINX Unit](#c---rust-library---examples-for-building-webassembly-modules-for-nginx-unit) +2. [Repository Layout](#repository-layout) +3. [Quickstart in Developing Rust WebAssembly Modules for Unit](#quickstart-in-developing-rust-webassembly-modules-for-unit) +4. [Getting Started](#getting-started) + 1. [Requirements](#requirements) + * [wasi-sysroot](#wasi-sysroot) + * [libclang_rt.builtins-wasm32-wasi](#libclang_rtbuiltins-wasm32-wasi) + 2. [Building libunit-wasm and C Examples](#building-libunit-wasm-and-c-examples) + 3. [Building the Rust libunit-wasm Crate and Examples](#building-the-rust-libunit-wasm-crate-and-examples) + 4. [Using With Unit](#using-with-unit) +5. [Consuming the C Library](#consuming-the-c-library) +6. [License](#license) + +## Repository Layout + +**src/c** contains the main libunit-wasm library. + +**src/rust** contains the rust version of the above. + +**examples/c** contains some demo WebAssembly modules that show both the raw +interface to Unit (\*-raw.c) and also the use of libunit-wasm (luw-\*.c). + +**examples/rust** contains rust versions of the above C demo modules. + +**examples/docker** contains docker files for building Unit with WebAssembly +support and the C examples. + +## Quickstart in Developing Rust WebAssembly Modules for Unit + +1) Have a suitable rust/wasm environment setup, See +[Building the Rust libunit-wasm Crate and Examples](#building-the-rust-libunit-wasm-crate-and-examples) for some details + +2) Create a new rust project + +``` +$ cargo init --lib my-wasm-example +``` + +3) Add the [unit-wasm crate](https://crates.io/crates/unit-wasm) as dependency + +``` +$ cd my-wasm-example +$ cargo add unit-wasm +``` + +4) Create the following _Cargo.toml_ file + +``` +[package] +name = "my-wasm-example" +version = "0.1.0" +edition = "2021" + +[dependencies] +unit-wasm = { version = "0.1.0" } + +[lib] +crate-type = ["cdylib"] +``` + +5) Create an example application + +To do this you can simply take a copy of our echo-request demo in this +repository + +``` +$ wget -O src/lib.rs https://raw.githubusercontent.com/nginx/unit-wasm/master/examples/rust/echo-request/src/lib.rs +``` + +6) Built it! + +``` +$ cargo build --target wasm32-wasi +``` + +You should now have a *target/wasm32-wasi/debug/my_wasm_example.wasm* file +(yes, hyphens will be turned to underscores) + +You can now use this in Unit with the following application config snippet + +```JSON + "my-wasm-example": { + "type": "wasm", + "module": "/path/to/my-wasm-example/target/wasm32-wasi/debug/my_wasm_example.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" + }, +``` + +7) Enjoy! + +## Getting Started + +### Requirements + +To build the C library and demo modules you will need make, clang, llvm, +compiler-rt & lld 8.0+. (GCC does not currently have any support for +WebAssembly). + +#### wasi-sysroot + +This is essentially a C library (based at least partly on cloudlibc and musl +libc) for WebAssembly and is required for the _wasm32-wasi_ target. + +Distributions are starting to package this. On Fedora for example you can +install the + +``` +wasi-libc-devel +wasi-libc-static +``` + +packages. + +On FreeBSD you can install the + +``` +wasi-libc +``` + +package. + +The Makefiles will pick this up automatically. + +Where _wasi-sysroot_ is not available you can grab it from +[here](https://github.com/WebAssembly/wasi-sdk/releases). Just grab the latest +wasi-sysroot-VERSION.tar.gz tarball. + +Untar the wasi-sysroot package someplace. + +#### libclang_rt.builtins-wasm32-wasi + +You will probably also need to grab the latest +libclang\_rt.builtins-wasm32-wasi-VERSION.tar.gz tarball from the same +location and keep it handy. + +### Building libunit-wasm and C Examples + +Once you have the above sorted you can simply try doing + +``` +$ make WASI_SYSROOT=/path/to/wasi-sysroot examples +``` + +**NOTE:** + +The Makefiles will look for an already installed wasi-sysroot on Fedora & +FreeBSD, so you may not need to specify it as above. + +If you do, you can set the WASI\_SYSROOT environment variable in your shell so +you don't need to specify it here. + +This will attempt to build libunit-wasm and the two example WebAssembly +modules, _luw-echo-request.wasm_ & _luw-upload-reflector.wasm_. + +If the above fails (which currently there is a good chance it will) with an +error message like + +``` +wasm-ld: error: cannot open /usr/lib64/clang/16/lib/wasi/libclang_rt.builtins-wasm32.a: No such file or directory +clang: error: linker command failed with exit code 1 (use -v to see invocation) +``` + +Then this is where that other tarball you downloaded comes in. Extract the +*libclang\_rt.builtins-wasm32.a* from it and copy it into the location +mentioned in the above error message. + +Try the make command again... + +### Building the Rust libunit-wasm Crate and Examples + +To build the rust stuff you will of course need rust and also cargo and the +rust wasm/wasi stuff. On Fedora this is the relevant packages I have installed + +``` +cargo +rust +rust-std-static +rust-std-static-wasm32-unknown-unknown +rust-std-static-wasm32-wasi +``` + +Install with $PKGMGR. + +If you have also completed the above building of the C library and examples +you should now be good to go. + +``` +$ make examples-rust +``` + +If you need to specify the *WASI_SYSROOT*, specify it in the make command as +above. + +This will build the libunit-wasm rust crate and rust example modules. + +### Using With Unit + +Now that you have all the above built, you are now ready to test it out with +Unit. + +If you created both the C and rust examples you will now have the following +WebAssembly modules + +``` +examples/c/luw-echo-request.wasm +examples/c/luw-upload-reflector.wasm +examples/rust/echo-request/target/wasm32-wasi/debug/rust_echo_test.wasm +examples/rust/upload-reflector/target/wasm32-wasi/debug/rust_upload_reflector.wasm +``` + +We won't go into the details of building Unit from source and enabling the +Unit WebAssembly language module here (see the [HOWTO.md](https://github.com/nginx/unit-wasm/blob/master/HOWTO.md) in the repository root for more details) but will +instead assume you already have a Unit with the WebAssembly language module +already running. + +Create the following Unit config + +```JSON +{ + "listeners": { + "[::1]:8888": { + "pass": "routes" + } + }, + + "settings": { + "http": { + "max_body_size": 1073741824 + } + }, + + "routes": [ + { + "match": { + "uri": "/echo*" + }, + "action": { + "pass": "applications/luw-echo-request" + } + }, + { + "match": { + "uri": "/upload*" + }, + "action": { + "pass": "applications/luw-upload-reflector" + } + }, + { + "match": { + "uri": "/rust-echo*" + }, + "action": { + "pass": "applications/rust-echo-test" + } + }, + { + "match": { + "uri": "/rust-upload*" + }, + "action": { + "pass": "applications/rust-upload-reflector" + } + } + ], + + "applications": { + "luw-echo-request": { + "type": "wasm", + "module": "/path/to/unit-wasm/examples/c/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" + }, + "luw-upload-reflector": { + "type": "wasm", + "module": "/path/to/unit-wasm/examples/c/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" + }, + "rust-echo-test": { + "type": "wasm", + "module": "/path/to/unit-wasm/examples/rust/echo-request/target/wasm32-wasi/debug/rust_echo_test.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" + }, + "rust-upload-reflector": { + "type": "wasm", + "module": "/path/to/unit-wasm/examples/rust/upload-reflector/rust_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" + } + } +} +``` + +Load this config then you should be ready to try it. + +```shell +$ curl -X POST -d "Hello World" --cookie "mycookie=hmmm" http://localhost:8888/echo/?q=a + *** Welcome to WebAssembly on Unit! [libunit-wasm (0.1.0/0x00010000)] *** + +[Request Info] +REQUEST_PATH = /echo/?q=a +METHOD = POST +VERSION = HTTP/1.1 +QUERY = q=a +REMOTE = ::1 +LOCAL_ADDR = ::1 +LOCAL_PORT = 8080 +SERVER_NAME = localhost + +[Request Headers] +Host = localhost:8080 +User-Agent = curl/8.0.1 +Accept = */* +Cookie = mycookie=hmmm +Content-Length = 11 +Content-Type = application/x-www-form-urlencoded + +[POST data] +Hello World +``` + +```shell +$ curl -v -X POST --data-binary @audio.flac -H "Content-Type: audio/flac" http://localhost:8888/upload-reflector/ -o wasm-test.dat +... +> Content-Type: audio/flac +> Content-Length: 60406273 +... + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 115M 100 57.6M 100 57.6M 47.6M 47.6M 0:00:01 0:00:01 --:--:-- 95.2M +... +< Content-Type: audio/flac +< Content-Length: 60406273 +... +$ sha1sum audio.flac wasm-test.dat +ef5c9c228544b237022584a8ac4612005cd6263e audio.flac +ef5c9c228544b237022584a8ac4612005cd6263e wasm-test.dat +``` + +## Consuming the C Library + +If **unit/unit-wasm.h** and **libunit.a** are installed into standard +include/library directories then + +Include the libunit-wasm header file + +```C +.... +#include <unit/unit-wasm.h> +... +``` + +Link against libunit-wasm + +``` +$ clang ... -o myapp.wasm myapp.c -lunit-wasm +``` + +See [API-C.md](https://github.com/nginx/unit-wasm/blob/master/API-C.md) for an +overview of the API. + +## License + +This project is licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). 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 <stdio.h> +#include <stdlib.h> +#include <stdint.h> +#include <string.h> + +#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 <stdio.h> +#include <stdlib.h> +#include <stdbool.h> +#include <string.h> + +#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 <stdio.h> +#include <stdlib.h> + +#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 <stdio.h> +#include <string.h> + +#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 <stddef.h> +#include <stdint.h> + +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 <stdio.h> +#include <stdlib.h> +#include <string.h> + +#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; +} diff --git a/shared.mk b/shared.mk new file mode 100644 index 0000000..5c2e112 --- /dev/null +++ b/shared.mk @@ -0,0 +1,51 @@ +# Some common Makefile stuff + +# Look for wasi-sysroot in some common places, falling back +# to provided WASI_SYSROOT +ifneq ("$(wildcard /usr/wasm32-wasi)", "") + # Fedora + WASI_SYSROOT ?= /usr/wasm32-wasi +else ifneq ("$(wildcard /usr/local/share/wasi-sysroot)", "") + # FreeBSD + WASI_SYSROOT ?= /usr/local/share/wasi-sysroot +endif + +# By default compiler etc output is hidden, use +# make V=1 ... +# to show it +v = @ +ifeq ($V,1) + v = +endif + +# Optionally enable debugging builds with +# make D=1 ... +# -g is always used, this just changes the optimisation level. +# On GCC this would be -Og, however according to the clang-16(1) +# man page, -O0 'generates the most debuggable code'. +ifeq ($D,1) + CFLAGS += -O0 +else + CFLAGS += -O2 +endif + +# Optionally enable Werror with +# make E=1 ... +ifeq ($E,1) + CFLAGS += -Werror +endif + +# Pretty print compiler etc actions... +PP_CC = @echo ' CC ' +PP_AR = @echo ' AR ' +PP_CCLNK = @echo ' CCLNK ' +PP_GEN = @echo ' GEN ' + +CC = clang +CFLAGS += -Wall -Wextra -Wdeclaration-after-statement -Wvla \ + -Wmissing-prototypes -Wstrict-prototypes -Wold-style-definition \ + -Wimplicit-function-declaration -Wimplicit-int -Wint-conversion \ + -std=gnu11 -g -fno-common -fno-strict-aliasing \ + --target=wasm32-wasi --sysroot=$(WASI_SYSROOT) +LDFLAGS = -Wl,--no-entry,--export=__heap_base,--export=__data_end,--export=malloc,--export=free,--stack-first,-z,stack-size=$$((8*1024*1024)) \ + -mexec-model=reactor --rtlib=compiler-rt diff --git a/src/c/.gitignore b/src/c/.gitignore new file mode 100644 index 0000000..a676009 --- /dev/null +++ b/src/c/.gitignore @@ -0,0 +1 @@ +libunit-wasm.a diff --git a/src/c/Makefile b/src/c/Makefile new file mode 100644 index 0000000..83262f6 --- /dev/null +++ b/src/c/Makefile @@ -0,0 +1,21 @@ +include ../../shared.mk + +CFLAGS += -Iinclude + +SDIR = src/c + +TARGETS = libunit-wasm.o libunit-wasm.a + +.PHONY: libunit-wasm +libunit-wasm: $(TARGETS) + +libunit-wasm.o: libunit-wasm.c include/unit/unit-wasm.h + $(PP_CC) $(SDIR)/$@ + $(v)$(CC) $(CFLAGS) -fvisibility=hidden -c $< + +libunit-wasm.a: libunit-wasm.o + $(PP_AR) $(SDIR)/$@ + $(v)llvm-ar rcs $@ $< + +clean: + rm -f *.o *.a *.gch diff --git a/src/c/include/unit/unit-wasm.h b/src/c/include/unit/unit-wasm.h new file mode 100644 index 0000000..df3852a --- /dev/null +++ b/src/c/include/unit/unit-wasm.h @@ -0,0 +1,211 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * Copyright (C) Andrew Clayton + * Copyright (C) F5, Inc. + */ + +#ifndef _UNIT_WASM_H_ +#define _UNIT_WASM_H_ + +#include <stddef.h> +#include <stdint.h> +#include <stdbool.h> +#include <strings.h> + +#ifdef __cplusplus +extern "C" { +#endif + +#define LUW_VERSION_MAJOR 0 +#define LUW_VERSION_MINOR 1 +#define LUW_VERSION_PATCH 0 + +/* Version number in hex 0xMMmmpp00 */ +#define LUW_VERSION_NUMBER \ + ( (LUW_VERSION_MAJOR << 24) | \ + (LUW_VERSION_MINOR << 16) | \ + (LUW_VERSION_PATCH << 8) ) + +#define __luw_export_name(name) __attribute__((export_name(name))) + +#define __luw_unused __attribute__((unused)) +#define __luw_maybe_unused __luw_unused + +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; + +struct luw_hdr_field { + u32 name_off; + u32 name_len; + u32 value_off; + u32 value_len; +}; + +struct luw_req { + u32 method_off; + u32 method_len; + u32 version_off; + u32 version_len; + u32 path_off; + u32 path_len; + u32 query_off; + u32 query_len; + u32 remote_off; + u32 remote_len; + u32 local_addr_off; + u32 local_addr_len; + u32 local_port_off; + u32 local_port_len; + u32 server_name_off; + u32 server_name_len; + + u32 content_off; + u32 content_len; + u32 content_sent; + u32 total_content_sent; + + u32 request_size; + + u32 nr_fields; + + u32 tls; + + struct luw_hdr_field fields[]; +}; + +struct luw_resp { + u32 size; + + u8 data[]; +}; + +struct luw_resp_hdr { + u32 nr_fields; + + struct luw_hdr_field fields[]; +}; + +typedef struct { + /* pointer to the shared memory */ + u8 *addr; + + /* points to the end of ctx->resp->data */ + u8 *mem; + + /* struct luw_req representation of the shared memory */ + struct luw_req *req; + + /* struct luw_resp representation of the shared memory */ + struct luw_resp *resp; + + /* struct luw_resp_hdr represnetation of the shared memory */ + struct luw_resp_hdr *resp_hdr; + + /* offset to where the struct resp starts in the shared memory */ + size_t resp_offset; + + /* points to the external buffer used for a copy of the request */ + u8 *req_buf; + + /* points to the end of the fields array in struct luw_resp_hdr */ + u8 *hdrp; + + /* points to the end of ctx->req_buf */ + u8 *reqp; +} luw_ctx_t; + +typedef enum { + LUW_SRB_NONE = 0x00, + LUW_SRB_APPEND = 0x01, + LUW_SRB_ALLOC = 0x02, + LUW_SRB_FULL_SIZE = 0x04, +} luw_srb_flags_t; +#define LUW_SRB_FLAGS_ALL \ + (LUW_SRB_NONE|LUW_SRB_APPEND|LUW_SRB_ALLOC|LUW_SRB_FULL_SIZE) + +typedef struct luw_hdr_field luw_http_hdr_iter_t; + +#define luw_foreach_http_hdr(ctx, iter, name, value) \ + for (iter = ctx.req->fields, \ + name = (const char *)ctx.req + iter->name_off; \ + (iter < (ctx.req->fields + ctx.req->nr_fields)) && \ + (value = (const char *)ctx.req + iter->value_off); \ + iter++, name = (const char *)ctx.req + iter->name_off) + +/* Imported functions from the host/runtime */ +__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_headers"))) +void nxt_wasm_send_headers(u32 offset); +__attribute__((import_module("env"), import_name("nxt_wasm_send_response"))) +void nxt_wasm_send_response(u32 offset); + +extern void luw_module_init_handler(void); +extern void luw_module_end_handler(void); +extern void luw_request_init_handler(void); +extern void luw_request_end_handler(void); +extern void luw_response_end_handler(void); +extern int luw_request_handler(u8 *addr); +extern void luw_free_handler(u32 addr); +extern u32 luw_malloc_handler(size_t size); + +#pragma GCC visibility push(default) + +extern void luw_init_ctx(luw_ctx_t *ctx, u8 *addr, size_t offset); +extern void luw_destroy_ctx(const luw_ctx_t *ctx); +extern int luw_set_req_buf(luw_ctx_t *ctx, u8 **buf, unsigned long flags); +extern const char *luw_get_http_path(const luw_ctx_t *ctx); +extern const char *luw_get_http_method(const luw_ctx_t *ctx); +extern const char *luw_get_http_version(const luw_ctx_t *ctx); +extern const char *luw_get_http_query(const luw_ctx_t *ctx); +extern const char *luw_get_http_remote(const luw_ctx_t *ctx); +extern const char *luw_get_http_local_addr(const luw_ctx_t *ctx); +extern const char *luw_get_http_local_port(const luw_ctx_t *ctx); +extern const char *luw_get_http_server_name(const luw_ctx_t *ctx); +extern const u8 *luw_get_http_content(const luw_ctx_t *ctx); +extern size_t luw_get_http_content_len(const luw_ctx_t *ctx); +extern size_t luw_get_http_content_sent(const luw_ctx_t *ctx); +extern bool luw_http_is_tls(const luw_ctx_t *ctx); +extern void luw_http_hdr_iter(luw_ctx_t *ctx, + bool (*luw_http_hdr_iter_func)(luw_ctx_t *ctx, + const char *name, + const char *value, + void *data), + void *user_data); +extern const char *luw_http_hdr_get_value(luw_ctx_t *ctx, const char *hdr); +extern size_t luw_get_response_data_size(const luw_ctx_t *ctx); +extern int luw_mem_writep(luw_ctx_t *ctx, const char *fmt, ...); +extern size_t luw_mem_writep_data(luw_ctx_t *ctx, const u8 *src, size_t size); +extern void luw_req_buf_append(luw_ctx_t *ctx, const u8 *src); +extern size_t luw_mem_fill_buf_from_req(luw_ctx_t *ctx, size_t from); +extern void luw_mem_reset(luw_ctx_t *ctx); +extern void luw_http_send_response(const luw_ctx_t *ctx); +extern void luw_http_init_headers(luw_ctx_t *ctx, size_t nr, size_t offset); +extern void luw_http_add_header(luw_ctx_t *ctx, u16 idx, const char *name, + const char *value); +extern void luw_http_send_headers(const luw_ctx_t *ctx); +extern void luw_http_response_end(void); +extern u32 luw_mem_get_init_size(void); + +/* + * Convenience wrappers for the Rust bindings, not for general consumption. + */ +extern void *luw_malloc(size_t size); +extern void luw_free(void *ptr); + +#pragma GCC visibility pop + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* _UNIT_WASM_H_ */ diff --git a/src/c/libunit-wasm.c b/src/c/libunit-wasm.c new file mode 100644 index 0000000..1860fe3 --- /dev/null +++ b/src/c/libunit-wasm.c @@ -0,0 +1,381 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * Copyright (C) Andrew Clayton + * Copyright (C) F5, Inc. + */ + +#define _GNU_SOURCE + +#include <stddef.h> +#include <stdio.h> +#include <stdlib.h> +#include <stdbool.h> +#include <stdarg.h> +#include <string.h> +#include <errno.h> + +#include "unit/unit-wasm.h" + +/* + * Some handlers are required some are optional. + * + * They are defined as _weak_ symbols so they can be overridden + * in the module. + */ + +/* Optional Handlers */ +__attribute__((export_name("luw_module_init_handler"), __weak__)) +void luw_module_init_handler(void) +{ +} + +__attribute__((export_name("luw_module_end_handler"), __weak__)) +void luw_module_end_handler(void) +{ +} + +__attribute__((export_name("luw_request_init_handler"), __weak__)) +void luw_request_init_handler(void) +{ +} + +__attribute__((export_name("luw_request_end_handler"), __weak__)) +void luw_request_end_handler(void) +{ +} + +__attribute__((export_name("luw_response_end_handler"), __weak__)) +void luw_response_end_handler(void) +{ +} + +/* Required Handlers */ +__attribute__((export_name("luw_request_handler"), __weak__)) +int luw_request_handler(u8 *addr) +{ + (void)addr; + + return 0; +} + +__attribute__((export_name("luw_free_handler"), __weak__)) +void luw_free_handler(u32 addr) +{ + free((void *)addr); +} + +__attribute__((export_name("luw_malloc_handler"), __weak__)) +u32 luw_malloc_handler(size_t size) +{ + return (u32)malloc(size); +} + +void luw_init_ctx(luw_ctx_t *ctx, u8 *addr, size_t offset) +{ + *ctx = (luw_ctx_t){ }; + + ctx->addr = addr; + ctx->req = (struct luw_req *)addr; + ctx->resp = (struct luw_resp *)(addr + offset); + ctx->mem = ctx->resp->data; + ctx->resp_offset = offset; + ctx->resp->size = 0; + ctx->resp_hdr->nr_fields = 0; +} + +/* + * Allows to set an external buffer to be used as a copy for + * the request. + * + * The flags dictate a few behaviours + * + * LUW_SRB_NONE - No specific action to be performed. It will + * simply copy the request data into the specified + * buffer. + * + * LUW_SRB_APPEND - Sets up append mode whereby multiple successive + * requests will be appended to the specified buffer. + * + * The first request will have all its metadata + * copied. Subsequent requests will _only_ have the + * actual body data appended. + * + * LUW_SRB_ALLOC - Allocate memory for the specified buffer. + * + * LUW_SRB_FULL_SIZE - Used in conjunction with LUW_SRB_ALLOC. By + * default only ctx->req->request_size is + * allocated. If this flag is present it says to + * allocate memory for the _entire_ request that + * will eventually be sent. + */ +int luw_set_req_buf(luw_ctx_t *ctx, u8 **buf, unsigned long flags) +{ + size_t alloc_size; + size_t copy_bytes; + + /* Check for unknown flags */ + if (flags & ~LUW_SRB_FLAGS_ALL) { + errno = EINVAL; + return -1; + } + + /* Check for invalid combinations of flags */ + if (flags & LUW_SRB_FULL_SIZE && !(flags & LUW_SRB_ALLOC)) { + errno = EINVAL; + return -1; + } + + alloc_size = copy_bytes = ctx->req->request_size; + + if (flags & LUW_SRB_FULL_SIZE) + alloc_size = ctx->req->content_off + ctx->req->content_len; + + if (flags & LUW_SRB_ALLOC) { + *buf = malloc(alloc_size); + if (!*buf) + return -1; + } + + memcpy(*buf, ctx->addr, copy_bytes); + ctx->req_buf = *buf; + ctx->req = (struct luw_req *)ctx->req_buf; + ctx->reqp = ctx->req_buf; + + if (flags & LUW_SRB_APPEND) + ctx->reqp = ctx->req_buf + copy_bytes; + + return 0; +} + +const char *luw_get_http_path(const luw_ctx_t *ctx) +{ + return (const char *)ctx->req + ctx->req->path_off; +} + +const char *luw_get_http_method(const luw_ctx_t *ctx) +{ + return (const char *)ctx->req + ctx->req->method_off; +} + +const char *luw_get_http_version(const luw_ctx_t *ctx) +{ + return (const char *)ctx->req + ctx->req->version_off; +} + +const char *luw_get_http_query(const luw_ctx_t *ctx) +{ + return (const char *)ctx->req + ctx->req->query_off; +} + +const char *luw_get_http_remote(const luw_ctx_t *ctx) +{ + return (const char *)ctx->req + ctx->req->remote_off; +} + +const char *luw_get_http_local_addr(const luw_ctx_t *ctx) +{ + return (const char *)ctx->req + ctx->req->local_addr_off; +} + +const char *luw_get_http_local_port(const luw_ctx_t *ctx) +{ + return (const char *)ctx->req + ctx->req->local_port_off; +} + +const char *luw_get_http_server_name(const luw_ctx_t *ctx) +{ + return (const char *)ctx->req + ctx->req->server_name_off; +} + +const u8 *luw_get_http_content(const luw_ctx_t *ctx) +{ + return (u8 *)ctx->req + ctx->req->content_off; +} + +/* Returns the size of the overall content length */ +size_t luw_get_http_content_len(const luw_ctx_t *ctx) +{ + return ctx->req->content_len; +} + +/* Returns the size of the content sent in _this_ request */ +size_t luw_get_http_content_sent(const luw_ctx_t *ctx) +{ + return ctx->req->content_sent; +} + +bool luw_http_is_tls(const luw_ctx_t *ctx) +{ + return ctx->req->tls; +} + +void luw_http_hdr_iter(luw_ctx_t *ctx, + bool (*luw_http_hdr_iter_func)(luw_ctx_t *ctx, + const char *name, + const char *value, + void *data), + void *user_data) +{ + struct luw_hdr_field *hf; + struct luw_hdr_field *hf_end; + + hf_end = ctx->req->fields + ctx->req->nr_fields; + for (hf = ctx->req->fields; hf < hf_end; hf++) { + const char *name = (const char *)ctx->req + hf->name_off; + const char *value = (const char *)ctx->req + hf->value_off; + bool again; + + again = luw_http_hdr_iter_func(ctx, name, value, user_data); + if (!again) + break; + } +} + +const char *luw_http_hdr_get_value(luw_ctx_t *ctx, const char *hdr) +{ + luw_http_hdr_iter_t *iter; + const char *name; + const char *value; + + luw_foreach_http_hdr((*ctx), iter, name, value) { + if (strcasecmp(name, hdr) == 0) + return value; + } + + return NULL; +} + +/* Returns the size of ctx->resp->data[] */ +size_t luw_get_response_data_size(const luw_ctx_t *ctx) +{ + return ctx->mem - ctx->resp->data; +} + +/* Appends (non-nul terminmates) formatted data to the response buffer */ +__attribute__((__format__(printf, 2, 3))) +int luw_mem_writep(luw_ctx_t *ctx, const char *fmt, ...) +{ + int len; + char *logbuf; + va_list ap; + + va_start(ap, fmt); + len = vasprintf(&logbuf, fmt, ap); + if (len == -1) { + va_end(ap); + return -1; + } + va_end(ap); + + ctx->mem = mempcpy(ctx->mem, logbuf, len); + ctx->resp->size += len; + + free(logbuf); + + return len; +} + +/* Appends data of length size to the response buffer */ +size_t luw_mem_writep_data(luw_ctx_t *ctx, const u8 *src, size_t size) +{ + ctx->mem = mempcpy(ctx->mem, src, size); + ctx->resp->size += size; + + return ctx->resp->size; +} + +/* Append data from the request to the previously setup request_buffer. */ +void luw_req_buf_append(luw_ctx_t *ctx, const u8 *src) +{ + struct luw_req *req = (struct luw_req *)src; + + ctx->reqp = mempcpy(ctx->reqp, src + req->content_off, + req->request_size); + ctx->req->content_sent = req->content_sent; + ctx->req->total_content_sent = req->total_content_sent; +} + +/* + * Convenience function to fill the response buffer with data from + * the request buffer. + * + * The runtime allocates NXT_WASM_MEM_SIZE + NXT_WASM_PAGE_SIZE + * bytes so we don't need to worry about the size of the actual + * response structures. + */ +size_t luw_mem_fill_buf_from_req(luw_ctx_t *ctx, size_t from) +{ + size_t write_bytes; + size_t mem_size = nxt_wasm_get_init_mem_size(); + + write_bytes = ctx->req->content_sent; + if (write_bytes > mem_size) + write_bytes = mem_size; + + memcpy(ctx->resp->data, ctx->req_buf + ctx->req->content_off + from, + write_bytes); + ctx->resp->size = write_bytes; + + return write_bytes; +} + +void luw_mem_reset(luw_ctx_t *ctx) +{ + ctx->mem = ctx->resp->data; + ctx->resp->size = 0; + ctx->resp_hdr->nr_fields = 0; +} + +void luw_http_send_response(const luw_ctx_t *ctx) +{ + nxt_wasm_send_response(ctx->resp_offset); +} + +void luw_http_init_headers(luw_ctx_t *ctx, size_t nr, size_t offset) +{ + ctx->resp_hdr = (struct luw_resp_hdr *)(ctx->addr + offset); + ctx->hdrp = (u8 *)ctx->resp_hdr + sizeof(struct luw_resp_hdr) + + (nr * sizeof(struct luw_hdr_field)); + + ctx->resp_hdr->nr_fields = nr; +} + +void luw_http_add_header(luw_ctx_t *ctx, u16 idx, const char *name, + const char *value) +{ + ctx->resp_hdr->fields[idx].name_off = ctx->hdrp - ctx->addr; + ctx->resp_hdr->fields[idx].name_len = strlen(name); + ctx->hdrp = mempcpy(ctx->hdrp, name, strlen(name)); + + ctx->resp_hdr->fields[idx].value_off = ctx->hdrp - ctx->addr; + ctx->resp_hdr->fields[idx].value_len = strlen(value); + ctx->hdrp = mempcpy(ctx->hdrp, value, strlen(value)); +} + +void luw_http_send_headers(const luw_ctx_t *ctx) +{ + nxt_wasm_send_headers((u8 *)ctx->resp_hdr - ctx->addr); +} + +void luw_http_response_end(void) +{ + nxt_wasm_response_end(); +} + +u32 luw_mem_get_init_size(void) +{ + return nxt_wasm_get_init_mem_size(); +} + +/* These are really just convenience wrappers for the rust bindings... */ + +void *luw_malloc(size_t size) +{ + return malloc(size); +} + +void luw_free(void *ptr) +{ + free(ptr); +} diff --git a/src/rust/.gitignore b/src/rust/.gitignore new file mode 100644 index 0000000..6c481e2 --- /dev/null +++ b/src/rust/.gitignore @@ -0,0 +1,4 @@ +bindings.rs +Cargo.lock + +target/ diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml new file mode 100644 index 0000000..2e0bba2 --- /dev/null +++ b/src/rust/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "unit-wasm" +version = "0.1.0" +authors = ["Timo Stark <t.stark@f5.com>", "Andrew Clayton <a.clayton@f5.com>"] +description = "WASM SDK for NGINX Unit" +license = "Apache-2.0" +keywords = ["nginx", "unit", "wasm", "wasi"] + +[lib] +crate-type = ["rlib"] +path = "src/lib.rs" + + +[dependencies] +unit-wasm-sys = { path = "unit-wasm-sys", version = "0.1.1" } diff --git a/src/rust/Makefile b/src/rust/Makefile new file mode 100644 index 0000000..d2e705a --- /dev/null +++ b/src/rust/Makefile @@ -0,0 +1,11 @@ +include ../../shared.mk + +SDIR = src/rust + +rustlib: + $(PP_GEN) $(SDIR)/target/wasm32-wasi + $(v)cargo build --target=wasm32-wasi + +clean: + rm -f Cargo.lock unit-wasm-sys/Cargo.lock + rm -rf target/ unit-wasm-sys/target/ diff --git a/src/rust/README.md b/src/rust/README.md new file mode 100644 index 0000000..a48e2cd --- /dev/null +++ b/src/rust/README.md @@ -0,0 +1,18 @@ +# Quickstart in Developing Rust WebAssembly Modules for Unit + +The current version is published to crates.io. To get started with the SDK +include `unit-wasm` as dependency. + +``` +cargo add unit-wasm +``` + +## Prerequisites + +- target add wasm32-wasi. `rustup target add wasm32-wasi` + +## From Source + +The Rust implementation is in an early stage. If you would like to build the +crate by yourself, we have to generate the `libunit-wasm` first. This step is +NOT included in the build process. diff --git a/src/rust/src/ffi/mod.rs b/src/rust/src/ffi/mod.rs new file mode 100644 index 0000000..087148a --- /dev/null +++ b/src/rust/src/ffi/mod.rs @@ -0,0 +1,3 @@ +// @todo: Check if this is valid?! +extern crate unit_wasm_sys; +pub use self::unit_wasm_sys::*; diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs new file mode 100644 index 0000000..dface32 --- /dev/null +++ b/src/rust/src/lib.rs @@ -0,0 +1,8 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * Copyright (C) Timo Stark + * Copyright (C) F5, Inc. + */ + +pub mod ffi; diff --git a/src/rust/unit-wasm-sys/Cargo.toml b/src/rust/unit-wasm-sys/Cargo.toml new file mode 100644 index 0000000..d8f64a3 --- /dev/null +++ b/src/rust/unit-wasm-sys/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "unit-wasm-sys" +version = "0.1.1" +edition = "2021" +authors = ["Timo Stark <t.stark@nginx.com>", "Andrew Clayton <a.clayton@nginx.com>"] +links = "unit-wasm" +description = "Native bindings for the libunit-wasm C API" +license = "Apache-2.0" +keywords = ["nginx", "unit", "ffi", "sys"] + +[lib] +name = "unit_wasm_sys" +path = "lib.rs" +crate-type = ["staticlib", "rlib"] + + +[build-dependencies] +bindgen = "0.66.1" diff --git a/src/rust/unit-wasm-sys/README.md b/src/rust/unit-wasm-sys/README.md new file mode 100644 index 0000000..fbc53f0 --- /dev/null +++ b/src/rust/unit-wasm-sys/README.md @@ -0,0 +1,4 @@ +# unit-wasm sys crate + +The symbolic link is beeing used to share the c code with cargo during +buildtime. diff --git a/src/rust/unit-wasm-sys/build.rs b/src/rust/unit-wasm-sys/build.rs new file mode 100644 index 0000000..bd2ebd5 --- /dev/null +++ b/src/rust/unit-wasm-sys/build.rs @@ -0,0 +1,52 @@ +// buildscript for the unit-wasm-sys crate. + +use std::env; +use std::path::{Path, PathBuf}; + +fn main() { + // Tell rustc where to find the libunit-wasm library. + let libunit_wasm_dir = "libunit-wasm"; + + let dir = env::var("CARGO_MANIFEST_DIR").unwrap(); + + // Some generics + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=wrapper.h"); + + // The rustc-link-search tells Cargo to pass the `-L` flag to the + // compiler to add a directory to the library search plugin. The + // `native` keyword means "only looking for `native libraries` in + // this directory". + println!( + "cargo:rustc-link-search=native={}", + Path::new(&dir).join(libunit_wasm_dir).display() + ); + + // The rustc-link-lib tells Cargo to link the given library using + // the compiler's `-l` flag. This is needed to start building our + // FFIs. + println!("cargo:rustc-link-lib=static=unit-wasm"); + + generate_bindings(); +} + +fn generate_bindings() { + let wasi_sysroot = "--sysroot=".to_owned() + &env::var("WASI_SYSROOT").unwrap(); + let bindings = bindgen::Builder::default() + // The input header file. + .header("libunit-wasm/include/unit/unit-wasm.h") + .allowlist_function("^luw_.*") + .allowlist_var("^luw_.*") + .allowlist_type("^luw_.*") + .clang_args(vec![wasi_sysroot]) // Needed for strings.h + .generate() + .expect("Unable to generate bindings"); + + let out_dir_env = + env::var("OUT_DIR").expect("The required environment variable OUT_DIR was not set"); + let out_path = PathBuf::from(out_dir_env); + + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/src/rust/unit-wasm-sys/lib.rs b/src/rust/unit-wasm-sys/lib.rs new file mode 100644 index 0000000..9ef60f6 --- /dev/null +++ b/src/rust/unit-wasm-sys/lib.rs @@ -0,0 +1,19 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * Copyright (C) Timo Stark + * Copyright (C) F5, Inc. + */ + +#[doc(hidden)] +mod bindings { + #![allow(non_upper_case_globals)] + #![allow(non_camel_case_types)] + #![allow(dead_code)] + + include!(concat!(env!("OUT_DIR"), "/bindings.rs")); + include!("macros.rs"); +} + +#[doc(no_inline)] +pub use bindings::*; diff --git a/src/rust/unit-wasm-sys/libunit-wasm b/src/rust/unit-wasm-sys/libunit-wasm new file mode 120000 index 0000000..2fcc511 --- /dev/null +++ b/src/rust/unit-wasm-sys/libunit-wasm @@ -0,0 +1 @@ +../../c
\ No newline at end of file diff --git a/src/rust/unit-wasm-sys/macros.rs b/src/rust/unit-wasm-sys/macros.rs new file mode 100644 index 0000000..d7fde22 --- /dev/null +++ b/src/rust/unit-wasm-sys/macros.rs @@ -0,0 +1,15 @@ +/* SPDX-License-Identifier: Apache-2.0 */ + +/* + * Copyright (C) Andrew Clayton + * Copyright (C) F5, Inc. + */ + +pub const LUW_VERSION_MAJOR: i32 = 0; +pub const LUW_VERSION_MINOR: i32 = 1; +pub const LUW_VERSION_PATCH: i32 = 0; + +pub const LUW_VERSION_NUMBER: i32 = + (LUW_VERSION_MAJOR << 24) | + (LUW_VERSION_MINOR << 16) | + (LUW_VERSION_PATCH << 8); diff --git a/unit-wasm-conf.json b/unit-wasm-conf.json new file mode 100644 index 0000000..fe997b3 --- /dev/null +++ b/unit-wasm-conf.json @@ -0,0 +1,87 @@ +{ + "listeners": { + "[::1]:8888": { + "pass": "routes" + } + }, + + "settings": { + "http": { + "max_body_size": 1073741824 + } + }, + + "routes": [ + { + "match": { + "uri": "/echo*" + }, + "action": { + "pass": "applications/luw-echo-request" + } + }, + { + "match": { + "uri": "/upload*" + }, + "action": { + "pass": "applications/luw-upload-reflector" + } + }, + { + "match": { + "uri": "/rust-echo*" + }, + "action": { + "pass": "applications/rust-echo-test" + } + }, + { + "match": { + "uri": "/rust-upload*" + }, + "action": { + "pass": "applications/rust-upload-reflector" + } + } + ], + + "applications": { + "luw-echo-request": { + "type": "wasm", + "module": "/path/to/unit-wasm/examples/c/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" + }, + "luw-upload-reflector": { + "type": "wasm", + "module": "/path/to/unit-wasm/examples/c/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" + }, + "rust-echo-test": { + "type": "wasm", + "module": "/path/to/unit-wasm/examples/rust/echo-request/target/wasm32-wasi/debug/rust_echo_test.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" + }, + "rust-upload-reflector": { + "type": "wasm", + "module": "/path/to/unit-wasm/examples/rust/upload-reflector/rust_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" + } + } +} |