From aea30b83dc7f3e76550b36e6425c5921aa6debfa Mon Sep 17 00:00:00 2001 From: Matevz Tadel Date: Tue, 13 Oct 2026 20:01:28 -0700 Subject: [PATCH 1/4] [cling] Let the ASTReader be freed with its interpreter InterpreterCallbacks wraps the ASTContext's existing external source, the ASTReader, into a multiplexer together with its own source. Before doing so it released the ASTContext's reference with resetWithoutRelease(), so the reference count of the ASTReader never dropped to zero and the ASTReader, with everything it had deserialized, leaked with every interpreter. ExternalASTSourceWrapper now holds the wrapped source by IntrusiveRefCntPtr, and the ASTContext's reference is released normally when the multiplexer replaces it. The ASTReader is deleted when the last of the wrapper and the CompilerInstance lets it go. Assisted-by: Claude:Opus 5.5 --- interpreter/cling/lib/Interpreter/InterpreterCallbacks.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/interpreter/cling/lib/Interpreter/InterpreterCallbacks.cpp b/interpreter/cling/lib/Interpreter/InterpreterCallbacks.cpp index 0281592abdab3..52364d8f5851e 100644 --- a/interpreter/cling/lib/Interpreter/InterpreterCallbacks.cpp +++ b/interpreter/cling/lib/Interpreter/InterpreterCallbacks.cpp @@ -72,7 +72,7 @@ namespace cling { /// \brief wraps an ExternalASTSource in an ExternalSemaSource. No functional /// difference between the original source and this wrapper intended. class ExternalASTSourceWrapper : public ExternalSemaSource { - ExternalASTSource* m_Source; + llvm::IntrusiveRefCntPtr m_Source; public: ExternalASTSourceWrapper(ExternalASTSource* Source) : m_Source(Source) { @@ -284,11 +284,6 @@ namespace cling { // If we already have source, we need to create a multiplexer with the // existing source. if (ExistingSource) { - // Make sure the context is not deleting the existing source. - // FIXME: We should delete this, but looking at the other TODO's in - // the destructor we can't easily free our callbacks from here... - Ctx.ExternalSource.resetWithoutRelease(); - // Wrap the existing source in a wrapper so that it becomes an // external sema source. This way we can use the existing multiplexer // for this. From a80dccb9313d12bfaa0dd2bb030af1278fb80e29 Mon Sep 17 00:00:00 2001 From: Matevz Tadel Date: Tue, 13 Oct 2026 21:24:46 -0700 Subject: [PATCH 2/4] [cling] Add --disposable, for interpreters that free all their memory An interpreter keeps its AST and its compiled code until the process ends, even when it is destroyed. CIFactory sets DisableFree for every interpreter, so Interpreter::ShutDown() hands the Sema and the ASTContext to llvm::BuryPointer. The JIT memory managers never free their sections, because unloading in ROOT's interpreter freed code that TClingCallFunc wrappers still pointed to (root-project/root#10898). Added the option --disposable. An interpreter created with it turns DisableFree off, and its memory managers release the JIT's memory. A program can then do work in an interpreter of its own and get all the memory back by destroying it. Interpreters created without the option behave as before. Assisted-by: Claude:Opus 5.5 --- .../include/cling/Interpreter/ClingOptions.td | 2 + .../cling/Interpreter/InvocationOptions.h | 1 + .../lib/Interpreter/IncrementalExecutor.cpp | 5 ++- .../lib/Interpreter/IncrementalExecutor.h | 3 +- .../cling/lib/Interpreter/IncrementalJIT.cpp | 40 +++++++++++++------ .../cling/lib/Interpreter/IncrementalJIT.h | 3 +- .../cling/lib/Interpreter/Interpreter.cpp | 8 +++- .../lib/Interpreter/InvocationOptions.cpp | 3 +- 8 files changed, 46 insertions(+), 19 deletions(-) diff --git a/interpreter/cling/include/cling/Interpreter/ClingOptions.td b/interpreter/cling/include/cling/Interpreter/ClingOptions.td index 69b34aba2dde8..21a1053232d6a 100644 --- a/interpreter/cling/include/cling/Interpreter/ClingOptions.td +++ b/interpreter/cling/include/cling/Interpreter/ClingOptions.td @@ -15,6 +15,8 @@ include "llvm/Option/OptParser.td" def _debugFlags_EQ : Joined<["--"], "debug-only=">; def _debugFlags : Flag<["--"], "debug-only">; #endif +def _disposable : Flag<["--"], "disposable">, + HelpText<"Free all memory of the interpreter, its compiled code included, when it is destroyed">; def _errorout : Flag<["--"], "errorout">, HelpText<"Do not recover from input errors">; // Re-implement to forward to our help def help : Flag<["-", "--"], "help">, HelpText<"Print this help text">; diff --git a/interpreter/cling/include/cling/Interpreter/InvocationOptions.h b/interpreter/cling/include/cling/Interpreter/InvocationOptions.h index e92c7aab5e1ad..226d876ab7139 100644 --- a/interpreter/cling/include/cling/Interpreter/InvocationOptions.h +++ b/interpreter/cling/include/cling/Interpreter/InvocationOptions.h @@ -97,6 +97,7 @@ namespace cling { unsigned ShowVersion : 1; unsigned Help : 1; unsigned NoRuntime : 1; + unsigned Disposable : 1; /// Free all memory on destruction unsigned PtrCheck : 1; /// Enable NullDerefProtectionTransformer bool Verbose() const { return CompilerOpts.Verbose; } diff --git a/interpreter/cling/lib/Interpreter/IncrementalExecutor.cpp b/interpreter/cling/lib/Interpreter/IncrementalExecutor.cpp index 8c184cb352d2b..4bd89ee0de09f 100644 --- a/interpreter/cling/lib/Interpreter/IncrementalExecutor.cpp +++ b/interpreter/cling/lib/Interpreter/IncrementalExecutor.cpp @@ -36,7 +36,8 @@ namespace cling { IncrementalExecutor::IncrementalExecutor(clang::DiagnosticsEngine& /*diags*/, const clang::CompilerInstance& CI, - void *ExtraLibHandle, bool Verbose): + void *ExtraLibHandle, bool Verbose, + bool ReleaseMemory): m_Callbacks(nullptr) #if 0 : m_Diags(diags) @@ -50,7 +51,7 @@ IncrementalExecutor::IncrementalExecutor(clang::DiagnosticsEngine& /*diags*/, llvm::Error Err = llvm::Error::success(); auto EPC = llvm::cantFail(llvm::orc::SelfExecutorProcessControl::Create()); m_JIT.reset(new IncrementalJIT(*this, CI, std::move(EPC), Err, - ExtraLibHandle, Verbose)); + ExtraLibHandle, Verbose, ReleaseMemory)); if (Err) { llvm::logAllUnhandledErrors(std::move(Err), llvm::errs(), "Fatal: "); llvm_unreachable("Propagate this error and exit gracefully"); diff --git a/interpreter/cling/lib/Interpreter/IncrementalExecutor.h b/interpreter/cling/lib/Interpreter/IncrementalExecutor.h index eeb89f2eabdd1..942a3d44641a1 100644 --- a/interpreter/cling/lib/Interpreter/IncrementalExecutor.h +++ b/interpreter/cling/lib/Interpreter/IncrementalExecutor.h @@ -143,7 +143,8 @@ namespace cling { IncrementalExecutor(clang::DiagnosticsEngine& diags, const clang::CompilerInstance& CI, void *ExtraLibHandle = nullptr, - bool Verbose = false); + bool Verbose = false, + bool ReleaseMemory = false); ~IncrementalExecutor(); diff --git a/interpreter/cling/lib/Interpreter/IncrementalJIT.cpp b/interpreter/cling/lib/Interpreter/IncrementalJIT.cpp index 2be6ea97c7d6e..33462eeb453fc 100644 --- a/interpreter/cling/lib/Interpreter/IncrementalJIT.cpp +++ b/interpreter/cling/lib/Interpreter/IncrementalJIT.cpp @@ -114,7 +114,11 @@ namespace { } class ClingMMapper final : public SectionMemoryManager::MemoryMapper { + bool m_Release; + public: + explicit ClingMMapper(bool Release) : m_Release(Release) {} + sys::MemoryBlock allocateMappedMemory(SectionMemoryManager::AllocationPurpose Purpose, size_t NumBytes, @@ -129,14 +133,13 @@ namespace { } std::error_code releaseMappedMemory(sys::MemoryBlock& M) override { - // Disabled until CallFunc is informed about unloading, and can - // re-generate the wrapper (if the decl is still available). See + if (m_Release) + return sys::Memory::releaseMappedMemory(M); + // Disabled, except for disposable interpreters, until CallFunc is + // informed about unloading, and can re-generate the wrapper (if the decl + // is still available). See // https://github.com/root-project/root/issues/10898 -#if 0 - return sys::Memory::releaseMappedMemory(M); -#else return {}; -#endif } }; @@ -263,15 +266,25 @@ namespace { bool needsToReserveAllocationSpace() override { return true; } }; - /// A JITLinkMemoryManager for Cling that never frees its allocations. + /// A JITLinkMemoryManager for Cling that frees its allocations only for a + /// disposable interpreter. class ClingJITLinkMemoryManager : public InProcessMemoryManager { + bool m_Release; + public: - using InProcessMemoryManager::InProcessMemoryManager; + ClingJITLinkMemoryManager(uint64_t PageSize, bool Release) + : InProcessMemoryManager(PageSize), m_Release(Release) {} void deallocate(std::vector Allocs, OnDeallocatedFunction OnDeallocated) override { - // Disabled until CallFunc is informed about unloading, and can - // re-generate the wrapper (if the decl is still available). See + if (m_Release) { + InProcessMemoryManager::deallocate(std::move(Allocs), + std::move(OnDeallocated)); + return; + } + // Disabled, except for disposable interpreters, until CallFunc is + // informed about unloading, and can re-generate the wrapper (if the decl + // is still available). See // https://github.com/root-project/root/issues/10898 // We still have to release the allocations which resets their addresses @@ -557,7 +570,7 @@ IncrementalJIT::~IncrementalJIT() { IncrementalJIT::IncrementalJIT( IncrementalExecutor& Executor, const clang::CompilerInstance &CI, std::unique_ptr EPC, Error& Err, - void *ExtraLibHandle, bool Verbose) + void *ExtraLibHandle, bool Verbose, bool ReleaseMemory) : SkipHostProcessLookup(false), m_JITLink(UseJITLink(CI.getTarget().getTriple())), m_TM(CreateTargetMachine(CI, m_JITLink)), @@ -594,11 +607,12 @@ IncrementalJIT::IncrementalJIT( // together which is needed for exception handling support. unsigned PageSize = cantFail(sys::Process::getPageSize()); auto ObjLinkingLayer = std::make_unique( - ES, std::make_unique(PageSize)); + ES, + std::make_unique(PageSize, ReleaseMemory)); return ObjLinkingLayer; } - auto MMapper = std::make_unique(); + auto MMapper = std::make_unique(ReleaseMemory); auto GetMemMgr = [MMapper = std::move(MMapper)](const llvm::MemoryBuffer &) { return std::make_unique(*MMapper); }; diff --git a/interpreter/cling/lib/Interpreter/IncrementalJIT.h b/interpreter/cling/lib/Interpreter/IncrementalJIT.h index 6fefecb2809f9..c4ce4ee1a8e98 100644 --- a/interpreter/cling/lib/Interpreter/IncrementalJIT.h +++ b/interpreter/cling/lib/Interpreter/IncrementalJIT.h @@ -60,7 +60,8 @@ class IncrementalJIT { IncrementalJIT(IncrementalExecutor& Executor, const clang::CompilerInstance &CI, std::unique_ptr EPC, - llvm::Error &Err, void *ExtraLibHandle, bool Verbose); + llvm::Error &Err, void *ExtraLibHandle, bool Verbose, + bool ReleaseMemory = false); ~IncrementalJIT(); diff --git a/interpreter/cling/lib/Interpreter/Interpreter.cpp b/interpreter/cling/lib/Interpreter/Interpreter.cpp index 2fc17556f4dc3..722395e293dd7 100644 --- a/interpreter/cling/lib/Interpreter/Interpreter.cpp +++ b/interpreter/cling/lib/Interpreter/Interpreter.cpp @@ -268,9 +268,15 @@ namespace cling { Sema& SemaRef = getSema(); + // A disposable interpreter frees its AST and its compiled code when it is + // destroyed. Other interpreters keep both until the process ends. + if (m_Opts.Disposable) + getCI()->getFrontendOpts().DisableFree = false; + if (!isInSyntaxOnlyMode() && !m_Opts.CompilerOpts.CUDADevice) { m_Executor.reset(new IncrementalExecutor(SemaRef.Diags, *getCI(), - extraLibHandle, m_Opts.Verbose())); + extraLibHandle, m_Opts.Verbose(), + m_Opts.Disposable)); if (!m_Executor) return; diff --git a/interpreter/cling/lib/Interpreter/InvocationOptions.cpp b/interpreter/cling/lib/Interpreter/InvocationOptions.cpp index 2acf8a1b5e40b..650a2e1060529 100644 --- a/interpreter/cling/lib/Interpreter/InvocationOptions.cpp +++ b/interpreter/cling/lib/Interpreter/InvocationOptions.cpp @@ -96,6 +96,7 @@ static const char kNoStdInc[] = "-nostdinc"; Opts.ShowVersion = Args.hasArg(OPT_version); Opts.Help = Args.hasArg(OPT_help); Opts.NoRuntime = Args.hasArg(OPT_noruntime); + Opts.Disposable = Args.hasArg(OPT__disposable); Opts.PtrCheck = Args.hasArg(OPT__ptrcheck); if (Arg* MetaStringArg = Args.getLastArg(OPT__metastr, OPT__metastr_EQ)) { Opts.MetaString = MetaStringArg->getValue(); @@ -212,7 +213,7 @@ bool CompilerOptions::DefaultLanguage(const LangOptions* LangOpts) const { InvocationOptions::InvocationOptions(int argc, const char* const* argv) : MetaString("."), ErrorOut(false), NoLogo(false), ShowVersion(false), - Help(false), NoRuntime(false) { + Help(false), NoRuntime(false), Disposable(false) { ArrayRef ArgStrings(argv, argv + argc); unsigned MissingArgIndex, MissingArgCount; From 796b1355529a087daff620e455c30638104689aa Mon Sep 17 00:00:00 2001 From: Matevz Tadel Date: Tue, 13 Oct 2026 21:24:47 -0700 Subject: [PATCH 3/4] [core] Add TInterpreter::CreateDisposable() The interpreter never frees the code it compiles: every line processed stays in memory until the process ends. Code that is not needed after it has run can instead be compiled in an interpreter of its own, which gives all its memory back when it is destroyed. TInterpreter::Disposable is the interface of such an interpreter, with Declare() and ProcessLine(). TCling keeps the arguments it creates its cling interpreter with, and CreateDisposable() creates a TClingDisposable: a cling interpreter with the same arguments and cling's --disposable. It finds the same headers and C++ modules, does not see what was declared in the main interpreter, and runs without cling's runtime. ROOT's LLVM is built without thread support, so its creation, its calls and its destruction take gInterpreterMutex. Added tests to TClingTests: a disposable interpreter finds ROOT's headers and runs code, and on Linux three of them with 200 lines each keep less than 1 MB of heap. Assisted-by: Claude:Opus 5.5 --- core/meta/inc/TInterpreter.h | 20 ++++++ core/metacling/src/CMakeLists.txt | 1 + core/metacling/src/TCling.cxx | 16 +++++ core/metacling/src/TCling.h | 3 + core/metacling/src/TClingDisposable.cxx | 82 +++++++++++++++++++++++++ core/metacling/src/TClingDisposable.h | 43 +++++++++++++ core/metacling/test/TClingTests.cxx | 41 ++++++++++++- 7 files changed, 205 insertions(+), 1 deletion(-) create mode 100644 core/metacling/src/TClingDisposable.cxx create mode 100644 core/metacling/src/TClingDisposable.h diff --git a/core/meta/inc/TInterpreter.h b/core/meta/inc/TInterpreter.h index fe782d13421ec..2be2a78c89174 100644 --- a/core/meta/inc/TInterpreter.h +++ b/core/meta/inc/TInterpreter.h @@ -29,6 +29,7 @@ #include "TVirtualRWMutex.h" #include +#include #include #include #include @@ -129,6 +130,24 @@ class TInterpreter : public TNamed { ~SuspendAutoLoadingRAII() { fInterp->SetClassAutoLoading(fOldValue); } }; + /// An interpreter of its own, for code that is not needed after it has run, created by + /// CreateDisposable(). Destroying it frees the memory of everything it compiled, which + /// the interpreter never does for its own code. + /// + /// It is created with the interpreter's arguments, so it finds the same headers and C++ + /// modules. It does not see what was declared in the interpreter itself, and it does not + /// load libraries: the code it compiles can call into libraries that are already loaded. + /// Nothing may point into its code once it is destroyed, eg a std::function holding a + /// lambda it compiled. Its calls take gInterpreterMutex. + class Disposable { + public: + virtual ~Disposable() = default; + /// Declare code, eg #include directives. Return false on error. + virtual bool Declare(const char *code) = 0; + /// Compile and run a line. Return false on error. + virtual bool ProcessLine(const char *line) = 0; + }; + typedef int (*AutoLoadCallBack_t)(const char*); typedef std::vector > FwdDeclArgsToKeepCollection_t; @@ -144,6 +163,7 @@ class TInterpreter : public TNamed { virtual Int_t AutoParse(const char* cls) = 0; virtual void ClearFileBusy() = 0; virtual void ClearStack() = 0; // Delete existing temporary values + virtual std::unique_ptr CreateDisposable() { return nullptr; } virtual Bool_t Declare(const char* code) = 0; virtual void EndOfLineAction() = 0; virtual TClass *GetClass(const std::type_info& typeinfo, Bool_t load) const = 0; diff --git a/core/metacling/src/CMakeLists.txt b/core/metacling/src/CMakeLists.txt index f7d91833b7e34..2d59287e9fe73 100644 --- a/core/metacling/src/CMakeLists.txt +++ b/core/metacling/src/CMakeLists.txt @@ -28,6 +28,7 @@ ROOT_OBJECT_LIBRARY(MetaCling TClingDataMemberInfo.cxx TClingDeclInfo.cxx TClingDiagnostics.cxx + TClingDisposable.cxx TClingMemberIter.cxx TClingMethodArgInfo.cxx TClingMethodInfo.cxx diff --git a/core/metacling/src/TCling.cxx b/core/metacling/src/TCling.cxx index 022cc1ddba473..e8e8fe53ab27e 100644 --- a/core/metacling/src/TCling.cxx +++ b/core/metacling/src/TCling.cxx @@ -23,6 +23,7 @@ clang/LLVM technology. #include "TClingBaseClassInfo.h" #include "TClingCallFunc.h" +#include "TClingDisposable.h" #include "TClingClassInfo.h" #include "TClingDataMemberInfo.h" #include "TClingMethodArgInfo.h" @@ -1587,6 +1588,9 @@ TCling::TCling(const char *name, const char *title, const char* const argv[], vo if (!EnvOpt.has_value()) extensions.push_back(std::make_shared()); + fDisposableArgs.assign(interpArgs.begin(), interpArgs.end()); + fLlvmResourceDir = llvmResourceDir.Data(); + fInterpreter = std::make_unique(interpArgs.size(), &(interpArgs[0]), llvmResourceDir, extensions, @@ -3201,6 +3205,18 @@ void TCling::ClearStack() // No-op for cling due to cling::Value. } +//////////////////////////////////////////////////////////////////////////////// +/// Create a disposable interpreter, with this interpreter's arguments. Return +/// nullptr if it could not be created. See TInterpreter::Disposable. + +std::unique_ptr TCling::CreateDisposable() +{ + auto disposable = std::make_unique(fDisposableArgs, fLlvmResourceDir); + if (!disposable->IsValid()) + return nullptr; + return disposable; +} + namespace { // Re-enable JIT symbol library autoloading (independently of class/dictionary // autoloading) for the lifetime of the object. See its use in TCling::Declare diff --git a/core/metacling/src/TCling.h b/core/metacling/src/TCling.h index 1c2be1bace445..8ecc65789aeab 100644 --- a/core/metacling/src/TCling.h +++ b/core/metacling/src/TCling.h @@ -130,6 +130,8 @@ class TCling final : public TInterpreter { Bool_t fCxxModulesEnabled;// True if C++ modules was enabled std::unique_ptr fInterpreter; // The interpreter. + std::vector fDisposableArgs; // The interpreter's arguments, for CreateDisposable() + std::string fLlvmResourceDir; // The interpreter's resource directory, for CreateDisposable() std::unique_ptr fMetaProcessor; // The metaprocessor. std::vector *fTemporaries; // Stack of temporaries @@ -208,6 +210,7 @@ class TCling final : public TInterpreter { Bool_t IsAutoLoadNamespaceCandidate(const clang::NamespaceDecl* nsDecl); void ClearFileBusy() final; void ClearStack() final; // Delete existing temporary values + std::unique_ptr CreateDisposable() final; Bool_t Declare(const char* code) final; void EndOfLineAction() final; TClass *GetClass(const std::type_info& typeinfo, Bool_t load) const final; diff --git a/core/metacling/src/TClingDisposable.cxx b/core/metacling/src/TClingDisposable.cxx new file mode 100644 index 0000000000000..877d285315ea6 --- /dev/null +++ b/core/metacling/src/TClingDisposable.cxx @@ -0,0 +1,82 @@ +// @(#)root/core/metacling:$Id$ +// Author: Matevz Tadel 13/10/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#include "TClingDisposable.h" + +#include "TVirtualMutex.h" + +#include "cling/Interpreter/Interpreter.h" + +#include "clang/Frontend/CompilerInstance.h" +#include "clang/Lex/PreprocessorOptions.h" + +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +/// Create the interpreter with `args`, the arguments of gInterpreter, and cling's +/// `--disposable`. It runs without cling's runtime: it does not print values. +/// +/// ROOT's LLVM is built without thread support, so only one interpreter may +/// compile at a time. Creation, every call and destruction take gInterpreterMutex. + +TClingDisposable::TClingDisposable(const std::vector &args, const std::string &llvmResourceDir) + : fArgs(args) +{ + fArgs.push_back("--disposable"); + for (const auto &arg : fArgs) + fArgv.push_back(arg.c_str()); + + R__LOCKGUARD(gInterpreterMutex); + fInterpreter = std::make_unique(fArgv.size(), fArgv.data(), llvmResourceDir.c_str(), + cling::Interpreter::ModuleFileExtensions{}, + /*extraLibHandle=*/nullptr, /*noRuntime=*/true); + if (!fInterpreter->getCI()) { + fInterpreter.reset(); + return; + } + + // The same settings as TCling's: do not check whether the modules' files exist, + // no spell checking, and modules on or off as in the arguments. + clang::CompilerInstance &CI = *fInterpreter->getCI(); + CI.getPreprocessorOpts().DisablePCHOrModuleValidation = clang::DisableValidationForModuleKind::All; + CI.getLangOpts().SpellChecking = false; + CI.getLangOpts().Modules = + std::any_of(fArgv.begin(), fArgv.end(), [](const char *arg) { return !strcmp(arg, "-fmodules"); }); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Destroy the interpreter, and with it all the code it compiled. + +TClingDisposable::~TClingDisposable() +{ + R__LOCKGUARD(gInterpreterMutex); + fInterpreter.reset(); +} + +//////////////////////////////////////////////////////////////////////////////// +/// Declare code, eg #include directives. Return false on error. + +bool TClingDisposable::Declare(const char *code) +{ + R__LOCKGUARD(gInterpreterMutex); + return fInterpreter->declare(code) == cling::Interpreter::kSuccess; +} + +//////////////////////////////////////////////////////////////////////////////// +/// Compile and run a line. Return false on error. + +bool TClingDisposable::ProcessLine(const char *line) +{ + R__LOCKGUARD(gInterpreterMutex); + return fInterpreter->process(line, /*V=*/nullptr, /*T=*/nullptr, /*disableValuePrinting=*/true) == + cling::Interpreter::kSuccess; +} diff --git a/core/metacling/src/TClingDisposable.h b/core/metacling/src/TClingDisposable.h new file mode 100644 index 0000000000000..a3208592d4680 --- /dev/null +++ b/core/metacling/src/TClingDisposable.h @@ -0,0 +1,43 @@ +// @(#)root/core/metacling:$Id$ +// Author: Matevz Tadel 13/10/2026 + +/************************************************************************* + * Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. * + * All rights reserved. * + * * + * For the licensing terms see $ROOTSYS/LICENSE. * + * For the list of contributors see $ROOTSYS/README/CREDITS. * + *************************************************************************/ + +#ifndef ROOT_TClingDisposable +#define ROOT_TClingDisposable + +#include "TInterpreter.h" + +#include +#include +#include + +namespace cling { +class Interpreter; +} + +/// A cling interpreter created with cling's `--disposable` option, which frees +/// all its memory, compiled code included, when it is destroyed. See +/// TInterpreter::Disposable. +class TClingDisposable final : public TInterpreter::Disposable { + std::vector fArgs; ///< cling keeps pointers into the arguments + std::vector fArgv; ///< The arguments as passed to cling + std::unique_ptr fInterpreter; ///< Declared last, so destroyed first + +public: + TClingDisposable(const std::vector &args, const std::string &llvmResourceDir); + ~TClingDisposable() override; + + bool IsValid() const { return fInterpreter != nullptr; } + + bool Declare(const char *code) final; + bool ProcessLine(const char *line) final; +}; + +#endif diff --git a/core/metacling/test/TClingTests.cxx b/core/metacling/test/TClingTests.cxx index 08f846ee5f256..e70fe81af588a 100644 --- a/core/metacling/test/TClingTests.cxx +++ b/core/metacling/test/TClingTests.cxx @@ -3,6 +3,7 @@ #include "TClass.h" #include "TInterpreter.h" #include "TROOT.h" +#include "TString.h" #include "TSystem.h" #include "gmock/gmock.h" @@ -11,6 +12,10 @@ #include #include +#ifdef __GLIBC__ +#include +#endif + // FIXME: We should probably have such a facility in TCling. static void cleanup() { @@ -509,4 +514,38 @@ using func0_ret_t = typename ROOT::TypeTraits::CallableTraits:: auto res = gInterpreter->Declare(expression.c_str()); EXPECT_TRUE(res); } -#endif \ No newline at end of file +#endif +// A disposable interpreter finds ROOT's headers and runs code that calls into loaded libraries. +TEST_F(TClingTests, Disposable) +{ + auto disposable = gInterpreter->CreateDisposable(); + ASSERT_NE(nullptr, disposable); + EXPECT_TRUE(disposable->Declare("#include \"TNamed.h\"")); + int result = 0; + EXPECT_TRUE(disposable->ProcessLine( + TString::Format("{ TNamed n(\"name\", \"title\"); *(int *)%p = n.GetTitle()[0]; }", (void *)&result))); + EXPECT_EQ('t', result); + EXPECT_FALSE(disposable->ProcessLine("TClingTests_undeclared();")); +} + +// Destroying a disposable interpreter frees the memory of the code it compiled. +#if defined(__GLIBC__) && (__GLIBC__ > 2 || (__GLIBC__ == 2 && __GLIBC_MINOR__ >= 33)) +TEST_F(TClingTests, DisposableFreesMemory) +{ + int sum = 0; + auto run = [&sum]() { + auto disposable = gInterpreter->CreateDisposable(); + for (int i = 0; i < 200; ++i) + disposable->ProcessLine(TString::Format("{ int v%d = %d; *(int *)%p += v%d; }", i, i, (void *)&sum, i)); + }; + // The first one leaves behind what stays for the process, eg the mapped module files. + run(); + const long before = mallinfo2().uordblks; + for (int i = 0; i < 3; ++i) + run(); + const long after = mallinfo2().uordblks; + EXPECT_EQ(4 * 19900, sum); + // gInterpreter keeps 5.7 MB for these 600 lines (Linux, Debug build). + EXPECT_LT(after - before, 1024 * 1024); +} +#endif From dda03faa338e8e7f051ae4b6eeafd52c7a364c61 Mon Sep 17 00:00:00 2001 From: Matevz Tadel Date: Thu, 15 Oct 2026 07:47:41 -0700 Subject: [PATCH 4/4] [eve7] Optionally execute MIRs in a disposable interpreter MIRs go through TMethodCall, which cannot pass every argument type: a MIR calling REveElement::SetTitle("...") crashes the server, because the string literal does not become the std::string the method takes. An alternative through gROOT->ProcessLine() was commented out, because every compiled MIR stays in the interpreter's memory. With WebEve.MIRInterpreter set to Disposable, a MIR is compiled and run in a disposable interpreter instead, which declares the header of each class it is used for. After WebEve.MIRsPerInterpreter MIRs, 1000 by default, the interpreter is replaced, and destroying it frees the memory of the code it compiled. TMethodCall stays the default: it is much faster for the MIRs it can handle. Assisted-by: Claude:Opus 5.5 --- graf3d/eve7/inc/ROOT/REveManager.hxx | 9 ++++ graf3d/eve7/src/REveManager.cxx | 79 +++++++++++++++++++++------- 2 files changed, 70 insertions(+), 18 deletions(-) diff --git a/graf3d/eve7/inc/ROOT/REveManager.hxx b/graf3d/eve7/inc/ROOT/REveManager.hxx index 82e6e547440fa..454b1ff1d6016 100644 --- a/graf3d/eve7/inc/ROOT/REveManager.hxx +++ b/graf3d/eve7/inc/ROOT/REveManager.hxx @@ -17,6 +17,7 @@ #include #include +#include "TInterpreter.h" #include "TSysEvtHandler.h" #include @@ -28,6 +29,7 @@ #include #include #include +#include class TMap; class TExMap; @@ -157,6 +159,12 @@ protected: ServerState fServerState; std::unordered_map > fMethCallMap; + // MIR execution in a disposable interpreter, see WebEve.MIRInterpreter + int fMIRsPerInterpreter{0}; ///< 0: MIRs go through TMethodCall + int fMIRInterpreterCalls{0}; + std::unique_ptr fMIRInterpreter; + std::unordered_set fMIRInterpreterClasses; ///< Classes declared in fMIRInterpreter + Logger fLogger; REveServerStatus fServerStatus; bool fIsRCore{false}; @@ -171,6 +179,7 @@ protected: void MIRExecThread(); void ExecuteMIR(std::shared_ptr mir); + void AssertMIRInterpreter(TClass *cls); void StreamSceneChangesToJson(); void SendMotionChanges(); diff --git a/graf3d/eve7/src/REveManager.cxx b/graf3d/eve7/src/REveManager.cxx index 913959309f8b3..93bbb7c5568a2 100644 --- a/graf3d/eve7/src/REveManager.cxx +++ b/graf3d/eve7/src/REveManager.cxx @@ -33,6 +33,7 @@ #include "TMap.h" #include "TExMap.h" #include "TEnv.h" +#include "TString.h" #include "TError.h" #include "TColor.h" #include "TPRegexp.h" @@ -48,6 +49,7 @@ #include #include #include +#include #include #include #include @@ -114,6 +116,8 @@ WebEve.DisableShow: 1 # do not start new web browser when REveManager::Show i WebEve.HTimeout: 200 # timeout in ms for elements highlight WebEve.DblClick: Off # mouse double click handling in GL viewer: Off or Reset WebEve.TableRowHeight: 33 # size of each row in pixels in the Table view, can be used to make design more compact +WebEve.MIRInterpreter: TMethodCall # how MIRs are executed: TMethodCall, or Disposable to compile them in a disposable interpreter +WebEve.MIRsPerInterpreter: 1000 # with Disposable, the number of MIRs after which the interpreter is replaced */ //////////////////////////////////////////////////////////////////////////////// @@ -214,6 +218,9 @@ REveManager::REveManager() if (strcmp(gl_viewer, "RCore") == 0) fIsRCore = true; + if (strcmp(gEnv->GetValue("WebEve.MIRInterpreter", "TMethodCall"), "Disposable") == 0) + fMIRsPerInterpreter = std::max(1, gEnv->GetValue("WebEve.MIRsPerInterpreter", 1000)); + // this is call-back, invoked when message received via websocket fWebWindow->SetCallBacks([this](unsigned connid) { WindowConnect(connid); }, [this](unsigned connid, const std::string &arg) { WindowData(connid, arg); }, @@ -1099,37 +1106,47 @@ void REveManager::ExecuteMIR(std::shared_ptr mir) tag = mir->fCtype + "::" + m.str(1); std::shared_ptr mc; - auto mmi = fMethCallMap.find(tag); - if (mmi != fMethCallMap.end()) + if (fMIRsPerInterpreter > 0) { - mc = mmi->second; + AssertMIRInterpreter(call_cls); } else { - const TMethod *meth = call_cls->GetMethodAllAny(m.str(1).c_str()); - if ( ! meth) - throw eh + "Can not find TMethod matching '" + m.str(1) + "'."; - mc = std::make_shared(meth); - fMethCallMap.insert(std::make_pair(tag, mc)); + auto mmi = fMethCallMap.find(tag); + if (mmi != fMethCallMap.end()) + { + mc = mmi->second; + } + else + { + const TMethod *meth = call_cls->GetMethodAllAny(m.str(1).c_str()); + if ( ! meth) + throw eh + "Can not find TMethod matching '" + m.str(1) + "'."; + mc = std::make_shared(meth); + fMethCallMap.insert(std::make_pair(tag, mc)); + } } REveElement::SetMirContext(el); - R__LOCKGUARD_CLING(gInterpreterMutex); - mc->Execute(el_casted, m.str(2).c_str()); + if (mc) + { + R__LOCKGUARD_CLING(gInterpreterMutex); + mc->Execute(el_casted, m.str(2).c_str()); + } + else + { + // Compiled code, so arguments of any type TMethodCall can not handle work, too. + std::string line = TString::Format("((%s *)%p)->%s(%s);", mir->fCtype.c_str(), el_casted, + m.str(1).c_str(), m.str(2).c_str()).Data(); + if ( ! fMIRInterpreter->ProcessLine(line.c_str())) + R__LOG_ERROR(REveLog()) << eh << "error compiling " << tag << ": '" << line << "'."; + } if (REveElement::stlMirError) { R__LOG_ERROR(REveLog()) << eh << "error executing " << tag << ":" << REveElement::stlMirErrorString << " (code " << REveElement::stlMirError << ")."; } REveElement::ClearMirContext(); - - // Alternative implementation through Cling. "Leaks" 200 kB per call. - // This might be needed for function calls that involve data-types TMethodCall - // can not handle. - // std::stringstream cmd; - // cmd << "((" << mir->fCtype << "*)" << std::hex << std::showbase << (size_t)el << ")->" << mir->fCmd << ";"; - // std::cout << cmd.str() << std::endl; - // gROOT->ProcessLine(cmd.str().c_str()); } catch (std::exception &e) { R__LOG_ERROR(REveLog()) << "caught exception executing " << tag << ": " << e.what(); } catch (...) { @@ -1137,6 +1154,32 @@ void REveManager::ExecuteMIR(std::shared_ptr mir) } } +//////////////////////////////////////////////////////////////////////////////// +/// Make sure the disposable interpreter for MIRs exists and has the declaration of +/// class `cls`. The interpreter is replaced every fMIRsPerInterpreter MIRs: destroying +/// it frees the memory of the code compiled for the MIRs it ran. + +void REveManager::AssertMIRInterpreter(TClass *cls) +{ + static const REveException eh(""); + + if (!fMIRInterpreter || fMIRInterpreterCalls >= fMIRsPerInterpreter) { + fMIRInterpreter.reset(); + fMIRInterpreterClasses.clear(); + fMIRInterpreterCalls = 0; + fMIRInterpreter = gInterpreter->CreateDisposable(); + if (!fMIRInterpreter) + throw eh + "Can not create a disposable interpreter for MIRs."; + } + if (fMIRInterpreterClasses.count(cls) == 0) { + const char *header = cls->GetDeclFileName(); + if (!header || !*header || !fMIRInterpreter->Declare(TString::Format("#include \"%s\"", header))) + throw eh + "Can not declare class '" + cls->GetName() + "' for MIRs."; + fMIRInterpreterClasses.insert(cls); + } + ++fMIRInterpreterCalls; +} + // Write scene change into scenes's internal json member void REveManager::StreamSceneChangesToJson() {