[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
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_cxxflags.m4])dnl
|
||||||
m4_include([tool/m4/ruby_try_ldflags.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_universal_arch.m4])dnl
|
||||||
|
m4_include([tool/m4/ruby_wasm_tools.m4])dnl
|
||||||
m4_include([tool/m4/ruby_werror_flag.m4])dnl
|
m4_include([tool/m4/ruby_werror_flag.m4])dnl
|
||||||
|
|
||||||
AC_ARG_VAR([cflags], [additional CFLAGS (ignored when CFLAGS is given)])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
|
AC_MSG_ERROR(cached CC is different -- throw away $cache_file
|
||||||
(it is also a good idea to do 'make clean' before compiling))
|
(it is also a good idea to do 'make clean' before compiling))
|
||||||
])
|
])
|
||||||
|
|
||||||
|
RUBY_WASM_TOOLS
|
||||||
|
|
||||||
AS_CASE(["${build_os}"],
|
AS_CASE(["${build_os}"],
|
||||||
[linux*|cygwin*|msys*], [
|
[linux*|cygwin*|msys*], [
|
||||||
# Naruse prefers GCC on Linux
|
# Naruse prefers GCC on Linux
|
||||||
@ -402,6 +406,15 @@ AS_CASE(["$build_os"],
|
|||||||
])
|
])
|
||||||
rm -fr conftest*
|
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=
|
cc_version=
|
||||||
for option in --version -v -V -qversion; do
|
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"
|
[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(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)
|
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"])
|
[ LIBS="-lm $LIBS"])
|
||||||
: ${ORIG_LIBS=$LIBS}
|
: ${ORIG_LIBS=$LIBS}
|
||||||
@ -3790,6 +3804,16 @@ AS_CASE(["$target_os"],
|
|||||||
LIBRUBY='lib$(RUBY_SO_NAME).a'
|
LIBRUBY='lib$(RUBY_SO_NAME).a'
|
||||||
LIBRUBYARG='-l$(RUBY_SO_NAME)'
|
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"
|
MINIOBJS="$MINIDLNOBJ"
|
||||||
|
11
main.c
11
main.c
@ -31,7 +31,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
int
|
int
|
||||||
main(int argc, char **argv)
|
rb_main(int argc, char **argv)
|
||||||
{
|
{
|
||||||
#ifdef RUBY_DEBUG_ENV
|
#ifdef RUBY_DEBUG_ENV
|
||||||
ruby_set_debug_option(getenv("RUBY_DEBUG"));
|
ruby_set_debug_option(getenv("RUBY_DEBUG"));
|
||||||
@ -47,3 +47,12 @@ main(int argc, char **argv)
|
|||||||
return ruby_run_node(ruby_options(argc, 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…
x
Reference in New Issue
Block a user