flan/test/reload_host.c

132 lines
5.9 KiB
C

/* reload_host.c — redefinition, exercised in one process.
*
* This is the smallest thing that can prove the dev loop's first two steps:
* a function recompiled into its own object, loaded into a program that is
* already running, *installed* there, and then reached by a call site that
* was compiled before it existed. No socket, no daemon, no frame boundary —
* those are step 3, and the agent that does them lives next to flan_rt.c for
* the same reason this host is C: there is no OCaml in a game process.
*
* It stands in for the entry point of a Flan program, so the .flan fixture it
* links against has no [main] of its own. Three things are being checked, and
* only a single process can check any of them:
*
* - installing a new body makes the host's own [outer] — linked once, never
* rebuilt — call it, which is the whole of C-c C-c;
* - the loaded copy writes the *host's* [counter] and calls the host's
* [helper], because a redefinition module declares both rather than
* defining them;
* - the state carries across every reload untouched;
* - a name the host was never built with — v3's [extra] and [added] — can be
* introduced, and then itself redefined by v4 while v3's already-installed
* [bump] keeps calling it. That last one is what separates a cell found by
* name from a function address cached by name; everything else passes
* either way.
*
* Each version is its own file rather than one path rewritten in place:
* dlopen keys its cache on the path, so re-opening the same name can hand back
* the handle it already has and the test would then "pass" on the code it
* loaded the first time. Nothing is ever dlclosed — a cell holds an address
* inside a module's text, and unloading it would leave call sites pointing at
* unmapped memory.
*/
#include <dlfcn.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
/* The Flan symbols the executable itself defines. Flan names contain
* characters C identifiers cannot, so each one is reached through its asm
* label — the same name Emit spells. */
/* The trailing ptr is the transfer channel spec-conditions.md §6 puts in every
* Flan signature. This host never transfers, so it passes a slot of its own
* that stays null — but the parameter is not optional: getting it wrong reads
* garbage as the channel and fails nowhere near here. */
extern int64_t flan_outer(void *xfer) __asm__("flan.outer");
extern int64_t flan_counter __asm__("flan.counter");
void flan_rt_init(int32_t argc, char **argv);
/* What a redefinition module exposes. It is a named function and not an ELF
* constructor on purpose: the agent has to choose when the store happens —
* on the game thread, between frames — and a constructor would do it during
* dlopen, wherever that call happened to be. */
typedef void (*install_fn)(void);
/* The load is timed here rather than from the test process, because this is
* the part that has to fit inside a frame. */
static double now_ms(void) {
struct timespec t;
clock_gettime(CLOCK_MONOTONIC, &t);
return (double)t.tv_sec * 1e3 + (double)t.tv_nsec / 1e6;
}
/* The same sentence [vendor/agent/flan_agent.c] says, for the same failure.
* Duplicated rather than shared: the agent is vendored to be dropped into a
* user's game and carries no header of its own, and this host is a test
* fixture that links against neither it nor the runtime's dev half.
*
* A dev build defines a marker naming the backend that built it, and a
* redefinition module holds a pointer to the marker it was itself built for.
* The two backends agree on scalars and disagree on every aggregate, so a
* crossed pair would run until the first call into a redefined function that
* takes or returns a struct and then die with SIGSEGV. The marker turns that
* into a relocation the loader cannot resolve. What it says then is
* "undefined symbol: flan.abi.x86", so the marker's name is matched — not the
* loader's phrasing, which is libc's to change — and the reason is stated. */
static const char *abi_mismatch(const char *err) {
if (err == NULL) return NULL;
if (strstr(err, "flan.abi.x86") != NULL)
return "the module and this host were built by different backends: the "
"module came from the x86 dev backend and needs flan.abi.x86, "
"which this host does not define. The two backends pass every "
"struct differently. Rebuild the host with --x86.";
if (strstr(err, "flan.abi.llvm") != NULL)
return "the module and this host were built by different backends: the "
"module came from LLVM and needs flan.abi.llvm, which an --x86 "
"host does not define. The two backends pass every struct "
"differently. Rebuild the host without --x86.";
return NULL;
}
static int install(const char *path) {
double t0 = now_ms();
void *h = dlopen(path, RTLD_NOW | RTLD_LOCAL);
if (h == NULL) {
/* [dlerror] is one-shot, so the pointer is taken once and used twice. */
const char *err = dlerror();
const char *why = abi_mismatch(err);
if (why != NULL) fprintf(stderr, "flan: %s\n", why);
else fprintf(stderr, "dlopen %s: %s\n", path, err);
return 0;
}
install_fn f = (install_fn)(uintptr_t)dlsym(h, "flan_reload_install");
if (f == NULL) {
fprintf(stderr, "dlsym flan_reload_install in %s: %s\n", path, dlerror());
return 0;
}
double t1 = now_ms();
f();
fprintf(stderr, "dlopen+dlsym %.2fms install %.4fms\n", t1 - t0,
now_ms() - t1);
return 1;
}
int main(int argc, char **argv) {
flan_rt_init(argc, argv);
if (argc < 2) {
fprintf(stderr, "usage: %s <module.so>...\n", argv[0]);
return 2;
}
void *xfer = NULL;
printf("host %lld\n", (long long)flan_outer(&xfer));
for (int i = 1; i < argc; i++) {
if (!install(argv[i])) return 1;
printf("after%d %lld\n", i, (long long)flan_outer(&xfer));
}
printf("counter %lld\n", (long long)flan_counter);
return 0;
}