Branch data Line data Source code
1 : : /*
2 : : * Copyright 2025 Jinwoo Sung
3 : : *
4 : : * Licensed under the Apache License, Version 2.0 (the "License");
5 : : * you may not use this file except in compliance with the License.
6 : : * You may obtain a copy of the License at
7 : : *
8 : : * http://www.apache.org/licenses/LICENSE-2.0
9 : : *
10 : : * Unless required by applicable law or agreed to in writing, software
11 : : * distributed under the License is distributed on an "AS IS" BASIS,
12 : : * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 : : * See the License for the specific language governing permissions and
14 : : * limitations under the License.
15 : : */
16 : :
17 : : #pragma once
18 : :
19 : : #include <chrono>
20 : : #include <optional>
21 : : #include <string>
22 : : #include <string_view>
23 : : #include <vector>
24 : :
25 : : #include "wirestead/base/common.hpp"
26 : : #include "wirestead/base/error_codes.hpp"
27 : : #include "wirestead/memory/safe_data_buffer.hpp"
28 : : #include "wirestead/memory/safe_span.hpp"
29 : :
30 : : namespace wirestead {
31 : : namespace wrapper {
32 : :
33 : : /**
34 : : * @brief Context for data/message related events
35 : : */
36 : : class MessageContext {
37 : : public:
38 : : /**
39 : : * @brief Construct a context that owns a copy of the payload.
40 : : *
41 : : * Use this whenever the context outlives the callback that produced the
42 : : * data — the batch queues hold contexts until the batch is flushed, so they
43 : : * must own.
44 : : */
45 : 152 : MessageContext(ClientId client_id, memory::SafeDataBuffer data, std::string client_info = "")
46 : 152 : : client_id_(client_id), owned_(std::move(data)), client_info_(std::move(client_info)) {}
47 : :
48 : : /**
49 : : * @brief Construct a context that borrows the payload without copying it.
50 : : *
51 : : * `data` must stay valid for the whole lifetime of this MessageContext. That
52 : : * holds for single-shot on_data()/on_message() dispatch, where the context is
53 : : * a temporary destroyed before the transport reuses its receive buffer, so
54 : : * those paths cost no per-chunk allocation at all. Do not use this
55 : : * constructor for a context that is queued or otherwise outlives the
56 : : * callback — use the SafeDataBuffer constructor above for those.
57 : : */
58 : 3645 : MessageContext(ClientId client_id, memory::ConstByteSpan data, std::string client_info = "")
59 : 3645 : : client_id_(client_id), view_(data), client_info_(std::move(client_info)) {}
60 : :
61 : : // A copy always owns its payload, even when the source only borrows. The
62 : : // callback receives a `const MessageContext&`, so copying is the only way a
63 : : // caller can keep a context past the callback, and that is exactly when the
64 : : // borrowed bytes stop being valid. Doing this in the copy costs the hot path
65 : : // nothing: dispatch passes the context by reference and never copies it.
66 : 3 : MessageContext(const MessageContext& other)
67 : 3 : : client_id_(other.client_id_),
68 : 3 : owned_(other.cloned_payload()),
69 : 3 : client_info_(other.client_info_),
70 : 6 : received_at_(other.received_at_) {}
71 : :
72 : 2 : MessageContext& operator=(const MessageContext& other) {
73 [ + - ]: 2 : if (this != &other) {
74 : 2 : client_id_ = other.client_id_;
75 : 2 : owned_ = other.cloned_payload();
76 : 2 : view_ = {};
77 : 2 : client_info_ = other.client_info_;
78 : 2 : received_at_ = other.received_at_;
79 : : }
80 : 2 : return *this;
81 : : }
82 : :
83 : : // Moves stay cheap and noexcept so the batch queues keep using them on
84 : : // vector reallocation instead of falling back to the copy above.
85 : 286 : MessageContext(MessageContext&&) noexcept = default;
86 : : MessageContext& operator=(MessageContext&&) noexcept = default;
87 : 4086 : ~MessageContext() = default;
88 : :
89 : : /** @brief Get the client identifier */
90 : 9 : ClientId client_id() const { return client_id_; }
91 : :
92 : : /**
93 : : * @brief Get the received data as a view.
94 : : *
95 : : * The returned view is only valid for the lifetime of this MessageContext.
96 : : * Do not store it past the callback's return — use data_as_string() instead.
97 : : */
98 : 1509 : std::string_view data() const {
99 : 1509 : const auto size = payload_size();
100 [ + + ]: 1509 : if (size == 0) return {};
101 : 1507 : return std::string_view(reinterpret_cast<const char*>(payload_data()), size);
102 : : }
103 : :
104 : : /**
105 : : * @brief Get the received data as a SafeDataBuffer reference.
106 : : *
107 : : * When this context borrows its payload (the single-shot callback paths),
108 : : * the buffer is materialized on first use and cached, so this accessor is
109 : : * the one place that still pays for a copy. Prefer data(), data_as_string(),
110 : : * or data_as_vector(). Materialization is not synchronized; like every other
111 : : * accessor here it assumes the single callback thread the context is
112 : : * delivered on (see docs/callbacks.md).
113 : : */
114 : 7 : const memory::SafeDataBuffer& safe_data() const {
115 [ + + ]: 7 : if (!owned_) owned_.emplace(view_);
116 : 7 : return *owned_;
117 : : }
118 : :
119 : : /** @brief Get the received data as a std::string */
120 : 1055 : std::string data_as_string() const {
121 : 1055 : const auto size = payload_size();
122 [ + + ]: 1055 : if (size == 0) return {};
123 : 2106 : return std::string(reinterpret_cast<const char*>(payload_data()), size);
124 : : }
125 : :
126 : : /** @brief Get the received data as a std::vector<uint8_t> */
127 : 49 : std::vector<uint8_t> data_as_vector() const {
128 : 49 : const auto* d = payload_data();
129 : 98 : return std::vector<uint8_t>(d, d + payload_size());
130 : : }
131 : :
132 : : /** @brief Get the client information (e.g., endpoint address) */
133 : 3 : const std::string& client_info() const { return client_info_; }
134 : :
135 : : /**
136 : : * @brief When this payload arrived, on the steady clock.
137 : : *
138 : : * Stamped where the context is constructed, which on every receive path is
139 : : * the moment the bytes came off the transport — not the moment the callback
140 : : * runs. The difference is what makes it worth having:
141 : : *
142 : : * - a batch callback delivers contexts stamped when each chunk arrived, not
143 : : * when the batch flushed, so a whole batch does not collapse onto one time;
144 : : * - a framer that finds several messages in one read gives each of them that
145 : : * read's arrival time, where a clock read inside the callback would be
146 : : * timing the parse instead.
147 : : *
148 : : * Steady, not system: stamping is exactly where a clock step would otherwise
149 : : * corrupt the data, and a difference between two steady readings survives
150 : : * one. To place it on a wall clock — a ROS header stamp, say — subtract the
151 : : * age rather than converting the epoch:
152 : : *
153 : : * ```cpp
154 : : * const auto age = std::chrono::steady_clock::now() - ctx.received_at();
155 : : * msg.header.stamp = node->now() - rclcpp::Duration(age);
156 : : * ```
157 : : */
158 : 6 : std::chrono::steady_clock::time_point received_at() const { return received_at_; }
159 : :
160 : : private:
161 [ + + ]: 2609 : const uint8_t* payload_data() const noexcept { return owned_ ? owned_->data() : view_.data(); }
162 [ + + ]: 2613 : size_t payload_size() const noexcept { return owned_ ? owned_->size() : view_.size(); }
163 : :
164 : : // Payload for a copy: reuse the owned buffer when there is one, otherwise
165 : : // take a copy of the borrowed bytes.
166 : 5 : std::optional<memory::SafeDataBuffer> cloned_payload() const {
167 [ + + ]: 5 : if (owned_) return owned_;
168 : 2 : return memory::SafeDataBuffer(view_);
169 : : }
170 : :
171 : : ClientId client_id_;
172 : : // Exactly one of these carries the payload: `owned_` when engaged, `view_`
173 : : // otherwise. `view_` is left default-constructed (and unread) on the owning
174 : : // path, which keeps the default copy/move operations correct — they never
175 : : // have to re-seat a pointer into relocated storage. `owned_` is mutable only
176 : : // so safe_data() can materialize a borrowed payload on demand.
177 : : mutable std::optional<memory::SafeDataBuffer> owned_;
178 : : memory::ConstByteSpan view_;
179 : : std::string client_info_;
180 : : // Default-initialized rather than passed in, so every construction site
181 : : // stamps arrival without threading a parameter through seven wrappers. The
182 : : // copy operations below must carry it across explicitly - a copy that let
183 : : // this initializer run again would silently re-stamp the payload with the
184 : : // time it was copied, which is precisely the error this field exists to
185 : : // prevent.
186 : : std::chrono::steady_clock::time_point received_at_{std::chrono::steady_clock::now()};
187 : : };
188 : :
189 : : /**
190 : : * @brief Context for connection/disconnection events
191 : : */
192 : : class ConnectionContext {
193 : : public:
194 : 469 : ConnectionContext(ClientId client_id, std::string client_info = "")
195 : 469 : : client_id_(client_id), client_info_(std::move(client_info)) {}
196 : :
197 : : /** @brief Get the client identifier */
198 : 3 : ClientId client_id() const { return client_id_; }
199 : :
200 : : /** @brief Get the client information (e.g., endpoint address) */
201 : : const std::string& client_info() const { return client_info_; }
202 : :
203 : : private:
204 : : ClientId client_id_;
205 : : std::string client_info_;
206 : : };
207 : :
208 : : /**
209 : : * @brief Context for error events
210 : : */
211 : : class ErrorContext {
212 : : public:
213 : 125 : ErrorContext(ErrorCode code, std::string_view message, std::optional<ClientId> client_id = std::nullopt)
214 : 375 : : code_(code), message_(message), client_id_(client_id) {}
215 : :
216 : : /** @brief Get the error code */
217 : 11 : ErrorCode code() const { return code_; }
218 : :
219 : : /** @brief Get the error message */
220 : 4 : std::string_view message() const { return message_; }
221 : :
222 : : /** @brief Get the associated client ID, if any */
223 : 5 : std::optional<ClientId> client_id() const { return client_id_; }
224 : :
225 : : private:
226 : : ErrorCode code_;
227 : : std::string message_;
228 : : std::optional<ClientId> client_id_;
229 : : };
230 : :
231 : : } // namespace wrapper
232 : : } // namespace wirestead
|