Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[RF] New RooFit::Owner pointer wrapper to flag owning return values #9392

Closed
wants to merge 3 commits into from

Conversation

guitargeek
Copy link
Contributor

In RooFit, there are many functions that return pointers that are owned
by the caller. We can't change this interface anymore, but we can wrap
the return values transparently in a raw pointer wrapper that is called
a RooFit::Owner.

On the C++ side, this helps to analyze your code and detect potential
memory leaks. On the Python side, we can tell cppyy to take ownership
of the object if the pointer is wrapped in a owning pointer such as the
RooFit::Owner. This is more flexible and convenient than the existing
cppyy way of flagging the CPPOverloads on the Python side with the
__creates__ = True attribute for at least two reasons:

  1. This flag can't be applied at the granularity of indivirual C++
    overloads
  2. It's only on the Python side, so if you want to flag these functions
    in C++ as well as in Python you have to do some bookkeeping

A unit test was implemented to check that the RooFit::Owner behaves in
Python as expected, and that there is no memory leaking when using
functions that return them.

As a first example, the RooFit::Owner is used in the highly used
function RooAbsPdf::generate, so we also get quite some test coverage
from the tutorials.

In the future after this initial effort, the remaining RooFit functions
should be migrated to fix many memory leaks in PyROOT user code.

Copy link
Member

@hageboeck hageboeck left a comment

Choose a reason for hiding this comment

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

Good idea for python.

Does this work?
if (generate(...) == nullptr)

