VLink  2.1.0
A high-performance communication middleware
node.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 node.h
26  * @brief Common CRTP base for every VLink communication primitive.
27  *
28  * @details
29  * @c Node<ImplT, SecT> is the polymorphic base class shared by all six
30  * VLink communication primitives. It owns the transport-specific
31  * implementation pointer (@c impl_), drives the node lifecycle, and exposes
32  * the cross-cutting services that every primitive needs: zero-copy loans,
33  * peer-discovery toggling, message-loop attachment, recording, security
34  * installation, profiling, and SSL/TLS configuration.
35  *
36  * @par Class Hierarchy
37  * @verbatim
38  * +--------------------------------------------------------------+
39  * | User-facing primitives |
40  * | Publisher<T> Subscriber<T> Setter<T> Getter<T> |
41  * | Client<Req,Resp> Server<Req,Resp> |
42  * +-----------------------------+--------------------------------+
43  * | inherits
44  * +-----------------------------v--------------------------------+
45  * | Node<ImplT, SecT> |
46  * | lifecycle | loans | properties | profiler | ssl | security |
47  * +-----------------------------+--------------------------------+
48  * | owns std::unique_ptr<ImplT>
49  * +-----------------------------v--------------------------------+
50  * | ImplT (PublisherImpl, SubscriberImpl, SetterImpl, ...) |
51  * +-----------------------------+--------------------------------+
52  * | dispatches to transport
53  * +-----------------------------v--------------------------------+
54  * | Transport back-ends |
55  * | intra | shm | shm2 | dds | ddsc | zenoh | someip | ... |
56  * +--------------------------------------------------------------+
57  * @endverbatim
58  *
59  * @par Lifecycle State Diagram
60  * @verbatim
61  * +------------+ constructor +------------+ init() +------------+
62  * | (uninited) |------------------>| parsed |------------->| active |
63  * | | parse URL / Conf | (impl_) | init_ext | (using API)|
64  * +------------+ +------------+ +------------+
65  * ^ |
66  * | init() | interrupt()
67  * | v
68  * +------+------+ +-------------+
69  * | deinited |<-----------| blocking |
70  * | | deinit() | wait aborts|
71  * +-------------+ +-------------+
72  * |
73  * | destructor (calls deinit if active)
74  * v
75  * destroyed
76  * @endverbatim
77  *
78  * | Step | Method | Notes |
79  * | ----------------- | ----------------------- | ------------------------------------------- |
80  * | Construction | constructor | Parses URL, creates impl via Conf factory. |
81  * | Initialisation | @c init() | Runs impl init + init_ext, samples loans. |
82  * | Active | publish / listen / ... | Normal operation. |
83  * | Interrupt | @c interrupt() | Aborts blocking waits immediately. |
84  * | Deinitialisation | @c deinit() | interrupt() then impl deinit + deinit_ext. |
85  * | Destruction | destructor | Auto-deinits if still active. |
86  *
87  * @par ImplType Roles
88  * | @c ImplType | Primitive that uses it | Direction |
89  * | ------------------- | --------------------------- | -------------- |
90  * | @c kPublisher | @c Publisher<T> | Event write |
91  * | @c kSubscriber | @c Subscriber<T> | Event read |
92  * | @c kClient | @c Client<Req,Resp> | RPC caller |
93  * | @c kServer | @c Server<Req,Resp> | RPC handler |
94  * | @c kSetter | @c Setter<T> | Field write |
95  * | @c kGetter | @c Getter<T> | Field read |
96  *
97  * @par Deferred Initialisation
98  * @code
99  * vlink::Publisher<MyMsg> pub("dds://topic", vlink::InitType::kWithoutInit);
100  * pub.set_ser_type("my.custom.Type");
101  * pub.set_discovery_enabled(false);
102  * pub.init();
103  * @endcode
104  *
105  * @par Security
106  * Use the @c Security* primitive aliases to enable per-message encryption.
107  * The @c Security::Config aggregate is passed as the second constructor
108  * argument; omitting it (or passing an empty aggregate) uses the built-in
109  * default symmetric slot:
110  * @code
111  * vlink::Security::Config cfg;
112  * cfg.key = "my-secret";
113  * vlink::SecurityPublisher<MyMsg> pub("shm://topic", cfg);
114  * @endcode
115  *
116  * @note @c intra:// and @c dds:// CDR payloads do not support per-message
117  * security; constructing a @c Security* primitive there is a fatal.
118  *
119  * @par Zero-copy Loans
120  * On loan-capable transports the loan API avoids extra copies:
121  * @code
122  * vlink::Publisher<vlink::Bytes> pub("shm://topic");
123  * if (pub.is_support_loan()) {
124  * vlink::Bytes buf = pub.loan(payload_size);
125  * write_into(buf);
126  * pub.publish(buf); // loan is returned automatically
127  * }
128  * @endcode
129  *
130  * @tparam ImplT Concrete transport implementation derived from @c NodeImpl.
131  * @tparam SecT Security mode: @c kWithoutSecurity (default) or @c kWithSecurity.
132  *
133  * @see publisher.h, subscriber.h, client.h, server.h, getter.h, setter.h,
134  * extension/security.h, extension/ssl_options.h
135  */
136 
137 #pragma once
138 
139 #include <atomic>
140 #include <memory>
141 #include <mutex>
142 #include <optional>
143 #include <string>
144 
145 #include "./extension/security.h"
146 #include "./impl/node_impl.h"
147 
148 namespace vlink {
149 
150 /**
151  * @class Node
152  * @brief Transport-agnostic CRTP base for all VLink communication primitives.
153  *
154  * @details
155  * Provides the lifecycle, loan, security, property, profiler, message-loop,
156  * recording, discovery, and TLS APIs shared by every primitive in the
157  * library. Subclasses fill in the role-specific operations (publish,
158  * listen, invoke, set, get, etc.).
159  *
160  * @tparam ImplT Concrete implementation class (e.g. @c PublisherImpl).
161  * @tparam SecT Security mode (@c kWithoutSecurity or @c kWithSecurity).
162  */
163 template <typename ImplT, SecurityType SecT>
164 class Node {
165  public:
166  using StatusCallback = NodeImpl::StatusCallback; ///< Handler signature for status-change notifications.
167 
168  /**
169  * @brief Initialises the node and its transport back-end.
170  *
171  * @details
172  * Uses an atomic compare-exchange to guard against double-initialisation.
173  * On success the method runs @c impl_->init() then @c impl_->init_ext()
174  * and finally samples the transport's loan capability flag. Calling
175  * @c init() on an already-initialised node is a no-op.
176  *
177  * @return @c true on first successful initialisation; @c false otherwise.
178  */
179  virtual bool init();
180 
181  /**
182  * @brief Tears the node down and releases all transport resources.
183  *
184  * @details
185  * Atomically guards against double-deinit, then runs @c interrupt(),
186  * @c impl_->deinit(), and @c impl_->deinit_ext(). When safe-quit mode is
187  * active the sequence runs under the safe-quit mutex. The destructor
188  * calls this automatically so explicit calls are only required for early
189  * shutdown.
190  *
191  * @return @c true on first successful deinit; @c false if not initialised.
192  */
193  virtual bool deinit();
194 
195  /**
196  * @brief Aborts any blocking wait on this node.
197  *
198  * @details
199  * Signals the internal interrupted flag and notifies the condition
200  * variable so that @c wait_for_subscribers(), @c wait_for_connected(), and
201  * @c wait_for_value() return immediately with @c false. @c Getter
202  * overrides this to additionally wake its own condition variable used by
203  * @c wait_for_value().
204  */
205  virtual void interrupt();
206 
207  /**
208  * @brief Reports whether @c init() has been successfully called.
209  *
210  * @return @c true when the node is currently in the initialised state.
211  */
212  [[nodiscard]] bool has_inited() const;
213 
214  /**
215  * @brief Reports whether the transport supports zero-copy loaned buffers.
216  *
217  * @details
218  * Delegates to the transport implementation. When loans are supported,
219  * @c publish() / @c set() / @c reply() automatically use them to avoid an
220  * extra memory copy.
221  *
222  * @return @c true if @c loan() / @c return_loan() are meaningful for this transport.
223  */
224  [[nodiscard]] bool is_support_loan() const;
225 
226  /**
227  * @brief Allocates a loaned buffer from the transport memory pool.
228  *
229  * @details
230  * Returns a @c Bytes backed by transport-managed memory of @p size bytes.
231  * The caller must either pass it to a publish/write call (which returns
232  * the loan automatically) or call @c return_loan() explicitly. Returns
233  * an empty @c Bytes on failure or when the transport has no loan pool.
234  *
235  * @param size Requested byte count; @c 0 is valid for empty messages.
236  * @return Loaned @c Bytes, or an empty @c Bytes on failure.
237  */
238  [[nodiscard]] Bytes loan(int64_t size);
239 
240  /**
241  * @brief Returns a previously loaned buffer to the transport pool.
242  *
243  * @details
244  * Must be called whenever a loan obtained via @c loan() is not consumed by
245  * a publish/write call; failing to return loans can exhaust the shared
246  * memory pool.
247  *
248  * @param bytes The loaned @c Bytes to return.
249  * @return @c true on success; @c false if the buffer is not a valid loan.
250  */
251  bool return_loan(const Bytes& bytes);
252 
253  /**
254  * @brief Suspends message delivery on this node.
255  *
256  * @details
257  * Transport-dependent behaviour: some back-ends buffer incoming messages
258  * while suspended, others drop them. Pair with @c resume().
259  *
260  * @return @c true if suspension succeeded.
261  */
262  bool suspend();
263 
264  /**
265  * @brief Resumes message delivery after a prior @c suspend().
266  *
267  * @return @c true if resumption succeeded.
268  */
269  bool resume();
270 
271  /**
272  * @brief Reports whether the node is currently suspended.
273  *
274  * @return @c true while @c suspend() is in effect.
275  */
276  [[nodiscard]] bool is_suspend() const;
277 
278  /**
279  * @brief Attaches the node to a @c MessageLoop for callback dispatch.
280  *
281  * @details
282  * After attachment, inbound callbacks are posted onto @p message_loop
283  * rather than invoked on the transport delivery thread. This serialises
284  * dispatch onto the loop's thread, which is convenient for
285  * single-threaded user code.
286  *
287  * @param message_loop Pointer to the target @c MessageLoop.
288  * @return @c true on success; @c false if a loop is already attached.
289  */
290  bool attach(class MessageLoop* message_loop);
291 
292  /**
293  * @brief Detaches the node from its current @c MessageLoop.
294  *
295  * @details
296  * After detachment the callback returns to running on the transport
297  * delivery thread.
298  *
299  * @return @c true on success; @c false if no loop was attached.
300  */
301  bool detach();
302 
303  /**
304  * @brief Returns the @c MessageLoop this node is currently attached to.
305  *
306  * @return Pointer to the attached @c MessageLoop, or @c nullptr.
307  */
308  [[nodiscard]] class MessageLoop* get_message_loop() const;
309 
310  /**
311  * @brief Returns the abstract-graph handle for runtime topology inspection.
312  *
313  * @details
314  * The @c AbstractNode pointer is usable with @c AbstractFactory and the
315  * proxy monitoring API to enumerate peer nodes in the same transport
316  * graph.
317  *
318  * @return Non-owning pointer to the @c AbstractNode, or @c nullptr if the
319  * transport does not expose one.
320  */
321  [[nodiscard]] const AbstractNode* get_abstract_node() const;
322 
323  /**
324  * @brief Retrieves the current status object for the requested category.
325  *
326  * @details
327  * Returns a polymorphic shared pointer. The concrete type and set of
328  * supported categories depend on the active transport; an unsupported
329  * @p type yields a @c Status::Unknown instance and logs a warning.
330  *
331  * @param type Status category to retrieve.
332  * @return Shared pointer to status data, or @c Status::Unknown when unsupported.
333  */
334  [[nodiscard]] Status::BasePtr get_status(Status::Type type) const;
335 
336  /**
337  * @brief Registers a handler invoked whenever the node's status changes.
338  *
339  * @details
340  * Only one handler can be registered at a time; subsequent calls replace
341  * the previous handler. The handler receives a @c Status::BasePtr
342  * describing the new state (connected, disconnected, error, etc.).
343  *
344  * @param callback @c void(const Status::BasePtr&) handler.
345  */
346  void register_status_handler(StatusCallback&& callback);
347 
348  /**
349  * @brief Sets a transport-specific string-keyed property.
350  *
351  * @details
352  * Extensibility mechanism for back-end-specific tuning knobs that do not
353  * have a dedicated method. Recognised keys depend on the active
354  * transport.
355  *
356  * @param prop Property key string.
357  * @param value Property value string.
358  */
359  void set_property(const std::string& prop, const std::string& value);
360 
361  /**
362  * @brief Retrieves a previously set transport-specific property value.
363  *
364  * @param prop Property key string.
365  * @return Property value string; empty if the key is unknown.
366  */
367  [[nodiscard]] std::string get_property(const std::string& prop) const;
368 
369  /**
370  * @brief Returns the @c TransportType this node is bound to.
371  *
372  * @return Enumerator such as @c kDds, @c kShm, @c kIntra, etc.
373  */
374  [[nodiscard]] TransportType get_transport_type() const;
375 
376  /**
377  * @brief Returns the URL string used to construct this node.
378  *
379  * @details
380  * Non-empty only when the node was constructed via a URL string or @c Url
381  * object; typed @c ConfT-based construction leaves this empty.
382  *
383  * @return Const reference to the URL string.
384  */
385  [[nodiscard]] const std::string& get_url() const;
386 
387  /**
388  * @brief Enables recording of inbound or outbound messages to a bag file.
389  *
390  * @details
391  * Not supported on @c intra:// nodes (triggers a fatal log). DDS CDR nodes
392  * record the complete payload, including the encapsulation header.
393  * Supported file suffixes are @c .vdb, @c .vdbx, @c .vcap, and @c .vcapx;
394  * an unknown suffix logs an error and disables recording.
395  *
396  * @param path Bag file path on disk.
397  */
398  void set_record_path(const std::string& path);
399 
400  /**
401  * @brief Overrides the runtime wire-metadata identifiers for this node.
402  *
403  * @details
404  * @p ser_type holds the concrete runtime type identifier; @p schema_type
405  * holds the coarse decoder family used by discovery, proxy, and bag
406  * metadata. When @p schema_type is @c SchemaType::kUnknown (the default)
407  * the family is inferred from @p ser_type: a @c "vlink::zerocopy::" prefix
408  * implies @c kZeroCopy, while values such as @c "raw", @c "string",
409  * @c "std::string", @c "text", or @c "json" imply @c kRaw. If no family
410  * can be inferred, an existing @c kRaw or @c kZeroCopy family is reset to
411  * @c kUnknown; any other existing family is preserved. Passing an empty
412  * @p ser_type clears both fields.
413  *
414  * If invoked post-@c init() the transport extension is restarted so that
415  * external metadata stays in sync. A DDS node's raw/CDR mode is part of its
416  * native Topic/DataWriter/DataReader type and therefore cannot be changed
417  * while initialised; neither can the type name of an initialised DDS CDR
418  * node. Call @c deinit() before changing either value, then call @c init()
419  * again.
420  *
421  * @param ser_type Concrete runtime type identifier, or empty to clear.
422  * @param schema_type Optional explicit schema family; default preserves the current family.
423  */
424  void set_ser_type(const std::string& ser_type, SchemaType schema_type = SchemaType::kUnknown);
425 
426  /**
427  * @brief Returns the current concrete runtime type identifier.
428  *
429  * @return Const reference to the type identifier string.
430  */
431  [[nodiscard]] const std::string& get_ser_type() const;
432 
433  /**
434  * @brief Returns the current coarse schema family.
435  *
436  * @return The @c SchemaType stored on the implementation.
437  */
438  [[nodiscard]] SchemaType get_schema_type() const;
439 
440  /**
441  * @brief Toggles peer-discovery on this node.
442  *
443  * @details
444  * Disabling discovery reduces CPU and network overhead for nodes that
445  * never need to locate peers. Reinitialises the transport extension if
446  * invoked post-@c init() so the change takes effect immediately.
447  *
448  * @param enable @c true (default) to enable discovery; @c false to disable.
449  */
450  void set_discovery_enabled(bool enable);
451 
452  /**
453  * @brief Reports whether peer-discovery is currently enabled.
454  *
455  * @return @c true if discovery is active.
456  */
457  [[nodiscard]] bool get_discovery_enabled() const;
458 
459  /**
460  * @brief Binds a Protobuf Arena for arena-allocated message objects.
461  *
462  * @details
463  * Required whenever this node must create a raw Protobuf pointer object
464  * (e.g. a subscriber message, server request/response, client response, or
465  * getter value of type @c MyProto*). The arena must outlive this node.
466  * Forgetting to bind it before the first such operation triggers a fatal
467  * log. Receive paths allocate a distinct message in the arena for each
468  * delivery, so that storage is retained until the caller resets or destroys
469  * the arena.
470  *
471  * @param proto_arena Pointer to a @c google::protobuf::Arena instance (typed as @c void*).
472  */
473  void bind_proto_arena(void* proto_arena);
474 
475  /**
476  * @brief Returns the cumulative CPU-usage ratio sampled by the profiler.
477  *
478  * @details
479  * Reports the percentage of wall-clock time this node has spent in active
480  * publish or receive code since the profiler was started. Available only
481  * when the CPU profiler is built in (@c VLINK_DISABLE_PROFILER not
482  * defined) and global profiling is enabled via the @c VLINK_PROFILER_ENABLE
483  * environment variable. Returns @c -1.0 if no profiler is attached.
484  *
485  * @return CPU usage percentage (may exceed @c 100.0 on multi-core systems);
486  * @c -1.0 if unavailable.
487  */
488  [[nodiscard]] double get_cpu_usage() const;
489 
490  /**
491  * @brief Reports whether safe-quit mode is currently active.
492  *
493  * @details
494  * Safe-quit mode holds a @c std::mutex around user callbacks and around
495  * @c deinit() to prevent use-after-free races when a node is destroyed
496  * while a callback is in flight.
497  *
498  * @return @c true if the safe-quit mutex is engaged.
499  */
500  [[nodiscard]] bool get_safety_quit() const;
501 
502  /**
503  * @brief Enables or disables safe-quit mode.
504  *
505  * @details
506  * When enabled, an internal @c std::mutex is allocated and locked around
507  * every callback invocation and around @c deinit(). Enable when the
508  * node's lifetime is shorter than the callback scope. There is a small
509  * synchronisation overhead; avoid enabling it on hot paths.
510  *
511  * @param safety_quit @c true to enable; @c false to disable (default).
512  */
513  void set_safety_quit(bool safety_quit);
514 
515  /**
516  * @brief Configures transport-layer SSL/TLS encryption for this node.
517  *
518  * @details
519  * Merges the fields of @p options into the node's internal property map
520  * via @c SslOptions::parse_to(). The transport reads the resulting
521  * @c ssl.* properties during @c init() to set up the TLS connection, so
522  * this method must be called before @c init() for the settings to take
523  * effect.
524  *
525  * SSL is considered enabled when @c SslOptions::is_valid() returns
526  * @c true (i.e. at least @c ca_file or @c cert_file is non-empty). Not
527  * all back-ends support TLS; see @c SslOptions for the per-backend
528  * compatibility table. Thread-safe -- the property map is updated under
529  * a mutex.
530  *
531  * @par Example
532  * @code
533  * vlink::Publisher<MyMsg> pub("mqtt://sensor/data", vlink::InitType::kWithoutInit);
534  * vlink::SslOptions ssl;
535  * ssl.ca_file = "/etc/certs/ca.pem";
536  * ssl.cert_file = "/etc/certs/client.pem";
537  * ssl.key_file = "/etc/certs/client-key.pem";
538  * pub.set_ssl_options(ssl);
539  * pub.init();
540  * @endcode
541  *
542  * @param options SSL/TLS configuration to apply.
543  *
544  * @see SslOptions, set_property()
545  */
546  void set_ssl_options(const SslOptions& options);
547 
548  protected:
549  Node();
550 
551  virtual ~Node();
552 
553  /**
554  * @brief Installs a @c Security configuration before transport initialisation.
555  *
556  * @details
557  * Internal helper used by the @c Security* primitive constructors after
558  * @c impl_ has been created but before @c init(). Delegates default
559  * handling, validation, and storage to @c NodeImpl::enable_security().
560  *
561  * @param cfg Security configuration aggregate.
562  * @return @c true when @p cfg (including the default empty case) is usable for this role / transport.
563  */
564  bool enable_security(const Security::Config& cfg);
565 
566  /**
567  * @brief Move overload for construction-time security installation.
568  *
569  * @details
570  * Used when an internal caller owns the config and can forward it without
571  * an extra copy.
572  *
573  * @param cfg Security configuration aggregate to consume.
574  * @return @c true when @p cfg (including the default empty case) is usable for this role / transport.
575  */
576  bool enable_security(Security::Config&& cfg);
577 
578  template <typename CallbackT, typename... ArgsT>
579  void invoke_callback(const CallbackT& callback, ArgsT&&... args);
580 
581  template <typename TypeT>
582  TypeT get_default_value();
583 
584  void* proto_arena_{nullptr};
585  bool is_support_loan_{false};
586 
587  std::atomic_bool has_inited_{false};
588  std::optional<std::mutex> quit_mtx_;
589 
590  std::unique_ptr<ImplT> impl_;
591 
592  private:
594 };
595 
596 } // namespace vlink
597 
598 #include "./internal/node-inl.h"
#define VLINK_DISALLOW_COPY_AND_ASSIGN(classname)
Deletes the copy constructor and copy-assignment operator of classname.
Definition: macros.h:174
Foundational base classes shared by every transport-backed VLink node.
Application-layer authenticated encryption with symmetric, hybrid asymmetric, and pluggable backends.