Skip to content

Commit a072133

Browse files
author
Vincent Palancher
committed
qipython: Fixes an object lifetime issue causing a memory leak. #46735
When an `AnyObject` was exposed to Python as a `py::object`, we previously created wrappers for its properties and signals, that held the `AnyObject`. This caused a dependency cycle: the object maintained the properties/signals alive and the properties/signals maintained the object alive. This led to a memory leak. This patch changes the properties/signals wrapper types, so that they hold a weak reference to the object instead. The resulting dependency graph is that the `AnyObject` Python object holds its properties/signals Python objects and not the reverse, thus breaking the cycle. However, this change in turn caused a regression in `ApplicationSession`. The `ApplicationSession::session()` member function was exposed as a Python read-only property of the Python `Application` type. The property getter C++ implementation transformed the `Session` object into a temporary `AnyObject`, which was then converted into a Python object. Every access to the property therefore returned a new temporary Python object. These objects previously leaked (because of the behavior described above) and were therefore kept accessible. Because of the leak fix, they are not held anymore and are immediately destroyed unless stored into a variable. Consequently this patch also reworks the `ApplicationSession` type exposed in Python so that it now holds the Python object that holds the `AnyObject` which itself holds the `Session`. This ensures that this `Session` Python object is persistent as long as the `Application` instance is alive. Change-Id: Id1d39be6ced9208a23ae53707d1ef4d466029c68 Reviewed-on: http://gerrit2.aldebaran.lan/1784 Reviewed-by: jmonnon <jmonnon@aldebaran.com> Reviewed-by: philippe.martin <philippe.martin@softbankrobotics.com> Tested-by: vincent.palancher <vincent.palancher@softbankrobotics.com>
1 parent 84f7892 commit a072133

5 files changed

Lines changed: 103 additions & 38 deletions

File tree

qipython/pyproperty.hpp

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,8 @@ namespace detail
2424
{
2525
struct ProxyProperty
2626
{
27-
AnyObject object;
27+
AnyWeakObject object;
2828
unsigned int propertyId;
29-
30-
~ProxyProperty();
3129
};
3230
}
3331

qipython/pysignal.hpp

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,41 @@ using SignalPtr = std::shared_ptr<Signal>;
2525
namespace detail
2626
{
2727

28+
/// The destructor of `AnyObject` can block waiting for callbacks to end.
29+
/// This type ensures that it's always called while the GIL is unlocked.
30+
struct AnyObjectGILSafe
31+
{
32+
AnyObjectGILSafe() = default;
33+
34+
/// @throws `std::runtime_error` if the weak object is expired.
35+
/// @post With s = AnyObjectGILSafe(weak), s->isValid()
36+
explicit AnyObjectGILSafe(AnyWeakObject weak);
37+
38+
// Copy/Move operations are safe to default because they will never cause the
39+
// destruction of the `AnyObject` contained in either the source or the
40+
// destination (this) object.
41+
// The declaration are still kept to enforce the 3/5/0 rule.
42+
AnyObjectGILSafe(const AnyObjectGILSafe& o) = default;
43+
AnyObjectGILSafe& operator=(const AnyObjectGILSafe& o) = default;
44+
AnyObjectGILSafe(AnyObjectGILSafe&& o) = default;
45+
AnyObjectGILSafe& operator=(AnyObjectGILSafe&& o) = default;
46+
47+
~AnyObjectGILSafe();
48+
49+
inline AnyObject& operator*() { return _obj; }
50+
inline const AnyObject& operator*() const { return _obj; }
51+
52+
inline AnyObject* operator->() { return &_obj; }
53+
inline const AnyObject* operator->() const { return &_obj; }
54+
55+
private:
56+
AnyObject _obj;
57+
};
58+
2859
struct ProxySignal
2960
{
30-
AnyObject object;
61+
AnyWeakObject object;
3162
unsigned int signalId;
32-
33-
~ProxySignal();
3463
};
3564

3665
pybind11::object signalConnect(SignalBase& sig,
@@ -41,12 +70,12 @@ pybind11::object signalDisconnect(SignalBase& sig, SignalLink id, bool async);
4170

4271
pybind11::object signalDisconnectAll(SignalBase& sig, bool async);
4372

44-
pybind11::object proxySignalConnect(const AnyObject& obj,
73+
pybind11::object proxySignalConnect(const AnyObjectGILSafe& obj,
4574
unsigned int signalId,
4675
const pybind11::function& callback,
4776
bool async);
4877

49-
pybind11::object proxySignalDisconnect(const AnyObject& obj,
78+
pybind11::object proxySignalDisconnect(const AnyObjectGILSafe& obj,
5079
SignalLink id,
5180
bool async);
5281
} // namespace detail

src/pyapplication.cpp

Lines changed: 41 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,45 @@ WithArgcArgv<ka::Decay<F>, ExtraArgs...> withArgcArgv(F&& f)
7474
return { std::forward<F>(f) };
7575
}
7676

77+
// Wrapper for the `qi::ApplicationSession` class.
78+
//
79+
// Stores the `::py::object` that represents the associated `Session` object.
80+
//
81+
// This `::py::object` holds an `AnyObject` that wraps the `Session` object.
82+
// Maintaining the `AnyObject` is necessary so that any properties or signals
83+
// wrappers that the `::py::object` also holds for this qi Object don't hold
84+
// an expired pointer to the `AnyObject`.
85+
class ApplicationSession : private qi::ApplicationSession
86+
{
87+
public:
88+
using Base = qi::ApplicationSession;
89+
using Config = Base::Config;
90+
91+
template<typename... Args>
92+
explicit ApplicationSession(Args&&... args)
93+
: Base(std::forward<Args>(args)...)
94+
, _session(py::makeSession(Base::session()))
95+
{
96+
}
97+
98+
~ApplicationSession()
99+
{
100+
GILAcquire lock;
101+
_session.release().dec_ref();
102+
}
103+
104+
using Base::stop;
105+
using Base::atRun;
106+
107+
void run() { return Base::run(); }
108+
void startSession() { return Base::startSession(); }
109+
std::string url() const { return Base::url().str(); }
110+
::py::object session() const { return _session; }
111+
112+
private:
113+
::py::object _session;
114+
};
115+
77116
} // namespace
78117

79118
void exportApplication(::py::module& m)
@@ -128,18 +167,12 @@ void exportApplication(::py::module& m)
128167
doc(
129168
"Add a callback that will be executed when run() is called."))
130169

