144 lines
6.9 KiB
C++
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;
|
|
};
|
|
|
|
}
|