Problem
When a JsAsyncRuntime Dart object is garbage collected (e.g. during Flutter hot restart), the Rust Drop calls QuickJS's JS_FreeRuntime, which hits this assertion:
Assertion failed: (list_empty(&rt->gc_obj_list)), function JS_FreeRuntime, file quickjs.c, line 2308.
This is a SIGABRT — it kills the process. It happens because QuickJS still has objects on its GC list (modules, bridge closures, built-in objects from JsBuiltinOptions.essential()) that weren't freed before the runtime was dropped.
Reproduction
- Create a runtime, context, and engine with a bridge
- Evaluate some JS (especially modules via
evaluateModule)
- Let the
JsAsyncRuntime Dart object get garbage collected (e.g. via Flutter hot restart, or by dropping all references)
final runtime = await JsAsyncRuntime.withOptions(
builtin: JsBuiltinOptions.essential(),
);
final context = await JsAsyncContext.from(runtime: runtime);
final engine = JsEngine(context: context);
await engine.init(bridge: (v) async => const JsResult.ok(JsValue.none()));
await engine.evaluateModule(
module: JsModule.code(module: '/test', code: 'export default 1;'),
);
// Later: runtime goes out of scope → Dart GC → Rust Drop → JS_FreeRuntime → SIGABRT
Calling engine.dispose() first doesn't help — the assertion still fires when the runtime is eventually dropped.
Workaround
Bump the Arc ref count so the Rust Drop never runs:
import 'package:flutter_rust_bridge/src/misc/rust_opaque.dart';
void preventNativeDrop(Object obj) {
if (obj is RustOpaque) {
obj.frbInternalCstEncode(move: false); // increments Arc strong count
}
}
// After creation:
preventNativeDrop(runtime);
This permanently leaks the QuickJS runtime (the Arc never reaches 0), but avoids the crash.
Suggested Fix
Before calling JS_FreeRuntime in the Rust Drop, run JS_RunGC and/or iterate rt->gc_obj_list to free remaining objects. Alternatively, skip the assertion and clean up gracefully — QuickJS's assert is unconditional (not behind #ifdef DEBUG).
Problem
When a
JsAsyncRuntimeDart object is garbage collected (e.g. during Flutter hot restart), the RustDropcalls QuickJS'sJS_FreeRuntime, which hits this assertion:This is a
SIGABRT— it kills the process. It happens because QuickJS still has objects on its GC list (modules, bridge closures, built-in objects fromJsBuiltinOptions.essential()) that weren't freed before the runtime was dropped.Reproduction
evaluateModule)JsAsyncRuntimeDart object get garbage collected (e.g. via Flutter hot restart, or by dropping all references)Calling
engine.dispose()first doesn't help — the assertion still fires when the runtime is eventually dropped.Workaround
Bump the Arc ref count so the Rust
Dropnever runs:This permanently leaks the QuickJS runtime (the Arc never reaches 0), but avoids the crash.
Suggested Fix
Before calling
JS_FreeRuntimein the RustDrop, runJS_RunGCand/or iteratert->gc_obj_listto free remaining objects. Alternatively, skip the assertion and clean up gracefully — QuickJS's assert is unconditional (not behind#ifdef DEBUG).