LCOV - code coverage report
Current view: top level - wirestead/wrapper - context.hpp (source / functions) Coverage Total Hit
Test: Wirestead Coverage Report Lines: 100.0 % 49 49
Test Date: 2026-08-30 10:35:09 Functions: 100.0 % 22 22
Legend: Lines: hit not hit | Branches: + taken - not taken # not executed Branches: 92.9 % 14 13

             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
        

Generated by: LCOV version 2.0-1