Helios Engine 0.1.0
A modular ECS based data-oriented C++23 game engine
 
Loading...
Searching...
No Matches
dynamic_module.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <helios/core_pch.hpp>
4
9
10#include <cstdint>
11#include <expected>
12#include <filesystem>
13#include <memory>
14#include <string>
15#include <string_view>
16
17namespace helios::app {
18
19// Forward declaration
20class App;
21
22/**
23 * @brief Function signature for module creation.
24 * @details Dynamic modules must export a function with this signature.
25 * The function should create and return a new Module instance.
26 */
27using CreateModuleFn = Module* (*)();
28
29/**
30 * @brief Function signature for getting module type ID.
31 * @details Dynamic modules must export a function with this signature.
32 * The function should return the unique type ID of the module.
33 */
35
36/**
37 * @brief Function signature for getting module name.
38 * @details Dynamic modules must export a function with this signature.
39 * The function should return the name of the module as a null-terminated string.
40 */
41using ModuleNameFn = const char* (*)();
42
43/**
44 * @brief Default symbol name for the module creation function.
45 */
46inline constexpr std::string_view kDefaultCreateSymbol = "helios_create_module";
47
48/**
49 * @brief Default symbol name for the module ID function.
50 */
51inline constexpr std::string_view kDefaultModuleIdSymbol = "helios_module_id";
52
53/**
54 * @brief Default symbol name for the module name function.
55 */
56inline constexpr std::string_view kDefaultModuleNameSymbol = "helios_module_name";
57
58/**
59 * @brief Error codes for dynamic module operations.
60 */
61enum class DynamicModuleError : uint8_t {
62 LibraryLoadFailed, ///< Failed to load the dynamic library
63 CreateSymbolNotFound, ///< Module creation function not found
64 IdSymbolNotFound, ///< Module ID function not found
65 NameSymbolNotFound, ///< Module name function not found
66 CreateFailed, ///< Module creation function returned nullptr
67 NotLoaded, ///< Module is not loaded
68 ReloadFailed, ///< Failed to reload module
69 FileNotChanged, ///< File has not been modified
70};
71
72/**
73 * @brief Gets a human-readable description for a DynamicModuleError.
74 * @param error The error code
75 * @return String description of the error
76 */
77[[nodiscard]] constexpr std::string_view DynamicModuleErrorToString(DynamicModuleError error) noexcept {
78 switch (error) {
80 return "Failed to load dynamic library";
82 return "Module creation function not found";
84 return "Module ID function not found";
86 return "Module name function not found";
88 return "Module creation function returned nullptr";
90 return "Module is not loaded";
92 return "Failed to reload module";
94 return "File has not been modified";
95 default:
96 return "Unknown error";
97 }
98}
99
100/**
101 * @brief Configuration for dynamic module loading.
102 */
104 std::string_view create_symbol = kDefaultCreateSymbol; ///< Name of the creation function
105 std::string_view module_id_symbol = kDefaultModuleIdSymbol; ///< Name of the module ID function
106 std::string_view module_name_symbol = kDefaultModuleNameSymbol; ///< Name of the module name function
107 bool auto_reload = false; ///< Enable automatic reload on file change
108};
109
110/**
111 * @brief Wrapper for dynamically loaded modules.
112 * @details Loads a module from a shared library and manages its lifecycle.
113 * Supports hot-reloading: when the library file changes, the module can be
114 * unloaded and reloaded without restarting the application.
115 *
116 * The dynamic library must export:
117 * - A creation function (default: "helios_create_module") that returns Module*
118 * - A module ID function (default: "helios_module_id") that returns ModuleTypeId
119 * - A module name function (default: "helios_module_name") that returns const char*
120 *
121 * @note Not thread-safe. External synchronization required for concurrent access.
122 *
123 * @example Library implementation:
124 * @code
125 * // In my_module.cpp (compiled as shared library)
126 * class MyModule : public helios::app::Module {
127 * public:
128 * static constexpr std::string_view GetName() noexcept { return "MyModule"; }
129 * void Build(App& app) override { ... }
130 * void Destroy(App& app) override { ... }
131 * };
132 *
133 * extern "C" {
134 * HELIOS_EXPORT helios::app::Module* helios_create_module() {
135 * return new MyModule();
136 * }
137 * HELIOS_EXPORT helios::app::ModuleTypeId helios_module_id() {
138 * return helios::app::ModuleTypeIdOf<MyModule>();
139 * }
140 * HELIOS_EXPORT const char* helios_module_name() {
141 * return "MyModule";
142 * }
143 * }
144 * @endcode
145 *
146 * @example Usage:
147 * @code
148 * DynamicModule dyn_module;
149 * if (auto result = dyn_module.Load("my_module.so"); result) {
150 * app.AddDynamicModule(std::move(dyn_module));
151 * }
152 * @endcode
153 */
155public:
156 using FileTime = std::filesystem::file_time_type;
157
158 DynamicModule() = default;
159
160 /**
161 * @brief Constructs and loads a module from the specified path.
162 * @param path Path to the dynamic library
163 * @param config Configuration options
164 */
165 explicit DynamicModule(const std::filesystem::path& path, DynamicModuleConfig config = {});
166 DynamicModule(const DynamicModule&) = delete;
167 DynamicModule(DynamicModule&& other) noexcept;
168
169 /**
170 * @brief Destructor that destroys the module if loaded.
171 */
172 ~DynamicModule() noexcept = default;
173
174 DynamicModule& operator=(const DynamicModule&) = delete;
175 DynamicModule& operator=(DynamicModule&& other) noexcept;
176
177 /**
178 * @brief Loads a module from the specified path.
179 * @param path Path to the dynamic library
180 * @param config Configuration options
181 * @return Expected with void on success, or error on failure
182 */
183 [[nodiscard]] auto Load(const std::filesystem::path& path, DynamicModuleConfig config = {})
184 -> std::expected<void, DynamicModuleError>;
185
186 /**
187 * @brief Unloads the current module.
188 * @details Releases the module and unloads the library.
189 * @return Expected with void on success, or error on failure
190 */
191 [[nodiscard]] auto Unload() -> std::expected<void, DynamicModuleError>;
192
193 /**
194 * @brief Reloads the module from the same path.
195 * @details Calls Destroy on the old module, unloads the library,
196 * loads it again, and calls Build on the new module.
197 * @param app Reference to the app for Build/Destroy calls
198 * @return Expected with void on success, or error on failure
199 */
200 [[nodiscard]] auto Reload(App& app) -> std::expected<void, DynamicModuleError>;
201
202 /**
203 * @brief Reloads the module only if the file has changed.
204 * @param app Reference to the app for Build/Destroy calls
205 * @return Expected with void on success, FileNotChanged if not modified, or error on failure
206 */
207 [[nodiscard]] auto ReloadIfChanged(App& app) -> std::expected<void, DynamicModuleError>;
208
209 /**
210 * @brief Updates the cached file modification time.
211 * @details Call this after detecting a change to reset the tracking.
212 */
213 void UpdateFileTime() noexcept;
214
215 /**
216 * @brief Checks if the library file has been modified since last load.
217 * @return True if the file modification time has changed
218 */
219 [[nodiscard]] bool HasFileChanged() const noexcept;
220
221 /**
222 * @brief Checks if a module is currently loaded.
223 * @return True if a module is loaded
224 */
225 [[nodiscard]] bool Loaded() const noexcept { return module_ != nullptr && library_.Loaded(); }
226
227 /**
228 * @brief Gets reference to the loaded module.
229 * @warning Triggers assertion if module is not loaded.
230 * @return Reference to the module
231 */
232 [[nodiscard]] Module& GetModule() noexcept;
233
234 /**
235 * @brief Gets const reference to the loaded module.
236 * @warning Triggers assertion if module is not loaded.
237 * @return Const reference to the module
238 */
239 [[nodiscard]] const Module& GetModule() const noexcept;
240
241 /**
242 * @brief Gets pointer to the loaded module.
243 * @return Pointer to module, or nullptr if not loaded
244 */
245 [[nodiscard]] Module* GetModulePtr() noexcept { return module_.get(); }
246
247 /**
248 * @brief Gets const pointer to the loaded module.
249 * @return Const pointer to module, or nullptr if not loaded
250 */
251 [[nodiscard]] const Module* GetModulePtr() const noexcept { return module_.get(); }
252
253 /**
254 * @brief Releases ownership of the module.
255 * @details After this call, the DynamicModule no longer owns the module.
256 * The caller is responsible for the module's lifetime.
257 * @return Unique pointer to the module
258 */
259 [[nodiscard]] std::unique_ptr<Module> ReleaseModule() noexcept { return std::move(module_); }
260
261 /**
262 * @brief Gets the module type ID.
263 * @return Module type ID, or 0 if not loaded
264 */
265 [[nodiscard]] ModuleTypeId GetModuleId() const noexcept { return module_id_; }
266
267 /**
268 * @brief Gets the module name.
269 * @return Module name, or empty string if not loaded
270 */
271 [[nodiscard]] std::string_view GetModuleName() const noexcept { return module_name_; }
272
273 /**
274 * @brief Gets the path of the loaded library.
275 * @return Path to the library, or empty if not loaded
276 */
277 [[nodiscard]] const std::filesystem::path& Path() const noexcept { return library_.Path(); }
278
279 /**
280 * @brief Gets reference to the underlying dynamic library.
281 * @return Reference to helios::utils::DynamicLibrary
282 */
283 [[nodiscard]] helios::utils::DynamicLibrary& Library() noexcept { return library_; }
284
285 /**
286 * @brief Gets const reference to the underlying dynamic library.
287 * @return Const reference to helios::utils::DynamicLibrary
288 */
289 [[nodiscard]] const helios::utils::DynamicLibrary& Library() const noexcept { return library_; }
290
291 /**
292 * @brief Gets the configuration used for this module.
293 * @return Configuration struct
294 */
295 [[nodiscard]] const DynamicModuleConfig& Config() const noexcept { return config_; }
296
297private:
298 /**
299 * @brief Loads module symbols and creates the module instance.
300 * @return Expected with void on success, or error on failure
301 */
302 [[nodiscard]] auto LoadModuleInstance() -> std::expected<void, DynamicModuleError>;
303
304 helios::utils::DynamicLibrary library_; ///< The dynamic library
305
306 std::unique_ptr<Module> module_; ///< The created module instance (owned)
307 ModuleTypeId module_id_ = 0; ///< Cached module type ID
308 std::string module_name_; ///< Cached module name
309
310 DynamicModuleConfig config_; ///< Module configuration
311 FileTime last_write_time_{}; ///< Last known file modification time
312};
313
314inline DynamicModule::DynamicModule(const std::filesystem::path& path, DynamicModuleConfig config) {
315 auto result = Load(path, config);
316 if (!result) {
317 HELIOS_ERROR("Failed to load dynamic module '{}': {}", path.string(), DynamicModuleErrorToString(result.error()));
318 }
319}
320
322 : library_(std::move(other.library_)),
323 module_(std::move(other.module_)),
324 module_id_(other.module_id_),
325 module_name_(std::move(other.module_name_)),
326 config_(other.config_),
327 last_write_time_(other.last_write_time_) {
328 other.module_id_ = 0;
329}
330
332 if (this != &other) {
333 module_.reset();
334 library_ = std::move(other.library_);
335 module_ = std::move(other.module_);
336 module_id_ = other.module_id_;
337 module_name_ = std::move(other.module_name_);
338 config_ = other.config_;
339 last_write_time_ = other.last_write_time_;
340
341 other.module_id_ = 0;
342 }
343 return *this;
344}
345
346inline auto DynamicModule::Load(const std::filesystem::path& path, DynamicModuleConfig config)
347 -> std::expected<void, DynamicModuleError> {
348 config_ = config;
349
350 // Load the library
351 const auto load_result = library_.Load(path);
352 if (!load_result) {
353 return std::unexpected(DynamicModuleError::LibraryLoadFailed);
354 }
355
356 // Load module symbols and create instance
357 auto module_result = LoadModuleInstance();
358 if (!module_result) {
359 [[maybe_unused]] auto _ = library_.Unload();
360 return module_result;
361 }
362
363 // Cache the file modification time
364 UpdateFileTime();
365
366 HELIOS_INFO("Loaded dynamic module '{}' from: {}", module_name_, path.string());
367 return {};
368}
369
370inline auto DynamicModule::Unload() -> std::expected<void, DynamicModuleError> {
371 if (!Loaded()) {
372 return std::unexpected(DynamicModuleError::NotLoaded);
373 }
374
375 // Release module before unloading library
376 module_.reset();
377 module_id_ = 0;
378 module_name_.clear();
379
380 auto unload_result = library_.Unload();
381 if (!unload_result) {
382 return std::unexpected(DynamicModuleError::LibraryLoadFailed);
383 }
384
385 return {};
386}
387
388inline auto DynamicModule::Reload(App& app) -> std::expected<void, DynamicModuleError> {
389 if (!Loaded()) {
390 return std::unexpected(DynamicModuleError::NotLoaded);
391 }
392
393 // Call Destroy on the old module
394 HELIOS_INFO("Reloading dynamic module '{}': {}", module_name_, library_.Path().string());
395 module_->Destroy(app);
396
397 // Save the path and config before unloading
398 auto saved_path = library_.Path();
399 auto saved_config = config_;
400
401 // Release module and unload library
402 module_.reset();
403 module_id_ = 0;
404 module_name_.clear();
405
406 auto unload_result = library_.Unload();
407 if (!unload_result) {
408 return std::unexpected(DynamicModuleError::ReloadFailed);
409 }
410
411 // Reload the library
412 auto load_result = library_.Load(saved_path);
413 if (!load_result) {
414 return std::unexpected(DynamicModuleError::ReloadFailed);
415 }
416
417 // Load module symbols and create new instance
418 config_ = saved_config;
419 auto module_result = LoadModuleInstance();
420 if (!module_result) {
421 [[maybe_unused]] auto _ = library_.Unload();
422 return std::unexpected(DynamicModuleError::ReloadFailed);
423 }
424
425 // Call Build on the new module
426 module_->Build(app);
427
428 // Update file time
429 UpdateFileTime();
430
431 HELIOS_INFO("Successfully reloaded dynamic module '{}': {}", module_name_, saved_path.string());
432 return {};
433}
434
435inline auto DynamicModule::ReloadIfChanged(App& app) -> std::expected<void, DynamicModuleError> {
436 if (!HasFileChanged()) {
437 return std::unexpected(DynamicModuleError::FileNotChanged);
438 }
439
440 return Reload(app);
441}
442
443inline void DynamicModule::UpdateFileTime() noexcept {
444 if (!library_.Loaded()) {
445 return;
446 }
447
448 std::error_code ec;
449 last_write_time_ = std::filesystem::last_write_time(library_.Path(), ec);
450}
451
452inline bool DynamicModule::HasFileChanged() const noexcept {
453 if (!library_.Loaded()) {
454 return false;
455 }
456
457 std::error_code ec;
458 auto current_time = std::filesystem::last_write_time(library_.Path(), ec);
459 if (ec) {
460 return false;
461 }
462
463 return current_time != last_write_time_;
464}
465
467 HELIOS_ASSERT(Loaded(), "Failed to get module: Module is not loaded!");
468 return *module_;
469}
470
471inline const Module& DynamicModule::GetModule() const noexcept {
472 HELIOS_ASSERT(Loaded(), "Failed to get module: Module is not loaded!");
473 return *module_;
474}
475
476inline auto DynamicModule::LoadModuleInstance() -> std::expected<void, DynamicModuleError> {
477 // Get create function
478 auto create_result = library_.GetSymbol<CreateModuleFn>(config_.create_symbol);
479 if (!create_result) {
480 HELIOS_ERROR("Create function '{}' not found in library", config_.create_symbol);
481 return std::unexpected(DynamicModuleError::CreateSymbolNotFound);
482 }
483
484 // Get module ID function
485 auto id_result = library_.GetSymbol<ModuleIdFn>(config_.module_id_symbol);
486 if (!id_result) {
487 HELIOS_ERROR("Module ID function '{}' not found in library", config_.module_id_symbol);
488 return std::unexpected(DynamicModuleError::IdSymbolNotFound);
489 }
490
491 // Get module name function
492 auto name_result = library_.GetSymbol<ModuleNameFn>(config_.module_name_symbol);
493 if (!name_result) {
494 HELIOS_ERROR("Module name function '{}' not found in library", config_.module_name_symbol);
495 return std::unexpected(DynamicModuleError::NameSymbolNotFound);
496 }
497
498 // Get module ID and name
499 ModuleIdFn id_fn = *id_result;
500 ModuleNameFn name_fn = *name_result;
501 module_id_ = id_fn();
502 module_name_ = name_fn();
503
504 // Create the module
505 CreateModuleFn create_fn = *create_result;
506 Module* raw_module = create_fn();
507
508 if (raw_module == nullptr) {
509 HELIOS_ERROR("Module creation function returned nullptr");
510 module_id_ = 0;
511 module_name_.clear();
512 return std::unexpected(DynamicModuleError::CreateFailed);
513 }
514
515 module_.reset(raw_module);
516 return {};
517}
518
519} // namespace helios::app
#define HELIOS_ASSERT(condition,...)
Assertion macro that aborts execution in debug builds.
Definition assert.hpp:140
Application class.
Definition app.hpp:97
const std::filesystem::path & Path() const noexcept
Gets the path of the loaded library.
std::string_view GetModuleName() const noexcept
Gets the module name.
auto Reload(App &app) -> std::expected< void, DynamicModuleError >
Reloads the module from the same path.
DynamicModule & operator=(const DynamicModule &)=delete
auto Unload() -> std::expected< void, DynamicModuleError >
Unloads the current module.
const helios::utils::DynamicLibrary & Library() const noexcept
Gets const reference to the underlying dynamic library.
auto Load(const std::filesystem::path &path, DynamicModuleConfig config={}) -> std::expected< void, DynamicModuleError >
Loads a module from the specified path.
helios::utils::DynamicLibrary & Library() noexcept
Gets reference to the underlying dynamic library.
~DynamicModule() noexcept=default
Destructor that destroys the module if loaded.
bool Loaded() const noexcept
Checks if a module is currently loaded.
void UpdateFileTime() noexcept
Updates the cached file modification time.
std::unique_ptr< Module > ReleaseModule() noexcept
Releases ownership of the module.
Module * GetModulePtr() noexcept
Gets pointer to the loaded module.
bool HasFileChanged() const noexcept
Checks if the library file has been modified since last load.
const DynamicModuleConfig & Config() const noexcept
Gets the configuration used for this module.
ModuleTypeId GetModuleId() const noexcept
Gets the module type ID.
auto ReloadIfChanged(App &app) -> std::expected< void, DynamicModuleError >
Reloads the module only if the file has changed.
const Module * GetModulePtr() const noexcept
Gets const pointer to the loaded module.
DynamicModule(const DynamicModule &)=delete
std::filesystem::file_time_type FileTime
Module & GetModule() noexcept
Gets reference to the loaded module.
Base class for all modules.
Definition module.hpp:25
bool Loaded() const noexcept
Checks if a library is currently loaded.
const std::filesystem::path & Path() const noexcept
Gets the path of the loaded library.
#define HELIOS_ERROR(...)
Definition logger.hpp:689
#define HELIOS_INFO(...)
Definition logger.hpp:685
DynamicModuleError
Error codes for dynamic module operations.
@ CreateFailed
Module creation function returned nullptr.
@ FileNotChanged
File has not been modified.
@ IdSymbolNotFound
Module ID function not found.
@ ReloadFailed
Failed to reload module.
@ CreateSymbolNotFound
Module creation function not found.
@ NotLoaded
Module is not loaded.
@ LibraryLoadFailed
Failed to load the dynamic library.
@ NameSymbolNotFound
Module name function not found.
Module *(*)() CreateModuleFn
Function signature for module creation.
constexpr std::string_view DynamicModuleErrorToString(DynamicModuleError error) noexcept
Gets a human-readable description for a DynamicModuleError.
ModuleTypeId(*)() ModuleIdFn
Function signature for getting module type ID.
constexpr std::string_view kDefaultModuleIdSymbol
Default symbol name for the module ID function.
size_t ModuleTypeId
Definition module.hpp:84
const char *(*)() ModuleNameFn
Function signature for getting module name.
constexpr std::string_view kDefaultModuleNameSymbol
Default symbol name for the module name function.
constexpr std::string_view kDefaultCreateSymbol
Default symbol name for the module creation function.
STL namespace.
Configuration for dynamic module loading.
std::string_view module_id_symbol
Name of the module ID function.
std::string_view module_name_symbol
Name of the module name function.
std::string_view create_symbol
Name of the creation function.
bool auto_reload
Enable automatic reload on file change.