Skip to content

Commit ae08094

Browse files
committed
[Python] Set memory policy to "strict"
The ROOT Python interfaces have many memory leaks, which is a major pain point for people using it for long-running scripts in batch jobs. One source of memory leaks was indentified to be the "heuristic memory policy" of cppyy. This means that cppyy assumes that every non-const pointer member function argument was interpreted as the object taking ownership if the argument. For examle, take the non-owning RooLinkedList container. It has a `RooLinkedList::Add(RooAbsArg *arg)` method. ROOT wrongly assumes that this means the RooLinkedList takes ownership of arg, and it drops the ROOT overship. Nobody feels responsible for deleting the object anymore, and there is a memory leak or `arg`. That particular leak was reported in this forum post: https://root-forum.cern.ch/t/memory-leak-in-fits/56249 Function parameters of type `T *` are very common in ROOT, and only rarely do they imply ownership transfer. So changing the memory policy to "strict" would surely fix also many other memory leaks that are not reported so far. In fact, upstream cppyy doesn't even have this heuristic memory policy anymore! So moving ROOT also to the strict memory policy closes the gap between ROOT and cppyy. The potential drawback of this change are crashes in usercode if memory is not properly managed. But these problems should either be fixed by: * the user * dedicated pythonizations for these methods to manage shared ownership via Python reference counters (i.e., setting the parameter as an attribute of the object that the member function was called on) This follows up on PR #4294, in particular it reverts 3a12063.
1 parent 6478dae commit ae08094

File tree

13 files changed

+206
-13
lines changed

13 files changed

+206
-13
lines changed

README/ReleaseNotes/v640/index.md

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ The following people have contributed to this new version:
3939
## Removals
4040

4141
* The `TH1K` class was removed. `TMath::KNNDensity` can be used in its stead.
42-
* The `TObject` equality operator pythonization (`TObject.__eq__`) that was deprecated in ROOT 6.38 and scheduled for removal in ROOT 6.40 is removed.
42+
* The `TObject` equality operator Pythonization (`TObject.__eq__`) that was deprecated in ROOT 6.38 and scheduled for removal in ROOT 6.40 is removed.
4343
* Comparing C++ `nullptr` objects with `None` in Python now raises a `TypeError`, as announced in the ROOT 6.38 release notes. Use truth-value checks like `if not x` or `x is None` instead.
4444
* The `TGLIncludes.h` and `TGLWSIncludes.h` that were deprecated in ROOT 6.38 and scheduled for removal are gone now. Please include your required headers like `<GL/gl.h>` or `<GL/glu.h>` directly.
4545
* The GLEW headers (`GL/eglew.h`, `GL/glew.h`, `GL/glxew.h`, and `GL/wglew.h`) that were installed when building ROOT with `builtin_glew=ON` are no longer installed. This is done because ROOT is moving away from GLEW for loading OpenGL extensions.
@@ -113,6 +113,60 @@ As part of this migration, the following build options are deprecated. From ROOT
113113

114114
ROOT dropped support for Python 3.9, meaning ROOT now requires at least Python 3.10.
115115

116+
### Changed ownership policy for non-`const` pointer member function parameters
117+
118+
If you have a member function taking a raw pointer, like `MyClass::foo(T *obj)`,
119+
calling such method on a Python object `my_instance` of type `MyClass`
120+
would assume that the memory ownership of `obj` transfers to `my_instance`.
121+
122+
However, this resulted in many memory leaks, since many functions with such a
123+
signature actually don't take ownership of the object.
124+
125+
Now, the Python interface of ROOT will not make this assumption anymore.
126+
Because of this change, some double-deletes or dangling references
127+
might creep up in your scripts. These need to be fixed by properly managing
128+
object lifetime with Python references.
129+
130+
You can fix the dangling references problem for example via:
131+
132+
1. Assigning the object to a Python variable
133+
2. Creating an owning collection that keeps the objects alive
134+
3. Writing a Pythonization for the member function that transfers the ownership if needed
135+
136+
The double-delete problems can be fixed via:
137+
138+
1. Dropping the ownership on the Python side with `ROOT.SetOwnership(obj, False)`
139+
3. Writing a Pythonization for the member function that drops the ownership on the Python side as above
140+
141+
This affects for example the `TList::Add(TObject *obj)` member function, which
142+
will not transfer ownership from PyROOT to the TList anymore. The new policy
143+
fixes a memory leak, but at the same time it is not possible anymore to create
144+
the contained elements in place:
145+
146+
```python
147+
# A TList is by default a non-owning container
148+
my_list = ROOT.TList()
149+
150+
151+
# This is working, but resulted in memory leak prior to ROOT 6.40:
152+
obj_1 = ROOT.TObjString("obj_1")
153+
my_list.Add(obj_1)
154+
155+
156+
# This is not allowed anymore, as the temporary would be
157+
# deleted immediately leaving a dangling pointer:
158+
my_list.Add(ROOT.TObjString("obj_2"))
159+
160+
# Python reference count to contained object is now zero,
161+
# TList contains dangling pointer!
162+
```
163+
164+
**Note:** You can change back to the old policy by calling
165+
`ROOT.SetMemoryPolicy(ROOT.kMemoryHeuristics)` after importing ROOT, but this
166+
should be only used for debugging purposes and this function might be removed
167+
in the future!
168+
169+
116170
## Command-line utilities
117171

