Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions libfjs/src/api/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1260,10 +1260,15 @@ fn new_bridge_call<'js>(
ctx: rquickjs::Ctx<'js>,
bridge: Arc<BridgeCallback>,
) -> rquickjs::CaughtResult<'js, rquickjs::Function<'js>> {
let ctx_for_catch = ctx.clone();
rquickjs::Function::new(
ctx.clone(),
move |args: rquickjs::function::Rest<rquickjs::Value<'js>>| -> rquickjs::Result<Promise<'js>> {
// `ctx` is a parameter, not a capture: capturing it would keep this
// function's context alive forever (the function lives on the global),
// so dropping the runtime without `close()` would crash in
// `JS_FreeRuntime`. See issue #8.
move |ctx: rquickjs::Ctx<'js>,
args: rquickjs::function::Rest<rquickjs::Value<'js>>|
-> rquickjs::Result<Promise<'js>> {
if args.0.len() > 1 {
return Err(rquickjs::Error::TooManyArgs {
expected: 1,
Expand Down Expand Up @@ -1297,5 +1302,5 @@ fn new_bridge_call<'js>(
})
},
)
.catch(&ctx_for_catch)
.catch(&ctx)
}
27 changes: 27 additions & 0 deletions libfjs/src/tests/engine_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,33 @@ async fn test_engine_init_with_bridge() {
engine.close().await.unwrap();
}

/// Regression test for issue #8: dropping an engine that has a bridge, without
/// calling `close()` (as Dart's GC does), must not crash. It used to abort in
/// `JS_FreeRuntime` because the bridge kept its own context alive.
///
/// Note: a regression is a process abort (SIGABRT), not a normal test failure —
/// the second engine below only runs if the first drop was clean.
#[tokio::test]
async fn test_engine_drop_with_bridge_without_close_does_not_abort() {
{
let engine = JsEngine::create(None, None, None).await.unwrap();
engine
.init(|value| Box::pin(async move { JsResult::Ok(value) }))
.await
.unwrap();
let result = engine.eval(JsCode::Code("1 + 1".to_string()), None).await;
assert!(matches!(result.unwrap(), JsValue::Integer(2)));
// Drop WITHOUT close(), mimicking Dart's GC finalizer / hot restart.
drop(engine);
}

// Reached only if the drop above did not abort the process.
let engine = JsEngine::create(None, None, None).await.unwrap();
engine.init_without_bridge().await.unwrap();
let result = engine.eval(JsCode::Code("2 + 3".to_string()), None).await;
assert!(matches!(result.unwrap(), JsValue::Integer(5)));
}

#[tokio::test]
async fn test_engine_double_init_fails() {
let engine = JsEngine::create(None, None, None).await.unwrap();
Expand Down