Skip to content

Commit 4a0736e

Browse files
davetcolemanclaude
andcommitted
Don't let exceptions escape destructors
TimeSource::NodeState, EventsCBGExecutor and WaitResult each call, from their destructor, a function that throws by contract. A destructor is implicitly noexcept, so such a throw calls std::terminate instead of unwinding. Guard each call the way ~Context already does: log the exception through RCLCPP_ERROR and let destruction finish. Found with clang-tidy bugprone-exception-escape. Signed-off-by: Dave Coleman <dave@picknik.ai> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 29de98c commit 4a0736e

4 files changed

Lines changed: 55 additions & 3 deletions

File tree

rclcpp/include/rclcpp/wait_result.hpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
#include "rcl/wait.h"
2525

26+
#include "rclcpp/logging.hpp"
2627
#include "rclcpp/macros.hpp"
2728
#include "rclcpp/wait_result_kind.hpp"
2829

@@ -137,7 +138,16 @@ class WaitResult final
137138
~WaitResult()
138139
{
139140
if (wait_set_pointer_) {
140-
wait_set_pointer_->wait_result_release();
141+
// wait_result_release() throws if the wait set is no longer holding this result.
142+
// A destructor is noexcept, so letting that escape would terminate the process.
143+
try {
144+
wait_set_pointer_->wait_result_release();
145+
} catch (const std::exception & exc) {
146+
RCLCPP_ERROR(
147+
rclcpp::get_logger("rclcpp"), "unhandled exception in ~WaitResult(): %s", exc.what());
148+
} catch (...) {
149+
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "unhandled exception in ~WaitResult()");
150+
}
141151
}
142152
}
143153

rclcpp/src/rclcpp/executors/events_cbg_executor/events_cbg_executor.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,18 @@ EventsCBGExecutor::EventsCBGExecutor(
116116

117117
EventsCBGExecutor::~EventsCBGExecutor()
118118
{
119-
shutdown();
119+
// shutdown() removes nodes and callback groups, which throws when an entity is no longer
120+
// associated with this executor. A destructor is noexcept, so letting that escape would
121+
// terminate the process.
122+
try {
123+
shutdown();
124+
} catch (const std::exception & exc) {
125+
RCLCPP_ERROR(
126+
rclcpp::get_logger("rclcpp"),
127+
"unhandled exception in ~EventsCBGExecutor(): %s", exc.what());
128+
} catch (...) {
129+
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "unhandled exception in ~EventsCBGExecutor()");
130+
}
120131
}
121132

122133
void EventsCBGExecutor::shutdown()

rclcpp/src/rclcpp/time_source.cpp

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,16 @@ class TimeSource::NodeState final
203203
node_base_ || node_topics_ || node_graph_ || node_services_ ||
204204
node_logging_ || node_clock_ || node_parameters_)
205205
{
206-
detachNode();
206+
// detachNode() removes the parameter callbacks, which throws while a set-parameters
207+
// callback is running. A destructor is noexcept, so letting that escape would terminate
208+
// the process.
209+
try {
210+
detachNode();
211+
} catch (const std::exception & exc) {
212+
RCLCPP_ERROR(logger_, "unhandled exception in ~NodeState(): %s", exc.what());
213+
} catch (...) {
214+
RCLCPP_ERROR(logger_, "unhandled exception in ~NodeState()");
215+
}
207216
}
208217
}
209218

rclcpp/test/rclcpp/test_time_source.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
#include <stdexcept>
2525
#include <string>
2626
#include <thread>
27+
#include <vector>
2728

2829
#include "rcl/error_handling.h"
2930
#include "rcl/time.h"
@@ -203,6 +204,27 @@ TEST_F(TestTimeSource, detachUnattached) {
203204
ASSERT_NO_THROW(ts.detachNode());
204205
}
205206

207+
TEST_F(TestTimeSource, destructor_does_not_terminate_when_detach_throws) {
208+
auto time_source = std::make_unique<rclcpp::TimeSource>(node);
209+
210+
// Destroying the time source from inside a set-parameters callback makes its destructor call
211+
// NodeParameters::remove_on_set_parameters_callback(), which throws because parameter
212+
// modification is disabled while such a callback runs. A destructor is implicitly noexcept, so
213+
// an unguarded throw here terminates the process instead of failing this expectation.
214+
auto handle = node->add_on_set_parameters_callback(
215+
[&time_source](const std::vector<rclcpp::Parameter> &) {
216+
time_source.reset();
217+
rcl_interfaces::msg::SetParametersResult result;
218+
result.successful = true;
219+
return result;
220+
});
221+
222+
EXPECT_NO_THROW(node->set_parameter({"use_sim_time", true}));
223+
EXPECT_EQ(nullptr, time_source);
224+
225+
node->remove_on_set_parameters_callback(handle.get());
226+
}
227+
206228
TEST_F(TestTimeSource, reattach) {
207229
rclcpp::TimeSource ts;
208230
// Try reattach

0 commit comments

Comments
 (0)