118172
## JavaScript ROOT

bindings/pyroot/pythonizations/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ if(roofit)
3838
ROOT/_pythonization/_roofit/_roojsonfactorywstool.py
3939
ROOT/_pythonization/_roofit/_roomcstudy.py
4040
ROOT/_pythonization/_roofit/_roomsgservice.py
41+
ROOT/_pythonization/_roofit/_rooplot.py
4142
ROOT/_pythonization/_roofit/_rooprodpdf.py
4243
ROOT/_pythonization/_roofit/_roorealvar.py
4344
ROOT/_pythonization/_roofit/_roosimultaneous.py

bindings/pyroot/pythonizations/python/ROOT/_facade.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,6 @@ def _finalSetup(self):
192192
if not self.gROOT.IsBatch() and self.PyConfig.StartGUIThread:
193193
self.app.init_graphics()
194194

195-
# Set memory policy to kUseHeuristics.
196-
# This restores the default in PyROOT which was changed
197-
# by new Cppyy
198-
self.SetHeuristicMemoryPolicy(True)
199-
200195
# The automatic conversion of ordinary obejcts to smart pointers is
201196
# disabled for ROOT because it can cause trouble with overload
202197
# resolution. If a function has overloads for both ordinary objects and

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_memory_utils.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,28 @@ def _SetDirectory_SetOwnership(self, dir):
6363
import ROOT
6464

6565
ROOT.SetOwnership(self, False)
66+
67+
68+
def declare_cpp_owned_arg(position, name, positional_args, keyword_args, condition=lambda _: True):
69+
"""
70+
Helper function to drop Python ownership of a specific function argument
71+
with a given position and name, referring to the C++ signature.
72+
73+
positional_args and keyword_args should be the usual args and kwargs passed
74+
to the function, and condition is an optional condition on which the Python
75+
ownership is dropped.
76+
"""
77+
import ROOT
78+
79+
# has to match the C++ argument name
80+
if name in keyword_args:
81+
arg = keyword_args[name]
82+
elif len(positional_args) > position:
83+
arg = positional_args[0]
84+
else:
85+
# This can happen if the function was called with too few arguments.
86+
# arguments. In that case we should not do anything.
87+
return
88+
89+
if condition(arg):
90+
ROOT.SetOwnership(arg, False)

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from ._roojsonfactorywstool import RooJSONFactoryWSTool
4141
from ._roomcstudy import RooMCStudy
4242
from ._roomsgservice import RooMsgService
43+
from ._rooplot import RooPlot
4344
from ._rooprodpdf import RooProdPdf
4445
from ._roorealvar import RooRealVar
4546
from ._roosimultaneous import RooSimultaneous
@@ -69,6 +70,7 @@
6970
RooJSONFactoryWSTool,
7071
RooMCStudy,
7172
RooMsgService,
73+
RooPlot,
7274
RooProdPdf,
7375
RooRealVar,
7476
RooSimultaneous,

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_roofit/_rooabspdf.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,19 @@ def _pack_cmd_args(*args, **kwargs):
3131
assert len(kwargs) == 0
3232

3333
# Put RooCmdArgs in a RooLinkedList
34-
cmdList = ROOT.RooLinkedList()
34+
cmd_list = ROOT.RooLinkedList()
3535
for cmd in args:
3636
if not isinstance(cmd, ROOT.RooCmdArg):
3737
raise TypeError("This function only takes RooFit command arguments.")
38-
cmdList.Add(cmd)
38+
cmd_list.Add(cmd)
3939

40-
return cmdList
40+
# The RooLinkedList passed to functions like fitTo() is expected to be
41+
# non-owning. To make sure that the RooCmdArgs live long enough, we attach
42+
# then as an attribute of the output list, such that the Python reference
43+
# counter doesn't hit zero.
44+
cmd_list._owning_pylist = args
45+
46+
return cmd_list
4147

4248

4349
class RooAbsPdf(RooAbsReal):
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Authors:
2+
# * Jonas Rembser 09/2023
3+
4+
################################################################################
5+
# Copyright (C) 1995-2020, Rene Brun and Fons Rademakers. #
6+
# All rights reserved. #
7+
# #
8+
# For the licensing terms see $ROOTSYS/LICENSE. #
9+
# For the list of contributors see $ROOTSYS/README/CREDITS. #
10+
################################################################################
11+
12+
13+
class RooPlot(object):
14+
def addObject(self, *args, **kwargs):
15+
from ROOT._pythonization._memory_utils import declare_cpp_owned_arg
16+
17+
# Python should transfer the ownership to the RooPlot
18+
declare_cpp_owned_arg(0, "obj", args, kwargs)
19+
20+
return self._addObject(*args, **kwargs)

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tcollection.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,26 @@ def _iter_pyz(self):
9393
yield o
9494

