nros C++ API
Lightweight ROS 2 client for embedded real-time systems (C++ headers)
Loading...
Searching...
No Matches
action_server.hpp
Go to the documentation of this file.
1// nros-cpp: Action server class
2// Freestanding C++ — no exceptions, no STL required
3
10#ifndef NROS_CPP_ACTION_SERVER_HPP
11#define NROS_CPP_ACTION_SERVER_HPP
12
13#include <cstdint>
14#include <cstddef>
15#include <string.h>
16
17#include "nros/config.hpp"
18#include "nros/result.hpp"
19
20// Phase 118.D — most action_server FFI symbols come from
21// `nros_cpp_ffi.h`; cbindgen renders Rust `*const [u8; 16]` as
22// pointer-to-array (`const uint8_t (*goal_id)[16]`), so callsites
23// below `reinterpret_cast` from the array-decay shape used in C++
24// member-function signatures.
25//
26// `nros_cpp_action_server_set_callbacks` is excluded from cbindgen
27// output (its Rust signature uses `Option<extern "C" fn(...)>`,
28// which cbindgen renders as opaque structs). Declare locally below
29// with plain function-pointer typedefs that match the actual ABI.
30#include "nros_cpp_ffi.h"
31
32extern "C" {
33typedef int32_t (*nros_cpp_goal_callback_t)(const uint8_t goal_id[16], const uint8_t* data,
34 size_t len, void* ctx);
35typedef int32_t (*nros_cpp_cancel_callback_t)(const uint8_t goal_id[16], void* ctx);
36
39 void* ctx);
40} // extern "C"
41
42namespace nros {
43
45enum class GoalResponse : int32_t {
46 Reject = 0,
49};
50
52enum class CancelResponse : int32_t {
53 Reject = 0,
54 Accept = 1,
55};
56
59enum class GoalStatus : int8_t {
60 Unknown = 0,
61 Accepted = 1,
62 Executing = 2,
63 Canceling = 3,
64 Succeeded = 4,
65 Canceled = 5,
66 Aborted = 6,
67};
68
92template <typename A> class ActionServer {
93 public:
94 using GoalType = typename A::Goal;
95 using ResultType = typename A::Result;
96 using FeedbackType = typename A::Feedback;
97
99 using TypedGoalFn = GoalResponse (*)(const uint8_t uuid[16], const GoalType& goal);
102 void* ctx);
104 using TypedCancelFn = CancelResponse (*)(const uint8_t uuid[16]);
106 using TypedCancelFnWithCtx = CancelResponse (*)(const uint8_t uuid[16], void* ctx);
109
116 template <typename F> Result set_goal_callback(F f) {
117 if (!initialized_) return Result(ErrorCode::NotInitialized);
118 user_goal_fn_ = TypedGoalFn(f); // compile error if F is not convertible
119 user_goal_fn_ctx_ = nullptr; // mutually exclusive with _with_ctx
120 user_goal_ctx_ = nullptr;
121 return install_callbacks();
122 }
123
132 if (!initialized_) return Result(ErrorCode::NotInitialized);
133 user_goal_fn_ctx_ = f;
134 user_goal_ctx_ = ctx;
135 user_goal_fn_ = nullptr;
136 return install_callbacks();
137 }
138
142 template <typename F> Result set_cancel_callback(F f) {
143 if (!initialized_) return Result(ErrorCode::NotInitialized);
144 user_cancel_fn_ = TypedCancelFn(f); // compile error if F is not convertible
145 user_cancel_fn_ctx_ = nullptr; // mutually exclusive with _with_ctx
146 user_cancel_ctx_ = nullptr;
147 return install_callbacks();
148 }
149
157 if (!initialized_) return Result(ErrorCode::NotInitialized);
158 user_cancel_fn_ctx_ = f;
159 user_cancel_ctx_ = ctx;
160 user_cancel_fn_ = nullptr;
161 return install_callbacks();
162 }
163
169 Result publish_feedback(const uint8_t goal_id[16], const FeedbackType& feedback) {
170 if (!initialized_) return Result(ErrorCode::NotInitialized);
171
172 uint8_t buf[FeedbackType::SERIALIZED_SIZE_MAX];
173 size_t len = 0;
174 if (FeedbackType::ffi_serialize(&feedback, buf, sizeof(buf), &len) != 0) {
175 return Result(ErrorCode::Error);
176 }
178 storage_, executor_, reinterpret_cast<const uint8_t(*)[16]>(goal_id), buf, len));
179 }
180
182 Result complete_goal(const uint8_t goal_id[16], const ResultType& result) {
183 if (!initialized_) return Result(ErrorCode::NotInitialized);
184
185 uint8_t buf[ResultType::SERIALIZED_SIZE_MAX];
186 size_t len = 0;
187 if (ResultType::ffi_serialize(&result, buf, sizeof(buf), &len) != 0) {
188 return Result(ErrorCode::Error);
189 }
191 storage_, executor_, reinterpret_cast<const uint8_t(*)[16]>(goal_id), buf, len));
192 }
193
202 template <typename F> Result for_each_active_goal(F f) {
203 using Fn = void (*)(const uint8_t[16], GoalStatus);
204 if (!initialized_) return Result(ErrorCode::NotInitialized);
205 user_visitor_fn_ = Fn(f); // compile error if F is not convertible
206
207 auto trampoline = [](const uint8_t goal_id[16], int8_t status, void* ctx) {
208 auto* self = static_cast<ActionServer*>(ctx);
209 if (!self || self->user_visitor_fn_ == nullptr) return;
210 self->user_visitor_fn_(goal_id, static_cast<GoalStatus>(status));
211 };
213 storage_, executor_,
214 reinterpret_cast<void (*)(const uint8_t(*)[16], int8_t, void*)>(+trampoline), this));
215 user_visitor_fn_ = nullptr; // one-shot — don't leak the function pointer between calls
216 return ret;
217 }
218
220 bool is_valid() const { return initialized_; }
221
224 if (initialized_) {
226 initialized_ = false;
227 }
228 }
229
230 // Move semantics (non-copyable). Relocation goes through the
231 // `nros_cpp_action_server_relocate` runtime call (Phase 84.C1) and
232 // then `install_callbacks()` re-registers the goal/cancel trampolines
233 // with the new `this` as the arena callback context — this is the one
234 // type in nros-cpp that registers its storage address externally.
236 : executor_(other.executor_), user_goal_fn_(other.user_goal_fn_),
237 user_goal_fn_ctx_(other.user_goal_fn_ctx_), user_goal_ctx_(other.user_goal_ctx_),
238 user_cancel_fn_(other.user_cancel_fn_), user_cancel_fn_ctx_(other.user_cancel_fn_ctx_),
239 user_cancel_ctx_(other.user_cancel_ctx_), user_visitor_fn_(other.user_visitor_fn_),
240 initialized_(other.initialized_) {
241 if (other.initialized_) {
242 nros_cpp_action_server_relocate(other.storage_, storage_);
243 other.initialized_ = false;
244 install_callbacks();
245 }
246 }
247
249 if (this != &other) {
250 if (initialized_) {
252 }
253 executor_ = other.executor_;
254 user_goal_fn_ = other.user_goal_fn_;
255 user_goal_fn_ctx_ = other.user_goal_fn_ctx_;
256 user_goal_ctx_ = other.user_goal_ctx_;
257 user_cancel_fn_ = other.user_cancel_fn_;
258 user_cancel_fn_ctx_ = other.user_cancel_fn_ctx_;
259 user_cancel_ctx_ = other.user_cancel_ctx_;
260 user_visitor_fn_ = other.user_visitor_fn_;
261 initialized_ = other.initialized_;
262 if (other.initialized_) {
263 nros_cpp_action_server_relocate(other.storage_, storage_);
264 other.initialized_ = false;
265 install_callbacks();
266 }
267 }
268 return *this;
269 }
270
274 : executor_(nullptr), user_goal_fn_(nullptr), user_goal_fn_ctx_(nullptr),
275 user_goal_ctx_(nullptr), user_cancel_fn_(nullptr), user_cancel_fn_ctx_(nullptr),
276 user_cancel_ctx_(nullptr), user_visitor_fn_(nullptr), initialized_(false) {}
277
278 private:
279 ActionServer(const ActionServer&) = delete;
280 ActionServer& operator=(const ActionServer&) = delete;
281
282 friend class Node;
283
284 // ── C trampolines ───────────────────────────────────────────────
285 //
286 // `ctx` is a pointer to this `ActionServer<A>` instance, so the
287 // trampoline reads the user's stored function pointer via the
288 // instance's own fields — no shared mutable statics.
289
290 static int32_t goal_trampoline(const uint8_t goal_id[16], const uint8_t* data, size_t len,
291 void* ctx) {
292 auto* self = static_cast<ActionServer*>(ctx);
293 if (!self) return static_cast<int32_t>(GoalResponse::Reject);
294 GoalType g;
295 if (GoalType::ffi_deserialize(data, len, &g) != 0) {
296 return static_cast<int32_t>(GoalResponse::Reject);
297 }
298 if (self->user_goal_fn_ctx_ != nullptr) {
299 return static_cast<int32_t>(self->user_goal_fn_ctx_(goal_id, g, self->user_goal_ctx_));
300 }
301 if (self->user_goal_fn_ != nullptr) {
302 return static_cast<int32_t>(self->user_goal_fn_(goal_id, g));
303 }
304 return static_cast<int32_t>(GoalResponse::Reject);
305 }
306
307 static int32_t cancel_trampoline(const uint8_t goal_id[16], void* ctx) {
308 auto* self = static_cast<ActionServer*>(ctx);
309 if (!self) return static_cast<int32_t>(CancelResponse::Accept);
310 if (self->user_cancel_fn_ctx_ != nullptr) {
311 return static_cast<int32_t>(self->user_cancel_fn_ctx_(goal_id, self->user_cancel_ctx_));
312 }
313 if (self->user_cancel_fn_ != nullptr) {
314 return static_cast<int32_t>(self->user_cancel_fn_(goal_id));
315 }
316 return static_cast<int32_t>(CancelResponse::Accept);
317 }
318
319 Result install_callbacks() {
320 bool goal_set = (user_goal_fn_ != nullptr) || (user_goal_fn_ctx_ != nullptr);
321 bool cancel_set = (user_cancel_fn_ != nullptr) || (user_cancel_fn_ctx_ != nullptr);
322 nros_cpp_goal_callback_t gcb = goal_set ? &goal_trampoline : nullptr;
323 nros_cpp_cancel_callback_t ccb = cancel_set ? &cancel_trampoline : nullptr;
324 return Result(nros_cpp_action_server_set_callbacks(storage_, gcb, ccb, this));
325 }
326
327 alignas(8) uint8_t storage_[NROS_CPP_ACTION_SERVER_STORAGE_SIZE];
328 void* executor_; // Executor context needed for feedback/result operations
329 TypedGoalFn user_goal_fn_;
330 TypedGoalFnWithCtx user_goal_fn_ctx_;
331 void* user_goal_ctx_;
332 TypedCancelFn user_cancel_fn_;
333 TypedCancelFnWithCtx user_cancel_fn_ctx_;
334 void* user_cancel_ctx_;
335 TypedVisitorFn user_visitor_fn_;
336 bool initialized_;
337 // Phase 87.6: action name buffer moved C++-side. 256 matches
338 // nros_node::limits::MAX_ACTION_NAME_LEN.
339 char action_name_[256] = {};
340};
341
342} // namespace nros
343
344// Phase 84.G8: out-of-line definition of Node::create_action_server<A>().
345#include "nros/node.hpp"
346
347namespace nros {
348
349template <typename A>
352 if (!initialized_) return Result(ErrorCode::NotInitialized);
354 ffi_qos.reliability = static_cast<nros_cpp_qos_reliability_t>(qos.reliability_raw());
355 ffi_qos.durability = static_cast<nros_cpp_qos_durability_t>(qos.durability_raw());
356 ffi_qos.history = static_cast<nros_cpp_qos_history_t>(qos.history_raw());
357 ffi_qos.liveliness_kind = static_cast<nros_cpp_qos_liveliness_t>(qos.liveliness_raw());
358 ffi_qos.depth = qos.depth();
359 ffi_qos.deadline_ms = qos.deadline_ms();
360 ffi_qos.lifespan_ms = qos.lifespan_ms();
361 ffi_qos.liveliness_lease_ms = qos.liveliness_lease_ms();
362 ffi_qos.avoid_ros_namespace_conventions = qos.avoid_ros_namespace_conventions() ? 1 : 0;
364 A::Goal::TYPE_HASH, ffi_qos, out.storage_);
365 if (ret != 0) return Result(ret);
366 // Register with executor — creates transport handles (3 queryables + 2 publishers).
367 // Deferred from create to avoid FreeRTOS QEMU deadlocks. Phase 87.6:
368 // names are passed at register-time (buffers live on the C++
369 // `nros::ActionServer<A>` class, not in the runtime struct).
370 // Phase 189.M3.3.c — `sched_context` binds the action's (arena-registered)
371 // goal-service handle to a scheduling context. UNSET ⇒ 0 (inherit, no-op).
372 uint8_t sched = (options.sched_context == SCHED_CONTEXT_UNSET)
373 ? 0u
374 : static_cast<uint8_t>(options.sched_context);
375 ret = nros_cpp_action_server_register(out.storage_, executor_handle_, action_name, A::TYPE_NAME,
376 A::Goal::TYPE_HASH, sched);
377 if (ret == 0) {
378 // Phase 87.6: copy action_name into the C++-owned buffer for
379 // `get_action_name()` accessor.
380 size_t name_len = 0;
381 while (action_name[name_len] != '\0' && name_len + 1 < sizeof(out.action_name_)) {
382 out.action_name_[name_len] = action_name[name_len];
383 ++name_len;
384 }
385 out.action_name_[name_len] = '\0';
386 out.executor_ = executor_handle_;
387 out.initialized_ = true;
388 }
389 return Result(ret);
390}
391
392} // namespace nros
393
394#endif // NROS_CPP_ACTION_SERVER_HPP
int32_t(* nros_cpp_goal_callback_t)(const uint8_t goal_id[16], const uint8_t *data, size_t len, void *ctx)
Definition action_server.hpp:33
nros_cpp_ret_t nros_cpp_action_server_set_callbacks(void *handle, nros_cpp_goal_callback_t goal_cb, nros_cpp_cancel_callback_t cancel_cb, void *ctx)
int32_t(* nros_cpp_cancel_callback_t)(const uint8_t goal_id[16], void *ctx)
Definition action_server.hpp:35
Definition action_server.hpp:92
ActionServer()
Definition action_server.hpp:273
Result for_each_active_goal(F f)
Definition action_server.hpp:202
bool is_valid() const
Check if the action server is initialized and valid.
Definition action_server.hpp:220
ActionServer & operator=(ActionServer &&other)
Definition action_server.hpp:248
typename A::Goal GoalType
Definition action_server.hpp:94
void(*)(const uint8_t uuid[16], GoalStatus status) TypedVisitorFn
User-facing visitor signature for for_each_active_goal.
Definition action_server.hpp:108
Result set_goal_callback(F f)
Definition action_server.hpp:116
Result set_cancel_callback(F f)
Definition action_server.hpp:142
Result set_cancel_callback_with_ctx(TypedCancelFnWithCtx f, void *ctx)
Definition action_server.hpp:156
~ActionServer()
Destructor — releases action server resources.
Definition action_server.hpp:223
Result publish_feedback(const uint8_t goal_id[16], const FeedbackType &feedback)
Definition action_server.hpp:169
CancelResponse(*)(const uint8_t uuid[16], void *ctx) TypedCancelFnWithCtx
User-facing typed cancel callback signature with user context (Phase 84.G9).
Definition action_server.hpp:106
Result set_goal_callback_with_ctx(TypedGoalFnWithCtx f, void *ctx)
Definition action_server.hpp:131
typename A::Feedback FeedbackType
Definition action_server.hpp:96
ActionServer(ActionServer &&other)
Definition action_server.hpp:235
GoalResponse(*)(const uint8_t uuid[16], const GoalType &goal, void *ctx) TypedGoalFnWithCtx
User-facing typed goal callback signature with user context (Phase 84.G9).
Definition action_server.hpp:102
GoalResponse(*)(const uint8_t uuid[16], const GoalType &goal) TypedGoalFn
User-facing typed goal callback signature.
Definition action_server.hpp:99
CancelResponse(*)(const uint8_t uuid[16]) TypedCancelFn
User-facing typed cancel callback signature.
Definition action_server.hpp:104
Result complete_goal(const uint8_t goal_id[16], const ResultType &result)
Complete a goal with a result.
Definition action_server.hpp:182
typename A::Result ResultType
Definition action_server.hpp:95
Definition future.hpp:40
Definition node.hpp:158
Result create_action_server(ActionServer< A > &out, const char *action_name, const QoS &qos=QoS::services(), const ActionServerOptions &options={})
Definition action_server.hpp:350
Definition qos.hpp:67
Definition result.hpp:52
Inline storage-size macros for opaque entity buffers.
int nros_cpp_ret_t
Definition future.hpp:20
Definition nros.hpp:42
GoalResponse
Goal acceptance response returned from the user's goal callback.
Definition action_server.hpp:45
CancelResponse
Cancel acceptance response returned from the user's cancel callback.
Definition action_server.hpp:52
GoalStatus
Definition action_server.hpp:59
@ Error
Generic failure not covered by a more specific code.
nros::Node and global session helpers.
nros_cpp_qos_history_t
Definition qos.hpp:31
nros_cpp_qos_liveliness_t
Definition qos.hpp:35
nros_cpp_qos_durability_t
Definition qos.hpp:27
nros_cpp_qos_reliability_t
Definition qos.hpp:23
nros::Result, nros::ErrorCode, and the NROS_TRY macro.
Definition qos.hpp:41