From 7f0d21d446ac4042bb4780e93903443ecf6fb07a Mon Sep 17 00:00:00 2001 From: "Enzo Persillet (Tutez)" Date: Tue, 29 Sep 2026 18:39:11 +0200 Subject: [PATCH 1/2] Mark every loaded cppia module on every collection. A module is an ordinary GC object, so a generational collection stops traversing it once it is old. What it holds still changes after that: a literal boxes itself on first use, the JIT embeds that box, a script static is assigned. None of those writes has a barrier, so a minor collection freed the young object while the module still pointed at it, and the next use read reused memory. Modules are now marked with the class statics. Co-Authored-By: Claude Opus 5.5 --- src/hx/cppia/CppiaModule.cpp | 33 +++++++++++++++++++++++++++++++++ src/hx/gc/Immix.cpp | 10 ++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/hx/cppia/CppiaModule.cpp b/src/hx/cppia/CppiaModule.cpp index 2d029c52b..134929b1c 100644 --- a/src/hx/cppia/CppiaModule.cpp +++ b/src/hx/cppia/CppiaModule.cpp @@ -355,6 +355,39 @@ class CppiaObject : public hx::CppiaLoadedModule_obj }; +/* + A loaded module is an ordinary GC object, so a generational collection stops traversing it once it + is old. What it holds can still change after that: a literal boxes itself on first use, the JIT + embeds that box in its code, a script static is assigned. None of those writes has a barrier, so a + minor collection freed the young object while the old module still pointed at it. + + So every loaded module is marked on every collection, the way class statics are. +*/ +void scriptMarkModules(hx::MarkContext *__inCtx) +{ + if (!gAllCppiaModules.mPtr) + return; + for(int i=0;ilength;i++) + { + CppiaObject *module = static_cast(gAllCppiaModules[i].mPtr); + if (module) + module->cppia->mark(__inCtx); + } +} + +#ifdef HXCPP_VISIT_ALLOCS +void scriptVisitModules(hx::VisitContext *__inCtx) +{ + if (!gAllCppiaModules.mPtr) + return; + for(int i=0;ilength;i++) + { + CppiaObject *module = static_cast(gAllCppiaModules[i].mPtr); + if (module) + module->cppia->visit(__inCtx); + } +} +#endif CppiaLoadedModule LoadCppia(const unsigned char *inData, int inDataLength) diff --git a/src/hx/gc/Immix.cpp b/src/hx/gc/Immix.cpp index 9eb91603e..995041e34 100644 --- a/src/hx/gc/Immix.cpp +++ b/src/hx/gc/Immix.cpp @@ -459,6 +459,10 @@ DECLARE_FAST_TLS_DATA(StackContext, tlsStackContext); #ifdef HXCPP_SCRIPTABLE extern void scriptMarkStack(hx::MarkContext *); +extern void scriptMarkModules(hx::MarkContext *); +#ifdef HXCPP_VISIT_ALLOCS +extern void scriptVisitModules(hx::VisitContext *); +#endif #endif } @@ -4268,6 +4272,9 @@ class GlobalAllocator VisitLocalAlloc(mLocalAllocs[i], inCtx); hx::VisitClassStatics(inCtx); + #ifdef HXCPP_SCRIPTABLE + hx::scriptVisitModules(inCtx); + #endif for(hx::RootSet::iterator i = hx::sgRootSet.begin(); i!=hx::sgRootSet.end(); ++i) { @@ -4712,6 +4719,9 @@ class GlobalAllocator mMarker.init(); hx::MarkClassStatics(&mMarker); + #ifdef HXCPP_SCRIPTABLE + hx::scriptMarkModules(&mMarker); + #endif { hx::AutoMarkPush info(&mMarker,"Roots","root"); From 3b884268760d7852bb6fc801f1d72ea9a312d253 Mon Sep 17 00:00:00 2001 From: "Enzo Persillet (Tutez)" Date: Sat, 3 Oct 2026 17:17:02 +0200 Subject: [PATCH 2/2] Test values that an old cppia module creates, with a generational GC. The cppia host now builds with HXCPP_GC_GENERATIONAL. After full collections make the module old, ClientModuleValues boxes a string literal and assigns a static array, which only the module references. Weak references then check that a generational collection keeps them. Without the fix, both are freed without the JIT, and the array with it (the JIT boxes the literal at load time). Co-Authored-By: Claude Opus 5.5 --- test/cppia/Client.hx | 18 ++++++++++++++++++ test/cppia/cases/TestCommon.hx | 23 +++++++++++++++++++++++ test/cppia/compile-host.hxml | 1 + 3 files changed, 42 insertions(+) diff --git a/test/cppia/Client.hx b/test/cppia/Client.hx index 710373f4b..840274396 100644 --- a/test/cppia/Client.hx +++ b/test/cppia/Client.hx @@ -14,6 +14,24 @@ class ClientFoo implements IFoo { } } +class ClientModuleValues { + + static var kept:Array; + + // Read as Dynamic, the literal is boxed on first use, and the module keeps the box. + public static function literal():Dynamic { + var boxed:Dynamic = "a string literal"; + return boxed; + } + + // Assigned after load: the module holds the only reference. + public static function assigned():Dynamic { + if (kept == null) + kept = ["assigned after load"]; + return kept; + } +} + class Client { public static var clientBool0 = true; diff --git a/test/cppia/cases/TestCommon.hx b/test/cppia/cases/TestCommon.hx index eaacb5a76..d6eef8058 100644 --- a/test/cppia/cases/TestCommon.hx +++ b/test/cppia/cases/TestCommon.hx @@ -1,6 +1,8 @@ package cases; import cpp.cppia.Host; +import cpp.vm.Gc; +import cpp.vm.WeakRef; import utest.Test; import utest.Assert; @@ -59,6 +61,27 @@ class TestCommon extends Test { Assert.equals(2, Common.callbackSet, 'Bad cppia closure'); } + @:depends(testStatus) + function testModuleValuesCreatedWhenOld() { + final values = Type.resolveClass('ClientModuleValues'); + + // Full collections make the module old: a generational collection no longer traverses it. + for (_ in 0...3) { + Gc.run(true); + } + final created = createValues(values); + Gc.run(false); + + for (value in created) { + Assert.notNull(value.get(), 'Freed a value that the old module holds'); + } + } + + // Only weak references to the values leave this frame, so that the stack does not keep them. + static function createValues(values:Dynamic):Array> { + return [for (name in ['literal', 'assigned']) new WeakRef(Reflect.callMethod(values, Reflect.field(values, name), []))]; + } + @:depends(testStatus) function testInterfaceCalling() { final obj : IFoo = Type.createInstance(Type.resolveClass('ClientFoo'), []); diff --git a/test/cppia/compile-host.hxml b/test/cppia/compile-host.hxml index 76bf2d790..051e26186 100644 --- a/test/cppia/compile-host.hxml +++ b/test/cppia/compile-host.hxml @@ -1,6 +1,7 @@ -m CppiaHost HostExtendedRoot -D scriptable +-D HXCPP_GC_GENERATIONAL -D dll_export=host_classes.info -L utest --dce no