Ruby: wasm/setjmp.c Source File

Ruby 4.0.5p0 (2026-05-20 revision 64336ffd0ee9e1f4c05891695a3d7b49cb709721)

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 <wasi/api.h>
33# include <unistd.h>
34// NOTE: We can't use printf() and most of library function that are
35// Asyncified due to the use of them in the application itself.
36// Use of printf() causes "unreachable" error because Asyncified
37// function misunderstands Asyncify's internal state during
38// start_unwind()...stop_unwind() and start_rewind()...stop_rewind().
39# define RB_WASM_DEBUG_LOG_INTERNAL(msg) do { \
40 const uint8_t *msg_start = (uint8_t *)msg; \
41 const uint8_t *msg_end = msg_start; \
42 for (; *msg_end != '\0'; msg_end++) {} \
43 __wasi_ciovec_t iov = {.buf = msg_start, .buf_len = msg_end - msg_start}; \
44 size_t nwritten; \
45 __wasi_fd_write(STDERR_FILENO, &iov, 1, &nwritten); \
46} while (0)
47# define RB_WASM_DEBUG_LOG(msg) \
48 RB_WASM_DEBUG_LOG_INTERNAL(__FILE__ ":" STRINGIZE(__LINE__) ": " msg "\n")
49#else
50# define RB_WASM_DEBUG_LOG(msg)
51#endif
52
53enum rb_wasm_jmp_buf_state {
54 // Initial state
55 JMP_BUF_STATE_INITIALIZED = 0,
56 // Unwinding to the root or rewinding to the setjmp call
57 // to capture the current execution context
58 JMP_BUF_STATE_CAPTURING = 1,
59 // Ready for longjmp
60 JMP_BUF_STATE_CAPTURED = 2,
61 // Unwinding to the root or rewinding to the setjmp call
62 // to restore the execution context
63 JMP_BUF_STATE_RETURNING = 3,
64};
65
66void
67async_buf_init(struct __rb_wasm_asyncify_jmp_buf* buf)
68{
69 buf->top = &buf->buffer[0];
70 buf->end = &buf->buffer[WASM_SETJMP_STACK_BUFFER_SIZE];
71}
72
73// Global unwinding/rewinding jmpbuf state
74static rb_wasm_jmp_buf *_rb_wasm_active_jmpbuf;
75void *rb_asyncify_unwind_buf;
76
77 __attribute__((noinline))
78int
79_rb_wasm_setjmp_internal(rb_wasm_jmp_buf *env)
80{
81 RB_WASM_DEBUG_LOG("enter _rb_wasm_setjmp_internal");
82 switch (env->state) {
83 case JMP_BUF_STATE_INITIALIZED: {
84 RB_WASM_DEBUG_LOG(" JMP_BUF_STATE_INITIALIZED");
85 env->state = JMP_BUF_STATE_CAPTURING;
86 env->payload = 0;
87 env->longjmp_buf_ptr = NULL;
88 _rb_wasm_active_jmpbuf = env;
89 async_buf_init(&env->setjmp_buf);
90 asyncify_start_unwind(&env->setjmp_buf);
91 return -1; // return a dummy value
92 }
93 case JMP_BUF_STATE_CAPTURING: {
94 asyncify_stop_rewind();
95 RB_WASM_DEBUG_LOG(" JMP_BUF_STATE_CAPTURING");
96 env->state = JMP_BUF_STATE_CAPTURED;
97 _rb_wasm_active_jmpbuf = NULL;
98 return 0;
99 }
100 case JMP_BUF_STATE_RETURNING: {
101 asyncify_stop_rewind();
102 RB_WASM_DEBUG_LOG(" JMP_BUF_STATE_RETURNING");
103 env->state = JMP_BUF_STATE_CAPTURED;
104 _rb_wasm_active_jmpbuf = NULL;
105 return env->payload;
106 }
107 default:
108 assert(0 && "unexpected state");
109 }
110 return 0;
111}
112
113void
114_rb_wasm_longjmp(rb_wasm_jmp_buf* env, int value)
115{
116 RB_WASM_DEBUG_LOG("enter _rb_wasm_longjmp");
117 assert(env->state == JMP_BUF_STATE_CAPTURED);
118 assert(value != 0);
119 env->state = JMP_BUF_STATE_RETURNING;
120 env->payload = value;
121 // Asyncify buffer built during unwinding for longjmp will not
122 // be used to rewind, so re-use static-variable.
123 static struct __rb_wasm_asyncify_jmp_buf tmp_longjmp_buf;
124 env->longjmp_buf_ptr = &tmp_longjmp_buf;
125 _rb_wasm_active_jmpbuf = env;
126 async_buf_init(env->longjmp_buf_ptr);
127 asyncify_start_unwind(env->longjmp_buf_ptr);
128}
129
130
131enum try_catch_phase {
132 TRY_CATCH_PHASE_MAIN = 0,
133 TRY_CATCH_PHASE_RESCUE = 1,
134};
135
136void
137rb_wasm_try_catch_init(struct rb_wasm_try_catch *try_catch,
138 rb_wasm_try_catch_func_t try_f,
139 rb_wasm_try_catch_func_t catch_f,
140 void *context)
141{
142 try_catch->state = TRY_CATCH_PHASE_MAIN;
143 try_catch->try_f = try_f;
144 try_catch->catch_f = catch_f;
145 try_catch->context = context;
146 try_catch->stack_pointer = NULL;
147}
148
149// NOTE: This function is not processed by Asyncify due to a call of asyncify_stop_rewind
150 __attribute__((noinline))
151void
152rb_wasm_try_catch_loop_run(struct rb_wasm_try_catch *try_catch, rb_wasm_jmp_buf *target)
153{
154 extern void *rb_asyncify_unwind_buf;
155 extern rb_wasm_jmp_buf *_rb_wasm_active_jmpbuf;
156
157 target->state = JMP_BUF_STATE_CAPTURED;
158
159 if (try_catch->stack_pointer == NULL) {
160 try_catch->stack_pointer = rb_wasm_get_stack_pointer();
161 }
162
163 switch ((enum try_catch_phase)try_catch->state) {
164 case TRY_CATCH_PHASE_MAIN:
165 // may unwind
166 try_catch->try_f(try_catch->context);
167 break;
168 case TRY_CATCH_PHASE_RESCUE:
169 if (try_catch->catch_f) {
170 // may unwind
171 try_catch->catch_f(try_catch->context);
172 }
173 break;
174 }
175
176 {
177 // catch longjmp with target jmp_buf
178 while (rb_asyncify_unwind_buf && _rb_wasm_active_jmpbuf == target) {
179 // do similar steps setjmp does when JMP_BUF_STATE_RETURNING
180
181 // stop unwinding
182 // (but call stop_rewind to update the asyncify state to "normal" from "unwind")
183 asyncify_stop_rewind();
184 // reset the stack pointer to what it was before the most recent call to try_f or catch_f
185 rb_wasm_set_stack_pointer(try_catch->stack_pointer);
186 // clear the active jmpbuf because it's already stopped
187 _rb_wasm_active_jmpbuf = NULL;
188 // reset jmpbuf state to be able to unwind again
189 target->state = JMP_BUF_STATE_CAPTURED;
190 // move to catch loop phase
191 try_catch->state = TRY_CATCH_PHASE_RESCUE;
192 if (try_catch->catch_f) {
193 try_catch->catch_f(try_catch->context);
194 }
195 }
196 // no unwind or unrelated unwind, then exit
197 }
198}
199
200void *
201rb_wasm_handle_jmp_unwind(void)
202{
203 RB_WASM_DEBUG_LOG("enter rb_wasm_handle_jmp_unwind");
204 if (!_rb_wasm_active_jmpbuf) {
205 return NULL;
206 }
207
208 switch (_rb_wasm_active_jmpbuf->state) {
209 case JMP_BUF_STATE_CAPTURING:
210 RB_WASM_DEBUG_LOG(" JMP_BUF_STATE_CAPTURING");
211 // save the captured Asyncify stack top
212 _rb_wasm_active_jmpbuf->dst_buf_top = _rb_wasm_active_jmpbuf->setjmp_buf.top;
213 break;
214 case JMP_BUF_STATE_RETURNING:
215 RB_WASM_DEBUG_LOG(" JMP_BUF_STATE_RETURNING");
216 // restore the saved Asyncify stack top
217 _rb_wasm_active_jmpbuf->setjmp_buf.top = _rb_wasm_active_jmpbuf->dst_buf_top;
218 break;
219 default:
220 assert(0 && "unexpected state");
221 }
222 return &_rb_wasm_active_jmpbuf->setjmp_buf;
223}
assert.h
stdbool.h
C99 shim for <stdbool.h>.
__rb_wasm_asyncify_jmp_buf
Definition setjmp.h:11
rb_wasm_jmp_buf
Definition setjmp.h:17
rb_wasm_try_catch
Definition setjmp.h:64
__attribute__
Definition vm_insnhelper.c:2399