Skip to content

Quickjs#132

Draft
CedricGuillemet wants to merge 17 commits intoBabylonJS:mainfrom
CedricGuillemet:quickjs
Draft

Quickjs#132
CedricGuillemet wants to merge 17 commits intoBabylonJS:mainfrom
CedricGuillemet:quickjs

Conversation

@CedricGuillemet
Copy link
Copy Markdown
Collaborator

No description provided.

Comment thread Core/AppRuntime/Source/WorkQueue.cpp Outdated
Comment thread Core/AppRuntime/CMakeLists.txt Outdated
@CedricGuillemet CedricGuillemet force-pushed the quickjs branch 2 times, most recently from cd46921 to 3debf52 Compare April 22, 2026 12:45
CedricGuillemet and others added 14 commits April 22, 2026 16:01
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Separate class IDs for ExternalData, ExternalCallback, and Wrap objects
  to prevent type confusion in GC finalizers (was using wrong class ID)
- Fix ExternalCallback::Finalize to free newTarget JSValue
- Fix atom leak in create_function_internal (JS_NewAtom for 'name' was never freed)
- Implement napi_strict_equals (was returning without setting result)
- Fix use-after-free in napi_get_typedarray_info (data accessed after buffer freed)
- Fix typed array type detection using JS_GetTypedArrayType instead of bytesPerElement
- Fix ArrayBufferFreeCallback to actually invoke user finalize callback
- Fix wrap class finalizer to use correct class ID (js_wrap_class_id)
- Fix InitClassIds to register class defs per-runtime instead of once globally

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace #ifdef USE_QUICKJS in AppRuntime::Run with a generic
SetPostTickCallback mechanism. AppRuntime_QuickJS.cpp now installs
JS_ExecutePendingJob as a post-tick callback before calling Run(),
keeping engine-specific code in engine-specific files.

Removes the USE_QUICKJS compile definition from CMakeLists.txt.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Napi::Detach now calls JS_FreeValue on all remaining JSValues in the
handle scope stack before deleting the env. Previously unique_ptr only
freed heap memory without decrementing QuickJS reference counts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@CedricGuillemet CedricGuillemet force-pushed the quickjs branch 2 times, most recently from ac212aa to fe26c61 Compare April 24, 2026 08:02
CedricGuillemet and others added 2 commits April 24, 2026 10:50
Two correctness bugs in the QuickJS NAPI implementation caused objects
to be pinned forever, contributing to the assert(list_empty(&rt->gc_obj_list))
failure in JS_FreeRuntime at teardown:

1. napi_create_reference always called JS_DupValue regardless of the
   initial refcount. Per Node-API semantics a reference created with
   count=0 is a *weak* reference that must not keep its value alive.
   The dup silently promoted it to strong, which meant every
   ObjectWrap<T> instance (which uses napi_wrap -> napi_create_reference
   with count=0 as a weak self-ref) was pinned forever, its
   FinalizeCallback never fired, and its C++ destructor never ran.

   Fixed by only dupping when count > 0, and paired the matching
   delete/ref/unref paths so:
     - napi_delete_reference only frees the dup when count > 0
     - napi_reference_ref on 0->1 takes a dup (weak -> strong)
     - napi_reference_unref on 1->0 frees the dup (strong -> weak)

2. ExternalCallback::newTarget was stored as JS_DupValue(func) which
   created a self-cycle: func -> c_function_data -> opaque
   ExternalCallback -> newTarget -> func. Because NapiCallback class has
   gc_mark=nullptr the cycle collector cannot break this. Store the raw
   JSValue instead; it is only read during callback invocation when
   QuickJS itself holds func alive.

Measured impact on UnitTests on Android arm64 (debug):
  - Leaked GC objects at teardown: 11147 -> 10751 (-396)
  - Leaked ObjectWrap instances:      151 ->    52 (-99)
  - Leaked NapiWrap prototypes:       151 ->    52 (-99)
  - 136 tests still pass (no regressions)

The assert still fires (remaining leaks are dominated by bytecode
closures transitively pinned via persistent polyfill singletons), but
these two bugs are genuine leaks independent of the rest.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
During teardown the QuickJS implementation of Napi::Detach would leave
outstanding strong napi_refs dangling. Those refs pin JS values from
outside the gc_obj_list graph, so the cycle collector in JS_FreeRuntime
cannot break them and the list_empty(gc_obj_list) assertion fires.

Mirror the V8 impl (napi_env__::DeleteMe): track every RefInfo created
by napi_create_reference in a per-env list and release their JS side in
Detach. The RefInfo allocations themselves are left for their owning
C++ holders to destroy (typically later, via wrapper finalizers); those
subsequent napi_delete_reference calls take a detached-env path that
is a no-op.

Also implement gc_mark on NapiCallback so the strong newTarget ref it
holds participates in cycle collection, fix a leak of the callbackData
ref in create_function_internal, and run JS_RunGC twice at the end of
Detach so wrapper finalizers (which release embedded Napi::Reference
instances) execute while the env is still valid.

Tests: 136/136 passing. Reduces teardown leak count significantly; a
small residual set of externally-pinned objects still prevents the
gc_obj_list assertion from passing in all cases.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant