diff options
Diffstat (limited to 'wasm')
-rw-r--r-- | wasm/GNUmakefile.in | 32 | ||||
-rw-r--r-- | wasm/README.md | 70 | ||||
-rw-r--r-- | wasm/asyncify.h | 23 | ||||
-rw-r--r-- | wasm/fiber.c | 83 | ||||
-rw-r--r-- | wasm/fiber.h | 43 | ||||
-rw-r--r-- | wasm/machine.c | 62 | ||||
-rw-r--r-- | wasm/machine.h | 25 | ||||
-rw-r--r-- | wasm/machine_core.S | 25 | ||||
-rw-r--r-- | wasm/missing.c | 199 | ||||
-rw-r--r-- | wasm/runtime.c | 54 | ||||
-rw-r--r-- | wasm/setjmp.c | 215 | ||||
-rw-r--r-- | wasm/setjmp.h | 95 | ||||
-rw-r--r-- | wasm/setjmp_core.S | 27 | ||||
-rw-r--r-- | wasm/tests/fiber_test.c | 66 | ||||
-rw-r--r-- | wasm/tests/machine_test.c | 115 | ||||
-rw-r--r-- | wasm/tests/setjmp_test.c | 108 | ||||
-rwxr-xr-x | wasm/wasm-opt | 36 |
17 files changed, 1278 insertions, 0 deletions
diff --git a/wasm/GNUmakefile.in b/wasm/GNUmakefile.in new file mode 100644 index 0000000000..18ddd06739 --- /dev/null +++ b/wasm/GNUmakefile.in @@ -0,0 +1,32 @@ +include Makefile +include $(srcdir)/template/GNUmakefile.in + +wasmdir = $(srcdir)/wasm +GNUmakefile: $(wasmdir)/GNUmakefile.in +WASMOPT = @WASMOPT@ +wasmoptflags = @wasmoptflags@ + +WASM_TESTRUNNER = wasmtime +WASM_TESTS = $(wasmdir)/tests/machine_test.wasm $(wasmdir)/tests/setjmp_test.wasm $(wasmdir)/tests/fiber_test.wasm +WASM_OBJS = $(wasmdir)/machine_core.o $(wasmdir)/machine.o $(wasmdir)/setjmp.o $(wasmdir)/setjmp_core.o $(wasmdir)/fiber.o $(wasmdir)/runtime.o + +wasm/missing.$(OBJEXT): $(wasmdir)/missing.c $(PLATFORM_D) +wasm/fiber.$(OBJEXT): $(wasmdir)/fiber.c $(wasmdir)/fiber.h $(wasmdir)/asyncify.h $(PLATFORM_D) +wasm/machine.$(OBJEXT): $(wasmdir)/machine.c $(srcdir)/wasm/machine.h $(wasmdir)/asyncify.h $(PLATFORM_D) +wasm/setjmp.$(OBJEXT): $(wasmdir)/setjmp.c $(wasmdir)/setjmp.h $(wasmdir)/machine.h $(wasmdir)/asyncify.h $(PLATFORM_D) +wasm/runtime.$(OBJEXT): $(wasmdir)/runtime.c $(wasmdir)/machine.h $(wasmdir)/asyncify.h $(wasmdir)/setjmp.h $(PLATFORM_D) + +wasm/%.$(OBJEXT): $(wasmdir)/%.S $(PLATFORM_D) + @$(ECHO) compiling $< + $(Q) $(CC) $(CFLAGS) $(COUTFLAG)$@ -c $< + +test-wasm: $(WASM_TESTS) + $(foreach x,$(WASM_TESTS), $(WASM_TESTRUNNER) $(x);) +clean-test-wasm: + @$(RM) $(WASM_TESTS) + +$(wasmdir)/tests/%.wasm: $(wasmdir)/tests/%.c $(WASM_OBJS) + $(Q) $(CC) -g $(XCFLAGS) $(CFLAGS) $^ -o $@ + $(Q) $(WASMOPT) -g --asyncify --pass-arg=asyncify-ignore-imports -o $@ $@ + +.PHONY: test-wasm clean-test-wasm diff --git a/wasm/README.md b/wasm/README.md new file mode 100644 index 0000000000..0f9ca1a3d5 --- /dev/null +++ b/wasm/README.md @@ -0,0 +1,70 @@ +# WebAssembly / WASI port of Ruby + +## How to cross-build + +### Requirement + +- Ruby (the same version as the building target version) (baseruby) +- GNU make +- [WASI SDK](https://github.com/WebAssembly/wasi-sdk) 14.0 or later +- [Binaryen](https://github.com/WebAssembly/binaryen) version 106 or later +- Linux or macOS build machine + +### Steps + +1. Download a prebuilt WASI SDK package from [WASI SDK release page](https://github.com/WebAssembly/wasi-sdk/releases). +2. Set `WASI_SDK_PATH` environment variable to the root directory of the WASI SDK package. +```console +$ export WASI_SDK_PATH=/path/to/wasi-sdk-X.Y +``` +3. Download a prebuilt binaryen from [Binaryen release page](https://github.com/WebAssembly/binaryen/releases) +4. Set PATH environment variable to lookup binaryen tools +```console +$ export PATH=path/to/binaryen:$PATH +``` +5. Download the latest `config.guess` with WASI support, and run `./autogen.sh` to generate configure when you + are building from the source checked out from Git repository +```console +$ ruby tool/downloader.rb -d tool -e gnu config.guess config.sub +$ ./autogen.sh +``` + +6. Configure + - You can select which extensions you want to build. + - If you got `Out of bounds memory access` while running the produced ruby, you may need to increase the maximum size of stack. +```console +$ ./configure LDFLAGS="-Xlinker -zstack-size=16777216" \ + --host wasm32-unknown-wasi \ + --with-destdir=./ruby-wasm32-wasi \ + --with-static-linked-ext \ + --with-ext=ripper,monitor +``` + +7. Make +```console +$ make install +``` + +Now you have a WASI compatible ruby binary. You can run it by any WebAssembly runtime like [`wasmtime`](https://github.com/bytecodealliance/wasmtime), [`wasmer`](https://github.com/wasmerio/wasmer), [Node.js](https://nodejs.org/api/wasi.html), or browser with [WASI polyfill](https://www.npmjs.com/package/@wasmer/wasi). + +Note: it may take a long time (~20 sec) for the first time for JIT compilation + +``` +$ wasmtime ruby-wasm32-wasi/usr/local/bin/ruby --mapdir /::./ruby-wasm32-wasi/ -- -e 'puts RUBY_PLATFORM' +wasm32-wasi +``` + +Note: you cannot run the built ruby without a WebAssembly runtime, because of the difference of the binary file type. + +``` +$ ruby-wasm32-wasi/usr/local/bin/ruby -e 'puts "a"' +bash: ruby-wasm32-wasi/usr/local/bin/ruby: cannot execute binary file: Exec format error + +$ file ruby-wasm32-wasi/usr/local/bin/ruby +ruby-wasm32-wasi/usr/local/bin/ruby: WebAssembly (wasm) binary module version 0x1 (MVP) +``` + +## Current Limitation + +- No `Thread` support for now. +- Spawning a new process is not supported. e.g. `Kernel.spawn` and `Kernel.system` diff --git a/wasm/asyncify.h b/wasm/asyncify.h new file mode 100644 index 0000000000..49eb125593 --- /dev/null +++ b/wasm/asyncify.h @@ -0,0 +1,23 @@ +#ifndef RB_WASM_SUPPORT_ASYNCIFY_H +#define RB_WASM_SUPPORT_ASYNCIFY_H + +__attribute__((import_module("asyncify"), import_name("start_unwind"))) +void asyncify_start_unwind(void *buf); +#define asyncify_start_unwind(buf) do { \ + extern void *rb_asyncify_unwind_buf; \ + rb_asyncify_unwind_buf = (buf); \ + asyncify_start_unwind((buf)); \ + } while (0) +__attribute__((import_module("asyncify"), import_name("stop_unwind"))) +void asyncify_stop_unwind(void); +#define asyncify_stop_unwind() do { \ + extern void *rb_asyncify_unwind_buf; \ + rb_asyncify_unwind_buf = NULL; \ + asyncify_stop_unwind(); \ + } while (0) +__attribute__((import_module("asyncify"), import_name("start_rewind"))) +void asyncify_start_rewind(void *buf); +__attribute__((import_module("asyncify"), import_name("stop_rewind"))) +void asyncify_stop_rewind(void); + +#endif diff --git a/wasm/fiber.c b/wasm/fiber.c new file mode 100644 index 0000000000..ecc481b0ee --- /dev/null +++ b/wasm/fiber.c @@ -0,0 +1,83 @@ +/* + This is a ucontext-like userland context switching API for WebAssembly based on Binaryen's Asyncify. + + * NOTE: + * This mechanism doesn't take care of stack state. Just save and restore program counter and + * registers (rephrased as locals by Wasm term). So use-site need to save and restore the C stack pointer. + * This Asyncify based implementation is not much efficient and will be replaced with future stack-switching feature. + */ + +#include <stdlib.h> +#include "wasm/fiber.h" +#include "wasm/asyncify.h" + +#ifdef RB_WASM_ENABLE_DEBUG_LOG +# include <stdio.h> +# define RB_WASM_DEBUG_LOG(...) fprintf(stderr, __VA_ARGS__) +#else +# define RB_WASM_DEBUG_LOG(...) +#endif + +void +rb_wasm_init_context(rb_wasm_fiber_context *fcp, void (*func)(void *, void *), void *arg0, void *arg1) +{ + fcp->asyncify_buf.top = &fcp->asyncify_buf.buffer[0]; + fcp->asyncify_buf.end = &fcp->asyncify_buf.buffer[WASM_FIBER_STACK_BUFFER_SIZE]; + fcp->is_rewinding = false; + fcp->is_started = false; + fcp->entry_point = func; + fcp->arg0 = arg0; + fcp->arg1 = arg1; + RB_WASM_DEBUG_LOG("[%s] fcp->asyncify_buf %p\n", __func__, &fcp->asyncify_buf); +} + +static rb_wasm_fiber_context *_rb_wasm_active_next_fiber; + +void +rb_wasm_swapcontext(rb_wasm_fiber_context *ofcp, rb_wasm_fiber_context *fcp) +{ + RB_WASM_DEBUG_LOG("[%s] enter ofcp = %p fcp = %p\n", __func__, ofcp, fcp); + if (ofcp->is_rewinding) { + asyncify_stop_rewind(); + ofcp->is_rewinding = false; + return; + } + _rb_wasm_active_next_fiber = fcp; + RB_WASM_DEBUG_LOG("[%s] start unwinding asyncify_buf = %p\n", __func__, &ofcp->asyncify_buf); + asyncify_start_unwind(&ofcp->asyncify_buf); +} + +void * +rb_wasm_handle_fiber_unwind(void (**new_fiber_entry)(void *, void *), + void **arg0, void **arg1, bool *is_new_fiber_started) +{ + rb_wasm_fiber_context *next_fiber; + if (!_rb_wasm_active_next_fiber) { + RB_WASM_DEBUG_LOG("[%s] no next fiber\n", __func__); + *is_new_fiber_started = false; + return NULL; + } + + next_fiber = _rb_wasm_active_next_fiber; + _rb_wasm_active_next_fiber = NULL; + + RB_WASM_DEBUG_LOG("[%s] next_fiber->asyncify_buf = %p\n", __func__, &next_fiber->asyncify_buf); + + *new_fiber_entry = next_fiber->entry_point; + *arg0 = next_fiber->arg0; + *arg1 = next_fiber->arg1; + + if (!next_fiber->is_started) { + RB_WASM_DEBUG_LOG("[%s] new fiber started\n", __func__); + // start a new fiber if not started yet. + next_fiber->is_started = true; + *is_new_fiber_started = true; + return NULL; + } else { + RB_WASM_DEBUG_LOG("[%s] resume a fiber\n", __func__); + // resume a fiber again + next_fiber->is_rewinding = true; + *is_new_fiber_started = false; + return &next_fiber->asyncify_buf; + } +} diff --git a/wasm/fiber.h b/wasm/fiber.h new file mode 100644 index 0000000000..0f3a336332 --- /dev/null +++ b/wasm/fiber.h @@ -0,0 +1,43 @@ +#ifndef RB_WASM_SUPPORT_FIBER_H +#define RB_WASM_SUPPORT_FIBER_H + +#include <stdbool.h> + +#ifndef WASM_FIBER_STACK_BUFFER_SIZE +# define WASM_FIBER_STACK_BUFFER_SIZE 6144 +#endif + +struct __rb_wasm_asyncify_fiber_ctx { + void* top; + void* end; + char buffer[WASM_FIBER_STACK_BUFFER_SIZE]; +}; + +// Fiber execution context needed to perform context switch +typedef struct { + // Fiber entry point called when the fiber started for the first time. + // NULL if the entry point is main + void (*entry_point)(void *, void *); + // Opaque argument pointers passed to the entry point function + void *arg0, *arg1; + + // Internal asyncify buffer space + struct __rb_wasm_asyncify_fiber_ctx asyncify_buf; + + bool is_rewinding; + bool is_started; +} rb_wasm_fiber_context; + +// Initialize a given fiber context to be ready to pass to `rb_wasm_swapcontext` +void rb_wasm_init_context(rb_wasm_fiber_context *fcp, void (*func)(void *, void *), void *arg0, void *arg1); + +// Swap the execution control with `target_fiber` and save the current context in `old_fiber` +// NOTE: `old_fiber` must be the current executing fiber context +void rb_wasm_swapcontext(rb_wasm_fiber_context *old_fiber, rb_wasm_fiber_context *target_fiber); + +// Returns the Asyncify buffer of next fiber if unwound for fiber context switch. +// Used by the top level Asyncify handling in wasm/runtime.c +void *rb_wasm_handle_fiber_unwind(void (**new_fiber_entry)(void *, void *), + void **arg0, void **arg1, bool *is_new_fiber_started); + +#endif diff --git a/wasm/machine.c b/wasm/machine.c new file mode 100644 index 0000000000..2ca8462502 --- /dev/null +++ b/wasm/machine.c @@ -0,0 +1,62 @@ +#include <stdlib.h> +#include "wasm/machine.h" +#include "wasm/asyncify.h" + +#ifndef WASM_SCAN_STACK_BUFFER_SIZE +# define WASM_SCAN_STACK_BUFFER_SIZE 6144 +#endif + +struct asyncify_buf { + void *top; + void *end; + uint8_t buffer[WASM_SCAN_STACK_BUFFER_SIZE]; +}; + +static void +init_asyncify_buf(struct asyncify_buf* buf) +{ + buf->top = &buf->buffer[0]; + buf->end = &buf->buffer[WASM_SCAN_STACK_BUFFER_SIZE]; +} + +static void *_rb_wasm_active_scan_buf = NULL; + +void +rb_wasm_scan_locals(rb_wasm_scan_func scan) +{ + static struct asyncify_buf buf; + static int spilling = 0; + if (!spilling) { + spilling = 1; + init_asyncify_buf(&buf); + _rb_wasm_active_scan_buf = &buf; + asyncify_start_unwind(&buf); + } else { + asyncify_stop_rewind(); + spilling = 0; + _rb_wasm_active_scan_buf = NULL; + scan(buf.top, buf.end); + } +} + +static void *rb_wasm_stack_base = NULL; + +__attribute__((constructor)) +int +rb_wasm_record_stack_base(void) +{ + rb_wasm_stack_base = rb_wasm_get_stack_pointer(); + return 0; +} + +void * +rb_wasm_stack_get_base(void) +{ + return rb_wasm_stack_base; +} + +void * +rb_wasm_handle_scan_unwind(void) +{ + return _rb_wasm_active_scan_buf; +} diff --git a/wasm/machine.h b/wasm/machine.h new file mode 100644 index 0000000000..1a60e51d11 --- /dev/null +++ b/wasm/machine.h @@ -0,0 +1,25 @@ +#ifndef RB_WASM_SUPPORT_MACHINE_H +#define RB_WASM_SUPPORT_MACHINE_H + +// Function pointer used as scan callbacks +typedef void (*rb_wasm_scan_func)(void*, void*); + +// Scan WebAssembly locals in the all call stack (like registers) spilled by Asyncify +// Used by conservative GC +void rb_wasm_scan_locals(rb_wasm_scan_func scan); + +// Get base address of userland C-stack memory space in WebAssembly. Used by conservative GC +void *rb_wasm_stack_get_base(void); + + +// Get the current stack pointer +void *rb_wasm_get_stack_pointer(void); + +// Set the current stack pointer +void rb_wasm_set_stack_pointer(void *sp); + +// Returns the Asyncify buffer of next rewinding if unwound for spilling locals. +// Used by the top level Asyncify handling in wasm/runtime.c +void *rb_wasm_handle_scan_unwind(void); + +#endif diff --git a/wasm/machine_core.S b/wasm/machine_core.S new file mode 100644 index 0000000000..f0050536f5 --- /dev/null +++ b/wasm/machine_core.S @@ -0,0 +1,25 @@ + # extern int __stack_pointer; + .globaltype __stack_pointer, i32 + + # NOTE: Implement this in raw assembly to avoid stack pointer + # operations in C-prologue and epilogue. + + # void *rb_wasm_get_stack_pointer(void); + .section .text.rb_wasm_get_stack_pointer,"",@ + .globl rb_wasm_get_stack_pointer + .type rb_wasm_get_stack_pointer,@function +rb_wasm_get_stack_pointer: + .functype rb_wasm_get_stack_pointer () -> (i32) + global.get __stack_pointer + end_function + + # void rb_wasm_set_stack_pointer(void *sp); + .section .text.rb_wasm_set_stack_pointer,"",@ + .globl rb_wasm_set_stack_pointer + .type rb_wasm_set_stack_pointer,@function +rb_wasm_set_stack_pointer: + .functype rb_wasm_set_stack_pointer (i32) -> () + local.get 0 + global.set __stack_pointer + end_function + diff --git a/wasm/missing.c b/wasm/missing.c new file mode 100644 index 0000000000..5bbf988642 --- /dev/null +++ b/wasm/missing.c @@ -0,0 +1,199 @@ +#include <errno.h> +#include <sys/types.h> +#include "ruby/missing.h" + +// Produce weak symbols for missing functions to replace them with actual ones if exists. +#define WASM_MISSING_LIBC_FUNC __attribute__((weak)) + +WASM_MISSING_LIBC_FUNC +int +chmod(const char *pathname, rb_mode_t mode) +{ + errno = ENOTSUP; + return -1; +} + +WASM_MISSING_LIBC_FUNC +int +chown(const char *pathname, rb_uid_t owner, rb_gid_t group) +{ + errno = ENOTSUP; + return -1; +} + +WASM_MISSING_LIBC_FUNC +int +dup(int oldfd) +{ + errno = ENOTSUP; + return -1; +} + +WASM_MISSING_LIBC_FUNC +int +dup2(int oldfd, int newfd) +{ + errno = ENOTSUP; + return -1; +} + +WASM_MISSING_LIBC_FUNC +int +execl(const char *path, const char *arg, ...) +{ + errno = ENOTSUP; + return -1; +} + +WASM_MISSING_LIBC_FUNC +int +execle(const char *path, const char *arg, ...) +{ + errno = ENOTSUP; + return -1; +} + +WASM_MISSING_LIBC_FUNC +int +execv(const char *path, char *const argv[]) +{ + errno = ENOTSUP; + return -1; +} + +WASM_MISSING_LIBC_FUNC +int +execve(const char *filename, char *const argv[], char *const envp[]) +{ + errno = ENOTSUP; + return -1; +} + +WASM_MISSING_LIBC_FUNC +rb_uid_t +geteuid(void) +{ + return 0; +} + +WASM_MISSING_LIBC_FUNC +rb_uid_t +getuid(void) +{ + return 0; +} + +WASM_MISSING_LIBC_FUNC +rb_pid_t +getppid(void) +{ + return 0; +} + +WASM_MISSING_LIBC_FUNC +rb_gid_t +getegid(void) +{ + return 0; +} + +WASM_MISSING_LIBC_FUNC +rb_gid_t +getgid(void) +{ + return 0; +} + +WASM_MISSING_LIBC_FUNC +char * +getlogin(void) +{ + errno = ENOTSUP; + return NULL; +} + +WASM_MISSING_LIBC_FUNC +rb_mode_t +umask(rb_mode_t mask) +{ + return 0; +} + +WASM_MISSING_LIBC_FUNC +int +mprotect(const void *addr, size_t len, int prot) +{ + return 0; +} + +WASM_MISSING_LIBC_FUNC +int +pclose(FILE *stream) +{ + errno = ENOTSUP; + return -1; +} + +WASM_MISSING_LIBC_FUNC +FILE * +popen(const char *command, const char *type) +{ + errno = ENOTSUP; + return NULL; +} + +WASM_MISSING_LIBC_FUNC +int +pipe(int pipefd[2]) +{ + errno = ENOTSUP; + return -1; +} + +WASM_MISSING_LIBC_FUNC +int +posix_madvise(void *addr, size_t len, int advice) +{ + errno = ENOTSUP; + return -1; +} + +WASM_MISSING_LIBC_FUNC +int +kill(rb_pid_t pid, int sig) +{ + errno = ENOTSUP; + return -1; +} + + +WASM_MISSING_LIBC_FUNC +void +tzset(void) +{ + return; +} + +WASM_MISSING_LIBC_FUNC +int +shutdown(int s, int how) +{ + errno = ENOTSUP; + return -1; +} + +WASM_MISSING_LIBC_FUNC +int +system(const char *command) +{ + errno = ENOTSUP; + return -1; +} + +WASM_MISSING_LIBC_FUNC +pid_t +waitpid(pid_t pid, int *wstatus, int options) +{ + errno = ENOTSUP; + return -1; +} diff --git a/wasm/runtime.c b/wasm/runtime.c new file mode 100644 index 0000000000..89b06be6ad --- /dev/null +++ b/wasm/runtime.c @@ -0,0 +1,54 @@ +#include "wasm/machine.h" +#include "wasm/setjmp.h" +#include "wasm/fiber.h" +#include "wasm/asyncify.h" +#include <stdlib.h> + +int rb_wasm_rt_start(int (main)(int argc, char **argv), int argc, char **argv) { + int result; + void *asyncify_buf; + + bool new_fiber_started = false; + void *arg0 = NULL, *arg1 = NULL; + void (*fiber_entry_point)(void *, void *) = NULL; + + while (1) { + if (fiber_entry_point) { + fiber_entry_point(arg0, arg1); + } else { + result = main(argc, argv); + } + + extern void *rb_asyncify_unwind_buf; + // Exit Asyncify loop if there is no unwound buffer, which + // means that main function has returned normally. + if (rb_asyncify_unwind_buf == NULL) { + break; + } + + // NOTE: it's important to call 'asyncify_stop_unwind' here instead in rb_wasm_handle_jmp_unwind + // because unless that, Asyncify inserts another unwind check here and it unwinds to the root frame. + asyncify_stop_unwind(); + + if ((asyncify_buf = rb_wasm_handle_jmp_unwind()) != NULL) { + asyncify_start_rewind(asyncify_buf); + continue; + } + if ((asyncify_buf = rb_wasm_handle_scan_unwind()) != NULL) { + asyncify_start_rewind(asyncify_buf); + continue; + } + + asyncify_buf = rb_wasm_handle_fiber_unwind(&fiber_entry_point, &arg0, &arg1, &new_fiber_started); + // Newly starting fiber doesn't have asyncify buffer yet, so don't rewind it for the first time entry + if (asyncify_buf) { + asyncify_start_rewind(asyncify_buf); + continue; + } else if (new_fiber_started) { + continue; + } + + break; + } + return result; +} diff --git a/wasm/setjmp.c b/wasm/setjmp.c new file mode 100644 index 0000000000..ebbf8949c1 --- /dev/null +++ b/wasm/setjmp.c @@ -0,0 +1,215 @@ +/* + This is a WebAssembly userland setjmp/longjmp implementation based on Binaryen's Asyncify. + Inspired by Alon Zakai's snippet released under the MIT License: + * https://github.com/kripken/talks/blob/991fb1e4b6d7e4b0ea6b3e462d5643f11d422771/jmp.c + + WebAssembly doesn't have context-switching mechanism for now, so emulate it by Asyncify, + which transforms WebAssembly binary to unwind/rewind the execution point and store/restore + locals. + + The basic concept of this implementation is: + 1. setjmp captures the current execution context by unwinding to the root frame, then immediately + rewind to the setjmp call using the captured context. The context is saved in jmp_buf. + 2. longjmp unwinds to the root frame and rewinds to a setjmp call re-using a passed jmp_buf. + + This implementation also supports switching context across different call stack (non-standard) + + This approach is good at behavior reproducibility and self-containedness compared to Emscripten's + JS exception approach. However this is super expensive because Asyncify inserts many glue code to + control execution point in userland. + + This implementation will be replaced with future stack-switching feature. + */ +#include <stdint.h> +#include <stdlib.h> +#include <assert.h> +#include <stdbool.h> +#include "wasm/asyncify.h" +#include "wasm/machine.h" +#include "wasm/setjmp.h" + +#ifdef RB_WASM_ENABLE_DEBUG_LOG +# include <wasi/api.h> +# include <unistd.h> +// NOTE: We can't use printf() and most of library function that are +// Asyncified due to the use of them in the application itself. +// Use of printf() causes "unreachable" error because Asyncified +// function misunderstands Asyncify's internal state during +// start_unwind()...stop_unwind() and start_rewind()...stop_rewind(). +# define RB_WASM_DEBUG_LOG_INTERNAL(msg) do { \ + const uint8_t *msg_start = (uint8_t *)msg; \ + const uint8_t *msg_end = msg_start; \ + for (; *msg_end != '\0'; msg_end++) {} \ + __wasi_ciovec_t iov = {.buf = msg_start, .buf_len = msg_end - msg_start}; \ + size_t nwritten; \ + __wasi_fd_write(STDERR_FILENO, &iov, 1, &nwritten); \ +} while (0) +# define RB_WASM_DEBUG_LOG(msg) \ + RB_WASM_DEBUG_LOG_INTERNAL(__FILE__ ":" STRINGIZE(__LINE__) ": " msg "\n") +#else +# define RB_WASM_DEBUG_LOG(msg) +#endif + +enum rb_wasm_jmp_buf_state { + // Initial state + JMP_BUF_STATE_INITIALIZED = 0, + // Unwinding to the root or rewinding to the setjmp call + // to capture the current execution context + JMP_BUF_STATE_CAPTURING = 1, + // Ready for longjmp + JMP_BUF_STATE_CAPTURED = 2, + // Unwinding to the root or rewinding to the setjmp call + // to restore the execution context + JMP_BUF_STATE_RETURNING = 3, +}; + +void +async_buf_init(struct __rb_wasm_asyncify_jmp_buf* buf) +{ + buf->top = &buf->buffer[0]; + buf->end = &buf->buffer[WASM_SETJMP_STACK_BUFFER_SIZE]; +} + +// Global unwinding/rewinding jmpbuf state +static rb_wasm_jmp_buf *_rb_wasm_active_jmpbuf; +void *rb_asyncify_unwind_buf; + +__attribute__((noinline)) +int +_rb_wasm_setjmp_internal(rb_wasm_jmp_buf *env) +{ + RB_WASM_DEBUG_LOG("enter _rb_wasm_setjmp_internal"); + switch (env->state) { + case JMP_BUF_STATE_INITIALIZED: { + RB_WASM_DEBUG_LOG(" JMP_BUF_STATE_INITIALIZED"); + env->state = JMP_BUF_STATE_CAPTURING; + env->payload = 0; + env->longjmp_buf_ptr = NULL; + _rb_wasm_active_jmpbuf = env; + async_buf_init(&env->setjmp_buf); + asyncify_start_unwind(&env->setjmp_buf); + return -1; // return a dummy value + } + case JMP_BUF_STATE_CAPTURING: { + asyncify_stop_rewind(); + RB_WASM_DEBUG_LOG(" JMP_BUF_STATE_CAPTURING"); + env->state = JMP_BUF_STATE_CAPTURED; + _rb_wasm_active_jmpbuf = NULL; + return 0; + } + case JMP_BUF_STATE_RETURNING: { + asyncify_stop_rewind(); + RB_WASM_DEBUG_LOG(" JMP_BUF_STATE_RETURNING"); + env->state = JMP_BUF_STATE_CAPTURED; + _rb_wasm_active_jmpbuf = NULL; + return env->payload; + } + default: + assert(0 && "unexpected state"); + } + return 0; +} + +void +_rb_wasm_longjmp(rb_wasm_jmp_buf* env, int value) +{ + RB_WASM_DEBUG_LOG("enter _rb_wasm_longjmp"); + assert(env->state == JMP_BUF_STATE_CAPTURED); + assert(value != 0); + env->state = JMP_BUF_STATE_RETURNING; + env->payload = value; + // Asyncify buffer built during unwinding for longjmp will not + // be used to rewind, so re-use static-variable. + static struct __rb_wasm_asyncify_jmp_buf tmp_longjmp_buf; + env->longjmp_buf_ptr = &tmp_longjmp_buf; + _rb_wasm_active_jmpbuf = env; + async_buf_init(env->longjmp_buf_ptr); + asyncify_start_unwind(env->longjmp_buf_ptr); +} + + +enum try_catch_phase { + TRY_CATCH_PHASE_MAIN = 0, + TRY_CATCH_PHASE_RESCUE = 1, +}; + +void +rb_wasm_try_catch_init(struct rb_wasm_try_catch *try_catch, + rb_wasm_try_catch_func_t try_f, + rb_wasm_try_catch_func_t catch_f, + void *context) +{ + try_catch->state = TRY_CATCH_PHASE_MAIN; + try_catch->try_f = try_f; + try_catch->catch_f = catch_f; + try_catch->context = context; +} + +// NOTE: This function is not processed by Asyncify due to a call of asyncify_stop_rewind +void +rb_wasm_try_catch_loop_run(struct rb_wasm_try_catch *try_catch, rb_wasm_jmp_buf *target) +{ + extern void *rb_asyncify_unwind_buf; + extern rb_wasm_jmp_buf *_rb_wasm_active_jmpbuf; + + target->state = JMP_BUF_STATE_CAPTURED; + + switch ((enum try_catch_phase)try_catch->state) { + case TRY_CATCH_PHASE_MAIN: + // may unwind + try_catch->try_f(try_catch->context); + break; + case TRY_CATCH_PHASE_RESCUE: + if (try_catch->catch_f) { + // may unwind + try_catch->catch_f(try_catch->context); + } + break; + } + + { + // catch longjmp with target jmp_buf + while (rb_asyncify_unwind_buf && _rb_wasm_active_jmpbuf == target) { + // do similar steps setjmp does when JMP_BUF_STATE_RETURNING + + // stop unwinding + // (but call stop_rewind to update the asyncify state to "normal" from "unwind") + asyncify_stop_rewind(); + // clear the active jmpbuf because it's already stopped + _rb_wasm_active_jmpbuf = NULL; + // reset jmpbuf state to be able to unwind again + target->state = JMP_BUF_STATE_CAPTURED; + // move to catch loop phase + try_catch->state = TRY_CATCH_PHASE_RESCUE; + if (try_catch->catch_f) { + try_catch->catch_f(try_catch->context); + } + } + // no unwind or unrelated unwind, then exit + } +} + +void * +rb_wasm_handle_jmp_unwind(void) +{ + RB_WASM_DEBUG_LOG("enter rb_wasm_handle_jmp_unwind"); + if (!_rb_wasm_active_jmpbuf) { + return NULL; + } + + switch (_rb_wasm_active_jmpbuf->state) { + case JMP_BUF_STATE_CAPTURING: + RB_WASM_DEBUG_LOG(" JMP_BUF_STATE_CAPTURING"); + // save the captured Asyncify stack top + _rb_wasm_active_jmpbuf->dst_buf_top = _rb_wasm_active_jmpbuf->setjmp_buf.top; + break; + case JMP_BUF_STATE_RETURNING: + RB_WASM_DEBUG_LOG(" JMP_BUF_STATE_RETURNING"); + // restore the saved Asyncify stack top + _rb_wasm_active_jmpbuf->setjmp_buf.top = _rb_wasm_active_jmpbuf->dst_buf_top; + break; + default: + assert(0 && "unexpected state"); + } + return &_rb_wasm_active_jmpbuf->setjmp_buf; +} diff --git a/wasm/setjmp.h b/wasm/setjmp.h new file mode 100644 index 0000000000..cc14df33be --- /dev/null +++ b/wasm/setjmp.h @@ -0,0 +1,95 @@ +#ifndef RB_WASM_SUPPORT_SETJMP_H +#define RB_WASM_SUPPORT_SETJMP_H + +#include "ruby/internal/config.h" +#include <stdbool.h> + +#ifndef WASM_SETJMP_STACK_BUFFER_SIZE +# define WASM_SETJMP_STACK_BUFFER_SIZE 6144 +#endif + +struct __rb_wasm_asyncify_jmp_buf { + void* top; + void* end; + char buffer[WASM_SETJMP_STACK_BUFFER_SIZE]; +}; + +typedef struct { + // Internal Asyncify buffer space to save execution context + struct __rb_wasm_asyncify_jmp_buf setjmp_buf; + // Internal Asyncify buffer space used while unwinding from longjmp + // but never used for rewinding. + struct __rb_wasm_asyncify_jmp_buf *longjmp_buf_ptr; + // Used to save top address of Asyncify stack `setjmp_buf`, which is + // overwritten during first rewind. + void *dst_buf_top; + // A payload value given by longjmp and returned by setjmp for the second time + int payload; + // Internal state field + int state; +} rb_wasm_jmp_buf; + +// noinline to avoid breaking Asyncify assumption +NOINLINE(int _rb_wasm_setjmp(rb_wasm_jmp_buf *env)); +NOINLINE(void _rb_wasm_longjmp(rb_wasm_jmp_buf *env, int payload)); + +#define rb_wasm_setjmp(env) ((env).state = 0, _rb_wasm_setjmp(&(env))) + +// NOTE: Why is `_rb_wasm_longjmp` not `noreturn`? Why put `unreachable` in the call site? +// Asyncify expects that `_rb_wasm_longjmp` returns its control, and Asyncify inserts a return +// for unwinding after the call. This means that "`_rb_wasm_longjmp` returns its control but the +// next line in the caller (C level) won't be executed". +// On the other hand, `noreturn` means the callee won't return its control to the caller, +// so compiler can assume that a function with the attribute won't reach the end of the function. +// Therefore `_rb_wasm_longjmp`'s semantics is not exactly same as `noreturn`. +#define rb_wasm_longjmp(env, payload) (_rb_wasm_longjmp(&env, payload), __builtin_unreachable()) + +// Returns the Asyncify buffer of next rewinding if unwound for setjmp capturing or longjmp. +// Used by the top level Asyncify handling in wasm/runtime.c +void *rb_wasm_handle_jmp_unwind(void); + + +// +// POSIX-compatible declarations +// + +typedef rb_wasm_jmp_buf jmp_buf; + +#define setjmp(env) rb_wasm_setjmp(env) +#define longjmp(env, payload) rb_wasm_longjmp(env, payload) + + +typedef void (*rb_wasm_try_catch_func_t)(void *ctx); + +struct rb_wasm_try_catch { + rb_wasm_try_catch_func_t try_f; + rb_wasm_try_catch_func_t catch_f; + void *context; + int state; +}; + +// +// Lightweight try-catch API without unwinding to root frame. +// + +void +rb_wasm_try_catch_init(struct rb_wasm_try_catch *try_catch, + rb_wasm_try_catch_func_t try_f, + rb_wasm_try_catch_func_t catch_f, + void *context); + +// Run, catch longjmp thrown by run, and re-catch longjmp thrown by catch, ... +// +// 1. run try_f of try_catch struct +// 2. catch longjmps with the given target jmp_buf or exit +// 3. run catch_f if not NULL, otherwise exit +// 4. catch longjmps with the given target jmp_buf or exit +// 5. repeat from step 3 +// +// NOTICE: This API assumes that all longjmp targeting the given jmp_buf are NOT called +// after the function that called this function has exited. +// +void +rb_wasm_try_catch_loop_run(struct rb_wasm_try_catch *try_catch, rb_wasm_jmp_buf *target); + +#endif diff --git a/wasm/setjmp_core.S b/wasm/setjmp_core.S new file mode 100644 index 0000000000..4a7194056e --- /dev/null +++ b/wasm/setjmp_core.S @@ -0,0 +1,27 @@ + # extern int _rb_wasm_setjmp_internal(rb_wasm_jmp_buf *env); + .functype _rb_wasm_setjmp_internal (i32) -> (i32) + # extern int __stack_pointer; + .globaltype __stack_pointer, i32 + + # A wrapper of _rb_wasm_setjmp_internal to save and restore stack pointer + # This cannot be implemented in C because there is no way to manipulate stack pointer + # without C-epilogue. + + # extern int _rb_wasm_setjmp(rb_wasm_jmp_buf *env); + .section .text._rb_wasm_setjmp,"",@ + .globl _rb_wasm_setjmp + .type _rb_wasm_setjmp,@function +_rb_wasm_setjmp: + .functype _rb_wasm_setjmp (i32) -> (i32) + .local i32, i32 + # save sp (this local is stored in asyncify stack and restored when rewinding) + global.get __stack_pointer + local.set 1 + + local.get 0 + call _rb_wasm_setjmp_internal + + # restore sp + local.get 1 + global.set __stack_pointer + end_function diff --git a/wasm/tests/fiber_test.c b/wasm/tests/fiber_test.c new file mode 100644 index 0000000000..e6b36631ce --- /dev/null +++ b/wasm/tests/fiber_test.c @@ -0,0 +1,66 @@ +#include "wasm/fiber.h" +#include "wasm/asyncify.h" +#include <stdio.h> +#include <stdlib.h> +#include <assert.h> + +static rb_wasm_fiber_context fctx_main, fctx_func1, fctx_func2; + +static int counter = 0; + +static void func1(void *arg0, void *arg1) { + assert(counter == 2); + fprintf(stderr, "func1: started\n"); + fprintf(stderr, "func1: swapcontext(&fctx_func1, &fctx_func2)\n"); + counter++; + rb_wasm_swapcontext(&fctx_func1, &fctx_func2); + + fprintf(stderr, "func1: returning\n"); +} + +static void func2(void *arg0, void *arg1) { + assert(counter == 1); + fprintf(stderr, "func2: started\n"); + fprintf(stderr, "func2: swapcontext(&fctx_func2, &fctx_func1)\n"); + counter++; + rb_wasm_swapcontext(&fctx_func2, &fctx_func1); + + assert(counter == 3); + fprintf(stderr, "func2: swapcontext(&fctx_func2, &fctx_func2)\n"); + counter++; + rb_wasm_swapcontext(&fctx_func2, &fctx_func2); + + assert(counter == 4); + fprintf(stderr, "func2: swapcontext(&fctx_func2, &fctx_main)\n"); + counter++; + rb_wasm_swapcontext(&fctx_func2, &fctx_main); + + fprintf(stderr, "func2: returning\n"); + assert(false && "unreachable"); +} + +// top level function should not be inlined to stop unwinding immediately after this function returns +__attribute__((noinline)) +int start(int argc, char **argv) { + rb_wasm_init_context(&fctx_main, NULL, NULL, NULL); + fctx_main.is_started = true; + + rb_wasm_init_context(&fctx_func1, func1, NULL, NULL); + + rb_wasm_init_context(&fctx_func2, func2, NULL, NULL); + + counter++; + fprintf(stderr, "start: swapcontext(&uctx_main, &fctx_func2)\n"); + rb_wasm_swapcontext(&fctx_main, &fctx_func2); + assert(counter == 5); + + fprintf(stderr, "start: exiting\n"); + return 42; +} + +int main(int argc, char **argv) { + extern int rb_wasm_rt_start(int (main)(int argc, char **argv), int argc, char **argv); + int result = rb_wasm_rt_start(start, argc, argv); + assert(result == 42); + return 0; +} diff --git a/wasm/tests/machine_test.c b/wasm/tests/machine_test.c new file mode 100644 index 0000000000..f4b62ff580 --- /dev/null +++ b/wasm/tests/machine_test.c @@ -0,0 +1,115 @@ +#include <stdio.h> +#include <assert.h> +#include <stdint.h> +#include <stdbool.h> +#include "wasm/machine.h" +#include "wasm/asyncify.h" + +void *rb_wasm_get_stack_pointer(void); + +static void *base_stack_pointer = NULL; + +int __attribute__((constructor)) record_base_sp(void) { + base_stack_pointer = rb_wasm_get_stack_pointer(); + return 0; +} + +void dump_memory(uint8_t *base, uint8_t *end) { + size_t chunk_size = 16; + + for (uint8_t *ptr = base; ptr <= end; ptr += chunk_size) { + printf("%p", ptr); + for (size_t offset = 0; offset < chunk_size; offset++) { + printf(" %02x", *(ptr + offset)); + } + printf("\n"); + } +} + +bool find_in_stack(uint32_t target, void *base, void *end) { + for (uint32_t *ptr = base; ptr <= (uint32_t *)end; ptr++) { + if (*ptr == target) { + return true; + } + } + return false; +} + +void *_rb_wasm_stack_mem[2]; +void rb_wasm_mark_mem_range(void *start, void *end) { + _rb_wasm_stack_mem[0] = start; + _rb_wasm_stack_mem[1] = end; +} + +#define check_live(target, ctx) do { \ + rb_wasm_scan_stack(rb_wasm_mark_mem_range); \ + _check_live(target, ctx); \ + } while (0); + +void _check_live(uint32_t target, const char *ctx) { + printf("checking %#x ... ", target); + bool found_in_locals = false, found_in_stack = false; + if (find_in_stack(target, _rb_wasm_stack_mem[0], _rb_wasm_stack_mem[1])) { + found_in_stack = true; + } + rb_wasm_scan_locals(rb_wasm_mark_mem_range); + if (find_in_stack(target, _rb_wasm_stack_mem[0], _rb_wasm_stack_mem[1])) { + found_in_locals = true; + } + if (found_in_locals && found_in_stack) { + printf("ok (found in C stack and Wasm locals)\n"); + } else if (found_in_stack) { + printf("ok (found in C stack)\n"); + } else if (found_in_locals) { + printf("ok (found in Wasm locals)\n"); + } else { + printf("not found: %s\n", ctx); + assert(false); + } +} + +void new_frame(uint32_t val, uint32_t depth) { + if (depth == 0) { + dump_memory(rb_wasm_get_stack_pointer(), base_stack_pointer); + for (uint32_t i = 0; i < 5; i++) { + check_live(0x00bab10c + i, "argument value"); + } + } else { + new_frame(val, depth - 1); + } +} + +uint32_t return_value(void) { + return 0xabadbabe; +} + +uint32_t check_return_value(void) { + check_live(0xabadbabe, "returned value"); + return 0; +} + +void take_two_args(uint32_t a, uint32_t b) { +} + +__attribute__((noinline)) +int start(int argc, char **argv) { + + uint32_t deadbeef; + register uint32_t facefeed; + deadbeef = 0xdeadbeef; + facefeed = 0xfacefeed; + + check_live(0xdeadbeef, "local variable"); + check_live(0xfacefeed, "local reg variable"); + + new_frame(0x00bab10c, 5); + + take_two_args(return_value(), check_return_value()); + + return 0; +} + +int main(int argc, char **argv) { + extern int rb_wasm_rt_start(int (main)(int argc, char **argv), int argc, char **argv); + return rb_wasm_rt_start(start, argc, argv); +} diff --git a/wasm/tests/setjmp_test.c b/wasm/tests/setjmp_test.c new file mode 100644 index 0000000000..f263dcfa3e --- /dev/null +++ b/wasm/tests/setjmp_test.c @@ -0,0 +1,108 @@ +#include "wasm/setjmp.h" +#include "wasm/asyncify.h" +#include "wasm/machine.h" +#include <stdio.h> +#include <assert.h> + +void check_direct(void) { + rb_wasm_jmp_buf buf; + int val; + printf("[%s] start\n", __func__); + printf("[%s] call rb_wasm_setjmp\n", __func__); + if ((val = rb_wasm_setjmp(buf)) == 0) { + printf("[%s] rb_wasm_setjmp(buf) == 0\n", __func__); + printf("[%s] call rb_wasm_longjmp(buf, 2)\n", __func__); + rb_wasm_longjmp(buf, 2); + assert(0 && "unreachable after longjmp"); + } else { + printf("[%s] rb_wasm_setjmp(buf) == %d\n", __func__, val); + printf("[%s] sp = %p\n", __func__, rb_wasm_get_stack_pointer()); + assert(val == 2 && "unexpected returned value"); + } + printf("[%s] end\n", __func__); +} + +void jump_to_dst(rb_wasm_jmp_buf *dst) { + rb_wasm_jmp_buf buf; + printf("[%s] start sp = %p\n", __func__, rb_wasm_get_stack_pointer()); + printf("[%s] call rb_wasm_setjmp\n", __func__); + if (rb_wasm_setjmp(buf) == 0) { + printf("[%s] rb_wasm_setjmp(buf) == 0\n", __func__); + printf("[%s] call rb_wasm_longjmp(dst, 4)\n", __func__); + rb_wasm_longjmp(*dst, 4); + assert(0 && "unreachable after longjmp"); + } else { + assert(0 && "unreachable"); + } + printf("[%s] end\n", __func__); +} + +void check_jump_two_level(void) { + rb_wasm_jmp_buf buf; + int val; + printf("[%s] start\n", __func__); + printf("[%s] call rb_wasm_setjmp\n", __func__); + if ((val = rb_wasm_setjmp(buf)) == 0) { + printf("[%s] rb_wasm_setjmp(buf) == 0\n", __func__); + printf("[%s] call jump_to_dst(&buf)\n", __func__); + jump_to_dst(&buf); + assert(0 && "unreachable after longjmp"); + } else { + printf("[%s] rb_wasm_setjmp(buf) == %d\n", __func__, val); + assert(val == 4 && "unexpected returned value"); + } + printf("[%s] end\n", __func__); +} + +void check_reuse(void) { + rb_wasm_jmp_buf buf; + int val; + printf("[%s] start\n", __func__); + printf("[%s] call rb_wasm_setjmp\n", __func__); + if ((val = rb_wasm_setjmp(buf)) == 0) { + printf("[%s] rb_wasm_setjmp(buf) == 0\n", __func__); + printf("[%s] call rb_wasm_longjmp(buf, 2)\n", __func__); + rb_wasm_longjmp(buf, 2); + assert(0 && "unreachable after longjmp"); + } else { + printf("[%s] rb_wasm_setjmp(buf) == %d\n", __func__, val); + if (val < 5) { + printf("[%s] re-call rb_wasm_longjmp(buf, %d)\n", __func__, val + 1); + rb_wasm_longjmp(buf, val + 1); + } + } + printf("[%s] end\n", __func__); +} + +void check_stack_ptr(void) { + static void *normal_sp; + rb_wasm_jmp_buf buf; + normal_sp = rb_wasm_get_stack_pointer(); + + printf("[%s] start sp = %p\n", __func__, normal_sp); + printf("[%s] call rb_wasm_setjmp\n", __func__); + if (rb_wasm_setjmp(buf) == 0) { + printf("[%s] call jump_to_dst(&buf)\n", __func__); + jump_to_dst(&buf); + assert(0 && "unreachable after longjmp"); + } else { + printf("[%s] sp = %p\n", __func__, rb_wasm_get_stack_pointer()); + assert(rb_wasm_get_stack_pointer() == normal_sp); + } + printf("[%s] end\n", __func__); +} + +// top level function should not be inlined to stop unwinding immediately after this function returns +__attribute__((noinline)) +int start(int argc, char **argv) { + check_direct(); + check_jump_two_level(); + check_reuse(); + check_stack_ptr(); + return 0; +} + +int main(int argc, char **argv) { + extern int rb_wasm_rt_start(int (main)(int argc, char **argv), int argc, char **argv); + return rb_wasm_rt_start(start, argc, argv); +} diff --git a/wasm/wasm-opt b/wasm/wasm-opt new file mode 100755 index 0000000000..3ae21b8206 --- /dev/null +++ b/wasm/wasm-opt @@ -0,0 +1,36 @@ +#!/bin/sh +# A fake wasm-opt, which does nothing at all +# See also: tool/wasm-clangw + +set -e +input= +output= +while [ $# -ne 0 ]; do + case "$1" in + -o) + shift + output=$1 + ;; + -*) + # ignore other options + ;; + *) + input=$1 + ;; + esac + shift +done + +if [ -z "$input" ]; then + echo "missing input binary" + exit 1 +fi + +if [ -z "$output" ]; then + echo "missing output binary" + exit 1 +fi + +if [ "$input" != "$output" ]; then + cp "$input" "$output" +fi |