9595

96+
def _TCollection_Add(self, *args, **kwargs):
97+
from ROOT._pythonization._memory_utils import declare_cpp_owned_arg
98+
99+
def condition(_):
100+
return self.IsOwner()
101+
102+
declare_cpp_owned_arg(0, "obj", args, kwargs, condition=condition)
103+
104+
self._Add(*args, **kwargs)
105+
106+
96107
@pythonization('TCollection')
97108
def pythonize_tcollection(klass):
98109
# Parameters:
99110
# klass: class to be pythonized
100111

112+
# Pythonize Add()
113+
klass._Add = klass.Add
114+
klass.Add = _TCollection_Add
115+
101116
# Add Python lists methods
102117
klass.append = klass.Add
103118
klass.remove = _remove_pyz

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tgraph.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,9 @@
181181

182182
import cppyy
183183

184+
from . import pythonization
185+
186+
184187
def set_size(self, buf):
185188
# Parameters:
186189
# - self: graph object
@@ -213,3 +216,26 @@ def set_size(self, buf):
213216
# Add the composite to the list of pythonizors
214217
cppyy.py.add_pythonization(comp)
215218

219+
def _TMultiGraph_Add(self, *args, **kwargs):
220+
"""
221+
The TMultiGraph takes ownership of the added graphs, unless we're using the
222+
Add(TMultiGraph*) overload, in which cases it adopts the graphs inside the
223+
other TMultiGraph.
224+
"""
225+
from ROOT._pythonization._memory_utils import declare_cpp_owned_arg
226+
227+
def condition(gr):
228+
import ROOT
229+
230+
return gr and not isinstance(gr, ROOT.TMultiGraph)
231+
232+
declare_cpp_owned_arg(0, "graph", args, kwargs, condition=condition)
233+
234+
self._Add(*args, **kwargs)
235+
236+
237+
@pythonization("TMultiGraph")
238+
def pythonize_tmultigraph(klass):
239+
# Pythonizations for TMultiGraph::Add
240+
klass._Add = klass.Add
241+
klass.Add = _TMultiGraph_Add

bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tseqcollection.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,10 @@ def _reverse_pyz(self):
200200
return
201201

202202
t = tuple(self)
203+
was_owner = self.IsOwner()
204+
self.SetOwner(False)
203205
self.Clear()
206+
self.SetOwner(was_owner)
204207
for elem in t:
205208
self.AddAt(elem, 0)
206209

@@ -222,7 +225,10 @@ def _sort_pyz(self, *args, **kwargs):
222225
# Sort in a Python list copy
223226
pylist = list(self)
224227
pylist.sort(*args, **kwargs)
228+
was_owner = self.IsOwner()
229+
self.SetOwner(False)
225230
self.Clear()
231+
self.SetOwner(was_owner)
226232
self.extend(pylist)
227233

228234

@@ -241,11 +247,30 @@ def _index_pyz(self, val):
241247
return idx
242248

243249

250+
def _TSeqCollection_AddAt(self, *args, **kwargs):
251+
from ROOT._pythonization._memory_utils import declare_cpp_owned_arg
252+
253+
def condition(_):
254+
return self.IsOwner()
255+
256+
declare_cpp_owned_arg(0, "obj", args, kwargs, condition=condition)
257+
258+
self._AddAt(*args, **kwargs)
259+
260+
244261
@pythonization('TSeqCollection')
245262
def pythonize_tseqcollection(klass):
263+
from ROOT._pythonization._tcollection import _TCollection_Add
264+
246265
# Parameters:
247266
# klass: class to be pythonized
248267

268+
# Pythonize Add() methods
269+
klass._Add = klass.Add
270+
klass.Add = _TCollection_Add
271+
klass._AddAt = klass.AddAt
272+
klass.AddAt = _TSeqCollection_AddAt
273+
249274
# Item access methods
250275
klass.__getitem__ = _getitem_pyz
251276
klass.__setitem__ = _setitem_pyz
@@ -257,3 +282,17 @@ def pythonize_tseqcollection(klass):
257282
klass.reverse = _reverse_pyz
258283
klass.sort = _sort_pyz
259284
klass.index = _index_pyz
285+
286+
287+
@pythonization("TList")
288+
def pythonize_tlist(klass):
289+
from ROOT._pythonization._tcollection import _TCollection_Add
290+
291+
# Parameters:
292+
# klass: class to be pythonized
293+
294+
# Pythonize Add() methods
295+
klass._Add = klass.Add
296+
klass.Add = _TCollection_Add
297+
klass._AddAt = klass.AddAt
298+
klass.AddAt = _TSeqCollection_AddAt

0 commit comments

Comments
 (0)