Skip to content

Commit f96693e

Browse files
Vincent Palanchernyibbang
authored andcommitted
Fixes a memory leak in native code and a regression. #46735
This patch fixes a leak caused by errors in the native code and a regression caused by a previous tentative of a fix for another leak. The leak occurs in qi objects bound functions arguments conversion. It was not clear whether `qi::StructTypeInterface` subclasses implementations of its `get` methods should return owned pointers or not (i.e. if the caller of these methods was expected to destroy these pointers). It seems after research that they must not, and our `StructuredIterableInterface` subtype implementation was wrong. These methods are reimplemented by this patch. The regression was caused by a previous fix. The fix (commit "a072133") aimed at resolving a leak that concerned qi objects containing properties or signals, which were kept alive after the Python object holding them was destroyed. This fix introduced a regression that prevented users to call functions of properties from temporary objects. Therefore, this patch reverts that commit. After further research, it was found that the leak is caused by objects being instantiated on the heap when we thought `pybind11` took ownership of these, which is not the case. Instead, the objects are now instantiated on the stack.
1 parent 5dde1d1 commit f96693e

9 files changed

Lines changed: 77 additions & 111 deletions

File tree

qi/test/test_module.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ def test_module_service():
4242
cat = session.service("Cat")
4343
assert cat.meow(3) == 'meow'
4444

45-
4645
def test_module_service_object_lifetime():
4746
session = qi.Session()
4847
session.listenStandalone("tcp://localhost:0")
@@ -72,3 +71,25 @@ def test_module_service_object_lifetime():
7271
assert cat.nbPlay() == 1
7372
del play
7473
assert cat.nbPlay() == 0
74+
75+
def test_object_bound_functions_arguments_conversion_does_not_leak():
76+
session = qi.Session()
77+
session.listenStandalone("tcp://localhost:0")
78+
session.loadServiceRename("moduletest.Cat", "", "truc")
79+
cat = session.service("Cat")
80+
81+
play = cat.makePlay()
82+
assert cat.nbPlay() == 1
83+
cat.order(play)
84+
assert cat.nbPlay() == 1
85+
del play
86+
assert cat.nbPlay() == 0
87+
88+
def test_temporary_object_bound_properties_are_usable():
89+
session = qi.Session()
90+
session.listenStandalone("tcp://localhost:0")
91+
session.loadServiceRename("moduletest.Cat", "", "truc")
92+
# The `volume` member is a bound property of the `Purr` object.
93+
# It should keep the object alive so that setting the property, which
94+
# requires accessing the object, does not fail.
95+
session.service("Cat").makePurr().volume.setValue(42)

qipython/pyproperty.hpp

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

qipython/pysignal.hpp

Lines changed: 5 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,41 +25,12 @@ 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-
5928
struct ProxySignal
6029
{
61-
AnyWeakObject object;
30+
AnyObject object;
6231
unsigned int signalId;
32+
33+
~ProxySignal();
6334
};
6435

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

7142
pybind11::object signalDisconnectAll(SignalBase& sig, bool async);
7243

73-
pybind11::object proxySignalConnect(const AnyObjectGILSafe& obj,
44+
pybind11::object proxySignalConnect(const AnyObject& obj,
7445
unsigned int signalId,
7546
const pybind11::function& callback,
7647
bool async);
7748

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

src/pyapplication.cpp

Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -74,45 +74,6 @@ 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-
11677
} // namespace
11778

11879
void exportApplication(::py::module& m)
@@ -167,12 +128,18 @@ void exportApplication(::py::module& m)
167128
doc(
168129
"Add a callback that will be executed when run() is called."))
169130

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

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

src/pyobject.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ void populateSignals(::py::object pyobj, const Object& obj)
121121
continue;
122122

123123
::py::setattr(pyobj, signalName.c_str(),
124-
castToPyObject(new detail::ProxySignal{ obj, signal.uid() }));
124+
castToPyObject(detail::ProxySignal{ obj, signal.uid() }));
125125
}
126126
}
127127

@@ -138,7 +138,7 @@ void populateProperties(::py::object pyobj, const Object& obj)
138138
continue;
139139

140140
::py::setattr(pyobj, propName.c_str(),
141-
castToPyObject(new detail::ProxyProperty{ obj, prop.uid() }));
141+
castToPyObject(detail::ProxyProperty{ obj, prop.uid() }));
142142
}
143143
}
144144

src/pyproperty.cpp

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

4544
} // namespace
4645

