Added ARM32 support

This commit is contained in:
alejo committed 2026-11-03 15:22:24 -03:00
1 parent df792729ef
commit 1212cb9dbe
36 files changed
+2381 -783

No files matched your search

+210 -36
View File
@@ -1,23 +1,194 @@
/**
* @file HotReloadApi.cpp
* @brief The extern "C" shim implementing hot_reload.h by delegating to hot_reload::HotReloadContext.
* @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 "../HotReloadContext.hpp"
#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
{
/// The opaque ::HotReloadContext the public header declares is
/// never actually instantiated as itself -- every instance really
/// is a hot_reload::HotReloadContext, following the same
/// opaque-pointer PIMPL pattern cpp-style-guide.md's NetManager
/// example uses. This is the one place that cast happens.
/// @param ctx A context previously returned by hot_reload_context_create(), or null.
/// @return The same object viewed as the C++ type it really is (null if @p ctx is null).
hot_reload::HotReloadContext* impl(::HotReloadContext* ctx)
/// 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()
{
return reinterpret_cast<hot_reload::HotReloadContext*>(ctx);
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;
}
}
@@ -26,11 +197,11 @@ namespace
// 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 is the only thing in hot_reload::
// HotReloadContext 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.
// 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"
{
@@ -38,7 +209,7 @@ extern "C"
{
try
{
return reinterpret_cast<::HotReloadContext*>(new hot_reload::HotReloadContext(options));
return new HotReloadContext(toCppOptions(options));
}
catch (...)
{
@@ -48,7 +219,17 @@ extern "C"
void hot_reload_context_destroy(HotReloadContext* ctx)
{
delete impl(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)
@@ -59,7 +240,9 @@ extern "C"
}
try
{
return impl(ctx)->load(module_name, path);
hot_reload::Result result = ctx->impl.load(module_name, path);
recordLastError(*ctx, result);
return toCStatus(result.status());
}
catch (...)
{
@@ -75,7 +258,9 @@ extern "C"
}
try
{
return impl(ctx)->reload(module_name, new_path);
hot_reload::Result result = ctx->impl.reload(module_name, new_path);
recordLastError(*ctx, result);
return toCStatus(result.status());
}
catch (...)
{
@@ -91,7 +276,7 @@ extern "C"
}
try
{
return impl(ctx)->getSymbol(module_name, symbol_name);
return ctx->impl.findSymbol(module_name, symbol_name);
}
catch (...)
{
@@ -107,7 +292,9 @@ extern "C"
}
try
{
return impl(ctx)->lastError().c_str();
auto& errors = t_lastErrorByContextId();
auto found = errors.find(ctx->id);
return found == errors.end() ? "" : found->second.c_str();
}
catch (...)
{
@@ -117,20 +304,7 @@ extern "C"
const char* hot_reload_status_string(HotReloadStatus status)
{
switch (status)
{
case HOT_RELOAD_OK:
return "ok";
case HOT_RELOAD_ERROR_INVALID_ARGUMENT:
return "invalid argument";
case HOT_RELOAD_ERROR_LOAD_FAILED:
return "load failed";
case HOT_RELOAD_ERROR_NO_PREVIOUS_VERSION:
return "no previous version loaded for this module name";
case HOT_RELOAD_ERROR_UNSUPPORTED_ARCHITECTURE:
return "no patch backend for this CPU architecture";
}
return "unknown hot reload status";
return hot_reload::toString(toCppStatus(status)).data(); // points into a static string literal either way
}
}