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:
Executable
+11
@@ -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)"
|
||||
Executable
+10
@@ -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"
|
||||
Executable
+15
@@ -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
|
||||
Executable
+24
@@ -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"
|
||||
Executable
+81
@@ -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
|
||||
Executable
+23
@@ -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"
|
||||
Reference in New Issue
Block a user