/** * @file HotReloadApi.cpp * @brief The extern "C" shim implementing hot_reload.h on top of hot_reload::Context. * * Deliberately loose coupling, not the tight opaque-pointer-reinterpret_cast PIMPL * cpp-style-guide.md's own NetManager example uses elsewhere in this codebase: `::HotReloadContext` * (the struct this file defines, matching the opaque forward declaration in `hot_reload.h`) *owns* * a `hot_reload::Context` by value, and every function below reaches it only through Context's own * public methods -- exactly as any other C++ caller of `Context.hpp` would, with no special access * and no reinterpret_cast between unrelated types. `hot_reload::Context` has no idea this file, or * the C API it implements, exists at all. */ #include "hot_reload/hot_reload.h" #include "hot_reload/Context.hpp" #include "hot_reload/Result.hpp" #include "hot_reload/Status.hpp" #include "hot_reload/SymbolEventInfo.hpp" #include #include #include #include /// Defines the type `hot_reload.h` only forward-declares (`typedef struct HotReloadContext /// HotReloadContext;`) -- every function in this file sees the real definition, so ordinary /// member access replaces the reinterpret_cast a true opaque pointer would otherwise need. struct HotReloadContext { explicit HotReloadContext(hot_reload::Options options) : impl(std::move(options)) { } /// A process-wide, never-reused identifier for this context, used only to key /// t_lastErrorByContextId() below -- see that function's own comment for why a raw `this` /// pointer is not used for that instead. std::uint64_t id = nextId(); /// The real implementation. Composition, not inheritance or a cast: this file uses it /// exactly the way any other hot_reload::Context caller would. hot_reload::Context impl; private: static std::uint64_t nextId() { static std::atomic counter{1}; return counter.fetch_add(1, std::memory_order_relaxed); } }; namespace { /// hot_reload.h's `hot_reload_last_error()` is a stateful call: "whatever the most recent /// load()/reload() on this ctx failed with". hot_reload::Result has no equivalent shared /// state at all -- each call's outcome comes back in its own Result -- which is exactly what /// makes the C++ API safe to use concurrently without two threads' errors ever being /// confused (see Context.hpp's thread-safety note). Reintroducing that stateful call for the /// C API's own backward compatibility, now safely, means tracking it per *(thread, context)* /// pair: a plain `thread_local` map needs no lock at all, since no other thread can ever see /// this one's instance of it. /// /// Keyed by HotReloadContext::id rather than the context's own address: a destroyed /// context's memory can be reused by an unrelated, later one, and keying by address would /// risk a brand new context momentarily reading a stale error string left behind by /// whatever used to live at that same address. std::unordered_map& t_lastErrorByContextId() { thread_local std::unordered_map errors; return errors; } /// Records @p result's message, if it has one, as this thread's last error for @p ctx. /// @param ctx The context the call was made on. /// @param result The call's own outcome. void recordLastError(const HotReloadContext& ctx, const hot_reload::Result& result) { if (result.message().empty()) { return; } t_lastErrorByContextId()[ctx.id] = result.message(); } /// @param status A hot_reload::Status. /// @return The equivalent HotReloadStatus. HotReloadStatus toCStatus(hot_reload::Status status) { switch (status) { case hot_reload::Status::Ok: return HOT_RELOAD_OK; case hot_reload::Status::InvalidArgument: return HOT_RELOAD_ERROR_INVALID_ARGUMENT; case hot_reload::Status::LoadFailed: return HOT_RELOAD_ERROR_LOAD_FAILED; case hot_reload::Status::NoPreviousVersion: return HOT_RELOAD_ERROR_NO_PREVIOUS_VERSION; case hot_reload::Status::UnsupportedArchitecture: return HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE; } return HOT_RELOAD_ERROR_LOAD_FAILED; // unreachable for any enumerator; -Wswitch catches a missing one above } /// @param status A HotReloadStatus. /// @return The equivalent hot_reload::Status. hot_reload::Status toCppStatus(HotReloadStatus status) { switch (status) { case HOT_RELOAD_OK: return hot_reload::Status::Ok; case HOT_RELOAD_ERROR_INVALID_ARGUMENT: return hot_reload::Status::InvalidArgument; case HOT_RELOAD_ERROR_LOAD_FAILED: return hot_reload::Status::LoadFailed; case HOT_RELOAD_ERROR_NO_PREVIOUS_VERSION: return hot_reload::Status::NoPreviousVersion; case HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE: return hot_reload::Status::UnsupportedArchitecture; } return hot_reload::Status::LoadFailed; // unreachable for any enumerator; -Wswitch catches a missing one above } /// @param kind A hot_reload::SymbolEvent. /// @return The equivalent HotReloadSymbolEvent. HotReloadSymbolEvent toCEvent(hot_reload::SymbolEvent kind) { switch (kind) { case hot_reload::SymbolEvent::Patched: return HOT_RELOAD_SYMBOL_PATCHED; case hot_reload::SymbolEvent::Added: return HOT_RELOAD_SYMBOL_ADDED; case hot_reload::SymbolEvent::Removed: return HOT_RELOAD_SYMBOL_REMOVED; case hot_reload::SymbolEvent::SkippedNotCode: return HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE; case hot_reload::SymbolEvent::SkippedTooSmall: return HOT_RELOAD_SYMBOL_SKIPPED_TOO_SMALL; case hot_reload::SymbolEvent::SkippedUnwritable: return HOT_RELOAD_SYMBOL_SKIPPED_UNWRITABLE; } return HOT_RELOAD_SYMBOL_SKIPPED_NOT_CODE; // unreachable for any enumerator; -Wswitch catches one missing above } /// Builds the hot_reload::Options this file's two C callback adapters below run through, /// from a (possibly null) HotReloadOptions*. /// @param options The options hot_reload_context_create() was called with; null means none. /// @return Options wrapping @p options's two callbacks (each a no-op if @p options is null, /// or if the specific callback field itself is null), capturing nothing but the /// plain C function pointer and user_data they were given, so this is safe to store /// for the Context's whole lifetime. hot_reload::Options toCppOptions(const HotReloadOptions* options) { hot_reload::Options result; if (options == nullptr) { return result; } if (options->on_symbol_event != nullptr) { HotReloadSymbolEventFn callback = options->on_symbol_event; void* userData = options->user_data; result.onSymbolEvent = [callback, userData](const hot_reload::SymbolEventInfo& event) { // name.data() is safe to hand out as a NUL-terminated const char* here: it // ultimately points into a detail::ExportedSymbol's own std::string (see // Context.cpp's emitEvent()), which is always NUL-terminated. HotReloadSymbolEventInfo cEvent; cEvent.name = event.name.data(); cEvent.kind = toCEvent(event.kind); cEvent.old_address = event.oldAddress; cEvent.new_address = event.newAddress; callback(&cEvent, userData); }; } if (options->on_deleted_symbol != nullptr) { HotReloadDeletedSymbolFn callback = options->on_deleted_symbol; void* userData = options->user_data; result.onDeletedSymbol = [callback, userData](std::string_view deletedSymbolName) -> void* { // Same reasoning as above for passing .data() directly as a C string. return callback(deletedSymbolName.data(), userData); }; } return result; } } // Per cpp-style-guide.md section 11: a C++ exception must never cross // an extern "C" boundary -- doing so is undefined behavior for any // caller that is not itself compiled to expect one, which a plain C // caller of this API never is. Every function here is a thin, // non-throwing wrapper for exactly that reason: std::string/vector/ // unordered_map construction (on the hot_reload::Context side of this // boundary) is the only thing that could throw at all (std::bad_alloc // under memory pressure), so catching (...) and mapping it to this // API's existing failure values is sufficient without needing // per-exception-type handling. extern "C" { HotReloadContext* hot_reload_context_create(const HotReloadOptions* options) { try { return new HotReloadContext(toCppOptions(options)); } catch (...) { return nullptr; } } void hot_reload_context_destroy(HotReloadContext* ctx) { if (ctx == nullptr) { return; } // Best-effort: removes this (calling) thread's own entry for ctx, if it has one. Another // thread's thread_local entry for the same id, if it made calls on this ctx too, is left // in place -- there is no way to reach another thread's thread_local storage from here -- // a small, bounded-by-distinct-destroyed-context-count leak, accepted as the cost of // hot_reload_last_error() needing no lock at all on the hot (every-call) path. t_lastErrorByContextId().erase(ctx->id); delete ctx; } HotReloadStatus hot_reload_load(HotReloadContext* ctx, const char* module_name, const char* path) { if (ctx == nullptr || module_name == nullptr || path == nullptr) { return HOT_RELOAD_ERROR_INVALID_ARGUMENT; } try { hot_reload::Result result = ctx->impl.load(module_name, path); recordLastError(*ctx, result); return toCStatus(result.status()); } catch (...) { return HOT_RELOAD_ERROR_LOAD_FAILED; } } HotReloadStatus hot_reload_reload(HotReloadContext* ctx, const char* module_name, const char* new_path) { if (ctx == nullptr || module_name == nullptr || new_path == nullptr) { return HOT_RELOAD_ERROR_INVALID_ARGUMENT; } try { hot_reload::Result result = ctx->impl.reload(module_name, new_path); recordLastError(*ctx, result); return toCStatus(result.status()); } catch (...) { return HOT_RELOAD_ERROR_LOAD_FAILED; } } void* hot_reload_get_symbol(HotReloadContext* ctx, const char* module_name, const char* symbol_name) { if (ctx == nullptr || module_name == nullptr || symbol_name == nullptr) { return nullptr; } try { return ctx->impl.findSymbol(module_name, symbol_name); } catch (...) { return nullptr; } } const char* hot_reload_last_error(HotReloadContext* ctx) { if (ctx == nullptr) { return ""; } try { auto& errors = t_lastErrorByContextId(); auto found = errors.find(ctx->id); return found == errors.end() ? "" : found->second.c_str(); } catch (...) { return ""; } } const char* hot_reload_status_string(HotReloadStatus status) { return hot_reload::toString(toCppStatus(status)).data(); // points into a static string literal either way } }