You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
When a Perry closure passed to a JS-imported function (via JsCreateCallback, landed in #248 Phase 2B / PR #254) reads a property from a JS object passed in as a callback argument, perry-jsruntime panics with RefCell already borrowed. The closure-marshaling itself works correctly; the issue is that V8's trampoline holds the JsRuntimeState borrow while invoking the Perry callback, and the callback's js_get_property (or any other with_runtime(...) site) re-enters on the same borrow.
perry compile test_re-entrancy.ts -o /tmp/re && /tmp/re
Output:
thread '<unnamed>' (...) panicked at crates/perry-jsruntime/src/lib.rs:123:28:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
thread '<unnamed>' (...) panicked at /rustc/.../library/core/src/panicking.rs:225:5:
panic in a function that cannot unwind
Exit code is 0 because the abort happens inside an extern "C" boundary that can't unwind.
Root cause
crates/perry-jsruntime/src/lib.rs:123 is inside with_runtime which does RUNTIME.with(|cell| cell.borrow_mut()). The call chain:
Inside the Perry closure body, ctx.deltaTime lowers to js_get_property(ctx, "deltaTime", ...) → with_runtime again → tries to borrow_mut the already-borrowed RefCell → panic.
Workarounds
Call the callback's argument-marshaling at the bridge layer instead of inside the Perry closure body. This means the trampoline itself converts each arg's JS-handle properties into plain values BEFORE invoking the callback, but is impractical without arity/property knowledge ahead of time.
Don't read JS-side properties inside callbacks. Pass primitives only (gp.run(16) instead of gp.run({deltaTime: 16})). Closes the immediate symptom but defeats the point of JS interop for any non-trivial use case.
Use a re-entrant runtime state — likely the right fix, but requires careful scope analysis of every with_runtime site.
Possible fix directions
Replace RefCell<JsRuntimeState> with RefCell<Option<JsRuntimeState>> + a take/restore pattern: each with_runtime body extracts the state, drops the RefCell borrow, runs the closure with &mut state, then puts it back. Re-entrant calls can take the state out again. Risky if a callback's exception unwinds past the put-back.
Or: switch the property-read FFI (and friends) to a non-with_runtime shape that only needs V8 handles, not the full JsRuntimeState. Probably the cleanest path but requires a per-FFI audit.
Or: have V8's trampoline drop its borrow before invoking the Perry callback (the borrow only protects the V8 init/setup, not the actual JS execution). Requires changes in native_callback_trampoline plus discipline that the trampoline doesn't touch state after the callback returns.
Summary
When a Perry closure passed to a JS-imported function (via
JsCreateCallback, landed in #248 Phase 2B / PR #254) reads a property from a JS object passed in as a callback argument, perry-jsruntime panics withRefCell already borrowed. The closure-marshaling itself works correctly; the issue is that V8's trampoline holds theJsRuntimeStateborrow while invoking the Perry callback, and the callback'sjs_get_property(or any otherwith_runtime(...)site) re-enters on the same borrow.Repro
perry compile test_re-entrancy.ts -o /tmp/re && /tmp/reOutput:
Exit code is 0 because the abort happens inside an
extern "C"boundary that can't unwind.Root cause
crates/perry-jsruntime/src/lib.rs:123is insidewith_runtimewhich doesRUNTIME.with(|cell| cell.borrow_mut()). The call chain:gp.run({deltaTime: 16})→js_call_method→ enterswith_runtime(borrow Support custom menu bar items #1).run()method → calls the registered Perry callback.native_callback_trampoline(crates/perry-jsruntime/src/interop.rs:993) callsjs_closure_call_array(closure_env, args_ptr, args_len).ctx.deltaTimelowers tojs_get_property(ctx, "deltaTime", ...)→with_runtimeagain → tries to borrow_mut the already-borrowed RefCell → panic.Workarounds
gp.run(16)instead ofgp.run({deltaTime: 16})). Closes the immediate symptom but defeats the point of JS interop for any non-trivial use case.with_runtimesite.Possible fix directions
RefCell<JsRuntimeState>withRefCell<Option<JsRuntimeState>>+ a take/restore pattern: eachwith_runtimebody extracts the state, drops the RefCell borrow, runs the closure with&mut state, then puts it back. Re-entrant calls can take the state out again. Risky if a callback's exception unwinds past the put-back.with_runtimeshape that only needs V8 handles, not the full JsRuntimeState. Probably the cleanest path but requires a per-FFI audit.native_callback_trampolineplus discipline that the trampoline doesn't touch state after the callback returns.Surfaced by