/* 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 #include #include #include #include /* 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 ...\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; }