132 lines
5.9 KiB
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;
|
|
}
|