Skip to content

Commit aece278

Browse files
author
opennao
committed
Merge commit 'refs/integration/team/platform/merge-309'
2 parents 228d3ef + b35f6dd commit aece278

14 files changed

Lines changed: 164 additions & 145 deletions

cmake/add_tests.cmake

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,32 @@ target_link_libraries(test_qipython
4949
qi.interface
5050
cxx11
5151
gmock)
52-
# Unfortunately, in some of our toolchains, gtest/gmock headers are found in the qi-framework
53-
# package, which comes first in the include paths order given to the compiler. This causes the
54-
# compiler to use those headers instead of the ones we got from a `FetchContent` of the googletest
55-
# repository.
56-
target_include_directories(test_qipython
57-
BEFORE # Force this path to come first in the list of include paths.
58-
PRIVATE $<TARGET_PROPERTY:gmock,INTERFACE_INCLUDE_DIRECTORIES>)
59-
enable_warnings(test_qipython)
60-
set_build_rpath_to_qipython_dependencies(test_qipython)
52+
53+
add_executable(test_qipython_local_interpreter)
54+
target_sources(test_qipython_local_interpreter
55+
PRIVATE tests/test_qipython_local_interpreter.cpp)
56+
target_link_libraries(test_qipython_local_interpreter
57+
PRIVATE Python::Python
58+
pybind11
59+
qi_python_objects
60+
qi.interface
61+
cxx11
62+
gmock)
6163

6264
set(_sdk_prefix "${CMAKE_BINARY_DIR}/sdk")
63-
gtest_discover_tests(test_qipython EXTRA_ARGS --qi-sdk-prefix ${_sdk_prefix})
65+
foreach(test_target IN ITEMS test_qipython
66+
test_qipython_local_interpreter)
67+
# Unfortunately, in some of our toolchains, gtest/gmock headers are found in the qi-framework
68+
# package, which comes first in the include paths order given to the compiler. This causes the
69+
# compiler to use those headers instead of the ones we got from a `FetchContent` of the googletest
70+
# repository.
71+
target_include_directories(${test_target}
72+
BEFORE # Force this path to come first in the list of include paths.
73+
PRIVATE $<TARGET_PROPERTY:gmock,INTERFACE_INCLUDE_DIRECTORIES>)
74+
enable_warnings(${test_target})
75+
set_build_rpath_to_qipython_dependencies(${test_target})
76+
gtest_discover_tests(${test_target} EXTRA_ARGS --qi-sdk-prefix ${_sdk_prefix})
77+
endforeach()
6478

6579
if(NOT Python_Interpreter_FOUND)
6680
message(WARNING "tests: a compatible Python Interpreter was NOT found, Python tests are DISABLED.")

cmake/set_libqi_dependency.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
overridable_variable(LIBQI_VERSION 2.1.0)
1+
overridable_variable(LIBQI_VERSION 3.0.0)
22

33
# Our github clone is sometimes late or is missing tags, so we enable
44
# customisation of the URL at configuration time, so users can use another clone.

qi/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '3.1.0'
1+
__version__ = '3.1.1'

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/pyguard.hpp

Lines changed: 20 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -143,59 +143,57 @@ void pybind11GuardDisarm(G& guard)
143143
/// RAII utility type that guarantees that the GIL is locked for the scope of
144144
/// the lifetime of the object.
145145
///
146+
/// Objects of this type (or objects composed of them) must not be kept alive
147+
/// after the hand is given back to the interpreter.
148+
///
146149
/// This type is re-entrant.
147150
///
148-
/// postcondition: `GILAcquire acq;` establishes `currentThreadHoldsGil()`
151+
/// postcondition: `GILAcquire acq;` establishes
152+
/// `(interpreterIsFinalizing() && *interpreterIsFinalizing()) || currentThreadHoldsGil()`
149153
struct GILAcquire
150154
{
151155
inline GILAcquire()
152156
{
157+
const auto optIsFinalizing = interpreterIsFinalizing();
158+
const auto definitelyFinalizing = optIsFinalizing && *optIsFinalizing;
153159
// `gil_scoped_acquire` is re-entrant by itself, so we don't need to check
154160
// whether or not the GIL is already held by the current thread.
155-
QI_ASSERT(currentThreadHoldsGil());
161+
if (!definitelyFinalizing)
162+
_acq.emplace();
163+
QI_ASSERT(definitelyFinalizing || currentThreadHoldsGil());
156164
}
157165

158166
GILAcquire(const GILAcquire&) = delete;
159167
GILAcquire& operator=(const GILAcquire&) = delete;
160168

161-
inline ~GILAcquire()
162-
{
163-
const auto optIsFinalizing = interpreterIsFinalizing();
164-
const auto definitelyFinalizing = optIsFinalizing && *optIsFinalizing;
165-
if (definitelyFinalizing)
166-
detail::pybind11GuardDisarm(_acq);
167-
}
168-
169169
private:
170-
pybind11::gil_scoped_acquire _acq;
170+
boost::optional<pybind11::gil_scoped_acquire> _acq;
171171
};
172172

173173
/// RAII utility type that guarantees that the GIL is unlocked for the scope of
174174
/// the lifetime of the object.
175175
///
176+
/// Objects of this type (or objects composed of them) must not be kept alive
177+
/// after the hand is given back to the interpreter.
178+
///
176179
/// This type is re-entrant.
177180
///
178-
/// postcondition: `GILRelease rel;` establishes `!currentThreadHoldsGil()`
181+
/// postcondition: `GILRelease rel;` establishes
182+
/// `(interpreterIsFinalizing() && *interpreterIsFinalizing()) || !currentThreadHoldsGil()`
179183
struct GILRelease
180184
{
181185
inline GILRelease()
182186
{
183-
if (currentThreadHoldsGil())
187+
const auto optIsFinalizing = interpreterIsFinalizing();
188+
const auto definitelyFinalizing = optIsFinalizing && *optIsFinalizing;
189+
if (!definitelyFinalizing && currentThreadHoldsGil())
184190
_release.emplace();
185-
QI_ASSERT(!currentThreadHoldsGil());
191+
QI_ASSERT(definitelyFinalizing || !currentThreadHoldsGil());
186192
}
187193

188194
GILRelease(const GILRelease&) = delete;
189195
GILRelease& operator=(const GILRelease&) = delete;
190196

191-
inline ~GILRelease()
192-
{
193-
const auto optIsFinalizing = interpreterIsFinalizing();
194-
const auto definitelyFinalizing = optIsFinalizing && *optIsFinalizing;
195-
if (_release && definitelyFinalizing)
196-
detail::pybind11GuardDisarm(*_release);
197-
}
198-
199197
private:
200198
boost::optional<pybind11::gil_scoped_release> _release;
201199
};

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
}

0 commit comments

Comments
 (0)