Ruby 3.2.3p157 (2024-01-18 revision 52bb2ac0a6971d0391efa2275f7a66bff319087c)
setjmp.c
1/*
2 This is a WebAssembly userland setjmp/longjmp implementation based on Binaryen's Asyncify.
3 Inspired by Alon Zakai's snippet released under the MIT License:
4 * https://github.com/kripken/talks/blob/991fb1e4b6d7e4b0ea6b3e462d5643f11d422771/jmp.c
5
6 WebAssembly doesn't have context-switching mechanism for now, so emulate it by Asyncify,
7 which transforms WebAssembly binary to unwind/rewind the execution point and store/restore
8 locals.
9
10 The basic concept of this implementation is:
11 1. setjmp captures the current execution context by unwinding to the root frame, then immediately
12 rewind to the setjmp call using the captured context. The context is saved in jmp_buf.
13 2. longjmp unwinds to the root frame and rewinds to a setjmp call re-using a passed jmp_buf.
14
15 This implementation also supports switching context across different call stack (non-standard)
16
17 This approach is good at behavior reproducibility and self-containedness compared to Emscripten's
18 JS exception approach. However this is super expensive because Asyncify inserts many glue code to
19 control execution point in userland.
20
21 This implementation will be replaced with future stack-switching feature.
22 */
23#include <stdint.h>
24#include <stdlib.h>
25#include <assert.h>
26#include <stdbool.h>
27#include "wasm/asyncify.h"
28#include "wasm/machine.h"
29#include "wasm/setjmp.h"
30
31#ifdef RB_WASM_ENABLE_DEBUG_LOG
32# include <stdio.h>
33# define RB_WASM_DEBUG_LOG(...) fprintf(stderr, __VA_ARGS__)
34#else
35# define RB_WASM_DEBUG_LOG(...)
36#endif
37
38enum rb_wasm_jmp_buf_state {
39 // Initial state
40 JMP_BUF_STATE_INITIALIZED = 0,
41 // Unwinding to the root or rewinding to the setjmp call
42 // to capture the current execution context
43 JMP_BUF_STATE_CAPTURING = 1,
44 // Ready for longjmp
45 JMP_BUF_STATE_CAPTURED = 2,
46 // Unwinding to the root or rewinding to the setjmp call
47 // to restore the execution context
48 JMP_BUF_STATE_RETURNING = 3,
49};
50
51void
52async_buf_init(struct __rb_wasm_asyncify_jmp_buf* buf)
53{
54 buf->top = &buf->buffer[0];
55 buf->end = &buf->buffer[WASM_SETJMP_STACK_BUFFER_SIZE];
56}
57
58// Global unwinding/rewinding jmpbuf state
59static rb_wasm_jmp_buf *_rb_wasm_active_jmpbuf;
60void *rb_asyncify_unwind_buf;
61
62__attribute__((noinline))
63int
64_rb_wasm_setjmp_internal(rb_wasm_jmp_buf *env)
65{
66 RB_WASM_DEBUG_LOG("[%s] env = %p, env->state = %d, _rb_wasm_active_jmpbuf = %p\n", __func__, env, env->state, _rb_wasm_active_jmpbuf);
67 switch (env->state) {
68 case JMP_BUF_STATE_INITIALIZED: {
69 RB_WASM_DEBUG_LOG("[%s] JMP_BUF_STATE_INITIALIZED\n", __func__);
70 env->state = JMP_BUF_STATE_CAPTURING;
71 env->payload = 0;
72 _rb_wasm_active_jmpbuf = env;
73 async_buf_init(&env->setjmp_buf);
74 asyncify_start_unwind(&env->setjmp_buf);
75 return -1; // return a dummy value
76 }
77 case JMP_BUF_STATE_CAPTURING: {
78 asyncify_stop_rewind();
79 RB_WASM_DEBUG_LOG("[%s] JMP_BUF_STATE_CAPTURING\n", __func__);
80 env->state = JMP_BUF_STATE_CAPTURED;
81 _rb_wasm_active_jmpbuf = NULL;
82 return 0;
83 }
84 case JMP_BUF_STATE_RETURNING: {
85 asyncify_stop_rewind();
86 RB_WASM_DEBUG_LOG("[%s] JMP_BUF_STATE_RETURNING\n", __func__);
87 env->state = JMP_BUF_STATE_CAPTURED;
88 _rb_wasm_active_jmpbuf = NULL;
89 return env->payload;
90 }
91 default:
92 assert(0 && "unexpected state");
93 }
94 return 0;
95}
96
97void
98_rb_wasm_longjmp(rb_wasm_jmp_buf* env, int value)
99{
100 RB_WASM_DEBUG_LOG("[%s] env = %p, env->state = %d, value = %d\n", __func__, env, env->state, value);
101 assert(env->state == JMP_BUF_STATE_CAPTURED);
102 assert(value != 0);
103 env->state = JMP_BUF_STATE_RETURNING;
104 env->payload = value;
105 _rb_wasm_active_jmpbuf = env;
106 async_buf_init(&env->longjmp_buf);
107 asyncify_start_unwind(&env->longjmp_buf);
108}
109
110
111enum try_catch_phase {
112 TRY_CATCH_PHASE_MAIN = 0,
113 TRY_CATCH_PHASE_RESCUE = 1,
114};
115
116void
117rb_wasm_try_catch_init(struct rb_wasm_try_catch *try_catch,
118 rb_wasm_try_catch_func_t try_f,
119 rb_wasm_try_catch_func_t catch_f,
120 void *context)
121{
122 try_catch->state = TRY_CATCH_PHASE_MAIN;
123 try_catch->try_f = try_f;
124 try_catch->catch_f = catch_f;
125 try_catch->context = context;
126}
127
128// NOTE: This function is not processed by Asyncify due to a call of asyncify_stop_rewind
129void
130rb_wasm_try_catch_loop_run(struct rb_wasm_try_catch *try_catch, rb_wasm_jmp_buf *target)
131{
132 extern void *rb_asyncify_unwind_buf;
133 extern rb_wasm_jmp_buf *_rb_wasm_active_jmpbuf;
134
135 target->state = JMP_BUF_STATE_CAPTURED;
136
137 switch ((enum try_catch_phase)try_catch->state) {
138 case TRY_CATCH_PHASE_MAIN: {
139 // may unwind
140 try_catch->try_f(try_catch->context);
141 break;
142 }
143 case TRY_CATCH_PHASE_RESCUE: {
144 if (try_catch->catch_f) {
145 // may unwind
146 try_catch->catch_f(try_catch->context);
147 }
148 break;
149 }
150 }
151
152 while (1) {
153 // catch longjmp with target jmp_buf
154 if (rb_asyncify_unwind_buf && _rb_wasm_active_jmpbuf == target) {
155 // do similar steps setjmp does when JMP_BUF_STATE_RETURNING
156
157 // stop unwinding
158 // (but call stop_rewind to update the asyncify state to "normal" from "unwind")
159 asyncify_stop_rewind();
160 // clear the active jmpbuf because it's already stopped
161 _rb_wasm_active_jmpbuf = NULL;
162 // reset jmpbuf state to be able to unwind again
163 target->state = JMP_BUF_STATE_CAPTURED;
164 // move to catch loop phase
165 try_catch->state = TRY_CATCH_PHASE_RESCUE;
166 if (try_catch->catch_f) {
167 try_catch->catch_f(try_catch->context);
168 }
169 continue;
170 } else if (rb_asyncify_unwind_buf /* unrelated unwind */) {
171 return;
172 }
173 // no unwind, then exit
174 break;
175 }
176 return;
177}
178
179void *
180rb_wasm_handle_jmp_unwind(void)
181{
182 RB_WASM_DEBUG_LOG("[%s] _rb_wasm_active_jmpbuf = %p\n", __func__, _rb_wasm_active_jmpbuf);
183 if (!_rb_wasm_active_jmpbuf) {
184 return NULL;
185 }
186
187 switch (_rb_wasm_active_jmpbuf->state) {
188 case JMP_BUF_STATE_CAPTURING: {
189 RB_WASM_DEBUG_LOG("[%s] JMP_BUF_STATE_CAPTURING\n", __func__);
190 // save the captured Asyncify stack top
191 _rb_wasm_active_jmpbuf->dst_buf_top = _rb_wasm_active_jmpbuf->setjmp_buf.top;
192 break;
193 }
194 case JMP_BUF_STATE_RETURNING: {
195 RB_WASM_DEBUG_LOG("[%s] JMP_BUF_STATE_RETURNING\n", __func__);
196 // restore the saved Asyncify stack top
197 _rb_wasm_active_jmpbuf->setjmp_buf.top = _rb_wasm_active_jmpbuf->dst_buf_top;
198 break;
199 }
200 default:
201 assert(0 && "unexpected state");
202 }
203 return &_rb_wasm_active_jmpbuf->setjmp_buf;
204}
C99 shim for <stdbool.h>