Bake Typed Jerboa Rust staticlib into jerboa-bin
ober
34000b220f38cda641ff7bc078d258648f7c0cf3
--- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ TYPED_RUST_SOURCES ?= $(TYPED_SOURCES) TYPED_RUST_DIR ?= build/typed/rust TYPED_WRAPPER_DIR ?= build/typed/jerboa -.PHONY: help chez chez-cross build binary binary-cross native-cross pure-audit typecheck typed-rust typed-wrappers typed-build typed-wrapper-smoke typed-split-tree-smoke typed-test typed-clean test test-reader test-core test-runtime test-stdlib test-ffi test-modules test-expanded test-contract test-ergo test-typed-core test-typed-parser test-typed-checker test-typed-rust test-typed-wrappers test-pure-audit test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-phase8 test-functional test-repl test-security test-security-profile test-native test-gaps native clean-native audit-native clean security security-production security-profile fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-fuzz fuzz-dns-fuzz fuzz-pregexp-fuzz fuzz-csv-fuzz fuzz-base64-fuzz fuzz-hex-fuzz fuzz-uri-fuzz fuzz-format-fuzz fuzz-router-fuzz fuzz-sandbox-fuzz test-rawstring test-regex test-rx test-peg test-regex-all check-docs check-docs-strict docker-build docker-push +.PHONY: help chez chez-cross build binary binary-typed binary-typed-smoke binary-cross native-cross pure-audit typecheck typed-rust typed-wrappers typed-build typed-wrapper-smoke typed-split-tree-smoke typed-test typed-clean test test-reader test-core test-runtime test-stdlib test-ffi test-modules test-expanded test-contract test-ergo test-typed-core test-typed-parser test-typed-checker test-typed-rust test-typed-wrappers test-pure-audit test-features test-wrappers test-phase4a test-phase4b test-phase4c test-phase4d test-phase4e test-phase4f test-phase5 test-phase5e test-phase6 test-phase7 test-phase8 test-functional test-repl test-security test-security-profile test-native test-gaps native clean-native audit-native clean security security-production security-profile fuzz fuzz-smoke fuzz-deep fuzz-reader-fuzz fuzz-json-fuzz fuzz-http2-fuzz fuzz-dns-fuzz fuzz-pregexp-fuzz fuzz-csv-fuzz fuzz-base64-fuzz fuzz-hex-fuzz fuzz-uri-fuzz fuzz-format-fuzz fuzz-router-fuzz fuzz-sandbox-fuzz test-rawstring test-regex test-rx test-peg test-regex-all check-docs check-docs-strict docker-build docker-push help: @echo "Usage: make <target>" @@ -344,6 +344,34 @@ typed-split-tree-smoke: typed-test: test-typed-core test-typed-parser test-typed-checker test-typed-rust test-typed-wrappers test-typed-fuzz typecheck +# Build a jerboa-bin that bakes in a Typed Jerboa Rust .a archive. The +# generated wrapper resolves its `jt_*` symbols via dlsym(RTLD_DEFAULT) — no +# JERBOA_TYPED_RUST_LIB env var needed at runtime. +# +# Override: +# TYPED_RUST_SOURCES — typed .ss source files (default: rust-basic fixture) +# TYPED_WRAPPER_DIR — where the generated `.ss` wrapper is written +# BINARY_OUTPUT — path of the resulting jerboa-bin binary +binary-typed: chez build + @$(MAKE) --no-print-directory typed-build \ + TYPED_RUST_SOURCES="$(TYPED_RUST_SOURCES)" \ + TYPED_WRAPPER_DIR="$(TYPED_WRAPPER_DIR)" + @archive="$(TYPED_RUST_DIR)/target/debug/libjerboa_typed_generated.a"; \ + test -f "$$archive" || { echo "ERROR: typed Rust .a not found at $$archive" >&2; exit 1; }; \ + JERBOA_TYPED_RUST_ARCHIVE="$$archive" \ + SCHEME=$(SCHEME) JERBOA_CHEZ_PREFIX=$(CHEZ_PREFIX) \ + support/build-binary.sh $(BINARY_ENTRY) $(BINARY_OUTPUT) + +# Smoke test: prove the static-link path resolves `jt_*` symbols without +# JERBOA_TYPED_RUST_LIB by running a tiny caller through the freshly built +# binary. +binary-typed-smoke: + @$(MAKE) --no-print-directory binary-typed \ + TYPED_RUST_SOURCES=tests/fixtures/typed/rust-basic.ss \ + TYPED_WRAPPER_DIR=build/typed/jerboa-binary-smoke \ + BINARY_OUTPUT=build/typed/jerboa-bin-typed + @unset JERBOA_TYPED_RUST_LIB; ./build/typed/jerboa-bin-typed tests/test-typed-binary-smoke.ss + typed-clean: @rm -rf build/typed --- a/docs/jerboa-to-rust.md +++ b/docs/jerboa-to-rust.md @@ -124,9 +124,17 @@ Done in subsequent phases: (`use crate::other_mod::*;`). No wildcard `use crate::*;` and no crate-root `pub use module::*;` re-exports. +- Static binary integration via `make binary-typed`. The build sets + `JERBOA_TYPED_RUST_ARCHIVE` so `support/build-binary.sh` `-force_load`s + (Darwin) / `--whole-archive`s (Linux/BSD) the `.a` into the final + executable, and emits an autogenerated `typed_symbols.h` that registers + each `jt_*` export through Chez's `Sregister_symbol`. The wrapper then + resolves its foreign-procedure names from the binary's own symbol table — + no `JERBOA_TYPED_RUST_LIB` is needed at runtime. + Still open: -- Direct `rustc` / static binary integration polish and eventual LLVM parity +- Direct `rustc` invocation (skipping `cargo build`) and eventual LLVM parity tests. Today the build goes through `cargo build` only. ## Output Layout --- a/docs/typed-jerboa.md +++ b/docs/typed-jerboa.md @@ -1033,7 +1033,11 @@ Minimum excluded features: - Generate a Cargo crate. Initial `make typed-rust` / `make typed-build` targets landed for a primitive typed fixture. - Compile generated Rust as a static or dynamic library. Initial disposable - `rlib`/`staticlib`/`cdylib` builds landed. + `rlib`/`staticlib`/`cdylib` builds landed. The `make binary-typed` target + links the `staticlib` into the standalone Jerboa binary so generated + wrappers resolve their `jt_*` foreign-procedure names directly from the + binary's own symbol table — no `JERBOA_TYPED_RUST_LIB` env var needed at + runtime. - Generate conversion functions. Initial scalar argument checks, `Char` code-point conversions, `String` argument/return UTF-8 bytevector conversions, and `Bytes` argument/return bytevector conversions landed at the --- a/support/build-binary.sh +++ b/support/build-binary.sh @@ -155,7 +155,7 @@ echo "" # ── Step 1: WPO-compile entry script -> program.so ─────────────────────────── WPO_SO="${OUTPUT}.wp.so" OBJ_DIR=$(mktemp -d "/tmp/jerboa-bin-obj.XXXXXX") -trap 'rm -rf "$OBJ_DIR" "$WPO_SO" petite_boot.h scheme_boot.h program_boot.h "${OUTPUT}-main.c"' EXIT +trap 'rm -rf "$OBJ_DIR" "$WPO_SO" petite_boot.h scheme_boot.h program_boot.h typed_symbols.h "${OUTPUT}-main.c"' EXIT echo "==> [1/4] WPO compile" JERBOA_XPATCH="$JERBOA_XPATCH" "$SCHEME" --libdirs "$BINARY_LIBDIRS" \ @@ -180,6 +180,36 @@ embed() { embed "$CSV_DIR/petite.boot" petite_boot embed "$CSV_DIR/scheme.boot" scheme_boot embed "$WPO_SO" program_boot + +# When baking a Typed Jerboa Rust staticlib, extract its jt_* exports and +# generate a typed_symbols.h that pre-registers each one through Chez's +# Sregister_symbol. Without this, Chez's foreign-procedure lookup only +# scans objects added via (load-shared-object PATH) — which the generated +# wrapper skips when JERBOA_TYPED_RUST_LIB is unset. +TYPED_SYMBOLS_CFLAGS="" +if [ -n "${JERBOA_TYPED_RUST_ARCHIVE:-}" ]; then + JT_SYMS=$(nm -g "$JERBOA_TYPED_RUST_ARCHIVE" 2>/dev/null \ + | awk '/ T _?jt_[A-Za-z0-9_]+$/ { + sym = $NF; sub(/^_/, "", sym); + if (!seen[sym]++) print sym; + }' \ + | sort) + if [ -n "$JT_SYMS" ]; then + { + echo '/* Generated by support/build-binary.sh — Sregister_symbol entries' + echo ' * for the Typed Jerboa Rust staticlib baked into this binary. */' + for s in $JT_SYMS; do + echo "extern int $s();" + done + echo 'static void jerboa_register_typed_symbols(void) {' + for s in $JT_SYMS; do + echo " Sregister_symbol(\"$s\", (void *)$s);" + done + echo '}' + } > typed_symbols.h + TYPED_SYMBOLS_CFLAGS="-DJERBOA_HAS_TYPED_SYMBOLS=1" + fi +fi echo "" # ── Step 3: Generate main.c ────────────────────────────────────────────────── @@ -198,6 +228,9 @@ cat > "${OUTPUT}-main.c" <<'CMAIN' #include "petite_boot.h" #include "scheme_boot.h" #include "program_boot.h" +#ifdef JERBOA_HAS_TYPED_SYMBOLS +#include "typed_symbols.h" +#endif static const char *write_program_tmpfile(void) { static char path[] = "/tmp/jerboa-prog-XXXXXX"; @@ -222,6 +255,9 @@ int main(int argc, const char *argv[]) { Sregister_boot_file_bytes("scheme", (void *)scheme_boot_data, scheme_boot_size); Sbuild_heap(NULL, NULL); +#ifdef JERBOA_HAS_TYPED_SYMBOLS + jerboa_register_typed_symbols(); +#endif const char *prog_path = write_program_tmpfile(); int status = Sscheme_program(prog_path, argc, argv); @@ -243,12 +279,36 @@ for a in liblz4.a libz.a; do fi done +# Optional: bake a Typed Jerboa Rust staticlib into the binary so generated +# wrappers can resolve their `jt_*` symbols via dlsym(RTLD_DEFAULT) without +# needing JERBOA_TYPED_RUST_LIB set at runtime. +TYPED_RUST_LINK="" +if [ -n "${JERBOA_TYPED_RUST_ARCHIVE:-}" ]; then + [ -f "$JERBOA_TYPED_RUST_ARCHIVE" ] || { + echo "ERROR: JERBOA_TYPED_RUST_ARCHIVE not found: $JERBOA_TYPED_RUST_ARCHIVE" >&2 + exit 1 + } + # The jt_* exports are made visible to Chez at runtime by way of + # Sregister_symbol calls emitted into main.c (see typed_symbols.h + # generation below); the linker just needs to actually drag the + # archive's object files into the executable. + case "$TARGET_OS" in + Darwin) + TYPED_RUST_LINK="-Wl,-force_load,$JERBOA_TYPED_RUST_ARCHIVE" ;; + Linux|FreeBSD|OpenBSD|NetBSD) + TYPED_RUST_LINK="-Wl,--whole-archive $JERBOA_TYPED_RUST_ARCHIVE -Wl,--no-whole-archive" ;; + *) + TYPED_RUST_LINK="$JERBOA_TYPED_RUST_ARCHIVE" ;; + esac +fi + # shellcheck disable=SC2086 -$CC -I"$CSV_DIR" -O2 $STATIC_CFLAGS \ +$CC -I. -I"$CSV_DIR" -O2 $STATIC_CFLAGS $TYPED_SYMBOLS_CFLAGS \ -o "$OUTPUT" \ "${OUTPUT}-main.c" \ "$CSV_DIR/libkernel.a" \ $EXTRA_ARCHIVES \ + $TYPED_RUST_LINK \ $OS_LIBS echo "" new file mode 100644 --- /dev/null +++ b/tests/test-typed-binary-smoke.ss @@ -0,0 +1,59 @@ +#!chezscheme +;;; Smoke test for the statically-baked Typed Jerboa Rust path. +;;; +;;; Invoked through `./build/typed/jerboa-bin-typed tests/test-typed-binary-smoke.ss` +;;; by `make binary-typed-smoke`. The binary embeds the Typed Jerboa Rust .a, so +;;; the generated wrapper must resolve its `jt_*` foreign-procedure bindings +;;; through the binary's own symbol table — no JERBOA_TYPED_RUST_LIB needed. + +(import (chezscheme)) + +;; The wrapper script imports (jerboa prelude), so the static binary needs to +;; know where source libraries live. The binary itself has no baked-in libdirs +;; — that's a deliberate design: users either set CHEZSCHEMELIBDIRS or have +;; the entry script call (library-directories ...). For this smoke test we +;; just point at the in-repo lib/ tree. +(library-directories '("lib")) + + +(define pass 0) +(define fail 0) + +(define (check name ok?) + (cond + [ok? + (set! pass (+ pass 1)) + (printf " ok ~a~%" name)] + [else + (set! fail (+ fail 1)) + (printf "FAIL ~a~%" name)])) + +(define wrapper-path + "build/typed/jerboa-binary-smoke/sample_typed_rust_basic.ss") + +(unless (file-exists? wrapper-path) + (printf "ERROR: wrapper not found at ~a~%" wrapper-path) + (exit 2)) + +(check "JERBOA_TYPED_RUST_LIB is unset for this run" + (let ([env (getenv "JERBOA_TYPED_RUST_LIB")]) + (or (not env) (string=? env "")))) + +(load wrapper-path) + +;; The wrapper exposes a `zero` (-> Nat) and an `add-one` (Nat -> Nat). +;; Calling either proves dlsym(RTLD_DEFAULT) resolved the static `jt_*` +;; symbols from the binary's own symbol table. +(check "zero resolves and returns 0" + (= (zero) 0)) + +(check "add-one resolves and adds" + (= (add-one 41) 42)) + +(check "greeting resolves and returns a non-empty string" + (let ([g (greeting)]) + (and (string? g) (> (string-length g) 0)))) + +(printf "~%Typed binary static-link smoke: ~a passed, ~a failed~%" pass fail) +(when (> fail 0) + (exit 1))