Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ On the module path the artifact needs `--add-modules dev.zudb.natives`. Nothing

The Foreign Function and Memory API is the primary path. The downcall handles are written by hand against `zu.h` rather than generated with `jextract`, because the C ABI here is around seventy functions with a stable shape, and a hand-written layer is where the interesting decisions live: which calls are `Linker.Option.critical` because they are short pure accessors, where the out-parameter scratch space comes from so that a query does not allocate, and how a `zu_error` becomes a typed Java exception exactly once.

An SDK that requires a recent JDK in 2026 excludes a large part of the enterprise ecosystem, so there is a JNI provider too. It is a small C shim, one build a platform, and it is the only native code in this repository that is ours. Four decisions in it are worth knowing about. It does not link against `libzu`, it opens one at run time and resolves what it calls, so building it needs a C compiler and the JDK headers and nothing else, and a shim built today opens a `libzu` built next year at the same ABI. Every string crosses as a `byte[]` rather than as a `jstring`, because JNI's own conversions speak modified UTF-8 and the engine validates the real thing, so an emoji handed to `NewStringUTF` is a byte sequence the engine refuses. One symbol is exported under the name JNI derives, and it registers the other hundred and two from a static table, because `JNI_OnLoad` cannot find a class that a module path or an application server's class loader holds. And a failure is built on the Java side, so the two providers cannot come to disagree about which exception a GQLSTATUS class names.
An SDK that requires a recent JDK in 2026 excludes a large part of the enterprise ecosystem, so there is a JNI provider too. It is a small C shim, one build a platform, and it is the only native code in this repository that is ours. Four decisions in it are worth knowing about. It does not link against `libzu`, it opens one at run time and resolves what it calls, so building it needs a C compiler and the JDK headers and nothing else, and a shim built today opens a `libzu` built next year at the same ABI. Every string crosses as a `byte[]` rather than as a `jstring`, because JNI's own conversions speak modified UTF-8 and the engine validates the real thing, so an emoji handed to `NewStringUTF` is a byte sequence the engine refuses. One symbol is exported under the name JNI derives, and it registers the other hundred and four from a static table, because `JNI_OnLoad` cannot find a class that a module path or an application server's class loader holds. And a failure is built on the Java side, so the two providers cannot come to disagree about which exception a GQLSTATUS class names.


| Artifact | Baseline | Role |
Expand Down
26 changes: 23 additions & 3 deletions api/surface.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
ctor public dev.zudb.Config(long, long, boolean)
ctor public dev.zudb.Diagnostic(dev.zudb.Status, java.lang.String, java.lang.String, java.lang.String, dev.zudb.Severity, int, int, int, java.lang.String, java.lang.String, boolean)
ctor public dev.zudb.Diagnostic(dev.zudb.Status, java.lang.String, java.lang.String, java.lang.String, dev.zudb.Severity, int, int, int, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean)
ctor public dev.zudb.Value$BindingTable()
ctor public dev.zudb.Value$Bool(boolean)
ctor public dev.zudb.Value$Bytes(byte[])
ctor public dev.zudb.Value$Field(java.lang.String, dev.zudb.Value)
ctor public dev.zudb.Value$Float(double)
ctor public dev.zudb.Value$Graph()
Expand Down Expand Up @@ -36,6 +37,7 @@ field public static final dev.zudb.Status dev.zudb.Status.UNKNOWN
field public static final dev.zudb.Status dev.zudb.Status.UNSUPPORTED
field public static final dev.zudb.Type dev.zudb.Type.BINDING_TABLE
field public static final dev.zudb.Type dev.zudb.Type.BOOL
field public static final dev.zudb.Type dev.zudb.Type.BYTES
field public static final dev.zudb.Type dev.zudb.Type.FLOAT
field public static final dev.zudb.Type dev.zudb.Type.GRAPH
field public static final dev.zudb.Type dev.zudb.Type.INT
Expand All @@ -62,6 +64,7 @@ method public abstract boolean dev.zudb.spi.ZuBinding.connInTransaction(long)
method public abstract boolean dev.zudb.spi.ZuBinding.connUnregister(long, java.lang.String)
method public abstract boolean dev.zudb.spi.ZuBinding.databaseIsMemory(long)
method public abstract boolean dev.zudb.spi.ZuBinding.valueBoolean(long)
method public abstract byte[] dev.zudb.spi.ZuBinding.valueBytes(long)
method public abstract dev.zudb.Diagnostic dev.zudb.spi.ZuBinding.resultNotice(long, int)
method public abstract dev.zudb.spi.ZuBinding dev.zudb.spi.ZuProvider.load(java.nio.file.Path)
method public abstract double dev.zudb.spi.ZuBinding.valueDouble(long)
Expand All @@ -73,6 +76,7 @@ method public abstract int dev.zudb.spi.ZuBinding.valueType(long)
method public abstract int dev.zudb.spi.ZuProvider.priority()
method public abstract java.lang.String dev.zudb.spi.ZuBinding.appenderColumnName(long, int)
method public abstract java.lang.String dev.zudb.spi.ZuBinding.connRegisteredName(long, long)
method public abstract java.lang.String dev.zudb.spi.ZuBinding.connTableName(long, int)
method public abstract java.lang.String dev.zudb.spi.ZuBinding.databasePath(long)
method public abstract java.lang.String dev.zudb.spi.ZuBinding.resultCellString(long, long, int)
method public abstract java.lang.String dev.zudb.spi.ZuBinding.resultColName(long, int)
Expand Down Expand Up @@ -178,7 +182,11 @@ method public boolean dev.zudb.Row.isNull(int)
method public boolean dev.zudb.Row.isNull(java.lang.String)
method public boolean dev.zudb.Statement.isClosed()
method public boolean dev.zudb.Value$Bool.value()
method public boolean dev.zudb.Value$Bytes.equals(java.lang.Object)
method public boolean dev.zudb.ZuException.retryable()
method public byte[] dev.zudb.Row.getBytes(int)
method public byte[] dev.zudb.Row.getBytes(java.lang.String)
method public byte[] dev.zudb.Value$Bytes.value()
method public dev.zudb.Appender dev.zudb.Appender.append(boolean)
method public dev.zudb.Appender dev.zudb.Appender.append(byte[])
method public dev.zudb.Appender dev.zudb.Appender.append(dev.zudb.Value$Temporal$Kind, long)
Expand Down Expand Up @@ -327,6 +335,7 @@ method public int dev.zudb.Result.columnIndex(java.lang.String)
method public int dev.zudb.Result.columns()
method public int dev.zudb.Status.value()
method public int dev.zudb.Type.value()
method public int dev.zudb.Value$Bytes.hashCode()
method public int dev.zudb.Value$Node.table()
method public int dev.zudb.Value$Rel.table()
method public int dev.zudb.Value$Temporal$Kind.value()
Expand All @@ -335,18 +344,24 @@ method public int dev.zudb.ZuException$Position.column()
method public int dev.zudb.ZuException$Position.line()
method public int dev.zudb.ZuException$Position.offset()
method public java.lang.String dev.zudb.Appender.columnName(int)
method public java.lang.String dev.zudb.Connection.tableName(int)
method public java.lang.String dev.zudb.Database.path()
method public java.lang.String dev.zudb.Diagnostic.code()
method public java.lang.String dev.zudb.Diagnostic.condition()
method public java.lang.String dev.zudb.Diagnostic.docUrl()
method public java.lang.String dev.zudb.Diagnostic.excerpt()
method public java.lang.String dev.zudb.Diagnostic.graph()
method public java.lang.String dev.zudb.Diagnostic.message()
method public java.lang.String dev.zudb.Diagnostic.schema()
method public java.lang.String dev.zudb.Diagnostic.subject()
method public java.lang.String dev.zudb.Diagnostic.subjectKind()
method public java.lang.String dev.zudb.Frame.name()
method public java.lang.String dev.zudb.Result.columnName(int)
method public java.lang.String dev.zudb.Result.gqlstatus()
method public java.lang.String dev.zudb.Row.getString(int)
method public java.lang.String dev.zudb.Row.getString(java.lang.String)
method public java.lang.String dev.zudb.Row.toString()
method public java.lang.String dev.zudb.Value$Bytes.toString()
method public java.lang.String dev.zudb.Value$Field.name()
method public java.lang.String dev.zudb.Value$Str.value()
method public java.nio.ByteBuffer dev.zudb.Chunk.valid(int)
Expand Down Expand Up @@ -378,6 +393,10 @@ method public java.util.Optional<java.lang.String> dev.zudb.ZuException.code()
method public java.util.Optional<java.lang.String> dev.zudb.ZuException.condition()
method public java.util.Optional<java.lang.String> dev.zudb.ZuException.docUrl()
method public java.util.Optional<java.lang.String> dev.zudb.ZuException.excerpt()
method public java.util.Optional<java.lang.String> dev.zudb.ZuException.graph()
method public java.util.Optional<java.lang.String> dev.zudb.ZuException.schema()
method public java.util.Optional<java.lang.String> dev.zudb.ZuException.subject()
method public java.util.Optional<java.lang.String> dev.zudb.ZuException.subjectKind()
method public java.util.Spliterator<dev.zudb.Row> dev.zudb.Result.spliterator()
method public java.util.stream.Stream<dev.zudb.Chunk> dev.zudb.Result.chunks()
method public java.util.stream.Stream<dev.zudb.Row> dev.zudb.Result.stream()
Expand Down Expand Up @@ -419,7 +438,7 @@ method public static dev.zudb.Database dev.zudb.Database.open(java.lang.String,
method public static dev.zudb.Database dev.zudb.Database.open(java.nio.file.Path)
method public static dev.zudb.Database dev.zudb.Database.open(java.nio.file.Path, dev.zudb.Config)
method public static dev.zudb.Diagnostic dev.zudb.Diagnostic.misuse(dev.zudb.Status, java.lang.String)
method public static dev.zudb.Diagnostic dev.zudb.Diagnostic.of(int, java.lang.String, java.lang.String, java.lang.String, int, int, int, int, java.lang.String, java.lang.String, boolean)
method public static dev.zudb.Diagnostic dev.zudb.Diagnostic.of(int, java.lang.String, java.lang.String, java.lang.String, int, int, int, int, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, java.lang.String, boolean)
method public static dev.zudb.Frame dev.zudb.Frame.of(java.lang.String, long)
method public static dev.zudb.Frame dev.zudb.Frame.of(java.lang.String, long, java.lang.Runnable)
method public static dev.zudb.Loader dev.zudb.Loader.create(java.nio.file.Path)
Expand Down Expand Up @@ -464,7 +483,7 @@ method public void dev.zudb.Result.exportArrow(long)
method public void dev.zudb.Result.exportArrow(long, long)
method public void dev.zudb.Statement.close()
type public abstract interface dev.zudb.Progress
type public abstract interface dev.zudb.Value sealed permits dev.zudb.Value$BindingTable, dev.zudb.Value$Bool, dev.zudb.Value$Float, dev.zudb.Value$Graph, dev.zudb.Value$Int, dev.zudb.Value$List, dev.zudb.Value$Node, dev.zudb.Value$Null, dev.zudb.Value$Path, dev.zudb.Value$Record, dev.zudb.Value$Rel, dev.zudb.Value$Str, dev.zudb.Value$Temporal
type public abstract interface dev.zudb.Value sealed permits dev.zudb.Value$BindingTable, dev.zudb.Value$Bool, dev.zudb.Value$Bytes, dev.zudb.Value$Float, dev.zudb.Value$Graph, dev.zudb.Value$Int, dev.zudb.Value$List, dev.zudb.Value$Node, dev.zudb.Value$Null, dev.zudb.Value$Path, dev.zudb.Value$Record, dev.zudb.Value$Rel, dev.zudb.Value$Str, dev.zudb.Value$Temporal
type public abstract interface dev.zudb.spi.ZuBinding
type public abstract interface dev.zudb.spi.ZuProvider
type public class dev.zudb.ZuClosedException extends dev.zudb.ZuProgrammingException
Expand Down Expand Up @@ -497,6 +516,7 @@ type public final record dev.zudb.Diagnostic extends java.lang.Record
type public static final enum dev.zudb.Value$Temporal$Kind extends java.lang.Enum<dev.zudb.Value$Temporal$Kind>
type public static final record dev.zudb.Value$BindingTable extends java.lang.Record implements dev.zudb.Value
type public static final record dev.zudb.Value$Bool extends java.lang.Record implements dev.zudb.Value
type public static final record dev.zudb.Value$Bytes extends java.lang.Record implements dev.zudb.Value
type public static final record dev.zudb.Value$Field extends java.lang.Record
type public static final record dev.zudb.Value$Float extends java.lang.Record implements dev.zudb.Value
type public static final record dev.zudb.Value$Graph extends java.lang.Record implements dev.zudb.Value
Expand Down
13 changes: 13 additions & 0 deletions zudb-ffm/src/main/java/dev/zudb/ffm/Abi.java
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ final class Abi {
final MethodHandle errorPosition;
final MethodHandle errorOffset;
final MethodHandle errorExcerpt;
final MethodHandle errorSubjectKind;
final MethodHandle errorSubject;
final MethodHandle errorGraph;
final MethodHandle errorSchema;
final MethodHandle errorFree;

final MethodHandle configSet;
Expand All @@ -73,6 +77,7 @@ final class Abi {
final MethodHandle connClose;
final MethodHandle connInterrupt;
final MethodHandle connRowsRead;
final MethodHandle connTableName;
final MethodHandle connSetProgress;
final MethodHandle connInTransaction;
final MethodHandle begin;
Expand Down Expand Up @@ -160,6 +165,7 @@ final class Abi {
final MethodHandle valueI64;
final MethodHandle valueF64;
final MethodHandle valueStr;
final MethodHandle valueBytes;
final MethodHandle valueTemporal;
final MethodHandle valueNode;
final MethodHandle valueRel;
Expand Down Expand Up @@ -193,6 +199,10 @@ final class Abi {
errorPosition = h("zu_error_position", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS));
errorOffset = h("zu_error_offset", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS));
errorExcerpt = h("zu_error_excerpt", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS));
errorSubjectKind = h("zu_error_subject_kind", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS));
errorSubject = h("zu_error_subject", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS));
errorGraph = h("zu_error_graph", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS));
errorSchema = h("zu_error_schema", FunctionDescriptor.of(ADDRESS, ADDRESS, ADDRESS));
errorFree = h("zu_error_free", FunctionDescriptor.ofVoid(ADDRESS));

