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
+4
View File
@@ -0,0 +1,4 @@
tools/
build/
bench/lib/
hs_err_pid*.log
+54
View File
@@ -0,0 +1,54 @@
# jextract: calling SQLite from Java with the FFM API
Companion code for the ankurm.com post **"Calling C from Java with jextract and the FFM API: A Real Library, No JNI"**. No `docs/` folder: the explanation lives in the article.
| Thing | Version |
|---|---|
| JDK | Temurin 25.0.5+8 (FFM API final since JDK 22, JEP 454) |
| jextract | `openjdk-25-jextract+2-4` early-access build, 2025-11-25 (jdk.java.net/jextract), libclang 13.0.0 |
| SQLite | 3.45.1 (Ubuntu `libsqlite3-dev 3.45.1-1ubuntu2.8`) |
| JMH | 1.37 |
| gcc | 13.3.0 (only for the JNI comparison) |
## Quickstart
```bash
sudo apt-get install libsqlite3-dev gcc # header + libsqlite3.so; gcc only for the JNI benchmark
export JAVA_HOME=/path/to/jdk-25
./scripts/fetch-jextract.sh # downloads and sha256-checks the jextract build into ./tools
export JEXTRACT=$PWD/tools/jextract-25/bin/jextract
./scripts/gen.sh # regenerate generated/ (the committed bindings)
./scripts/run-all.sh # compile everything, regenerate output/01..09 and 11
./scripts/fetch-jmh.sh && ./scripts/run-jmh.sh # the FFM vs JNI vs plain Java benchmark, about 4 minutes -> output/10-jmh.txt
```
Run any demo by hand: `java --enable-native-access=ALL-UNNAMED -cp build/classes demo.Tour`.
## Layout
| Path | What |
|---|---|
| `scripts/gen.sh` | the jextract command, filtered to 25 functions and 13 constants |
| `generated/demo/sqlite/` | what jextract produced from that command (4 files, committed so you can read them) |
| `src/demo/` | hand-written code: `Db` (the wrapper), `Hello`, `Tour`, `Arenas`, `Errors`, `Trap`, `NullText`, `Callbacks` |
| `src/extra/V.java` | one-line program used by `run-all.sh` to test library loading with throwaway bindings |
| `jni/` | the JNI twin used by the benchmark: `SqliteJni.java` and `sqlite_jni.c` |
| `bench/src/bench/SqliteBench.java` | JMH benchmark: plain Java vs FFM vs JNI |
## Captured output (`output/`)
| File | What it is |
|---|---|
| `01-tools.txt` | versions of every tool used |
| `02-generate.txt` | the trimmed generation vs the whole header: file counts, line counts |
| `03-hello.txt` | first call, plus `-Djextract.trace.downcalls=true` |
| `04-native-access.txt` | JDK 25 with no `--enable-native-access` flag: the warning, and `--illegal-native-access=deny` |
| `05-tour.txt` | open, create, bind, insert, query |
| `06-arenas.txt` | confined, shared, auto and global arenas used wrongly |
| `07-pointer-traps.txt` | zero-length pointers, and the `SIGSEGV` from `getString` on a NULL pointer |
| `08-errors.txt` | return codes turned into exceptions |
| `09-callbacks.txt` | `sqlite3_exec` with an upcall, and an exception thrown inside it |
| `10-jmh.txt` | JMH numbers (machine dependent: shape, not result) |
| `11-library-loading.txt` | what `-l` produces, and the failure when the library name is wrong |
Timings and addresses change between runs; everything else is byte-stable. The Linux/x64 jextract build is used throughout; nothing here was run on macOS or Windows.
+98
View File
@@ -0,0 +1,98 @@
package bench;
import static demo.sqlite.sqlite3_h.*;
import demo.Db;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.util.concurrent.TimeUnit;
import org.openjdk.jmh.annotations.*;
/**
* The same SQLite calls three ways: plain Java (floor), jextract/FFM, hand-written JNI.
* versionNumber: no arguments, as cheap as a native call gets.
* strlike: two C strings in, an int out.
* insert1000: 1000 prepared-statement inserts into an in-memory table, rolled back each time.
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
public class SqliteBench {
static final String GLOB = "ab%d_f";
static final String TEXT = "abcdef";
static final String INSERT = "INSERT INTO t(a, b) VALUES (?1, ?2)";
int constant = 3045001; // a field, so the JIT cannot fold the "plain Java" call away
MemorySegment cGlob, cText; // allocated once, for the "pre-allocated" FFM variant
Arena global = Arena.ofShared();
Db ffmDb;
long jniDb;
String[] rowText = new String[1000];
@Setup
public void setup() {
cGlob = global.allocateFrom(GLOB);
cText = global.allocateFrom(TEXT);
for (int i = 0; i < rowText.length; i++) rowText[i] = "row-" + i;
ffmDb = Db.open(":memory:");
ffmDb.exec("CREATE TABLE t(a INTEGER, b TEXT)");
jniDb = SqliteJni.open(":memory:");
SqliteJni.exec(jniDb, "CREATE TABLE t(a INTEGER, b TEXT)");
// sanity: all three agree before we time anything
if (sqlite3_libversion_number() != SqliteJni.versionNumber()) throw new AssertionError("version mismatch");
if (sqlite3_strlike(cGlob, cText, 0) != SqliteJni.strlike(GLOB, TEXT)) throw new AssertionError("strlike mismatch");
}
@TearDown
public void tearDown() {
ffmDb.close();
SqliteJni.close(jniDb);
global.close();
}
// ---- no-argument call ----
@Benchmark public int versionNumber_plainJava() { return constant; }
@Benchmark public int versionNumber_ffm() { return sqlite3_libversion_number(); }
@Benchmark public int versionNumber_jni() { return SqliteJni.versionNumber(); }
// ---- two C strings ----
@Benchmark public int strlike_ffm_preallocated() { return sqlite3_strlike(cGlob, cText, 0); }
@Benchmark public int strlike_ffm_arenaPerCall() {
try (Arena a = Arena.ofConfined()) {
return sqlite3_strlike(a.allocateFrom(GLOB), a.allocateFrom(TEXT), 0);
}
}
@Benchmark public int strlike_jni() { return SqliteJni.strlike(GLOB, TEXT); }
// ---- a real workload ----
@Benchmark public int insert1000_ffm() {
ffmDb.exec("BEGIN");
try (Arena arena = Arena.ofConfined(); Db.Stmt st = ffmDb.prepare(arena, INSERT)) {
for (int i = 0; i < 1000; i++) {
st.bindInt(1, i);
st.bindText(arena, 2, rowText[i]);
st.step();
st.reset();
}
}
ffmDb.exec("ROLLBACK");
return 1000;
}
@Benchmark public int insert1000_jni() {
SqliteJni.exec(jniDb, "BEGIN");
long st = SqliteJni.prepare(jniDb, INSERT);
for (int i = 0; i < 1000; i++) {
SqliteJni.bindInt(st, 1, i);
SqliteJni.bindText(st, 2, rowText[i]);
SqliteJni.step(st);
SqliteJni.reset(st);
}
SqliteJni.finalizeStmt(st);
SqliteJni.exec(jniDb, "ROLLBACK");
return 1000;
}
}
@@ -0,0 +1,69 @@
// Generated by jextract
package demo.sqlite;
import java.lang.invoke.*;
import java.lang.foreign.*;
import java.nio.ByteOrder;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import static java.lang.foreign.ValueLayout.*;
import static java.lang.foreign.MemoryLayout.PathElement.*;
/**
* {@snippet lang=c :
* void (*)(void *)
* }
*/
public final class sqlite3_bind_text$x0 {
private sqlite3_bind_text$x0() {
// Should not be called directly
}
/**
* The function pointer signature, expressed as a functional interface
*/
public interface Function {
void apply(MemorySegment _x0);
}
private static final FunctionDescriptor $DESC = FunctionDescriptor.ofVoid(
sqlite3_h.C_POINTER
);
/**
* The descriptor of this function pointer
*/
public static FunctionDescriptor descriptor() {
return $DESC;
}
private static final MethodHandle UP$MH = sqlite3_h.upcallHandle(sqlite3_bind_text$x0.Function.class, "apply", $DESC);
/**
* Allocates a new upcall stub, whose implementation is defined by {@code fi}.
* The lifetime of the returned segment is managed by {@code arena}
*/
public static MemorySegment allocate(sqlite3_bind_text$x0.Function fi, Arena arena) {
return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, arena);
}
private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC);
/**
* Invoke the upcall stub {@code funcPtr}, with given parameters
*/
public static void invoke(MemorySegment funcPtr, MemorySegment _x0) {
try {
DOWN$MH.invokeExact(funcPtr, _x0);
} catch (Error | RuntimeException ex) {
throw ex;
} catch (Throwable ex$) {
throw new AssertionError("should not reach here", ex$);
}
}
}
@@ -0,0 +1,73 @@
// Generated by jextract
package demo.sqlite;
import java.lang.invoke.*;
import java.lang.foreign.*;
import java.nio.ByteOrder;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import static java.lang.foreign.ValueLayout.*;
import static java.lang.foreign.MemoryLayout.PathElement.*;
/**
* {@snippet lang=c :
* int (*callback)(void *, int, char **, char **)
* }
*/
public final class sqlite3_exec$callback {
private sqlite3_exec$callback() {
// Should not be called directly
}
/**
* The function pointer signature, expressed as a functional interface
*/
public interface Function {
int apply(MemorySegment _x0, int _x1, MemorySegment _x2, MemorySegment _x3);
}
private static final FunctionDescriptor $DESC = FunctionDescriptor.of(
sqlite3_h.C_INT,
sqlite3_h.C_POINTER,
sqlite3_h.C_INT,
sqlite3_h.C_POINTER,
sqlite3_h.C_POINTER
);
/**
* The descriptor of this function pointer
*/
public static FunctionDescriptor descriptor() {
return $DESC;
}
private static final MethodHandle UP$MH = sqlite3_h.upcallHandle(sqlite3_exec$callback.Function.class, "apply", $DESC);
/**
* Allocates a new upcall stub, whose implementation is defined by {@code fi}.
* The lifetime of the returned segment is managed by {@code arena}
*/
public static MemorySegment allocate(sqlite3_exec$callback.Function fi, Arena arena) {
return Linker.nativeLinker().upcallStub(UP$MH.bindTo(fi), $DESC, arena);
}
private static final MethodHandle DOWN$MH = Linker.nativeLinker().downcallHandle($DESC);
/**
* Invoke the upcall stub {@code funcPtr}, with given parameters
*/
public static int invoke(MemorySegment funcPtr, MemorySegment _x0, int _x1, MemorySegment _x2, MemorySegment _x3) {
try {
return (int) DOWN$MH.invokeExact(funcPtr, _x0, _x1, _x2, _x3);
} catch (Error | RuntimeException ex) {
throw ex;
} catch (Throwable ex$) {
throw new AssertionError("should not reach here", ex$);
}
}
}
@@ -0,0 +1,63 @@
// Generated by jextract
package demo.sqlite;
import java.lang.invoke.*;
import java.lang.foreign.*;
import java.nio.ByteOrder;
import java.util.*;
import java.util.function.*;
import java.util.stream.*;
import static java.lang.foreign.ValueLayout.*;
import static java.lang.foreign.MemoryLayout.PathElement.*;
public class sqlite3_h$shared {
sqlite3_h$shared() {
// Should not be called directly
}
public static final ValueLayout.OfBoolean C_BOOL = (ValueLayout.OfBoolean) Linker.nativeLinker().canonicalLayouts().get("bool");
public static final ValueLayout.OfByte C_CHAR =(ValueLayout.OfByte)Linker.nativeLinker().canonicalLayouts().get("char");
public static final ValueLayout.OfShort C_SHORT = (ValueLayout.OfShort) Linker.nativeLinker().canonicalLayouts().get("short");
public static final ValueLayout.OfInt C_INT = (ValueLayout.OfInt) Linker.nativeLinker().canonicalLayouts().get("int");
public static final ValueLayout.OfLong C_LONG_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long long");
public static final ValueLayout.OfFloat C_FLOAT = (ValueLayout.OfFloat) Linker.nativeLinker().canonicalLayouts().get("float");
public static final ValueLayout.OfDouble C_DOUBLE = (ValueLayout.OfDouble) Linker.nativeLinker().canonicalLayouts().get("double");
public static final AddressLayout C_POINTER = ((AddressLayout) Linker.nativeLinker().canonicalLayouts().get("void*"))
.withTargetLayout(MemoryLayout.sequenceLayout(java.lang.Long.MAX_VALUE, C_CHAR));
public static final ValueLayout.OfLong C_LONG = (ValueLayout.OfLong) Linker.nativeLinker().canonicalLayouts().get("long");
static final boolean TRACE_DOWNCALLS = Boolean.getBoolean("jextract.trace.downcalls");
static void traceDowncall(String name, Object... args) {
String traceArgs = Arrays.stream(args)
.map(Object::toString)
.collect(Collectors.joining(", "));
System.out.printf("%s(%s)\n", name, traceArgs);
}
static MethodHandle upcallHandle(Class<?> fi, String name, FunctionDescriptor fdesc) {
try {
return MethodHandles.lookup().findVirtual(fi, name, fdesc.toMethodType());
} catch (ReflectiveOperationException ex) {
throw new AssertionError(ex);
}
}
static MemoryLayout align(MemoryLayout layout, long align) {
return switch (layout) {
case PaddingLayout p -> p;
case ValueLayout v -> v.withByteAlignment(align);
case GroupLayout g -> {
MemoryLayout[] alignedMembers = g.memberLayouts().stream()
.map(m -> align(m, align)).toArray(MemoryLayout[]::new);
yield g instanceof StructLayout ?
MemoryLayout.structLayout(alignedMembers) : MemoryLayout.unionLayout(alignedMembers);
}
case SequenceLayout s -> MemoryLayout.sequenceLayout(s.elementCount(), align(s.elementLayout(), align));
};
}
}
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
package bench;
/** The JNI twin of the jextract bindings: the same SQLite calls, through a hand-written C shim (jni/sqlite_jni.c). */
public final class SqliteJni {
static {
System.loadLibrary("sqlitejni"); // libsqlitejni.so, found through -Djava.library.path
}
private SqliteJni() {}
public static native int versionNumber();
public static native int strlike(String glob, String text);
public static native long open(String path);
public static native int exec(long db, String sql);
public static native long prepare(long db, String sql);
public static native int bindInt(long stmt, int index, int value);
public static native int bindText(long stmt, int index, String value);
public static native int step(long stmt);
public static native int reset(long stmt);
public static native int finalizeStmt(long stmt);
public static native int close(long db);
}
+69
View File
@@ -0,0 +1,69 @@
/* The hand-written JNI shim: one C function per Java native method, with the marshalling done by hand.
Header generated by `javac -h`; see scripts/build-jni.sh. */
#include <stdint.h>
#include <jni.h>
#include <sqlite3.h>
#include "bench_SqliteJni.h"
JNIEXPORT jint JNICALL Java_bench_SqliteJni_versionNumber(JNIEnv *env, jclass c) {
return sqlite3_libversion_number();
}
JNIEXPORT jint JNICALL Java_bench_SqliteJni_strlike(JNIEnv *env, jclass c, jstring glob, jstring text) {
const char *g = (*env)->GetStringUTFChars(env, glob, NULL);
const char *t = (*env)->GetStringUTFChars(env, text, NULL);
int r = sqlite3_strlike(g, t, 0);
(*env)->ReleaseStringUTFChars(env, text, t);
(*env)->ReleaseStringUTFChars(env, glob, g);
return r;
}
JNIEXPORT jlong JNICALL Java_bench_SqliteJni_open(JNIEnv *env, jclass c, jstring path) {
const char *p = (*env)->GetStringUTFChars(env, path, NULL);
sqlite3 *db = NULL;
int rc = sqlite3_open(p, &db);
(*env)->ReleaseStringUTFChars(env, path, p);
return rc == SQLITE_OK ? (jlong)(intptr_t)db : 0;
}
JNIEXPORT jint JNICALL Java_bench_SqliteJni_exec(JNIEnv *env, jclass c, jlong db, jstring sql) {
const char *s = (*env)->GetStringUTFChars(env, sql, NULL);
int rc = sqlite3_exec((sqlite3 *)(intptr_t)db, s, NULL, NULL, NULL);
(*env)->ReleaseStringUTFChars(env, sql, s);
return rc;
}
JNIEXPORT jlong JNICALL Java_bench_SqliteJni_prepare(JNIEnv *env, jclass c, jlong db, jstring sql) {
const char *s = (*env)->GetStringUTFChars(env, sql, NULL);
sqlite3_stmt *st = NULL;
int rc = sqlite3_prepare_v2((sqlite3 *)(intptr_t)db, s, -1, &st, NULL);
(*env)->ReleaseStringUTFChars(env, sql, s);
return rc == SQLITE_OK ? (jlong)(intptr_t)st : 0;
}
JNIEXPORT jint JNICALL Java_bench_SqliteJni_bindInt(JNIEnv *env, jclass c, jlong st, jint i, jint v) {
return sqlite3_bind_int((sqlite3_stmt *)(intptr_t)st, i, v);
}
JNIEXPORT jint JNICALL Java_bench_SqliteJni_bindText(JNIEnv *env, jclass c, jlong st, jint i, jstring v) {
const char *s = (*env)->GetStringUTFChars(env, v, NULL);
int rc = sqlite3_bind_text((sqlite3_stmt *)(intptr_t)st, i, s, -1, SQLITE_TRANSIENT);
(*env)->ReleaseStringUTFChars(env, v, s);
return rc;
}
JNIEXPORT jint JNICALL Java_bench_SqliteJni_step(JNIEnv *env, jclass c, jlong st) {
return sqlite3_step((sqlite3_stmt *)(intptr_t)st);
}
JNIEXPORT jint JNICALL Java_bench_SqliteJni_reset(JNIEnv *env, jclass c, jlong st) {
return sqlite3_reset((sqlite3_stmt *)(intptr_t)st);
}
JNIEXPORT jint JNICALL Java_bench_SqliteJni_finalizeStmt(JNIEnv *env, jclass c, jlong st) {
return sqlite3_finalize((sqlite3_stmt *)(intptr_t)st);
}
JNIEXPORT jint JNICALL Java_bench_SqliteJni_close(JNIEnv *env, jclass c, jlong db) {
return sqlite3_close((sqlite3 *)(intptr_t)db);
}
+16
View File
@@ -0,0 +1,16 @@
$ jextract --version
jextract 25
JDK version 25+37-3491
LibClang version clang version 13.0.0
$ java -version
openjdk version "25.0.5" 2026-10-20 LTS
OpenJDK Runtime Environment Temurin-25.0.5+8 (build 25.0.5+8-LTS)
OpenJDK 64-Bit Server VM Temurin-25.0.5+8 (build 25.0.5+8-LTS, mixed mode, sharing)
$ gcc --version | head -1
gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
$ dpkg-query -W libsqlite3-dev libsqlite3-0
libsqlite3-0:amd64 3.45.1-1ubuntu2.8
libsqlite3-dev:amd64 3.45.1-1ubuntu2.8
$ grep -E '#define SQLITE_(VERSION|VERSION_NUMBER) ' /usr/include/sqlite3.h
#define SQLITE_VERSION "3.45.1"
#define SQLITE_VERSION_NUMBER 3045001
+26
View File
@@ -0,0 +1,26 @@
$ scripts/gen.sh
$ find generated -name '*.java' | wc -l
4
$ wc -l $(find generated -name '*.java') | sort -n
63 generated/demo/sqlite/sqlite3_h$shared.java
69 generated/demo/sqlite/sqlite3_bind_text$x0.java
73 generated/demo/sqlite/sqlite3_exec$callback.java
1662 generated/demo/sqlite/sqlite3_h.java
1867 total
$ jextract --output <tmp> -t demo.sqlite.full -l sqlite3 /usr/include/sqlite3.h # no filters: the whole header
warnings: 14
sqlite3.h:272:16: warning: Skipping sqlite3 (type Declared(sqlite3) is not supported)
sqlite3.h:4045:16: warning: Skipping sqlite3_stmt (type Declared(sqlite3_stmt) is not supported)
files: 83
lines: 44295
bytes: 1339454
lines in the one big header class: 22335
$ grep -o 'public static [A-Za-z.]* sqlite3_open[a-z_0-9$]*(' generated/demo/sqlite/sqlite3_h.java | sort -u
public static FunctionDescriptor sqlite3_open$descriptor(
public static FunctionDescriptor sqlite3_open_v2$descriptor(
public static MemorySegment sqlite3_open$address(
public static MemorySegment sqlite3_open_v2$address(
public static MethodHandle sqlite3_open$handle(
public static MethodHandle sqlite3_open_v2$handle(
public static int sqlite3_open(
public static int sqlite3_open_v2(
+14
View File
@@ -0,0 +1,14 @@
$ java --enable-native-access=ALL-UNNAMED -cp build/classes demo.Hello
sqlite3_libversion() = 3.45.1
returned segment byteSize = 9223372036854775807
sqlite3_libversion_number() = 3045001
sqlite3_sourceid() = 2024-01-30 16:01:20 e876e51a0ed5c5b3126f52e532044363a014bc594cfefa87ffb5b82257ccalt1
$ java --enable-native-access=ALL-UNNAMED -Djextract.trace.downcalls=true -cp build/classes demo.Hello
sqlite3_libversion()
sqlite3_libversion() = 3.45.1
returned segment byteSize = 9223372036854775807
sqlite3_libversion_number()
sqlite3_libversion_number() = 3045001
sqlite3_sourceid()
sqlite3_sourceid() = 2024-01-30 16:01:20 e876e51a0ed5c5b3126f52e532044363a014bc594cfefa87ffb5b82257ccalt1
+18
View File
@@ -0,0 +1,18 @@
$ java -cp build/classes demo.Hello # no flag at all, JDK 25 default
WARNING: A restricted method in java.lang.foreign.AddressLayout has been called
WARNING: java.lang.foreign.AddressLayout::withTargetLayout has been called by demo.sqlite.sqlite3_h$shared in an unnamed module (file:/tmp/work/jextract/repo/jextract/build/classes/)
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
sqlite3_libversion() = 3.45.1
returned segment byteSize = 9223372036854775807
sqlite3_libversion_number() = 3045001
sqlite3_sourceid() = 2024-01-30 16:01:20 e876e51a0ed5c5b3126f52e532044363a014bc594cfefa87ffb5b82257ccalt1
$ java --illegal-native-access=deny -cp build/classes demo.Hello
Exception in thread "main" java.lang.ExceptionInInitializerError
at demo.Hello.main(Hello.java:10)
Caused by: java.lang.IllegalCallerException: Illegal native access from an unnamed module (file:/tmp/work/jextract/repo/jextract/build/classes/)
at java.base/java.lang.Module.ensureNativeAccess(Module.java:307)
at java.base/java.lang.System$1.ensureNativeAccess(System.java:2107)
at java.base/jdk.internal.reflect.Reflection.ensureNativeAccess(Reflection.java:123)
+6
View File
@@ -0,0 +1,6 @@
$ java --enable-native-access=ALL-UNNAMED -cp build/classes demo.Tour
rows changed by last statement: 1, last rowid: 3
[1, Ankur, 41]
[2, Asha, 36]
[3, Ravi, 29]
[[NoAge, null]]
+21
View File
@@ -0,0 +1,21 @@
$ java --enable-native-access=ALL-UNNAMED -cp build/classes demo.Arenas
== confined: one owner thread
owner reads -> ok
other thread reads -> WrongThreadException: Attempted access outside owning thread
other thread closes -> WrongThreadException: Attempted access outside owning thread
read after close -> IllegalStateException: Already closed
close twice -> IllegalStateException: Already closed
== shared: any thread, one close
other thread reads -> ok
other thread closes -> ok
read after close -> IllegalStateException: Already closed
== auto: the garbage collector frees it; close() is not allowed
allocate 8 bytes -> ok
close() -> UnsupportedOperationException: Attempted to close a non-closeable session
== global: never freed; close() is not allowed
allocate 8 bytes -> ok
close() -> UnsupportedOperationException: Attempted to close a non-closeable session
== bounds: a segment knows its size
read int at offset 0 -> ok
read int at offset 4 -> IndexOutOfBoundsException: Out of bound access on segment MemorySegment{ kind: native, address: 0x7fba74666600, byteSize: 4 }; new offset = 4; new length = 4
read long at offset 0 -> IndexOutOfBoundsException: Out of bound access on segment MemorySegment{ kind: native, address: 0x7fba74666600, byteSize: 4 }; new offset = 0; new length = 8
+18
View File
@@ -0,0 +1,18 @@
$ java --enable-native-access=ALL-UNNAMED -cp build/classes demo.Trap
ValueLayout.ADDRESS : byteSize=0
getString(0) : IndexOutOfBoundsException, "No null terminator found..."
reinterpret(100) : hello from C land
C_POINTER : byteSize=9223372036854775807
getString(0) : hello from C land
$ java --enable-native-access=ALL-UNNAMED -XX:ErrorFile=/tmp/jextract-hs_err.log -cp build/classes demo.NullText # first lines of the crash banner
address=0 byteSize=9223372036854775807 equals(NULL)=true
about to call getString(0) on it
#
# A fatal error has been detected by the Java Runtime Environment:
#
# SIGSEGV (0xb) at pc=0x00007f002fe4645d, pid=20843, tid=20844
#
# JRE version: OpenJDK Runtime Environment Temurin-25.0.5+8 (25.0.5+8) (build 25.0.5+8-LTS)
# Java VM: OpenJDK 64-Bit Server VM Temurin-25.0.5+8 (25.0.5+8-LTS, mixed mode, sharing, tiered, compressed oops, compressed class ptrs, g1 gc, linux-amd64)
exit code: 134
+8
View File
@@ -0,0 +1,8 @@
$ java --enable-native-access=ALL-UNNAMED -cp build/classes demo.Errors
open in a directory that does not exist -> unable to open database file (rc=14)
open a missing file read-only -> unable to open database file (rc=14)
syntax error -> near "SELEC": syntax error (rc=1)
missing table -> no such table: ghost (rc=1)
NOT NULL violation -> NOT NULL constraint failed: t.name (rc=19)
PRIMARY KEY violation -> UNIQUE constraint failed: t.id (rc=19)
SQLITE_CONSTRAINT = 19, sqlite3_errstr(19) = constraint failed
+13
View File
@@ -0,0 +1,13 @@
$ java --enable-native-access=ALL-UNNAMED -cp build/classes demo.Callbacks
row: id=1 name=one
row: id=2 name=two
row: id=3 name=NULL
sqlite3_exec returned 0
$ java --enable-native-access=ALL-UNNAMED -cp build/classes demo.Callbacks explode
java.lang.IllegalStateException: thrown inside the callback
at demo.Callbacks.lambda$main$0(Callbacks.java:23)
at demo.sqlite.sqlite3_h.sqlite3_exec(sqlite3_h.java:417)
at demo.Callbacks.main(Callbacks.java:39)
Unrecoverable uncaught exception encountered. The VM will now exit
exit code: 1
+14
View File
@@ -0,0 +1,14 @@
$ java --enable-native-access=ALL-UNNAMED -Djava.library.path=build/lib org.openjdk.jmh.Main -f 5 -wi 3 -w 1 -i 5 -r 1 (25.0.5+8-LTS, JMH 1.37)
machine: 2 CPUs, Intel(R) Xeon(R) Processor @ 2.10GHz
load average before the run: 4.00 4.80 2.92 (1, 5, 15 minutes; this box has 2 CPUs and was shared with other jobs)
sqlite: libsqlite3-dev 3.45.1-1ubuntu2.8
Benchmark Mode Cnt Score Error Units
SqliteBench.insert1000_ffm avgt 25 653368.858 ± 161095.825 ns/op
SqliteBench.insert1000_jni avgt 25 544963.964 ± 97090.203 ns/op
SqliteBench.strlike_ffm_arenaPerCall avgt 25 192.233 ± 62.055 ns/op
SqliteBench.strlike_ffm_preallocated avgt 25 82.660 ± 16.253 ns/op
SqliteBench.strlike_jni avgt 25 244.929 ± 82.330 ns/op
SqliteBench.versionNumber_ffm avgt 25 16.819 ± 4.063 ns/op
SqliteBench.versionNumber_jni avgt 25 14.054 ± 4.949 ns/op
SqliteBench.versionNumber_plainJava avgt 25 0.651 ± 0.162 ns/op
load average after the run: 5.97 6.05 4.10
+14
View File
@@ -0,0 +1,14 @@
$ jextract -t demo.sqlite -l sqlitez --include-function sqlite3_libversion --header-class-name sqlite3_h /usr/include/sqlite3.h
static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.libraryLookup(System.mapLibraryName("sqlitez"), LIBRARY_ARENA)
$ java --enable-native-access=ALL-UNNAMED -cp <classes> V
Exception in thread "main" java.lang.ExceptionInInitializerError
at V.main(V.java:6)
Caused by: java.lang.IllegalArgumentException: Cannot open library: libsqlitez.so
at java.base/java.lang.foreign.SymbolLookup.libraryLookup(SymbolLookup.java:350)
at java.base/java.lang.foreign.SymbolLookup.libraryLookup(SymbolLookup.java:301)
$ jextract -t demo.sqlite -l :libsqlite3.so.0 --include-function sqlite3_libversion --header-class-name sqlite3_h /usr/include/sqlite3.h
static final SymbolLookup SYMBOL_LOOKUP = SymbolLookup.libraryLookup("libsqlite3.so.0", LIBRARY_ARENA)
$ java --enable-native-access=ALL-UNNAMED -cp <classes> V
sqlite3_libversion() = 3.45.1
+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"
+65
View File
@@ -0,0 +1,65 @@
package demo;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
/** The four arena kinds, and what each one does when you use it wrongly. Every message is real. */
public class Arenas {
static void show(String label, Runnable r) {
try {
r.run();
System.out.println(label + " -> ok");
} catch (Throwable t) {
System.out.println(label + " -> " + t.getClass().getSimpleName() + ": " + t.getMessage());
}
}
static void inThread(Runnable r) {
Thread t = new Thread(r, "other");
t.start();
try {
t.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
System.out.println("== confined: one owner thread");
Arena confined = Arena.ofConfined();
MemorySegment c = confined.allocate(8);
c.set(ValueLayout.JAVA_LONG, 0, 42L);
show("owner reads", () -> c.get(ValueLayout.JAVA_LONG, 0));
inThread(() -> show("other thread reads", () -> c.get(ValueLayout.JAVA_LONG, 0)));
inThread(() -> show("other thread closes", confined::close));
confined.close();
show("read after close", () -> c.get(ValueLayout.JAVA_LONG, 0));
show("close twice", confined::close);
System.out.println("== shared: any thread, one close");
Arena shared = Arena.ofShared();
MemorySegment s = shared.allocate(8);
s.set(ValueLayout.JAVA_LONG, 0, 7L);
inThread(() -> show("other thread reads", () -> s.get(ValueLayout.JAVA_LONG, 0)));
inThread(() -> show("other thread closes", shared::close));
show("read after close", () -> s.get(ValueLayout.JAVA_LONG, 0));
System.out.println("== auto: the garbage collector frees it; close() is not allowed");
Arena auto = Arena.ofAuto();
show("allocate 8 bytes", () -> auto.allocate(8));
show("close()", auto::close);
System.out.println("== global: never freed; close() is not allowed");
show("allocate 8 bytes", () -> Arena.global().allocate(8));
show("close()", () -> Arena.global().close());
System.out.println("== bounds: a segment knows its size");
try (Arena a = Arena.ofConfined()) {
MemorySegment four = a.allocate(4);
show("read int at offset 0", () -> four.get(ValueLayout.JAVA_INT, 0));
show("read int at offset 4", () -> four.get(ValueLayout.JAVA_INT, 4));
show("read long at offset 0", () -> four.get(ValueLayout.JAVA_LONG, 0));
}
}
}
+43
View File
@@ -0,0 +1,43 @@
package demo;
import static demo.sqlite.sqlite3_h.*;
import demo.sqlite.sqlite3_exec$callback;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
/**
* sqlite3_exec calls a C function pointer once per result row. jextract generated an interface
* for that pointer type; a lambda implements it, and allocate() turns the lambda into a real C function pointer.
*/
public class Callbacks {
public static void main(String[] args) {
boolean explode = args.length > 0 && args[0].equals("explode");
try (Db db = Db.open(":memory:"); Arena arena = Arena.ofConfined()) {
db.exec("CREATE TABLE t(id INTEGER, name TEXT)");
db.exec("INSERT INTO t VALUES (1,'one'),(2,'two'),(3,NULL)");
sqlite3_exec$callback.Function onRow = (user, argc, values, names) -> {
if (explode) {
throw new IllegalStateException("thrown inside the callback");
}
// values and names are char** : argc pointers each. Read them as an array of pointers.
MemorySegment vals = values.reinterpret((long) argc * ValueLayout.ADDRESS.byteSize());
MemorySegment nms = names.reinterpret((long) argc * ValueLayout.ADDRESS.byteSize());
StringBuilder sb = new StringBuilder("row:");
for (int i = 0; i < argc; i++) {
MemorySegment v = vals.getAtIndex(C_POINTER, i);
String value = v.address() == 0 ? "NULL" : v.getString(0);
sb.append(' ').append(nms.getAtIndex(C_POINTER, i).getString(0)).append('=').append(value);
}
System.out.println(sb);
return 0; // non-zero would abort the query with SQLITE_ABORT
};
MemorySegment stub = sqlite3_exec$callback.allocate(onRow, arena); // the C function pointer
MemorySegment errOut = arena.allocate(C_POINTER);
int rc = sqlite3_exec(db.handle(), arena.allocateFrom("SELECT id, name FROM t"), stub, MemorySegment.NULL, errOut);
System.out.println("sqlite3_exec returned " + rc);
}
}
}
+155
View File
@@ -0,0 +1,155 @@
package demo;
import static demo.sqlite.sqlite3_h.*;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.util.ArrayList;
import java.util.List;
/**
* A small hand-written layer over the jextract-generated {@code sqlite3_h} class.
* Everything below the {@code public} methods is plain FFM; nothing here is generated.
*/
public final class Db implements AutoCloseable {
private final MemorySegment handle; // the sqlite3* — owned by SQLite, NOT by any Arena
private Db(MemorySegment handle) {
this.handle = handle;
}
public static Db open(String path) {
return open(path, SQLITE_OPEN_READWRITE() | SQLITE_OPEN_CREATE());
}
public static Db open(String path, int flags) {
// A confined arena: the two small native buffers below live exactly as long as this block.
try (Arena arena = Arena.ofConfined()) {
MemorySegment out = arena.allocate(C_POINTER); // room for ONE pointer: the sqlite3** out-parameter
MemorySegment cPath = arena.allocateFrom(path); // Java String -> NUL-terminated UTF-8 C string
int rc = sqlite3_open_v2(cPath, out, flags, MemorySegment.NULL);
MemorySegment db = out.get(C_POINTER, 0); // read the pointer SQLite wrote
if (rc != SQLITE_OK()) {
// SQLite hands back a handle even on failure; it carries the message and must still be closed.
String message = sqlite3_errmsg(db).getString(0);
sqlite3_close(db);
throw new SqliteException(rc, message);
}
return new Db(db);
}
}
/** Runs SQL that returns no rows (DDL, INSERT...). Uses sqlite3_exec without a callback. */
public void exec(String sql) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment errOut = arena.allocate(C_POINTER); // char** errmsg, SQLite fills it only on failure
int rc = sqlite3_exec(handle, arena.allocateFrom(sql), MemorySegment.NULL, MemorySegment.NULL, errOut);
if (rc != SQLITE_OK()) {
MemorySegment msg = errOut.get(C_POINTER, 0);
String text = msg.address() == 0 ? errmsg() : msg.getString(0);
sqlite3_free(msg); // this string came from sqlite3_malloc: we must free it
throw new SqliteException(rc, text);
}
}
}
/** Runs a SELECT and returns every row as a list of strings. */
public List<List<String>> query(String sql) {
try (Arena arena = Arena.ofConfined(); Stmt st = prepare(arena, sql)) {
List<List<String>> rows = new ArrayList<>();
int cols = sqlite3_column_count(st.ptr);
int rc;
while ((rc = sqlite3_step(st.ptr)) == SQLITE_ROW()) {
List<String> row = new ArrayList<>(cols);
for (int i = 0; i < cols; i++) {
row.add(text(st.ptr, i));
}
rows.add(row);
}
if (rc != SQLITE_DONE()) {
throw new SqliteException(rc, errmsg());
}
return rows;
}
}
/** Column i of the current row as a String; null for SQL NULL. */
static String text(MemorySegment stmt, int i) {
if (sqlite3_column_type(stmt, i) == SQLITE_NULL()) {
return null; // sqlite3_column_text would return a NULL pointer here
}
return sqlite3_column_text(stmt, i).getString(0);
}
/** A prepared statement. The sqlite3_stmt* belongs to SQLite, so it needs an explicit finalize. */
public final class Stmt implements AutoCloseable {
final MemorySegment ptr;
public MemorySegment ptr() {
return ptr;
}
Stmt(MemorySegment ptr) {
this.ptr = ptr;
}
public void bindInt(int index, int value) {
check(sqlite3_bind_int(ptr, index, value));
}
/** SQLITE_TRANSIENT tells SQLite to copy the bytes now, so the temporary C string may be freed straight away. */
public void bindText(Arena arena, int index, String value) {
check(sqlite3_bind_text(ptr, index, arena.allocateFrom(value), -1, SQLITE_TRANSIENT()));
}
public boolean step() {
int rc = sqlite3_step(ptr);
if (rc == SQLITE_ROW()) return true;
if (rc == SQLITE_DONE()) return false;
throw new SqliteException(rc, errmsg());
}
public void reset() {
sqlite3_reset(ptr);
}
@Override
public void close() {
sqlite3_finalize(ptr);
}
}
public Stmt prepare(Arena arena, String sql) {
MemorySegment out = arena.allocate(C_POINTER);
int rc = sqlite3_prepare_v2(handle, arena.allocateFrom(sql), -1, out, MemorySegment.NULL);
check(rc);
return new Stmt(out.get(C_POINTER, 0));
}
public int changes() {
return sqlite3_changes(handle);
}
public long lastInsertRowid() {
return sqlite3_last_insert_rowid(handle);
}
public String errmsg() {
return sqlite3_errmsg(handle).getString(0);
}
public MemorySegment handle() {
return handle;
}
private void check(int rc) {
if (rc != SQLITE_OK()) {
throw new SqliteException(rc, errmsg());
}
}
@Override
public void close() {
sqlite3_close(handle);
}
}
+29
View File
@@ -0,0 +1,29 @@
package demo;
import static demo.sqlite.sqlite3_h.*;
/** Return codes become exceptions. Each failure below is real SQLite output. */
public class Errors {
static void attempt(String label, Runnable r) {
try {
r.run();
System.out.println(label + " -> no error");
} catch (SqliteException e) {
System.out.println(label + " -> " + e.getMessage());
}
}
public static void main(String[] args) {
attempt("open in a directory that does not exist", () -> Db.open("/no/such/dir/app.db").close());
attempt("open a missing file read-only", () -> Db.open("/tmp/jextract-demo-missing.db", SQLITE_OPEN_READONLY()).close());
try (Db db = Db.open(":memory:")) {
attempt("syntax error", () -> db.exec("SELEC 1"));
attempt("missing table", () -> db.exec("INSERT INTO ghost VALUES (1)"));
db.exec("CREATE TABLE t(id INTEGER PRIMARY KEY, name TEXT NOT NULL)");
attempt("NOT NULL violation", () -> db.exec("INSERT INTO t(id) VALUES (1)"));
db.exec("INSERT INTO t VALUES (1, 'a')");
attempt("PRIMARY KEY violation", () -> db.exec("INSERT INTO t VALUES (1, 'b')"));
System.out.println("SQLITE_CONSTRAINT = " + SQLITE_CONSTRAINT() + ", sqlite3_errstr(19) = " + sqlite3_errstr(19).getString(0));
}
}
}
+16
View File
@@ -0,0 +1,16 @@
package demo;
import static demo.sqlite.sqlite3_h.*;
import java.lang.foreign.MemorySegment;
/** The smallest useful call: ask the native library which version it is. */
public class Hello {
public static void main(String[] args) {
MemorySegment v = sqlite3_libversion(); // const char* -> MemorySegment
System.out.println("sqlite3_libversion() = " + v.getString(0));
System.out.println("returned segment byteSize = " + v.byteSize());
System.out.println("sqlite3_libversion_number() = " + sqlite3_libversion_number());
System.out.println("sqlite3_sourceid() = " + sqlite3_sourceid().getString(0));
}
}
+28
View File
@@ -0,0 +1,28 @@
package demo;
import static demo.sqlite.sqlite3_h.*;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
/** What sqlite3_column_text returns for a SQL NULL, and what happens if you call getString(0) on it anyway. */
public class NullText {
public static void main(String[] args) {
try (Db db = Db.open(":memory:"); Arena arena = Arena.ofConfined()) {
db.exec("CREATE TABLE t(a TEXT)");
db.exec("INSERT INTO t VALUES (NULL)");
try (Db.Stmt st = db.prepare(arena, "SELECT a FROM t")) {
st.step();
MemorySegment p = sqlite3_column_text(st.ptr(), 0);
System.out.println("address=" + p.address() + " byteSize=" + p.byteSize() + " equals(NULL)=" + p.equals(MemorySegment.NULL));
System.out.println("about to call getString(0) on it");
System.out.flush();
try {
System.out.println("getString -> " + p.getString(0));
} catch (Throwable t) {
System.out.println("getString -> " + t.getClass().getSimpleName() + ": " + t.getMessage());
}
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
package demo;
/** A SQLite result code turned into an exception: the numeric code plus the text from sqlite3_errmsg. */
public final class SqliteException extends RuntimeException {
private final int code;
public SqliteException(int code, String message) {
super(message + " (rc=" + code + ")");
this.code = code;
}
public int code() {
return code;
}
}
+28
View File
@@ -0,0 +1,28 @@
package demo;
import java.lang.foreign.Arena;
/** Open a database, create a table, insert with bind parameters, read it back. */
public class Tour {
public static void main(String[] args) {
try (Db db = Db.open(":memory:")) {
db.exec("CREATE TABLE person(id INTEGER PRIMARY KEY, name TEXT NOT NULL, age INTEGER)");
try (Arena arena = Arena.ofConfined(); Db.Stmt ins = db.prepare(arena, "INSERT INTO person(name, age) VALUES (?1, ?2)")) {
String[] names = {"Ankur", "Asha", "Ravi"};
int[] ages = {41, 36, 29};
for (int i = 0; i < names.length; i++) {
ins.bindText(arena, 1, names[i]);
ins.bindInt(2, ages[i]);
ins.step();
ins.reset();
}
}
System.out.println("rows changed by last statement: " + db.changes() + ", last rowid: " + db.lastInsertRowid());
for (var row : db.query("SELECT id, name, age FROM person ORDER BY id")) {
System.out.println(row);
}
db.exec("INSERT INTO person(name) VALUES ('NoAge')");
System.out.println(db.query("SELECT name, age FROM person WHERE age IS NULL"));
}
}
}
+33
View File
@@ -0,0 +1,33 @@
package demo;
import static demo.sqlite.sqlite3_h.*;
import java.lang.foreign.Arena;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
/**
* The trap that costs the first hour: a pointer you read out of memory is a zero-length segment
* unless its layout says what it points at. jextract's C_POINTER says "unbounded chars"; plain ADDRESS does not.
*/
public class Trap {
public static void main(String[] args) {
try (Arena arena = Arena.ofConfined()) {
MemorySegment slot = arena.allocate(ValueLayout.ADDRESS);
slot.set(ValueLayout.ADDRESS, 0, arena.allocateFrom("hello from C land"));
MemorySegment viaAddress = slot.get(ValueLayout.ADDRESS, 0);
System.out.println("ValueLayout.ADDRESS : byteSize=" + viaAddress.byteSize());
try {
viaAddress.getString(0);
} catch (IndexOutOfBoundsException e) {
System.out.println("getString(0) : IndexOutOfBoundsException, \"" + e.getMessage().substring(0, e.getMessage().indexOf(':', 22)) + "...\"");
}
System.out.println("reinterpret(100) : " + viaAddress.reinterpret(100).getString(0));
MemorySegment viaCPointer = slot.get(C_POINTER, 0);
System.out.println("C_POINTER : byteSize=" + viaCPointer.byteSize());
System.out.println("getString(0) : " + viaCPointer.getString(0));
}
}
}
+8
View File
@@ -0,0 +1,8 @@
// Used only by scripts/run-all.sh to test library loading with throwaway bindings (package demo.sqlite, one function).
import static demo.sqlite.sqlite3_h.*;
public class V {
public static void main(String[] a) {
System.out.println("sqlite3_libversion() = " + sqlite3_libversion().getString(0));
}
}