@@ -230,6 +230,7 @@ ROOT_STANDARD_LIBRARY_PACKAGE(RooFitCore
RooNaNPacker.h
RooBinSamplingPdf.h
RooBinWidthFunction.h
RooFit/Owner.h
Copy link
Member

Choose a reason for hiding this comment

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

Is this a conscious decision to put this in RooFit? You're adding one more directory that has to be managed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that was a deliberate choice. We have too many headers now with terribly generic names like Floats.h, UniqueId.h, and not possible Owner.h.

Many distributors install the ROOT headers right into /usr/include, so it's bad to have these header file names I'd say. Fortunately it's not too late to do something about it, so I wanted to establish a convention to avoid name collisions:

  • Installed RooFit headers must start with Roo* or must be located in a subdirectory starting with Roo* (e.g. RooFit or RooStats).
  • Similarly, if the class name doesn't start with Roo, it has to go in a Roo* namespace (usually RooFit)
  • Free functions always need to go in this namespace
  • For implmentation details that we can't avoid installing, we can use a Roo*::Detail namespace like we have with ROOT::Detail (same with Experimental)
    I'd still have time to move Floats.h and and UniqueId.h into RooFit.

What are your thoughts on this?

@guitargeek
Copy link
Contributor Author

guitargeek commented Dec 7, 2021

Good idea for python.

Does this work? if (generate(...) == nullptr)

Yes that works! You can also do these checks here:

RooFit::Owner<int*>(nullptr) == nullptr
RooFit::Owner<int*>(new int) == nullptr

Also maybe a question also to @etejedor and @Axel-Naumann: you think this is general enough to have the Owner in the ROOT namespace rather than RooFit?

// owner pointer.
if (cppInstance->IsSmart()) {
if(Cppyy::IsOwnerPtr(cppInstance->GetSmartIsA())) {
// Here we create a new Python object with the same flags that
Copy link
Contributor

Choose a reason for hiding this comment

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

I was wondering why you need to create this Python object yourself and configure that Python owns the C++ object.
With your new mechanism, if instead of returning a RooDataSet* (whose Python proxy would not own) now you return a RooFit::Owner<RooDataSet*>, doesn't cppyy already return a Python proxy that owns what it has in its belly and therefore would delete the C++ object when no more references are left for the Python proxy?

Copy link
Contributor Author

@guitargeek guitargeek Dec 13, 2021

Choose a reason for hiding this comment

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

Hi @etejedor, thanks for your comment! I don't quite understand your question, I'm not so fluent with the cppyy terminology...

If I understand correctly we have separate objects:

  1. The Python Proxy of type PyObject *
  2. The C++ proxy that is owned by the Python proxy which points to the actual object, can be of type raw pointer RooDataSet * or any smart pointer known to cppyy (e.g. unique_ptr<RooDataSet> or now RooFit::Owner<RooDataSet>)
  3. The actual C++ object owned by the C++ proxy, or by Python if kIsOwner is true

What I want to achieve here is to

  1. Tell the PyObject that it owns the C++ object, not only the C++ proxy
  2. Replace the RooFit::Owner<RooDataSet> proxy with a raw pointer proxy RooDataSet * such that there is no confusing owned by RooFit::Owner<RooDataSet> in the message when you print the Python object. But this is not the main point, only cosmetics.

If I understand your question...

doesn't cppyy already return a Python proxy that owns what it has in its belly and therefore would delete the C++ object when no more references are left for the Python proxy?

...correctly, you are wondering why the C++ object doesn't get deleted when I delete the original PyObject, right? Well, only the C++ proxy object (in this case the RooFit::Owner<RooDataSet*>) gets deleted. But the RooFit::Owner doesn't actually own, it only indicates ownership by the caller. That's why the C++ object survives.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes that clarifies my doubt thanks!

@guitargeek guitargeek force-pushed the RooFit_Owner_1 branch 2 times, most recently from 1802287 to c3fb4b7 Compare February 5, 2022 02:01
@@ -144,6 +145,41 @@ static inline PyObject* HandleReturn(
int ll_action = 0;
if (result) {

// If the "smart pointer" is only an observing pointer that indicated
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you add to the commit a patch file that contains the changes done to cppyy? Especifically here: https://github.com/root-project/root/tree/master/bindings/pyroot/cppyy/patches

@@ -128,7 +128,13 @@ static std::set<std::string> g_builtins =
// smart pointer types
static std::set<std::string> gSmartPtrTypes =
{"auto_ptr", "std::auto_ptr", "shared_ptr", "std::shared_ptr",
"unique_ptr", "std::unique_ptr", "weak_ptr", "std::weak_ptr"};
"unique_ptr", "std::unique_ptr", "weak_ptr", "std::weak_ptr",
"RooFit::Owner"};
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you strictly need that RooFit::Owner is treated as a smart ptr too?

@guitargeek guitargeek force-pushed the RooFit_Owner_1 branch 2 times, most recently from 967d4a6 to f3d15a3 Compare May 16, 2022 16:10
@root-project root-project deleted a comment from phsft-bot May 16, 2022
@root-project root-project deleted a comment from phsft-bot May 16, 2022
@root-project root-project deleted a comment from phsft-bot May 16, 2022
@root-project root-project deleted a comment from phsft-bot May 16, 2022
@root-project root-project deleted a comment from phsft-bot May 16, 2022
@root-project root-project deleted a comment from phsft-bot May 16, 2022
@root-project root-project deleted a comment from phsft-bot May 16, 2022
@root-project root-project deleted a comment from phsft-bot May 16, 2022
@root-project root-project deleted a comment from phsft-bot May 16, 2022
@root-project root-project deleted a comment from phsft-bot May 16, 2022
@root-project root-project deleted a comment from phsft-bot May 16, 2022
@root-project root-project deleted a comment from phsft-bot Dec 6, 2023
@root-project root-project deleted a comment from phsft-bot Dec 6, 2023
@root-project root-project deleted a comment from phsft-bot Dec 6, 2023
@root-project root-project deleted a comment from phsft-bot Dec 6, 2023
@root-project root-project deleted a comment from phsft-bot Dec 6, 2023
@root-project root-project deleted a comment from phsft-bot Dec 6, 2023
@root-project root-project deleted a comment from phsft-bot Dec 6, 2023
@root-project root-project deleted a comment from phsft-bot Dec 6, 2023
@root-project root-project deleted a comment from phsft-bot Dec 6, 2023
@root-project root-project deleted a comment from phsft-bot Dec 6, 2023
@root-project root-project deleted a comment from phsft-bot Dec 7, 2023
@root-project root-project deleted a comment from phsft-bot Dec 7, 2023
@root-project root-project deleted a comment from phsft-bot Dec 7, 2023
@root-project root-project deleted a comment from phsft-bot Dec 7, 2023
@root-project root-project deleted a comment from phsft-bot Dec 7, 2023
@root-project root-project deleted a comment from phsft-bot Dec 7, 2023
vepadulano and others added 3 commits December 7, 2023 11:53
In RooFit, there are many functions that return pointers that are owned
by the caller. We can't change this interface anymore, but we can wrap
the return values transparently in a raw pointer wrapper that is called
a `RooFit::Owner`.

On the C++ side, this helps to analyze your code and detect potential
memory leaks. On the Python side, we can tell cppyy to take ownership
of the object if the pointer is wrapped in a owning pointer such as the
`RooFit::Owner`. This is more flexible and convenient than the existing
cppyy way of flagging the CPPOverloads on the Python side with the
`__creates__ = True` attribute for at least two reasons:

 1. This flag can't be applied at the granularity of indivirual C++
    overloads
 2. It's only on the Python side, so if you want to flag these functions
    in C++ as well as in Python you have to do some bookkeeping

A unit test was implemented to check that the `RooFit::Owner` behaves in
Python as expected, and that there is no memory leaking when using
functions that return them.

As a first example, the `RooFit::Owner` is used in the highly used
function `RooAbsPdf::generate`, so we also get quite some test coverage
from the tutorials.

In the future after this initial effort, the remaining RooFit functions
should be migrated to fix many memory leaks in PyROOT user code.
@phsft-bot
Copy link

Starting build on ROOT-performance-centos8-multicore/soversion, ROOT-ubuntu2204/nortcxxmod, ROOT-ubuntu2004/python3, mac12arm/cxx20, windows10/default
How to customize builds

@phsft-bot
Copy link

Build failed on ROOT-ubuntu2204/nortcxxmod.
Running on root-ubuntu-2204-2.cern.ch:/home/sftnight/build/workspace/root-pullrequests-build
See console output.

Failing tests:

@phsft-bot
Copy link

Build failed on windows10/default.
Running on null:C:\build\workspace\root-pullrequests-build
See console output.

Errors:

  • [2023-12-07T11:10:28.319Z] C:\build\workspace\root-pullrequests-build\root\tree\readspeed\src\ReadSpeed.cxx(40,106): error C2440: '<function-style-cast>': cannot convert from 'ROOT::ROwningPtr<TFile>' to 'std::unique_ptr<TFile,std::default_delete<TFile>>' [C:\build\workspace\root-pullrequests-build\build\tree\readspeed\ReadSpeed.vcxproj]
  • [2023-12-07T11:10:28.319Z] C:\build\workspace\root-pullrequests-build\root\tree\readspeed\src\ReadSpeed.cxx(40,15): error C2737: 'f': const object must be initialized [C:\build\workspace\root-pullrequests-build\build\tree\readspeed\ReadSpeed.vcxproj]
  • [2023-12-07T11:10:28.319Z] C:\build\workspace\root-pullrequests-build\root\tree\readspeed\src\ReadSpeed.cxx(41,7): error C3536: 'f': cannot be used before it is initialized [C:\build\workspace\root-pullrequests-build\build\tree\readspeed\ReadSpeed.vcxproj]
  • [2023-12-07T11:10:28.636Z] C:\build\workspace\root-pullrequests-build\root\tree\readspeed\src\ReadSpeed.cxx(41,7): error C2446: '==': no conversion from 'nullptr' to 'int' [C:\build\workspace\root-pullrequests-build\build\tree\readspeed\ReadSpeed.vcxproj]
  • [2023-12-07T11:10:28.637Z] C:\build\workspace\root-pullrequests-build\root\tree\readspeed\src\ReadSpeed.cxx(179,103): error C2440: '<function-style-cast>': cannot convert from 'ROOT::ROwningPtr<TFile>' to 'std::unique_ptr<TFile,std::default_delete<TFile>>' [C:\build\workspace\root-pullrequests-build\build\tree\readspeed\ReadSpeed.vcxproj]
  • [2023-12-07T11:10:28.938Z] C:\build\workspace\root-pullrequests-build\root\tree\readspeed\src\ReadSpeed.cxx(180,10): error C3536: 'f': cannot be used before it is initialized [C:\build\workspace\root-pullrequests-build\build\tree\readspeed\ReadSpeed.vcxproj]
  • [2023-12-07T11:10:28.938Z] C:\build\workspace\root-pullrequests-build\root\tree\readspeed\src\ReadSpeed.cxx(180,10): error C2446: '==': no conversion from 'nullptr' to 'int' [C:\build\workspace\root-pullrequests-build\build\tree\readspeed\ReadSpeed.vcxproj]
  • [2023-12-07T11:10:29.304Z] C:\build\workspace\root-pullrequests-build\root\tree\readspeed\src\ReadSpeed.cxx(185,18): error C2737: 'byteData': const object must be initialized [C:\build\workspace\root-pullrequests-build\build\tree\readspeed\ReadSpeed.vcxproj]
  • [2023-12-07T11:10:29.304Z] C:\build\workspace\root-pullrequests-build\root\tree\readspeed\src\ReadSpeed.cxx(186,40): error C3536: 'byteData': cannot be used before it is initialized [C:\build\workspace\root-pullrequests-build\build\tree\readspeed\ReadSpeed.vcxproj]
  • [2023-12-07T11:10:29.304Z] C:\build\workspace\root-pullrequests-build\root\tree\readspeed\src\ReadSpeed.cxx(207,96): error C2668: 'std::unique_ptr<TFile,std::default_delete<TFile>>::unique_ptr': ambiguous call to overloaded function [C:\build\workspace\root-pullrequests-build\build\tree\readspeed\ReadSpeed.vcxproj]

And 1 more

@phsft-bot
Copy link

Build failed on ROOT-performance-centos8-multicore/soversion.
Running on olbdw-01.cern.ch:/data/sftnight/workspace/root-pullrequests-build
See console output.

Failing tests:

@phsft-bot
Copy link

@phsft-bot
Copy link

Build failed on ROOT-ubuntu2004/python3.
Running on root-ubuntu-2004-3.cern.ch:/home/sftnight/build/workspace/root-pullrequests-build
See console output.

Failing tests:

@guitargeek
Copy link
Contributor Author

Closing this PR, because this is probably not the way to solve the ownership problem.

It would be better to have another approach here, with attributes on the C++ side that cppyy can understand.

Either way, if we would go with a special owning pointer class, the logic suggested in this PR can better be implemented with a custom CPyCppyy executor that is declared via the public CPyCppyy API.

@guitargeek guitargeek closed this Mar 20, 2024
@guitargeek guitargeek deleted the RooFit_Owner_1 branch March 20, 2024 12:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[RF] Memory leak when using any function that returns an owning pointer in PyROOT
5 participants