Skip to content

Commit 7e47c42

Browse files
committed
[ROOT-10469] Introduce two teardown modes for PyROOT: soft and hard
Motivation: we need to make sure that, if PyROOT is used from another process that will keep on living after the Python interpreter dies, PyROOT does not shut down the ROOT interpreter via TROOT::EndOfProcessCleanups when running its atexit handler. In that sense, this commit adds a configuration option to tell PyROOT if the teardown needs to be soft, i.e. we do not want to shut down the ROOT interpreter. Instead, in the soft mode, we we only want (and need) to clean the objects that are controlled by PyROOT via its TMemoryRegulator.
1 parent fba31b1 commit 7e47c42

File tree

6 files changed

+66
-13
lines changed

6 files changed

+66
-13
lines changed

bindings/pyroot/ROOT.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -168,14 +168,15 @@ def ismethod( object ):
168168
### configuration ---------------------------------------------------------------
169169
class _Configuration( object ):
170170
__slots__ = [ 'IgnoreCommandLineOptions', 'StartGuiThread', 'ExposeCppMacros',
171-
'_gts', 'DisableRootLogon' ]
171+
'_gts', 'DisableRootLogon', 'ShutDown' ]
172172

173173
def __init__( self ):
174174
self.IgnoreCommandLineOptions = 0
175175
self.StartGuiThread = True
176176
self.ExposeCppMacros = False
177177
self._gts = []
178178
self.DisableRootLogon = False
179+
self.ShutDown = True
179180

180181
def __setGTS( self, value ):
181182
for c in value:
@@ -866,21 +867,29 @@ def cleanup():
866867
del v, k, items, types
867868

868869
# destroy facade
870+
shutdown = PyConfig.ShutDown
869871
facade.__dict__.clear()
870872
del facade
871873

872874
if 'libPyROOT' in sys.modules:
873-
# run part the gROOT shutdown sequence ... running it here ensures that
874-
# it is done before any ROOT libraries are off-loaded, with unspecified
875-
# order of static object destruction;
876-
gROOT = sys.modules[ 'libPyROOT' ].gROOT
877-
gROOT.EndOfProcessCleanups()
878-
del gROOT
875+
pyroot_backend = sys.modules['libPyROOT']
876+
if shutdown:
877+
# run part the gROOT shutdown sequence ... running it here ensures that
878+
# it is done before any ROOT libraries are off-loaded, with unspecified
879+
# order of static object destruction;
880+
pyroot_backend.ClearProxiedObjects()
881+
pyroot_backend.gROOT.EndOfProcessCleanups()
882+
else:
883+
# Soft teardown
884+
# Make sure all the objects regulated by PyROOT are deleted and their
885+
# Python proxies are properly nonified.
886+
pyroot_backend.ClearProxiedObjects()
879887

880888
# cleanup cached python strings
881-
sys.modules[ 'libPyROOT' ]._DestroyPyStrings()
889+
pyroot_backend._DestroyPyStrings()
882890

883891
# destroy ROOT extension module
892+
del pyroot_backend
884893
del sys.modules[ 'libPyROOT' ]
885894

886895
# destroy ROOT module

