#pragma once #include "hot_reload/Options.hpp" #include "hot_reload/Result.hpp" #include #include #include /** * @file Context.hpp * @brief hot_reload::Context -- the primary, modern C++ API this library offers. * * `hot_reload.h`'s plain C API is a separate, independently stable surface built *on top of* * this one (see `c-api/HotReloadApi.cpp`): it goes through Context's public methods exactly as * any other caller would, with no special access to this class's internals, so this header and * everything it includes has no idea the C API exists at all. Prefer this header for any new * C++ code; reach for `hot_reload.h` only where C linkage itself is the requirement (a plugin * boundary, a different language's FFI). */ namespace hot_reload { /** * @brief Loads and hot-reloads shared libraries ("modules"), redirecting matching exported * functions from a module's previous build to its new one in place. * * A context owns every version of every module loaded through it. Destroying it closes all * of them, after which any function or data pointer obtained from any of those modules -- * including an old, patched entry point that now only jumps into a newer version -- is * dangling. * * @par Thread safety * Safe to call load(), reload(), and findSymbol() on the same Context from multiple threads * at once: an internal reader-writer lock serializes load()/reload() against each other and * against findSymbol(), while multiple concurrent findSymbol() calls run in parallel against * each other. This covers this *object's own bookkeeping* -- it does not and architecturally * cannot cover a thread that is, at the exact moment of a reload(), already executing inside * the function being replaced; reload() from a quiescent point for the module involved * (nothing currently running inside it) the same as a single-threaded caller would. See * `source/HotReload/README.md`'s "What this cannot detect" for the full reasoning, including * exactly which write-ordering protections the underlying patch *does* carry regardless. * Unlike the C API's `hot_reload_last_error()`, there is no shared, stateful "last error" to * race on here at all: every call's outcome comes back in its own Result, so two threads' * calls can never be confused with each other. * * @code * hot_reload::Context ctx; * if (hot_reload::Result result = ctx.load("plugin", "plugin.so"); !result) * { * std::cerr << result.message() << '\n'; * } * auto tick = reinterpret_cast(ctx.findSymbol("plugin", "tick")); * @endcode */ class Context { public: /** * @brief Creates an empty context. * @param options Callbacks to use; defaults to no callbacks at all (every event is * simply not observed, and every removed symbol keeps its previous build * running, exactly as if Options::onDeletedSymbol had returned null). */ explicit Context(Options options = {}); /// Unloads every module this context loaded (see this class's own doc comment). ~Context(); Context(const Context&) = delete; Context& operator=(const Context&) = delete; /// Leaves @p other in a valid but unspecified state, the same contract /// `std::unique_ptr` itself carries after a move: safe to destroy or to move-assign to /// (including moving something else into it), but calling load(), reload(), or /// findSymbol() on it afterwards is undefined behavior. Context(Context&& other) noexcept; /// Unloads this context's own modules first, exactly as the destructor would, then /// takes over @p other's; @p other is left in the same moved-from state the move /// constructor leaves it in (see that constructor's own doc comment). Context& operator=(Context&& other) noexcept; /** * @brief Loads a module for the first time under a logical name. * * The file is not loaded in place: it is first copied to a uniquely named sibling and * that copy is loaded, which keeps a rebuild that overwrites @p path in place from * being mistaken for the already-loaded version. The copy is deleted again right after * loading on POSIX; on Windows it stays until the module is unloaded. This means the * directory containing @p path must be writable. * * The module's static constructors run as part of loading it. * * @param moduleName An arbitrary caller-chosen key used to look the module back up in * later reload()/findSymbol() calls; it does not need to match anything on disk. * @param path Path to the shared library (`.so`/`.dll`) to load. A relative path is * always resolved against the current working directory (never a system library * search path), exactly as a path you typed yourself would be. * @return Status::Ok, or Status::InvalidArgument if @p moduleName was already loaded * (call reload() instead of load() again for the same name), or * Status::LoadFailed (see the returned Result's message() for why). */ [[nodiscard]] Result load(std::string_view moduleName, const std::filesystem::path& path); /** * @brief Loads a new build of a module, diffs its exported functions against the * previous version, and patches every one that is safe to patch in place. * * The previous version stays loaded (its entry points now jump into the new one), and * the new build's static constructors run. If the new build exports no functions at all * (including when its symbol table could not be read), every previously exported * function is reported through Options::onSymbolEvent as SymbolEvent::Removed. * * @param moduleName The same logical name passed to load(). * @param newPath Path to the new build of the shared library; follows the same rules as * @p path in load(). * @return Status::Ok, or a failing Status. On any failure, nothing was changed: the * previous version keeps running exactly as it was. */ [[nodiscard]] Result reload(std::string_view moduleName, const std::filesystem::path& newPath); /** * @brief Resolves a symbol by name, against a module's newest loaded build. * * Always resolves against the freshest build loaded for @p moduleName, patched or not * -- useful for calling a function that did not exist yet when the host first started, * such as one reported through Options::onSymbolEvent as SymbolEvent::Added. Any * exported symbol resolves, data included; only functions are ever patched. * * @param moduleName The module to resolve @p symbolName in. * @param symbolName The symbol's real (possibly mangled) name. * @return The symbol's current address, or null if @p moduleName is unknown or * @p symbolName is not exported by its newest build. */ [[nodiscard]] void* findSymbol(std::string_view moduleName, std::string_view symbolName) const noexcept; private: struct Impl; std::unique_ptr m_impl; }; }