From 85f383242370a9673679bb32094e1239efd23106 Mon Sep 17 00:00:00 2001 From: Juan Miguel Carceller Date: Thu, 22 Oct 2026 17:11:40 +0200 Subject: [PATCH] [cppjit] Remember templated names resolved by interop::GetScope --- .../cppjit/src/interop/interop_wrapper.cxx | 17 +++++++++++++-- .../pyroot/cppjit/test/test_regression.py | 21 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx index ef48f950e50d2..2290af6f6a5bf 100644 --- a/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx +++ b/bindings/pyroot/cppjit/src/interop/interop_wrapper.cxx @@ -764,6 +764,17 @@ interop::TCppScope_t interop::GetUnderlyingScope(TCppScope_t scope) { interop::TCppScope_t interop::GetScope(const std::string& name, TCppScope_t parent_scope) { std::unique_lock Lock(InterOpMutex); + // Cpp::GetScope fails on a templated name, which makes ROOT try to autoload + // it, parsing the name again, and AppendTypesSlow below declares a new + // variable to resolve it. Both keep memory that is never freed, so remember + // the result instead of repeating this on every lookup of the same name. + static std::map, TCppScope_t> template_scopes; + const bool is_template = name.find('<') != std::string::npos; + const auto key = std::make_pair(parent_scope.data, name); + if (is_template) + if (auto it = template_scopes.find(key); it != template_scopes.end()) + return it->second; + // CppInterOp directly looks at the AST which is not enough. // We require lazy module loading that ROOT relies on, so we do it here // first. Use TClass::GetClass to trigger auto-loading of dictionaries and @@ -792,7 +803,7 @@ interop::TCppScope_t interop::GetScope(const std::string& name, } // FIXME: avoid string parsing here - if (name.find('<') != std::string::npos) { + if (is_template) { // Templated type; may need instantiation. Resolve the whole type // expression (e.g. "std::array") and read back its scope. // Splitting off the argument list and resolving it directly cannot @@ -815,8 +826,10 @@ interop::TCppScope_t interop::GetScope(const std::string& name, // it, so the specialization may still be declared-but-undefined. // Force its definition: callers expect a complete scope, e.g. to // walk its base classes. - if (scope) + if (scope) { Cpp::IsComplete(scope); + template_scopes[key] = scope; + } return scope; } } diff --git a/bindings/pyroot/cppjit/test/test_regression.py b/bindings/pyroot/cppjit/test/test_regression.py index cbd5f566667d4..ce9804bde7e89 100644 --- a/bindings/pyroot/cppjit/test/test_regression.py +++ b/bindings/pyroot/cppjit/test/test_regression.py @@ -1733,3 +1733,24 @@ def test56_enum_arg_typedefed_underlying_type(self): assert ns.pick(ns.S.A) == 1 assert ns.pick(ns.S.B) == 5 + + def test57_repeated_template_scope_lookup(self): + """Binding to a templated class by name does not grow the memory""" + + from pytest import importorskip + + psutil = importorskip("psutil") + + import cppjit + + v = cppjit.gbl.std.vector[int]() + addr = cppjit.addressof(v) + process = psutil.Process() + for name in ["std::vector", "vector"]: + cppjit.bind_object(addr, name) + before = process.memory_info().rss + for _ in range(2000): + cppjit.bind_object(addr, name) + # resolving the name used to declare code in the interpreter on + # every call, growing the memory by about 10 kB each time + assert process.memory_info().rss - before < 8 * 1024 * 1024, name