bindings/pyroot/src/RootModule.cxx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -798,6 +798,8 @@ static PyMethodDef gPyROOTMethods[] = {
798798
METH_NOARGS, (char*) "PyROOT internal function" },
799799
{ (char*) "_ResetRootModule", (PyCFunction)RootModuleResetCallback,
800800
METH_NOARGS, (char*) "PyROOT internal function" },
801+
{ (char*) "ClearProxiedObjects", (PyCFunction)ClearProxiedObjects,
802+
METH_NOARGS, (char*) "PyROOT internal function" },
801803
{ (char*) "AddressOf", (PyCFunction)AddressOf,
802804
METH_VARARGS, (char*) "Retrieve address of held object in a buffer" },
803805
{ (char*) "addressof", (PyCFunction)addressof,

bindings/pyroot/src/RootWrapper.cxx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,15 +175,19 @@ namespace {
175175
} // unnamed namespace
176176

177177

178+
static TMemoryRegulator &GetMemoryRegulator() {
179+
static TMemoryRegulator m;
180+
return m;
181+
}
182+
178183
//- public functions ---------------------------------------------------------
179184
void PyROOT::InitRoot()
180185
{
181186
// setup interpreter locks to allow for threading in ROOT
182187
PyEval_InitThreads();
183188

184189
// memory management
185-
static TMemoryRegulator m;
186-
gROOT->GetListOfCleanups()->Add( &m );
190+
gROOT->GetListOfCleanups()->Add( &GetMemoryRegulator() );
187191

188192
// bind ROOT globals that are needed in ROOT.py
189193
AddToGlobalScope( "gROOT", "TROOT.h", gROOT, Cppyy::GetScope( gROOT->IsA()->GetName() ) );
@@ -810,6 +814,15 @@ PyObject* PyROOT::GetCppGlobal( const std::string& name )
810814
return 0;
811815
}
812816

817+
////////////////////////////////////////////////////////////////////////////////
818+
/// Delete all memory-regulated objects
819+
820+
PyObject *PyROOT::ClearProxiedObjects()
821+
{
822+
GetMemoryRegulator().ClearProxiedObjects();
823+
Py_RETURN_NONE;
824+
}
825+
813826
////////////////////////////////////////////////////////////////////////////////
814827
/// only known or knowable objects will be bound (null object is ok)
815828

bindings/pyroot/src/RootWrapper.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ namespace PyROOT {
2727
PyObject* GetCppGlobal( const std::string& name );
2828
PyObject* GetCppGlobal( PyObject*, PyObject* args );
2929

30+
// clean up all objects controlled by TMemoryRegulator
31+
PyObject *ClearProxiedObjects();
32+
3033
// bind a ROOT object into a Python object
3134
PyObject* BindCppObjectNoCast( Cppyy::TCppObject_t object, Cppyy::TCppType_t klass,
3235
Bool_t isRef = kFALSE, Bool_t isValue = kFALSE );

bindings/pyroot/src/TMemoryRegulator.cxx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,29 @@ void PyROOT::TMemoryRegulator::RecursiveRemove( TObject* object )
191191
}
192192
}
193193

194+
////////////////////////////////////////////////////////////////////////////////
195+
/// clean up all tracked objects
196+
197+
void PyROOT::TMemoryRegulator::ClearProxiedObjects()
198+
{
199+
while (!fgObjectTable->empty()) {
200+
auto elem = fgObjectTable->begin();
201+
auto cppobj = elem->first;
202+
auto pyobj = (ObjectProxy*)PyWeakref_GetObject(elem->second);
203+
204+
if (pyobj && (pyobj->fFlags & ObjectProxy::kIsOwner)) {
205+
// Only delete the C++ object if the Python proxy owns it.
206+
// The deletion will trigger RecursiveRemove on the object
207+
delete cppobj;
208+
}
209+
else {
210+
// Non-owning proxy, just unregister to clean tables.
211+
// The proxy deletion by Python will have no effect on C++, so all good
212+
UnregisterObject(cppobj);
213+
}
214+
}
215+
}
216+
194217
////////////////////////////////////////////////////////////////////////////////
195218
/// start tracking <object> proxied by <pyobj>
196219

bindings/pyroot/src/TMemoryRegulator.h

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
#include "TObject.h"
99

1010
// Standard
11-
#include <map>
11+
#include <unordered_map>
1212

1313

1414
namespace PyROOT {
@@ -29,6 +29,9 @@ namespace PyROOT {
2929
// callback for ROOT/CINT
3030
virtual void RecursiveRemove( TObject* object );
3131

32+
// cleanup of all tracked objects
33+
void ClearProxiedObjects();
34+
3235
// add a python object to the table of managed objects
3336
static Bool_t RegisterObject( ObjectProxy* pyobj, TObject* object );
3437

@@ -42,8 +45,8 @@ namespace PyROOT {
4245
static PyObject* ObjectEraseCallback( PyObject*, PyObject* pyref );
4346

4447
private:
45-
typedef std::map< TObject*, PyObject* > ObjectMap_t;
46-
typedef std::map< PyObject*, ObjectMap_t::iterator > WeakRefMap_t;
48+
typedef std::unordered_map< TObject*, PyObject* > ObjectMap_t;
49+
typedef std::unordered_map< PyObject*, ObjectMap_t::iterator > WeakRefMap_t;
4750

4851
static ObjectMap_t* fgObjectTable;
4952
static WeakRefMap_t* fgWeakRefTable;

0 commit comments

Comments
 (0)