PyBind11 destructor not invoked?

Question:

I have a c++ class wrapped with PyBind11. The issue is: when the Python script ends the c++ destructor is not being automatically invoked. This causes an untidy exit because networking resources need to be released by the destructor.

As a work-around it is necessary to explicitly delete the Python object, but I don’t understand why!

Please could someone explain what is wrong here and how to get the destructor called automatically when the Python object is garbage collected?

Pybind11 binding code:

py::class_<pcs::Listener>(m, "listener")
    .def(py::init<const py::object &, const std::string &, const std::string &, const std::string &, const std::string &, const std::set<std::string> &, const std::string & , const bool & , const bool & >(), R"pbdoc(
    Monitors network traffic.

    When a desired data source is detected a client instance is connected to consume the data stream.

    Reconstructs data on receipt, like a jigsaw.  Makes requests to fill any gaps.  Verifies the data as sequential.

    Data is output by callback to Python.  Using the method specified in the constructor, which must accept a string argument.
)pbdoc");

In Python:

#Function to callback
def print_string(str):
    print("Python; " + str)

lstnr = listener(print_string, 'tcp://127.0.0.1:9001', clientCertPath, serverCertPath, proxyCertPath, desiredSources, 'time_series_data', enableCurve, enableVerbose)

#Run for a minute
cnt = 0
while cnt < 60:
    cnt += 1
    time.sleep(1)

#Need to call the destructor explicity for some reason    
del lstnr
Asked By: GoFaster

||

Answers:

As was mentioned in a comment, the proximate cause of this behavior is the Python garbage collector: When the reference counter for an object gets to zero, the garbage collector may destroy the object (and thereby invoke the c++ destructor) but it doesn’t have to do it at that moment.

This idea is elaborated more fully in the answer here:

https://stackoverflow.com/a/38238013/790979

As also mentioned in the above link, if you’ve got clean up to do at the end of an object’s lifetime in Python, a nice solution is context management, where you’d define __enter__ and __exit__ in the object’s wrapper (either in pybind11 or in Python itself), have __exit__ release the networking resources, and then, in the Python client code, something like:


with listener(print_string, 'tcp://127.0.0.1:9001', clientCertPath, serverCertPath, proxyCertPath, desiredSources, 'time_series_data', enableCurve, enableVerbose) as lstnr:
    # Run for a minute
    cnt = 0
    while cnt < 60:
        cnt += 1
        time.sleep(1)
Answered By: charleslparker

So some years later I fixed this issue by enabling Python context manager with support by adding __enter__ and __exit__ method handling to my PyBind11 code:

py::class_<pcs::Listener>(m, "listener")
.def(py::init<const py::object &, const std::string &, const std::string &, const std::string &, const std::string &, const std::set<std::string> &, const std::string & , const bool & , const bool & >(), R"pbdoc(
    Monitors network traffic.

    When a desired data source is detected a client instance is connected to consume the data stream.
    
    Specify 'type' as 'string' or 'market_data' to facilitate appropriate handling of BarData or string messages.

    Reconstructs data on receipt, like a jigsaw.  Makes requests to fill any gaps.  Verifies the data as sequential.

    Data is output by callback to Python.  Using the method specified in the constructor, which must accept a string argument.
)pbdoc")
.def("__enter__", &pcs::Listener::enter, R"pbdoc(
    Python 'with' context manager support.
)pbdoc")    
.def("__exit__", &pcs::Listener::exit, R"pbdoc(
    Python 'with' context manager support.
)pbdoc");

Added corresponding functions to the C++ class, like so:

//For Python 'with' context manager
auto enter(){std::cout << "Context Manager: Enter" << std::endl; return py::cast(this); }//returns a pointer to this object for 'with'....'as' python functionality
auto exit(py::handle type, py::handle value, py::handle traceback){ std::cout << "Context Manager: Exit: " << type << " " << value << " " << traceback <<  std::endl; }

N.B.

  1. The returned pointer value from enter() is important to the as functionality in a with….as statement.

  2. The parameters passed to exit(py::handle type, py::handle value, py::handle traceback) are useful debugging info.

Python usage:

with listener(cb, endpoint, clientCertPath, serverCertPath, proxyCertPath, desiredSources, type, enableCurve, enableVerbose):
cnt = 0
while cnt < 10:
    cnt += 1
    time.sleep(1)

The Python context manager now calls the destructor on the C++ object thus smoothly releasing the networking resources.

Answered By: GoFaster

GoFaster’s solution above is helpful and the correct approach but I just wanted to clarify and correct their assertion that

The Python context manager now calls the destructor on the C++ object thus smoothly releasing the networking resources

This is simply not true. The context manager only guarantees that __exit__ will be called, not that any destructor will be called. Let me demonstrate – here’s a managed resource implemented in C++:

class ManagedResource
{
public:
    ManagedResource(int i) : pi(std::make_unique<int>(i))
    {
        py::print("ManagedResource ctor");
    }

    ~ManagedResource()
    {
        py::print("ManagedResource dtor");
    }

    int get() const { return *pi; }

    py::object enter()
    {
        py::print("entered context manager");
        return py::cast(this);
    }

    void exit(py::handle type, py::handle value, py::handle traceback)
    {
        // release resources
        // pi.reset();
        py::print("exited context manager");
    }

private:
    std::unique_ptr<int> pi;
};

the python bindings:

    py::class_<ManagedResource>(m, "ManagedResource")
    .def(py::init<int>())
    .def("get", &ManagedResource::get)
    .def("__enter__", &ManagedResource::enter, R"""(
        Enter context manager.
    )""")
    .def("__exit__", &ManagedResource::exit, R"""(
        Leave context manager.
    )""");

and some python test code (note that the code above doesn’t (yet) release the resource in __exit__):

def f():
    with ManagedResource(42) as resource1:
        print(f"get = {resource1.get()}")
    print(f"hey look I'm still here {resource1.get()}") # not destroyed


if __name__ == "__main__":
    f()
    print("end")

which produces:

ManagedResource ctor
entered context manager
get = 42
exited context manager
hey look I'm still here 42
ManagedResource dtor
end

so the resource is constructed, acquiring memory, and accessed within the context manager. All good so far. However the memory is still accessible outside the context manager (and before the destuctor is called, which is decided by the python runtime and outside our control unless we force it with del, which completely defeats the point of the context manager.

But we didnt actually release the resource in __exit__. If you uncomment pi.reset() in that function, you’ll get this:

ManagedResource ctor
entered context manager
get = 42
exited context manager
Segmentation fault (core dumped)

this time, when you call get() outside the context manager, the ManagedResource object itself is still very much not destructed, but the resource inside it has been released,

And there’s even more danger: if you create a ManagedResource outside a with block, you’ll leak resource as __exit__ will never get called. To fix this, you’ll need to defer acquiring the resources from the constructor to the __enter__ method, as well as putting checks that the resource exists in get.

In short, the morals of this story are:

  • you can’t rely on when/where python objects are destructed, even for context managers
  • you can control acquisition and release of resources within a context manager
  • resources should be acquired in the __enter__ method, not in the constructor
  • resources should be released in the __exit__ method, not is the destructor
  • you should put sufficient guards around access to the resources

A context-managed object is not an RAII resource itself, but a wrapper around a RAII resource.

Answered By: virgesmith
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.