Repository navigation
[v636][metacling] Move wrapper caches from local statics to file scope - #23873
Merged
guitargeek merged 3 commits intoOct 31, 2026
Merged
Conversation
TClingCallFunc caches the JIT-compiled call, constructor and destructor wrappers in std::maps that were function-local statics. Such a static registers its destructor on first use, and for the destructor-wrapper cache the first use can be in the middle of the user's program, e.g. when PyROOT destroys an object of a class template instantiation without compiled dictionary. That is after ROOT registered its atexit handlers, so at exit the cache was destroyed before TROOT::EndOfProcessCleanups() deleted the files and trees still alive, and the destructor lookup for a branch object without compiled dictionary (e.g. std::vector<std::vector<int>>) ran on a dead map: segfault or "corrupted double-linked list" at exit, depending on the heap state. Making the maps globals of libCling ties their lifetime to the library, like the other interpreter globals. They are then constructed when libCling is loaded by TROOT::InitInterpreter(), i.e. before the atexit handlers that run the end-of-process cleanups are registered, and outlive them. This is the 6.36 version of the fix for root-project#23728. In 6.40 and later the caches live in CppInterOp, which fixed the same issue by moving them to per-interpreter storage (CppInterOp commit b8ae9f666c). Assisted-by: claude-code:claude-fable-5-1
…oject#23728 Reads a tree with a branch of a dictionary-less collection in a fresh Python process, lets TROOT::EndOfProcessCleanups delete it at exit, and constructs another dictionary-less object before exiting. This crashed in the JIT-compiled destructor lookup before the CppInterOp upgrade. The child process runs with MALLOC_PERTURB_ set, because a use of destroyed objects only crashes reliably when freed memory is poisoned, and with ROOT_OBJECT_AUTO_REGISTRATION=1 plus explicit checks that the file is registered with gROOT and owns the tree, so the test keeps exercising the scenario when object auto-registration is disabled. Assisted-by: claude-code:claude-fable-5-1 (cherry picked from commit a41307c)
guitargeek
requested review from
bellenot,
dpiparo,
pcanal and
vepadulano
as code owners
October 28, 2026 06:51
Test Results 15 files 15 suites 2d 6h 3m 41s ⏱️ Results for commit 9d2c54a. ♻️ This comment has been updated with latest results. |
Member
|
The errors (that happens to be in the Windows) build seems completely related to this change and are likely indicative of additional order destruction issues. (For example, a 'con' of this change is now the map might still contain stuff and try to delete them after the interpreter is torn down). |
Contributor
Author
|
Yes I'm on it. I'm still missing some fix, but is must be possible to get the test also to work on Windows, because I have merged the exact same test also in |
…vely The test left the destructor of the branch class uncompiled until exit, so that cling had to JIT-compile it from TROOT::EndOfProcessCleanups(). On Windows, that cleanup runs from libCore's DLL_PROCESS_DETACH, after libCling's statics have already been torn down, so compiling anything there crashes with or without the fix (the no-fix run in root-project#23850 fails identically). Pre-compile the exact destructor instead: the exit-time call is then a pure cache lookup, which is the regular exit path on Windows. The bug was in that lookup on the destroyed function-local static, so the test still fails without the fix. Write the branch through the overload taking the class name and the address. The templated Branch() that the pythonization on master calls refuses collections without a compiled CollectionProxy, which silently left the tree without the branch and the test without its purpose. Also give the child process a timeout: without the fix, ROOT's crash handler can hang instead of terminating the process. Assisted-by: claude-code:claude-fable-5-1 (cherry picked from commit 98526e5)
guitargeek
force-pushed
the
v6-36-fix-23728
branch
from
October 30, 2026 19:04
f90fc13 to
9d2c54a
Compare
Contributor
Author
|
Windows failures fixed by 98526e5, backported to this feature branch. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TClingCallFunc caches the JIT-compiled call, constructor and destructor wrappers in std::maps that were function-local statics. Such a static registers its destructor on first use, and for the destructor-wrapper cache the first use can be in the middle of the user's program, e.g. when PyROOT destroys an object of a class template instantiation without compiled dictionary. That is after ROOT registered its atexit handlers, so at exit the cache was destroyed before TROOT::EndOfProcessCleanups() deleted the files and trees still alive, and the destructor lookup for a branch object without compiled dictionary (e.g. std::vector<std::vector>) ran on a dead map: segfault or "corrupted double-linked list" at exit, depending on the heap state.
Making the maps globals of libCling ties their lifetime to the library, like the other interpreter globals. They are then constructed when libCling is loaded by TROOT::InitInterpreter(), i.e. before the atexit handlers that run the end-of-process cleanups are registered, and outlive them.
This is the 6.36 version of the fix for #23728. In 6.40 and later the caches live in CppInterOp, which fixed the same issue by moving them to per-interpreter storage (CppInterOp commit b8ae9f666c).
Assisted-by: claude-code:claude-fable-5-1