Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions README/ReleaseNotes/v640/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,61 @@ As part of this migration, the following build options are deprecated. From ROOT

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

### Changed ownership policy for non-`const` pointer member function parameters

If you have a member function taking a raw pointer, like `MyClass::foo(T *obj)`,
PyROOT was so far assuming that calling this method on `my_instance`
transfers the ownership of `obj` to `my_instance`.
Comment on lines +118 to +120
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If you have a member function taking a raw pointer, like `MyClass::foo(T *obj)`,
PyROOT was so far assuming that calling this method on `my_instance`
transfers the ownership of `obj` to `my_instance`.
If you have a member function taking a raw pointer, like `MyClass::foo(T *obj)`,
calling such method on a Python object `my_instance` of type `MyClass`
would assume that the memory ownership of `obj` transfers to `my_instance`.


However, this resulted in many memory leaks, since many functions with such a
signature actually don't take ownership of the object.

To avoid such memory leaks, PyROOT now doesn't make this guess anymore as of
ROOT 6.32. Because of this change, some double-deletes or dangling references
might creep up in your scripts. These need to be fixed by properly managing
object lifetime with Python references.
Comment on lines +125 to +128
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
To avoid such memory leaks, PyROOT now doesn't make this guess anymore as of
ROOT 6.32. Because of this change, some double-deletes or dangling references
might creep up in your scripts. These need to be fixed by properly managing
object lifetime with Python references.
Now, the Python interface of ROOT will not make this assumption anymore.
Because of this change, some double-deletes or dangling references
might creep up in your scripts. These need to be fixed by properly managing
object lifetime with Python references.


You can fix the dangling references problem for example via:

1. Assigning the object to a python variable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. Assigning the object to a python variable
1. Assigning the object to a Python variable

2. Creating an owning collection that keeps the objects alive
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
2. Creating an owning collection that keeps the objects alive
2. Creating an owning collection that keeps the objects alive, such as...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be further specified

3. Writing a pythonization for the member function that does the ownership
transfer if needed
Comment on lines +134 to +135
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
3. Writing a pythonization for the member function that does the ownership
transfer if needed
3. Writing a Pythonization for the member function that transfers the ownership if needed


The double-delete problems can be fixed via:

1. Drop the ownership on the Python side with `ROOT.SetOwnership(obj, False)`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
1. Drop the ownership on the Python side with `ROOT.SetOwnership(obj, False)`
1. Dropping the ownership on the Python side with `ROOT.SetOwnership(obj, False)`

3. Writing a pythonization for the member function that drops the ownership on the Python side as above

This affects for example the `TList::Add(TObject *obj)` member function, which
will not transfer ownership from PyROOT to the TList anymore. The new policy
fixes a memory leak, but at the same time it is not possible anymore to create
the contained elements in place:
Comment on lines +142 to +145
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is now invalid in light of the latest Pythonizations you added, right?


```python
# A TList is by default a non-owning container
my_list = ROOT.TList()


# This is working, but resulted in memory leak prior to ROOT 6.32:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# This is working, but resulted in memory leak prior to ROOT 6.32:
# This is working, but resulted in memory leak prior to ROOT 6.40:

obj_1 = ROOT.TObjString("obj_1")
my_list.Add(obj_1)


# This is not allowed anymore, as the temporary would be
# deleted immediately leaving a dangling pointer:
my_list.Add(ROOT.TObjString("obj_2"))
Comment on lines +157 to +159
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you remind me where this is enforced in code?


# Python reference count to contained object is now zero,
# TList contains dangling pointer!
Comment on lines +161 to +162
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also this is now not true thanks to the latest changes, right?

```

**Note:** You can change back to the old policy by calling
`ROOT.SetMemoryPolicy(ROOT.kMemoryHeuristics)` after importing ROOT, but this
should be only used for debugging purposes and this function might be removed
in the future!


## Command-line utilities

