VLink  2.0.0
A high-performance communication middleware
bag_plugin_interface.h
Go to the documentation of this file.
1 /*
2  * Copyright (C) 2026 by Thun Lu. All rights reserved.
3  * Author: Thun Lu <thun.lu@zohomail.cn>
4  * Repo: https://github.com/thun-res/vlink
5  * _ __ __ _ __
6  * | | / / / / (_) ____ / /__
7  * | | / / / / / / / __ \ / //_/
8  * | |/ / / /___ / / / / / / / ,<
9  * |___/ /_____/ /_/ /_/ /_/ /_/|_|
10  *
11  * Licensed under the Apache License, Version 2.0 (the "License");
12  * you may not use this file except in compliance with the License.
13  * You may obtain a copy of the License at
14  *
15  * http://www.apache.org/licenses/LICENSE-2.0
16  *
17  * Unless required by applicable law or agreed to in writing, software
18  * distributed under the License is distributed on an "AS IS" BASIS,
19  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20  * See the License for the specific language governing permissions and
21  * limitations under the License.
22  */
23 
24 /**
25  * @file bag_plugin_interface.h
26  * @brief Unified plugin contract for rewriting bag traffic on both playback (read) and
27  * recording (write).
28  *
29  * @details
30  * @c BagPluginInterface is a dynamic plugin loaded through the VLink @c Plugin framework and
31  * attached either to a @c BagReader (via @c BagReader::bind_plugin_interface()) or to a
32  * @c BagWriter (via @c BagWriter::bind_plugin_interface()). A single class serves both
33  * directions; an implementation discovers which side it is bound to through
34  * @c get_direction() and overrides only the hooks for that side.
35  *
36  * The two directions are @b symmetric: each is a frame-forwarding pipeline. The host supplies a
37  * downstream sink at bind time and the plugin re-emits every @c Frame -- optionally transformed,
38  * dropped, fanned out, or @e reordered -- through that sink. Plugins never touch the raw sink
39  * member directly; they emit by calling the single @c do_callback() helper. Read and write share
40  * one callable type (@c Callback, a @c const @c Frame& sink); a plugin overrides the @c on_read() or
41  * @c on_write() hook for its bound direction.
42  *
43  * - @b Read (playback). Frames originate inside the reader and flow @e out to the user.
44  * @c on_read() receives each frame and re-emits it via @c do_callback(). URL/type remapping is a
45  * separate, once-per-URL hook, @c convert_url_meta(), applied when the bag is opened. On the read
46  * side @c Frame::ser_type / @c schema_type are empty (query @c BagReader::get_ser_type() if needed).
47  *
48  * - @b Write (recording). Frames originate from the caller and flow @e into the bag. @c on_write()
49  * receives each frame -- fully populated with @c ser_type / @c schema_type because recording
50  * persists them -- and re-emits it via @c do_callback().
51  *
52  * Both hooks may forward unchanged, transcode (e.g. record a raw image as compressed JPEG by emitting
53  * new @c Frame::data plus a new @c ser_type / @c schema_type), drop a frame (by not emitting), fan a
54  * frame out into several, or buffer frames and emit them @e reordered by their true data-plane time --
55  * a sliding-window reorder, identical on both sides, typically built on a @c BagProcessor.
56  *
57  * Plugin contract:
58  *
59  * | Hook | Dir | Purpose |
60  * | ------------------- | ----- | ------------------------------------------------------------- |
61  * | get_version_info() | both | After load: return a populated VersionInfo |
62  * | bind_direction() | both | At bind time: stored, observable via get_direction() |
63  * | register_callback() | both | At bind time: store the forwarding sink |
64  * | convert_url_meta() | read | Once per URL at open: true = keep, false = drop |
65  * | on_read() | read | Every replayed frame: re-emit via do_callback() |
66  * | on_write() | write | Every frame before persist: re-emit via do_callback() |
67  * | do_callback() | both | Forward one frame to the sink (drop = not call) |
68  *
69  * Lifecycle:
70  *
71  * @verbatim
72  * read : load .so -> bind_direction(kRead) -> register_callback -> convert_url_meta (per URL)
73  * -> on_read (per frame) -> do_callback -> callback_ -> user
74  * write : load .so -> bind_direction(kWrite) -> register_callback
75  * -> on_write (per frame) -> do_callback -> callback_ -> writer persists
76  * @endverbatim
77  *
78  * @par Read-side example (rename a topic on replay)
79  * @code
80  * class MyReadPlugin : public vlink::BagPluginInterface {
81  * public:
82  * VersionInfo get_version_info() const override { return {"my-read", "1.0.0", __DATE__, "", ""}; }
83  *
84  * bool convert_url_meta(std::string& url, std::string& ser_type,
85  * vlink::SchemaType& schema_type) override {
86  * if (url.rfind("dds://legacy/", 0) == 0) { url.replace(0, 13, "dds://v2/"); }
87  * (void)ser_type;
88  * (void)schema_type;
89  * return true;
90  * }
91  *
92  * void on_read(const vlink::Frame& frame) override {
93  * do_callback(frame); // forward downstream (drop by not calling)
94  * }
95  * };
96  * VLINK_PLUGIN_DECLARE(MyReadPlugin, 1, 0)
97  * @endcode
98  *
99  * @par Write-side example (sliding-window reorder by true data-plane time before persist)
100  * @code
101  * class MyWritePlugin : public vlink::BagPluginInterface {
102  * public:
103  * MyWritePlugin() {
104  * processor_.register_output_callback([this](const vlink::Frame& frame) { do_callback(frame); });
105  * }
106  *
107  * VersionInfo get_version_info() const override { return {"my-write", "1.0.0", __DATE__, "", ""}; }
108  *
109  * void on_write(const vlink::Frame& frame) override {
110  * const int64_t data_timestamp = parse_header_time(frame.data); // plugin extracts the data-plane time
111  * processor_.push(data_timestamp, frame); // reorder by data_timestamp; emit via
112  * do_callback()
113  * }
114  *
115  * void flush() override { processor_.flush(); } // drain the buffered tail at teardown
116  *
117  * private:
118  * vlink::BagProcessor processor_;
119  * };
120  * VLINK_PLUGIN_DECLARE(MyWritePlugin, 1, 0)
121  * @endcode
122  */
123 
124 #pragma once
125 
126 #include <cstdint>
127 #include <string>
128 #include <utility>
129 
130 #include "../base/functional.h"
131 #include "../base/plugin.h"
132 #include "../impl/types.h"
133 
134 namespace vlink {
135 
136 /**
137  * @class BagPluginInterface
138  * @brief Abstract plugin base shared by bag playback and bag recording.
139  *
140  * @details
141  * The host binds an instance through @c BagReader::bind_plugin_interface() or
142  * @c BagWriter::bind_plugin_interface(). At bind time the host calls @c bind_direction() to
143  * record which side the plugin serves and @c register_callback() to supply the forwarding sink.
144  * Every hook carries a default implementation, so an implementation overrides only the hooks
145  * relevant to its direction (plus the mandatory @c get_version_info()). Implementations are
146  * expected to be thread-compatible with the host's loop thread.
147  */
150 
151  protected:
152  BagPluginInterface() = default;
153 
154  virtual ~BagPluginInterface() = default;
155 
156  public:
157  /**
158  * @brief Identifies whether the plugin is bound to a reader or a writer.
159  */
160  enum Direction : uint8_t {
161  kRead = 0, ///< Bound to a @c BagReader; the plugin forwards replayed frames.
162  kWrite = 1, ///< Bound to a @c BagWriter; the plugin forwards frames before they are persisted.
163  };
164 
165  /**
166  * @struct VersionInfo
167  * @brief Build identity returned by @c get_version_info().
168  */
169  struct VersionInfo final {
170  std::string name; ///< Human-readable plugin display name.
171  std::string version; ///< Semantic version (e.g. @c "1.2.3").
172  std::string timestamp; ///< Build timestamp string.
173  std::string tag; ///< Source-control tag or label.
174  std::string commit_id; ///< Source-control commit hash.
175  };
176 
177  /**
178  * @brief Forwarding sink used by @c do_callback() to re-emit a frame downstream.
179  *
180  * @details
181  * Supplied by the host (@c BagReader or @c BagWriter) at bind time and stored internally. A single
182  * @c const @c Frame& sink serves both directions: read plugins re-emit toward playback, write plugins
183  * toward persistence.
184  */
186 
187  /**
188  * @brief Returns the build identity used by the host for logging and diagnostics.
189  *
190  * @return Populated @c VersionInfo describing this plugin.
191  */
192  [[nodiscard]] virtual VersionInfo get_version_info() const = 0;
193 
194  /**
195  * @brief Records the binding direction so the plugin can branch on read vs write.
196  *
197  * @details
198  * Invoked by the host at attach time before any other hook. The value is observable from
199  * the hooks through @c get_direction().
200  *
201  * @param direction Side the plugin is being bound to.
202  */
203  void bind_direction(Direction direction);
204 
205  /**
206  * @brief Returns the side this plugin is currently bound to.
207  *
208  * @return @c Direction::kRead when bound to a reader, @c Direction::kWrite when bound to a writer.
209  */
210  [[nodiscard]] Direction get_direction() const;
211 
212  /**
213  * @brief Stores the forwarding sink used by @c do_callback().
214  *
215  * @details
216  * Invoked by the host's @c bind_plugin_interface() at attach time. The plugin keeps @p callback in
217  * @c callback_ and calls it from @c do_callback() to deliver a frame downstream -- toward the user's
218  * playback callback on the read side, or toward persistence on the write side. Cleared (with an
219  * empty callable) on rebind and at host teardown, so a plugin-owned worker thread cannot reach a
220  * destroyed host.
221  *
222  * @param callback Sink that forwards a frame downstream.
223  */
224  void register_callback(Callback&& callback);
225 
226  /**
227  * @brief Rewrites or filters a stored URL before playback begins (read side).
228  *
229  * @details
230  * Called once per URL contained in the bag when the reader opens the file. Implementations
231  * may modify any of the three parameters in place to remap topics or override schema
232  * metadata. The default implementation keeps every URL unchanged.
233  *
234  * @param url URL string; may be modified in place.
235  * @param ser_type Serialisation type; may be modified in place.
236  * @param schema_type Coarse schema family; may be modified in place.
237  * @return @c true to retain the URL in playback; @c false to exclude it.
238  */
239  virtual bool convert_url_meta(std::string& url, std::string& ser_type, SchemaType& schema_type);
240 
241  /**
242  * @brief Intercepts a replayed frame on its way to the user (read side).
243  *
244  * @details
245  * Called for every replayed frame after timing pacing. Forward it downstream by calling
246  * @c do_callback(); transforming the payload, dropping the frame (by not emitting), fanning it out
247  * (emitting several), or buffering and re-emitting frames @e reordered by data-plane time (e.g. via
248  * @c BagProcessor) is permitted. The default implementation forwards the frame unchanged.
249  *
250  * @note @p frame::ser_type / @c schema_type are empty on the read side; query
251  * @c BagReader::get_ser_type() / @c get_schema_type() (or stash them from @c convert_url_meta())
252  * if needed. The payload is a shallow view valid for the duration of the call; copy it before
253  * buffering for asynchronous emit.
254  *
255  * @param frame Replayed frame.
256  */
257  virtual void on_read(const Frame& frame);
258 
259  /**
260  * @brief Intercepts a frame before it is persisted (write side).
261  *
262  * @details
263  * Called for every frame handed to the writer, before it is recorded. Re-emit it by calling
264  * @c do_callback(). Transcoding (rewrite @c frame.data plus @c frame.ser_type / @c schema_type, e.g.
265  * raw image to JPEG), dropping the frame (by not emitting), fanning it out, or buffering and
266  * re-emitting frames @e reordered by data-plane time (a sliding-window reorder, e.g. via
267  * @c BagProcessor) is permitted. The default implementation forwards the frame unchanged.
268  *
269  * @note The payload is a view valid for the duration of the call; a plugin that emits
270  * asynchronously must copy it before buffering.
271  *
272  * @note When a plugin renames the URL, the recorder learns the source-to-recorded mapping only for
273  * @e synchronous emits (within this @c on_write() call) and only when the rewrite is one-to-one,
274  * so URL-level metadata such as loss stays correctly attributed. A plugin that both renames
275  * and emits asynchronously is responsible for any loss attribution itself.
276  *
277  * @note @c BagWriter::push() resolves a negative @c Frame::timestamp to the writer clock @e before
278  * calling this hook; that auto-assignment does @b not re-run on the frames a plugin emits. A
279  * re-emitted frame is persisted with its own @c Frame::timestamp verbatim, so a plugin that
280  * constructs a fresh frame must set a resolved (non-negative) timestamp on it.
281  *
282  * @param frame Frame to persist.
283  */
284  virtual void on_write(const Frame& frame);
285 
286  /**
287  * @brief Drains any internally-buffered frames downstream before the host unbinds and tears down.
288  *
289  * @details
290  * Called by the host on its own thread, while its sink is still valid, right before it detaches this
291  * plugin (read side: at reader teardown; write side: at writer teardown, before the recording loop is
292  * stopped). A plugin that buffers frames for asynchronous re-emit (e.g. a @c BagProcessor reorder
293  * buffer) must override this to flush those frames synchronously -- typically @c processor_.flush() --
294  * so a buffered tail is recorded / replayed instead of dropped. The default implementation is a no-op
295  * (a synchronous plugin holds nothing back). After @c flush() returns the host stops delivering this
296  * plugin's emitted frames, so any frame produced afterwards is ignored.
297  */
298  virtual void flush();
299 
300  /**
301  * @brief Forwards one frame downstream through the registered sink.
302  *
303  * @details
304  * The emit helper a plugin calls -- typically from @c on_read() / @c on_write() or from a reorder
305  * buffer's output callback -- to deliver a frame downstream without touching @c callback_ directly.
306  * Invokes @c callback_ when one is registered and is otherwise a no-op.
307  *
308  * @param frame Frame to forward to the sink.
309  */
310  void do_callback(const Frame& frame);
311 
312  protected:
313  Direction direction_{Direction::kRead};
314 
315  private:
316  Callback callback_;
317 
319 };
320 
321 ////////////////////////////////////////////////////////////////
322 /// Details
323 ////////////////////////////////////////////////////////////////
324 
325 inline void BagPluginInterface::bind_direction(Direction direction) { direction_ = direction; }
326 
328 
329 inline void BagPluginInterface::register_callback(Callback&& callback) { callback_ = std::move(callback); }
330 
331 inline bool BagPluginInterface::convert_url_meta(std::string& url, std::string& ser_type,
332  SchemaType& schema_type) { // LCOV_EXCL_LINE GCOVR_EXCL_LINE
333  (void)url;
334  (void)ser_type;
335  (void)schema_type;
336 
337  return true; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
338 }
339 
340 inline void BagPluginInterface::on_read(const Frame& frame) { do_callback(frame); }
341 
342 inline void BagPluginInterface::on_write(const Frame& frame) { do_callback(frame); }
343 
345 
346 inline void BagPluginInterface::do_callback(const Frame& frame) {
347  if (callback_) {
348  callback_(frame);
349  }
350 }
351 
352 } // namespace vlink
#define VLINK_DISALLOW_COPY_AND_ASSIGN(classname)
Deletes the copy constructor and copy-assignment operator of classname.
Definition: macros.h:174
#define VLINK_PLUGIN_REGISTER(InterfaceType)
Declares a plugin's identity from the demangled name of its abstract interface.
Definition: plugin.h:345