VLink 2.0.0
A high-performance communication middleware
Loading...
Searching...
No Matches
plugin.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 plugin.h
26 * @brief Type-safe dynamic plugin loader with version checking and lifecycle management.
27 *
28 * @details
29 * @c Plugin wraps @c dlopen / @c LoadLibrary to load shared libraries that implement a
30 * given abstract C++ interface. Each plugin library must use the @c VLINK_PLUGIN_DECLARE
31 * macro to export @c vlink_plugin_create and @c vlink_plugin_destroy entry points.
32 *
33 * Plugin ID:
34 * Every interface type @c T has a unique ID derived from its demangled type name (via
35 * @c NameDetector::get<T>()) or from a user-supplied literal (via @c VLINK_PLUGIN_REGISTER_BY_ID).
36 * The @c Plugin loader verifies that the ID embedded in the shared library matches the
37 * caller's expected interface type before returning a @c shared_ptr<T>.
38 *
39 * Version checking:
40 * The caller specifies a required major/minor version. @c process_plugin_internal() performs
41 * the check inside the library's entry point and returns @c nullptr if the versions are
42 * incompatible, preventing ABI mismatches.
43 *
44 * Lifecycle:
45 * - @c load<T>() opens the library, calls @c vlink_plugin_create, and wraps the result in
46 * a @c shared_ptr<T> with a custom deleter that calls @c vlink_plugin_destroy.
47 * - @c unload<T>() decrements the reference count; the library is closed when the count
48 * reaches zero.
49 * - @c clear() unloads all libraries.
50 *
51 * Macros (defined in this header):
52 *
53 * | Macro | Purpose |
54 * | --------------------------------- | ----------------------------------------------- |
55 * | @c VLINK_PLUGIN_REGISTER | In concrete class: derive plugin ID from type |
56 * | @c VLINK_PLUGIN_REGISTER_BY_ID | In concrete class: use a literal string as ID |
57 * | @c VLINK_PLUGIN_DECLARE | In .cpp: export create/destroy entry points |
58 *
59 * @par Example
60 * @code
61 * // Interface header (my_plugin.h):
62 * class MyPlugin {
63 * VLINK_PLUGIN_REGISTER(MyPlugin)
64 * public:
65 * virtual ~MyPlugin() = default;
66 * virtual void do_work() = 0;
67 * };
68 *
69 * // Implementation .cpp (my_plugin_impl.cpp):
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 * // Loader:
78 * vlink::Plugin plugin;
79 * auto impl = plugin.load<MyPlugin>("my_plugin_impl", 1, 0);
80 * if (impl) {
81 * impl->do_work();
82 * }
83 * @endcode
84 */
85
86#pragma once
87
88#include <cstdint>
89#include <deque>
90#include <memory>
91#include <string>
92#include <string_view>
93
94#include "./logger.h"
95#include "./macros.h"
96#include "./name_detector.h"
97
98/**
99 * @def VLINK_PLUGIN_CREATE_FUNC_NAME
100 * @brief Name of the plugin creation entry point exported by @c VLINK_PLUGIN_DECLARE.
101 */
102#define VLINK_PLUGIN_CREATE_FUNC_NAME vlink_plugin_create
103
104/**
105 * @def VLINK_PLUGIN_DESTROY_FUNC_NAME
106 * @brief Name of the plugin destruction entry point exported by @c VLINK_PLUGIN_DECLARE.
107 */
108#define VLINK_PLUGIN_DESTROY_FUNC_NAME vlink_plugin_destroy
109
110namespace vlink {
111
112struct PluginEntry;
113
114/**
115 * @class Plugin
116 * @brief Type-safe dynamic plugin loader with version verification and lifecycle management.
117 *
118 * @details
119 * Loads one or more shared library plugins by interface type @c T.
120 * Multiple distinct interface types may be loaded by a single @c Plugin instance.
121 */
122class VLINK_EXPORT Plugin final {
123 public:
124 /**
125 * @brief Opaque handle to a loaded shared library object (used internally).
126 */
127 using Handle = void*;
128
129 /**
130 * @brief Constructs a @c Plugin manager with an empty library registry.
131 */
133
134 /**
135 * @brief Destructor. Calls @c clear() to unload all still-loaded libraries.
136 */
138
139 /**
140 * @brief Sets the log level used for plugin load/unload diagnostics.
141 *
142 * @param level Logger level.
143 */
145
146 /**
147 * @brief Returns the current log level for plugin diagnostics.
148 *
149 * @return Current log level.
150 */
151 [[nodiscard]] Logger::Level get_log_level() const;
152
153 /**
154 * @brief Returns the default search path list for finding plugin shared libraries.
155 *
156 * @details
157 * Includes the executable directory, the system library directories, and
158 * the current working directory.
159 *
160 * @return Deque of directory path strings to search in order.
161 */
162 [[nodiscard]] static std::deque<std::string> default_search_path();
163
164 /**
165 * @brief Loads a plugin implementing interface @c T from a shared library.
166 *
167 * @details
168 * -# Resolves the library file by searching @p search_paths for a file whose name
169 * matches @p lib_name (with platform-appropriate prefix/suffix).
170 * -# Opens the library with @c dlopen (or @c LoadLibrary on Windows).
171 * -# Calls the @c vlink_plugin_create entry point with version and ID information.
172 * -# Returns a @c shared_ptr<T> whose custom deleter calls @c vlink_plugin_destroy.
173 *
174 * @tparam T Interface type. Must have a @c get_plugin_id() static method
175 * (inserted by @c VLINK_PLUGIN_REGISTER or @c VLINK_PLUGIN_REGISTER_BY_ID).
176 * @param lib_name File name of the shared library (without prefix/suffix).
177 * @param version_major Required major version.
178 * @param version_minor Required minor version.
179 * @param dir_name Optional explicit directory to search first. Default: empty.
180 * @param search_paths Ordered list of directories to search. Default: @c default_search_path().
181 * @param function_name Name of the creation entry point. Default: @c vlink_plugin_create.
182 * @return @c shared_ptr<T> on success, 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 Unloads the plugin library for interface @c T.
192 *
193 * @details
194 * Removes the library from the internal registry and decrements the reference count.
195 * The library is actually closed when all @c shared_ptr instances to the plugin object
196 * are destroyed.
197 *
198 * @tparam T Interface type.
199 * @param lib_name Library file name used during @c load().
200 * @return @c true if the library was found and unloaded.
201 */
202 template <class T>
203 bool unload(const std::string& lib_name);
204
205 /**
206 * @brief Returns @c true if the plugin for interface @c T is currently loaded.
207 *
208 * @tparam T Interface type.
209 * @param lib_name Library file name used during @c load().
210 * @return @c true if loaded.
211 */
212 template <class T>
213 [[nodiscard]] bool has_loaded(const std::string& lib_name);
214
215 /**
216 * @brief Returns the composite key used internally to identify a (library, interface) pair.
217 *
218 * @details
219 * The key is @c lib_name + "@" + T::get_plugin_id().
220 *
221 * @tparam T Interface type.
222 * @param lib_name Library file name.
223 * @return Composite ID string.
224 */
225 template <class T>
226 [[nodiscard]] std::string get_plugin_complex_id(const std::string& lib_name);
227
228 /**
229 * @brief Unloads all loaded plugin libraries.
230 *
231 * @details
232 * Equivalent to calling @c unload<T>(lib_name) for every previously loaded plugin.
233 */
234 void clear();
235
236 /**
237 * @brief Internal entry-point handler called from @c VLINK_PLUGIN_DECLARE.
238 *
239 * @details
240 * Validates the plugin ID and version, then configures the logger level inside the plugin.
241 * Should not be called directly by user code.
242 *
243 * @param lib_name Library name (for logging).
244 * @param local_plugin_id ID exported by the plugin implementation.
245 * @param local_version_major Major version exported by the plugin.
246 * @param local_version_minor Minor version exported by the plugin.
247 * @param target_plugin_id ID expected by the caller (from @c T::get_plugin_id()).
248 * @param target_version_major Major version required by the caller.
249 * @param target_version_minor Minor version required by the caller.
250 * @param log_level Logger level to propagate into the plugin.
251 * @return @c true if the ID and version match.
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 std::unique_ptr<struct PluginImpl> impl_;
272
274};
275
276////////////////////////////////////////////////////////////////
277/// Details
278////////////////////////////////////////////////////////////////
279
280template <class T>
281inline std::shared_ptr<T> Plugin::load(const std::string& lib_name, uint16_t version_major, uint16_t version_minor,
282 const std::string& dir_name, const std::deque<std::string>& search_paths,
283 const std::string& function_name) {
284 static_assert(!T::get_plugin_id().empty(), "Plugin id can not be empty.");
285
286 std::shared_ptr<PluginEntry> plugin_entry;
287 auto* handle = load_and_create(T::get_plugin_id().data(), lib_name, version_major, version_minor, dir_name,
288 search_paths, function_name, &plugin_entry);
289
290 if VUNLIKELY (!handle) {
291 return nullptr;
292 }
293
294 return std::shared_ptr<T>(static_cast<T*>(handle), [plugin_entry = std::move(plugin_entry)](T* interface_ptr) {
295 destroy(std::move(plugin_entry), interface_ptr);
296 });
297}
298
299template <class T>
300inline bool Plugin::unload(const std::string& lib_name) {
301 static_assert(!T::get_plugin_id().empty(), "Plugin id can not be empty.");
302
303 return unload(get_plugin_complex_id<T>(lib_name));
304}
305
306template <class T>
307inline bool Plugin::has_loaded(const std::string& lib_name) {
308 static_assert(!T::get_plugin_id().empty(), "Plugin id can not be empty.");
309
310 return has_loaded(get_plugin_complex_id<T>(lib_name));
311}
312
313template <class T>
314inline std::string Plugin::get_plugin_complex_id(const std::string& lib_name) {
315 static_assert(!T::get_plugin_id().empty(), "Plugin id can not be empty.");
316
317 return lib_name + "@" + T::get_plugin_id().data();
318}
319
320} // namespace vlink
321
322////////////////////////////////////////////////////////////////
323/// Macro Definitions
324////////////////////////////////////////////////////////////////
325
326#if defined(_WIN32) || defined(__CYGWIN__)
327#define VLINK_PLUGIN_EXPORT __declspec(dllexport)
328#else
329#define VLINK_PLUGIN_EXPORT __attribute__((visibility("default")))
330#endif
331
332/**
333 * @brief Macro to register a plugin, automatically deriving its ID from the interface type name.
334 *
335 * This macro should be used within the definition of a concrete plugin class.
336 * It defines a static constexpr member function `get_plugin_id()` that returns
337 * the name of the `InterfaceType` as the plugin's ID.
338 * It also includes a static assertion to ensure that the `InterfaceType` is an abstract class.
339 *
340 * @param InterfaceType The abstract interface class that the plugin implements.
341 * The plugin ID will be derived from the name of this type.
342 */
343#define VLINK_PLUGIN_REGISTER(InterfaceType) \
344 public: \
345 static constexpr std::string_view get_plugin_id() { \
346 static_assert(std::is_abstract_v<InterfaceType>, "Plugin interface must be abstract class."); \
347 static_assert(std::has_virtual_destructor_v<InterfaceType>, "Plugin interface must have a virtual destructor."); \
348 return vlink::NameDetector::get<InterfaceType>(); \
349 }
350
351/**
352 * @brief Macro to register a plugin with a specific, user-provided ID.
353 *
354 * This macro should be used within the definition of a concrete plugin class when you
355 * want to explicitly specify the plugin's ID, rather than deriving it from the interface type name.
356 * It defines a static constexpr member function `get_plugin_id()` that returns the provided `PluginID`.
357 * It also includes a static assertion to ensure that the `InterfaceType` is an abstract class.
358 *
359 * @param InterfaceType The abstract interface class that the plugin implements.
360 * @param PluginID The string literal to be used as the plugin's unique identifier.
361 */
362#define VLINK_PLUGIN_REGISTER_BY_ID(InterfaceType, PluginID) \
363 public: \
364 static constexpr std::string_view get_plugin_id() { \
365 static_assert(std::is_abstract_v<InterfaceType>, "Plugin interface must be abstract class."); \
366 static_assert(std::has_virtual_destructor_v<InterfaceType>, "Plugin interface must have a virtual destructor."); \
367 return PluginID; \
368 }
369
370/**
371 * @brief Declares a plugin creation and destruction interface.
372 *
373 * This macro declares the plugin entry points for creating and destroying the plugin interface.
374 * These functions are used by the `Plugin` class to load and unload the plugin.
375 *
376 * @param ImplementType The concrete class implementing the plugin interface.
377 * @param VersionMajor The major version number of the plugin.
378 * @param VersionMinor The minor version number of the plugin.
379 */
380#define VLINK_PLUGIN_DECLARE(ImplementType, VersionMajor, VersionMinor) \
381 extern "C" { \
382 VLINK_PLUGIN_EXPORT void* VLINK_PLUGIN_CREATE_FUNC_NAME(const char* lib_name, const char* plugin_id, \
383 uint16_t version_major, uint16_t version_minor, \
384 uint8_t log_level) { \
385 static_assert(std::is_default_constructible_v<ImplementType>, \
386 "Plugin implementation must have default constructible"); \
387 static_assert(!ImplementType::get_plugin_id().empty(), "Plugin id can not be empty."); \
388 static_assert(!std::is_abstract_v<ImplementType>, "Plugin implementation cannot be an abstract class."); \
389 \
390 /*NOLINTBEGIN*/ \
391 if VUNLIKELY (!vlink::Plugin::process_plugin_internal(lib_name, ImplementType::get_plugin_id().data(), \
392 VersionMajor, VersionMinor, plugin_id, version_major, \
393 version_minor, log_level)) { \
394 return nullptr; \
395 } \
396 \
397 return new ImplementType; \
398 /*NOLINTEND*/ \
399 } \
400 \
401 VLINK_PLUGIN_EXPORT bool VLINK_PLUGIN_DESTROY_FUNC_NAME(void* handle) { \
402 if VUNLIKELY (!handle) { \
403 return false; \
404 } \
405 \
406 /*NOLINTBEGIN*/ \
407 delete static_cast<ImplementType*>(handle); \
408 \
409 return true; \
410 /*NOLINTEND*/ \
411 } \
412 }
Global singleton logger with three output styles and pluggable backends.
Platform-independent macro definitions for the VLink library.
#define VUNLIKELY(...)
Shorthand alias for VLINK_UNLIKELY. Hints that the expression is unlikely true.
Definition macros.h:302
#define VLINK_EXPORT
Definition macros.h:85
#define VLINK_MACRO_STRING_GET(name)
Stringifies the expanded value of macro name (two-level expansion).
Definition macros.h:274
#define VLINK_DISALLOW_COPY_AND_ASSIGN(classname)
Deletes the copy constructor and copy-assignment operator of classname.
Definition macros.h:184
Compile-time type-name and enum-name detection utilities.
#define VLINK_PLUGIN_DESTROY_FUNC_NAME
Name of the plugin destruction entry point exported by VLINK_PLUGIN_DECLARE.
Definition plugin.h:108
#define VLINK_PLUGIN_CREATE_FUNC_NAME
Name of the plugin creation entry point exported by VLINK_PLUGIN_DECLARE.
Definition plugin.h:102