Repository navigation
Keep the rest of the runtime's statics alive through exit #371
Description
#67 made the GC's statics immortal, allocated once and never destroyed, so a thread still running when main returns or Sys.exit is called cannot use them after their destructors run. HaxeFoundation/hxcpp#1433 proposes the same for stock hxcpp behind an opt-in define, HXCPP_IMMORTAL_STATICS, and covers more of the runtime than #67 did.
Its repro, a worker filling an ObjectMap while main returns, does not crash libhxcpp: 0 in 200 runs on Windows x64 at 299cff6b. The object id map it reaches is one #67 already made immortal, and libhxcpp's ObjectMap hashes keys by address when objects do not move, so it does not reach that map at all. The PR reports most of its crashes on macOS, which I have not run.
But libhxcpp still has statics outside the GC with exit-time destructors, which a thread still running at exit can use after they are destroyed:
sModuleLockinsrc/hx/cppia/CppiaModule.cpp, taken when a cppia module loads or releasessYoungValueLockinsrc/hx/gc/Immix.cpp, added for Track dirty pointer-array ranges to reduce minor GC work after sparse writes #301 and Avoid scanning entire old maps after sparse writes during minor GC #305:hx::FastMutexis an SRW lock on Windows, butstd::mutexelsewhere, so it is destroyed at exit while another thread's write barrier can still take it- the kind maps
sgKindMapandsgReverseKindMap, and the mutex beside them, insrc/hx/CFFI.cpp sLoadedMap,sgLoadedModule,sgOrderedModules,sgLibPath,sgLastLoadErrorand the loader's recursive mutex insrc/hx/Lib.cppsVarIdNameMapinsrc/hx/cppia/CppiaVars.cpp- the environment mutex in
src/hx/libs/std/Sys.cpp initReturnBufferinsrc/hx/StdLibs.cppandsgResultBufferinsrc/hx/RunLibs.cpp- the profiler, telemetry and Tracy mutexes, maps and lists, in builds with those enabled
As with #67 and the GC's statics, these should become immortal unconditionally rather than behind a define: they live until the process ends anyway, so never destroying them costs nothing. Where a static is used from a static initializer, as the CFFI kind maps can be through hxcpp_kind_share, it should be made on first use, as the PR does.
To do:
- Make each of these allocated once and never destroyed, or created on first use where static initialization order matters.
- Add a regression test that keeps a worker using these paths (cppia module loading, CFFI kinds, library loading, environment access) while
mainreturns, run many times over. - Check with clang's
-Wexit-time-destructorsthat nothing else in the runtime is destroyed at exit.
Related: HaxeFoundation/hxcpp#1433, #67.
Activity
dimensionscape commentedon Oct 26, 2026
Fixed in
4cdca9600f0a7c182693d244a00389ef3b9a75df. Every static listed above is now allocated once and never destroyed, as the collector's have been since #67: the cppia module lock,sYoungValueLock, the CFFI kind maps and their lock, the library loader's maps, path list, error string and lock,sVarIdNameMap, the environment lock, the two result buffers, and the profiler, telemetry and Tracy statics. The CFFI kind maps are made on first use, since an extension can share a kind from a static initializer. A search of the runtime found no other static with a destructor that runs at exit; I could not run clang's-Wexit-time-destructorslocally.Test: ExitWithBusyWorker runs a worker on the environment, a failing library load, or young stores into a large old array while
mainreturns, 40 times each, and checks that every run exits cleanly. It passes on every CI platform. It is skipped on Emscripten, which has no processes or threads.