Add jextract module: SQLite bindings generated with jextract, FFM arenas, C strings, out-parameters, errors, upcalls, and a JMH comparison with JNI

jextract-25 early-access build (25-jextract+2-4) on JDK 25.0.5+8 against SQLite 3.45.1: 25 functions and 13 constants
generated into 4 committed files (the whole header would be 83 files), a small Db wrapper, the four arena kinds
used wrongly on purpose, ADDRESS vs C_POINTER zero-length vs unbounded segments and the SIGSEGV from getString
on a NULL pointer, sqlite3_open out-parameter, return codes to exceptions, a sqlite3_exec upcall and the VM exit
when it throws, the JDK 25 native-access warning and --illegal-native-access=deny, library loading failures,
and a JMH benchmark of FFM vs hand-written JNI vs plain Java (indicative only: shared 2-CPU machine).

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_017ic4b6Kg9RWSTJzyz6KP9X
This commit is contained in:
2026-10-30 16:36:09 +00:00
co-authored by Claude Sonnet 5.5
parent 41101bea40
commit ecceaa8b32
37 changed files with 2867 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Compile the JNI twin: javac -h generates the header, gcc builds libsqlitejni.so. Output goes to $BUILD (default ./build).
set -euo pipefail
unset JAVA_TOOL_OPTIONS
HERE="$(cd "$(dirname "$0")/.." && pwd)"
JAVA_HOME="${JAVA_HOME:?set JAVA_HOME to a JDK 25}"
BUILD="${BUILD:-$HERE/build}"; mkdir -p "$BUILD/jni-classes" "$BUILD/jni-h" "$BUILD/lib"
"$JAVA_HOME/bin/javac" -h "$BUILD/jni-h" -d "$BUILD/jni-classes" "$HERE/jni/SqliteJni.java"
gcc -O2 -fPIC -shared -Wall -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -I"$BUILD/jni-h" \
"$HERE/jni/sqlite_jni.c" -o "$BUILD/lib/libsqlitejni.so" -lsqlite3
echo "built $BUILD/lib/libsqlitejni.so with $(gcc --version | head -1)"
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Downloads the jextract early-access build for JDK 25 (linux-x64) from jdk.java.net and checks its SHA-256. Unpacks into ./tools.
set -euo pipefail
cd "$(dirname "$0")/.."; mkdir -p tools; cd tools
F="openjdk-25-jextract+2-4_linux-x64_bin.tar.gz"
U="https://download.java.net/java/early_access/jextract/25/2/$F"
[ -s "$F" ] || curl -sSLO "$U"
want="$(curl -sSL "$U.sha256" | cut -d' ' -f1)"; got="$(sha256sum "$F" | cut -d' ' -f1)"
[ "$want" = "$got" ] && echo "sha256 ok $got" || { echo "SHA256 MISMATCH"; exit 1; }
tar xzf "$F"; echo "jextract at $(pwd)/jextract-25/bin/jextract"
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
# Downloads JMH 1.37 and its two runtime dependencies from Maven Central into ./bench/lib, checking each against the published .sha1
set -euo pipefail
cd "$(dirname "$0")/../bench"; mkdir -p lib
M=https://repo1.maven.org/maven2
for a in org/openjdk/jmh/jmh-core/1.37/jmh-core-1.37 \
org/openjdk/jmh/jmh-generator-annprocess/1.37/jmh-generator-annprocess-1.37 \
net/sf/jopt-simple/jopt-simple/5.0.4/jopt-simple-5.0.4 \
org/apache/commons/commons-math3/3.6.1/commons-math3-3.6.1; do
f="lib/$(basename "$a").jar"
for try in 1 2 3 4 5; do [ -s "$f" ] && break; curl -sfL -o "$f" "$M/$a.jar" || { rm -f "$f"; sleep 5; }; done
want=""; for try in 1 2 3 4 5; do want="$(curl -sfL "$M/$a.jar.sha1" | cut -d' ' -f1)" && [ -n "$want" ] && break; sleep 5; done
got="$(sha1sum "$f" | cut -d' ' -f1)"
[ "$want" = "$got" ] && echo "ok $f $got" || { echo "SHA1 MISMATCH $f"; exit 1; }
done
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Regenerate the trimmed SQLite bindings with jextract. Run from anywhere.
# Needs: JEXTRACT (path to the jextract binary), SQLITE_H (default /usr/include/sqlite3.h).
set -euo pipefail
HERE="$(cd "$(dirname "$0")/.." && pwd)"
JEXTRACT="${JEXTRACT:-jextract}"
SQLITE_H="${SQLITE_H:-/usr/include/sqlite3.h}"
OUT="$HERE/generated"
rm -rf "$OUT"; mkdir -p "$OUT"
args=()
for f in sqlite3_libversion sqlite3_libversion_number sqlite3_sourceid \
sqlite3_open sqlite3_open_v2 sqlite3_close sqlite3_exec \
sqlite3_errmsg sqlite3_errstr sqlite3_extended_errcode sqlite3_free \
sqlite3_prepare_v2 sqlite3_step sqlite3_reset sqlite3_finalize \
sqlite3_bind_int sqlite3_bind_text sqlite3_column_count sqlite3_column_name \
sqlite3_column_int sqlite3_column_text sqlite3_changes sqlite3_last_insert_rowid sqlite3_strlike sqlite3_column_type; do
args+=(--include-function "$f")
done
for c in SQLITE_OK SQLITE_ERROR SQLITE_BUSY SQLITE_CANTOPEN SQLITE_ROW SQLITE_DONE \
SQLITE_OPEN_READONLY SQLITE_OPEN_READWRITE SQLITE_OPEN_CREATE SQLITE_TRANSIENT SQLITE_STATIC SQLITE_CONSTRAINT SQLITE_NULL; do
args+=(--include-constant "$c")
done
"$JEXTRACT" --output "$OUT" --target-package demo.sqlite --header-class-name sqlite3_h \
-l sqlite3 "${args[@]}" "$SQLITE_H"
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Regenerates every file in output/ except 10-jmh.txt (that one has its own script, about 4 minutes).
# JAVA_HOME=/path/to/jdk-25 JEXTRACT=/path/to/jextract ./scripts/run-all.sh
set -uo pipefail
unset JAVA_TOOL_OPTIONS # the JVM prints a "Picked up JAVA_TOOL_OPTIONS" banner in some sandboxes; it is noise in a transcript
HERE="$(cd "$(dirname "$0")/.." && pwd)"; cd "$HERE"
JAVA_HOME="${JAVA_HOME:?set JAVA_HOME to a JDK 25}"; export PATH="$JAVA_HOME/bin:$PATH"
JEXTRACT="${JEXTRACT:-$HERE/tools/jextract-25/bin/jextract}"
SQLITE_H="${SQLITE_H:-/usr/include/sqlite3.h}"
BUILD="${BUILD:-$HERE/build}"; OUT="$HERE/output"; mkdir -p "$OUT" "$BUILD/classes"
NA="--enable-native-access=ALL-UNNAMED"
run() { echo "\$ java $NA -cp build/classes demo.$1${2:+ $2}"; java $NA -cp "$BUILD/classes" demo."$1" ${2:-} 2>&1; }
# 01 - what was installed
{
echo "\$ jextract --version"; "$JEXTRACT" --version 2>&1
echo "\$ java -version"; java -version 2>&1
echo "\$ gcc --version | head -1"; gcc --version | head -1
echo "\$ dpkg-query -W libsqlite3-dev libsqlite3-0"; dpkg-query -W libsqlite3-dev libsqlite3-0
echo "\$ grep -E '#define SQLITE_(VERSION|VERSION_NUMBER) ' $SQLITE_H"; grep -E '#define SQLITE_(VERSION|VERSION_NUMBER) ' "$SQLITE_H"
} > "$OUT/01-tools.txt"
# 02 - generate bindings: the trimmed set that is committed, and the whole header for size comparison
{
echo "\$ scripts/gen.sh"
JEXTRACT="$JEXTRACT" SQLITE_H="$SQLITE_H" scripts/gen.sh 2>&1
echo "\$ find generated -name '*.java' | wc -l"; find generated -name '*.java' | wc -l
echo "\$ wc -l \$(find generated -name '*.java') | sort -n"; wc -l $(find generated -name '*.java') | sort -n
F="$(mktemp -d)"
echo "\$ jextract --output <tmp> -t demo.sqlite.full -l sqlite3 $SQLITE_H # no filters: the whole header"
"$JEXTRACT" --output "$F" -t demo.sqlite.full -l sqlite3 "$SQLITE_H" > "$F.log" 2>&1
echo "warnings: $(grep -c warning "$F.log")"; grep 'Skipping sqlite3 ' "$F.log"; grep 'Skipping sqlite3_stmt ' "$F.log"
echo "files: $(find "$F" -name '*.java' | wc -l)"
echo "lines: $(cat $(find "$F" -name '*.java') | wc -l)"
echo "bytes: $(cat $(find "$F" -name '*.java') | wc -c)"
echo "lines in the one big header class: $(wc -l < "$F/demo/sqlite/full/sqlite3_h.java")"
rm -rf "$F" "$F.log"
echo "\$ grep -o 'public static [A-Za-z.]* sqlite3_open[a-z_0-9\$]*(' generated/demo/sqlite/sqlite3_h.java | sort -u"
grep -o 'public static [A-Za-z.]* sqlite3_open[a-z_0-9$]*(' generated/demo/sqlite/sqlite3_h.java | sort -u
} > "$OUT/02-generate.txt"
# compile generated + hand-written sources
rm -rf "$BUILD/classes"; mkdir -p "$BUILD/classes"
javac -d "$BUILD/classes" $(find generated src -name '*.java') || exit 1
{ run Hello; echo; echo "\$ java $NA -Djextract.trace.downcalls=true -cp build/classes demo.Hello"; java $NA -Djextract.trace.downcalls=true -cp "$BUILD/classes" demo.Hello 2>&1; } > "$OUT/03-hello.txt"
{
echo "\$ java -cp build/classes demo.Hello # no flag at all, JDK 25 default"
java -cp "$BUILD/classes" demo.Hello 2>&1
echo; echo "\$ java --illegal-native-access=deny -cp build/classes demo.Hello"
java --illegal-native-access=deny -cp "$BUILD/classes" demo.Hello 2>&1 | head -6
} > "$OUT/04-native-access.txt"
run Tour > "$OUT/05-tour.txt"
run Arenas > "$OUT/06-arenas.txt"
{
run Trap; echo
echo "\$ java $NA -XX:ErrorFile=/tmp/jextract-hs_err.log -cp build/classes demo.NullText # first lines of the crash banner"
java $NA -XX:ErrorFile=/tmp/jextract-hs_err.log -cp "$BUILD/classes" demo.NullText 2>&1 | sed -n '1,9p'
echo "exit code: ${PIPESTATUS[0]}"
} > "$OUT/07-pointer-traps.txt"
run Errors > "$OUT/08-errors.txt"
{ run Callbacks; echo; echo "\$ java $NA -cp build/classes demo.Callbacks explode"; java $NA -cp "$BUILD/classes" demo.Callbacks explode 2>&1; echo "exit code: $?"; } > "$OUT/09-callbacks.txt"
# 11 - how the -l option decides what gets loaded
{
T="$(mktemp -d)"
n=0
for lib in sqlitez :libsqlite3.so.0; do
n=$((n+1))
echo "\$ jextract -t demo.sqlite -l $lib --include-function sqlite3_libversion --header-class-name sqlite3_h $SQLITE_H"
"$JEXTRACT" --output "$T/gen$n" -t demo.sqlite -l "$lib" --include-function sqlite3_libversion --header-class-name sqlite3_h "$SQLITE_H" 2>&1
grep -h 'libraryLookup' "$T/gen$n/demo/sqlite/sqlite3_h.java" | sed 's/^ *//'
mkdir -p "$T/c$n"; javac -d "$T/c$n" "$T/gen$n"/demo/sqlite/*.java src/extra/V.java
echo "\$ java $NA -cp <classes> V"
java $NA -cp "$T/c$n" V 2>&1 | head -5
echo
done
rm -rf "$T"
} > "$OUT/11-library-loading.txt"
echo done
+23
View File
@@ -0,0 +1,23 @@
#!/usr/bin/env bash
# Builds everything and runs the JMH comparison (FFM vs JNI vs plain Java). About 4 minutes. Writes output/10-jmh.txt.
# Needs: JAVA_HOME (JDK 25), gcc, libsqlite3-dev, and `scripts/fetch-jmh.sh` run once. JEXTRACT is only needed by gen.sh, not here.
set -uo pipefail
unset JAVA_TOOL_OPTIONS
HERE="$(cd "$(dirname "$0")/.." && pwd)"; cd "$HERE"
JAVA_HOME="${JAVA_HOME:?set JAVA_HOME to a JDK 25}"
BUILD="${BUILD:-$HERE/build}"; mkdir -p "$BUILD/classes" "$HERE/output"
"$HERE/scripts/build-jni.sh" >/dev/null
CP="$(ls bench/lib/*.jar | tr '\n' ':')"
"$JAVA_HOME/bin/javac" -cp "$CP:$BUILD/jni-classes" -processorpath "$CP" -d "$BUILD/classes" \
$(find generated src bench/src -name '*.java') 2>&1 | grep -v '^Note:'
OPTS=(-f 5 -wi 3 -w 1 -i 5 -r 1)
{
echo "\$ java --enable-native-access=ALL-UNNAMED -Djava.library.path=build/lib org.openjdk.jmh.Main ${OPTS[*]} ($("$JAVA_HOME/bin/java" -version 2>&1 | sed -n 2p | sed 's/.*(build \(.*\))/\1/'), JMH 1.37)"
echo "machine: $(nproc) CPUs, $(grep -m1 'model name' /proc/cpuinfo | sed 's/.*: //')"
echo "load average before the run: $(cut -d" " -f1-3 /proc/loadavg) (1, 5, 15 minutes; this box has 2 CPUs and was shared with other jobs)"
echo "sqlite: $(dpkg-query -W -f='${Package} ${Version}' libsqlite3-dev 2>/dev/null)"
"$JAVA_HOME/bin/java" -cp "$BUILD/classes:$BUILD/jni-classes:$CP" org.openjdk.jmh.Main "${OPTS[@]}" \
-jvmArgs "--enable-native-access=ALL-UNNAMED -Djava.library.path=$BUILD/lib" 2>&1 | sed -n '/^Benchmark /,$p'
echo "load average after the run: $(cut -d' ' -f1-3 /proc/loadavg)"
} > "$HERE/output/10-jmh.txt"
cat "$HERE/output/10-jmh.txt"