Skip to content

Keep the rest of the runtime's statics alive through exit #371

Closed

Description

@dimensionscape

#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:

  • sModuleLock in src/hx/cppia/CppiaModule.cpp, taken when a cppia module loads or releases
  • sYoungValueLock in src/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::FastMutex is an SRW lock on Windows, but std::mutex elsewhere, so it is destroyed at exit while another thread's write barrier can still take it
  • the kind maps sgKindMap and sgReverseKindMap, and the mutex beside them, in src/hx/CFFI.cpp
  • sLoadedMap, sgLoadedModule, sgOrderedModules, sgLibPath, sgLastLoadError and the loader's recursive mutex in src/hx/Lib.cpp
  • sVarIdNameMap in src/hx/cppia/CppiaVars.cpp
  • the environment mutex in src/hx/libs/std/Sys.cpp
  • initReturnBuffer in src/hx/StdLibs.cpp and sgResultBuffer in src/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 main returns, run many times over.
  • Check with clang's -Wexit-time-destructors that nothing else in the runtime is destroyed at exit.

Related: HaxeFoundation/hxcpp#1433, #67.

Activity

  1. dimensionscape commented on Oct 26, 2026

    @dimensionscape
    MemberAuthor

    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-destructors locally.

    Test: ExitWithBusyWorker runs a worker on the environment, a failing library load, or young stores into a large old array while main returns, 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions