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

+16
View File
@@ -0,0 +1,16 @@
# Exercises hot_reload::Context (the C++ API) directly, including a real multi-threaded stress
# test -- see Main.cpp's own file comment. Reuses PluginV1/PluginV2 from
# source/examples/HotReloadDemo as fixtures, exactly as HotReloadTests does and for the same
# reason (see that CMakeLists.txt's own comment); requires BUILD_EXAMPLES as well as BUILD_TESTS
# for that reason.
add_test_module(context_tests OUTPUT_NAME "context_tests" DEPENDS hotreload_lib)
if(BUILD_EXAMPLES)
add_dependencies(context_tests hotreload_demo_plugin_v1 hotreload_demo_plugin_v2)
target_compile_definitions(context_tests PRIVATE
"PLUGIN_V1_PATH=\"$<TARGET_FILE:hotreload_demo_plugin_v1>\""
"PLUGIN_V2_PATH=\"$<TARGET_FILE:hotreload_demo_plugin_v2>\"")
else()
message(WARNING "context_tests needs BUILD_EXAMPLES=ON too (it reuses the HotReloadDemo plugins as fixtures); skipping its checks.")
target_compile_definitions(context_tests PRIVATE HOTRELOAD_TESTS_SKIP=1)
endif()
+328
View File
@@ -0,0 +1,328 @@
/**
* @file Main.cpp
* @brief Exercises hot_reload::Context directly -- the modern C++ API -- rather than through
* hot_reload.h, which HotReloadTests/Main.cpp already covers thoroughly. Two things only that
* test cannot check at all: that Context is genuinely usable without ever touching the C API,
* and that it is safe to call concurrently (see runConcurrencyStress() below, meant to be run
* under ThreadSanitizer -- see source/HotReload/README.md's "What's actually been tested").
*
* Reuses PluginV1/PluginV2 from source/examples/HotReloadDemo as fixtures, exactly as
* HotReloadTests/Main.cpp does and for the same reason (see that file's own comment).
*/
#include "hot_reload/Context.hpp"
#include "hot_reload/Demangle.hpp"
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <thread>
#include <vector>
#ifndef HOTRELOAD_TESTS_SKIP
namespace
{
int g_failures = 0;
/// Records a failed check to stderr and increments g_failures. Called only through CHECK().
/// @param condition The check's result.
/// @param expression The checked expression, stringified.
/// @param file The source file the check appears in.
/// @param line The line the check appears on.
void checkImpl(bool condition, const char* expression, const char* file, int line)
{
if (!condition)
{
std::fprintf(stderr, "CHECK FAILED: %s (%s:%d)\n", expression, file, line);
++g_failures;
}
}
}
/// Checks @p expr and records a failure (with its source location) if it is false, without
/// relying on assert(), which NDEBUG (this project's default Release build) compiles out.
#define CHECK(expr) checkImpl((expr), #expr, __FILE__, __LINE__)
namespace
{
using PluginFunction = int (*)(int);
/// Basic single-threaded correctness: load, resolve, call, reload, call the same pointer
/// again -- the same story HotReloadTests/Main.cpp already tells through the C API, told
/// here entirely through hot_reload::Context instead, to confirm the C API is a wrapper
/// around this and not the only way to get this behavior.
void runBasicUsage()
{
hot_reload::Context ctx;
hot_reload::Result loaded = ctx.load("plugin", PLUGIN_V1_PATH);
CHECK(loaded.ok());
CHECK(static_cast<bool>(loaded));
CHECK(loaded.status() == hot_reload::Status::Ok);
CHECK(loaded.message().empty());
// Loading the same name twice is InvalidArgument, with a message that explains why --
// reload() is for this, not a second load().
hot_reload::Result reloadedAsLoad = ctx.load("plugin", PLUGIN_V1_PATH);
CHECK(!reloadedAsLoad.ok());
CHECK(reloadedAsLoad.status() == hot_reload::Status::InvalidArgument);
auto tick = reinterpret_cast<PluginFunction>(ctx.findSymbol("plugin", "plugin_tick"));
CHECK(tick != nullptr);
CHECK(tick(10) == 20); // V1: input * 2
int patchedCount = 0;
int removedCount = 0;
hot_reload::Options options;
options.onSymbolEvent = [&patchedCount, &removedCount](const hot_reload::SymbolEventInfo& event)
{
if (event.kind == hot_reload::SymbolEvent::Patched)
{
++patchedCount;
}
else if (event.kind == hot_reload::SymbolEvent::Removed)
{
++removedCount;
CHECK(event.name == "plugin_legacy_feature");
}
CHECK(!event.summary().empty()); // exercises SymbolEventInfo::summary() for real
};
hot_reload::Context ctxWithCallback(std::move(options));
CHECK(ctxWithCallback.load("plugin", PLUGIN_V1_PATH).ok());
hot_reload::Result reloadResult = ctxWithCallback.reload("plugin", PLUGIN_V2_PATH);
CHECK(reloadResult.ok());
CHECK(patchedCount > 0);
CHECK(removedCount == 1);
auto tickAfterReload = reinterpret_cast<PluginFunction>(ctxWithCallback.findSymbol("plugin", "plugin_tick"));
CHECK(tickAfterReload != nullptr);
CHECK(tickAfterReload(10) == 30); // V2: input * 3
// reload() for a name that was never load()'ed.
hot_reload::Context emptyCtx;
hot_reload::Result noPrevious = emptyCtx.reload("never-loaded", PLUGIN_V1_PATH);
CHECK(!noPrevious.ok());
CHECK(noPrevious.status() == hot_reload::Status::NoPreviousVersion);
// load() for a file that cannot possibly exist.
hot_reload::Result loadFailed = emptyCtx.load("bogus", "/this/path/does/not/exist.so");
CHECK(!loadFailed.ok());
CHECK(loadFailed.status() == hot_reload::Status::LoadFailed);
CHECK(!loadFailed.message().empty());
// findSymbol() for an unknown module, and for an unknown symbol in a known one.
CHECK(ctx.findSymbol("never-loaded", "anything") == nullptr);
CHECK(ctx.findSymbol("plugin", "this_symbol_does_not_exist") == nullptr);
}
/// Result::success()/failure() constructed directly, outside any Context call at all --
/// confirms the type is a freely usable value type, not something only Context can produce.
void runResultAsValueType()
{
hot_reload::Result ok = hot_reload::Result::success();
CHECK(ok.ok());
CHECK(ok.status() == hot_reload::Status::Ok);
CHECK(ok.message().empty());
hot_reload::Result failed = hot_reload::Result::failure(hot_reload::Status::LoadFailed, "a specific reason");
CHECK(!failed.ok());
CHECK(failed.status() == hot_reload::Status::LoadFailed);
CHECK(failed.message() == "a specific reason");
for (hot_reload::Status status : {hot_reload::Status::Ok,
hot_reload::Status::InvalidArgument,
hot_reload::Status::LoadFailed,
hot_reload::Status::NoPreviousVersion,
hot_reload::Status::UnsupportedArchitecture})
{
CHECK(!hot_reload::toString(status).empty()); // every enumerator must map to something real
}
for (hot_reload::SymbolEvent event : {hot_reload::SymbolEvent::Patched,
hot_reload::SymbolEvent::Added,
hot_reload::SymbolEvent::Removed,
hot_reload::SymbolEvent::SkippedNotCode,
hot_reload::SymbolEvent::SkippedTooSmall,
hot_reload::SymbolEvent::SkippedUnwritable})
{
CHECK(!hot_reload::toString(event).empty());
}
}
/// Context is movable even though its Impl holds a (non-movable) std::shared_mutex --
/// confirms the PIMPL actually buys the movability Context.hpp's doc comment promises.
void runMoveSemantics()
{
hot_reload::Context original;
CHECK(original.load("plugin", PLUGIN_V1_PATH).ok());
hot_reload::Context moved(std::move(original));
CHECK(moved.findSymbol("plugin", "plugin_tick") != nullptr);
hot_reload::Context another;
another = std::move(moved);
CHECK(another.findSymbol("plugin", "plugin_tick") != nullptr);
}
/// hot_reload::demangle() used directly, not through hot_reload_demangle() -- see
/// Demangle.hpp. A real mangled name, not a hand-typed guess: taken from the demo's own
/// SymbolEvent log for CppWidget's constructor, which is exactly what a real caller would
/// have in hand (a name straight out of a SymbolEventInfo).
void runDemangle()
{
hot_reload::Context ctx;
CHECK(ctx.load("plugin", PLUGIN_V1_PATH).ok());
std::string realMangledName;
hot_reload::Options options;
options.onSymbolEvent = [&realMangledName](const hot_reload::SymbolEventInfo& event)
{
if (realMangledName.empty() && event.name.find("CppWidget") != std::string_view::npos)
{
realMangledName = event.name;
}
};
hot_reload::Context ctxForReload(std::move(options));
CHECK(ctxForReload.load("plugin", PLUGIN_V1_PATH).ok());
CHECK(ctxForReload.reload("plugin", PLUGIN_V2_PATH).ok());
CHECK(!realMangledName.empty());
std::string demangled = hot_reload::demangle(realMangledName);
CHECK(demangled.find("CppWidget") != std::string::npos);
// Something that is definitely not a mangled C++ name comes back unchanged, not empty
// and not garbage.
CHECK(hot_reload::demangle("plugin_tick") == "plugin_tick");
}
/// The thread-safety claim in Context.hpp's own doc comment, actually exercised: several
/// threads calling findSymbol() on a shared Context concurrently with other threads
/// load()/reload()-ing (some on the SAME module name as each other -- a genuine race for
/// which build ends up active, which this makes no assertion about -- and some on
/// per-thread-unique names, which this does check resolve correctly). Run this binary under
/// ThreadSanitizer for the result that actually matters: zero reported data races, not just
/// "it didn't crash this time".
void runConcurrencyStress()
{
constexpr int kReaderThreads = 4;
constexpr int kReaderIterations = 500;
constexpr int kSharedReloaderThreads = 2;
constexpr int kSharedReloadIterations = 25;
constexpr int kUniqueLoaderThreads = 4;
hot_reload::Context ctx;
CHECK(ctx.load("plugin", PLUGIN_V1_PATH).ok());
std::atomic<bool> stop{false};
std::atomic<int> readSuccesses{0};
std::vector<std::thread> threads;
// Readers: hammer findSymbol() on the one module every writer thread below is also
// touching, for the whole stress run.
for (int i = 0; i < kReaderThreads; ++i)
{
threads.emplace_back(
[&ctx, &stop, &readSuccesses]()
{
int iterations = 0;
while (!stop.load(std::memory_order_relaxed) && iterations < kReaderIterations)
{
const void* symbol = ctx.findSymbol("plugin", "plugin_tick");
if (symbol != nullptr)
{
readSuccesses.fetch_add(1, std::memory_order_relaxed);
}
++iterations;
}
});
}
// Writers: alternate reload()-ing the SAME module name between V1 and V2, concurrently
// with each other and with the readers above. No assertion on which build "wins" any
// one moment -- only that nothing crashes and the context stays internally consistent.
for (int i = 0; i < kSharedReloaderThreads; ++i)
{
threads.emplace_back(
[&ctx]()
{
for (int iteration = 0; iteration < kSharedReloadIterations; ++iteration)
{
const char* path = (iteration % 2 == 0) ? PLUGIN_V2_PATH : PLUGIN_V1_PATH;
hot_reload::Result result = ctx.reload("plugin", path);
checkImpl(result.ok(), "result.ok() during concurrent reload", __FILE__, __LINE__);
}
});
}
// Loaders: each thread load()s its OWN uniquely-named module (same underlying file,
// different logical names) -- concurrent INSERTS into the same map, from different
// threads, each then confirmed to resolve correctly afterward.
std::vector<std::thread> loaderThreads;
std::atomic<int> loadFailures{0};
for (int i = 0; i < kUniqueLoaderThreads; ++i)
{
threads.emplace_back(
[&ctx, &loadFailures, i]()
{
std::string name = "unique-module-" + std::to_string(i);
hot_reload::Result result = ctx.load(name, PLUGIN_V1_PATH);
if (!result.ok())
{
loadFailures.fetch_add(1, std::memory_order_relaxed);
return;
}
const void* symbol = ctx.findSymbol(name, "plugin_tick");
if (symbol == nullptr)
{
loadFailures.fetch_add(1, std::memory_order_relaxed);
}
});
}
for (std::thread& thread : threads)
{
thread.join();
}
stop.store(true, std::memory_order_relaxed);
CHECK(readSuccesses.load() > 0); // readers actually found the symbol at least sometimes
CHECK(loadFailures.load() == 0); // every per-thread-unique load()+findSymbol() succeeded
// The context is still fully usable after all of that -- one more reload, one more call,
// for a final sanity check that nothing was left in a corrupted state.
CHECK(ctx.reload("plugin", PLUGIN_V2_PATH).ok());
auto tick = reinterpret_cast<PluginFunction>(ctx.findSymbol("plugin", "plugin_tick"));
CHECK(tick != nullptr);
CHECK(tick(10) == 30);
}
}
int main()
{
runBasicUsage();
runResultAsValueType();
runMoveSemantics();
runDemangle();
runConcurrencyStress();
if (g_failures == 0)
{
std::printf("context_tests: all checks passed\n");
}
else
{
std::fprintf(stderr, "context_tests: %d check(s) failed\n", g_failures);
}
return g_failures == 0 ? 0 : 1;
}
#else
int main()
{
std::printf("context_tests: skipped (HOTRELOAD_TESTS_SKIP set -- needs BUILD_EXAMPLES=ON too)\n");
return 0;
}
#endif