jetc: add --shared mode to compile templates to a dlopen()-able .so

For the server you're about to build: `jetc --shared template.jet out.cpp`
now emits no main() at all - instead the render logic is exported as
`extern "C" void jetc_render_<template>(jet::Global &global,
coreutils::MString &out)`, one call per request, meant to be dlsym()'d
by a long-running host process instead of spawning a jet-2.0 process (or
even a separate compiled executable) per request. extern "C" here only
fixes the symbol's name mangling for a stable dlsym() target - the
parameter types are still real C++ classes, safe to pass across the
dlopen() boundary since the host and the .so share the same
CoreUtils/JetCore headers and toolchain (a same-ABI plugin boundary, not
a foreign C one). coreutils::Exception propagates out of the call
uncaught, so the host decides how to handle a render failure rather than
the module unilaterally deciding (the executable mode's try/catch prints
to stdout and exits, which is CGI-appropriate but wrong for a persistent
server). The exported symbol name is derived from the input filename
(jetc.cpp's baseNameNoExt+sanitizeIdent) so multiple compiled templates'
.so's stay collision-free even under dlopen(..., RTLD_GLOBAL).

jetc/build-shared.sh builds the .so: JetCore's 22 tag classes are still
force-linked directly (Tag::interpretSpan's fallback path can dispatch
to any of them at runtime, regardless of what a given template
statically references), but CoreUtils objects are compiled -fPIC and
archived into a throwaway libCoreUtils_pic.a instead of being linked
directly - libCoreUtils.a itself isn't position-independent, so linking
it into a .so fails outright ("relocation ... can not be used when
making a shared object"), and force-linking every CoreUtils .cpp
directly (rather than archiving, so the linker only pulls in what's
actually referenced) surfaced an unrelated, pre-existing incomplete
class - IMFMessage's vtable is never emitted anywhere, presumably a
partially-migrated class (see project memory on IMFMultipart/IMFText
still being fd-based) - as an unresolved dlopen()-time symbol, even
though nothing here uses it. A normal archive-based link (like the
existing libCoreUtils.a already does for jet-2.0) never pulls IMFMessage
in at all, so building libCoreUtils_pic.a the same way sidesteps it
without touching the unrelated bug itself.

jetc/host_test.cpp is a minimal stand-in for the real server: dlopen()s
a compiled .so, dlsym()s its entry point, and calls it twice with fresh
jet::Global objects (simulating two separate requests against one loaded
module) to prove the .so is genuinely reusable, not single-use. Verified
both demo templates (demo.jet, demo2.jet - the latter covering nested
if/for, a stateful while loop, and the interpretSpan fallback path) build
as .so's and produce the same byte-identical output as the executable
and interpreter paths, across two calls each. Also re-verified the
existing (non-shared) executable build path still matches jet-2.0's
output after the Codegen::generate() signature change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Brad ArantandClaude Sonnet 5 committed 2026-08-23 19:40:50 -07:00
1 parent d52ca61250
commit a94fc5e8fb
5 files changed
+214 -9

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
#!/bin/bash
# Compiles a .jet file into a shared object exporting a stable, dlsym()-able
# render entry point - `jetc_render_<basename>(jet::Global &global,
# coreutils::MString &out)` - for a long-running server to load and call
# directly instead of spawning a jet-2.0 process per request.
#
# Usage: ./build-shared.sh <input.jet> <output.so>
#
# The host server links/dlopen()s the .so and calls the exported symbol
# (see the "entry symbol:" line jetc prints) with its own jet::Global -
# this script does not decide that lifecycle, only builds the module.
# coreutils::Exception propagates out of the call uncaught; the host
# decides how to handle a rendering failure (log it, 500, etc.).
set -e
JC="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CU="$JC/../CoreUtils"
HERE="$JC/jetc"
if [ $# -ne 2 ]; then
echo "usage: $0 <input.jet> <output.so>"
exit 1
fi
INPUT="$1"
OUTPUT="$2"
STEM="$(basename "$OUTPUT" .so)"
if [ ! -x "$HERE/jetc" ]; then
echo "Building jetc..."
g++ -g -std=c++23 -o "$HERE/jetc" "$HERE/jetc.cpp" "$HERE/JetParser.cpp" "$HERE/JetCodegen.cpp"
fi
echo "Compiling $INPUT -> $STEM.compiled.cpp (shared mode)"
"$HERE/jetc" --shared "$INPUT" "$HERE/$STEM.compiled.cpp"
echo "Building JetCore runtime objects as position-independent code..."
list=""
for f in "$JC"/*.cpp; do
base=$(basename "$f")
if [ "$base" = "jet-2.0.cpp" ]; then continue; fi
obj="$HERE/${base%.cpp}.pic.o"
if [ ! -f "$obj" ]; then
g++ -g -fPIC -c -std=c++23 -I"$CU" -I"$JC" "$f" -o "$obj" &
fi
list="$list $obj"
done
# libCoreUtils.a (built by CoreUtils/compile) is not position-independent -
# ld refuses to link non-PIC static-library code into a shared object
# ("relocation ... can not be used when making a shared object"). Rather
# than touching CoreUtils's own build (its .a is fine and used as-is by
# jet-2.0 and everything else), build a PIC copy of its objects here and
# archive them - archived the same way libCoreUtils.a itself is, so the
# linker only pulls in objects something actually references, same as a
# normal -lCoreUtils link. This matters: force-linking every CoreUtils
# .cpp directly (as JetCore's own objects are, below) surfaces an
# unrelated, pre-existing incomplete class (IMFMessage - a known
# partially-migrated class, see project memory) as an unresolved vtable
# at dlopen() time, even though nothing here actually uses it - a normal
# archive link never pulls IMFMessage.o in at all, so it never hits this.
cu_pic_list=""
for f in "$CU"/*.cpp; do
base=$(basename "$f")
obj="$HERE/cu_${base%.cpp}.pic.o"
if [ ! -f "$obj" ]; then
g++ -g -fPIC -c -std=c++23 -I"$CU" "$f" -o "$obj" &
fi
cu_pic_list="$cu_pic_list $obj"
done
wait
ar rcs "$HERE/libCoreUtils_pic.a" $cu_pic_list
g++ -g -fPIC -c -std=c++23 -I"$CU" -I"$JC" "$HERE/$STEM.compiled.cpp" -o "$HERE/$STEM.compiled.pic.o"
echo "Linking $OUTPUT..."
g++ -shared -fPIC -o "$OUTPUT" "$HERE/$STEM.compiled.pic.o" $list -std=c++23 \
-L"$HERE" -lCoreUtils_pic -lmysqlclient -lcrypto
echo "Built $OUTPUT"