Branch data Line data Source code
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 : : #include "./base/plugin.h"
25 : :
26 : : #include <deque>
27 : : #include <filesystem>
28 : : #include <memory>
29 : : #include <mutex>
30 : : #include <shared_mutex>
31 : : #include <stdexcept>
32 : : #include <string>
33 : : #include <unordered_map>
34 : : #include <utility>
35 : :
36 : : #if defined(_WIN32) || defined(_WIN64)
37 : : #ifndef WIN32_LEAN_AND_MEAN
38 : : #define WIN32_LEAN_AND_MEAN
39 : : #define VLINK_PLUGIN_UNDEFINE_LEAN_AND_MEAN
40 : : #endif
41 : : #ifndef NOMINMAX
42 : : #define NOMINMAX
43 : : #define VLINK_PLUGIN_UNDEFINE_NOMINMAX
44 : : #endif
45 : : #include <windows.h>
46 : : #ifdef VLINK_PLUGIN_UNDEFINE_LEAN_AND_MEAN
47 : : #undef WIN32_LEAN_AND_MEAN
48 : : #undef VLINK_PLUGIN_UNDEFINE_LEAN_AND_MEAN
49 : : #endif
50 : : #ifdef VLINK_PLUGIN_UNDEFINE_NOMINMAX
51 : : #undef NOMINMAX
52 : : #undef VLINK_PLUGIN_UNDEFINE_NOMINMAX
53 : : #endif
54 : : #else
55 : : #include <dlfcn.h>
56 : : #endif
57 : :
58 : : #include "./base/helpers.h"
59 : : #include "./base/logger.h"
60 : : #include "./base/utils.h"
61 : :
62 : : namespace vlink {
63 : :
64 : 9 : [[maybe_unused]] static std::string get_current_dir() {
65 : : try {
66 [ + - + - ]: 18 : return std::filesystem::current_path().string();
67 : : } catch (std::filesystem::filesystem_error&) { // LCOV_EXCL_LINE GCOVR_EXCL_LINE
68 : : return "."; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
69 : : } // LCOV_EXCL_LINE GCOVR_EXCL_LINE
70 : : }
71 : :
72 : 52 : [[maybe_unused]] static bool check_exists(const std::string& path) {
73 : : try {
74 [ + - + - ]: 52 : return std::filesystem::exists(path);
75 : : } catch (std::filesystem::filesystem_error&) { // LCOV_EXCL_LINE GCOVR_EXCL_LINE
76 : : return false; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
77 : : } // LCOV_EXCL_LINE GCOVR_EXCL_LINE
78 : : }
79 : :
80 : : // DynamicLibrary
81 : : class DynamicLibrary final {
82 : : public:
83 : : #if defined(_WIN32) || defined(_WIN64)
84 : : static constexpr const char* kFilenamePrefix = "";
85 : : static constexpr const char* kFilenameSuffix = ".dll";
86 : : using NativeHandle = HINSTANCE;
87 : : using NativeSymbol = FARPROC;
88 : : #elif defined(__APPLE__)
89 : : static constexpr const char* kFilenamePrefix = "lib";
90 : : static constexpr const char* kFilenameSuffix = ".dylib";
91 : : using NativeHandle = void*;
92 : : using NativeSymbol = void*;
93 : : #else
94 : : static constexpr const char* kFilenamePrefix = "lib";
95 : : static constexpr const char* kFilenameSuffix = ".so";
96 : : using NativeHandle = void*;
97 : : using NativeSymbol = void*;
98 : : #endif
99 : :
100 : : static_assert(std::is_pointer_v<NativeHandle>, "Expecting native handle to be a pointer.");
101 : : static_assert(std::is_pointer_v<NativeSymbol>, "Expecting native symbol to be a pointer.");
102 : :
103 : : class Exception : public std::runtime_error {
104 : : using std::runtime_error::runtime_error;
105 : : };
106 : :
107 : : class LoadError : public Exception {
108 : : using Exception::Exception;
109 : : };
110 : :
111 : : class SymbolError : public Exception {
112 : : using Exception::Exception;
113 : : };
114 : :
115 : 5 : explicit DynamicLibrary(const std::string& path) {
116 : 5 : handle_ = open_library(path.c_str());
117 : :
118 [ + + ]: 5 : if VUNLIKELY (!handle_) {
119 [ + - + - : 1 : throw LoadError("Could not load library \"" + path + "\"\n" + get_error_description());
+ - + - ]
120 : : }
121 : 4 : }
122 : :
123 : 4 : ~DynamicLibrary() {
124 [ + - ]: 4 : if VLIKELY (handle_) {
125 : 4 : close_library(handle_);
126 : : }
127 : 4 : }
128 : :
129 : : DynamicLibrary(DynamicLibrary&& other) noexcept : handle_(other.handle_) { other.handle_ = nullptr; }
130 : :
131 : : DynamicLibrary& operator=(DynamicLibrary&& other) noexcept {
132 : : if VLIKELY (this != &other) {
133 : : std::swap(handle_, other.handle_);
134 : : }
135 : :
136 : : return *this;
137 : : }
138 : :
139 : : template <typename SignatureT>
140 : 6 : SignatureT* get_function(const std::string& name) const {
141 [ - + ]: 6 : if VUNLIKELY (!handle_) {
142 [ # # ]: 0 : throw std::logic_error("The dynamic library handle is null. This object may have been moved from.");
143 : : }
144 : :
145 : 6 : auto* symbol = locate_symbol(handle_, name.c_str());
146 : :
147 [ + + ]: 6 : if VUNLIKELY (!symbol) {
148 [ + - + - : 1 : throw SymbolError("Could not get symbol \"" + name + "\"\n" + get_error_description());
+ - + - ]
149 : : }
150 : :
151 : : #if defined(__GNUC__) && __GNUC__ >= 8
152 : : #pragma GCC diagnostic push
153 : : #pragma GCC diagnostic ignored "-Wcast-function-type"
154 : : #endif
155 : :
156 : 5 : return reinterpret_cast<SignatureT*>(symbol);
157 : :
158 : : #if defined(__GNUC__) && __GNUC__ >= 8
159 : : #pragma GCC diagnostic pop
160 : : #endif
161 : : }
162 : :
163 : : NativeHandle native_handle() const noexcept { return handle_; }
164 : :
165 : : private:
166 : 5 : static NativeHandle open_library(const char* path) noexcept {
167 : : #if defined(_WIN32) || defined(_WIN64)
168 : : return ::LoadLibraryA(path);
169 : : #else
170 : 5 : return ::dlopen(path, RTLD_NOW | RTLD_LOCAL);
171 : : #endif
172 : : }
173 : :
174 : 6 : static NativeSymbol locate_symbol(NativeHandle lib, const char* name) noexcept {
175 : : #if defined(_WIN32) || defined(_WIN64)
176 : : return ::GetProcAddress(lib, name);
177 : : #else
178 : 6 : return ::dlsym(lib, name);
179 : : #endif
180 : : }
181 : :
182 : 4 : static void close_library(NativeHandle lib) noexcept {
183 : : #if defined(_WIN32) || defined(_WIN64)
184 : : ::FreeLibrary(lib);
185 : : #else
186 : 4 : ::dlclose(lib);
187 : : #endif
188 : 4 : }
189 : :
190 : 2 : static std::string get_error_description() noexcept {
191 : : #if defined(_WIN32) || defined(_WIN64)
192 : : constexpr size_t kBufferSize = 512;
193 : : const auto error_code = ::GetLastError();
194 : :
195 : : if (!error_code) {
196 : : return "No error reported by GetLastError";
197 : : }
198 : :
199 : : char description[kBufferSize];
200 : :
201 : : const auto language = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
202 : : const DWORD length =
203 : : ::FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM, nullptr, error_code, language, description, kBufferSize, nullptr);
204 : :
205 : : return (length == 0) ? "Unknown error (FormatMessage failed)" : description;
206 : : #else
207 : 2 : const auto* description = ::dlerror();
208 : :
209 [ + - ]: 2 : return (description == nullptr) ? "No error reported by dlerror" : description;
210 : : #endif
211 : : }
212 : :
213 : : NativeHandle handle_{nullptr};
214 : :
215 : : VLINK_DISALLOW_COPY_AND_ASSIGN(DynamicLibrary)
216 : : };
217 : :
218 : : // PluginEntry
219 : : struct PluginEntry final {
220 : : std::unique_ptr<DynamicLibrary> loader;
221 : : std::string plugin_complex_id;
222 : : Logger::Level log_level{Logger::kTrace};
223 : : };
224 : :
225 : : // Plugin::Impl
226 : : struct Plugin::Impl final {
227 : : Logger::Level log_level{Logger::kTrace};
228 : : std::shared_mutex mtx;
229 : : std::unordered_map<std::string, std::shared_ptr<PluginEntry>> plugin_map;
230 : : };
231 : :
232 : : // Plugin
233 : 173 : Plugin::Plugin() : impl_(std::make_unique<Impl>()) {}
234 : :
235 : 4 : void Plugin::set_log_level(Logger::Level level) { impl_->log_level = level; }
236 : :
237 : 2 : Logger::Level Plugin::get_log_level() const { return impl_->log_level; }
238 : :
239 : 173 : Plugin::~Plugin() = default;
240 : :
241 : 9 : std::deque<std::string> Plugin::default_search_path() {
242 [ + - ]: 9 : const std::string current_dir = get_current_dir();
243 [ + - + - ]: 18 : const std::string plugin_dir_env = Utils::get_env("VLINK_PLUGIN_DIR");
244 : 9 : const std::string local_app_dir = Utils::get_app_dir();
245 : :
246 : : std::deque<std::string> search_path{
247 : : current_dir,
248 : : local_app_dir,
249 : : local_app_dir + "/../lib64",
250 : : local_app_dir + "/../lib",
251 : : local_app_dir + "/lib64",
252 : : local_app_dir + "/lib",
253 : : "/lib64", // LCOV_EXCL_LINE GCOVR_EXCL_LINE
254 : : "/lib",
255 [ + - + - : 99 : };
+ - + - +
- + - + -
+ - + - ]
256 : :
257 : 9 : const auto plugin_dirs = Helpers::split_any(plugin_dir_env);
258 : :
259 [ + + ]: 9 : if (!plugin_dirs.empty()) {
260 [ + - ]: 1 : search_path.insert(search_path.begin(), plugin_dirs.begin(), plugin_dirs.end());
261 : : }
262 : :
263 : 18 : return search_path;
264 : 9 : }
265 : :
266 : 59 : void Plugin::clear() {
267 [ + - ]: 59 : std::lock_guard lock(impl_->mtx);
268 : 59 : impl_->plugin_map.clear();
269 : 59 : }
270 : :
271 : 14 : Plugin::Handle Plugin::load_and_create(const std::string& plugin_id, const std::string& lib_name,
272 : : uint16_t version_major, uint16_t version_minor, const std::string& dir_name,
273 : : const std::deque<std::string>& search_paths, const std::string& function_name,
274 : : std::shared_ptr<PluginEntry>* plugin_entry) {
275 [ - + ]: 14 : if VUNLIKELY (plugin_id.empty()) {
276 : : if (impl_->log_level <= Logger::kError) { // LCOV_EXCL_LINE GCOVR_EXCL_LINE
277 : : VLOG_E("Plugin: Plugin id is empty."); // LCOV_EXCL_LINE GCOVR_EXCL_LINE
278 : : }
279 : :
280 : : return nullptr; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
281 : : }
282 : :
283 [ + + ]: 14 : if VUNLIKELY (lib_name.empty()) {
284 [ + - ]: 1 : if (impl_->log_level <= Logger::kError) {
285 [ + - + - ]: 2 : VLOG_E("Plugin: Lib name is empty.");
286 : : }
287 : :
288 : 1 : return nullptr;
289 : : }
290 : :
291 [ + - + - ]: 13 : std::string plugin_complex_id = lib_name + "@" + plugin_id;
292 : :
293 : : {
294 [ + - ]: 13 : std::unique_lock load_lock(impl_->mtx);
295 : :
296 [ + - + + ]: 13 : if VUNLIKELY (impl_->plugin_map.count(plugin_complex_id) != 0U) {
297 [ + - ]: 1 : if (impl_->log_level <= Logger::kError) {
298 [ + - + - ]: 2 : VLOG_E("Plugin: Already loaded (", plugin_complex_id, ").");
299 : : }
300 : :
301 : 1 : return nullptr;
302 : : }
303 [ + + ]: 13 : }
304 : :
305 : 12 : std::string plugin_path;
306 : :
307 : : {
308 [ + - + - ]: 12 : std::string plugin_name = DynamicLibrary::kFilenamePrefix + lib_name + DynamicLibrary::kFilenameSuffix;
309 : 12 : std::string check_path;
310 : :
311 [ + + ]: 59 : for (const auto& path : search_paths) {
312 [ + + ]: 52 : if (dir_name.empty()) {
313 : : // NOLINTNEXTLINE(performance-inefficient-string-concatenation)
314 [ + - + - ]: 51 : check_path = path + "/" + plugin_name;
315 : : } else {
316 : : // NOLINTNEXTLINE(performance-inefficient-string-concatenation)
317 [ + - + - : 1 : check_path = path + "/" + dir_name + "/" + plugin_name;
+ - + - ]
318 : : }
319 : :
320 [ + - + + ]: 52 : if (check_exists(check_path)) {
321 : 5 : plugin_path = std::move(check_path);
322 : 5 : break;
323 : : }
324 : : }
325 : 12 : }
326 : :
327 : 12 : Handle handle = nullptr;
328 : :
329 [ + + ]: 12 : if VUNLIKELY (plugin_path.empty()) {
330 [ + + ]: 7 : if (impl_->log_level <= Logger::kError) {
331 [ + - + - ]: 12 : VLOG_E("Plugin: Cannot find plugin (", plugin_complex_id, ").");
332 : : }
333 : :
334 : 7 : return handle;
335 : : }
336 : :
337 [ + + ]: 5 : if (impl_->log_level <= Logger::kTrace) {
338 [ + - + - ]: 8 : VLOG_T("Plugin: Loading plugin: ", plugin_path, ".");
339 : : }
340 : :
341 : : try {
342 [ + - ]: 5 : auto entry = std::make_shared<PluginEntry>();
343 [ + + ]: 5 : entry->loader = std::make_unique<DynamicLibrary>(std::move(plugin_path));
344 [ + - ]: 4 : entry->plugin_complex_id = plugin_complex_id;
345 : 4 : entry->log_level = impl_->log_level;
346 : :
347 : : auto create_function =
348 [ + + ]: 4 : entry->loader->get_function<Handle(const char*, const char*, uint16_t, uint16_t, uint8_t)>(function_name);
349 : :
350 [ - + ]: 3 : if VUNLIKELY (!create_function) {
351 : : // LCOV_EXCL_START GCOVR_EXCL_START
352 : : if (impl_->log_level <= Logger::kError) {
353 : : VLOG_E("Plugin: Cannot find symbol function to create (", plugin_complex_id, ").");
354 : : }
355 : :
356 : : return handle;
357 : : // LCOV_EXCL_STOP GCOVR_EXCL_STOP
358 : : }
359 : :
360 [ + - ]: 3 : handle = create_function(lib_name.c_str(), plugin_id.c_str(), version_major, version_minor, impl_->log_level);
361 : :
362 [ + + ]: 3 : if VUNLIKELY (!handle) {
363 [ + - ]: 1 : if (impl_->log_level <= Logger::kError) {
364 [ + - + - ]: 2 : VLOG_E("Plugin: Failed to create handle (", plugin_complex_id, ").");
365 : : }
366 : :
367 : 1 : return handle;
368 : : }
369 : :
370 [ + + ]: 2 : if (impl_->log_level <= Logger::kTrace) {
371 [ + - + - ]: 2 : VLOG_T("Plugin: Loaded successfully (", plugin_complex_id, ").");
372 : : }
373 : :
374 : 2 : bool inserted = false;
375 : :
376 : : {
377 [ + - ]: 2 : std::unique_lock load_lock(impl_->mtx);
378 [ + - ]: 2 : auto [iter, emplaced] = impl_->plugin_map.emplace(plugin_complex_id, entry);
379 : : (void)iter;
380 : 2 : inserted = emplaced;
381 : 2 : }
382 : :
383 [ - + ]: 2 : if VUNLIKELY (!inserted) {
384 : : destroy(entry, handle); // LCOV_EXCL_LINE GCOVR_EXCL_LINE
385 : : return nullptr; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
386 : : }
387 : :
388 [ + - ]: 2 : if (plugin_entry) {
389 : 2 : *plugin_entry = entry;
390 : : }
391 : :
392 : 2 : return handle;
393 [ - + ]: 7 : } catch (const DynamicLibrary::Exception& e) {
394 [ + - ]: 2 : if (impl_->log_level <= Logger::kError) {
395 [ + - + - ]: 4 : VLOG_E("Plugin: Failed to load plugin (", plugin_complex_id, "): ", e.what(), ".");
396 : : }
397 : :
398 : 2 : return handle;
399 : 2 : }
400 : 13 : }
401 : :
402 : 2 : bool Plugin::unload(const std::string& plugin_complex_id) {
403 [ - + ]: 2 : if VUNLIKELY (plugin_complex_id.empty()) {
404 : : if (impl_->log_level <= Logger::kError) { // LCOV_EXCL_LINE GCOVR_EXCL_LINE
405 : : VLOG_E("Plugin: Plugin id is empty."); // LCOV_EXCL_LINE GCOVR_EXCL_LINE
406 : : }
407 : :
408 : : return false; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
409 : : }
410 : :
411 [ + + ]: 2 : if VUNLIKELY (!has_loaded(plugin_complex_id)) {
412 [ + - ]: 1 : if (impl_->log_level <= Logger::kError) {
413 [ + - + - ]: 2 : VLOG_E("Plugin: Not loaded (", plugin_complex_id, ").");
414 : : }
415 : :
416 : 1 : return false;
417 : : }
418 : :
419 : : {
420 [ + - ]: 1 : std::lock_guard lock(impl_->mtx);
421 [ + - ]: 1 : impl_->plugin_map.erase(plugin_complex_id);
422 : 1 : }
423 : :
424 : 1 : return true;
425 : : }
426 : :
427 : 8 : bool Plugin::has_loaded(const std::string& plugin_complex_id) {
428 [ + - ]: 8 : std::shared_lock lock(impl_->mtx);
429 [ + - ]: 16 : return impl_->plugin_map.count(plugin_complex_id) != 0;
430 : 8 : }
431 : :
432 : 2 : bool Plugin::destroy(std::shared_ptr<PluginEntry> plugin_entry, Handle handle, const std::string& function_name) {
433 [ + - - + : 2 : if VUNLIKELY (!plugin_entry || !plugin_entry->loader || !handle) {
+ - - + -
+ ]
434 : : return false; // LCOV_EXCL_LINE GCOVR_EXCL_LINE
435 : : }
436 : :
437 : : try {
438 [ + - ]: 2 : auto destroy_function = plugin_entry->loader->get_function<bool(Handle)>(function_name);
439 : :
440 [ - + ]: 2 : if VUNLIKELY (!destroy_function) {
441 : : // LCOV_EXCL_START GCOVR_EXCL_START
442 : : if (plugin_entry->log_level <= Logger::kError) {
443 : : VLOG_E("Plugin: Cannot find symbol function to destroy (", plugin_entry->plugin_complex_id, ").");
444 : : }
445 : :
446 : : return false;
447 : : // LCOV_EXCL_STOP GCOVR_EXCL_STOP
448 : : }
449 : :
450 [ + - - + ]: 2 : if VUNLIKELY (!destroy_function(handle)) {
451 : : // LCOV_EXCL_START GCOVR_EXCL_START
452 : : if (plugin_entry->log_level <= Logger::kError) {
453 : : VLOG_E("Plugin: Failed to destroy handle (", plugin_entry->plugin_complex_id, ").");
454 : : }
455 : :
456 : : return false;
457 : : }
458 : : } catch (const std::exception& e) {
459 : : if (plugin_entry->log_level <= Logger::kError) {
460 : : VLOG_E("Plugin: Failed to destroy handle (", plugin_entry->plugin_complex_id, "): ", e.what(), ".");
461 : : }
462 : :
463 : : return false;
464 : : } catch (...) {
465 : : if (plugin_entry->log_level <= Logger::kError) {
466 : : VLOG_E("Plugin: Failed to destroy handle (", plugin_entry->plugin_complex_id, "): non-std exception.");
467 : : }
468 : :
469 : : return false;
470 : : }
471 : : // LCOV_EXCL_STOP GCOVR_EXCL_STOP
472 : :
473 : 2 : return true;
474 : : }
475 : :
476 : 11 : bool Plugin::process_plugin_internal(const std::string& lib_name, const std::string& local_plugin_id,
477 : : uint16_t local_version_major, uint16_t local_version_minor,
478 : : const std::string& target_plugin_id, uint16_t target_version_major,
479 : : uint16_t target_version_minor, uint8_t log_level) {
480 [ + + ]: 11 : if (log_level <= Logger::kInfo) {
481 [ + - + - ]: 6 : VLOG_I("Plugin: ", lib_name, "@", local_plugin_id, "#", local_version_major, ".", local_version_minor, ".");
482 : : }
483 : :
484 [ + + + + : 11 : if VUNLIKELY (target_plugin_id.empty() || target_plugin_id != local_plugin_id) {
+ + ]
485 [ + - ]: 3 : if (log_level <= Logger::kError) {
486 [ + - + - ]: 6 : VLOG_E("Plugin: Plugin id mismatch: expected '", local_plugin_id, "', got '", target_plugin_id, "'.");
487 : : }
488 : :
489 : 3 : return false;
490 : : }
491 : :
492 [ + + + + : 8 : if VUNLIKELY (target_version_major != local_version_major || target_version_minor > local_version_minor) {
+ + ]
493 [ + - ]: 2 : if (log_level <= Logger::kError) {
494 [ + - + - ]: 4 : VLOG_E("Plugin: Version mismatch: local ", local_version_major, ".", local_version_minor, ", required ",
495 : : target_version_major, ".", target_version_minor, ".");
496 : : }
497 : :
498 : 2 : return false;
499 : : }
500 : :
501 : 6 : return true;
502 : : }
503 : :
504 : : } // namespace vlink
|