131-
.def_property_readonly("url",
132-
[](const ApplicationSession& app) {
133-
return app.url().str();
134-
},
170+
.def_property_readonly("url", &ApplicationSession::url,
135171
call_guard<GILRelease>(),
136172
doc("The url given to the Application. It's the url "
137173
"used to connect the session."))
138174

139-
.def_property_readonly("session",
140-
[](const ApplicationSession& app) {
141-
return makeSession(app.session());
142-
},
175+
.def_property_readonly("session", &ApplicationSession::session,
143176
doc("The session associated to the application."));
144177
}
145178

src/pyproperty.cpp

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,12 @@ ::py::object proxyPropertyConnect(detail::ProxyProperty& prop,
3838
bool async)
3939
{
4040
GILAcquire lock;
41-
return detail::proxySignalConnect(prop.object, prop.propertyId, callback, async);
41+
detail::AnyObjectGILSafe object(prop.object);
42+
return detail::proxySignalConnect(object, prop.propertyId, callback, async);
4243
}
4344

4445
} // namespace
4546

46-
detail::ProxyProperty::~ProxyProperty()
47-
{
48-
// The destructor can lock when waiting for callbacks to end.
49-
GILRelease unlock;
50-
object.reset();
51-
}
52-
5347
bool isProperty(const pybind11::object& obj)
5448
{
5549
GILAcquire lock;
@@ -127,7 +121,8 @@ void exportProperty(::py::module& m)
127121
class_<detail::ProxyProperty>(m, "_ProxyProperty")
128122
.def("value",
129123
[](const detail::ProxyProperty& prop, bool async) {
130-
const auto fut = prop.object.property(prop.propertyId).async();
124+
detail::AnyObjectGILSafe object(prop.object);
125+
const auto fut = object->property(prop.propertyId).async();
131126
GILAcquire lock;
132127
return resultObject(fut, async);
133128
},
@@ -136,8 +131,9 @@ void exportProperty(::py::module& m)
136131
[](detail::ProxyProperty& prop, object pyValue, bool async) {
137132
AnyValue value(unwrapAsRef(pyValue));
138133
GILRelease unlock;
134+
detail::AnyObjectGILSafe object(prop.object);
139135
const auto fut =
140-
toFuture(prop.object.setProperty(prop.propertyId, std::move(value))
136+
toFuture(object->setProperty(prop.propertyId, std::move(value))
141137
.async());
142138
GILAcquire lock;
143139
return resultObject(fut, async);
@@ -149,7 +145,7 @@ void exportProperty(::py::module& m)
149145
arg(asyncArgName) = false)
150146
.def("disconnect",
151147
[](detail::ProxyProperty& prop, SignalLink id, bool async) {
152-
return detail::proxySignalDisconnect(prop.object, id, async);
148+
return detail::proxySignalDisconnect(detail::AnyObjectGILSafe(prop.object), id, async);
153149
},
154150
"id"_a, arg(asyncArgName) = false);
155151
}

src/pysignal.cpp

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,21 +65,28 @@ ::py::object proxySignalConnect(detail::ProxySignal& sig,
6565
bool async)
6666
{
6767
GILAcquire lock;
68-
return detail::proxySignalConnect(sig.object, sig.signalId, callback, async);
68+
return detail::proxySignalConnect(detail::AnyObjectGILSafe(sig.object), sig.signalId, callback, async);
6969
}
7070

7171
} // namespace
7272

7373
namespace detail
7474
{
7575

76-
detail::ProxySignal::~ProxySignal()
76+
static constexpr const auto objectExpiredMsg = "object has expired";
77+
78+
AnyObjectGILSafe::AnyObjectGILSafe(AnyWeakObject weakObj)
7779
{
78-
// The destructor can lock waiting for callbacks to end.
79-
GILRelease unlock;
80-
object.reset();
80+
_obj = weakObj.lock();
81+
if (!_obj.isValid())
82+
throw std::runtime_error(objectExpiredMsg);
8183
}
8284

85+
AnyObjectGILSafe::~AnyObjectGILSafe()
86+
{
87+
GILRelease unlock;
88+
_obj = {};
89+
}
8390

8491
::py::object signalConnect(SignalBase& sig,
8592
const ::py::function& callback,
@@ -119,7 +126,7 @@ ::py::object signalDisconnectAll(SignalBase& sig, bool async)
119126
return resultObject(fut, async);
120127
}
121128

122-
::py::object proxySignalConnect(const AnyObject& obj,
129+
::py::object proxySignalConnect(const AnyObjectGILSafe& obj,
123130
unsigned int signalId,
124131
const ::py::function& callback,
125132
bool async)
@@ -128,18 +135,18 @@ ::py::object proxySignalConnect(const AnyObject& obj,
128135
return connect(
129136
[&](const SignalSubscriber& sub) {
130137
GILRelease unlock;
131-
return obj.connect(signalId, sub).async();
138+
return obj->connect(signalId, sub).async();
132139
},
133140
callback, async);
134141
}
135142

136-
::py::object proxySignalDisconnect(const AnyObject& obj,
143+
::py::object proxySignalDisconnect(const AnyObjectGILSafe& obj,
137144
SignalLink id,
138145
bool async)
139146
{
140147
const auto fut = [&] {
141148
GILRelease unlock;
142-
return toFuture(obj.disconnect(id));
149+
return toFuture(obj->disconnect(id));
143150
}();
144151
GILAcquire lock;
145152
return resultObject(fut, async);
@@ -212,15 +219,17 @@ void exportSignal(::py::module& m)
212219
arg(asyncArgName) = false)
213220
.def("disconnect",
214221
[](detail::ProxySignal& sig, SignalLink id, bool async) {
215-
return detail::proxySignalDisconnect(sig.object, id, async);
222+
auto object = detail::AnyObjectGILSafe(sig.object);
223+
return detail::proxySignalDisconnect(object, id, async);
216224
},
217225
"id"_a, arg(asyncArgName) = false)
218226
.def("__call__",
219227
[](detail::ProxySignal& sig, args pyArgs) {
220228
const auto args =
221229
AnyReference::from(pyArgs).content().asTupleValuePtr();
222230
GILRelease unlock;
223-
sig.object.metaPost(sig.signalId, args);
231+
auto object = detail::AnyObjectGILSafe(sig.object);
232+
object->metaPost(sig.signalId, args);
224233
});
225234
}
226235

0 commit comments

Comments
 (0)