mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
[wasm] add asyncify based setjmp, fiber, register scan emulation
configure.ac: setup build tools and register objects main.c: wrap main with rb_wasm_rt_start to handle asyncify unwinds tool/m4/ruby_wasm_tools.m4: setup default command based on WASI_SDK_PATH environment variable. checks wasm-opt which is used for asyncify. tool/wasm-clangw wasm/wasm-opt: a clang wrapper which replaces real wasm-opt with do-nothing wasm-opt to avoid misoptimization before asyncify. asyncify is performed at POSTLINK, but clang linker driver tries to run optimization by wasm-opt unconditionally. inlining pass at wasm level breaks asyncify's assumption, so should not optimize before POSTLIK. wasm/GNUmakefile.in: wasm specific rules to compile objects
This commit is contained in:
parent
e41b121e94
commit
65f95f26ff
Notes:
git
2022-01-19 11:19:45 +09:00
16 changed files with 640 additions and 1 deletions
24
configure.ac
24
configure.ac
|
@ -42,6 +42,7 @@ m4_include([tool/m4/ruby_try_cflags.m4])dnl
|
|||
m4_include([tool/m4/ruby_try_cxxflags.m4])dnl
|
||||
m4_include([tool/m4/ruby_try_ldflags.m4])dnl
|
||||
m4_include([tool/m4/ruby_universal_arch.m4])dnl
|
||||
m4_include([tool/m4/ruby_wasm_tools.m4])dnl
|
||||
m4_include([tool/m4/ruby_werror_flag.m4])dnl
|
||||
|
||||
AC_ARG_VAR([cflags], [additional CFLAGS (ignored when CFLAGS is given)])dnl
|
||||
|
@ -136,6 +137,9 @@ AS_IF([test ! -z "$ac_cv_prog_CC" -a ! -z "$CC" -a "$CC" != "$ac_cv_prog_CC"], [
|
|||
AC_MSG_ERROR(cached CC is different -- throw away $cache_file
|
||||
(it is also a good idea to do 'make clean' before compiling))
|
||||
])
|
||||
|
||||
RUBY_WASM_TOOLS
|
||||
|
||||
AS_CASE(["${build_os}"],
|
||||
[linux*|cygwin*|msys*], [
|
||||
# Naruse prefers GCC on Linux
|
||||
|
@ -402,6 +406,15 @@ AS_CASE(["$build_os"],
|
|||
])
|
||||
rm -fr conftest*
|
||||
])
|
||||
AS_CASE(["$target_os"],
|
||||
[wasi*], [
|
||||
# Clang linker automatically uses wasm-opt with -O if it found.
|
||||
# https://github.com/llvm/llvm-project/blob/812828984c10857a4cd260eb638c52a4411f9143/clang/lib/Driver/ToolChains/WebAssembly.cpp#L95-L118
|
||||
# However optimization before asyncify causes misoptimization,
|
||||
# so wrap clang to insert our fake wasm-opt, which does nothing, in PATH.
|
||||
CC_WRAPPER=`cd -P "${tooldir}" && pwd`/wasm-clangw
|
||||
CC="$CC_WRAPPER $CC"
|
||||
])
|
||||
|
||||
cc_version=
|
||||
for option in --version -v -V -qversion; do
|
||||
|
@ -1218,6 +1231,7 @@ main()
|
|||
[wasi*],[ LIBS="-lm -lwasi-emulated-mman -lwasi-emulated-signal -lwasi-emulated-getpid -lwasi-emulated-process-clocks $LIBS"
|
||||
RUBY_APPEND_OPTIONS(CFLAGS, -D_WASI_EMULATED_SIGNAL -D_WASI_EMULATED_MMAN -D_WASI_EMULATED_GETPID -D_WASI_EMULATED_PROCESS_CLOCKS)
|
||||
RUBY_APPEND_OPTIONS(CPPFLAGS, -D_WASI_EMULATED_SIGNAL -D_WASI_EMULATED_MMAN -D_WASI_EMULATED_GETPID -D_WASI_EMULATED_PROCESS_CLOCKS)
|
||||
POSTLINK="\$(WASMOPT) --asyncify \$(wasmoptflags) --pass-arg=asyncify-ignore-imports -o \$@ \$@${POSTLINK:+; $POSTLINK}"
|
||||
]
|
||||
[ LIBS="-lm $LIBS"])
|
||||
: ${ORIG_LIBS=$LIBS}
|
||||
|
@ -3790,6 +3804,16 @@ AS_CASE(["$target_os"],
|
|||
LIBRUBY='lib$(RUBY_SO_NAME).a'
|
||||
LIBRUBYARG='-l$(RUBY_SO_NAME)'
|
||||
])
|
||||
],
|
||||
[wasi*], [
|
||||
FIRSTMAKEFILE=GNUmakefile:wasm/GNUmakefile.in
|
||||
AC_LIBOBJ([wasm/runtime])
|
||||
AC_LIBOBJ([wasm/fiber])
|
||||
AC_LIBOBJ([wasm/machine])
|
||||
AC_LIBOBJ([wasm/setjmp])
|
||||
AC_LIBOBJ([wasm/machine_core])
|
||||
AC_LIBOBJ([wasm/setjmp_core])
|
||||
PLATFORM_DIR=wasm
|
||||
])
|
||||
|
||||
MINIOBJS="$MINIDLNOBJ"
|
||||
|
|
11
main.c
11
main.c
|
@ -31,7 +31,7 @@
|
|||
#endif
|
||||
|
||||
int
|
||||
main(int argc, char **argv)
|
||||
rb_main(int argc, char **argv)
|
||||
{
|
||||
#ifdef RUBY_DEBUG_ENV
|
||||
ruby_set_debug_option(getenv("RUBY_DEBUG"));
|
||||
|
@ -47,3 +47,12 @@ main(int argc, char **argv)
|
|||
return ruby_run_node(ruby_options(argc, argv));
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
#if defined(__wasm__) && !defined(__EMSCRIPTEN__)
|
||||
int rb_wasm_rt_start(int (main)(int argc, char **argv), int argc, char **argv);
|
||||
return rb_wasm_rt_start(rb_main, argc, argv);
|
||||
#else
|
||||
return rb_main(argc, argv);
|
||||
#endif
|
||||
}
|
||||
|
|
21
tool/m4/ruby_wasm_tools.m4
Normal file
21
tool/m4/ruby_wasm_tools.m4
Normal file
|
@ -0,0 +1,21 @@
|
|||
dnl -*- Autoconf -*-
|
||||
AC_DEFUN([RUBY_WASM_TOOLS],
|
||||
[AS_CASE(["$target_os"],
|
||||
[wasi*], [
|
||||
AC_CHECK_TOOL(WASMOPT, wasm-opt)
|
||||
AS_IF([test x"${WASMOPT}" = x], [
|
||||
AC_MSG_ERROR([wasm-opt is required])
|
||||
])
|
||||
AC_SUBST(wasmoptflags)
|
||||
: ${wasmoptflags=-O3}
|
||||
|
||||
AC_MSG_CHECKING([wheather \$WASI_SDK_PATH is set])
|
||||
AS_IF([test x"${WASI_SDK_PATH}" = x], [AC_MSG_RESULT([no])], [
|
||||
AC_MSG_RESULT([yes])
|
||||
CC="${WASI_SDK_PATH}/bin/clang"
|
||||
LD="${WASI_SDK_PATH}/bin/clang"
|
||||
AR="${WASI_SDK_PATH}/bin/llvm-ar"
|
||||
RANLIB="${WASI_SDK_PATH}/bin/llvm-ranlib"
|
||||
])
|
||||
])
|
||||
])dnl
|
9
tool/wasm-clangw
Executable file
9
tool/wasm-clangw
Executable file
|
@ -0,0 +1,9 @@
|
|||
#!/bin/sh
|
||||
# A Clang wrapper script to fake the clang linker driver.
|
||||
# Clang linker automatically uses wasm-opt with -O if it found.
|
||||
# However optimization before asyncify causes misoptimization,
|
||||
# so wrap clang to insert our fake wasm-opt, which does nothing, in PATH.
|
||||
|
||||
src_dir="$(cd "$(dirname "$0")/../wasm" && pwd)"
|
||||
export PATH="$src_dir:$PATH"
|
||||
exec "$@"
|
19
wasm/GNUmakefile.in
Normal file
19
wasm/GNUmakefile.in
Normal file
|
@ -0,0 +1,19 @@
|
|||
include Makefile
|
||||
include $(srcdir)/template/GNUmakefile.in
|
||||
|
||||
wasmdir = $(srcdir)/wasm
|
||||
GNUmakefile: $(wasmdir)/GNUmakefile.in
|
||||
WASMOPT = @WASMOPT@
|
||||
wasmoptflags = @wasmoptflags@
|
||||
|
||||
WASM_OBJS = $(wasmdir)/machine_core.o $(wasmdir)/machine.o $(wasmdir)/setjmp.o $(wasmdir)/setjmp_core.o $(wasmdir)/fiber.o $(wasmdir)/runtime.o
|
||||
|
||||
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 $<
|
||||
|
13
wasm/asyncify.h
Normal file
13
wasm/asyncify.h
Normal file
|
@ -0,0 +1,13 @@
|
|||
#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);
|
||||
__attribute__((import_module("asyncify"), import_name("stop_unwind")))
|
||||
void asyncify_stop_unwind(void);
|
||||
__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
|
83
wasm/fiber.c
Normal file
83
wasm/fiber.c
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
43
wasm/fiber.h
Normal file
43
wasm/fiber.h
Normal file
|
@ -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
|
62
wasm/machine.c
Normal file
62
wasm/machine.c
Normal file
|
@ -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_scan_stack(rb_wasm_scan_func scan, void *current)
|
||||
{
|
||||
scan(current, rb_wasm_stack_base);
|
||||
}
|
||||
|
||||
void *
|
||||
rb_wasm_handle_scan_unwind(void)
|
||||
{
|
||||
return _rb_wasm_active_scan_buf;
|
||||
}
|
26
wasm/machine.h
Normal file
26
wasm/machine.h
Normal file
|
@ -0,0 +1,26 @@
|
|||
#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);
|
||||
|
||||
// Scan userland C-stack memory space in WebAssembly. Used by conservative GC
|
||||
#define rb_wasm_scan_stack(scan) _rb_wasm_scan_stack((scan), rb_wasm_get_stack_pointer())
|
||||
void _rb_wasm_scan_stack(rb_wasm_scan_func scan, void *current);
|
||||
|
||||
|
||||
// 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
|
25
wasm/machine_core.S
Normal file
25
wasm/machine_core.S
Normal file
|
@ -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
|
||||
|
47
wasm/runtime.c
Normal file
47
wasm/runtime.c
Normal file
|
@ -0,0 +1,47 @@
|
|||
#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);
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
134
wasm/setjmp.c
Normal file
134
wasm/setjmp.c
Normal file
|
@ -0,0 +1,134 @@
|
|||
/*
|
||||
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 <stdio.h>
|
||||
# define RB_WASM_DEBUG_LOG(...) fprintf(stderr, __VA_ARGS__)
|
||||
#else
|
||||
# define RB_WASM_DEBUG_LOG(...)
|
||||
#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;
|
||||
|
||||
__attribute__((noinline))
|
||||
int
|
||||
_rb_wasm_setjmp_internal(rb_wasm_jmp_buf *env)
|
||||
{
|
||||
RB_WASM_DEBUG_LOG("[%s] env = %p, env->state = %d, _rb_wasm_active_jmpbuf = %p\n", __func__, env, env->state, _rb_wasm_active_jmpbuf);
|
||||
switch (env->state) {
|
||||
case JMP_BUF_STATE_INITIALIZED: {
|
||||
RB_WASM_DEBUG_LOG("[%s] JMP_BUF_STATE_INITIALIZED\n", __func__);
|
||||
env->state = JMP_BUF_STATE_CAPTURING;
|
||||
env->payload = 0;
|
||||
_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("[%s] JMP_BUF_STATE_CAPTURING\n", __func__);
|
||||
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("[%s] JMP_BUF_STATE_RETURNING\n", __func__);
|
||||
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("[%s] env = %p, env->state = %d, value = %d\n", __func__, env, env->state, value);
|
||||
assert(env->state == JMP_BUF_STATE_CAPTURED);
|
||||
assert(value != 0);
|
||||
env->state = JMP_BUF_STATE_RETURNING;
|
||||
env->payload = value;
|
||||
_rb_wasm_active_jmpbuf = env;
|
||||
async_buf_init(&env->longjmp_buf);
|
||||
asyncify_start_unwind(&env->longjmp_buf);
|
||||
}
|
||||
|
||||
void *
|
||||
rb_wasm_handle_jmp_unwind(void)
|
||||
{
|
||||
RB_WASM_DEBUG_LOG("[%s] _rb_wasm_active_jmpbuf = %p\n", __func__, _rb_wasm_active_jmpbuf);
|
||||
if (!_rb_wasm_active_jmpbuf) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
switch (_rb_wasm_active_jmpbuf->state) {
|
||||
case JMP_BUF_STATE_CAPTURING: {
|
||||
RB_WASM_DEBUG_LOG("[%s] JMP_BUF_STATE_CAPTURING\n", __func__);
|
||||
// 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("[%s] JMP_BUF_STATE_RETURNING\n", __func__);
|
||||
// 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;
|
||||
}
|
61
wasm/setjmp.h
Normal file
61
wasm/setjmp.h
Normal file
|
@ -0,0 +1,61 @@
|
|||
#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;
|
||||
// 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)
|
||||
|
||||
#endif
|
27
wasm/setjmp_core.S
Normal file
27
wasm/setjmp_core.S
Normal file
|
@ -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
|
36
wasm/wasm-opt
Executable file
36
wasm/wasm-opt
Executable file
|
@ -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
|
Loading…
Reference in a new issue