Helios Engine
A modular ECS based data-oriented C++23 game engine framework
Loading...
Searching...
No Matches
dynamic_plugin.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/app/details/profile.hpp>
5#include <helios/assert.hpp>
8
9#include <cstdint>
10#include <expected>
11#include <filesystem>
12#include <format>
13#include <memory>
14#include <string_view>
15#include <system_error>
16
17namespace helios::app {
18
19// Forward declaration
20class App;
21
22/**
23 * @brief Function signature for plugin creation.
24 * @details Dynamic plugins must export a function with this signature.
25 * The function should create and return a new Plugin instance.
26 */
27using CreatePluginFn = Plugin* (*)();
28
29/**
30 * @brief C-compatible plugin identity exported by dynamic libraries.
31 * @details Returned from `helios_plugin_type_id`. `qualified_name` may be null;
32 * when set, storage must outlive the plugin image (typically static data).
33 */
35 uint64_t hash = 0; ///< `TypeIndex::Hash()` for the plugin type
36 const char* qualified_name = nullptr; ///< Optional qualified type name
37
38 /**
39 * @brief Builds an export descriptor from a `PluginTypeId`.
40 * @param type_id Plugin type identity
41 * @return Export descriptor suitable for `helios_plugin_type_id`
42 */
43 [[nodiscard]] static constexpr PluginTypeExport From(
44 const PluginTypeId& type_id) noexcept {
45 const std::string_view qualified = type_id.QualifiedName();
46 const uint64_t hash = type_id.Index().Hash();
47 return {.hash = hash,
48 .qualified_name = qualified.empty() ? nullptr : qualified.data()};
49 }
50
51 /**
52 * @brief Builds an export descriptor from a plugin type.
53 * @tparam T Plugin type
54 * @return Export descriptor suitable for `helios_plugin_type_id`
55 */
56 template <typename T>
57 [[nodiscard]] static constexpr PluginTypeExport From() noexcept {
58 const PluginTypeId type_id = PluginTypeId::From<T>();
59 return {.hash = type_id.Index().Hash(),
61 }
62
63 /**
64 * @brief Builds an export descriptor from a plugin type.
65 * @tparam T Plugin type
66 * @param Plugin instance
67 * @return Export descriptor suitable for `helios_plugin_type_id`
68 */
69 template <typename T>
70 [[nodiscard]] static constexpr PluginTypeExport From(
71 const T& plugin) noexcept {
73 }
74};
75
76/**
77 * @brief Function signature for the plugin type export symbol.
78 * @details Dynamic plugins must export this with C linkage. The returned
79 * pointer must remain valid for the lifetime of the library image.
80 */
82
83/// @brief Default symbol name for the plugin creation function.
84inline constexpr std::string_view kDefaultCreateSymbol = "helios_create_plugin";
85
86/// @brief Default symbol name for the plugin ID function.
87inline constexpr std::string_view kDefaultPluginIdSymbol =
88 "helios_plugin_type_id";
89
90/// @brief Error codes for dynamic plugin operations.
91enum class DynamicPluginError : uint8_t {
92 kLibraryLoadFailed, ///< Failed to load the dynamic library
93 kCreateSymbolNotFound, ///< Plugin creation function not found
94 kIdSymbolNotFound, ///< Plugin ID function not found
95 kNameSymbolNotFound, ///< Plugin name function not found
96 kCreateFailed, ///< Plugin creation function returned nullptr
97 kReloadFailed, ///< Failed to reload plugin
98};
99
100template <typename T>
101using DynamicPluginResult = std::expected<T, DynamicPluginError>;
102
103/**
104 * @brief Gets a human-readable description for a `DynamicPluginError`.
105 * @param error The error code
106 * @return String description of the error
107 */
108[[nodiscard]] constexpr std::string_view DynamicPluginErrorToString(
109 DynamicPluginError error) noexcept {
110 switch (error) {
112 return "Failed to load dynamic library";
114 return "Plugin creation function not found";
116 return "Plugin ID function not found";
118 return "Plugin name function not found";
120 return "Plugin creation function returned nullptr";
122 return "Failed to reload plugin";
123 default:
124 return "Unknown error";
125 }
126}
127
128/// @brief Configuration for dynamic plugin loading.
130 std::string_view create_symbol =
131 kDefaultCreateSymbol; ///< Name of the creation function
132 std::string_view plugin_type_id_symbol =
133 kDefaultPluginIdSymbol; ///< Name of the plugin ID function
134 bool auto_reload = false; ///< Enable automatic reload on file change
135};
136
137/**
138 * @brief Wrapper for dynamically loaded plugins.
139 * @details Loads a plugin from a shared library and manages its lifecycle.
140 * Supports hot-reloading: when the library file changes, the plugin can be
141 * unloaded and reloaded without restarting the application.
142 *
143 * The dynamic library must export:
144 * - A creation function (default: "helios_create_plugin") that returns
145 * `Plugin*`
146 * - A plugin type export function (default: "helios_plugin_type_id") that
147 * returns a `PluginTypeExport` via `PluginTypeExportFn`
148 *
149 * @note Not thread-safe.
150 *
151 * @code
152 * // In my_plugin.cpp (compiled as shared library)
153 * class MyPlugin : public helios::app::Plugin {
154 * public:
155 * static constexpr std::string_view kName = "MyPlugin";
156 *
157 * void Build(App& app) override { ... }
158 * void Destroy(App& app) override { ... }
159 * };
160 *
161 * extern "C" {
162 *
163 * HELIOS_EXPORT helios::app::Plugin* helios_create_plugin() {
164 * return new MyPlugin();
165 * }
166 *
167 * HELIOS_EXPORT auto helios_plugin_type_id()
168 * -> const helios::app::PluginTypeExport* {
169 * static const auto kExport =
170 * helios::app::PluginTypeExport::From<MyPlugin>();
171 * return &kExport;
172 * }
173 *
174 * }
175 * @endcode
176 *
177 * @code
178 * DynamicPlugin dyn_plugin;
179 * if (auto result = dyn_plugin.Load("my_plugin.so"); result) {
180 * app.AddDynamicPlugin(std::move(dyn_plugin));
181 * }
182 * @endcode
183 */
185public:
186 using FileTime = std::filesystem::file_time_type;
187
188 DynamicPlugin() = default;
189
190 /**
191 * @brief Constructs and loads a plugin from the specified path.
192 * @param path Path to the dynamic library
193 * @param config Configuration options
194 */
195 explicit DynamicPlugin(const std::filesystem::path& path,
196 DynamicPluginConfig config = {});
197 DynamicPlugin(const DynamicPlugin&) = delete;
198 DynamicPlugin(DynamicPlugin&& other) noexcept;
199 ~DynamicPlugin() noexcept = default;
200
201 DynamicPlugin& operator=(const DynamicPlugin&) = delete;
202 DynamicPlugin& operator=(DynamicPlugin&& other) noexcept;
203
204 /**
205 * @brief Loads a plugin from the specified path.
206 * @param path Path to the dynamic library
207 * @param config Configuration options
208 * @return `DynamicPluginResult` that holds void on success, or
209 * `DynamicPluginError` on failure
210 */
211 [[nodiscard]] auto Load(const std::filesystem::path& path,
212 DynamicPluginConfig config = {})
214
215 /**
216 * @brief Unloads the current plugin.
217 * @details Releases the plugin and unloads the library.
218 * @warning Triggers assertion if the plugin is not loaded.
219 * @return `DynamicPluginResult` that holds void on success, or
220 * `DynamicPluginError` on failure
221 */
222 [[nodiscard]] auto Unload() -> DynamicPluginResult<void>;
223
224 /**
225 * @brief Reloads the plugin from the same path.
226 * @details Calls Destroy on the old plugin, unloads the library,
227 * loads it again, and calls Build on the new plugin.
228 * @warning Triggers assertion if the plugin is not loaded.
229 * @param app Reference to the app for Build/Destroy calls
230 * @return `DynamicPluginResult` that holds void on success, or
231 * `DynamicPluginError` on failure
232 */
233 [[nodiscard]] auto Reload(App& app) -> DynamicPluginResult<void>;
234
235 /**
236 * @brief Reloads the plugin only if the file has changed.
237 * @warning Triggers assertion if the plugin is not loaded.
238 * @param app Reference to the app for Build/Destroy calls
239 * @return `DynamicPluginResult` that holds void on success, or
240 * `DynamicPluginError` on failure
241 */
242 [[nodiscard]] auto ReloadIfChanged(App& app) -> DynamicPluginResult<void>;
243
244 /**
245 * @brief Updates the cached file modification time.
246 * @details Call this after detecting a change to reset the tracking.
247 */
248 void UpdateFileTime() noexcept;
249
250 /**
251 * @brief Checks if the library file has been modified since last load.
252 * @return True if the file modification time has changed
253 */
254 [[nodiscard]] bool HasFileChanged() const noexcept;
255
256 /**
257 * @brief Checks if a plugin is currently loaded.
258 * @return True if a plugin is loaded
259 */
260 [[nodiscard]] bool Loaded() const noexcept {
261 return plugin_ != nullptr && library_.Loaded();
262 }
263
264 /**
265 * @brief Gets reference to the loaded plugin.
266 * @warning Triggers assertion if plugin is not loaded.
267 * @return Reference to the plugin
268 */
269 [[nodiscard]] Plugin& GetPlugin() noexcept;
270
271 /**
272 * @brief Gets const reference to the loaded plugin.
273 * @warning Triggers assertion if plugin is not loaded.
274 * @return Const reference to the plugin
275 */
276 [[nodiscard]] const Plugin& GetPlugin() const noexcept;
277
278 /**
279 * @brief Tries to get the loaded plugin.
280 * @return Pointer to plugin, or `nullptr` if not loaded
281 */
282 [[nodiscard]] Plugin* TryGetPlugin() noexcept { return plugin_.get(); }
283
284 /**
285 * @brief Tries to get the loaded plugin.
286 * @return Const pointer to plugin, or `nullptr` if not loaded
287 */
288 [[nodiscard]] const Plugin* TryGetPlugin() const noexcept {
289 return plugin_.get();
290 }
291
292 /**
293 * @brief Releases ownership of the plugin.
294 * @details After this call, the `DynamicPlugin` no longer owns the plugin.
295 * The caller is responsible for the plugin's lifetime.
296 * @return Unique pointer to the plugin
297 */
298 [[nodiscard]] auto ReleasePlugin() noexcept -> std::unique_ptr<Plugin> {
299 return std::move(plugin_);
300 }
301
302 /**
303 * @brief Gets the plugin type ID.
304 * @return Plugin type ID, or 0 if not loaded
305 */
306 [[nodiscard]] PluginTypeId GetPluginTypeId() const noexcept {
307 return plugin_type_id_;
308 }
309
310 /**
311 * @brief Gets the plugin name.
312 * @return Plugin name, or empty string if not loaded
313 */
314 [[nodiscard]] std::string_view GetPluginName() const noexcept {
315 return plugin_type_id_.Name();
316 }
317
318 /**
319 * @brief Gets the path of the loaded library.
320 * @return Path to the library, or empty if not loaded
321 */
322 [[nodiscard]] const std::filesystem::path& Path() const noexcept {
323 return library_.Path();
324 }
325
326 /**
327 * @brief Gets reference to the underlying dynamic library.
328 * @return Reference to `DynamicLibrary`
329 */
330 [[nodiscard]] helios::utils::DynamicLibrary& Library() noexcept {
331 return library_;
332 }
333
334 /**
335 * @brief Gets const reference to the underlying dynamic library.
336 * @return Const reference to `DynamicLibrary`
337 */
338 [[nodiscard]] const helios::utils::DynamicLibrary& Library() const noexcept {
339 return library_;
340 }
341
342 /**
343 * @brief Gets the configuration used for this plugin.
344 * @return Configuration struct
345 */
346 [[nodiscard]] const DynamicPluginConfig& Config() const noexcept {
347 return config_;
348 }
349
350private:
351 /**
352 * @brief Loads plugin symbols and creates the plugin instance.
353 * @return `DynamicPluginResult` that holds void on success, or
354 * `DynamicPluginError` on failure
355 */
356 [[nodiscard]] auto LoadPluginInstance() -> DynamicPluginResult<void>;
357
358 utils::DynamicLibrary library_; ///< The dynamic library
359
360 std::unique_ptr<Plugin> plugin_; ///< The created plugin instance (owned)
361 PluginTypeId plugin_type_id_; ///< Cached plugin type ID
362
363 DynamicPluginConfig config_; ///< Plugin configuration
364 FileTime last_write_time_; ///< Last known file modification time
365};
366
367inline DynamicPlugin::DynamicPlugin(const std::filesystem::path& path,
368 DynamicPluginConfig config) {
369 const auto result = Load(path, config);
370 if (!result) [[unlikely]] {
371 log::Error("Failed to load dynamic plugin '{}': {}!", path.string(),
372 DynamicPluginErrorToString(result.error()));
373 }
374}
375
377 : library_(std::move(other.library_)),
378 plugin_(std::move(other.plugin_)),
379 plugin_type_id_(other.plugin_type_id_),
380 config_(other.config_),
381 last_write_time_(other.last_write_time_) {}
382
384 if (this == &other) [[unlikely]] {
385 return *this;
386 }
387
388 plugin_.reset();
389 library_ = std::move(other.library_);
390 plugin_ = std::move(other.plugin_);
391 plugin_type_id_ = other.plugin_type_id_;
392 config_ = other.config_;
393 last_write_time_ = other.last_write_time_;
394
395 return *this;
396}
397
398inline auto DynamicPlugin::Load(const std::filesystem::path& path,
399 DynamicPluginConfig config)
401 HELIOS_APP_PROFILE_SCOPE();
402 HELIOS_APP_PROFILE_ZONE_TEXT(
403 std::format("{} ({})", plugin_type_id_.QualifiedName(), path.string()));
404
405 config_ = config;
406
407 // Load the library
408 const auto load_result = library_.Load(path);
409 if (!load_result) [[unlikely]] {
410 return std::unexpected(DynamicPluginError::kLibraryLoadFailed);
411 }
412
413 // Load plugin symbols and create instance
414 auto plugin_result = LoadPluginInstance();
415 if (!plugin_result) {
416 [[maybe_unused]] const auto result = library_.Unload();
417 return plugin_result;
418 }
419
420 // Cache the file modification time
422
423 log::Info("Loaded dynamic plugin '{}' from: {}", GetPluginName(),
424 path.string());
425 return {};
426}
427
429 HELIOS_APP_PROFILE_SCOPE();
430 HELIOS_APP_PROFILE_ZONE_TEXT(std::format(
431 "{} ({})", plugin_type_id_.QualifiedName(), library_.Path().string()));
432
433 HELIOS_ASSERT(Loaded(), "Plugin is not loaded!");
434
435 // Release plugin before unloading library
436 plugin_.reset();
437
438 const auto unload_result = library_.Unload();
439 if (!unload_result) [[unlikely]] {
440 return std::unexpected(DynamicPluginError::kLibraryLoadFailed);
441 }
442
443 return {};
444}
445
447 HELIOS_APP_PROFILE_SCOPE();
448 HELIOS_APP_PROFILE_ZONE_TEXT(std::format(
449 "{} ({})", plugin_type_id_.QualifiedName(), library_.Path().string()));
450
451 HELIOS_ASSERT(Loaded(), "Plugin is not loaded!");
452
453 // Call Destroy on the old plugin
454 log::Info("Reloading dynamic plugin '{}' from '{}'", GetPluginName(),
455 library_.Path().string());
456 plugin_->Destroy(app);
457
458 // Save the path and config before unloading
459 const auto saved_path = library_.Path();
460 auto saved_config = config_;
461
462 // Release plugin and unload library
463 plugin_.reset();
464
465 const auto unload_result = library_.Unload();
466 if (!unload_result) [[unlikely]] {
467 return std::unexpected(DynamicPluginError::kReloadFailed);
468 }
469
470 // Reload the library
471 const auto load_result = library_.Load(saved_path);
472 if (!load_result) [[unlikely]] {
473 return std::unexpected(DynamicPluginError::kReloadFailed);
474 }
475
476 // Load plugin symbols and create new instance
477 config_ = saved_config;
478 const auto plugin_result = LoadPluginInstance();
479 if (!plugin_result) [[unlikely]] {
480 [[maybe_unused]] const auto result = library_.Unload();
481 return std::unexpected(DynamicPluginError::kReloadFailed);
482 }
483
484 // Call Build on the new plugin
485 plugin_->Build(app);
486
487 // Update file time
489
490 log::Info("Successfully reloaded dynamic plugin '{}' from '{}'",
491 GetPluginName(), saved_path.string());
492 return {};
493}
494
497 HELIOS_ASSERT(Loaded(), "Plugin is not loaded!");
498
499 if (!HasFileChanged()) {
500 return {};
501 }
502
503 return Reload(app);
504}
505
506inline void DynamicPlugin::UpdateFileTime() noexcept {
507 if (!library_.Loaded()) [[unlikely]] {
508 return;
509 }
510
511 std::error_code ec;
512 last_write_time_ = std::filesystem::last_write_time(library_.Path(), ec);
513}
514
515inline bool DynamicPlugin::HasFileChanged() const noexcept {
516 if (!library_.Loaded()) {
517 return false;
518 }
519
520 std::error_code ec;
521 const auto current_time =
522 std::filesystem::last_write_time(library_.Path(), ec);
523 if (ec) [[unlikely]] {
524 return false;
525 }
526
527 return current_time != last_write_time_;
528}
529
531 HELIOS_ASSERT(Loaded(), "Plugin is not loaded!");
532 return *plugin_;
533}
534
535inline const Plugin& DynamicPlugin::GetPlugin() const noexcept {
536 HELIOS_ASSERT(Loaded(), "Plugin is not loaded!");
537 return *plugin_;
538}
539
540inline auto DynamicPlugin::LoadPluginInstance() -> DynamicPluginResult<void> {
541 // Get create function
542 const auto create_result =
543 library_.GetSymbol<CreatePluginFn>(config_.create_symbol);
544 if (!create_result) [[unlikely]] {
545 log::Error("Create function '{}' not found in library!",
546 config_.create_symbol);
547 return std::unexpected(DynamicPluginError::kCreateSymbolNotFound);
548 }
549
550 // Get plugin type export function
551 const auto id_result =
552 library_.GetSymbol<PluginTypeExportFn>(config_.plugin_type_id_symbol);
553 if (!id_result) [[unlikely]] {
554 log::Error("Plugin ID function '{}' not found in library!",
555 config_.plugin_type_id_symbol);
556 return std::unexpected(DynamicPluginError::kIdSymbolNotFound);
557 }
558
559 const PluginTypeExportFn export_fn = *id_result;
560 const PluginTypeExport* export_info = export_fn();
561 if (export_info == nullptr) [[unlikely]] {
562 log::Error("Plugin type export function returned nullptr!");
563 return std::unexpected(DynamicPluginError::kCreateFailed);
564 }
565
566 const std::string_view qualified_name =
567 export_info->qualified_name != nullptr
568 ? std::string_view{export_info->qualified_name}
569 : std::string_view{};
570 plugin_type_id_ =
571 PluginTypeId::FromExported(export_info->hash, qualified_name);
572
573 // Create the plugin
574 CreatePluginFn create_fn = *create_result;
575 Plugin* raw_plugin = create_fn();
576
577 if (raw_plugin == nullptr) [[unlikely]] {
578 log::Error("Plugin creation function returned nullptr!");
579 return std::unexpected(DynamicPluginError::kCreateFailed);
580 }
581
582 plugin_.reset(raw_plugin);
583 return {};
584}
585
586} // namespace helios::app
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:259
Application class.
auto Reload(App &app) -> DynamicPluginResult< void >
Reloads the plugin from the same path.
std::filesystem::file_time_type FileTime
const std::filesystem::path & Path() const noexcept
Gets the path of the loaded library.
Plugin & GetPlugin() noexcept
Gets reference to the loaded plugin.
const DynamicPluginConfig & Config() const noexcept
Gets the configuration used for this plugin.
const Plugin * TryGetPlugin() const noexcept
Tries to get the loaded plugin.
PluginTypeId GetPluginTypeId() const noexcept
Gets the plugin type ID.
DynamicPlugin & operator=(const DynamicPlugin &)=delete
auto Load(const std::filesystem::path &path, DynamicPluginConfig config={}) -> DynamicPluginResult< void >
Loads a plugin from the specified path.
DynamicPlugin(const DynamicPlugin &)=delete
~DynamicPlugin() noexcept=default
Plugin * TryGetPlugin() noexcept
Tries to get the loaded plugin.
bool HasFileChanged() const noexcept
Checks if the library file has been modified since last load.
std::string_view GetPluginName() const noexcept
Gets the plugin name.
const helios::utils::DynamicLibrary & Library() const noexcept
Gets const reference to the underlying dynamic library.
bool Loaded() const noexcept
Checks if a plugin is currently loaded.
auto ReleasePlugin() noexcept -> std::unique_ptr< Plugin >
Releases ownership of the plugin.
helios::utils::DynamicLibrary & Library() noexcept
Gets reference to the underlying dynamic library.
auto Unload() -> DynamicPluginResult< void >
Unloads the current plugin.
void UpdateFileTime() noexcept
Updates the cached file modification time.
auto ReloadIfChanged(App &app) -> DynamicPluginResult< void >
Reloads the plugin only if the file has changed.
Base class for all plugins.
Definition plugin.hpp:14
Cross-platform dynamic library loader.
static constexpr TypeId From() noexcept
Constructs a TypeId from a type T.
static constexpr TypeId FromExported(uint64_t hash, std::string_view qualified_name={}) noexcept
Constructs a TypeId from a precomputed hash and optional name.
constexpr TypeIndex Index() const noexcept
Retrieves the type index associated with this TypeId.
constexpr size_t Hash() const noexcept
Retrieves the hash value associated with this TypeIndex.
constexpr std::string_view kDefaultPluginIdSymbol
Default symbol name for the plugin ID function.
std::expected< T, DynamicPluginError > DynamicPluginResult
constexpr std::string_view DynamicPluginErrorToString(DynamicPluginError error) noexcept
Gets a human-readable description for a DynamicPluginError.
Plugin *(*)() CreatePluginFn
Function signature for plugin creation.
constexpr std::string_view kDefaultCreateSymbol
Default symbol name for the plugin creation function.
utils::TypeId PluginTypeId
Type id for plugins.
Definition plugin.hpp:68
const PluginTypeExport *(*)() PluginTypeExportFn
Function signature for the plugin type export symbol.
DynamicPluginError
Error codes for dynamic plugin operations.
@ kCreateFailed
Plugin creation function returned nullptr.
@ kCreateSymbolNotFound
Plugin creation function not found.
@ kReloadFailed
Failed to reload plugin.
@ kLibraryLoadFailed
Failed to load the dynamic library.
@ kNameSymbolNotFound
Plugin name function not found.
@ kIdSymbolNotFound
Plugin ID function not found.
void Error(std::string_view message) noexcept
Logs an error message with the default logger.
Definition logger.hpp:577
void Info(std::string_view message) noexcept
Logs an info message with the default logger.
Definition logger.hpp:483
consteval const char * GetFullTypeNameCString() noexcept
Retrieves a null-terminated fully qualified type name for T.
STL namespace.
Configuration for dynamic plugin loading.
bool auto_reload
Enable automatic reload on file change.
std::string_view create_symbol
Name of the creation function.
std::string_view plugin_type_id_symbol
Name of the plugin ID function.
C-compatible plugin identity exported by dynamic libraries.
static constexpr PluginTypeExport From() noexcept
Builds an export descriptor from a plugin type.
const char * qualified_name
Optional qualified type name.
static constexpr PluginTypeExport From(const T &plugin) noexcept
Builds an export descriptor from a plugin type.
static constexpr PluginTypeExport From(const PluginTypeId &type_id) noexcept
Builds an export descriptor from a PluginTypeId.
uint64_t hash
TypeIndex::Hash() for the plugin type