VLink  2.0.0
A high-performance communication middleware
plugin.h
浏览该文件的文档.
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 plugin.h
26  * @brief Strongly-typed shared-library plugin loader with ID and version verification.
27  *
28  * @details
29  * @c vlink::Plugin wraps the host platform's dynamic-library API (@c dlopen / @c LoadLibrary)
30  * and resolves the @c vlink_plugin_create / @c vlink_plugin_destroy entry points exported
31  * by every plugin built with @c VLINK_PLUGIN_DECLARE. Plugin implementations are bound to
32  * an abstract interface type so the loader can verify the ABI contract before any virtual
33  * call crosses the library boundary.
34  *
35  * Plugin lifecycle observed by the loader:
36  *
37  * @verbatim
38  * load() ---> open() ---> create() ---> in use ---+
39  * ^ |
40  * | v
41  * clear() <--- close() <--- destroy() <--- unload()
42  * @endverbatim
43  *
44  * Interface / implementation contract:
45  *
46  * | Side | Required macro | Result |
47  * | ------------------ | ------------------------------------------- | ---------------------------- |
48  * | Abstract interface | @c VLINK_PLUGIN_REGISTER | Plugin ID = demangled name |
49  * | Abstract interface | @c VLINK_PLUGIN_REGISTER_BY_ID(_, "id") | Plugin ID = literal string |
50  * | Concrete impl .cc | @c VLINK_PLUGIN_DECLARE(Impl, major, minor) | Exports create/destroy ABI |
51  *
52  * @par Version verification
53  * @c process_plugin_internal() runs inside the plugin entry point and verifies that the
54  * plugin ID matches and that the plugin's major version equals the host's required major
55  * and the plugin's minor is no lower than the host's required minor. Mismatches return
56  * @c nullptr from @c vlink_plugin_create() so an incompatible binary never crosses the
57  * vtable boundary.
58  *
59  * @par Example
60  * @code
61  * // Interface (header):
62  * class MyPlugin {
63  * VLINK_PLUGIN_REGISTER(MyPlugin)
64  * public:
65  * virtual ~MyPlugin() = default;
66  * virtual void do_work() = 0;
67  * };
68  *
69  * // Implementation (.cc):
70  * class MyPluginImpl : public MyPlugin {
71  * VLINK_PLUGIN_REGISTER(MyPlugin)
72  * public:
73  * void do_work() override { ... }
74  * };
75  * VLINK_PLUGIN_DECLARE(MyPluginImpl, 1, 0)
76  *
77  * // Host:
78  * vlink::Plugin plugin;
79  * if (auto impl = plugin.load<MyPlugin>("my_plugin_impl", 1, 0)) {
80  * impl->do_work();
81  * }
82  * @endcode
83  */
84 
85 #pragma once
86 
87 #include <cstdint>
88 #include <deque>
89 #include <memory>
90 #include <string>
91 #include <string_view>
92 
93 #include "./logger.h"
94 #include "./macros.h"
95 #include "./name_detector.h"
96 
97 /**
98  * @def VLINK_PLUGIN_CREATE_FUNC_NAME
99  * @brief Symbol name of the plugin construction entry point exported by @c VLINK_PLUGIN_DECLARE.
100  */
101 #define VLINK_PLUGIN_CREATE_FUNC_NAME vlink_plugin_create
102 
103 /**
104  * @def VLINK_PLUGIN_DESTROY_FUNC_NAME
105  * @brief Symbol name of the plugin destruction entry point exported by @c VLINK_PLUGIN_DECLARE.
106  */
107 #define VLINK_PLUGIN_DESTROY_FUNC_NAME vlink_plugin_destroy
108 
109 namespace vlink {
110 
111 struct PluginEntry;
112 
113 /**
114  * @class Plugin
115  * @brief Manager that loads, tracks and unloads shared-library plugins by interface type.
116  *
117  * @details
118  * A single @c Plugin instance can host multiple distinct interface types simultaneously
119  * and tracks each loaded library so repeated @c load() calls share the underlying
120  * @c dlopen handle. All operations are thread safe through the internal implementation.
121  */
122 class VLINK_EXPORT Plugin final {
123  public:
124  /**
125  * @brief Opaque handle to a loaded shared library; treated as a token by the public API.
126  */
127  using Handle = void*;
128 
129  /**
130  * @brief Constructs an empty plugin manager with no libraries loaded.
131  */
133 
134  /**
135  * @brief Destroys the plugin manager and unloads every still-resident library via @c clear().
136  */
138 
139  /**
140  * @brief Sets the verbosity level used for plugin diagnostic messages.
141  *
142  * @param level Logger level to apply to plugin load/unload tracing.
143  */
145 
146  /**
147  * @brief Returns the verbosity level currently used for plugin diagnostics.
148  *
149  * @return Current logger level.
150  */
151  [[nodiscard]] Logger::Level get_log_level() const;
152 
153  /**
154  * @brief Returns the default ordered search path used when locating plugin libraries.
155  *
156  * @details
157  * The list contains, in priority order, directories from @c VLINK_PLUGIN_DIR
158  * (comma or whitespace separated), the current working directory, the executable
159  * directory, and the system library directories appropriate for the platform.
160  *
161  * @return Deque of directory paths searched left to right.
162  */
163  [[nodiscard]] static std::deque<std::string> default_search_path();
164 
165  /**
166  * @brief Loads a shared library that implements interface @c T and returns a tracked handle.
167  *
168  * @details
169  * The loader appends the platform's library prefix/suffix to @p lib_name, scans
170  * @p search_paths, opens the first matching file, invokes the @p function_name entry
171  * point with the caller's ID and version, and finally wraps the returned object pointer
172  * in a @c shared_ptr<T> whose deleter invokes @c vlink_plugin_destroy.
173  *
174  * @tparam T Interface type carrying @c get_plugin_id() (added by
175  * @c VLINK_PLUGIN_REGISTER or @c VLINK_PLUGIN_REGISTER_BY_ID).
176  * @param lib_name Library file name without prefix/suffix.
177  * @param version_major Required interface major version.
178  * @param version_minor Required interface minor version.
179  * @param dir_name Optional directory searched before @p search_paths.
180  * @param search_paths Ordered fallback search list. Default: @c default_search_path().
181  * @param function_name Symbol name of the construction entry point.
182  * @return @c shared_ptr<T> owning the plugin instance, or @c nullptr on failure.
183  */
184  template <class T>
185  [[nodiscard]] std::shared_ptr<T> load(
186  const std::string& lib_name, uint16_t version_major, uint16_t version_minor, const std::string& dir_name = "",
187  const std::deque<std::string>& search_paths = default_search_path(),
188  const std::string& function_name = VLINK_MACRO_STRING_GET(VLINK_PLUGIN_CREATE_FUNC_NAME));
189 
190  /**
191  * @brief Removes a previously loaded plugin from the registry.
192  *
193  * @details
194  * The shared library is finally unmapped once every @c shared_ptr returned by
195  * @c load() has been destroyed; this call only releases the tracker entry.
196  *
197  * @tparam T Interface type used during the original @c load() call.
198  * @param lib_name Library file name passed to @c load().
199  * @return @c true when the registry entry existed and was removed.
200  */
201  template <class T>
202  bool unload(const std::string& lib_name);
203 
204  /**
205  * @brief Reports whether a plugin for interface @c T is currently registered.
206  *
207  * @tparam T Interface type used during the original @c load() call.
208  * @param lib_name Library file name passed to @c load().
209  * @return @c true when the registry entry is present.
210  */
211  template <class T>
212  [[nodiscard]] bool has_loaded(const std::string& lib_name);
213 
214  /**
215  * @brief Builds the composite key used internally to identify a (library, interface) pair.
216  *
217  * @details
218  * The key has the form @c lib_name + "@" + T::get_plugin_id() so the same shared library
219  * can be loaded twice when consumed via two different interfaces.
220  *
221  * @tparam T Interface type.
222  * @param lib_name Library file name.
223  * @return Composite identifier string.
224  */
225  template <class T>
226  [[nodiscard]] std::string get_plugin_complex_id(const std::string& lib_name);
227 
228  /**
229  * @brief Unloads every library currently tracked by this manager.
230  */
231  void clear();
232 
233  /**
234  * @brief Internal version/ID gate invoked from the @c VLINK_PLUGIN_DECLARE entry point.
235  *
236  * @details
237  * Compares the plugin's exported ID and version against the host's expectations, emitting
238  * informational or error diagnostics gated by @p log_level. User code should never call
239  * this function directly. @p log_level only filters this function's own output and is
240  * not propagated into the plugin module's runtime logger.
241  *
242  * @param lib_name Library file name (used as a tag in diagnostic output).
243  * @param local_plugin_id Plugin ID compiled into the plugin binary.
244  * @param local_version_major Major version compiled into the plugin binary.
245  * @param local_version_minor Minor version compiled into the plugin binary.
246  * @param target_plugin_id Plugin ID required by the host caller.
247  * @param target_version_major Major version required by the host caller.
248  * @param target_version_minor Minor version required by the host caller.
249  * @param log_level Threshold used to filter this function's own diagnostics.
250  * @return @c true when IDs match and @c local_major @c == @c target_major and
251  * @c local_minor @c >= @c target_minor.
252  */
253  static bool process_plugin_internal(const std::string& lib_name, const std::string& local_plugin_id,
254  uint16_t local_version_major, uint16_t local_version_minor,
255  const std::string& target_plugin_id, uint16_t target_version_major,
256  uint16_t target_version_minor, uint8_t log_level);
257 
258  private:
259  Handle load_and_create(const std::string& plugin_id, const std::string& lib_name, uint16_t version_major,
260  uint16_t version_minor, const std::string& dir_name,
261  const std::deque<std::string>& search_paths, const std::string& function_name,
262  std::shared_ptr<PluginEntry>* plugin_entry);
263 
264  bool unload(const std::string& plugin_complex_id);
265 
266  bool has_loaded(const std::string& plugin_complex_id);
267 
268  static bool destroy(std::shared_ptr<PluginEntry> plugin_entry, Handle handle,
269  const std::string& function_name = VLINK_MACRO_STRING_GET(VLINK_PLUGIN_DESTROY_FUNC_NAME));
270 
271  struct Impl;
272  std::unique_ptr<Impl> impl_;
273 
275 };
276 
277 ////////////////////////////////////////////////////////////////
278 /// Details
279 ////////////////////////////////////////////////////////////////
280 
281 template <class T>
282 inline std::shared_ptr<T> Plugin::load(const std::string& lib_name, uint16_t version_major, uint16_t version_minor,
283  const std::string& dir_name, const std::deque<std::string>& search_paths,
284  const std::string& function_name) {
285  static_assert(!T::get_plugin_id().empty(), "Plugin id can not be empty.");
286 
287  std::shared_ptr<PluginEntry> plugin_entry;
288  auto* handle = load_and_create(T::get_plugin_id().data(), lib_name, version_major, version_minor, dir_name,
289  search_paths, function_name, &plugin_entry);
290 
291  if VUNLIKELY (!handle) {
292  return nullptr;
293  }
294 
295  return std::shared_ptr<T>(static_cast<T*>(handle), [plugin_entry = std::move(plugin_entry)](T* interface_ptr) {
296  destroy(std::move(plugin_entry), interface_ptr);
297  });
298 }
299 
300 template <class T>
301 inline bool Plugin::unload(const std::string& lib_name) {
302  static_assert(!T::get_plugin_id().empty(), "Plugin id can not be empty.");
303 
304  return unload(get_plugin_complex_id<T>(lib_name));
305 }
306 
307 template <class T>
308 inline bool Plugin::has_loaded(const std::string& lib_name) {
309  static_assert(!T::get_plugin_id().empty(), "Plugin id can not be empty.");
310 
311  return has_loaded(get_plugin_complex_id<T>(lib_name));
312 }
313 
314 template <class T>
315 inline std::string Plugin::get_plugin_complex_id(const std::string& lib_name) {
316  static_assert(!T::get_plugin_id().empty(), "Plugin id can not be empty.");
317 
318  return lib_name + "@" + T::get_plugin_id().data();
319 }
320 
321 } // namespace vlink
322 
323 ////////////////////////////////////////////////////////////////
324 /// Macro Definitions
325 ////////////////////////////////////////////////////////////////
326 
327 #if defined(_WIN32) || defined(__CYGWIN__)
328 #define VLINK_PLUGIN_EXPORT __declspec(dllexport)
329 #else
330 #define VLINK_PLUGIN_EXPORT __attribute__((visibility("default")))
331 #endif
332 
333 /**
334  * @def VLINK_PLUGIN_REGISTER(InterfaceType)
335  * @brief Declares a plugin's identity from the demangled name of its abstract interface.
336  *
337  * @details
338  * Injects a @c static @c constexpr @c get_plugin_id() member that returns the demangled
339  * name of @p InterfaceType. Static assertions enforce that the interface is abstract
340  * and exposes a virtual destructor so polymorphic delete across the library boundary
341  * is well defined.
342  *
343  * @param InterfaceType Abstract interface class the plugin implements.
344  */
345 #define VLINK_PLUGIN_REGISTER(InterfaceType) \
346  public: \
347  static constexpr std::string_view get_plugin_id() { \
348  static_assert(std::is_abstract_v<InterfaceType>, "Plugin interface must be abstract class."); \
349  static_assert(std::has_virtual_destructor_v<InterfaceType>, "Plugin interface must have a virtual destructor."); \
350  return vlink::NameDetector::get<InterfaceType>(); \
351  }
352 
353 /**
354  * @def VLINK_PLUGIN_REGISTER_BY_ID(InterfaceType, PluginID)
355  * @brief Declares a plugin's identity from an explicit literal string.
356  *
357  * @details
358  * Same contract as @c VLINK_PLUGIN_REGISTER but @c get_plugin_id() returns @p PluginID
359  * instead of the demangled type name, which is useful when the plugin ID must remain
360  * stable across refactors that rename the interface class.
361  *
362  * @param InterfaceType Abstract interface class the plugin implements.
363  * @param PluginID Literal string used as the plugin identity.
364  */
365 #define VLINK_PLUGIN_REGISTER_BY_ID(InterfaceType, PluginID) \
366  public: \
367  static constexpr std::string_view get_plugin_id() { \
368  static_assert(std::is_abstract_v<InterfaceType>, "Plugin interface must be abstract class."); \
369  static_assert(std::has_virtual_destructor_v<InterfaceType>, "Plugin interface must have a virtual destructor."); \
370  return PluginID; \
371  }
372 
373 /**
374  * @def VLINK_PLUGIN_DECLARE(ImplementType, VersionMajor, VersionMinor)
375  * @brief Emits the @c extern @c "C" construction and destruction entry points exported by a plugin module.
376  *
377  * @details
378  * The construction entry point validates the plugin ID and major/minor version against
379  * the caller's expectations via @c Plugin::process_plugin_internal() and returns a new
380  * instance of @p ImplementType only when the contract holds. The destruction entry point
381  * deletes the implementation pointer.
382  *
383  * @param ImplementType Concrete class implementing the abstract interface.
384  * @param VersionMajor Major version exposed by this plugin binary.
385  * @param VersionMinor Minor version exposed by this plugin binary.
386  */
387 #define VLINK_PLUGIN_DECLARE(ImplementType, VersionMajor, VersionMinor) \
388  extern "C" { \
389  VLINK_PLUGIN_EXPORT void* VLINK_PLUGIN_CREATE_FUNC_NAME(const char* lib_name, const char* plugin_id, \
390  uint16_t version_major, uint16_t version_minor, \
391  uint8_t log_level) { \
392  static_assert(std::is_default_constructible_v<ImplementType>, \
393  "Plugin implementation must have default constructible"); \
394  static_assert(!ImplementType::get_plugin_id().empty(), "Plugin id can not be empty."); \
395  static_assert(!std::is_abstract_v<ImplementType>, "Plugin implementation cannot be an abstract class."); \
396  \
397  /*NOLINTBEGIN*/ \
398  if VUNLIKELY (!vlink::Plugin::process_plugin_internal(lib_name, ImplementType::get_plugin_id().data(), \
399  VersionMajor, VersionMinor, plugin_id, version_major, \
400  version_minor, log_level)) { \
401  return nullptr; \
402  } \
403  \
404  return new ImplementType; \
405  /*NOLINTEND*/ \
406  } \
407  \
408  VLINK_PLUGIN_EXPORT bool VLINK_PLUGIN_DESTROY_FUNC_NAME(void* handle) { \
409  if VUNLIKELY (!handle) { \
410  return false; \
411  } \
412  \
413  /*NOLINTBEGIN*/ \
414  delete static_cast<ImplementType*>(handle); \
415  \
416  return true; \
417  /*NOLINTEND*/ \
418  } \
419  }
Singleton logger with stream / format / printf / RAII-stream entry points.
Cross-platform macros for visibility, branch hints, copy prevention, singletons and string helpers.
#define VUNLIKELY(...)
Short alias for VLINK_UNLIKELY.
Definition: macros.h:289
#define VLINK_EXPORT
Definition: macros.h:81
#define VLINK_MACRO_STRING_GET(name)
Stringifies the expanded value of a macro (two-step expansion).
Definition: macros.h:261
#define VLINK_DISALLOW_COPY_AND_ASSIGN(classname)
Deletes the copy constructor and copy-assignment operator of classname.
Definition: macros.h:174
Header-only compile-time introspection of type names and enumerator labels.
#define VLINK_PLUGIN_DESTROY_FUNC_NAME
Symbol name of the plugin destruction entry point exported by VLINK_PLUGIN_DECLARE.
Definition: plugin.h:107
#define VLINK_PLUGIN_CREATE_FUNC_NAME
Symbol name of the plugin construction entry point exported by VLINK_PLUGIN_DECLARE.
Definition: plugin.h:101