configSet =
Expand All @@ -216,6 +226,8 @@ final class Abi {
connClose = h("zu_conn_close", FunctionDescriptor.ofVoid(ADDRESS));
connInterrupt = h("zu_conn_interrupt", FunctionDescriptor.of(JAVA_INT, ADDRESS));
connRowsRead = h("zu_conn_rows_read", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS));
connTableName = h("zu_conn_table_name", FunctionDescriptor.of(
ADDRESS, ADDRESS, JAVA_INT, ADDRESS));
connSetProgress = h("zu_conn_set_progress", FunctionDescriptor.of(
JAVA_INT, ADDRESS, ADDRESS, ADDRESS, JAVA_LONG));
connInTransaction = h("zu_conn_in_transaction", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS));
Expand Down Expand Up @@ -423,6 +435,7 @@ final class Abi {
valueI64 = h("zu_value_i64", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS));
valueF64 = h("zu_value_f64", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS));
valueStr = h("zu_value_str", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS));
valueBytes = h("zu_value_bytes", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS));
valueTemporal =
h("zu_value_temporal", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS, ADDRESS));
valueNode = h("zu_value_node", FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS));
Expand Down
50 changes: 43 additions & 7 deletions zudb-ffm/src/main/java/dev/zudb/ffm/FfmBinding.java
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,19 @@ public long connRowsRead(long conn) {
}
}

