VLink  2.1.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_bag_interface()) or to a
32  * @c BagWriter (via @c BagWriter::bind_bag_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. The effective
46  * @c Frame::ser_type / @c schema_type metadata is populated before @c on_read() runs.
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  * | bind_direction() | both | At bind time: stored, observable via get_direction() |
62  * | register_callback() | both | At bind time: store the forwarding sink |
63  * | convert_url_meta() | read | Once per URL at open: true = keep, false = drop |
64  * | on_reset() | read | Before a playback session: discard retained session state |
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  * | flush() | both | At a completed boundary or detach: drain buffered tail |
68  * | do_callback() | both | Forward one frame to the sink (drop = not call) |
69  *
70  * @c on_read() and @c on_write() are both pure: an implementation defines both even when it serves a single
71  * direction, leaving the unused one as a trivial pass-through (@c do_callback(frame)) or an empty body.
72  *
73  * Lifecycle:
74  *
75  * @verbatim
76  * read : load .so -> bind_direction(kRead) -> register_callback -> convert_url_meta (per URL)
77  * -> on_reset -> [on_read -> do_callback -> callback_ -> user] (per frame) -> flush
78  * write : load .so -> bind_direction(kWrite) -> register_callback
79  * -> on_write (per frame) -> do_callback -> callback_ -> writer persists
80  * @endverbatim
81  *
82  * A read pass calls @c flush() only after natural completion. If the reader observes @c stop() or
83  * @c jump() before the boundary drain begins, it skips @c flush(); the next top-level session calls
84  * @c on_reset() to discard that retained tail.
85  *
86  * @par Read-side example (rename a topic on replay)
87  * @code
88  * class MyReadPlugin : public vlink::BagPluginInterface {
89  * public:
90  * bool convert_url_meta(std::string& url, std::string& ser_type,
91  * vlink::SchemaType& schema_type) override {
92  * if (url.rfind("dds://legacy/", 0) == 0) { url.replace(0, 13, "dds://v2/"); }
93  * (void)ser_type;
94  * (void)schema_type;
95  * return true;
96  * }
97  *
98  * void on_read(const vlink::Frame& frame) override {
99  * do_callback(frame); // forward downstream (drop by not calling)
100  * }
101  *
102  * void on_write(const vlink::Frame& frame) override { do_callback(frame); } // unused on the read side
103  * };
104  * VLINK_PLUGIN_DECLARE(MyReadPlugin, 2, 0)
105  * @endcode
106  *
107  * @par Write-side example (sliding-window reorder by true data-plane time before persist)
108  * @code
109  * class MyWritePlugin : public vlink::BagPluginInterface {
110  * public:
111  * MyWritePlugin() {
112  * processor_.register_output_callback([this](const vlink::Frame& frame) { do_callback(frame); });
113  * }
114  *
115  * void on_read(const vlink::Frame& frame) override { do_callback(frame); } // unused on the write side
116  *
117  * void on_write(const vlink::Frame& frame) override {
118  * const int64_t data_timestamp = parse_header_time(frame.data); // plugin extracts the data-plane time
119  * processor_.push(data_timestamp, frame); // reorder by data_timestamp; emit via
120  * do_callback()
121  * }
122  *
123  * void flush() override { processor_.flush(); } // drain the buffered tail at teardown
124  *
125  * private:
126  * vlink::BagProcessor processor_;
127  * };
128  * VLINK_PLUGIN_DECLARE(MyWritePlugin, 2, 0)
129  * @endcode
130  */
131 
132 #pragma once
133 
134 #include <cstdint>
135 #include <string>
136 #include <utility>
137 
138 #include "../base/plugin.h"
139 #include "../impl/types.h"
140 
141 namespace vlink {
142 
143 /**
144  * @class BagPluginInterface
145  * @brief Abstract plugin base shared by bag playback and bag recording.
146  *
147  * @details
148  * The host binds an instance through @c BagReader::bind_bag_interface() or
149  * @c BagWriter::bind_bag_interface(). At bind time the host calls @c bind_direction() to
150  * record which side the plugin serves and @c register_callback() to supply the forwarding sink. The frame
151  * hooks @c on_read() and @c on_write() are pure and must both be defined; @c convert_url_meta(), @c on_reset(),
152  * and @c flush() carry defaults. Implementations are expected to be thread-compatible with the host's loop thread.
153  */
156 
157  protected:
158  BagPluginInterface() = default;
159 
160  virtual ~BagPluginInterface() = default;
161 
162  public:
163  /**
164  * @brief Identifies whether the plugin is bound to a reader or a writer.
165  */
166  enum Direction : uint8_t {
167  kRead = 0, ///< Bound to a @c BagReader; the plugin forwards replayed frames.
168  kWrite = 1, ///< Bound to a @c BagWriter; the plugin forwards frames before they are persisted.
169  };
170 
171  /**
172  * @brief Forwarding sink used by @c do_callback() to re-emit a frame downstream.
173  *
174  * @details
175  * Supplied by the host (@c BagReader or @c BagWriter) at bind time and stored internally. A single
176  * @c const @c Frame& sink serves both directions: read plugins re-emit toward playback, write plugins
177  * toward persistence.
178  */
180 
181  /**
182  * @brief Records the binding direction so the plugin can branch on read vs write.
183  *
184  * @details
185  * Invoked by the host at attach time before any other hook. The value is observable from
186  * the hooks through @c get_direction().
187  *
188  * @param direction Side the plugin is being bound to.
189  */
190  void bind_direction(Direction direction);
191 
192  /**
193  * @brief Returns the side this plugin is currently bound to.
194  *
195  * @return @c Direction::kRead when bound to a reader, @c Direction::kWrite when bound to a writer.
196  */
197  [[nodiscard]] Direction get_direction() const;
198 
199  /**
200  * @brief Stores the forwarding sink used by @c do_callback().
201  *
202  * @details
203  * Invoked by the host's @c bind_bag_interface() at attach time. The plugin keeps @p callback in
204  * @c callback_ and calls it from @c do_callback() to deliver a frame downstream -- toward the user's
205  * playback callback on the read side, or toward persistence on the write side. Cleared (with an
206  * empty callable) on rebind and at host teardown, so a plugin-owned worker thread cannot reach a
207  * destroyed host.
208  *
209  * @param callback Sink that forwards a frame downstream.
210  */
211  void register_callback(Callback&& callback);
212 
213  /**
214  * @brief Rewrites or filters a stored URL before playback begins (read side).
215  *
216  * @details
217  * Called once per URL contained in the bag when the reader opens the file. Implementations
218  * may modify any of the three parameters in place to remap topics or override schema
219  * metadata. The default implementation keeps every URL unchanged.
220  *
221  * @param url URL string; may be modified in place.
222  * @param ser_type Serialisation type; may be modified in place.
223  * @param schema_type Coarse schema family; may be modified in place.
224  * @return @c true to retain the URL in playback; @c false to exclude it.
225  */
226  virtual bool convert_url_meta(std::string& url, std::string& ser_type, SchemaType& schema_type);
227 
228  /**
229  * @brief Intercepts a replayed frame on its way to the user (read side).
230  *
231  * @details
232  * Called for every replayed frame after timing pacing. Forward it downstream by calling
233  * @c do_callback(); transforming the payload, dropping the frame (by not emitting), fanning it out
234  * (emitting several), or buffering and re-emitting frames @e reordered by data-plane time (e.g. via
235  * @c BagProcessor) is permitted.
236  *
237  * @note @c Frame::ser_type and @c Frame::schema_type contain the effective URL metadata, including
238  * overrides made by @c convert_url_meta(). The payload is a shallow view valid for the duration
239  * of the call; copy it before buffering for asynchronous emit.
240  *
241  * @note Prefer @c convert_url_meta() for stable URL remapping. If this hook emits a frame under a
242  * different URL, existing type fields remain authoritative because the plugin may have renamed a
243  * TypeA payload or transcoded it intentionally. To resolve metadata registered for the emitted
244  * URL, clear @c ser_type and set @c schema_type to @c SchemaType::kUnknown before calling
245  * @c do_callback(); otherwise update both fields to describe the emitted payload explicitly.
246  *
247  * @param frame Replayed frame.
248  */
249  virtual void on_read(const Frame& frame) = 0;
250 
251  /**
252  * @brief Intercepts a frame before it is persisted (write side).
253  *
254  * @details
255  * Called for every frame handed to the writer, before it is recorded. Re-emit it by calling
256  * @c do_callback(). Transcoding (rewrite @c frame.data plus @c frame.ser_type / @c schema_type, e.g.
257  * raw image to JPEG), dropping the frame (by not emitting), fanning it out, or buffering and
258  * re-emitting frames @e reordered by data-plane time (a sliding-window reorder, e.g. via
259  * @c BagProcessor) is permitted.
260  *
261  * @note The payload is a view valid for the duration of the call; a plugin that emits
262  * asynchronously must copy it before buffering.
263  *
264  * @note When a plugin renames the URL, the recorder learns the source-to-recorded mapping only for
265  * @e synchronous emits (within this @c on_write() call) and only when the rewrite is one-to-one,
266  * so URL-level metadata such as loss stays correctly attributed. A plugin that both renames
267  * and emits asynchronously is responsible for any loss attribution itself.
268  *
269  * @note @c BagWriter::push() resolves a negative @c Frame::timestamp to the writer clock @e before
270  * calling this hook; that auto-assignment does @b not re-run on the frames a plugin emits. A
271  * re-emitted frame is persisted with its own @c Frame::timestamp verbatim, so a plugin that
272  * constructs a fresh frame must set a resolved (non-negative) timestamp on it.
273  *
274  * @param frame Frame to persist.
275  */
276  virtual void on_write(const Frame& frame) = 0;
277 
278  /**
279  * @brief Discards state retained from an earlier read-side playback session.
280  *
281  * @details
282  * Called synchronously before a reader starts each top-level playback session and before its ready
283  * callback. A plugin that buffers frames must override this to discard the cache and reset all time
284  * anchors without emitting frames, typically with @c processor_.reset(). This isolates a new play or
285  * jump from frames retained when the preceding session was interrupted. The default implementation is
286  * a no-op for synchronous plugins. The writer does not call this hook.
287  */
288  virtual void on_reset();
289 
290  /**
291  * @brief Drains any internally-buffered frames downstream before the host unbinds and tears down.
292  *
293  * @details
294  * Called by the host on its own thread, while its sink is still valid, after each naturally completed
295  * read-side playback pass and right before either side detaches the plugin. An interrupted pass skips
296  * this boundary call. A plugin that buffers frames for
297  * asynchronous re-emit (e.g. a @c BagProcessor reorder buffer) must override this to flush those frames
298  * synchronously -- typically @c processor_.flush() -- so a buffered tail is recorded / replayed instead
299  * of dropped and cannot leak into the next playback pass. The default implementation is a no-op (a
300  * synchronous plugin holds nothing back). On detach, after @c flush() returns, the host stops delivering
301  * this plugin's emitted frames, so any frame produced afterwards is ignored.
302  */
303  virtual void flush();
304 
305  /**
306  * @brief Forwards one frame downstream through the registered sink.
307  *
308  * @details
309  * The emit helper a plugin calls -- typically from @c on_read() / @c on_write() or from a reorder
310  * buffer's output callback -- to deliver a frame downstream without touching @c callback_ directly.
311  * Invokes @c callback_ when one is registered and is otherwise a no-op.
312  *
313  * @param frame Frame to forward to the sink.
314  */
315  void do_callback(const Frame& frame);
316 
317  protected:
318  Direction direction_{Direction::kRead};
319 
320  private:
321  Callback callback_;
322 
324 };
325 
326 ////////////////////////////////////////////////////////////////
327 /// Details
328 ////////////////////////////////////////////////////////////////
329 
330 inline void BagPluginInterface::bind_direction(Direction direction) { direction_ = direction; }
331 
333 
334 inline void BagPluginInterface::register_callback(Callback&& callback) { callback_ = std::move(callback); }
335 
336 inline bool BagPluginInterface::convert_url_meta(std::string& url, std::string& ser_type,
337  SchemaType& schema_type) { // LCOV_EXCL_LINE GCOVR_EXCL_LINE
338  (void)url;
339  (void)ser_type;
340  (void)schema_type;
341 
342  return true; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
343 }
344 
346 
348 
349 inline void BagPluginInterface::do_callback(const Frame& frame) {
350  if (callback_) {
351  callback_(frame);
352  }
353 }
354 
355 } // 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:347