nros C++ API
Lightweight ROS 2 client for embedded real-time systems (C++ headers)
Loading...
Searching...
No Matches
future.hpp
Go to the documentation of this file.
1// nros-cpp: Future<T> -- single-shot deferred result
2// Freestanding C++ -- no exceptions, no STL required
3
9
10#ifndef NROS_RCLCPP_FUTURE_HPP
11#define NROS_RCLCPP_FUTURE_HPP
12
13#include <cstdint>
14#include <cstddef>
15
16#include "rclcpp/result.hpp"
17#include "rclcpp/size_bound.hpp" // rclcpp::rx_buffer_capacity<T> — the receive-buffer size
18// issue 1996 — `refuse<T>`, `say_refused` and the `NROS_RCLCPP_REFUSE_FUTURE_GET*`
19// messages; it also brings `std_detect.hpp`, which defines the capability
20// macros `get()` and `wait_for()` are gated on.
21#include "rclcpp/logging.hpp"
22
23// FFI declarations
24extern "C" {
25typedef int nros_cpp_ret_t;
26nros_cpp_ret_t nros_cpp_spin_once(void* handle, int32_t timeout_ms);
29uint64_t nros_cpp_time_ns(void);
30}
31
32// phase-428 (RFC-0089): the entity types that reach into this one's private
33// constructor are DEFINED in the upstream namespace now, so the friend
34// declarations below name them there. A qualified friend cannot introduce a
35// name, hence these declarations.
36namespace rclcpp {
37template <typename M> class Subscription;
38} // namespace rclcpp
39
40namespace rclcpp_action {
41template <typename A> class Client;
42} // namespace rclcpp_action
43
44namespace rclcpp {
45
46// phase-456 W9 — `send_request` and the `Future` it hands back moved off
47// `rclcpp::Client<S>` onto `rclcpp::PollClient<S>`, the half that owns the
48// `RmwServiceClient` this future takes from. The friend list FOLLOWS the method:
49// the poll client is declared here, and `rclcpp::Client<S>` left both this
50// declaration and the friend list below, because a friendship nothing uses is a
51// claim about the code that is not true.
52template <typename S> class PollClient;
53
54namespace detail {
55
59constexpr int64_t SEQUENCE_ID_UNKNOWN = (-9223372036854775807LL - 1);
60
63constexpr int MAX_STALE_DRAIN_PER_POLL = 8;
64
67using TakeSeqFn = nros_cpp_ret_t (*)(void*, uint8_t*, size_t, size_t*, int64_t*);
68
75using PumpFn = nros_cpp_ret_t (*)(void*);
76
81struct PendingList;
83 PendingLink* next = nullptr;
86 PendingList* list = nullptr;
89 void* storage = nullptr;
95 bool by_goal = false;
96 uint8_t goal_id[16] = {};
97 uint8_t* buf = nullptr;
98 size_t cap = 0;
99 size_t len = 0;
100 bool ready = false;
102 bool orphaned = false;
103};
104
127 PendingLink* head = nullptr;
128 uint8_t orphans = 0;
135 void (*forget)(void* ctx, const uint8_t goal[16]) = nullptr;
136 void* forget_ctx = nullptr;
137
138 size_t count() const {
139 size_t n = 0;
140 for (PendingLink* l = head; l != nullptr; l = l->next)
141 ++n;
142 return n;
143 }
144
145 bool can_send() const {
146 for (PendingLink* l = head; l != nullptr; l = l->next)
147 if (l->seq == SEQUENCE_ID_UNKNOWN) return false;
148 return true;
149 }
150
151 void link(PendingLink* l) {
152 l->next = head;
153 head = l;
154 }
155
158 bool unlink(PendingLink* l, bool abandoned) {
159 for (PendingLink** pp = &head; *pp != nullptr; pp = &(*pp)->next) {
160 if (*pp == l) {
161 *pp = l->next;
162 l->next = nullptr;
163 // A goal-keyed (action) future names its reply exactly, so its
164 // removal costs no later reply (issue 2011).
165 if (abandoned && !l->by_goal && l->seq == SEQUENCE_ID_UNKNOWN && orphans < 255)
166 ++orphans;
167 // Issue 2027 — an abandoned goal-keyed future was the owner of
168 // one request in the Rust core's bounded table; tell the list's
169 // owner, so the slot does not outlive the last owner.
170 if (abandoned && l->by_goal && forget != nullptr) forget(forget_ctx, l->goal_id);
171 return true;
172 }
173 }
174 return false;
175 }
176
178 void relink(PendingLink* from, PendingLink* to) {
179 for (PendingLink** pp = &head; *pp != nullptr; pp = &(*pp)->next) {
180 if (*pp == from) {
181 to->next = from->next;
182 *pp = to;
183 from->next = nullptr;
184 return;
185 }
186 }
187 }
188
191 PendingLink* resolve(int64_t seq) {
192 if (seq == SEQUENCE_ID_UNKNOWN) {
193 if (orphans > 0) {
194 --orphans;
195 return nullptr;
196 }
197 return (head != nullptr && head->next == nullptr) ? head : nullptr;
198 }
199 for (PendingLink* l = head; l != nullptr; l = l->next)
200 if (l->seq == seq) return l;
201 // The backend gave the REPLY an id but not the request: the sole
202 // pending request is the only candidate.
203 if (head != nullptr && head->next == nullptr && head->seq == SEQUENCE_ID_UNKNOWN)
204 return head;
205 return nullptr;
206 }
207
211 PendingLink* resolve_goal(const uint8_t goal[16]) {
212 for (PendingLink* l = head; l != nullptr; l = l->next) {
213 if (!l->by_goal) continue;
214 bool same = true;
215 for (int i = 0; i < 16; ++i) {
216 if (l->goal_id[i] != goal[i]) {
217 same = false;
218 break;
219 }
220 }
221 if (same) return l;
222 }
223 return nullptr;
224 }
225
228 void move_to(PendingList& to, void* storage) {
229 to.head = head;
230 to.orphans = orphans;
231 for (PendingLink* l = head; l != nullptr; l = l->next) {
232 l->list = &to;
233 l->storage = storage;
234 }
235 head = nullptr;
236 orphans = 0;
237 }
238
242 size_t prune() {
243 size_t n = 0;
244 while (head != nullptr) {
245 PendingLink* l = head;
246 (void)unlink(l, true);
247 l->list = nullptr;
248 l->orphaned = true;
249 ++n;
250 }
251 return n;
252 }
253
259 bool remove_seq(int64_t seq) {
260 if (seq == SEQUENCE_ID_UNKNOWN) return false;
261 for (PendingLink* l = head; l != nullptr; l = l->next) {
262 if (l->by_goal || l->seq != seq) continue;
263 (void)unlink(l, true);
264 l->list = nullptr;
265 l->orphaned = true;
266 return true;
267 }
268 return false;
269 }
270
273 void orphan_all() {
274 PendingLink* l = head;
275 while (l != nullptr) {
276 PendingLink* next = l->next;
277 l->next = nullptr;
278 l->list = nullptr;
279 l->storage = nullptr;
280 l->orphaned = true;
281 l = next;
282 }
283 head = nullptr;
284 orphans = 0;
285 }
286
288 void deliver(PendingLink* owner, const uint8_t* data, size_t len) {
289 const size_t n = len < owner->cap ? len : owner->cap;
290 if (owner->buf != data) {
291 for (size_t i = 0; i < n; ++i)
292 owner->buf[i] = data[i];
293 }
294 owner->len = n;
295 owner->ready = true;
296 (void)unlink(owner, false);
297 }
298};
299
300} // namespace detail
301
337template <typename T, size_t Cap = ::rclcpp::rx_buffer_capacity<T>::value> class Future {
338 public:
345 bool is_ready() {
346 if (!live()) return false;
347 if (link_.ready) return true;
348 if (take_seq_fn_ != nullptr) return poll_keyed();
349 if (pump_fn_ != nullptr) return poll_pumped();
350 if (!take_fn_) return false;
351 size_t len = 0;
352 nros_cpp_ret_t ret = take_fn_(link_.storage, cached_buf_, sizeof(cached_buf_), &len);
353 if (ret == 0 && len > 0) {
354 link_.ready = true;
355 link_.len = len < sizeof(cached_buf_) ? len : sizeof(cached_buf_);
356 return true;
357 }
358 return false;
359 }
360
365 Result try_take(T& out) {
366 if (!live()) return Result(ErrorCode::Error);
367 if (!link_.ready && !is_ready()) return Result(ErrorCode::Error);
368 slot_ = -1; // consume
369 if (T::ffi_deserialize(cached_buf_, link_.len, &out) != 0) {
370 return Result(ErrorCode::Error);
371 }
372 return Result::success();
373 }
374
386 Result wait(void* executor_handle, uint32_t timeout_ms, T& out, uint32_t poll_ms = 10) {
387 if (!live()) return Result(ErrorCode::Error);
388 if (poll_ms == 0) poll_ms = 1;
389 // Phase 89.2: budget by wall-clock. Accumulating `step` per iteration
390 // breaks when the underlying `zpico_spin_once` returns early on a
391 // signaled condvar (keep-alives, discovery gossip) — the 500-step
392 // default-timeout loop can then collapse into milliseconds and
393 // return ErrorCode::Timeout before the reply has a chance to land.
394 const uint64_t start_ns = nros_cpp_time_ns();
395 const uint64_t budget_ns = static_cast<uint64_t>(timeout_ms) * 1000000ULL;
396 while (true) {
397 nros_cpp_ret_t ret = nros_cpp_spin_once(executor_handle, static_cast<int32_t>(poll_ms));
398 // Transient conditions: keep polling. Anything else propagates.
399 // - Ok (0): nothing to dispatch this round.
400 // - Timeout (-2): spin_once returned after its timeout — normal.
401 // - TryAgain (-6): transport hint to retry.
402 if (ret != 0 && ret != static_cast<nros_cpp_ret_t>(ErrorCode::Timeout) &&
403 ret != static_cast<nros_cpp_ret_t>(ErrorCode::TryAgain)) {
404 return Result(ret);
405 }
406 if (is_ready()) return try_take(out);
407 const uint64_t now_ns = nros_cpp_time_ns();
408 if (now_ns - start_ns >= budget_ns) break;
409 }
411 }
412
416 void cancel() {
417 detach(true);
418 slot_ = -1;
419 }
420
426 bool valid() const { return live(); }
427
428#if defined(NROS_CPP_HAS_SHARED_PTR) // hosted-family: shared-ptr-interop
447 ::std::shared_ptr<T> get() {
448 if (!live() || (!link_.ready && !is_ready())) {
449 NROS_RCLCPP_SAY_REFUSED(NROS_RCLCPP_REFUSE_FUTURE_GET_NOT_READY_RUNTIME);
450 return ::std::shared_ptr<T>();
451 }
452 ::std::shared_ptr<T> out = ::std::make_shared<T>();
453 if (!try_take(*out).ok()) return ::std::shared_ptr<T>();
454 return out;
455 }
456#else
459 template <typename U = void> void get() {
460 static_assert(::rclcpp::detail::refuse<U>::value, NROS_RCLCPP_REFUSE_FUTURE_GET);
461 }
462#endif // NROS_CPP_HAS_SHARED_PTR
463
464#if defined(NROS_CPP_HAS_STD_FUTURE) // hosted-family: future-interop
480 template <typename Rep, typename Period>
481 ::std::future_status wait_for(::std::chrono::duration<Rep, Period> timeout) {
482 if (!live()) return ::std::future_status::timeout;
483 if (link_.ready || is_ready()) return ::std::future_status::ready;
484 const int64_t ms = ::rclcpp::detail::chrono_wait_budget_ms(timeout);
485 if (ms <= 0 || executor_ == nullptr) {
486 if (executor_ != nullptr) (void)nros_cpp_spin_once(executor_, 0);
487 return is_ready() ? ::std::future_status::ready : ::std::future_status::timeout;
488 }
489 const uint64_t start_ns = nros_cpp_time_ns();
490 const uint64_t budget_ns = static_cast<uint64_t>(ms) * 1000000ULL;
491 while (true) {
492 const uint64_t elapsed_ns = nros_cpp_time_ns() - start_ns;
493 if (elapsed_ns >= budget_ns) break;
494 const uint64_t left_ms = (budget_ns - elapsed_ns + 999999ULL) / 1000000ULL;
495 const int32_t slice = left_ms < 10ULL ? static_cast<int32_t>(left_ms) : 10;
496 (void)nros_cpp_spin_once(executor_, slice);
497 if (is_ready()) return ::std::future_status::ready;
498 }
499 return is_ready() ? ::std::future_status::ready : ::std::future_status::timeout;
500 }
501#endif // NROS_CPP_HAS_STD_FUTURE
502
504 bool is_consumed() const { return !live(); }
505
506 // Move semantics (non-copyable, single-shot). A linked future's list entry
507 // FOLLOWS it to the new address (issue 2009), so a future may be returned
508 // by value and stored wherever the caller likes while it is in flight.
509 Future(Future&& other) noexcept
510 : take_fn_(other.take_fn_), take_seq_fn_(other.take_seq_fn_), pump_fn_(other.pump_fn_),
511 executor_(other.executor_), slot_(other.slot_) {
512 adopt(other);
513 }
514
515 Future& operator=(Future&& other) noexcept {
516 if (this != &other) {
517 cancel();
518 take_fn_ = other.take_fn_;
519 take_seq_fn_ = other.take_seq_fn_;
520 pump_fn_ = other.pump_fn_;
521 executor_ = other.executor_;
522 slot_ = other.slot_;
523 adopt(other);
524 }
525 return *this;
526 }
527
529
532 : take_fn_(nullptr), take_seq_fn_(nullptr), pump_fn_(nullptr), executor_(nullptr),
533 slot_(-1) {
534 link_.buf = cached_buf_;
535 link_.cap = sizeof(cached_buf_);
536 }
537
538 private:
539 Future(const Future&) = delete;
540 Future& operator=(const Future&) = delete;
541
542 template <typename S> friend class ::rclcpp::PollClient;
543 template <typename A> friend class ::rclcpp_action::Client;
544
545 using TryRecvFn = nros_cpp_ret_t (*)(void*, uint8_t*, size_t, size_t*);
546
547 // `executor` is what `wait_for` drives (issue 1996); nullptr = no
548 // executor known, and `wait_for` then checks once instead of waiting.
549 // An UNKEYED future: the take hook carries no request id, so the future
550 // trusts whatever reply it takes. Nothing in-tree builds one any more —
551 // the action futures are goal-keyed since issue 2011 — but the shape stays
552 // for a caller with exactly one request in flight.
553 Future(void* storage, TryRecvFn fn, int slot, void* executor = nullptr)
554 : take_fn_(fn), take_seq_fn_(nullptr), pump_fn_(nullptr), executor_(executor), slot_(slot) {
555 link_.buf = cached_buf_;
556 link_.cap = sizeof(cached_buf_);
557 link_.storage = storage;
558 }
559
560 // Issue 2009 — a KEYED future: the request went out as `seq`, and the
561 // future joins `list`, the pending list of the client it was sent on.
562 Future(void* storage, ::rclcpp::detail::TakeSeqFn fn, int64_t seq,
563 ::rclcpp::detail::PendingList* list, void* executor)
564 : take_fn_(nullptr), take_seq_fn_(fn), pump_fn_(nullptr), executor_(executor), slot_(0) {
565 link_.buf = cached_buf_;
566 link_.cap = sizeof(cached_buf_);
567 link_.storage = storage;
568 link_.seq = seq;
569 link_.list = list;
570 if (list != nullptr) list->link(&link_);
571 }
572
573 // Issue 2011 — a GOAL-KEYED action future: it joins `list` (the action
574 // client's acceptances or results) under `goal`. The client's callbacks
575 // deliver into it (`PendingList::resolve_goal` + `deliver`); `pump` is the
576 // client's own dispatch, for a reply no `spin_once` has taken yet.
577 Future(void* storage, ::rclcpp::detail::PumpFn pump, const uint8_t goal[16],
578 ::rclcpp::detail::PendingList* list, void* executor)
579 : take_fn_(nullptr), take_seq_fn_(nullptr), pump_fn_(pump), executor_(executor), slot_(0) {
580 link_.buf = cached_buf_;
581 link_.cap = sizeof(cached_buf_);
582 link_.storage = storage;
583 link_.by_goal = true;
584 for (int i = 0; i < 16; ++i)
585 link_.goal_id[i] = goal[i];
586 link_.list = list;
587 if (list != nullptr) list->link(&link_);
588 }
589
590 bool live() const { return slot_ >= 0 && !link_.orphaned; }
591
594 bool poll_keyed() {
595 ::rclcpp::detail::PendingList* list = link_.list;
596 if (list == nullptr) return false;
597 for (int i = 0; i < ::rclcpp::detail::MAX_STALE_DRAIN_PER_POLL; ++i) {
598 size_t len = 0;
600 nros_cpp_ret_t ret =
601 take_seq_fn_(link_.storage, cached_buf_, sizeof(cached_buf_), &len, &seq);
602 if (ret != 0) break; // TRY_AGAIN (nothing yet) or a transport error
603 ::rclcpp::detail::PendingLink* owner = list->resolve(seq);
604 if (owner != nullptr) list->deliver(owner, cached_buf_, len);
605 if (link_.ready) return true;
606 }
607 return link_.ready;
608 }
609
612 bool poll_pumped() {
613 if (link_.list == nullptr || link_.storage == nullptr) return link_.ready;
614 (void)pump_fn_(link_.storage);
615 return link_.ready;
616 }
617
620 void detach(bool abandoned) {
621 if (link_.list != nullptr && !link_.ready) (void)link_.list->unlink(&link_, abandoned);
622 link_.list = nullptr;
623 }
624
626 void adopt(Future& other) {
627 link_.buf = cached_buf_;
628 link_.cap = sizeof(cached_buf_);
629 link_.storage = other.link_.storage;
630 link_.seq = other.link_.seq;
631 link_.by_goal = other.link_.by_goal;
632 for (int i = 0; i < 16; ++i)
633 link_.goal_id[i] = other.link_.goal_id[i];
634 link_.ready = other.link_.ready;
635 link_.orphaned = other.link_.orphaned;
636 link_.len = other.link_.len;
637 link_.list = other.link_.list;
638 link_.next = nullptr;
639 if (link_.ready) {
640 for (size_t i = 0; i < link_.len; ++i)
641 cached_buf_[i] = other.cached_buf_[i];
642 }
643 if (link_.list != nullptr && !link_.ready) link_.list->relink(&other.link_, &link_);
644 other.link_.list = nullptr;
645 other.link_.ready = false;
646 other.link_.next = nullptr;
647 other.slot_ = -1;
648 }
649
650 TryRecvFn take_fn_;
651 ::rclcpp::detail::TakeSeqFn take_seq_fn_;
653 void* executor_;
654 int slot_;
657 ::rclcpp::detail::PendingLink link_;
658 uint8_t cached_buf_[Cap];
659};
660
661} // namespace rclcpp
662#endif // NROS_RCLCPP_FUTURE_HPP
Definition future.hpp:337
Future & operator=(Future &&other) noexcept
Definition future.hpp:515
Result wait(void *executor_handle, uint32_t timeout_ms, T &out, uint32_t poll_ms=10)
Definition future.hpp:386
bool valid() const
Definition future.hpp:426
Future(Future &&other) noexcept
Definition future.hpp:509
Future()
Default constructor – creates an empty/consumed future.
Definition future.hpp:531
~Future()
Definition future.hpp:528
bool is_consumed() const
Check if the future has been consumed or cancelled.
Definition future.hpp:504
void cancel()
Definition future.hpp:416
Result try_take(T &out)
Definition future.hpp:365
void get()
Definition future.hpp:459
bool is_ready()
Definition future.hpp:345
Definition node.hpp:221
bool ok() const
Returns true if the operation succeeded.
Definition result.hpp:227
static constexpr ResultOf success()
Named constructors.
Definition result.hpp:239
Definition subscription.hpp:197
Definition client.hpp:256
int nros_cpp_ret_t
Definition future.hpp:25
nros_cpp_ret_t nros_cpp_spin_once(void *handle, int32_t timeout_ms)
uint64_t nros_cpp_time_ns(void)
Definition rclcpp.hpp:418
nros_cpp_ret_t(*)(void *, uint8_t *, size_t, size_t *, int64_t *) TakeSeqFn
Definition future.hpp:67
nros_cpp_ret_t(*)(void *) PumpFn
Definition future.hpp:75
constexpr int MAX_STALE_DRAIN_PER_POLL
Definition future.hpp:63
constexpr int64_t SEQUENCE_ID_UNKNOWN
Definition future.hpp:59
Definition node.hpp:203
Definition rclcpp.hpp:75
@ Error
Generic failure not covered by a more specific code.
Definition result.hpp:118
@ Timeout
Operation deadline elapsed before completion.
Definition result.hpp:120
@ TryAgain
Transient — no data ready yet (non-blocking take). Retry later.
Definition result.hpp:148
ResultOf< void > Result
Definition result.hpp:247
**auto handle
Definition client.hpp:127
rclcpp::Result, rclcpp::ErrorCode, and the RCLCPP_TRY macro.
Definition future.hpp:126
PendingLink * resolve(int64_t seq)
Definition future.hpp:191
PendingLink * resolve_goal(const uint8_t goal[16])
Definition future.hpp:211
void orphan_all()
Definition future.hpp:273
bool remove_seq(int64_t seq)
Definition future.hpp:259
void move_to(PendingList &to, void *storage)
Definition future.hpp:228
void * forget_ctx
Definition future.hpp:136
void link(PendingLink *l)
Definition future.hpp:151
void(* forget)(void *ctx, const uint8_t goal[16])
Definition future.hpp:135
size_t count() const
Definition future.hpp:138
PendingLink * head
Definition future.hpp:127
uint8_t orphans
Definition future.hpp:128
void deliver(PendingLink *owner, const uint8_t *data, size_t len)
Hand a reply to owner, which is answered and leaves the list.
Definition future.hpp:288
size_t prune()
Definition future.hpp:242
bool unlink(PendingLink *l, bool abandoned)
Definition future.hpp:158
void relink(PendingLink *from, PendingLink *to)
A moved future: the list must point at its new address.
Definition future.hpp:178
bool can_send() const
Definition future.hpp:145
**timer cancel()