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// issue 0964 — the feedback/result TX buffers below size through
20// `detail::buffer_bounds`, so this header needs it directly rather than by
21// luck through a transitive include.
22#include "nros/size_bound.hpp"
23
24// Phase 118.D — most action_server FFI symbols come from
25// `nros_cpp_ffi.h`; cbindgen renders Rust `*const [u8; 16]` as
26// pointer-to-array (`const uint8_t (*goal_id)[16]`), so callsites
27// below `reinterpret_cast` from the array-decay shape used in C++
28// member-function signatures.
29//
30// `nros_cpp_action_server_set_callbacks` is excluded from cbindgen
31// output (its Rust signature uses `Option<extern "C" fn(...)>`,
32// which cbindgen renders as opaque structs). Declare locally below
33// with plain function-pointer typedefs that match the actual ABI.
34#include "nros_cpp_ffi.h"
35
36extern "C" {
37typedef int32_t (*nros_cpp_goal_callback_t)(const uint8_t goal_id[16], const uint8_t* data,
38 size_t len, void* ctx);
39typedef int32_t (*nros_cpp_cancel_callback_t)(const uint8_t goal_id[16], void* ctx);
40/* Issue 0796 — post-accept hook. Excluded from cbindgen for the same reason as
41 * the two above (`Option<extern "C" fn>` renders as an opaque struct), so this
42 * declaration is MIRRORED in `nros/component.h` as well; `just check c`'s
43 * cross-include TU is the gate that catches a half-updated mirror. */
44typedef void (*nros_cpp_accepted_callback_t)(const uint8_t goal_id[16], void* ctx);
45
48 void* ctx);
51 void* ctx);
52} // extern "C"
53
54namespace nros {
55
57enum class GoalResponse : int32_t {
58 Reject = 0,
61};
62
71enum class CancelResponse : int32_t {
72 Reject = 0,
73 Accept = 1,
74};
75
83enum class CancelReturnCode : int8_t {
84 Ok = 0,
85 Rejected = 1,
86 UnknownGoal = 2,
88};
89
92enum class GoalStatus : int8_t {
93 Unknown = 0,
94 Accepted = 1,
95 Executing = 2,
96 Canceling = 3,
97 Succeeded = 4,
98 Canceled = 5,
99 Aborted = 6,
100};
101
125template <typename A> class ActionServer {
126 public:
127 using GoalType = typename A::Goal;
128 using ResultType = typename A::Result;
129 using FeedbackType = typename A::Feedback;
130
132 using TypedGoalFn = GoalResponse (*)(const uint8_t uuid[16], const GoalType& goal);
134 using TypedGoalFnWithCtx = GoalResponse (*)(const uint8_t uuid[16], const GoalType& goal,
135 void* ctx);
137 using TypedCancelFn = CancelResponse (*)(const uint8_t uuid[16]);
139 using TypedCancelFnWithCtx = CancelResponse (*)(const uint8_t uuid[16], void* ctx);
141 using TypedAcceptedFn = void (*)(const uint8_t uuid[16]);
143 using TypedAcceptedFnWithCtx = void (*)(const uint8_t uuid[16], void* ctx);
145 using TypedVisitorFn = void (*)(const uint8_t uuid[16], GoalStatus status);
146
153 template <typename F> Result set_goal_callback(F f) {
154 if (!initialized_) return Result(ErrorCode::NotInitialized);
155 user_goal_fn_ = TypedGoalFn(f); // compile error if F is not convertible
156 user_goal_fn_ctx_ = nullptr; // mutually exclusive with _with_ctx
157 user_goal_ctx_ = nullptr;
158 return install_callbacks();
159 }
160
169 if (!initialized_) return Result(ErrorCode::NotInitialized);
170 user_goal_fn_ctx_ = f;
171 user_goal_ctx_ = ctx;
172 user_goal_fn_ = nullptr;
173 return install_callbacks();
174 }
175
179 template <typename F> Result set_cancel_callback(F f) {
180 if (!initialized_) return Result(ErrorCode::NotInitialized);
181 user_cancel_fn_ = TypedCancelFn(f); // compile error if F is not convertible
182 user_cancel_fn_ctx_ = nullptr; // mutually exclusive with _with_ctx
183 user_cancel_ctx_ = nullptr;
184 return install_callbacks();
185 }
186
194 if (!initialized_) return Result(ErrorCode::NotInitialized);
195 user_cancel_fn_ctx_ = f;
196 user_cancel_ctx_ = ctx;
197 user_cancel_fn_ = nullptr;
198 return install_callbacks();
199 }
200
216 template <typename F> Result set_accepted_callback(F f) {
217 if (!initialized_) return Result(ErrorCode::NotInitialized);
218 user_accepted_fn_ = TypedAcceptedFn(f); // compile error if F is not convertible
219 user_accepted_fn_ctx_ = nullptr; // mutually exclusive with _with_ctx
220 user_accepted_ctx_ = nullptr;
221 return install_callbacks();
222 }
223
229 if (!initialized_) return Result(ErrorCode::NotInitialized);
230 user_accepted_fn_ctx_ = f;
231 user_accepted_ctx_ = ctx;
232 user_accepted_fn_ = nullptr;
233 return install_callbacks();
234 }
235
241 Result publish_feedback(const uint8_t goal_id[16], const FeedbackType& feedback) {
242 if (!initialized_) return Result(ErrorCode::NotInitialized);
243
244 uint8_t buf[::nros::detail::buffer_bounds<FeedbackType>::tx];
245 size_t len = 0;
246 if (FeedbackType::ffi_serialize(&feedback, buf, sizeof(buf), &len) != 0) {
247 return Result(ErrorCode::Error);
248 }
249 return Result(nros_cpp_action_server_publish_feedback(
250 storage_, executor_, reinterpret_cast<const uint8_t(*)[16]>(goal_id), buf, len));
251 }
252
261 Result complete_goal(const uint8_t goal_id[16], GoalStatus status, const ResultType& result) {
262 if (!initialized_) return Result(ErrorCode::NotInitialized);
263
264 uint8_t buf[::nros::detail::buffer_bounds<ResultType>::tx];
265 size_t len = 0;
266 if (ResultType::ffi_serialize(&result, buf, sizeof(buf), &len) != 0) {
267 return Result(ErrorCode::Error);
268 }
269 return Result(nros_cpp_action_server_complete_goal(
270 storage_, executor_, reinterpret_cast<const uint8_t(*)[16]>(goal_id),
271 static_cast<int32_t>(status), buf, len));
272 }
273
276 Result complete_goal(const uint8_t goal_id[16], const ResultType& result) {
277 return complete_goal(goal_id, GoalStatus::Succeeded, result);
278 }
279
288 template <typename F> Result for_each_active_goal(F f) {
289 using Fn = void (*)(const uint8_t[16], GoalStatus);
290 if (!initialized_) return Result(ErrorCode::NotInitialized);
291 user_visitor_fn_ = Fn(f); // compile error if F is not convertible
292
293 auto trampoline = [](const uint8_t goal_id[16], int8_t status, void* ctx) {
294 auto* self = static_cast<ActionServer*>(ctx);
295 if (!self || self->user_visitor_fn_ == nullptr) return;
296 self->user_visitor_fn_(goal_id, static_cast<GoalStatus>(status));
297 };
298 Result ret(nros_cpp_action_server_for_each_active_goal(
299 storage_, executor_,
300 reinterpret_cast<void (*)(const uint8_t(*)[16], int8_t, void*)>(+trampoline), this));
301 user_visitor_fn_ = nullptr; // one-shot — don't leak the function pointer between calls
302 return ret;
303 }
304
306 bool is_valid() const { return initialized_; }
307
310 if (initialized_) {
311 nros_cpp_action_server_destroy(storage_);
312 initialized_ = false;
313 }
314 }
315
316 // Move semantics (non-copyable). Relocation goes through the
317 // `nros_cpp_action_server_relocate` runtime call (Phase 84.C1) and
318 // then `install_callbacks()` re-registers the goal/cancel trampolines
319 // with the new `this` as the arena callback context — this is the one
320 // type in nros-cpp that registers its storage address externally.
322 : executor_(other.executor_), user_goal_fn_(other.user_goal_fn_),
323 user_goal_fn_ctx_(other.user_goal_fn_ctx_), user_goal_ctx_(other.user_goal_ctx_),
324 user_cancel_fn_(other.user_cancel_fn_), user_cancel_fn_ctx_(other.user_cancel_fn_ctx_),
325 user_cancel_ctx_(other.user_cancel_ctx_), user_accepted_fn_(other.user_accepted_fn_),
326 user_accepted_fn_ctx_(other.user_accepted_fn_ctx_),
327 user_accepted_ctx_(other.user_accepted_ctx_), user_visitor_fn_(other.user_visitor_fn_),
328 initialized_(other.initialized_) {
329 if (other.initialized_) {
330 nros_cpp_action_server_relocate(other.storage_, storage_);
331 other.initialized_ = false;
332 install_callbacks();
333 }
334 }
335
337 if (this != &other) {
338 if (initialized_) {
339 nros_cpp_action_server_destroy(storage_);
340 }
341 executor_ = other.executor_;
342 user_goal_fn_ = other.user_goal_fn_;
343 user_goal_fn_ctx_ = other.user_goal_fn_ctx_;
344 user_goal_ctx_ = other.user_goal_ctx_;
345 user_cancel_fn_ = other.user_cancel_fn_;
346 user_cancel_fn_ctx_ = other.user_cancel_fn_ctx_;
347 user_cancel_ctx_ = other.user_cancel_ctx_;
348 user_accepted_fn_ = other.user_accepted_fn_;
349 user_accepted_fn_ctx_ = other.user_accepted_fn_ctx_;
350 user_accepted_ctx_ = other.user_accepted_ctx_;
351 user_visitor_fn_ = other.user_visitor_fn_;
352 initialized_ = other.initialized_;
353 if (other.initialized_) {
354 nros_cpp_action_server_relocate(other.storage_, storage_);
355 other.initialized_ = false;
356 install_callbacks();
357 }
358 }
359 return *this;
360 }
361
365 : executor_(nullptr), user_goal_fn_(nullptr), user_goal_fn_ctx_(nullptr),
366 user_goal_ctx_(nullptr), user_cancel_fn_(nullptr), user_cancel_fn_ctx_(nullptr),
367 user_cancel_ctx_(nullptr), user_accepted_fn_(nullptr), user_accepted_fn_ctx_(nullptr),
368 user_accepted_ctx_(nullptr), user_visitor_fn_(nullptr), initialized_(false) {}
369
370 private:
371 ActionServer(const ActionServer&) = delete;
372 ActionServer& operator=(const ActionServer&) = delete;
373
374 friend class Node;
375
376 // ── C trampolines ───────────────────────────────────────────────
377 //
378 // `ctx` is a pointer to this `ActionServer<A>` instance, so the
379 // trampoline reads the user's stored function pointer via the
380 // instance's own fields — no shared mutable statics.
381
382 static int32_t goal_trampoline(const uint8_t goal_id[16], const uint8_t* data, size_t len,
383 void* ctx) {
384 auto* self = static_cast<ActionServer*>(ctx);
385 if (!self) return static_cast<int32_t>(GoalResponse::Reject);
386 GoalType g;
387 if (GoalType::ffi_deserialize(data, len, &g) != 0) {
388 return static_cast<int32_t>(GoalResponse::Reject);
389 }
390 if (self->user_goal_fn_ctx_ != nullptr) {
391 return static_cast<int32_t>(self->user_goal_fn_ctx_(goal_id, g, self->user_goal_ctx_));
392 }
393 if (self->user_goal_fn_ != nullptr) {
394 return static_cast<int32_t>(self->user_goal_fn_(goal_id, g));
395 }
396 return static_cast<int32_t>(GoalResponse::Reject);
397 }
398
399 static int32_t cancel_trampoline(const uint8_t goal_id[16], void* ctx) {
400 auto* self = static_cast<ActionServer*>(ctx);
401 if (!self) return static_cast<int32_t>(CancelResponse::Accept);
402 if (self->user_cancel_fn_ctx_ != nullptr) {
403 return static_cast<int32_t>(self->user_cancel_fn_ctx_(goal_id, self->user_cancel_ctx_));
404 }
405 if (self->user_cancel_fn_ != nullptr) {
406 return static_cast<int32_t>(self->user_cancel_fn_(goal_id));
407 }
408 return static_cast<int32_t>(CancelResponse::Accept);
409 }
410
411 static void accepted_trampoline(const uint8_t goal_id[16], void* ctx) {
412 auto* self = static_cast<ActionServer*>(ctx);
413 if (!self) return;
414 if (self->user_accepted_fn_ctx_ != nullptr) {
415 self->user_accepted_fn_ctx_(goal_id, self->user_accepted_ctx_);
416 return;
417 }
418 if (self->user_accepted_fn_ != nullptr) {
419 self->user_accepted_fn_(goal_id);
420 }
421 }
422
423 Result install_callbacks() {
424 bool goal_set = (user_goal_fn_ != nullptr) || (user_goal_fn_ctx_ != nullptr);
425 bool cancel_set = (user_cancel_fn_ != nullptr) || (user_cancel_fn_ctx_ != nullptr);
426 bool accepted_set = (user_accepted_fn_ != nullptr) || (user_accepted_fn_ctx_ != nullptr);
427 nros_cpp_goal_callback_t gcb = goal_set ? &goal_trampoline : nullptr;
428 nros_cpp_cancel_callback_t ccb = cancel_set ? &cancel_trampoline : nullptr;
429 nros_cpp_ret_t ret = nros_cpp_action_server_set_callbacks(storage_, gcb, ccb, this);
430 if (ret != 0) return Result(ret);
431 // Issue 0796 — a SECOND install call rather than a fourth parameter on
432 // the first: `nros_cpp_action_server_set_callbacks` is declared in
433 // three places and called directly by C components, so growing it
434 // would have broken every one of them. Both calls pass the same
435 // `this`, which is the shared context slot the runtime keeps.
436 nros_cpp_accepted_callback_t acb = accepted_set ? &accepted_trampoline : nullptr;
437 return Result(nros_cpp_action_server_set_accepted_callback(storage_, acb, this));
438 }
439
440 alignas(8) uint8_t storage_[NROS_CPP_ACTION_SERVER_STORAGE_SIZE];
441 void* executor_; // Executor context needed for feedback/result operations
442 TypedGoalFn user_goal_fn_;
443 TypedGoalFnWithCtx user_goal_fn_ctx_;
444 void* user_goal_ctx_;
445 TypedCancelFn user_cancel_fn_;
446 TypedCancelFnWithCtx user_cancel_fn_ctx_;
447 void* user_cancel_ctx_;
448 TypedAcceptedFn user_accepted_fn_;
449 TypedAcceptedFnWithCtx user_accepted_fn_ctx_;
450 void* user_accepted_ctx_;
451 TypedVisitorFn user_visitor_fn_;
452 bool initialized_;
453 // Phase 87.6: action name buffer moved C++-side. 256 matches
454 // nros_node::limits::MAX_ACTION_NAME_LEN.
455 char action_name_[256] = {};
456};
457
458} // namespace nros
459
460// Phase 84.G8: out-of-line definition of Node::create_action_server<A>().
461#include "nros/node.hpp"
462
463namespace nros {
464
465template <typename A>
466Result Node::create_action_server(ActionServer<A>& out, const char* action_name, const QoS& qos,
467 const ActionServerOptions& options) {
468 if (!initialized_) return Result(ErrorCode::NotInitialized);
469 nros_cpp_qos_t ffi_qos = detail::qos_to_ffi(qos);
470 nros_cpp_ret_t ret = nros_cpp_action_server_create(&handle_, action_name, A::TYPE_NAME,
471 A::Goal::TYPE_HASH, ffi_qos, out.storage_);
472 if (ret != 0) return Result(ret);
473 // Register with executor — creates transport handles (3 queryables + 2 publishers).
474 // Deferred from create to avoid FreeRTOS QEMU deadlocks. Phase 87.6:
475 // names are passed at register-time (buffers live on the C++
476 // `nros::ActionServer<A>` class, not in the runtime struct).
477 // Phase 189.M3.3.c — `sched_context` binds the action's (arena-registered)
478 // goal-service handle to a scheduling context. UNSET ⇒ 0 (inherit, no-op).
479 uint8_t sched = (options.sched_context == SCHED_CONTEXT_UNSET)
480 ? 0u
481 : static_cast<uint8_t>(options.sched_context);
482 ret = nros_cpp_action_server_register(out.storage_, executor_handle_, action_name, A::TYPE_NAME,
483 A::Goal::TYPE_HASH, sched);
484 if (ret == 0) {
485 // Phase 87.6: copy action_name into the C++-owned buffer for
486 // `get_action_name()` accessor.
487 size_t name_len = 0;
488 while (action_name[name_len] != '\0' && name_len + 1 < sizeof(out.action_name_)) {
489 out.action_name_[name_len] = action_name[name_len];
490 ++name_len;
491 }
492 out.action_name_[name_len] = '\0';
493 out.executor_ = executor_handle_;
494 out.initialized_ = true;
495 }
496 return Result(ret);
497}
498
499} // namespace nros
500
501#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:37
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:39
nros_cpp_ret_t nros_cpp_action_server_set_accepted_callback(void *handle, nros_cpp_accepted_callback_t accepted_cb, void *ctx)
void(* nros_cpp_accepted_callback_t)(const uint8_t goal_id[16], void *ctx)
Definition action_server.hpp:44
Definition action_server.hpp:125
void(*)(const uint8_t uuid[16]) TypedAcceptedFn
User-facing typed accepted-goal callback signature (issue 0796).
Definition action_server.hpp:141
ActionServer()
Definition action_server.hpp:364
Result for_each_active_goal(F f)
Definition action_server.hpp:288
bool is_valid() const
Check if the action server is initialized and valid.
Definition action_server.hpp:306
ActionServer & operator=(ActionServer &&other)
Definition action_server.hpp:336
typename A::Goal GoalType
Definition action_server.hpp:127
Result complete_goal(const uint8_t goal_id[16], GoalStatus status, const ResultType &result)
Definition action_server.hpp:261
void(*)(const uint8_t uuid[16], GoalStatus status) TypedVisitorFn
User-facing visitor signature for for_each_active_goal.
Definition action_server.hpp:145
Result set_goal_callback(F f)
Definition action_server.hpp:153
void(*)(const uint8_t uuid[16], void *ctx) TypedAcceptedFnWithCtx
User-facing typed accepted-goal callback signature with user context.
Definition action_server.hpp:143
Result set_cancel_callback(F f)
Definition action_server.hpp:179
Result set_cancel_callback_with_ctx(TypedCancelFnWithCtx f, void *ctx)
Definition action_server.hpp:193
~ActionServer()
Destructor — releases action server resources.
Definition action_server.hpp:309
Result publish_feedback(const uint8_t goal_id[16], const FeedbackType &feedback)
Definition action_server.hpp:241
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:139
Result set_goal_callback_with_ctx(TypedGoalFnWithCtx f, void *ctx)
Definition action_server.hpp:168
typename A::Feedback FeedbackType
Definition action_server.hpp:129
ActionServer(ActionServer &&other)
Definition action_server.hpp:321
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:135
GoalResponse(*)(const uint8_t uuid[16], const GoalType &goal) TypedGoalFn
User-facing typed goal callback signature.
Definition action_server.hpp:132
CancelResponse(*)(const uint8_t uuid[16]) TypedCancelFn
User-facing typed cancel callback signature.
Definition action_server.hpp:137
Result complete_goal(const uint8_t goal_id[16], const ResultType &result)
Definition action_server.hpp:276
Result set_accepted_callback(F f)
Definition action_server.hpp:216
typename A::Result ResultType
Definition action_server.hpp:128
Result set_accepted_callback_with_ctx(TypedAcceptedFnWithCtx f, void *ctx)
Definition action_server.hpp:228
Definition node.hpp:211
Result create_action_server(ActionServer< A > &out, const char *action_name, const QoS &qos=QoS::services(), const ActionServerOptions &options={})
Definition action_server.hpp:466
Definition qos.hpp:173
Definition result.hpp:90
Inline storage-size macros for opaque entity buffers.
int nros_cpp_ret_t
Definition future.hpp:21
Definition nros.hpp:55
GoalResponse
Goal acceptance response returned from the user's goal callback.
Definition action_server.hpp:57
CancelResponse
Definition action_server.hpp:71
GoalStatus
Definition action_server.hpp:92
@ Error
Generic failure not covered by a more specific code.
CancelReturnCode
Definition action_server.hpp:83
nros::Node and global session helpers.
nros::Result, nros::ErrorCode, and the NROS_TRY macro.
Definition qos.hpp:54