VLink  2.1.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 by its library/interface pair. Loading the same pair
120  * more than once returns @c nullptr. All operations are thread safe through the internal
121  * implementation.
122  */
123 class VLINK_EXPORT Plugin final {
124  public:
125  /**
126  * @brief Opaque handle to a loaded shared library; treated as a token by the public API.
127  */
128  using Handle = void*;
129 
130  /**
131  * @brief Constructs an empty plugin manager with no libraries loaded.
132  */
134 
135  /**
136  * @brief Destroys the plugin manager and unloads every still-resident library via @c clear().
137  */
139 
140  /**
141  * @brief Sets the verbosity level used for plugin diagnostic messages.
142  *
143  * @param level Logger level to apply to plugin load/unload tracing.
144  */
146 
147  /**
148  * @brief Returns the verbosity level currently used for plugin diagnostics.
149  *
150  * @return Current logger level.
151  */
152  [[nodiscard]] Logger::Level get_log_level() const;
153 
154  /**
155  * @brief Returns the default ordered search path used when locating plugin libraries.
156  *
157  * @details
158  * The list contains, in priority order, directories from @c VLINK_PLUGIN_DIR
159  * (comma or whitespace separated), the current working directory, the executable
160  * directory, and the system library directories appropriate for the platform.
161  *
162  * @return Deque of directory paths searched left to right.
163  */
164  [[nodiscard]] static std::deque<std::string> default_search_path();
165 
166  /**
167  * @brief Loads a shared library that implements interface @c T and returns a tracked handle.
168  *
169  * @details
170  * A path-like or platform-suffixed @p lib_name that names a regular file is opened
171  * directly. Otherwise the loader appends the platform's library prefix/suffix, scans
172  * @p search_paths, and opens the first matching regular file. It then invokes the
173  * @p function_name entry point with the caller's ID and version and wraps the returned
174  * object pointer in a @c shared_ptr<T> whose deleter invokes @c vlink_plugin_destroy.
175  *
176  * @tparam T Interface type carrying @c get_plugin_id() (added by
177  * @c VLINK_PLUGIN_REGISTER or @c VLINK_PLUGIN_REGISTER_BY_ID).
178  * @param lib_name Library stem without prefix/suffix, or a direct shared-library path.
179  * @param version_major Required interface major version.
180  * @param version_minor Required interface minor version.
181  * @param dir_name Optional subdirectory tried under each entry of @p search_paths.
182  * @param search_paths Ordered fallback search list. Default: @c default_search_path().
183  * @param function_name Symbol name of the construction entry point.
184  * @return @c shared_ptr<T> owning the plugin instance, or @c nullptr on failure.
185  */
186  template <class T>
187  [[nodiscard]] std::shared_ptr<T> load(
188  const std::string& lib_name, uint16_t version_major, uint16_t version_minor, const std::string& dir_name = "",
189  const std::deque<std::string>& search_paths = default_search_path(),
190  const std::string& function_name = VLINK_MACRO_STRING_GET(VLINK_PLUGIN_CREATE_FUNC_NAME));
191 
192  /**
193  * @brief Removes a previously loaded plugin from the registry.
194  *
195  * @details
196  * The shared library is finally unmapped once every @c shared_ptr returned by
197  * @c load() has been destroyed; this call only releases the tracker entry.
198  *
199  * @tparam T Interface type used during the original @c load() call.
200  * @param lib_name Library file name passed to @c load().
201  * @return @c true when the registry entry existed and was removed.
202  */
203  template <class T>
204  bool unload(const std::string& lib_name);
205 
206  /**
207  * @brief Reports whether a plugin for interface @c T is currently registered.
208  *
209  * @tparam T Interface type used during the original @c load() call.
210  * @param lib_name Library file name passed to @c load().
211  * @return @c true when the registry entry is present.
212  */
213  template <class T>
214  [[nodiscard]] bool has_loaded(const std::string& lib_name);
215 
216  /**
217  * @brief Builds the composite key used internally to identify a (library, interface) pair.
218  *
219  * @details
220  * The key has the form @c lib_name + "@" + T::get_plugin_id() so the same shared library
221  * can be loaded twice when consumed via two different interfaces.
222  *
223  * @tparam T Interface type.
224  * @param lib_name Library file name.
225  * @return Composite identifier string.
226  */
227  template <class T>
228  [[nodiscard]] std::string get_plugin_complex_id(const std::string& lib_name);
229 
230  /**
231  * @brief Unloads every library currently tracked by this manager.
232  */
233  void clear();
234 
235  /**
236  * @brief Internal version/ID gate invoked from the @c VLINK_PLUGIN_DECLARE entry point.
237  *
238  * @details
239  * Compares the plugin's exported ID and version against the host's expectations, emitting
240  * informational or error diagnostics gated by @p log_level. User code should never call
241  * this function directly. @p log_level only filters this function's own output and is
242  * not propagated into the plugin module's runtime logger.
243  *
244  * @param lib_name Library file name (used as a tag in diagnostic output).
245  * @param local_plugin_id Plugin ID compiled into the plugin binary.
246  * @param local_version_major Major version compiled into the plugin binary.
247  * @param local_version_minor Minor version compiled into the plugin binary.
248  * @param target_plugin_id Plugin ID required by the host caller.
249  * @param target_version_major Major version required by the host caller.
250  * @param target_version_minor Minor version required by the host caller.
251  * @param log_level Threshold used to filter this function's own diagnostics.
252  * @return @c true when IDs match and @c local_major @c == @c target_major and
253  * @c local_minor @c >= @c target_minor.
254  */
255  static bool process_plugin_internal(const std::string& lib_name, const std::string& local_plugin_id,
256  uint16_t local_version_major, uint16_t local_version_minor,
257  const std::string& target_plugin_id, uint16_t target_version_major,
258  uint16_t target_version_minor, uint8_t log_level);
259 
260  private:
261  Handle load_and_create(const std::string& plugin_id, const std::string& lib_name, uint16_t version_major,
262  uint16_t version_minor, const std::string& dir_name,
263  const std::deque<std::string>& search_paths, const std::string& function_name,
264  std::shared_ptr<PluginEntry>* plugin_entry);
265 
266  bool unload(const std::string& plugin_complex_id);
267 
268  bool has_loaded(const std::string& plugin_complex_id);
269 
270  static bool destroy(std::shared_ptr<PluginEntry> plugin_entry, Handle handle,
271  const std::string& function_name = VLINK_MACRO_STRING_GET(VLINK_PLUGIN_DESTROY_FUNC_NAME));
272 
273  struct Impl;
274  std::unique_ptr<Impl> impl_;
275 
277 };
278 
279 ////////////////////////////////////////////////////////////////
280 /// Details
281 ////////////////////////////////////////////////////////////////
282 
283 template <class T>
284 inline std::shared_ptr<T> Plugin::load(const std::string& lib_name, uint16_t version_major, uint16_t version_minor,
285  const std::string& dir_name, const std::deque<std::string>& search_paths,
286  const std::string& function_name) {
287  static_assert(!T::get_plugin_id().empty(), "Plugin id can not be empty.");
288 
289  std::shared_ptr<PluginEntry> plugin_entry;
290  auto* handle = load_and_create(T::get_plugin_id().data(), lib_name, version_major, version_minor, dir_name,
291  search_paths, function_name, &plugin_entry);
292 
293  if VUNLIKELY (!handle) {
294  return nullptr;
295  }
296 
297  return std::shared_ptr<T>(static_cast<T*>(handle), [plugin_entry = std::move(plugin_entry)](T* interface_ptr) {
298  destroy(std::move(plugin_entry), interface_ptr);
299  });
300 }
301 
302 template <class T>
303 inline bool Plugin::unload(const std::string& lib_name) {
304  static_assert(!T::get_plugin_id().empty(), "Plugin id can not be empty.");
305 
306  return unload(get_plugin_complex_id<T>(lib_name));
307 }
308 
309 template <class T>
310 inline bool Plugin::has_loaded(const std::string& lib_name) {
311  static_assert(!T::get_plugin_id().empty(), "Plugin id can not be empty.");
312 
313  return has_loaded(get_plugin_complex_id<T>(lib_name));
314 }
315 
316 template <class T>
317 inline std::string Plugin::get_plugin_complex_id(const std::string& lib_name) {
318  static_assert(!T::get_plugin_id().empty(), "Plugin id can not be empty.");
319 
320  return lib_name + "@" + T::get_plugin_id().data();
321 }
322 
323 } // namespace vlink
324 
325 ////////////////////////////////////////////////////////////////
326 /// Macro Definitions
327 ////////////////////////////////////////////////////////////////
328 
329 #if defined(_WIN32) || defined(__CYGWIN__)
330 #define VLINK_PLUGIN_EXPORT __declspec(dllexport)
331 #else
332 #define VLINK_PLUGIN_EXPORT __attribute__((visibility("default")))
333 #endif
334 
335 /**
336  * @def VLINK_PLUGIN_REGISTER(InterfaceType)
337  * @brief Declares a plugin's identity from the demangled name of its abstract interface.
338  *
339  * @details
340  * Injects a @c static @c constexpr @c get_plugin_id() member that returns the demangled
341  * name of @p InterfaceType. Static assertions enforce that the interface is abstract
342  * and exposes a virtual destructor so polymorphic delete across the library boundary
343  * is well defined.
344  *
345  * @param InterfaceType Abstract interface class the plugin implements.
346  */
347 #define VLINK_PLUGIN_REGISTER(InterfaceType) \
348  public: \
349  static constexpr std::string_view get_plugin_id() { \
350  static_assert(std::is_abstract_v<InterfaceType>, "Plugin interface must be abstract class."); \
351  static_assert(std::has_virtual_destructor_v<InterfaceType>, "Plugin interface must have a virtual destructor."); \
352  return vlink::NameDetector::get<InterfaceType>(); \
353  }
354 
355 /**
356  * @def VLINK_PLUGIN_REGISTER_BY_ID(InterfaceType, PluginID)
357  * @brief Declares a plugin's identity from an explicit literal string.
358  *
359  * @details
360  * Same contract as @c VLINK_PLUGIN_REGISTER but @c get_plugin_id() returns @p PluginID
361  * instead of the demangled type name, which is useful when the plugin ID must remain
362  * stable across refactors that rename the interface class.
363  *
364  * @param InterfaceType Abstract interface class the plugin implements.
365  * @param PluginID Literal string used as the plugin identity.
366  */
367 #define VLINK_PLUGIN_REGISTER_BY_ID(InterfaceType, PluginID) \
368  public: \
369  static constexpr std::string_view get_plugin_id() { \
370  static_assert(std::is_abstract_v<InterfaceType>, "Plugin interface must be abstract class."); \
371  static_assert(std::has_virtual_destructor_v<InterfaceType>, "Plugin interface must have a virtual destructor."); \
372  return PluginID; \
373  }
374 
375 /**
376  * @def VLINK_PLUGIN_DECLARE(ImplementType, VersionMajor, VersionMinor)
377  * @brief Emits the @c extern @c "C" construction and destruction entry points exported by a plugin module.
378  *
379  * @details
380  * The construction entry point validates the plugin ID and major/minor version against
381  * the caller's expectations via @c Plugin::process_plugin_internal() and returns a new
382  * instance of @p ImplementType only when the contract holds. The destruction entry point
383  * deletes the implementation pointer.
384  *
385  * @param ImplementType Concrete class implementing the abstract interface.
386  * @param VersionMajor Major version exposed by this plugin binary.
387  * @param VersionMinor Minor version exposed by this plugin binary.
388  */
389 #define VLINK_PLUGIN_DECLARE(ImplementType, VersionMajor, VersionMinor) \
390  extern "C" { \
391  VLINK_PLUGIN_EXPORT void* VLINK_PLUGIN_CREATE_FUNC_NAME(const char* lib_name, const char* plugin_id, \
392  uint16_t version_major, uint16_t version_minor, \
393  uint8_t log_level) { \
394  static_assert(std::is_default_constructible_v<ImplementType>, \
395  "Plugin implementation must have default constructible"); \
396  static_assert(!ImplementType::get_plugin_id().empty(), "Plugin id can not be empty."); \
397  static_assert(!std::is_abstract_v<ImplementType>, "Plugin implementation cannot be an abstract class."); \
398  \
399  /*NOLINTBEGIN*/ \
400  if VUNLIKELY (!vlink::Plugin::process_plugin_internal(lib_name, ImplementType::get_plugin_id().data(), \
401  VersionMajor, VersionMinor, plugin_id, version_major, \
402  version_minor, log_level)) { \
403  return nullptr; \
404  } \
405  \
406  return new ImplementType; \
407  /*NOLINTEND*/ \
408  } \
409  \
410  VLINK_PLUGIN_EXPORT bool VLINK_PLUGIN_DESTROY_FUNC_NAME(void* handle) { \
411  if VUNLIKELY (!handle) { \
412  return false; \
413  } \
414  \
415  /*NOLINTBEGIN*/ \
416  delete static_cast<ImplementType*>(handle); \
417  \
418  return true; \
419  /*NOLINTEND*/ \
420  } \
421  }
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