## JavaScript ROOT
Expand Down
1 change: 1 addition & 0 deletions bindings/pyroot/pythonizations/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ if(roofit)
ROOT/_pythonization/_roofit/_roojsonfactorywstool.py
ROOT/_pythonization/_roofit/_roomcstudy.py
ROOT/_pythonization/_roofit/_roomsgservice.py
ROOT/_pythonization/_roofit/_rooplot.py
ROOT/_pythonization/_roofit/_rooprodpdf.py
ROOT/_pythonization/_roofit/_roorealvar.py
ROOT/_pythonization/_roofit/_roosimultaneous.py
Expand Down
5 changes: 0 additions & 5 deletions bindings/pyroot/pythonizations/python/ROOT/_facade.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,6 @@ def _finalSetup(self):
if not self.gROOT.IsBatch() and self.PyConfig.StartGUIThread:
self.app.init_graphics()

# Set memory policy to kUseHeuristics.
# This restores the default in PyROOT which was changed
# by new Cppyy
self.SetHeuristicMemoryPolicy(True)

# The automatic conversion of ordinary obejcts to smart pointers is
# disabled for ROOT because it can cause trouble with overload
# resolution. If a function has overloads for both ordinary objects and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,26 @@ def _SetDirectory_SetOwnership(self, dir):
import ROOT

ROOT.SetOwnership(self, False)


def declare_cpp_owned_arg(position, name, positional_args, keyword_args, condition=lambda _: True):
"""
Helper function to drop Python ownership of a specific funciton argument
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Helper function to drop Python ownership of a specific funciton argument
Helper function to drop Python ownership of a specific function argument

with a given position and name, referring to the C++ signature.

positional_args and keyword_args should be the usual args and kwargs passed
to the function, and condition is an optional condition on which the Python
ownership is dropped.
"""
import ROOT

arg = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not required in Python, unless you expect sometimes the two if conditions to never trigger thus arg would not be defined. If this is a possibility, then I wonder if condition(arg) could be ill-defined, depending on whether the function condition behaves well with None


# has to match the C++ argument name
if name in keyword_args:
arg = keyword_args[name]
elif len(positional_args) > position:
arg = positional_args[0]

if condition(arg):
ROOT.SetOwnership(arg, False)
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from ._roojsonfactorywstool import RooJSONFactoryWSTool
from ._roomcstudy import RooMCStudy
from ._roomsgservice import RooMsgService
from ._rooplot import RooPlot
from ._rooprodpdf import RooProdPdf
from ._roorealvar import RooRealVar
from ._roosimultaneous import RooSimultaneous
Expand Down Expand Up @@ -69,6 +70,7 @@
RooJSONFactoryWSTool,
RooMCStudy,
RooMsgService,
RooPlot,
RooProdPdf,
RooRealVar,
RooSimultaneous,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,19 @@ def _pack_cmd_args(*args, **kwargs):
assert len(kwargs) == 0

# Put RooCmdArgs in a RooLinkedList
cmdList = ROOT.RooLinkedList()
cmd_list = ROOT.RooLinkedList()
for cmd in args:
if not isinstance(cmd, ROOT.RooCmdArg):
raise TypeError("This function only takes RooFit command arguments.")
cmdList.Add(cmd)
cmd_list.Add(cmd)

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

return cmd_list


class RooAbsPdf(RooAbsReal):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Authors:
# * Jonas Rembser 09/2023

################################################################################
# Copyright (C) 1995-2020, Rene Brun and Fons Rademakers. #
# All rights reserved. #
# #
# For the licensing terms see $ROOTSYS/LICENSE. #
# For the list of contributors see $ROOTSYS/README/CREDITS. #
################################################################################


class RooPlot(object):
def addObject(self, *args, **kwargs):
from ROOT._pythonization._memory_utils import declare_cpp_owned_arg

# Python should transfer the ownership to the RooPlot
declare_cpp_owned_arg(0, "obj", args, kwargs)

return self._addObject(*args, **kwargs)
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,26 @@ def _iter_pyz(self):
yield o


def _TCollection_Add(self, *args, **kwargs):
from ROOT._pythonization._memory_utils import declare_cpp_owned_arg

def condition(_):
return self.IsOwner()

declare_cpp_owned_arg(0, "obj", args, kwargs, condition=condition)

self._Add(*args, **kwargs)


@pythonization('TCollection')
def pythonize_tcollection(klass):
# Parameters:
# klass: class to be pythonized

# Pythonize Add()
klass._Add = klass.Add
klass.Add = _TCollection_Add

