flan/spike/embed/harness4.c
Joseph Ferano d272a5b1e5 Six probes for whether the OCaml compiler can live in the game's process
Item 12 asks five questions and says to answer them with a spike rather than a
rewrite. spike/embed/ is that spike: one script, six binaries, each one built to
fail loudly at the thing it is asking about. It is deliberately not a dune
target -- the root dune only excludes old-ocaml/, so a dune file here would land
in @default and make the spike part of the build. It drives ocamlfind and clang
by hand against the flan.cmxa dune already produces.

The probes, in the order they would kill the idea: the smallest possible link, a
C main() reaching one OCaml function; the whole compiler linked in and doing
real work; the same again with lib/dynload_stubs.c from the unmerged dlopen
branch, because that is the only C the compiler itself is built from; the game
keeping the main thread while caml_startup happens on a pthread beside it; the
SIGSEGV disposition read on both sides of caml_startup; and an 8 MiB arena
checked byte for byte across a compaction.

No result is written down yet. This is the apparatus.
2026-09-12 20:27:45 +07:00

107 lines
3.6 KiB
C

/* Step 4: the macOS shape, and the discriminating test of the whole spike.
*
* main() is the game: it takes the thread the window needs and runs a loop it
* never leaves until the compiler says stop. The OCaml runtime is started on a
* pthread that C spawned -- exactly where vendor/agent/flan_agent.c already
* puts its listener.
*
* Two separate claims get tested:
* a. caml_startup works on a non-main, C-created thread at all.
* b. a *different* C thread, one the runtime never created, can call into
* OCaml after caml_c_thread_register().
* (b) is the one that matters for the agent: its listener thread is spawned by
* flan_agent_start and would have to be able to reach the compiler.
*/
#include <caml/callback.h>
#include <caml/alloc.h>
#include <caml/mlvalues.h>
#include <caml/threads.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
static char **g_argv;
static const char *g_src = "test/programs/edn.flan";
static atomic_int compiler_up = 0;
static atomic_int quit = 0;
static pthread_t main_tid;
static void nap(long ms) {
struct timespec t = { ms / 1000, (ms % 1000) * 1000000L };
nanosleep(&t, NULL);
}
static const value *need(const char *n) {
const value *f = caml_named_value(n);
if (!f) fprintf(stderr, "spike: %s not registered\n", n);
return f;
}
/* The compiler thread: starts the OCaml runtime off the main thread. */
static void *compiler_thread(void *unused) {
const value *f;
(void)unused;
printf(" [compiler thread] is main thread? %s\n",
pthread_equal(pthread_self(), main_tid) ? "YES (wrong)" : "no (correct)");
caml_startup(g_argv);
printf(" [compiler thread] caml_startup returned off the main thread\n");
f = need("spike_domains");
if (f) printf(" [compiler thread] %s\n", String_val(caml_callback(*f, Val_unit)));
f = need("spike_thread_compile");
if (f) printf(" [compiler thread] %s\n",
String_val(caml_callback(*f, caml_copy_string(g_src))));
/* Hand the runtime over so another C thread can borrow it, and prove the
main loop kept running throughout. */
atomic_store(&compiler_up, 1);
caml_release_runtime_system();
nap(300);
caml_acquire_runtime_system();
atomic_store(&quit, 1);
return NULL;
}
/* A second C thread, like the agent's listener: never created by OCaml. */
static void *listener_thread(void *unused) {
const value *f;
(void)unused;
while (!atomic_load(&compiler_up)) nap(5);
if (caml_c_thread_register() == 0) {
printf(" [listener thread] caml_c_thread_register FAILED\n");
return NULL;
}
caml_acquire_runtime_system();
f = need("spike_thread_compile");
if (f) printf(" [listener thread] %s\n",
String_val(caml_callback(*f, caml_copy_string(g_src))));
caml_release_runtime_system();
caml_c_thread_unregister();
printf(" [listener thread] registered, called OCaml, unregistered\n");
return NULL;
}
int main(int argc, char **argv) {
pthread_t comp, lst;
long frames = 0;
g_argv = argv;
if (argc > 1) g_src = argv[1];
main_tid = pthread_self();
if (pthread_create(&comp, NULL, compiler_thread, NULL) != 0) return 1;
if (pthread_create(&lst, NULL, listener_thread, NULL) != 0) return 1;
/* The game loop. This thread never calls into OCaml and never blocks on it --
it is the window's thread, and on macOS it has to be this one. */
while (!atomic_load(&quit)) { frames++; nap(1); }
pthread_join(comp, NULL);
pthread_join(lst, NULL);
printf(" [main thread] ran %ld frames without ever entering OCaml\n", frames);
printf("spike: the game kept the main thread\n");
return 0;
}