Added ARM32 support

This commit is contained in:
alejo committed 2026-11-03 15:22:24 -03:00
1 parent df792729ef
commit 1212cb9dbe
36 files changed
+2381 -783

No files matched your search

@@ -0,0 +1,143 @@
#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;
};
}
@@ -0,0 +1,35 @@
#pragma once
#include <string>
#include <string_view>
/**
* @file Demangle.hpp
* @brief hot_reload::demangle() -- a human-readable form of a real (possibly mangled) C++ name.
*
* Every C++ symbol name this library's events and lookups ever use is always the real
* symbol-table name (whatever Context::findSymbol() needs to find it again) -- this function is
* an opt-in convenience for *displaying* that name, never applied automatically, so there is
* never a mismatch between what an event reports and what you would pass back in to look the
* symbol up.
*/
namespace hot_reload
{
/**
* @brief Demangles @p name if it looks like a mangled C++ name; returns it unchanged
* otherwise.
*
* Replaces hot_reload.h's `hot_reload_demangle()` / `hot_reload_free_string()` pair: the
* result is returned by value, so there is nothing to separately free.
*
* @param name The real (possibly mangled) symbol name -- e.g. from
* SymbolEventInfo::name.
* @return The demangled form (GCC/Clang: Itanium ABI via `abi::__cxa_demangle`; MSVC:
* `UnDecorateSymbolName`), or @p name itself, unchanged, if it does not demangle
* (not a mangled C++ name at all, or memory ran out while demangling it) or this
* build has no demangler for the compiler it was built with.
*/
[[nodiscard]] std::string demangle(std::string_view name);
}
@@ -0,0 +1,65 @@
#pragma once
#include "hot_reload/SymbolEventInfo.hpp"
#include <functional>
#include <string_view>
/**
* @file Options.hpp
* @brief hot_reload::Options -- callbacks a Context is constructed with.
*/
namespace hot_reload
{
/**
* @brief Callbacks and configuration a Context is constructed with.
*
* Replaces hot_reload.h's `HotReloadOptions`: `std::function` in place of a raw function
* pointer plus a manually-threaded `void* user_data` -- a capturing lambda plays the role
* `user_data` used to, so there is no separate field for it here at all.
*
* A pure aggregate with no behavior of its own, so -- unlike every other new public type in
* this library -- there is no matching Options.cpp; see Context.hpp for the type whose
* constructor actually reads these.
*
* @code
* hot_reload::Options options;
* options.onSymbolEvent = [](const hot_reload::SymbolEventInfo& event)
* {
* std::clog << event.summary() << '\n';
* };
* hot_reload::Context ctx(std::move(options));
* @endcode
*/
struct Options
{
/**
* @brief Called once per symbol on every Context::reload() call, synchronously, on the
* calling thread, while the reload is in progress.
*
* @warning Do not call load(), reload(), or findSymbol() on the same Context from
* inside this callback (see Context.hpp's thread-safety note: that Context's
* own lock is already held, in a non-reentrant way, by the reload() call this
* callback runs inside of -- doing so deadlocks rather than corrupts anything).
*/
std::function<void(const SymbolEventInfo& event)> onSymbolEvent;
/**
* @brief Called instead of patching, once per SymbolEvent::Removed event, so the caller
* can redirect old callers of a deleted function somewhere deliberate instead of
* leaving its previous build running untouched (the default with no callback, or
* with one that returns null).
*
* Called synchronously, on the calling thread, before the corresponding onSymbolEvent
* call for the same symbol; the same warning about not calling load()/reload()/
* findSymbol() applies.
*
* @param deletedSymbolName The removed function's real (possibly mangled) name.
* @return The address of a replacement to redirect old callers to, or null to leave the
* previous build's implementation running (the default).
*/
std::function<void*(std::string_view deletedSymbolName)> onDeletedSymbol;
};
}
@@ -0,0 +1,97 @@
#pragma once
#include "hot_reload/Status.hpp"
#include <string>
#include <utility>
/**
* @file Result.hpp
* @brief hot_reload::Result -- what Context::load() and Context::reload() return.
*/
namespace hot_reload
{
/**
* @brief The outcome of one Context::load() or Context::reload() call: a Status, and the
* platform's own message when that status is a failure worth explaining.
*
* This replaces the C API's split between a returned `HotReloadStatus` and a separate,
* stateful `hot_reload_last_error()` call: every Result is self-contained, which is what
* makes it safe to use from multiple threads without the two ever referring to different
* calls -- see Context.hpp's own thread-safety note for why that split is not just a style
* preference here.
*
* @code
* hot_reload::Result result = ctx.load("plugin", "plugin.so");
* if (!result) // or !result.ok()
* {
* std::cerr << hot_reload::toString(result.status()) << ": " << result.message() << '\n';
* }
* @endcode
*/
class Result
{
public:
/**
* @brief Constructs a successful result.
* @return A Result whose status() is Status::Ok and whose message() is empty.
*/
[[nodiscard]] static Result success() noexcept;
/**
* @brief Constructs a failed result.
* @param status The failure's status; must not be Status::Ok (use ok() for that).
* @param message Why, in the platform's own words where there is one (see each
* Status enumerator's own doc comment for what it carries); may be empty for a
* failure that is fully explained by its status alone.
* @return A Result whose status() is @p status and whose message() is @p message.
*/
[[nodiscard]] static Result failure(Status status, std::string message = {}) noexcept;
/**
* @brief Whether this result represents success.
* @return True if status() is Status::Ok.
*/
[[nodiscard]] bool ok() const noexcept
{
return m_status == Status::Ok;
}
/// Equivalent to ok() -- lets a Result be used directly as `if (result)`.
[[nodiscard]] explicit operator bool() const noexcept
{
return ok();
}
/**
* @brief This result's status.
* @return The status passed to failure(), or Status::Ok for ok().
*/
[[nodiscard]] Status status() const noexcept
{
return m_status;
}
/**
* @brief This result's message.
* @return The message passed to failure(), or empty for ok() or a status that needed no
* further explanation.
*/
[[nodiscard]] const std::string& message() const noexcept
{
return m_message;
}
private:
Result(Status status, std::string message) noexcept
: m_status(status)
, m_message(std::move(message))
{
}
Status m_status;
std::string m_message;
};
}
@@ -0,0 +1,66 @@
#pragma once
#include <string_view>
/**
* @file Status.hpp
* @brief hot_reload::Status -- the C++ API's outcome code, and toString() for logging it.
*
* Carries the same meaning as hot_reload.h's `HotReloadStatus`, which exists side by side with
* this one: the plain C API is a wrapper around the C++ API this file is part of (see
* `Context.hpp`), not the other way around, but both are supported, stable public surfaces --
* `HotReloadStatus`'s own values are not going away. A Status is never constructed on its own;
* see Result.hpp for the type that actually carries one back from a call.
*/
namespace hot_reload
{
/**
* @brief The outcome of a Context::load() or Context::reload() call.
*
* @see Result, which pairs a Status with the human-readable message a failure deserves.
*/
enum class Status
{
/** The call did exactly what it says: loaded, or diffed-and-patched. */
Ok,
/**
* A required argument was empty or otherwise invalid, or Context::load() was called for
* a module name that is already loaded -- call Context::reload() for that name instead.
*/
InvalidArgument,
/**
* The platform could not load the image at all. Result::message() carries the
* platform's own explanation -- this one status covers everything from a missing file,
* to a genuinely corrupt image, to a shared library with an unresolved symbol
* reference, and those need very different fixes. On Context::reload(), this is
* non-destructive: the previously loaded version is left running untouched, exactly as
* if reload had not been called.
*/
LoadFailed,
/** Context::reload() was called for a module name nothing has ever loaded. */
NoPreviousVersion,
/**
* This process's CPU architecture has no patch backend compiled in (see
* `Architecture.hpp` -- currently x86, x86-64, ARM64, and ARM32). Nothing was patched;
* Context::load() still succeeded, you just cannot Context::reload() on this target.
*/
UnsupportedArchitecture,
};
/**
* @brief A short, human-readable label for @p status, suitable for logging.
*
* The C API's equivalent, `hot_reload_status_string()`, calls this.
*
* @param status The status to describe.
* @return A `string_view` into a static string -- valid for the life of the program, safe to
* copy out or hold onto indefinitely, never null.
*/
[[nodiscard]] std::string_view toString(Status status) noexcept;
}
@@ -0,0 +1,72 @@
#pragma once
#include <string_view>
/**
* @file SymbolEvent.hpp
* @brief hot_reload::SymbolEvent -- what happened to one symbol during a Context::reload() call.
*
* Carries the same meaning as hot_reload.h's `HotReloadSymbolEvent`; see Status.hpp's file
* comment for how the C and C++ APIs relate.
*/
namespace hot_reload
{
/**
* @brief One symbol's outcome during a Context::reload() call, reported through
* Options::onSymbolEvent.
*
* Reload diffs the previous build's exported functions against the new build's by name;
* every name on either side gets exactly one of these.
*/
enum class SymbolEvent
{
/**
* Present, and a function, in both builds, with a different compiled body -- and
* successfully redirected: every existing caller of the old address, including one
* that cached the pointer directly, now runs the new build's code.
*/
Patched,
/** A function in the new build with no same-named symbol in the previous one at all. */
Added,
/**
* In the previous build, not in the new one. The default: the old implementation keeps
* running as-is unless Options::onDeletedSymbol supplies a stub for it.
*/
Removed,
/**
* Present in both builds, but not classified as a function (ELF `STT_FUNC` in an
* executable section, or a PE export whose address falls inside one) on at least one
* side -- the mechanical half of this library's "only executable code is ever touched"
* guarantee. Most often an exported variable; never patched, in either direction.
*/
SkippedNotCode,
/**
* Present, and a function, in both builds -- but the previous build's compiled body
* was not large enough to safely hold the jump this library writes over a function's
* entry. Skipped rather than guessed. Compile the module with
* `-fpatchable-function-entry=32,0` (GCC/Clang) or `/hotpatch` (MSVC) to stop this from
* happening; `hot_reload_enable_patchable_functions()` in `cmake/HotReload.cmake`
* applies whichever one your compiler needs.
*/
SkippedTooSmall,
/**
* Present, and a function, and large enough -- but the operating system refused to make
* that page of memory temporarily writable (an unusually locked-down environment).
*/
SkippedUnwritable,
};
/**
* @brief A short, human-readable label for @p event, suitable for logging.
* @param event The event to describe.
* @return A `string_view` into a static string -- valid for the life of the program.
*/
[[nodiscard]] std::string_view toString(SymbolEvent event) noexcept;
}
@@ -0,0 +1,53 @@
#pragma once
#include "hot_reload/SymbolEvent.hpp"
#include <string>
#include <string_view>
/**
* @file SymbolEventInfo.hpp
* @brief hot_reload::SymbolEventInfo -- the argument Options::onSymbolEvent is called with.
*/
namespace hot_reload
{
/**
* @brief One symbol's outcome during a Context::reload() call.
*
* Carries the same fields as hot_reload.h's `HotReloadSymbolEventInfo`; see Status.hpp's
* file comment for how the C and C++ APIs relate.
*
* @note Valid only for the duration of the Options::onSymbolEvent call it was passed to --
* in particular, @ref name points into storage Context owns, not this struct's own.
* Copy out whatever you need (summary() is one convenient way to) before returning.
*/
struct SymbolEventInfo
{
/// The symbol's real (possibly mangled) name. See hot_reload::demangle() for a
/// human-readable form.
std::string_view name;
/// What happened to it. SymbolEvent::Removed is not a privileged "zero value" for this
/// type the way, say, Status::Ok is for Status -- this default exists purely so a
/// default-constructed SymbolEventInfo is well-defined rather than full of
/// indeterminate values; every real one is fully populated by Context before use.
SymbolEvent kind = SymbolEvent::Removed;
/// The symbol's address in the previous build, or null if not applicable (kind is
/// SymbolEvent::Added).
void* oldAddress = nullptr;
/// The symbol's address in the new build, or null if not applicable (kind is
/// SymbolEvent::Removed).
void* newAddress = nullptr;
/**
* @brief A one-line, human-readable summary of this event -- e.g. for a log line.
* @return Something like `"patched 'tick' (0x1234 -> 0x5678)"`; addresses are included
* only where this event's kind() makes them meaningful.
*/
[[nodiscard]] std::string summary() const;
};
}
@@ -182,9 +182,11 @@ extern "C"
* @brief Opaque handle to a hot-reload context.
*
* See `cpp-style-guide.md` section 16 ("Do: use opaque pointers ... to
* hide implementation details"). The real definition is the C++ class
* `hot_reload::HotReloadContext` (`HotReloadContext.hpp`), never visible
* from this header.
* hide implementation details"). The real definition lives in
* `c-api/HotReloadApi.cpp`: a small wrapper struct that owns a
* `hot_reload::Context` (`Context.hpp`) by composition -- never a cast between unrelated
* types, and never visible from this header. New C++ code should generally prefer
* `hot_reload::Context` directly; see that header's own doc comment for why.
*/
typedef struct HotReloadContext HotReloadContext;
@@ -237,7 +239,7 @@ typedef enum HotReloadStatus
/**
* This process's CPU architecture has no patch backend compiled in
* (see `Architecture.hpp` -- currently x86, x86-64, and ARM64).
* (see `Architecture.hpp` -- currently x86, x86-64, ARM64, and ARM32).
* Nothing was patched; hot_reload_load() still succeeded, you just
* cannot hot_reload_reload() on this target.
*/