@Override
public String connTableName(long conn, int table) {
Scratch s = Scratch.get();
MemorySegment sl = s.slots();
try {
MemorySegment out =
(MemorySegment) abi.connTableName.invokeExact(ptr(conn), table, sl.asSlice(LEN, 8));
return utf8(out.address(), sl.get(JAVA_LONG, LEN));
} catch (Throwable t) {
throw fail("zu_conn_table_name", t);
}
}

@Override
public void connSetProgress(long conn, dev.zudb.Progress watcher, long intervalMillis) {
if (watcher == null) {
Expand Down Expand Up @@ -697,6 +710,20 @@ public String valueString(long value) {
}
}

@Override
public byte[] valueBytes(long value) {
Scratch s = Scratch.get();
MemorySegment sl = s.slots();
try {
int st = (int) abi.valueBytes.invokeExact(ptr(value), sl.asSlice(OUT, 8), sl.asSlice(LEN, 8));
check("zu_value_bytes", st, null);
byte[] octets = octets(sl.get(ADDRESS, OUT).address(), sl.get(JAVA_LONG, LEN));
return octets == null ? new byte[0] : octets;
} catch (Throwable t) {
throw fail("zu_value_bytes", t);
}
}

@Override
public long[] valueTemporal(long value) {
Scratch s = Scratch.get();
Expand Down Expand Up @@ -1600,6 +1627,10 @@ private Diagnostic diagnostic(long err) {
String condition = text(abi.errorStandardText, e);
String docUrl = text(abi.errorDocUrl, e);
String excerpt = text(abi.errorExcerpt, e);
String subjectKind = text(abi.errorSubjectKind, e);
String subject = text(abi.errorSubject, e);
String graph = text(abi.errorGraph, e);
String schema = text(abi.errorSchema, e);
int line = -1;
int column = -1;
int offset = -1;
Expand All @@ -1612,8 +1643,8 @@ private Diagnostic diagnostic(long err) {
offset = sl.get(JAVA_INT, C);
}
return Diagnostic.of(
status, message, code, condition, severity, line, column, offset, excerpt, docUrl,
retryable == 1);
status, message, code, condition, severity, line, column, offset, excerpt,
subjectKind, subject, graph, schema, docUrl, retryable == 1);
} catch (Throwable t) {
throw fail("zu_error", t);
} finally {
Expand All @@ -1633,15 +1664,20 @@ private static String text(java.lang.invoke.MethodHandle mh, MemorySegment e) th
}

private static String utf8(long address, long length) {
byte[] bytes = octets(address, length);
return bytes == null ? null : new String(bytes, StandardCharsets.UTF_8);
}

/** A run of native bytes, copied out, or null for a null pointer. */
private static byte[] octets(long address, long length) {
if (address == 0) {
return null;
}
if (length == 0) {
return "";
}
byte[] bytes = new byte[(int) length];
MemorySegment.copy(reinterpret(address, length), JAVA_BYTE, 0, bytes, 0, bytes.length);
return new String(bytes, StandardCharsets.UTF_8);
if (length > 0) {
MemorySegment.copy(reinterpret(address, length), JAVA_BYTE, 0, bytes, 0, bytes.length);
}
return bytes;
}

@SuppressWarnings("restricted")
Expand Down
Loading
Loading