Files
Volkan/source/HotReload/include/hot_reload/Context.hpp
T
2026-11-03 15:22:24 -03:00

144 lines
6.9 KiB
C++

#pragma once
#include "hot_reload/Options.hpp"
#include "hot_reload/Result.hpp"
#include <filesystem>
#include <memory>
#include <string_view>
/**
* @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<int (*)(int)>(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<Impl> m_impl;
};
}