# Add Python lists methods
klass.append = klass.Add
klass.remove = _remove_pyz
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@

import cppyy

from . import pythonization


def set_size(self, buf):
# Parameters:
# - self: graph object
Expand Down Expand Up @@ -213,3 +216,26 @@ def set_size(self, buf):
# Add the composite to the list of pythonizors
cppyy.py.add_pythonization(comp)

def _TMultiGraph_Add(self, *args, **kwargs):
"""
The TMultiGraph takes ownership of the added graphs, unless we're using the
Add(TMultiGraph*) overload, in which cases it adopts the graphs inside the
other TMultiGraph.
Comment on lines +221 to +223
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From this phrasing it seems that also for the Add(TMultiGraph*) the TMultiGraph takes ownership, isn't it so? I don't understand if you mean that in that case the TMultiGraph is not owning the graphs.

"""
from ROOT._pythonization._memory_utils import declare_cpp_owned_arg

def condition(gr):
import ROOT

return gr and not isinstance(gr, ROOT.TMultiGraph)

declare_cpp_owned_arg(0, "graph", args, kwargs, condition=condition)

self._Add(*args, **kwargs)


@pythonization("TMultiGraph")
def pythonize_tmultigraph(klass):
# Pythonizations for TMultiGraph::Add
klass._Add = klass.Add
klass.Add = _TMultiGraph_Add
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,10 @@ def _reverse_pyz(self):
return

t = tuple(self)
was_owner = self.IsOwner()
self.SetOwner(False)
self.Clear()
self.SetOwner(was_owner)
for elem in t:
self.AddAt(elem, 0)

Expand All @@ -222,7 +225,10 @@ def _sort_pyz(self, *args, **kwargs):
# Sort in a Python list copy
pylist = list(self)
pylist.sort(*args, **kwargs)
was_owner = self.IsOwner()
self.SetOwner(False)
self.Clear()
self.SetOwner(was_owner)
self.extend(pylist)


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


def _TSeqCollection_AddAt(self, *args, **kwargs):
from ROOT._pythonization._memory_utils import declare_cpp_owned_arg

def condition(_):
return self.IsOwner()

declare_cpp_owned_arg(0, "obj", args, kwargs, condition=condition)

self._AddAt(*args, **kwargs)


@pythonization('TSeqCollection')
def pythonize_tseqcollection(klass):
from ROOT._pythonization._tcollection import _TCollection_Add

# Parameters:
# klass: class to be pythonized

# Pythonize Add() methods
klass._Add = klass.Add
klass.Add = _TCollection_Add
klass._AddAt = klass.AddAt
klass.AddAt = _TSeqCollection_AddAt

# Item access methods
klass.__getitem__ = _getitem_pyz
klass.__setitem__ = _setitem_pyz
Expand All @@ -257,3 +282,17 @@ def pythonize_tseqcollection(klass):
klass.reverse = _reverse_pyz
klass.sort = _sort_pyz
klass.index = _index_pyz


@pythonization("TList")
def pythonize_tlist(klass):
from ROOT._pythonization._tcollection import _TCollection_Add

# Parameters:
# klass: class to be pythonized

# Pythonize Add() methods
klass._Add = klass.Add
klass.Add = _TCollection_Add
klass._AddAt = klass.AddAt
klass.AddAt = _TSeqCollection_AddAt
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ class TCollectionListMethods(unittest.TestCase):
# Helpers
def create_tcollection(self):
c = ROOT.TList()
c.SetOwner(True)
for _ in range(self.num_elems):
o = ROOT.TObject()
# Prevent immediate deletion of C++ TObjects
ROOT.SetOwnership(o, False)
c.Add(o)

return c
Expand Down Expand Up @@ -76,6 +75,7 @@ def test_extend(self):
len1 = c1.GetEntries()
len2 = c2.GetEntries()

c2.SetOwner(False)
c1.extend(c2)

len1_final = c1.GetEntries()
Expand Down Expand Up @@ -105,6 +105,8 @@ def test_count(self):
self.assertEqual(c.count(o1), 2)
self.assertEqual(c.count(o2), 1)

c.Clear()


if __name__ == '__main__':
unittest.main()
Expand Down
Loading
Loading