#!/usr/bin/env bash # Spike: what it costs to link the OCaml compiler into a native Flan dev build. # # 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. # Instead this drives ocamlfind and clang by hand, against the flan.cmxa that # dune already produces. Run it from anywhere: bash spike/embed/run.sh set -u here=$(cd "$(dirname "$0")" && pwd) root=$(cd "$here/../.." && pwd) cd "$here" || exit 1 OCAMLLIB=$(ocamlopt -where) CAMLINC="-I$OCAMLLIB" # OCaml 5.2's marshaller is compressed, so -output-complete-obj pulls in zstd. SYSLIBS="-lm -lpthread -ldl -lzstd" step() { printf '\n=== %s ===\n' "$1"; } # ---------------------------------------------------------------- 1. smallest step "1. smallest link: C main() -> caml_startup -> OCaml callback" ocamlfind ocamlopt -package unix -linkpkg -output-complete-obj \ -o embed1.o hello_ml.ml || exit 1 clang $CAMLINC harness1.c embed1.o -o spike1 $SYSLIBS || exit 1 ./spike1 || echo "spike1 FAILED" ls -l spike1 | awk '{print "spike1 size: " $5 " bytes"}' # ------------------------------------------------------- 2. the real compiler step "2. link the whole flan compiler (flan.cmxa) into a C binary" CMXA="$root/_build/default/lib/flan.cmxa" if [ ! -f "$CMXA" ]; then echo "no $CMXA -- run 'dune build --root .' first"; exit 1 fi ocamlfind ocamlopt -package unix -linkpkg -output-complete-obj \ -I "$root/_build/default/lib/.flan.objs/byte" \ -I "$root/_build/default/lib/.flan.objs/native" \ -o embed2.o "$CMXA" whole_ml.ml || exit 1 clang $CAMLINC harness2.c embed2.o -o spike2 $SYSLIBS || exit 1 (cd "$root" && "$here/spike2" test/programs/edn.flan) || echo "spike2 FAILED" ls -l spike2 | awk '{print "spike2 size: " $5 " bytes"}' ls -l "$root/_build/default/bin/main.exe" | awk '{print "main.exe size: " $5 " bytes"}' clang $CAMLINC baseline.c -o baseline ls -l baseline | awk '{print "bare C baseline: " $5 " bytes"}' # ------------------------------------------------------------- 3. with stubs step "3. the same, with the project's own C stubs compiled in" ocamlfind ocamlopt -package unix -linkpkg -output-complete-obj \ -I "$root/_build/default/lib/.flan.objs/byte" \ -I "$root/_build/default/lib/.flan.objs/native" \ -o embed3.o "$CMXA" stubs_ml.ml dynload_stubs.c || exit 1 clang $CAMLINC harness3.c embed3.o -o spike3 $SYSLIBS || exit 1 ./spike3 || echo "spike3 FAILED" # ------------------------------------------ 4. threads: game owns main thread step "4. threads: C main runs the 'game loop', OCaml starts on another thread" ocamlfind ocamlopt -thread -package unix,threads.posix -linkpkg -output-complete-obj \ -I "$root/_build/default/lib/.flan.objs/byte" \ -I "$root/_build/default/lib/.flan.objs/native" \ -o embed4.o "$CMXA" thread_ml.ml || exit 1 clang $CAMLINC harness4.c embed4.o -o spike4 $SYSLIBS || exit 1 (cd "$root" && "$here/spike4" test/programs/edn.flan) || echo "spike4 FAILED" # ------------------------------------------------------------ 5. signals step "5. signals: who owns SIGSEGV across caml_startup" clang $CAMLINC harness5.c embed2.o -o spike5 $SYSLIBS || exit 1 ./spike5 || echo "spike5 exited nonzero" # ------------------------------------------------------- 6. GC vs raw memory step "6. GC: does a compaction move or touch a C-owned arena" ocamlfind ocamlopt -package unix -linkpkg -output-complete-obj \ -o embed6.o gc_ml.ml || exit 1 clang $CAMLINC harness6.c embed6.o -o spike6 $SYSLIBS || exit 1 ./spike6 || echo "spike6 FAILED" printf '\nspike: done\n'