316 lines
15 KiB
C++
316 lines
15 KiB
C++
/* hot_reload.h
|
|
*
|
|
* Public C API for the hot-reload library -- see cpp-style-guide.md
|
|
* Part II (C API Style) for the naming/formatting rules this header
|
|
* follows, and section 11 ("Runtime Loading") for why this boundary is
|
|
* plain C in the first place: anything crossing a dlopen()/LoadLibrary()
|
|
* boundary must have C linkage, so this is exactly the kind of surface
|
|
* that section calls out.
|
|
*
|
|
* WHAT THIS LIBRARY DOES
|
|
* -----------------------
|
|
* hot_reload_load() loads a shared library (.so/.dll) under a logical
|
|
* name. hot_reload_reload() loads a NEW build of that same shared
|
|
* library and, for every function the two builds have in common,
|
|
* rewrites the FIRST FEW BYTES of the old function in memory into a
|
|
* jump to the new function -- so every existing call site (anyone who
|
|
* already resolved the old function's address, including via a plain
|
|
* C function pointer) transparently starts running the new code,
|
|
* without restarting the process.
|
|
*
|
|
* WHAT THIS LIBRARY DELIBERATELY DOES NOT DO
|
|
* --------------------------------------------
|
|
* It never patches anything that isn't executable code. A symbol is
|
|
* only ever a patch candidate if the loader's own symbol table marks
|
|
* it as a function (ELF STT_FUNC / a PE export whose address falls
|
|
* inside an executable section) -- see the design note in
|
|
* SymbolTable.hpp for exactly how that's determined. Global variables,
|
|
* class/struct instances, vtables, and anything else classified as
|
|
* data are always left alone, on both the old and the new module --
|
|
* there is no code path in this library that ever writes to a data
|
|
* symbol. This is a mechanical guarantee from how symbols are
|
|
* classified, not a naming convention you opt into.
|
|
*
|
|
* It also cannot verify that a hot-reloadable function's SIGNATURE
|
|
* hasn't changed. Patching only ever redirects control flow at a fixed
|
|
* entry point; it has no way to know, from a symbol table alone, that
|
|
* `void tick(int)` became `void tick(int, int)` between builds. If a
|
|
* function's signature changes, give it a new exported name (so the
|
|
* old symbol is treated as removed and the new one as added -- see
|
|
* HOT_RELOAD_SYMBOL_REMOVED below) rather than reusing the old name.
|
|
* See the top-level README for the full list of hazards this library
|
|
* cannot detect for you.
|
|
*
|
|
* REQUIREMENTS ON A HOT-RELOADABLE MODULE
|
|
* ------------------------------------------
|
|
* Matching an old build's function up with the new build's requires a
|
|
* name that is stable across recompiles. extern "C" linkage -- see
|
|
* cpp-style-guide.md section 11 -- is the simplest way to get one: no
|
|
* mangling at all, so the name never depends on the compiler, its
|
|
* version, or the function's signature.
|
|
*
|
|
* A real, unmodified C++ mangled name works too, and needs no special
|
|
* handling by this library -- classification only ever looks at a
|
|
* symbol's type and section, never at what its name looks like, so an
|
|
* ordinary out-of-line method, a virtual method (redirected wherever
|
|
* it's reached from -- directly or through a vtable slot, since both
|
|
* hold the same address), and an operator overload all patch exactly
|
|
* like any extern "C" function. What extern "C" actually buys you is
|
|
* a name you can type by hand into hot_reload_get_symbol() and a
|
|
* guarantee that it won't change if you rebuild with a different
|
|
* compiler; a mangled name gives up both of those, only stays stable
|
|
* within one ABI family (GCC and Clang share the Itanium C++ ABI;
|
|
* MSVC does not), and -- the one hazard that has nothing to do with
|
|
* this library and everything to do with what "hot-reloadable" can
|
|
* even mean -- only exists to patch at all if the compiler actually
|
|
* emitted an out-of-line body for it, which an implicitly-inline
|
|
* method defined in a class body, or a template instantiated at the
|
|
* call site, generally will not. See the top-level README's "Hot
|
|
* reloading C++ symbols" section for the full picture, including how
|
|
* to get a readable name for logging with hot_reload_demangle().
|
|
*/
|
|
#ifndef HOT_RELOAD_H
|
|
#define HOT_RELOAD_H
|
|
|
|
#include <stddef.h>
|
|
|
|
#ifdef _WIN32
|
|
#ifdef HOT_RELOAD_BUILD_SHARED
|
|
#define HOT_RELOAD_API __declspec(dllexport)
|
|
#elif defined(HOT_RELOAD_USE_SHARED)
|
|
#define HOT_RELOAD_API __declspec(dllimport)
|
|
#else
|
|
#define HOT_RELOAD_API
|
|
#endif
|
|
#else
|
|
#if defined(HOT_RELOAD_BUILD_SHARED) || defined(HOT_RELOAD_USE_SHARED)
|
|
#define HOT_RELOAD_API __attribute__((visibility("default")))
|
|
#else
|
|
#define HOT_RELOAD_API
|
|
#endif
|
|
#endif
|
|
|
|
/* Marks a single declaration for export under
|
|
* hot_reload_restrict_exports() (cmake/HotReload.cmake) without
|
|
* otherwise changing its linkage: pair with extern "C" yourself for a
|
|
* free function you want a stable, human-typable name for (see this
|
|
* file's own comment on when you'd want that) -- extern "C" first,
|
|
* then this macro, then the return type, e.g.
|
|
* `extern "C" HOT_RELOAD_EXPORT int my_function(...)`, which is the
|
|
* one ordering GCC/Clang accept without a warning here. Leave the
|
|
* extern "C" off to keep a C++ class or method's real mangled name,
|
|
* e.g. a virtual method a host only ever reaches through a vtable
|
|
* slot (see the top-level README's "Hot reloading C++ symbols"
|
|
* section).
|
|
*
|
|
* hot_reload_restrict_exports() is a narrower export surface, not a
|
|
* quieter one: see the README's "Why do std::string/std::vector
|
|
* symbols show up as PATCHED?" section for why it does not, and
|
|
* structurally cannot, suppress those specifically. */
|
|
#ifdef _WIN32
|
|
#define HOT_RELOAD_EXPORT __declspec(dllexport)
|
|
#else
|
|
#define HOT_RELOAD_EXPORT __attribute__((visibility("default")))
|
|
#endif
|
|
|
|
#ifdef __cplusplus
|
|
extern "C"
|
|
{
|
|
#endif
|
|
|
|
/* Opaque -- 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. */
|
|
typedef struct HotReloadContext HotReloadContext;
|
|
|
|
/* Every fallible function returns one of these -- see section 16
|
|
* ("Do: return an integer error code / status enum from every
|
|
* fallible function"). */
|
|
typedef enum HotReloadStatus
|
|
{
|
|
HOT_RELOAD_OK = 0,
|
|
|
|
/* ctx, module_name, or path/new_path was NULL, or module_name
|
|
* was not found for hot_reload_reload / hot_reload_get_symbol. */
|
|
HOT_RELOAD_ERROR_INVALID_ARGUMENT,
|
|
|
|
/* dlopen()/LoadLibrary() (or the equivalent parse of the file on
|
|
* disk) failed for the given path. On hot_reload_reload(),
|
|
* this is non-destructive: the previously loaded version is left
|
|
* running untouched, exactly as if reload had not been called --
|
|
* see the file comment above HotReloadContext::reload(). Call
|
|
* hot_reload_last_error() for the platform's own description of
|
|
* why -- this one status code covers everything from a missing
|
|
* file to a genuinely corrupt image to (a real case, found while
|
|
* building this feature) a shared library with an unresolved
|
|
* symbol reference, and those need very different fixes. */
|
|
HOT_RELOAD_ERROR_LOAD_FAILED,
|
|
|
|
/* hot_reload_reload() was called for a module_name that was never
|
|
* hot_reload_load()'ed first, so there is no "old" version to
|
|
* diff against. Call hot_reload_load() once, up front. */
|
|
HOT_RELOAD_ERROR_NO_PREVIOUS_VERSION,
|
|
|
|
/* This process's CPU architecture has no patch backend compiled
|
|
* in (see Architecture.hpp -- currently x86, x86-64, and ARM64).
|
|
* Nothing was patched; hot_reload_load() still succeeded, you
|
|
* just cannot hot_reload_reload() on this target. */
|
|
HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE
|
|
} HotReloadStatus;
|
|
|
|
/* Reported once per matched/unmatched symbol during hot_reload_reload()
|
|
* via HotReloadOptions::on_symbol_event, so the host application can
|
|
* log (or assert on, in a test) exactly what happened -- nothing here
|
|
* is silent. */
|
|
typedef enum HotReloadSymbolEvent
|
|
{
|
|
/* Existed in both builds and was classified as code in both --
|
|
* the old function's entry now jumps to the new one. */
|
|
HOT_RELOAD_SYMBOL_PATCHED,
|
|
|
|
/* Exists in the new build only. Nothing to patch -- it becomes
|
|
* reachable the normal way, e.g. via hot_reload_get_symbol(). */
|
|
HOT_RELOAD_SYMBOL_ADDED,
|
|
|
|
/* Existed in the old build, missing from the new one. Per
|
|
* cpp-style-guide.md's spirit of never leaving a caller with an
|
|
* ambiguous, half-updated state: the old implementation is left
|
|
* running as-is unless HotReloadOptions::on_deleted_symbol
|
|
* supplies a stub to redirect to instead -- see that field. */
|
|
HOT_RELOAD_SYMBOL_REMOVED,
|
|
|
|
/* Present in both builds by name, but the symbol table classifies
|
|
* it as data (or something else non-function) in either build --
|
|
* e.g. an accidentally-exported global. Never touched, in either
|
|
* direction. This is the mechanical half of the "only executable
|
|
* code changes" guarantee described at the top of this file. */
|
|
HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE,
|
|
|
|
/* Classified as a function in both builds, but the OLD function's
|
|
* compiled size is smaller than the number of bytes this
|
|
* platform's jump sequence needs to write -- patching it would
|
|
* overwrite whatever comes after it in memory. Skipped rather
|
|
* than guessed; see hot_reload_enable_patchable_functions() in
|
|
* cmake/HotReload.cmake for how to stop this from happening. */
|
|
HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL,
|
|
|
|
/* Classified as a function in both builds, correctly sized, but
|
|
* the OS refused to make its page temporarily writable (mprotect /
|
|
* VirtualProtect failed -- e.g. a hardened environment that denies
|
|
* W^X transitions). Rare in a normal developer setup; reported
|
|
* rather than silently ignored either way. */
|
|
HOT_RELOAD_SYMBOL_SKIPPED_UNWRITABLE
|
|
} HotReloadSymbolEvent;
|
|
|
|
/* One event report, passed to HotReloadOptions::on_symbol_event.
|
|
* old_address/new_address are NULL when not applicable (e.g.
|
|
* old_address is NULL for HOT_RELOAD_SYMBOL_ADDED). */
|
|
typedef struct HotReloadSymbolEventInfo
|
|
{
|
|
const char* name;
|
|
HotReloadSymbolEvent kind;
|
|
void* old_address;
|
|
void* new_address;
|
|
} HotReloadSymbolEventInfo;
|
|
|
|
typedef void (*HotReloadSymbolEventFn)(const HotReloadSymbolEventInfo* event, void* user_data);
|
|
|
|
/* Called instead of patching, once per HOT_RELOAD_SYMBOL_REMOVED
|
|
* event, so the host can catch stale references during development
|
|
* instead of them silently continuing to run old code forever. Return
|
|
* the address of a replacement to redirect the old function to (its
|
|
* only safe use is a fixed-signature stub -- e.g. `void stub(void)`--
|
|
* that logs/asserts; this library has no way to know the deleted
|
|
* function's real signature, so redirecting to anything that reads
|
|
* arguments the caller didn't pass is undefined behavior, exactly
|
|
* like calling any function through the wrong function-pointer type).
|
|
* Return NULL (or pass a NULL callback in HotReloadOptions to begin
|
|
* with) to leave the old implementation running untouched -- the
|
|
* default, and the safe choice unless you specifically want deleted
|
|
* functions to be loud. */
|
|
typedef void* (*HotReloadDeletedSymbolFn)(const char* symbol_name, void* user_data);
|
|
|
|
/* Passed to hot_reload_context_create(); NULL is a valid options
|
|
* pointer meaning "no callbacks, default policy for everything". */
|
|
typedef struct HotReloadOptions
|
|
{
|
|
HotReloadSymbolEventFn on_symbol_event; /* nullable */
|
|
HotReloadDeletedSymbolFn on_deleted_symbol; /* nullable */
|
|
void* user_data; /* passed back to both callbacks, untouched */
|
|
} HotReloadOptions;
|
|
|
|
/* Caller owns the returned pointer and must call
|
|
* hot_reload_context_destroy() -- see cpp-style-guide.md section 16
|
|
* ("Document ownership transfer explicitly"). options may be NULL. */
|
|
HOT_RELOAD_API HotReloadContext* hot_reload_context_create(const HotReloadOptions* options);
|
|
|
|
HOT_RELOAD_API void hot_reload_context_destroy(HotReloadContext* ctx);
|
|
|
|
/* Loads path for the first time under module_name -- an arbitrary
|
|
* caller-chosen key used to look the module back up in later
|
|
* hot_reload_reload()/hot_reload_get_symbol() calls; it does not need
|
|
* to match anything on disk. Returns HOT_RELOAD_ERROR_INVALID_ARGUMENT
|
|
* if module_name was already loaded -- call hot_reload_reload()
|
|
* instead of hot_reload_load() again for the same name. */
|
|
HOT_RELOAD_API HotReloadStatus hot_reload_load(HotReloadContext* ctx, const char* module_name, const char* path);
|
|
|
|
/* Loads a NEW build of the module previously registered under
|
|
* module_name, diffs its exported functions against the previous
|
|
* version, and patches every one that is safe to patch in place --
|
|
* see HotReloadSymbolEvent above for exactly what happens to each
|
|
* kind of change. On any HOT_RELOAD_ERROR_* return, nothing was
|
|
* changed: the previous version keeps running exactly as it was. */
|
|
HOT_RELOAD_API HotReloadStatus hot_reload_reload(HotReloadContext* ctx, const char* module_name, const char* new_path);
|
|
|
|
/* Resolves symbol_name in module_name's CURRENT build (i.e. always
|
|
* the freshest one loaded, patched or not) -- for calling a function
|
|
* that did not exist yet when the host first started, such as one
|
|
* reported via HOT_RELOAD_SYMBOL_ADDED. Returns NULL if module_name
|
|
* is unknown or symbol_name is not an exported function in it. */
|
|
HOT_RELOAD_API void* hot_reload_get_symbol(HotReloadContext* ctx, const char* module_name, const char* symbol_name);
|
|
|
|
/* Only meaningful immediately after hot_reload_load() or
|
|
* hot_reload_reload() on this SAME ctx returned something other than
|
|
* HOT_RELOAD_OK: the underlying platform's own description of why --
|
|
* dlerror() on POSIX, the formatted message for GetLastError() on
|
|
* Windows -- e.g. "undefined symbol: _ZN6Widget4tickEi" for a shared
|
|
* library with an unresolved reference (a real, confusing-until-you-
|
|
* see-this-message case: a non-inline static class member that is
|
|
* declared but never given an out-of-class definition compiles fine
|
|
* and links fine into a shared library -- the linker allows a shared
|
|
* library to keep undefined symbols by default -- and only fails,
|
|
* right here, once something ODR-uses it and the loader tries to
|
|
* resolve it for real). Overwritten by the next
|
|
* hot_reload_load()/hot_reload_reload() call on this ctx; copy it out
|
|
* immediately if you need to keep it past that point (e.g. for a log
|
|
* line). Empty if the platform gave no further detail, or if the
|
|
* failure never reached the platform loader at all (e.g. a NULL
|
|
* argument, which is HOT_RELOAD_ERROR_INVALID_ARGUMENT before
|
|
* anything platform-specific happens). A static string internally --
|
|
* never free() this. */
|
|
HOT_RELOAD_API const char* hot_reload_last_error(HotReloadContext* ctx);
|
|
|
|
/* A static string literal -- never free() this. */
|
|
HOT_RELOAD_API const char* hot_reload_status_string(HotReloadStatus status);
|
|
|
|
/* Renders name for a human to read: a real C++ mangled name (Itanium
|
|
* on GCC/Clang/MinGW, MSVC's own scheme under MSVC -- whichever this
|
|
* library itself was built with) becomes something like
|
|
* "Widget::tick(int)"; anything else, including an ordinary extern "C"
|
|
* name, comes back unchanged. Safe to call unconditionally on any
|
|
* HotReloadSymbolEventInfo::name or hot_reload_get_symbol() key --
|
|
* this never changes what name means for lookup purposes, it is only
|
|
* ever applied when you ask for it, purely for display. Returns NULL
|
|
* only if name itself is NULL or allocation fails; caller must free
|
|
* a non-NULL result with hot_reload_free_string(). */
|
|
HOT_RELOAD_API char* hot_reload_demangle(const char* name);
|
|
|
|
HOT_RELOAD_API void hot_reload_free_string(char* str);
|
|
|
|
#ifdef __cplusplus
|
|
}
|
|
#endif
|
|
|
|
#endif /* HOT_RELOAD_H */
|