/** * @file hot_reload.h * @brief Public C API for the hot-reload library. * * See @c 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. * * @section hotreload_what_it_does 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. * * @section hotreload_what_it_wont 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 at the top of * `SymbolTable.cpp` (and detail::ExportedSymbol::isFunction) 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) rather than reusing the old name. See the * "Limitations" section of the top-level README (and "What this cannot * detect" in `source/HotReload/README.md`) for the full list of hazards * this library cannot detect for you. * * @section hotreload_requirements 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 "Hot * reloading C++ symbols" section of `source/HotReload/README.md` for the * full picture, including how to get a readable name for logging with * hot_reload_demangle(). * * Every function the module exports needs room at its entry point for the * jump this library writes over it. Compile the module with * `-fpatchable-function-entry=32,0` (GCC/Clang) or `/hotpatch` (MSVC) to * guarantee that; without it, a function too small to hold the jump is * skipped and reported as ::HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL rather than * patched. Also avoid identical-code-folding linker options (MSVC * `/OPT:ICF`, `--icf` in gold/lld) for the module: two different exported * functions folded into one address can only be patched to one target. * * @section hotreload_lifetime What carries over a reload -- and what does not * * Only *code* is redirected. Every build of the module is a separate * mapping with its own copy of everything else, so: * - Global and `static` variables are not carried over. Code that runs * after a reload uses the *new* build's copies, freshly initialised; * keep state you need to survive in memory the host owns and pass it in. * - Static constructors of the new build run when it is loaded (on every * reload); static destructors of every version run when the context is * destroyed. * - Every version stays loaded until hot_reload_context_destroy(). Old * entry points keep working (they are now jumps), one extra hop per * reload; destroying the context unloads them all, so no pointer into * any of the modules may be used afterwards. * * @section hotreload_threads Threading * * A context is not thread-safe: serialize all calls that use one context. * Call hot_reload_reload() from a quiescent point -- one where no thread is * executing inside the module's functions -- since a function that is being * executed while its entry bytes are rewritten cannot be protected. */ #ifndef HOT_RELOAD_H #define HOT_RELOAD_H #include /** * @def HOT_RELOAD_API * @brief Marks every function of this library's own C API. * * Expands to nothing in the default configuration, where the library is linked statically. * Define `HOT_RELOAD_BUILD_SHARED` while building the library as a shared library, and * `HOT_RELOAD_USE_SHARED` while compiling code that uses such a build, to get * `__declspec(dllexport)`/`__declspec(dllimport)` on Windows or default symbol visibility * elsewhere. * * Not to be confused with ::HOT_RELOAD_EXPORT, which marks symbols in a hot-reloadable * *module* rather than in this library. */ #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 /** * @def HOT_RELOAD_EXPORT * @brief Marks a single declaration in a hot-reloadable *module* for export * under `hot_reload_restrict_exports()` (`cmake/HotReload.cmake`). * * This is for the plugins you hot-reload, not for this library's own API * above (::HOT_RELOAD_API). It does not otherwise change a declaration's * linkage: pair it with `extern "C"` yourself for a free function you want * a stable, human-typable name for (see @ref hotreload_requirements for * 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 "Hot reloading C++ symbols" section of * `source/HotReload/README.md`). * * @note `hot_reload_restrict_exports()` is a *narrower* export surface, * not a *quieter* one: see the "Why do std::string/std::vector * symbols show up as PATCHED?" section of `source/HotReload/README.md` * 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 /** * @defgroup hotreload_types Types and callbacks * @brief The status codes, event/option structs, and callback signatures * used throughout the rest of this API. * @{ */ /** * @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 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; /** * @brief Result of any fallible call in this API. * * See `cpp-style-guide.md` section 16 ("Do: return an integer error * code / status enum from every fallible function"). Render one of these * as text with hot_reload_status_string(); for a fuller, platform-specific * explanation of a ::HOT_RELOAD_ERROR_LOAD_FAILED, see * hot_reload_last_error(). */ typedef enum HotReloadStatus { /** Success. */ HOT_RELOAD_OK = 0, /** * A required argument (@c ctx, @c module_name, or @c path / @c new_path) * was `NULL`, or hot_reload_load() was called for a @c module_name that is * already loaded -- call hot_reload_reload() for that name instead. */ 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. 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. * * It is also reported when the file could not be copied to its staging * name (see hot_reload_load()): the source is unreadable, or the directory * it lives in is not writable. */ HOT_RELOAD_ERROR_LOAD_FAILED, /** * hot_reload_reload() was called for a @c 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, ARM64, and ARM32). * Nothing was patched; hot_reload_load() still succeeded, you just * cannot hot_reload_reload() on this target. */ HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE } HotReloadStatus; /** * @brief What happened to one symbol during a hot_reload_reload() call. * * Reported once per matched/unmatched symbol 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 in * @ref hotreload_what_it_wont. */ 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. 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. */ 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; /** * @brief One symbol's outcome, passed to HotReloadOptions::on_symbol_event. */ typedef struct HotReloadSymbolEventInfo { const char* name; /**< The symbol's real name (mangled, if it's a C++ symbol -- see hot_reload_demangle()). */ HotReloadSymbolEvent kind; /**< What happened to it -- see ::HotReloadSymbolEvent. */ void* old_address; /**< The symbol's address in the previous build, or `NULL` if not applicable (e.g. ::HOT_RELOAD_SYMBOL_ADDED). */ void* new_address; /**< The symbol's address in the new build, or `NULL` if not applicable (e.g. ::HOT_RELOAD_SYMBOL_REMOVED). */ } HotReloadSymbolEventInfo; /** * @brief Callback invoked once per symbol on every hot_reload_reload() call. * * Called synchronously, on the thread that called hot_reload_reload(), while the reload is in * progress. * * @param event Describes one symbol's outcome; valid only for the duration of the call. * @param user_data Whatever was passed as HotReloadOptions::user_data, untouched. * * @warning Do not call hot_reload_load() or hot_reload_reload() on the same context from inside * this callback. */ typedef void (*HotReloadSymbolEventFn)(const HotReloadSymbolEventInfo* event, void* user_data); /** * @brief Callback invoked instead of patching, once per ::HOT_RELOAD_SYMBOL_REMOVED event. * * Lets the host catch stale references during development instead of them * silently continuing to run old code forever. * * Called synchronously, on the thread that called hot_reload_reload(), while the reload is in * progress; like HotReloadSymbolEventFn, it must not call hot_reload_load() or * hot_reload_reload() on the same context. * * @param symbol_name The removed function's real (possibly mangled) name. * @param user_data Whatever was passed as HotReloadOptions::user_data, untouched. * @return The address of a replacement to redirect the old function to, or * `NULL` to leave the old implementation running untouched (the * default, and the safe choice unless you specifically want * deleted functions to be loud). * * @warning A non-`NULL` return's 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. */ typedef void* (*HotReloadDeletedSymbolFn)(const char* symbol_name, void* user_data); /** * @brief Options passed to hot_reload_context_create(). * * A `NULL` options pointer is valid and means "no callbacks, default * policy for everything." */ typedef struct HotReloadOptions { HotReloadSymbolEventFn on_symbol_event; /**< Nullable. See ::HotReloadSymbolEventFn. */ HotReloadDeletedSymbolFn on_deleted_symbol; /**< Nullable. See ::HotReloadDeletedSymbolFn. */ void* user_data; /**< Passed back to both callbacks, untouched. */ } HotReloadOptions; /** @} */ /* end of hotreload_types */ /** * @defgroup hotreload_lifecycle Context lifecycle * @brief Creating and destroying a hot-reload context. * @{ */ /** * @brief Creates a new hot-reload context. * @param options Nullable; see ::HotReloadOptions. Its contents are copied, so it need not * outlive this call. * @return A new context, or `NULL` if allocation failed. The caller owns it and must call * hot_reload_context_destroy() (see `cpp-style-guide.md` section 16, * "Document ownership transfer explicitly"). */ HOT_RELOAD_API HotReloadContext* hot_reload_context_create(const HotReloadOptions* options); /** * @brief Destroys a context created by hot_reload_context_create(). * * Unloads every version of every module the context loaded (running their static destructors). * From then on, every pointer obtained through this context -- functions, data, and the old * entry points that had been patched to jump into newer versions -- is dangling and must not be * used. Destroy a context only when nothing can still call into its modules. * * @param ctx The context to destroy. Safe to pass `NULL`. */ HOT_RELOAD_API void hot_reload_context_destroy(HotReloadContext* ctx); /** @} */ /* end of hotreload_lifecycle */ /** * @defgroup hotreload_loading Loading and reloading * @brief The core operations: register a module, hot-reload it, and * resolve its symbols. * @{ */ /** * @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 * (`.hotreload..`) and that copy is loaded, which keeps a rebuild that * overwrites @c 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 * (see the top-level README). This means the directory containing @c path must be writable. * * The module's static constructors run as part of loading it. * * @note If the module's symbol table cannot be read (for instance a `.so` whose section headers * were stripped), loading still succeeds but the module exports nothing as far as this * library is concerned: hot_reload_get_symbol() finds nothing and a later * hot_reload_reload() has nothing to patch. * * @param ctx The context to load into. * @param 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. * @param path Path to the shared library (`.so`/`.dll`) to load. Always treated as a file path: * a bare file name such as `plugin.so` means the file of that name in the current * working directory (it is never looked up through the library search path). * @return ::HOT_RELOAD_OK, or ::HOT_RELOAD_ERROR_INVALID_ARGUMENT if * an argument was `NULL` or @c module_name was already loaded (call * hot_reload_reload() instead of hot_reload_load() again for the same * name), or ::HOT_RELOAD_ERROR_LOAD_FAILED (see hot_reload_last_error() * for why). */ HOT_RELOAD_API HotReloadStatus hot_reload_load(HotReloadContext* ctx, const char* module_name, const char* path); /** * @brief Hot-reloads a module previously registered with hot_reload_load(). * * Loads a new build of the module, diffs its exported functions against * the previous version, and patches every one that is safe to patch in * place -- see ::HotReloadSymbolEvent for exactly what happens to each * kind of change. 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 * as ::HOT_RELOAD_SYMBOL_REMOVED. * * Call this from a quiescent point (see @ref hotreload_threads), and see * @ref hotreload_lifetime for what does not carry over. @c new_path follows * the same rules as @c path in hot_reload_load(). * * @param ctx The context @c module_name was hot_reload_load()'ed into. * @param module_name The same logical name passed to hot_reload_load(). * @param new_path Path to the new build of the shared library. * @return ::HOT_RELOAD_OK, or an `HOT_RELOAD_ERROR_*` code. On any error, * 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); /** * @brief Resolves a symbol in a module's current build. * * Always resolves against the freshest build loaded for @c module_name, * patched or not -- useful for calling a function that did not exist yet * when the host first started, such as one reported via * ::HOT_RELOAD_SYMBOL_ADDED. Any exported symbol resolves, data included * (a global variable, say); only functions are ever patched. * * @param ctx The context @c module_name was hot_reload_load()'ed into. * @param module_name The module to resolve @c symbol_name in. * @param symbol_name The symbol's real (possibly mangled) name. * @return The symbol's current address, or `NULL` if an argument is `NULL`, * @c module_name is unknown, or @c symbol_name is not exported by * the module's newest build. */ HOT_RELOAD_API void* hot_reload_get_symbol(HotReloadContext* ctx, const char* module_name, const char* symbol_name); /** @} */ /* end of hotreload_loading */ /** * @defgroup hotreload_diagnostics Diagnostics * @brief Turning a status code or a mangled name into something readable. * @{ */ /** * @brief The platform's own explanation for the most recent failure on this context. * * Only meaningful immediately after hot_reload_load() or * hot_reload_reload() on this *same* @c 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). * * @param ctx The context to query. * @return The platform's error message. Empty if it 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). Owned by @c ctx -- never `free()` * this. It stays valid only until the next hot_reload_load()/ * hot_reload_reload() call on this @c ctx, or until the context is * destroyed; copy it out immediately if you need to keep it past * that point (e.g. for a log line). */ HOT_RELOAD_API const char* hot_reload_last_error(HotReloadContext* ctx); /** * @brief Renders a ::HotReloadStatus as text. * @param status The status to render. * @return A static string literal -- never `free()` this. */ HOT_RELOAD_API const char* hot_reload_status_string(HotReloadStatus status); /** * @brief Renders a symbol 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 a name means for * lookup purposes, it is only ever applied when you ask for it, purely * for display. * * @param name The name to render. * @return The rendered name, or `NULL` if @c 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); /** * @brief Frees a string returned by hot_reload_demangle(). * @param str The string to free. Safe to pass `NULL`. */ HOT_RELOAD_API void hot_reload_free_string(char* str); /** @} */ /* end of hotreload_diagnostics */ #ifdef __cplusplus } #endif #endif /* HOT_RELOAD_H */