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:
@@ -74,6 +74,7 @@ These modules keep only sources, a `run.sh` and their transcripts (`output/`). T
|
|||||||
| [`structured-concurrency-final/`](structured-concurrency-final) | Structured Concurrency Is Final in JDK 28 (JEP 543): From Preview to Production | the thread leak of `ExecutorService` and its fix, every built-in `Joiner`, structure violations, timeouts, a custom joiner, JSON thread dumps, scoped values, `javap` diffs of the API on JDK 25, 26, 27 and 28-ea, the compiler output for six pieces of 25/26-era code, and the preview class-file trap between 27 and 28 |
|
| [`structured-concurrency-final/`](structured-concurrency-final) | Structured Concurrency Is Final in JDK 28 (JEP 543): From Preview to Production | the thread leak of `ExecutorService` and its fix, every built-in `Joiner`, structure violations, timeouts, a custom joiner, JSON thread dumps, scoped values, `javap` diffs of the API on JDK 25, 26, 27 and 28-ea, the compiler output for six pieces of 25/26-era code, and the preview class-file trap between 27 and 28 |
|
||||||
| [`kdf/`](kdf) | Key Derivation Function API in Java (JEP 510): HKDF Explained with Real Examples | `javax.crypto.KDF` final in JDK 25, `HKDF-SHA256/384/512`, extract vs expand, RFC 5869 vectors asserted, a hand-written HKDF and 300 generated cases compared with Python `cryptography` and `openssl kdf`, per-tenant keys with length-prefixed `info`, and the mistakes reproduced (same `info`, ambiguous concatenation, PIN through HKDF vs PBKDF2) |
|
| [`kdf/`](kdf) | Key Derivation Function API in Java (JEP 510): HKDF Explained with Real Examples | `javax.crypto.KDF` final in JDK 25, `HKDF-SHA256/384/512`, extract vs expand, RFC 5869 vectors asserted, a hand-written HKDF and 300 generated cases compared with Python `cryptography` and `openssl kdf`, per-tenant keys with length-prefixed `info`, and the mistakes reproduced (same `info`, ambiguous concatenation, PIN through HKDF vs PBKDF2) |
|
||||||
| [`classfile-api/`](classfile-api) | The Java ClassFile API (JEP 484): Read, Generate and Transform Bytecode Without ASM | `java.lang.classfile` final since JDK 24: `parse` and `ClassModel` traversal, `ClassFile.build` with `defineClass` and `defineHiddenClass`, five deliberately broken classes (including swapped exception-table labels that `verify()` misses), timing inserted with `ClassTransform`/`MethodTransform`/`CodeTransform`, the same three jobs in ASM 9.11.1 asserted equal, class-file version limits, a JDK 24/25/26 API diff and a look inside `spring-core` 7.0.10 |
|
| [`classfile-api/`](classfile-api) | The Java ClassFile API (JEP 484): Read, Generate and Transform Bytecode Without ASM | `java.lang.classfile` final since JDK 24: `parse` and `ClassModel` traversal, `ClassFile.build` with `defineClass` and `defineHiddenClass`, five deliberately broken classes (including swapped exception-table labels that `verify()` misses), timing inserted with `ClassTransform`/`MethodTransform`/`CodeTransform`, the same three jobs in ASM 9.11.1 asserted equal, class-file version limits, a JDK 24/25/26 API diff and a look inside `spring-core` 7.0.10 |
|
||||||
|
| [`jextract/`](jextract) | Calling C from Java with jextract and the FFM API: A Real Library, No JNI | jextract (early-access build for JDK 25) bindings for SQLite 3.45.1, the four arena kinds used wrongly on purpose, `C_POINTER` vs `ADDRESS` (zero-length pointers, and a `SIGSEGV` from `getString` on a NULL pointer), `sqlite3_open` out-parameter, return codes to exceptions, a `sqlite3_exec` upcall (and the JVM exit when it throws), the JDK 25 native-access warning and `--illegal-native-access=deny`, and a JMH comparison of FFM, hand-written JNI and plain Java |
|
||||||
|
|
||||||
## Captured output (`docs/output/`)
|
## Captured output (`docs/output/`)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
tools/
|
||||||
|
build/
|
||||||
|
bench/lib/
|
||||||
|
hs_err_pid*.log
|
||||||
@@ -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.
|
||||||
@@ -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
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
@@ -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(
|
||||||
@@ -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
|
||||||
@@ -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)
|
||||||
@@ -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]]
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
|
|
||||||
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"
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user