Output parameters in PyROOT

Hello
I’m trying to call a C++ function from PyROOT. The function in C++ has 2 parameters that are pointers to pointers, e.g.

void foo(Bar** bar, Baz** baz)

which the function initializes with a pointer value.

Bar* bar;
Baz* baz;
foo(&bar, &baz);
// bar and baz can be used here

Is it possible to call this function from python? What is the correct syntax?
Thank you in advance.

Hi @elusian ,

if you have a C++ function foo like that and you need to pass 2 C++ pointers bar and baz like that, the simplest way to do it is probably to do it in C++. In PyROOT you can do e.g.:

ROOT.gInterpreter.ProcessLine("foo(&bar, &baz)")

Similarly you can declare any helper function that might be useful with ROOT.gInterpreter.Declare and the function will be accessible as ROOT.functionname.

Feel free to share a minimal reproducer in case you need something more specific.
Cheers,
Enrico

P.S.
PyROOT is based on cppyy so maybe some of the cppyy docs about interfacing to low-level code can also help: Low-level code — cppyy 2.4.0 documentation

Hello
how do I declare bar and baz so that they are visible from python and possibly register them in the python GC as if they were normal variables?

Could you translate the whole example

void foo(Bar** bar, Baz** baz) {
    *bar = new Bar;
    *baz = new Baz;
}

Bar* bar;
Baz* baz;
foo(&bar, &baz);

bar->do_stuff();
baz->do_other_stuff();

in python?

Also, about cppyy.ll, how could I access that from ROOT? Should I import cppyy on my own or does the ROOT module reexports it?

Cheers,
Enrico

I can provide something, but what the best translation is really depends on the actual context in which this code is used: “who” creates Bar and Baz, what’s their lifetime? Do you care about the “pointer-ness” of bar and baz when using them from Python or do you just need access to the values they point to? Are the Bar and Baz types expensive to copy? Etc. etc.

One way to do the same thing as that C++ snippet in Python is:

import ROOT

# Given this C++ setup...
ROOT.gInterpreter.Declare("""
struct Bar {
    Bar() { std::cout << "Bar constructed\\n"; }
    void bar_method() { std::cout << "some_method called\\n"; }
};
Bar* bar = nullptr;
void foo(Bar **x) { std::cout << "foo called\\n"; *x = new Bar(); }
""")

ROOT.foo(ROOT.bar)
ROOT.bar.bar_method()

A patched version of cppyy comes with ROOT. import cppyy should work if import ROOT works.

This registers bar in the global ROOT module, so I would only be able to call it once.

I think in this case I can however try with

ROOT.gInterpreter.Declare('''
    std::pair<std::unique_ptr<Bar>, std::unique_ptr<Baz>> foo_adapter() {
        Bar* bar;
        Baz* baz;
        
        foo(&bar, &baz);
        
        return {std::unique_ptr<Bar>(bar), std::unique_ptr<Baz>(baz)};
    }
''')

bar_in_py, baz_in_py = ROOT.foo_adapter()

bar_in_py.bar_method()
baz_in_py.baz_method()

This works (thanks!) but is it correct w.r.t. ownership? The outputs of this function (bar_in_py and baz_in_py) should be managed by python (e.g., the pointed objects should be deleted when bar_in_py and baz_in_py are garbage collected).

How would I do instead if foo was a method? Is it possible to attach the adapter function to the same class with a pythonization?

Hi @elusian,
The behaviour is what you would expect, the Python objects connected to the unique_ptrs also manage them, for example:

import ROOT

ROOT.gInterpreter.Declare('''
    struct Bar{
      ~Bar(){
          std::cout << "Destroying C++ Bar object." << std::endl;
      }
    };

    struct Baz{
      ~Baz(){
          std::cout << "Destroying C++ Baz object." << std::endl;
      }
    };

    std::pair<std::unique_ptr<Bar>, std::unique_ptr<Baz>> foo_adapter() {
        return {std::make_unique<Bar>(), std::make_unique<Baz>()};
    }
''')

bar_in_py, baz_in_py = ROOT.foo_adapter()
bar_in_py.__class__.__del__ = lambda self: print(f"Destroying Python {self.__class__.__name__} object")
baz_in_py.__class__.__del__ = lambda self: print(f"Destroying Python {self.__class__.__name__} object")
del bar_in_py
del baz_in_py

print("After del statements")

This prints

Destroying Python Bar object
Destroying Python Baz object
Destroying C++ Baz object.
Destroying C++ Bar object.
After del statements
1 Like