From b5d658213a46df0af6f5eda3c1b6d34c4c31e486 Mon Sep 17 00:00:00 2001 From: itsbryanman Date: Mon, 18 May 2026 13:01:31 +0200 Subject: [PATCH] fix: guard asyncify_stop_unwind() for async web API import unwinds (issue #7) Only call asyncify_stop_unwind() when pl_asyncify_unwind_buf is non-NULL, which indicates a C-level (setjmp/longjmp/scan) unwind. When an asyncify import like call_host_function triggers an unwind, the transform bypasses our macro so pl_asyncify_unwind_buf stays NULL. Calling asyncify_stop_unwind() in that case clears __asyncify_state from 1 (UNWINDING) to 0 before the export returns to JS, hiding the pending async op from asyncify-wasm. Fixes #7 --- stubs/runtime.c | 58 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/stubs/runtime.c b/stubs/runtime.c index b1a0f7d..e820a69 100644 --- a/stubs/runtime.c +++ b/stubs/runtime.c @@ -3,6 +3,9 @@ #include "setjmp.h" #include +/* defined in setjmp.c; set by asyncify_start_unwind macro, cleared by asyncify_stop_unwind macro */ +extern void *pl_asyncify_unwind_buf; + int asyncjmp_rt_start(int(main)(int argc, char **argv), int argc, char **argv) { int result; @@ -12,29 +15,44 @@ int asyncjmp_rt_start(int(main)(int argc, char **argv), int argc, char **argv) { result = main(argc, argv); - extern void *pl_asyncify_unwind_buf; - // Exit Asyncify loop if there is no unwound buffer, which - // means that main function has returned normally. - if (pl_asyncify_unwind_buf == NULL) { - break; - } - - // NOTE: it's important to call 'asyncify_stop_unwind' here instead in - // asyncjmp_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 = asyncjmp_handle_jmp_unwind()) != NULL) + /* + * Only call asyncify_stop_unwind() when this was a C-level unwind + * initiated by zeroperl's own asyncify_start_unwind() macro. + * The macro sets pl_asyncify_unwind_buf; asyncify import handling does not. + * + * If pl_asyncify_unwind_buf == NULL and main() returned, we are either: + * (a) Normal Perl exit: __asyncify_state == 0. Break normally. + * (b) Asyncify-import unwind (async web API via call_host_function): + * __asyncify_state == 1 (UNWINDING). Do NOT clear it here. + * Let it propagate so the JS host detects state == 1, awaits + * the Promise, calls asyncify_start_rewind(), and re-invokes + * the export to complete the rewind. + */ + if (pl_asyncify_unwind_buf != NULL) { - asyncify_start_rewind(asyncify_buf); - continue; - } - if ((asyncify_buf = asyncjmp_handle_scan_unwind()) != NULL) - { - asyncify_start_rewind(asyncify_buf); - continue; + /* + * C-level unwind (setjmp / longjmp / GC scan). + * NOTE: asyncify_stop_unwind() must be called here (in the loop), + * not inside the handler functions, or Asyncify inserts another + * unwind check here and unwinds to the root frame. + */ + asyncify_stop_unwind(); + + if ((asyncify_buf = asyncjmp_handle_jmp_unwind()) != NULL) + { + asyncify_start_rewind(asyncify_buf); + continue; + } + if ((asyncify_buf = asyncjmp_handle_scan_unwind()) != NULL) + { + asyncify_start_rewind(asyncify_buf); + continue; + } + /* pl_asyncify_unwind_buf was set but no handler claimed it */ + break; } + /* Normal exit or async import unwind */ break; } return result;