311 lines
11 KiB
C++
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
|
|
}
|
|
|
|
}
|