Files
Volkan/source/HotReload/src/c-api/HotReloadApi.cpp
T
2026-11-03 15:22:24 -03:00

311 lines
11 KiB
C++

/**
* @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 <atomic>
#include <cstdint>
#include <string>
#include <unordered_map>
/// 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<std::uint64_t> 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<std::uint64_t, std::string>& t_lastErrorByContextId()
{
thread_local std::unordered_map<std::uint64_t, std::string> 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
}
}