1
0
Fork 0
mirror of https://github.com/ruby/ruby.git synced 2022-11-09 12:17:21 -05:00
ruby--ruby/wasm/setjmp.c
Yuta Saito dff70b50d0 [wasm] vm.c: stop unwinding to main for every vm_exec call by setjmp
the original rb_wasm_setjmp implementation always unwinds to the root
call frame to have setjmp compatible interface, and simulate sjlj's
undefined behavior. Therefore, every vm_exec call unwinds to main, and
a deep call stack makes setjmp call very expensive. The following
snippet from optcarrot takes 5s even though it takes less than 0.3s on
native.

```
[0x0, 0x4, 0x8, 0xc].map do |attr|
  (0..7).map do |j|
    (0...0x10000).map do |i|
      clr = i[15 - j] * 2 + i[7 - j]
      clr != 0 ? attr | clr : 0
    end
  end
end
```

This patch adds a WASI specialized vm_exec which uses lightweight
try-catch API without unwinding to the root frame. After this patch, the
above snippet takes only 0.5s.
2022-02-18 18:28:18 +09:00

204 lines
6.7 KiB
C

/*
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;
void *rb_asyncify_unwind_buf;
__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);
}
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;
}
}
while (1) {
// catch longjmp with target jmp_buf
if (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);
}
continue;
} else if (rb_asyncify_unwind_buf /* unrelated unwind */) {
return;
}
// no unwind, then exit
break;
}
return;
}
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;
}