46+
detail::ProxyProperty::~ProxyProperty()
47+
{
48+
// The destructor can lock when waiting for callbacks to end.
49+
GILRelease unlock;
50+
object.reset();
51+
}
52+
4753
bool isProperty(const pybind11::object& obj)
4854
{
4955
GILAcquire lock;
@@ -121,8 +127,7 @@ void exportProperty(::py::module& m)
121127
class_<detail::ProxyProperty>(m, "_ProxyProperty")
122128
.def("value",
123129
[](const detail::ProxyProperty& prop, bool async) {
124-
detail::AnyObjectGILSafe object(prop.object);
125-
const auto fut = object->property(prop.propertyId).async();
130+
const auto fut = prop.object.property(prop.propertyId).async();
126131
GILAcquire lock;
127132
return resultObject(fut, async);
128133
},
@@ -131,9 +136,8 @@ void exportProperty(::py::module& m)
131136
[](detail::ProxyProperty& prop, object pyValue, bool async) {
132137
AnyValue value(unwrapAsRef(pyValue));
133138
GILRelease unlock;
134-
detail::AnyObjectGILSafe object(prop.object);
135139
const auto fut =
136-
toFuture(object->setProperty(prop.propertyId, std::move(value))
140+
toFuture(prop.object.setProperty(prop.propertyId, std::move(value))
137141
.async());
138142
GILAcquire lock;
139143
return resultObject(fut, async);
@@ -145,7 +149,7 @@ void exportProperty(::py::module& m)
145149
arg(asyncArgName) = false)
146150
.def("disconnect",
147151
[](detail::ProxyProperty& prop, SignalLink id, bool async) {
148-
return detail::proxySignalDisconnect(detail::AnyObjectGILSafe(prop.object), id, async);
152+
return detail::proxySignalDisconnect(prop.object, id, async);
149153
},
150154
"id"_a, arg(asyncArgName) = false);
151155
}

src/pysignal.cpp

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

7171
} // namespace
7272

7373
namespace detail
7474
{
7575

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

83+
9184
::py::object signalConnect(SignalBase& sig,
9285
const ::py::function& callback,
9386
bool async)
@@ -126,7 +119,7 @@ ::py::object signalDisconnectAll(SignalBase& sig, bool async)
126119
return resultObject(fut, async);
127120
}
128121

129-
::py::object proxySignalConnect(const AnyObjectGILSafe& obj,
122+
::py::object proxySignalConnect(const AnyObject& obj,
130123
unsigned int signalId,
131124
const ::py::function& callback,
132125
bool async)
@@ -135,18 +128,18 @@ ::py::object proxySignalConnect(const AnyObjectGILSafe& obj,
135128
return connect(
136129
[&](const SignalSubscriber& sub) {
137130
GILRelease unlock;
138-
return obj->connect(signalId, sub).async();
131+
return obj.connect(signalId, sub).async();
139132
},
140133
callback, async);
141134
}
142135

143-
::py::object proxySignalDisconnect(const AnyObjectGILSafe& obj,
136+
::py::object proxySignalDisconnect(const AnyObject& obj,
144137
SignalLink id,
145138
bool async)
146139
{
147140
const auto fut = [&] {
148141
GILRelease unlock;
149-
return toFuture(obj->disconnect(id));
142+
return toFuture(obj.disconnect(id));
150143
}();
151144
GILAcquire lock;
152145
return resultObject(fut, async);
@@ -219,17 +212,15 @@ void exportSignal(::py::module& m)
219212
arg(asyncArgName) = false)
220213
.def("disconnect",
221214
[](detail::ProxySignal& sig, SignalLink id, bool async) {
222-
auto object = detail::AnyObjectGILSafe(sig.object);
223-
return detail::proxySignalDisconnect(object, id, async);
215+
return detail::proxySignalDisconnect(sig.object, id, async);
224216
},
225217
"id"_a, arg(asyncArgName) = false)
226218
.def("__call__",
227219
[](detail::ProxySignal& sig, args pyArgs) {
228220
const auto args =
229221
AnyReference::from(pyArgs).content().asTupleValuePtr();
230222
GILRelease unlock;
231-
auto object = detail::AnyObjectGILSafe(sig.object);
232-
object->metaPost(sig.signalId, args);
223+
sig.object.metaPost(sig.signalId, args);
233224
});
234225
}
235226

src/pytypes.cpp

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -535,8 +535,10 @@ class StructuredIterableInterface
535535
res.reserve(_size);
536536
for (const ::py::handle itemHandle : obj)
537537
{
538-
auto item = ::py::reinterpret_borrow<::py::object>(itemHandle);
539-
res.push_back(AnyValue::from(item).release().rawValue());
538+
const auto item = ::py::reinterpret_borrow<::py::object>(itemHandle);
539+
const auto itemRef = AnyValue::from(item).release();
540+
storeDisownedReference(storage, itemRef);
541+
res.push_back(itemRef.rawValue());
540542
}
541543
return res;
542544
}
@@ -551,8 +553,10 @@ class StructuredIterableInterface
551553
// `pybind11::iterator` is not. We use advance instead.
552554
auto it = obj.begin();
553555
std::advance(it, index);
554-
const auto item = *it;
555-
return AnyValue::from(item).release().rawValue();
556+
const auto item = ::py::reinterpret_borrow<::py::object>(*it);
557+
const auto itemRef = AnyValue::from(item).release();
558+
storeDisownedReference(storage, itemRef);
559+
return itemRef.rawValue();
556560
}
557561

558562
void set(void** /*storage*/, const std::vector<void*>&) override

tests/moduletest.cpp

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ class Cat
121121
return boost::make_shared<Play>(playCounter);
122122
}
123123

124+
void order(qi::AnyObject /*action*/) const
125+
{
126+
// Cats do not follow orders, they do nothing.
127+
}
128+
124129
int nbPurr()
125130
{
126131
return purrCounter->load();
@@ -146,7 +151,8 @@ class Cat
146151
};
147152

148153
QI_REGISTER_OBJECT(Cat, meow, cloneMe, hunger, boredom, cuteness,
149-
makePurr, makeSleep, makePlay, nbPurr, nbSleep, nbPlay);
154+
makePurr, makeSleep, makePlay, order,
155+
nbPurr, nbSleep, nbPlay);
150156

151157
Cat::Cat()
152158
{

0 commit comments

Comments
 (0)