8383#include < string>
8484#include < string_view>
8585#include < thread>
86+ #include < unordered_set>
8687#include < utility>
8788#include < vector>
8889
@@ -459,6 +460,21 @@ struct Context::Impl {
459460 // Optional diagnostics: track live JS handles we create (Value copies etc).
460461 std::atomic<int > live_handles{0 };
461462
463+ // Track all native holders (DomValueHolder, FunctionHolder) so we can
464+ // delete them during cleanup if JerryScript's GC doesn't finalize them.
465+ // This handles the case where objects are still referenced from globals.
466+ std::unordered_set<NativeHolder*> holders;
467+
468+ void registerHolder (NativeHolder* h)
469+ {
470+ holders.insert (h);
471+ }
472+
473+ void unregisterHolder (NativeHolder* h)
474+ {
475+ holders.erase (h);
476+ }
477+
462478 Impl ()
463479 {
464480 // Temporarily set TLS so jerry_init() can find the context.
@@ -531,10 +547,8 @@ struct Context::Impl {
531547 void * prev_ctx = get_tls_jerry_context ();
532548 set_tls_jerry_context (jerry_ctx);
533549
534- // Run full garbage collection to finalize all objects and trigger
535- // native pointer free callbacks (DomValueHolder::free_cb, etc.)
536- // before jerry_cleanup(). This ensures all our native holders are
537- // properly deleted.
550+ // Run full garbage collection to finalize unreferenced objects and
551+ // trigger native pointer free callbacks (DomValueHolder::free_cb, etc.)
538552 jerry_heap_gc (JERRY_GC_PRESSURE_HIGH);
539553
540554 // jerry_cleanup() cleans up JS objects but with JERRY_EXTERNAL_CONTEXT=ON,
@@ -545,6 +559,16 @@ struct Context::Impl {
545559 // jerry_port_context_free is our implementation that calls std::free().
546560 jerry_port_context_free (jerry_ctx, 0 );
547561
562+ // Delete any remaining native holders that weren't garbage collected.
563+ // This handles objects still referenced from globals at cleanup time.
564+ // The free_cb won't be called for these since JerryScript just abandons
565+ // them during cleanup, so we delete them manually.
566+ for (NativeHolder* h : holders)
567+ {
568+ delete h;
569+ }
570+ holders.clear ();
571+
548572 // Context is now destroyed. Set jerry_ctx to nullptr and mark dead.
549573 jerry_ctx = nullptr ;
550574 alive = false ;
@@ -568,6 +592,11 @@ struct Context::Impl {
568592void DomValueHolder::free_cb (void * p, jerry_object_native_info_t *)
569593{
570594 auto * h = static_cast <DomValueHolder*>(p);
595+ // Always unregister from tracking set so we don't double-free during cleanup.
596+ if (h->impl )
597+ {
598+ h->impl ->unregisterHolder (h);
599+ }
571600 delete h;
572601}
573602
@@ -1634,6 +1663,7 @@ makeObjectProxy(dom::Object obj, std::shared_ptr<Context::Impl> impl)
16341663 auto * holder = new DomValueHolder ();
16351664 holder->impl = impl;
16361665 holder->value = dom::Value (std::move (obj));
1666+ impl->registerHolder (holder);
16371667
16381668 // Create an empty target object (the proxy intercepts all access)
16391669 jerry_value_t target = jerry_object ();
@@ -1804,6 +1834,11 @@ struct FunctionHolder : NativeHolder {
18041834 free_cb (void * p, jerry_object_native_info_t *)
18051835 {
18061836 auto * h = static_cast <FunctionHolder*>(p);
1837+ // Always unregister from tracking set so we don't double-free during cleanup.
1838+ if (h->impl )
1839+ {
1840+ h->impl ->unregisterHolder (h);
1841+ }
18071842 delete h;
18081843 }
18091844};
@@ -1818,6 +1853,7 @@ makeFunctionProxy(dom::Function fn, std::shared_ptr<Context::Impl> impl)
18181853 auto * holder = new FunctionHolder ();
18191854 holder->impl = impl;
18201855 holder->fn = std::move (fn);
1856+ impl->registerHolder (holder);
18211857
18221858 jerry_value_t func = jerry_function_external (
18231859 [](jerry_call_info_t const * call_info_p,
0 commit comments