summaryrefslogtreecommitdiff
path: root/wasm
diff options
context:
space:
mode:
Diffstat (limited to 'wasm')
-rw-r--r--wasm/GNUmakefile.in32
-rw-r--r--wasm/README.md70
-rw-r--r--wasm/asyncify.h23
-rw-r--r--wasm/fiber.c83
-rw-r--r--wasm/fiber.h43
-rw-r--r--wasm/machine.c62
-rw-r--r--wasm/machine.h25
-rw-r--r--wasm/machine_core.S25
-rw-r--r--wasm/missing.c199
-rw-r--r--wasm/runtime.c54
-rw-r--r--wasm/setjmp.c215
-rw-r--r--wasm/setjmp.h95
-rw-r--r--wasm/setjmp_core.S27
-rw-r--r--wasm/tests/fiber_test.c66
-rw-r--r--wasm/tests/machine_test.c115
-rw-r--r--wasm/tests/setjmp_test.c108
-rwxr-xr-xwasm/wasm-opt36
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