+#endif
+
+/* ------------------------------------------------------------------ */
+/* the library */
+/* ------------------------------------------------------------------ */
+
+/*
+ * Every call this client makes, as a pointer resolved once. The list is
+ * the whole of what the shim depends on, so a library missing any of it
+ * is refused by name at load rather than found by a segfault later.
+ */
+#define ZU_SYMBOLS(X) \
+ X(version, const char *, (void)) \
+ X(error_status, zu_status, (const zu_error *)) \
+ X(error_message, const char *, (const zu_error *, size_t *)) \
+ X(error_code, const char *, (const zu_error *, size_t *)) \
+ X(error_standard_text, const char *, (const zu_error *, size_t *)) \
+ X(error_doc_url, const char *, (const zu_error *, size_t *)) \
+ X(error_severity, int32_t, (const zu_error *)) \
+ X(error_retryable, int32_t, (const zu_error *)) \
+ X(error_position, zu_status, (const zu_error *, uint32_t *, uint32_t *)) \
+ X(error_offset, zu_status, (const zu_error *, uint32_t *)) \
+ X(error_excerpt, const char *, (const zu_error *, size_t *)) \
+ X(error_free, void, (zu_error *)) \
+ X(config_set, zu_status, \
+ (zu_config *, const char *, size_t, const char *, size_t, zu_error **)) \
+ X(database_open, zu_status, \
+ (const char *, size_t, const zu_config *, zu_database **, zu_error **)) \
+ X(database_create, zu_status, \
+ (const char *, size_t, const zu_config *, zu_database **, zu_error **)) \
+ X(database_memory, zu_status, (const zu_config *, zu_database **, zu_error **)) \
+ X(database_is_memory, zu_status, (const zu_database *)) \
+ X(database_path, zu_status, (const zu_database *, const char **, size_t *)) \
+ X(database_close, void, (zu_database *)) \
+ X(connect, zu_status, (zu_database *, zu_conn **, zu_error **)) \
+ X(open, zu_status, (const char *, size_t, zu_conn **, zu_error **)) \
+ X(create, zu_status, (const char *, size_t, zu_conn **, zu_error **)) \
+ X(memory, zu_status, (zu_conn **, zu_error **)) \
+ X(conn_duplicate, zu_status, (zu_conn *, zu_conn **, zu_error **)) \
+ X(conn_close, void, (zu_conn *)) \
+ X(conn_interrupt, zu_status, (zu_conn *)) \
+ X(conn_rows_read, zu_status, (zu_conn *, uint64_t *)) \
+ X(conn_set_progress, zu_status, (zu_conn *, zu_progress_fn, void *, uint64_t)) \
+ X(query, zu_status, (zu_conn *, const char *, size_t, zu_result **, zu_error **)) \
+ X(prepare, zu_status, (zu_conn *, const char *, size_t, zu_stmt **, zu_error **)) \
+ X(bind_i64, zu_status, (zu_stmt *, const char *, size_t, int64_t)) \
+ X(bind_f64, zu_status, (zu_stmt *, const char *, size_t, double)) \
+ X(bind_bool, zu_status, (zu_stmt *, const char *, size_t, int)) \
+ X(bind_str, zu_status, (zu_stmt *, const char *, size_t, const char *, size_t)) \
+ X(bind_temporal, zu_status, (zu_stmt *, const char *, size_t, int32_t, int64_t, int32_t)) \
+ X(bind_null, zu_status, (zu_stmt *, const char *, size_t)) \
+ X(execute, zu_status, (zu_stmt *, zu_result **, zu_error **)) \
+ X(stmt_close, void, (zu_stmt *)) \
+ X(begin, zu_status, (zu_conn *, int32_t, zu_error **)) \
+ X(commit, zu_status, (zu_conn *, zu_error **)) \
+ X(rollback, zu_status, (zu_conn *, zu_error **)) \
+ X(conn_in_transaction, zu_status, (zu_conn *, int32_t *)) \
+ X(result_rows, uint64_t, (const zu_result *)) \
+ X(result_cols, uint32_t, (const zu_result *)) \
+ X(result_col_name, zu_status, (const zu_result *, uint32_t, const char **, size_t *)) \
+ X(result_cell_type, zu_status, (const zu_result *, uint64_t, uint32_t, int32_t *)) \
+ X(result_col_i64, zu_status, (zu_result *, uint32_t, const int64_t **)) \
+ X(result_col_f64, zu_status, (zu_result *, uint32_t, const double **)) \
+ X(result_col_node_offset, zu_status, (zu_result *, uint32_t, const uint64_t **)) \
+ X(result_col_valid, zu_status, (zu_result *, uint32_t, const uint8_t **)) \
+ X(result_chunk_count, uint64_t, (const zu_result *)) \
+ X(result_chunk, zu_status, (const zu_result *, uint64_t, uint64_t *, uint64_t *)) \
+ X(result_chunk_col_i64, zu_status, (zu_result *, uint64_t, uint32_t, const int64_t **)) \
+ X(result_chunk_col_f64, zu_status, (zu_result *, uint64_t, uint32_t, const double **)) \
+ X(result_chunk_col_node_offset, zu_status, \
+ (zu_result *, uint64_t, uint32_t, const uint64_t **)) \
+ X(result_chunk_col_valid, zu_status, (zu_result *, uint64_t, uint32_t, const uint8_t **)) \
+ X(result_cell_str, zu_status, (zu_result *, uint64_t, uint32_t, const char **, size_t *)) \
+ X(result_cell, zu_status, (const zu_result *, uint64_t, uint32_t, const zu_value **)) \
+ X(result_free, void, (zu_result *)) \
+ X(result_arrow, zu_status, \
+ (zu_conn *, zu_result **, uint64_t, struct ArrowArrayStream *, zu_error **)) \
+ X(result_gqlstatus, const char *, (zu_result *, size_t *)) \
+ X(result_notices, uint32_t, (zu_result *)) \
+ X(result_notice, zu_status, (zu_result *, uint32_t, zu_error **)) \
+ X(value_type, int32_t, (const zu_value *)) \
+ X(value_bool, zu_status, (const zu_value *, int32_t *)) \
+ X(value_i64, zu_status, (const zu_value *, int64_t *)) \
+ X(value_f64, zu_status, (const zu_value *, double *)) \
+ X(value_str, zu_status, (const zu_value *, const char **, size_t *)) \
+ X(value_temporal, zu_status, (const zu_value *, int32_t *, int64_t *, int32_t *)) \
+ X(value_node, zu_status, (const zu_value *, uint32_t *, uint64_t *)) \
+ X(value_rel, zu_status, (const zu_value *, uint32_t *, uint64_t *, uint64_t *)) \
+ X(value_len, uint64_t, (const zu_value *)) \
+ X(value_at, zu_status, (const zu_value *, uint64_t, const zu_value **)) \
+ X(value_field, zu_status, (const zu_value *, uint64_t, const char **, size_t *)) \
+ X(loader_create, zu_status, (const char *, size_t, zu_loader **, zu_error **)) \
+ X(loader_table, zu_status, \
+ (zu_loader *, const char *, size_t, const char *, size_t, uint64_t, zu_error **)) \
+ X(loader_edges, zu_status, \
+ (zu_loader *, const uint32_t *, const uint32_t *, uint64_t, zu_error **)) \
+ X(loader_col_i64, zu_status, \
+ (zu_loader *, const char *, size_t, const int64_t *, uint64_t, zu_error **)) \
+ X(loader_col_f64, zu_status, \
+ (zu_loader *, const char *, size_t, const double *, uint64_t, zu_error **)) \
+ X(loader_col_bool, zu_status, \
+ (zu_loader *, const char *, size_t, const int32_t *, uint64_t, zu_error **)) \
+ X(loader_col_str, zu_status, \
+ (zu_loader *, const char *, size_t, const char *const *, const size_t *, uint64_t, \
+ zu_error **)) \
+ X(loader_col_temporal, zu_status, \
+ (zu_loader *, const char *, size_t, int32_t, const int64_t *, uint64_t, zu_error **)) \
+ X(loader_finish, zu_status, (zu_loader *, zu_error **)) \
+ X(loader_free, void, (zu_loader *)) \
+ X(appender_open, zu_status, \
+ (zu_conn *, const char *, size_t, zu_appender **, zu_error **)) \
+ X(append_bool, zu_status, (zu_appender *, int32_t, zu_error **)) \
+ X(append_i64, zu_status, (zu_appender *, int64_t, zu_error **)) \
+ X(append_f64, zu_status, (zu_appender *, double, zu_error **)) \
+ X(append_str, zu_status, (zu_appender *, const char *, size_t, zu_error **)) \
+ X(append_bytes, zu_status, (zu_appender *, const uint8_t *, size_t, zu_error **)) \
+ X(append_temporal, zu_status, (zu_appender *, int32_t, int64_t, zu_error **)) \
+ X(append_end_row, zu_status, (zu_appender *, zu_error **)) \
+ X(appender_flush, zu_status, (zu_appender *, zu_error **)) \
+ X(appender_buffered, zu_status, (zu_appender *, uint64_t *)) \
+ X(appender_committed, zu_status, (zu_appender *, uint64_t *)) \
+ X(appender_cols, zu_status, (zu_appender *, uint32_t *)) \
+ X(appender_col_name, const char *, (zu_appender *, uint32_t, size_t *)) \
+ X(appender_discard, zu_status, (zu_appender *, uint64_t *)) \
+ X(appender_close, zu_status, (zu_appender *, uint64_t *, zu_error **)) \
+ X(appender_free, void, (zu_appender *)) \
+ X(frame_new, zu_status, \
+ (const char *, size_t, uint64_t, void *, void (*)(void *), zu_frame **, zu_error **)) \
+ X(frame_col_int, zu_status, \
+ (zu_frame *, const char *, size_t, const void *, uint64_t, int32_t, int32_t, int64_t, \
+ int32_t, zu_error **)) \
+ X(frame_col_float, zu_status, \
+ (zu_frame *, const char *, size_t, const void *, uint64_t, int32_t, zu_error **)) \
+ X(frame_col_bool, zu_status, \
+ (zu_frame *, const char *, size_t, const void *, uint64_t, zu_error **)) \
+ X(frame_col_str, zu_status, \
+ (zu_frame *, const char *, size_t, const void *, int32_t, const void *, size_t, \
+ uint64_t, zu_error **)) \
+ X(frame_col_view, zu_status, \
+ (zu_frame *, const char *, size_t, const void *, const void *const *, const size_t *, \
+ size_t, uint64_t, zu_error **)) \
+ X(frame_free, void, (zu_frame *)) \
+ X(conn_register, zu_status, (zu_conn *, zu_frame *, zu_error **)) \
+ X(conn_unregister, zu_status, (zu_conn *, const char *, size_t, int32_t *, zu_error **)) \
+ X(conn_registered_count, zu_status, (zu_conn *, uint64_t *)) \
+ X(conn_registered_name, const char *, (zu_conn *, uint64_t, size_t *))
+
+#define ZU_DECLARE(name, ret, params) static ret (*p_##name) params;
+ZU_SYMBOLS(ZU_DECLARE)
+#undef ZU_DECLARE
+
+/* ------------------------------------------------------------------ */
+/* what this side remembers */
+/* ------------------------------------------------------------------ */
+
+static JavaVM *vm;
+static jclass c_binding; /* global ref, for the two static helpers */
+static jmethodID m_diagnostic;
+static jmethodID m_misuse;
+static jmethodID m_to_exception;
+static jmethodID m_progress_at;
+static jmethodID m_runnable_run;
+
+#define H(type, handle) ((type *)(intptr_t)(handle))
+#define A(handle) ((jlong)(intptr_t)(handle))
+
+/* ------------------------------------------------------------------ */
+/* threads */
+/* ------------------------------------------------------------------ */
+
+/*
+ * A callback arrives on a thread of the engine's, which the JVM has
+ * never seen. Attaching for the call and detaching after it is not the
+ * cheapest arrangement, but it is the one that is correct when the
+ * engine's pool retires a worker: a thread that exits while still
+ * attached takes the process with it, and nothing here is told when a
+ * worker goes.
+ */
+static JNIEnv *attach(int *attached) {
+ void *env = NULL;
+ *attached = 0;
+ if ((*vm)->GetEnv(vm, &env, JNI_VERSION_1_8) == JNI_OK) {
+ return (JNIEnv *)env;
+ }
+ if ((*vm)->AttachCurrentThreadAsDaemon(vm, &env, NULL) != JNI_OK) {
+ return NULL;
+ }
+ *attached = 1;
+ return (JNIEnv *)env;
+}
+
+static void detach(int attached) {
+ if (attached) {
+ (*vm)->DetachCurrentThread(vm);
+ }
+}
+
+/* ------------------------------------------------------------------ */
+/* strings and arrays */
+/* ------------------------------------------------------------------ */
+
+/* A byte array borrowed for the length of one call. */
+typedef struct {
+ jbyteArray array;
+ jbyte *data;
+ jsize length;
+} borrowed;
+
+static int borrow(JNIEnv *env, jbyteArray array, borrowed *out) {
+ out->array = array;
+ out->data = NULL;
+ out->length = 0;
+ if (array == NULL) {
+ return 1;
+ }
+ out->data = (*env)->GetByteArrayElements(env, array, NULL);
+ if (out->data == NULL) {
+ return 0; /* the out of memory error is already pending */
+ }
+ out->length = (*env)->GetArrayLength(env, array);
+ return 1;
+}
+
+static void giveback(JNIEnv *env, borrowed *b) {
+ if (b->data != NULL) {
+ (*env)->ReleaseByteArrayElements(env, b->array, b->data, JNI_ABORT);
+ }
+}
+
+#define S(b) ((const char *)(b).data)
+#define L(b) ((size_t)(b).length)
+
+/* A run of bytes as a Java array, or null for a pointer that is null. */
+static jbyteArray bytes(JNIEnv *env, const char *p, size_t len) {
+ jbyteArray a;
+ if (p == NULL) {
+ return NULL;
+ }
+ if (len > (size_t)INT32_MAX) {
+ len = (size_t)INT32_MAX;
+ }
+ a = (*env)->NewByteArray(env, (jsize)len);
+ if (a == NULL) {
+ return NULL;
+ }
+ if (len > 0) {
+ (*env)->SetByteArrayRegion(env, a, 0, (jsize)len, (const jbyte *)p);
+ }
+ return a;
+}
+
+/* The same for a NUL terminated string, which is what zu_version is. */
+static jbyteArray cstring(JNIEnv *env, const char *p) {
+ return p == NULL ? NULL : bytes(env, p, strlen(p));
+}
+
+static jlongArray longs(JNIEnv *env, const jlong *values, jsize count) {
+ jlongArray a = (*env)->NewLongArray(env, count);
+ if (a != NULL) {
+ (*env)->SetLongArrayRegion(env, a, 0, count, values);
+ }
+ return a;
+}
+
+/*
+ * The address of a direct buffer, which is where the Java side has
+ * already put whatever the engine is about to read. A buffer that is
+ * not direct never reaches here: the Java side either copies it into
+ * one, for a call that reads and is done, or refuses it, for a frame
+ * that keeps the pointer.
+ */
+static void *address(JNIEnv *env, jobject buffer) {
+ return buffer == NULL ? NULL : (*env)->GetDirectBufferAddress(env, buffer);
+}
+
+/* ------------------------------------------------------------------ */
+/* failures */
+/* ------------------------------------------------------------------ */
+
+/* One of the const char * accessors on a zu_error, as a Java array. */
+static jbyteArray field(JNIEnv *env, const zu_error *e,
+ const char *(*get)(const zu_error *, size_t *)) {
+ size_t len = 0;
+ const char *p = get(e, &len);
+ return bytes(env, p, len);
+}
+
+/*
+ * Turns what the ABI answered into the exception it names and throws
+ * it, then frees the error. The record is built in Java because that is
+ * where the mapping from a GQLSTATUS class to an exception class lives,
+ * and it lives in one place so that the two providers cannot come to
+ * differ about it.
+ */
+static void raise(JNIEnv *env, zu_status status, zu_error *e, const char *what) {
+ jobject record;
+ jobject thrown;
+ uint32_t line = 0;
+ uint32_t column = 0;
+ uint32_t offset = 0;
+ jint jline = -1;
+ jint jcolumn = -1;
+ jint joffset = -1;
+
+ if (e == NULL) {
+ jstring name = (*env)->NewStringUTF(env, what);
+ if (name == NULL) {
+ return;
+ }
+ thrown = (*env)->CallStaticObjectMethod(env, c_binding, m_misuse, (jint)status, name);
+ (*env)->DeleteLocalRef(env, name);
+ if (thrown != NULL) {
+ (*env)->Throw(env, (jthrowable)thrown);
+ }
+ return;
+ }
+
+ if (p_error_position(e, &line, &column) == ZU_OK) {
+ jline = (jint)line;
+ jcolumn = (jint)column;
+ }
+ if (p_error_offset(e, &offset) == ZU_OK) {
+ joffset = (jint)offset;
+ }
+
+ record = (*env)->CallStaticObjectMethod(
+ env, c_binding, m_diagnostic, (jint)p_error_status(e), field(env, e, p_error_message),
+ field(env, e, p_error_code), field(env, e, p_error_standard_text),
+ (jint)p_error_severity(e), jline, jcolumn, joffset, field(env, e, p_error_excerpt),
+ field(env, e, p_error_doc_url), p_error_retryable(e) == 1 ? JNI_TRUE : JNI_FALSE);
+ p_error_free(e);
+ if (record == NULL) {
+ return; /* something is already pending, and it is the truer failure */
+ }
+ thrown = (*env)->CallObjectMethod(env, record, m_to_exception);
+ if (thrown != NULL) {
+ (*env)->Throw(env, (jthrowable)thrown);
+ }
+}
+
+/* The record a notice is, which is the same reading without the throw. */
+static jobject record(JNIEnv *env, zu_error *e) {
+ jobject out;
+ uint32_t line = 0;
+ uint32_t column = 0;
+ uint32_t offset = 0;
+ jint jline = -1;
+ jint jcolumn = -1;
+ jint joffset = -1;
+
+ if (p_error_position(e, &line, &column) == ZU_OK) {
+ jline = (jint)line;
+ jcolumn = (jint)column;
+ }
+ if (p_error_offset(e, &offset) == ZU_OK) {
+ joffset = (jint)offset;
+ }
+ out = (*env)->CallStaticObjectMethod(
+ env, c_binding, m_diagnostic, (jint)p_error_status(e), field(env, e, p_error_message),
+ field(env, e, p_error_code), field(env, e, p_error_standard_text),
+ (jint)p_error_severity(e), jline, jcolumn, joffset, field(env, e, p_error_excerpt),
+ field(env, e, p_error_doc_url), p_error_retryable(e) == 1 ? JNI_TRUE : JNI_FALSE);
+ p_error_free(e);
+ return out;
+}
+
+/* A status that is not OK and carries no error of its own. */
+#define FAIL_IF(env, st, err, what) \
+ do { \
+ if ((st) != ZU_OK) { \
+ raise((env), (st), (err), (what)); \
+ return; \
+ } \
+ } while (0)
+
+#define FAIL_IF_V(env, st, err, what, value) \
+ do { \
+ if ((st) != ZU_OK) { \
+ raise((env), (st), (err), (what)); \
+ return (value); \
+ } \
+ } while (0)
+
+/* ------------------------------------------------------------------ */
+/* loading */
+/* ------------------------------------------------------------------ */
+
+#ifdef _WIN32
+typedef HMODULE handle;
+static handle lib_open(const char *path) {
+ int wide = MultiByteToWideChar(CP_UTF8, 0, path, -1, NULL, 0);
+ wchar_t *w;
+ handle h;
+ if (wide <= 0) {
+ return NULL;
+ }
+ w = (wchar_t *)calloc((size_t)wide, sizeof(wchar_t));
+ if (w == NULL) {
+ return NULL;
+ }
+ MultiByteToWideChar(CP_UTF8, 0, path, -1, w, wide);
+ h = LoadLibraryW(w);
+ free(w);
+ return h;
+}
+static void *lib_sym(handle h, const char *name) {
+ return (void *)(intptr_t)GetProcAddress(h, name);
+}
+#else
+typedef void *handle;
+static handle lib_open(const char *path) { return dlopen(path, RTLD_NOW | RTLD_LOCAL); }
+static void *lib_sym(handle h, const char *name) { return dlsym(h, name); }
+#endif
+
+static handle library;
+
+/*
+ * Opens libzu and binds every call in it.
+ *
+ * Answers null for a library that opened and had everything, and
+ * otherwise a sentence saying which of the two went wrong. A provider
+ * turns that into ProviderUnavailableException, which is not a failure
+ * of the program but a fact about this machine.
+ */
+static jbyteArray n_load(JNIEnv *env, jclass self, jbyteArray path) {
+ borrowed p;
+ char *copy;
+ handle h;
+ (void)self;
+
+ if (library != NULL) {
+ return NULL; /* already bound, and a second library would be a second engine */
+ }
+ if (!borrow(env, path, &p)) {
+ return NULL;
+ }
+ copy = (char *)malloc(L(p) + 1);
+ if (copy == NULL) {
+ giveback(env, &p);
+ return cstring(env, "out of memory");
+ }
+ memcpy(copy, S(p), L(p));
+ copy[L(p)] = '\0';
+ giveback(env, &p);
+
+ h = lib_open(copy);
+ if (h == NULL) {
+ char message[1024];
+#ifdef _WIN32
+ snprintf(message, sizeof message, "%s did not load (error %lu)", copy,
+ (unsigned long)GetLastError());
+#else
+ const char *why = dlerror();
+ snprintf(message, sizeof message, "%s did not load: %s", copy, why == NULL ? "" : why);
+#endif
+ free(copy);
+ return cstring(env, message);
+ }
+ free(copy);
+
+#define ZU_BIND(name, ret, params) \
+ p_##name = (ret(*) params)lib_sym(h, "zu_" #name); \
+ if (p_##name == NULL) { \
+ return cstring(env, "this library has no zu_" #name \
+ ", so it is not a zu of this ABI"); \
+ }
+ ZU_SYMBOLS(ZU_BIND)
+#undef ZU_BIND
+
+ library = h;
+ return NULL;
+}
+
+/* ------------------------------------------------------------------ */
+/* the calls */
+/* ------------------------------------------------------------------ */
+
+static jbyteArray n_version(JNIEnv *env, jclass self) {
+ (void)self;
+ return cstring(env, p_version());
+}
+
+static void config(zu_config *cfg, jlong memory, jlong threads, jboolean read_only) {
+ memset(cfg, 0, sizeof *cfg);
+ cfg->struct_size = sizeof *cfg;
+ cfg->memory_limit = (size_t)memory;
+ cfg->threads = (size_t)threads;
+ cfg->read_only = read_only == JNI_TRUE ? 1 : 0;
+}
+
+static jlongArray n_config_set(JNIEnv *env, jclass self, jlong memory, jlong threads,
+ jboolean read_only, jbyteArray key, jbyteArray value) {
+ zu_config cfg;
+ zu_error *e = NULL;
+ zu_status st;
+ borrowed k;
+ borrowed v;
+ jlong out[3];
+ (void)self;
+
+ if (!borrow(env, key, &k)) {
+ return NULL;
+ }
+ if (!borrow(env, value, &v)) {
+ giveback(env, &k);
+ return NULL;
+ }
+ config(&cfg, memory, threads, read_only);
+ st = p_config_set(&cfg, S(k), L(k), S(v), L(v), &e);
+ giveback(env, &k);
+ giveback(env, &v);
+ FAIL_IF_V(env, st, e, "zu_config_set", NULL);
+ out[0] = (jlong)cfg.memory_limit;
+ out[1] = (jlong)cfg.threads;
+ out[2] = (jlong)cfg.read_only;
+ return longs(env, out, 3);
+}
+
+static jlong open_or_create(JNIEnv *env, jbyteArray path, jlong memory, jlong threads,
+ jboolean read_only, int creating) {
+ zu_config cfg;
+ zu_database *db = NULL;
+ zu_error *e = NULL;
+ zu_status st;
+ borrowed p;
+
+ if (!borrow(env, path, &p)) {
+ return 0;
+ }
+ config(&cfg, memory, threads, read_only);
+ st = creating ? p_database_create(S(p), L(p), &cfg, &db, &e)
+ : p_database_open(S(p), L(p), &cfg, &db, &e);
+ giveback(env, &p);
+ FAIL_IF_V(env, st, e, creating ? "zu_database_create" : "zu_database_open", 0);
+ return A(db);
+}
+
+static jlong n_database_open(JNIEnv *env, jclass self, jbyteArray path, jlong memory,
+ jlong threads, jboolean read_only) {
+ (void)self;
+ return open_or_create(env, path, memory, threads, read_only, 0);
+}
+
+static jlong n_database_create(JNIEnv *env, jclass self, jbyteArray path, jlong memory,
+ jlong threads, jboolean read_only) {
+ (void)self;
+ return open_or_create(env, path, memory, threads, read_only, 1);
+}
+
+static jlong n_database_memory(JNIEnv *env, jclass self, jlong memory, jlong threads,
+ jboolean read_only) {
+ zu_config cfg;
+ zu_database *db = NULL;
+ zu_error *e = NULL;
+ zu_status st;
+ (void)self;
+
+ config(&cfg, memory, threads, read_only);
+ st = p_database_memory(&cfg, &db, &e);
+ FAIL_IF_V(env, st, e, "zu_database_memory", 0);
+ return A(db);
+}
+
+static jboolean n_database_is_memory(JNIEnv *env, jclass self, jlong db) {
+ (void)env;
+ (void)self;
+ return p_database_is_memory(H(zu_database, db)) == ZU_OK ? JNI_TRUE : JNI_FALSE;
+}
+
+static jbyteArray n_database_path(JNIEnv *env, jclass self, jlong db) {
+ const char *p = NULL;
+ size_t len = 0;
+ zu_status st = p_database_path(H(zu_database, db), &p, &len);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_database_path", NULL);
+ return bytes(env, p, len);
+}
+
+static void n_database_close(JNIEnv *env, jclass self, jlong db) {
+ (void)env;
+ (void)self;
+ p_database_close(H(zu_database, db));
+}
+
+static jlong n_connect(JNIEnv *env, jclass self, jlong db) {
+ zu_conn *conn = NULL;
+ zu_error *e = NULL;
+ zu_status st = p_connect(H(zu_database, db), &conn, &e);
+ (void)self;
+ FAIL_IF_V(env, st, e, "zu_connect", 0);
+ return A(conn);
+}
+
+static jlong path_conn(JNIEnv *env, jbyteArray path, int creating) {
+ zu_conn *conn = NULL;
+ zu_error *e = NULL;
+ zu_status st;
+ borrowed p;
+
+ if (!borrow(env, path, &p)) {
+ return 0;
+ }
+ st = creating ? p_create(S(p), L(p), &conn, &e) : p_open(S(p), L(p), &conn, &e);
+ giveback(env, &p);
+ FAIL_IF_V(env, st, e, creating ? "zu_create" : "zu_open", 0);
+ return A(conn);
+}
+
+static jlong n_open(JNIEnv *env, jclass self, jbyteArray path) {
+ (void)self;
+ return path_conn(env, path, 0);
+}
+
+static jlong n_create(JNIEnv *env, jclass self, jbyteArray path) {
+ (void)self;
+ return path_conn(env, path, 1);
+}
+
+static jlong n_memory(JNIEnv *env, jclass self) {
+ zu_conn *conn = NULL;
+ zu_error *e = NULL;
+ zu_status st = p_memory(&conn, &e);
+ (void)self;
+ FAIL_IF_V(env, st, e, "zu_memory", 0);
+ return A(conn);
+}
+
+static jlong n_conn_duplicate(JNIEnv *env, jclass self, jlong conn) {
+ zu_conn *out = NULL;
+ zu_error *e = NULL;
+ zu_status st = p_conn_duplicate(H(zu_conn, conn), &out, &e);
+ (void)self;
+ FAIL_IF_V(env, st, e, "zu_conn_duplicate", 0);
+ return A(out);
+}
+
+static void n_conn_close(JNIEnv *env, jclass self, jlong conn) {
+ (void)env;
+ (void)self;
+ p_conn_close(H(zu_conn, conn));
+}
+
+static void n_conn_interrupt(JNIEnv *env, jclass self, jlong conn) {
+ zu_status st = p_conn_interrupt(H(zu_conn, conn));
+ (void)self;
+ FAIL_IF(env, st, NULL, "zu_conn_interrupt");
+}
+
+static jlong n_conn_rows_read(JNIEnv *env, jclass self, jlong conn) {
+ uint64_t out = 0;
+ zu_status st = p_conn_rows_read(H(zu_conn, conn), &out);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_conn_rows_read", 0);
+ return (jlong)out;
+}
+
+/* ---- the progress callback ---- */
+
+/*
+ * What a watcher is on this side: one global reference, which the
+ * engine holds as its user_data and this shim frees when the connection
+ * stops watching or closes. It is a heap cell rather than the reference
+ * itself because a jobject is not a pointer on every JVM.
+ */
+typedef struct {
+ jobject watcher;
+} watch;
+
+static int on_progress(void *user_data, uint64_t rows, uint64_t ms) {
+ watch *w = (watch *)user_data;
+ int attached = 0;
+ JNIEnv *env = attach(&attached);
+ jboolean go;
+
+ if (env == NULL) {
+ return 0; /* nothing can be asked, so stop rather than run on unwatched */
+ }
+ go = (*env)->CallBooleanMethod(env, w->watcher, m_progress_at, (jlong)rows, (jlong)ms);
+ /*
+ * Nothing may be thrown out of a callback the engine is inside of. A
+ * watcher that threw is a program that has stopped wanting the
+ * answer, so the exception is cleared and described, and the
+ * statement is stopped, which is the reading that throws least away.
+ */
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionDescribe(env);
+ (*env)->ExceptionClear(env);
+ go = JNI_FALSE;
+ }
+ detach(attached);
+ return go == JNI_TRUE ? 1 : 0;
+}
+
+static jlong n_conn_set_progress(JNIEnv *env, jclass self, jlong conn, jobject watcher,
+ jlong interval) {
+ watch *w = NULL;
+ zu_status st;
+ (void)self;
+
+ if (watcher != NULL) {
+ w = (watch *)calloc(1, sizeof *w);
+ if (w == NULL) {
+ raise(env, ZU_ERROR, NULL, "zu_conn_set_progress");
+ return 0;
+ }
+ w->watcher = (*env)->NewGlobalRef(env, watcher);
+ if (w->watcher == NULL) {
+ free(w);
+ return 0;
+ }
+ }
+ st = p_conn_set_progress(H(zu_conn, conn), w == NULL ? NULL : on_progress, w,
+ (uint64_t)interval);
+ if (st != ZU_OK) {
+ if (w != NULL) {
+ (*env)->DeleteGlobalRef(env, w->watcher);
+ free(w);
+ }
+ raise(env, st, NULL, "zu_conn_set_progress");
+ return 0;
+ }
+ return A(w);
+}
+
+/* Says nothing will call this watcher again, which is what taking the
+ arrangement back means. */
+static void n_watch_free(JNIEnv *env, jclass self, jlong cookie) {
+ watch *w = H(watch, cookie);
+ (void)self;
+ if (w != NULL) {
+ (*env)->DeleteGlobalRef(env, w->watcher);
+ free(w);
+ }
+}
+
+static jboolean n_conn_in_transaction(JNIEnv *env, jclass self, jlong conn) {
+ int32_t out = 0;
+ zu_status st = p_conn_in_transaction(H(zu_conn, conn), &out);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_conn_in_transaction", JNI_FALSE);
+ return out != 0 ? JNI_TRUE : JNI_FALSE;
+}
+
+static void n_begin(JNIEnv *env, jclass self, jlong conn, jboolean read_only) {
+ zu_error *e = NULL;
+ zu_status st = p_begin(H(zu_conn, conn), read_only == JNI_TRUE ? 1 : 0, &e);
+ (void)self;
+ FAIL_IF(env, st, e, "zu_begin");
+}
+
+static void n_commit(JNIEnv *env, jclass self, jlong conn) {
+ zu_error *e = NULL;
+ zu_status st = p_commit(H(zu_conn, conn), &e);
+ (void)self;
+ FAIL_IF(env, st, e, "zu_commit");
+}
+
+static void n_rollback(JNIEnv *env, jclass self, jlong conn) {
+ zu_error *e = NULL;
+ zu_status st = p_rollback(H(zu_conn, conn), &e);
+ (void)self;
+ FAIL_IF(env, st, e, "zu_rollback");
+}
+
+static jlong n_query(JNIEnv *env, jclass self, jlong conn, jbyteArray statement) {
+ zu_result *out = NULL;
+ zu_error *e = NULL;
+ zu_status st;
+ borrowed q;
+ (void)self;
+
+ if (!borrow(env, statement, &q)) {
+ return 0;
+ }
+ st = p_query(H(zu_conn, conn), S(q), L(q), &out, &e);
+ giveback(env, &q);
+ FAIL_IF_V(env, st, e, "zu_query", 0);
+ return A(out);
+}
+
+static jlong n_prepare(JNIEnv *env, jclass self, jlong conn, jbyteArray statement) {
+ zu_stmt *out = NULL;
+ zu_error *e = NULL;
+ zu_status st;
+ borrowed q;
+ (void)self;
+
+ if (!borrow(env, statement, &q)) {
+ return 0;
+ }
+ st = p_prepare(H(zu_conn, conn), S(q), L(q), &out, &e);
+ giveback(env, &q);
+ FAIL_IF_V(env, st, e, "zu_prepare", 0);
+ return A(out);
+}
+
+static void n_bind_long(JNIEnv *env, jclass self, jlong stmt, jbyteArray name, jlong value) {
+ borrowed n;
+ zu_status st;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return;
+ }
+ st = p_bind_i64(H(zu_stmt, stmt), S(n), L(n), (int64_t)value);
+ giveback(env, &n);
+ FAIL_IF(env, st, NULL, "zu_bind_i64");
+}
+
+static void n_bind_double(JNIEnv *env, jclass self, jlong stmt, jbyteArray name, jdouble value) {
+ borrowed n;
+ zu_status st;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return;
+ }
+ st = p_bind_f64(H(zu_stmt, stmt), S(n), L(n), (double)value);
+ giveback(env, &n);
+ FAIL_IF(env, st, NULL, "zu_bind_f64");
+}
+
+static void n_bind_boolean(JNIEnv *env, jclass self, jlong stmt, jbyteArray name,
+ jboolean value) {
+ borrowed n;
+ zu_status st;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return;
+ }
+ st = p_bind_bool(H(zu_stmt, stmt), S(n), L(n), value == JNI_TRUE ? 1 : 0);
+ giveback(env, &n);
+ FAIL_IF(env, st, NULL, "zu_bind_bool");
+}
+
+static void n_bind_string(JNIEnv *env, jclass self, jlong stmt, jbyteArray name,
+ jbyteArray value) {
+ borrowed n;
+ borrowed v;
+ zu_status st;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return;
+ }
+ if (!borrow(env, value, &v)) {
+ giveback(env, &n);
+ return;
+ }
+ st = p_bind_str(H(zu_stmt, stmt), S(n), L(n), S(v), L(v));
+ giveback(env, &n);
+ giveback(env, &v);
+ FAIL_IF(env, st, NULL, "zu_bind_str");
+}
+
+static void n_bind_temporal(JNIEnv *env, jclass self, jlong stmt, jbyteArray name, jint kind,
+ jlong count, jint offset) {
+ borrowed n;
+ zu_status st;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return;
+ }
+ st = p_bind_temporal(H(zu_stmt, stmt), S(n), L(n), (int32_t)kind, (int64_t)count,
+ (int32_t)offset);
+ giveback(env, &n);
+ FAIL_IF(env, st, NULL, "zu_bind_temporal");
+}
+
+static void n_bind_null(JNIEnv *env, jclass self, jlong stmt, jbyteArray name) {
+ borrowed n;
+ zu_status st;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return;
+ }
+ st = p_bind_null(H(zu_stmt, stmt), S(n), L(n));
+ giveback(env, &n);
+ FAIL_IF(env, st, NULL, "zu_bind_null");
+}
+
+static jlong n_execute(JNIEnv *env, jclass self, jlong stmt) {
+ zu_result *out = NULL;
+ zu_error *e = NULL;
+ zu_status st = p_execute(H(zu_stmt, stmt), &out, &e);
+ (void)self;
+ FAIL_IF_V(env, st, e, "zu_execute", 0);
+ return A(out);
+}
+
+static void n_stmt_close(JNIEnv *env, jclass self, jlong stmt) {
+ (void)env;
+ (void)self;
+ p_stmt_close(H(zu_stmt, stmt));
+}
+
+/* ---- results ---- */
+
+static jlong n_result_rows(JNIEnv *env, jclass self, jlong result) {
+ (void)env;
+ (void)self;
+ return (jlong)p_result_rows(H(zu_result, result));
+}
+
+static jint n_result_cols(JNIEnv *env, jclass self, jlong result) {
+ (void)env;
+ (void)self;
+ return (jint)p_result_cols(H(zu_result, result));
+}
+
+static jbyteArray n_result_col_name(JNIEnv *env, jclass self, jlong result, jint col) {
+ const char *p = NULL;
+ size_t len = 0;
+ zu_status st = p_result_col_name(H(zu_result, result), (uint32_t)col, &p, &len);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_result_col_name", NULL);
+ return bytes(env, p, len);
+}
+
+static jint n_result_cell_type(JNIEnv *env, jclass self, jlong result, jlong row, jint col) {
+ int32_t out = 0;
+ zu_status st =
+ p_result_cell_type(H(zu_result, result), (uint64_t)row, (uint32_t)col, &out);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_result_cell_type", 0);
+ return (jint)out;
+}
+
+static jbyteArray n_result_cell_string(JNIEnv *env, jclass self, jlong result, jlong row,
+ jint col) {
+ const char *p = NULL;
+ size_t len = 0;
+ zu_status st =
+ p_result_cell_str(H(zu_result, result), (uint64_t)row, (uint32_t)col, &p, &len);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_result_cell_str", NULL);
+ return bytes(env, p, len);
+}
+
+static jbyteArray n_result_gqlstatus(JNIEnv *env, jclass self, jlong result) {
+ size_t len = 0;
+ const char *p = p_result_gqlstatus(H(zu_result, result), &len);
+ (void)self;
+ return bytes(env, p, len);
+}
+
+static jint n_result_notices(JNIEnv *env, jclass self, jlong result) {
+ (void)env;
+ (void)self;
+ return (jint)p_result_notices(H(zu_result, result));
+}
+
+static jobject n_result_notice(JNIEnv *env, jclass self, jlong result, jint index) {
+ zu_error *e = NULL;
+ zu_status st = p_result_notice(H(zu_result, result), (uint32_t)index, &e);
+ (void)self;
+ if (st == ZU_DONE) {
+ return NULL;
+ }
+ FAIL_IF_V(env, st, NULL, "zu_result_notice", NULL);
+ return e == NULL ? NULL : record(env, e);
+}
+
+static void n_result_free(JNIEnv *env, jclass self, jlong result) {
+ (void)env;
+ (void)self;
+ p_result_free(H(zu_result, result));
+}
+
+/*
+ * A column, as a buffer over the engine's own memory. Nothing is copied
+ * here and nothing is copied on the Java side either: what comes back
+ * is a window onto the result, good until zu_result_free, which is the
+ * rule the API module holds callers to.
+ *
+ * A null buffer is not a failure. It is the answer for a column the
+ * result built a row at a time, which has no run of values to point at,
+ * and the API module falls back to reading it cell by cell.
+ */
+static jobject window(JNIEnv *env, const void *p, jlong rows, jlong width) {
+ if (p == NULL) {
+ return NULL;
+ }
+ return (*env)->NewDirectByteBuffer(env, (void *)(intptr_t)p, rows * width);
+}
+
+#define COLUMN(fn, sym, what, type, width) \
+ static jobject fn(JNIEnv *env, jclass self, jlong result, jint col, jlong rows) { \
+ const type *p = NULL; \
+ zu_status st = sym(H(zu_result, result), (uint32_t)col, &p); \
+ (void)self; \
+ if (st == ZU_DONE) { \
+ return NULL; \
+ } \
+ FAIL_IF_V(env, st, NULL, what, NULL); \
+ return window(env, p, rows, width); \
+ }
+
+COLUMN(n_col_longs, p_result_col_i64, "zu_result_col_i64", int64_t, 8)
+COLUMN(n_col_doubles, p_result_col_f64, "zu_result_col_f64", double, 8)
+COLUMN(n_col_node_offsets, p_result_col_node_offset, "zu_result_col_node_offset", uint64_t, 8)
+COLUMN(n_col_valid, p_result_col_valid, "zu_result_col_valid", uint8_t, 1)
+
+#define CHUNK_COLUMN(fn, sym, what, type, width) \
+ static jobject fn(JNIEnv *env, jclass self, jlong result, jlong chunk, jint col, \
+ jlong rows) { \
+ const type *p = NULL; \
+ zu_status st = sym(H(zu_result, result), (uint64_t)chunk, (uint32_t)col, &p); \
+ (void)self; \
+ if (st == ZU_DONE) { \
+ return NULL; \
+ } \
+ FAIL_IF_V(env, st, NULL, what, NULL); \
+ return window(env, p, rows, width); \
+ }
+
+CHUNK_COLUMN(n_chunk_longs, p_result_chunk_col_i64, "zu_result_chunk_col_i64", int64_t, 8)
+CHUNK_COLUMN(n_chunk_doubles, p_result_chunk_col_f64, "zu_result_chunk_col_f64", double, 8)
+CHUNK_COLUMN(n_chunk_node_offsets, p_result_chunk_col_node_offset,
+ "zu_result_chunk_col_node_offset", uint64_t, 8)
+CHUNK_COLUMN(n_chunk_valid, p_result_chunk_col_valid, "zu_result_chunk_col_valid", uint8_t, 1)
+
+static jlong n_chunk_count(JNIEnv *env, jclass self, jlong result) {
+ (void)env;
+ (void)self;
+ return (jlong)p_result_chunk_count(H(zu_result, result));
+}
+
+static jlongArray n_chunk(JNIEnv *env, jclass self, jlong result, jlong chunk) {
+ uint64_t offset = 0;
+ uint64_t rows = 0;
+ jlong out[2];
+ zu_status st = p_result_chunk(H(zu_result, result), (uint64_t)chunk, &offset, &rows);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_result_chunk", NULL);
+ out[0] = (jlong)offset;
+ out[1] = (jlong)rows;
+ return longs(env, out, 2);
+}
+
+/*
+ * The whole result to Arrow, which is the one call that spends what it
+ * is given. The result is nulled by the engine on every path, including
+ * the refusal, so the Java side has already let go of the handle before
+ * this runs and there is nothing here to put back.
+ */
+static void n_result_arrow(JNIEnv *env, jclass self, jlong conn, jlong result,
+ jlong rows_per_batch, jlong stream) {
+ zu_result *r = H(zu_result, result);
+ zu_error *e = NULL;
+ zu_status st = p_result_arrow(H(zu_conn, conn), &r, (uint64_t)rows_per_batch,
+ (struct ArrowArrayStream *)(intptr_t)stream, &e);
+ (void)self;
+ FAIL_IF(env, st, e, "zu_result_arrow");
+}
+
+/* ---- values ---- */
+
+static jlong n_result_cell(JNIEnv *env, jclass self, jlong result, jlong row, jint col) {
+ const zu_value *out = NULL;
+ zu_status st = p_result_cell(H(zu_result, result), (uint64_t)row, (uint32_t)col, &out);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_result_cell", 0);
+ return A(out);
+}
+
+static jint n_value_type(JNIEnv *env, jclass self, jlong value) {
+ (void)env;
+ (void)self;
+ return (jint)p_value_type(H(zu_value, value));
+}
+
+static jboolean n_value_boolean(JNIEnv *env, jclass self, jlong value) {
+ int32_t out = 0;
+ zu_status st = p_value_bool(H(zu_value, value), &out);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_value_bool", JNI_FALSE);
+ return out != 0 ? JNI_TRUE : JNI_FALSE;
+}
+
+static jlong n_value_long(JNIEnv *env, jclass self, jlong value) {
+ int64_t out = 0;
+ zu_status st = p_value_i64(H(zu_value, value), &out);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_value_i64", 0);
+ return (jlong)out;
+}
+
+static jdouble n_value_double(JNIEnv *env, jclass self, jlong value) {
+ double out = 0;
+ zu_status st = p_value_f64(H(zu_value, value), &out);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_value_f64", 0);
+ return (jdouble)out;
+}
+
+static jbyteArray n_value_string(JNIEnv *env, jclass self, jlong value) {
+ const char *p = NULL;
+ size_t len = 0;
+ zu_status st = p_value_str(H(zu_value, value), &p, &len);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_value_str", NULL);
+ return bytes(env, p, len);
+}
+
+static jlongArray n_value_temporal(JNIEnv *env, jclass self, jlong value) {
+ int32_t kind = 0;
+ int64_t count = 0;
+ int32_t offset = 0;
+ jlong out[3];
+ zu_status st = p_value_temporal(H(zu_value, value), &kind, &count, &offset);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_value_temporal", NULL);
+ out[0] = (jlong)kind;
+ out[1] = (jlong)count;
+ out[2] = (jlong)offset;
+ return longs(env, out, 3);
+}
+
+static jlongArray n_value_node(JNIEnv *env, jclass self, jlong value) {
+ uint32_t table = 0;
+ uint64_t offset = 0;
+ jlong out[2];
+ zu_status st = p_value_node(H(zu_value, value), &table, &offset);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_value_node", NULL);
+ out[0] = (jlong)table;
+ out[1] = (jlong)offset;
+ return longs(env, out, 2);
+}
+
+static jlongArray n_value_rel(JNIEnv *env, jclass self, jlong value) {
+ uint32_t table = 0;
+ uint64_t src = 0;
+ uint64_t dst = 0;
+ jlong out[3];
+ zu_status st = p_value_rel(H(zu_value, value), &table, &src, &dst);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_value_rel", NULL);
+ out[0] = (jlong)table;
+ out[1] = (jlong)src;
+ out[2] = (jlong)dst;
+ return longs(env, out, 3);
+}
+
+static jlong n_value_length(JNIEnv *env, jclass self, jlong value) {
+ (void)env;
+ (void)self;
+ return (jlong)p_value_len(H(zu_value, value));
+}
+
+static jlong n_value_at(JNIEnv *env, jclass self, jlong value, jlong index) {
+ const zu_value *out = NULL;
+ zu_status st = p_value_at(H(zu_value, value), (uint64_t)index, &out);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_value_at", 0);
+ return A(out);
+}
+
+static jbyteArray n_value_field(JNIEnv *env, jclass self, jlong value, jlong index) {
+ const char *p = NULL;
+ size_t len = 0;
+ zu_status st = p_value_field(H(zu_value, value), (uint64_t)index, &p, &len);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_value_field", NULL);
+ return bytes(env, p, len);
+}
+
+/* ---- the loader ---- */
+
+static jlong n_loader_create(JNIEnv *env, jclass self, jbyteArray path) {
+ zu_loader *out = NULL;
+ zu_error *e = NULL;
+ zu_status st;
+ borrowed p;
+ (void)self;
+
+ if (!borrow(env, path, &p)) {
+ return 0;
+ }
+ st = p_loader_create(S(p), L(p), &out, &e);
+ giveback(env, &p);
+ FAIL_IF_V(env, st, e, "zu_loader_create", 0);
+ return A(out);
+}
+
+static void n_loader_table(JNIEnv *env, jclass self, jlong loader, jbyteArray nodes,
+ jbyteArray edges, jlong rows) {
+ borrowed n;
+ borrowed g;
+ zu_error *e = NULL;
+ zu_status st;
+ (void)self;
+
+ if (!borrow(env, nodes, &n)) {
+ return;
+ }
+ if (!borrow(env, edges, &g)) {
+ giveback(env, &n);
+ return;
+ }
+ st = p_loader_table(H(zu_loader, loader), S(n), L(n), S(g), L(g), (uint64_t)rows, &e);
+ giveback(env, &n);
+ giveback(env, &g);
+ FAIL_IF(env, st, e, "zu_loader_table");
+}
+
+static void n_loader_edges(JNIEnv *env, jclass self, jlong loader, jobject from, jobject to,
+ jlong count) {
+ zu_error *e = NULL;
+ zu_status st;
+ (void)self;
+
+ st = p_loader_edges(H(zu_loader, loader), (const uint32_t *)address(env, from),
+ (const uint32_t *)address(env, to), (uint64_t)count, &e);
+ FAIL_IF(env, st, e, "zu_loader_edges");
+}
+
+#define LOADER_COLUMN(fn, sym, what, type) \
+ static void fn(JNIEnv *env, jclass self, jlong loader, jbyteArray name, jobject values, \
+ jlong count) { \
+ borrowed n; \
+ zu_error *e = NULL; \
+ zu_status st; \
+ (void)self; \
+ if (!borrow(env, name, &n)) { \
+ return; \
+ } \
+ st = sym(H(zu_loader, loader), S(n), L(n), (const type *)address(env, values), \
+ (uint64_t)count, &e); \
+ giveback(env, &n); \
+ FAIL_IF(env, st, e, what); \
+ }
+
+LOADER_COLUMN(n_loader_col_longs, p_loader_col_i64, "zu_loader_col_i64", int64_t)
+LOADER_COLUMN(n_loader_col_doubles, p_loader_col_f64, "zu_loader_col_f64", double)
+LOADER_COLUMN(n_loader_col_booleans, p_loader_col_bool, "zu_loader_col_bool", int32_t)
+
+static void n_loader_col_temporal(JNIEnv *env, jclass self, jlong loader, jbyteArray name,
+ jint kind, jobject values, jlong count) {
+ borrowed n;
+ zu_error *e = NULL;
+ zu_status st;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return;
+ }
+ st = p_loader_col_temporal(H(zu_loader, loader), S(n), L(n), (int32_t)kind,
+ (const int64_t *)address(env, values), (uint64_t)count, &e);
+ giveback(env, &n);
+ FAIL_IF(env, st, e, "zu_loader_col_temporal");
+}
+
+/*
+ * A column of strings, which is the one call that has to gather. The
+ * values arrive as one array of arrays, already encoded on the Java
+ * side, and are laid out here as the two parallel arrays the ABI takes.
+ * Everything is freed before this returns: the engine copies what it
+ * keeps.
+ */
+static void n_loader_col_strings(JNIEnv *env, jclass self, jlong loader, jbyteArray name,
+ jobjectArray values) {
+ borrowed n;
+ zu_error *e = NULL;
+ zu_status st;
+ jsize count = (*env)->GetArrayLength(env, values);
+ const char **pointers = NULL;
+ size_t *lengths = NULL;
+ jsize i;
+ jsize made = 0;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return;
+ }
+ pointers = (const char **)calloc((size_t)count + 1, sizeof *pointers);
+ lengths = (size_t *)calloc((size_t)count + 1, sizeof *lengths);
+ if (pointers == NULL || lengths == NULL) {
+ free(pointers);
+ free((void *)lengths);
+ giveback(env, &n);
+ raise(env, ZU_ERROR, NULL, "zu_loader_col_str");
+ return;
+ }
+ for (i = 0; i < count; i++) {
+ jbyteArray one = (jbyteArray)(*env)->GetObjectArrayElement(env, values, i);
+ jsize len = (*env)->GetArrayLength(env, one);
+ char *copy = (char *)malloc((size_t)len + 1);
+ if (copy == NULL) {
+ break;
+ }
+ (*env)->GetByteArrayRegion(env, one, 0, len, (jbyte *)copy);
+ copy[len] = '\0';
+ pointers[i] = copy;
+ lengths[i] = (size_t)len;
+ made++;
+ (*env)->DeleteLocalRef(env, one);
+ }
+ if (made == count) {
+ st = p_loader_col_str(H(zu_loader, loader), S(n), L(n), pointers, lengths,
+ (uint64_t)count, &e);
+ } else {
+ st = ZU_ERROR;
+ }
+ for (i = 0; i < made; i++) {
+ free((void *)pointers[i]);
+ }
+ free(pointers);
+ free(lengths);
+ giveback(env, &n);
+ FAIL_IF(env, st, e, "zu_loader_col_str");
+}
+
+static void n_loader_finish(JNIEnv *env, jclass self, jlong loader) {
+ zu_error *e = NULL;
+ zu_status st = p_loader_finish(H(zu_loader, loader), &e);
+ (void)self;
+ FAIL_IF(env, st, e, "zu_loader_finish");
+}
+
+static void n_loader_free(JNIEnv *env, jclass self, jlong loader) {
+ (void)env;
+ (void)self;
+ p_loader_free(H(zu_loader, loader));
+}
+
+/* ---- the appender ---- */
+
+static jlong n_appender_open(JNIEnv *env, jclass self, jlong conn, jbyteArray table) {
+ zu_appender *out = NULL;
+ zu_error *e = NULL;
+ zu_status st;
+ borrowed t;
+ (void)self;
+
+ if (!borrow(env, table, &t)) {
+ return 0;
+ }
+ st = p_appender_open(H(zu_conn, conn), S(t), L(t), &out, &e);
+ giveback(env, &t);
+ FAIL_IF_V(env, st, e, "zu_appender_open", 0);
+ return A(out);
+}
+
+static void n_append_boolean(JNIEnv *env, jclass self, jlong appender, jboolean value) {
+ zu_error *e = NULL;
+ zu_status st =
+ p_append_bool(H(zu_appender, appender), value == JNI_TRUE ? 1 : 0, &e);
+ (void)self;
+ FAIL_IF(env, st, e, "zu_append_bool");
+}
+
+static void n_append_long(JNIEnv *env, jclass self, jlong appender, jlong value) {
+ zu_error *e = NULL;
+ zu_status st = p_append_i64(H(zu_appender, appender), (int64_t)value, &e);
+ (void)self;
+ FAIL_IF(env, st, e, "zu_append_i64");
+}
+
+static void n_append_double(JNIEnv *env, jclass self, jlong appender, jdouble value) {
+ zu_error *e = NULL;
+ zu_status st = p_append_f64(H(zu_appender, appender), (double)value, &e);
+ (void)self;
+ FAIL_IF(env, st, e, "zu_append_f64");
+}
+
+static void n_append_string(JNIEnv *env, jclass self, jlong appender, jbyteArray value) {
+ borrowed v;
+ zu_error *e = NULL;
+ zu_status st;
+ (void)self;
+
+ if (!borrow(env, value, &v)) {
+ return;
+ }
+ st = p_append_str(H(zu_appender, appender), S(v), L(v), &e);
+ giveback(env, &v);
+ FAIL_IF(env, st, e, "zu_append_str");
+}
+
+static void n_append_bytes(JNIEnv *env, jclass self, jlong appender, jobject value,
+ jlong length) {
+ zu_error *e = NULL;
+ zu_status st = p_append_bytes(H(zu_appender, appender),
+ (const uint8_t *)address(env, value), (size_t)length, &e);
+ (void)self;
+ FAIL_IF(env, st, e, "zu_append_bytes");
+}
+
+static void n_append_temporal(JNIEnv *env, jclass self, jlong appender, jint kind,
+ jlong count) {
+ zu_error *e = NULL;
+ zu_status st =
+ p_append_temporal(H(zu_appender, appender), (int32_t)kind, (int64_t)count, &e);
+ (void)self;
+ FAIL_IF(env, st, e, "zu_append_temporal");
+}
+
+static void n_append_end_row(JNIEnv *env, jclass self, jlong appender) {
+ zu_error *e = NULL;
+ zu_status st = p_append_end_row(H(zu_appender, appender), &e);
+ (void)self;
+ FAIL_IF(env, st, e, "zu_append_end_row");
+}
+
+static void n_appender_flush(JNIEnv *env, jclass self, jlong appender) {
+ zu_error *e = NULL;
+ zu_status st = p_appender_flush(H(zu_appender, appender), &e);
+ (void)self;
+ FAIL_IF(env, st, e, "zu_appender_flush");
+}
+
+static jlong n_appender_buffered(JNIEnv *env, jclass self, jlong appender) {
+ uint64_t out = 0;
+ zu_status st = p_appender_buffered(H(zu_appender, appender), &out);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_appender_buffered", 0);
+ return (jlong)out;
+}
+
+static jlong n_appender_committed(JNIEnv *env, jclass self, jlong appender) {
+ uint64_t out = 0;
+ zu_status st = p_appender_committed(H(zu_appender, appender), &out);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_appender_committed", 0);
+ return (jlong)out;
+}
+
+static jint n_appender_columns(JNIEnv *env, jclass self, jlong appender) {
+ uint32_t out = 0;
+ zu_status st = p_appender_cols(H(zu_appender, appender), &out);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_appender_cols", 0);
+ return (jint)out;
+}
+
+static jbyteArray n_appender_column_name(JNIEnv *env, jclass self, jlong appender, jint col) {
+ size_t len = 0;
+ const char *p = p_appender_col_name(H(zu_appender, appender), (uint32_t)col, &len);
+ (void)self;
+ return bytes(env, p, len);
+}
+
+static jlong n_appender_discard(JNIEnv *env, jclass self, jlong appender) {
+ uint64_t out = 0;
+ zu_status st = p_appender_discard(H(zu_appender, appender), &out);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_appender_discard", 0);
+ return (jlong)out;
+}
+
+static jlong n_appender_close(JNIEnv *env, jclass self, jlong appender) {
+ uint64_t out = 0;
+ zu_error *e = NULL;
+ zu_status st = p_appender_close(H(zu_appender, appender), &out, &e);
+ (void)self;
+ FAIL_IF_V(env, st, e, "zu_appender_close", 0);
+ return (jlong)out;
+}
+
+static void n_appender_free(JNIEnv *env, jclass self, jlong appender) {
+ (void)env;
+ (void)self;
+ p_appender_free(H(zu_appender, appender));
+}
+
+/* ---- frames ---- */
+
+/*
+ * The release callback, which is how a host learns the engine has
+ * finished with the buffers it lent. It runs once, on a thread of the
+ * library's, after the last statement reading the frame ends.
+ *
+ * Nothing may be thrown out of here either, and for the same reason.
+ */
+static void on_release(void *owner) {
+ jobject runnable = (jobject)owner;
+ int attached = 0;
+ JNIEnv *env;
+
+ if (runnable == NULL) {
+ return;
+ }
+ env = attach(&attached);
+ if (env == NULL) {
+ return;
+ }
+ (*env)->CallVoidMethod(env, runnable, m_runnable_run);
+ if ((*env)->ExceptionCheck(env)) {
+ (*env)->ExceptionDescribe(env);
+ (*env)->ExceptionClear(env);
+ }
+ (*env)->DeleteGlobalRef(env, runnable);
+ detach(attached);
+}
+
+static jlong n_frame_new(JNIEnv *env, jclass self, jbyteArray name, jlong rows,
+ jobject release) {
+ borrowed n;
+ zu_frame *out = NULL;
+ zu_error *e = NULL;
+ zu_status st;
+ jobject owner = NULL;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return 0;
+ }
+ if (release != NULL) {
+ owner = (*env)->NewGlobalRef(env, release);
+ if (owner == NULL) {
+ giveback(env, &n);
+ return 0;
+ }
+ }
+ st = p_frame_new(S(n), L(n), (uint64_t)rows, owner, owner == NULL ? NULL : on_release, &out,
+ &e);
+ giveback(env, &n);
+ if (st != ZU_OK) {
+ /* The frame was never made, so nothing will ever call the release,
+ and the reference this side took has to go back here. */
+ if (owner != NULL) {
+ (*env)->DeleteGlobalRef(env, owner);
+ }
+ raise(env, st, e, "zu_frame_new");
+ return 0;
+ }
+ return A(out);
+}
+
+static void n_frame_col_int(JNIEnv *env, jclass self, jlong frame, jbyteArray name,
+ jobject values, jlong count, jint bits, jboolean is_signed,
+ jlong scale, jint temporal) {
+ borrowed n;
+ zu_error *e = NULL;
+ zu_status st;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return;
+ }
+ st = p_frame_col_int(H(zu_frame, frame), S(n), L(n), address(env, values), (uint64_t)count,
+ (int32_t)bits, is_signed == JNI_TRUE ? 1 : 0, (int64_t)scale,
+ (int32_t)temporal, &e);
+ giveback(env, &n);
+ FAIL_IF(env, st, e, "zu_frame_col_int");
+}
+
+static void n_frame_col_float(JNIEnv *env, jclass self, jlong frame, jbyteArray name,
+ jobject values, jlong count, jint bits) {
+ borrowed n;
+ zu_error *e = NULL;
+ zu_status st;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return;
+ }
+ st = p_frame_col_float(H(zu_frame, frame), S(n), L(n), address(env, values),
+ (uint64_t)count, (int32_t)bits, &e);
+ giveback(env, &n);
+ FAIL_IF(env, st, e, "zu_frame_col_float");
+}
+
+static void n_frame_col_bool(JNIEnv *env, jclass self, jlong frame, jbyteArray name,
+ jobject bitmap, jlong count) {
+ borrowed n;
+ zu_error *e = NULL;
+ zu_status st;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return;
+ }
+ st = p_frame_col_bool(H(zu_frame, frame), S(n), L(n), address(env, bitmap), (uint64_t)count,
+ &e);
+ giveback(env, &n);
+ FAIL_IF(env, st, e, "zu_frame_col_bool");
+}
+
+static void n_frame_col_str(JNIEnv *env, jclass self, jlong frame, jbyteArray name,
+ jobject offsets, jboolean wide, jobject data, jlong data_length,
+ jlong count) {
+ borrowed n;
+ zu_error *e = NULL;
+ zu_status st;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return;
+ }
+ st = p_frame_col_str(H(zu_frame, frame), S(n), L(n), address(env, offsets),
+ wide == JNI_TRUE ? 1 : 0, address(env, data), (size_t)data_length,
+ (uint64_t)count, &e);
+ giveback(env, &n);
+ FAIL_IF(env, st, e, "zu_frame_col_str");
+}
+
+static void n_frame_col_view(JNIEnv *env, jclass self, jlong frame, jbyteArray name,
+ jobject views, jobjectArray data, jlongArray data_lengths,
+ jlong count) {
+ borrowed n;
+ zu_error *e = NULL;
+ zu_status st;
+ jsize buffers = (*env)->GetArrayLength(env, data);
+ const void **pointers;
+ size_t *lengths;
+ jlong *given;
+ jsize i;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return;
+ }
+ pointers = (const void **)calloc((size_t)buffers + 1, sizeof *pointers);
+ lengths = (size_t *)calloc((size_t)buffers + 1, sizeof *lengths);
+ if (pointers == NULL || lengths == NULL) {
+ free(pointers);
+ free(lengths);
+ giveback(env, &n);
+ raise(env, ZU_ERROR, NULL, "zu_frame_col_view");
+ return;
+ }
+ given = (*env)->GetLongArrayElements(env, data_lengths, NULL);
+ for (i = 0; i < buffers; i++) {
+ jobject one = (*env)->GetObjectArrayElement(env, data, i);
+ pointers[i] = address(env, one);
+ lengths[i] = given == NULL ? 0 : (size_t)given[i];
+ (*env)->DeleteLocalRef(env, one);
+ }
+ if (given != NULL) {
+ (*env)->ReleaseLongArrayElements(env, data_lengths, given, JNI_ABORT);
+ }
+ st = p_frame_col_view(H(zu_frame, frame), S(n), L(n), address(env, views), pointers,
+ lengths, (size_t)buffers, (uint64_t)count, &e);
+ free(pointers);
+ free(lengths);
+ giveback(env, &n);
+ FAIL_IF(env, st, e, "zu_frame_col_view");
+}
+
+static void n_frame_free(JNIEnv *env, jclass self, jlong frame) {
+ (void)env;
+ (void)self;
+ p_frame_free(H(zu_frame, frame));
+}
+
+static void n_conn_register(JNIEnv *env, jclass self, jlong conn, jlong frame) {
+ zu_error *e = NULL;
+ zu_status st = p_conn_register(H(zu_conn, conn), H(zu_frame, frame), &e);
+ (void)self;
+ FAIL_IF(env, st, e, "zu_conn_register");
+}
+
+static jboolean n_conn_unregister(JNIEnv *env, jclass self, jlong conn, jbyteArray name) {
+ borrowed n;
+ int32_t out = 0;
+ zu_error *e = NULL;
+ zu_status st;
+ (void)self;
+
+ if (!borrow(env, name, &n)) {
+ return JNI_FALSE;
+ }
+ st = p_conn_unregister(H(zu_conn, conn), S(n), L(n), &out, &e);
+ giveback(env, &n);
+ FAIL_IF_V(env, st, e, "zu_conn_unregister", JNI_FALSE);
+ return out != 0 ? JNI_TRUE : JNI_FALSE;
+}
+
+static jlong n_conn_registered_count(JNIEnv *env, jclass self, jlong conn) {
+ uint64_t out = 0;
+ zu_status st = p_conn_registered_count(H(zu_conn, conn), &out);
+ (void)self;
+ FAIL_IF_V(env, st, NULL, "zu_conn_registered_count", 0);
+ return (jlong)out;
+}
+
+static jbyteArray n_conn_registered_name(JNIEnv *env, jclass self, jlong conn, jlong index) {
+ size_t len = 0;
+ const char *p = p_conn_registered_name(H(zu_conn, conn), (uint64_t)index, &len);
+ (void)self;
+ return bytes(env, p, len);
+}
+
+/* ------------------------------------------------------------------ */
+/* binding */
+/* ------------------------------------------------------------------ */
+
+static const JNINativeMethod methods[] = {
+ {"nLoad", "([B)[B", (void *)n_load},
+ {"nVersion", "()[B", (void *)n_version},
+ {"nConfigSet", "(JJZ[B[B)[J", (void *)n_config_set},
+ {"nDatabaseOpen", "([BJJZ)J", (void *)n_database_open},
+ {"nDatabaseCreate", "([BJJZ)J", (void *)n_database_create},
+ {"nDatabaseMemory", "(JJZ)J", (void *)n_database_memory},
+ {"nDatabaseIsMemory", "(J)Z", (void *)n_database_is_memory},
+ {"nDatabasePath", "(J)[B", (void *)n_database_path},
+ {"nDatabaseClose", "(J)V", (void *)n_database_close},
+ {"nConnect", "(J)J", (void *)n_connect},
+ {"nOpen", "([B)J", (void *)n_open},
+ {"nCreate", "([B)J", (void *)n_create},
+ {"nMemory", "()J", (void *)n_memory},
+ {"nConnDuplicate", "(J)J", (void *)n_conn_duplicate},
+ {"nConnClose", "(J)V", (void *)n_conn_close},
+ {"nConnInterrupt", "(J)V", (void *)n_conn_interrupt},
+ {"nConnRowsRead", "(J)J", (void *)n_conn_rows_read},
+ {"nConnSetProgress", "(JLdev/zudb/Progress;J)J", (void *)n_conn_set_progress},
+ {"nWatchFree", "(J)V", (void *)n_watch_free},
+ {"nConnInTransaction", "(J)Z", (void *)n_conn_in_transaction},
+ {"nBegin", "(JZ)V", (void *)n_begin},
+ {"nCommit", "(J)V", (void *)n_commit},
+ {"nRollback", "(J)V", (void *)n_rollback},
+ {"nQuery", "(J[B)J", (void *)n_query},
+ {"nPrepare", "(J[B)J", (void *)n_prepare},
+ {"nBindLong", "(J[BJ)V", (void *)n_bind_long},
+ {"nBindDouble", "(J[BD)V", (void *)n_bind_double},
+ {"nBindBoolean", "(J[BZ)V", (void *)n_bind_boolean},
+ {"nBindString", "(J[B[B)V", (void *)n_bind_string},
+ {"nBindTemporal", "(J[BIJI)V", (void *)n_bind_temporal},
+ {"nBindNull", "(J[B)V", (void *)n_bind_null},
+ {"nExecute", "(J)J", (void *)n_execute},
+ {"nStmtClose", "(J)V", (void *)n_stmt_close},
+ {"nResultRows", "(J)J", (void *)n_result_rows},
+ {"nResultCols", "(J)I", (void *)n_result_cols},
+ {"nResultColName", "(JI)[B", (void *)n_result_col_name},
+ {"nResultCellType", "(JJI)I", (void *)n_result_cell_type},
+ {"nResultCellString", "(JJI)[B", (void *)n_result_cell_string},
+ {"nResultGqlstatus", "(J)[B", (void *)n_result_gqlstatus},
+ {"nResultNotices", "(J)I", (void *)n_result_notices},
+ {"nResultNotice", "(JI)Ldev/zudb/Diagnostic;", (void *)n_result_notice},
+ {"nResultFree", "(J)V", (void *)n_result_free},
+ {"nColLongs", "(JIJ)Ljava/nio/ByteBuffer;", (void *)n_col_longs},
+ {"nColDoubles", "(JIJ)Ljava/nio/ByteBuffer;", (void *)n_col_doubles},
+ {"nColNodeOffsets", "(JIJ)Ljava/nio/ByteBuffer;", (void *)n_col_node_offsets},
+ {"nColValid", "(JIJ)Ljava/nio/ByteBuffer;", (void *)n_col_valid},
+ {"nChunkCount", "(J)J", (void *)n_chunk_count},
+ {"nChunk", "(JJ)[J", (void *)n_chunk},
+ {"nChunkLongs", "(JJIJ)Ljava/nio/ByteBuffer;", (void *)n_chunk_longs},
+ {"nChunkDoubles", "(JJIJ)Ljava/nio/ByteBuffer;", (void *)n_chunk_doubles},
+ {"nChunkNodeOffsets", "(JJIJ)Ljava/nio/ByteBuffer;", (void *)n_chunk_node_offsets},
+ {"nChunkValid", "(JJIJ)Ljava/nio/ByteBuffer;", (void *)n_chunk_valid},
+ {"nResultArrow", "(JJJJ)V", (void *)n_result_arrow},
+ {"nResultCell", "(JJI)J", (void *)n_result_cell},
+ {"nValueType", "(J)I", (void *)n_value_type},
+ {"nValueBoolean", "(J)Z", (void *)n_value_boolean},
+ {"nValueLong", "(J)J", (void *)n_value_long},
+ {"nValueDouble", "(J)D", (void *)n_value_double},
+ {"nValueString", "(J)[B", (void *)n_value_string},
+ {"nValueTemporal", "(J)[J", (void *)n_value_temporal},
+ {"nValueNode", "(J)[J", (void *)n_value_node},
+ {"nValueRel", "(J)[J", (void *)n_value_rel},
+ {"nValueLength", "(J)J", (void *)n_value_length},
+ {"nValueAt", "(JJ)J", (void *)n_value_at},
+ {"nValueField", "(JJ)[B", (void *)n_value_field},
+ {"nLoaderCreate", "([B)J", (void *)n_loader_create},
+ {"nLoaderTable", "(J[B[BJ)V", (void *)n_loader_table},
+ {"nLoaderEdges", "(JLjava/nio/Buffer;Ljava/nio/Buffer;J)V", (void *)n_loader_edges},
+ {"nLoaderColLongs", "(J[BLjava/nio/Buffer;J)V", (void *)n_loader_col_longs},
+ {"nLoaderColDoubles", "(J[BLjava/nio/Buffer;J)V", (void *)n_loader_col_doubles},
+ {"nLoaderColBooleans", "(J[BLjava/nio/Buffer;J)V", (void *)n_loader_col_booleans},
+ {"nLoaderColStrings", "(J[B[[B)V", (void *)n_loader_col_strings},
+ {"nLoaderColTemporal", "(J[BILjava/nio/Buffer;J)V", (void *)n_loader_col_temporal},
+ {"nLoaderFinish", "(J)V", (void *)n_loader_finish},
+ {"nLoaderFree", "(J)V", (void *)n_loader_free},
+ {"nAppenderOpen", "(J[B)J", (void *)n_appender_open},
+ {"nAppendBoolean", "(JZ)V", (void *)n_append_boolean},
+ {"nAppendLong", "(JJ)V", (void *)n_append_long},
+ {"nAppendDouble", "(JD)V", (void *)n_append_double},
+ {"nAppendString", "(J[B)V", (void *)n_append_string},
+ {"nAppendBytes", "(JLjava/nio/Buffer;J)V", (void *)n_append_bytes},
+ {"nAppendTemporal", "(JIJ)V", (void *)n_append_temporal},
+ {"nAppendEndRow", "(J)V", (void *)n_append_end_row},
+ {"nAppenderFlush", "(J)V", (void *)n_appender_flush},
+ {"nAppenderBuffered", "(J)J", (void *)n_appender_buffered},
+ {"nAppenderCommitted", "(J)J", (void *)n_appender_committed},
+ {"nAppenderColumns", "(J)I", (void *)n_appender_columns},
+ {"nAppenderColumnName", "(JI)[B", (void *)n_appender_column_name},
+ {"nAppenderDiscard", "(J)J", (void *)n_appender_discard},
+ {"nAppenderClose", "(J)J", (void *)n_appender_close},
+ {"nAppenderFree", "(J)V", (void *)n_appender_free},
+ {"nFrameNew", "([BJLjava/lang/Runnable;)J", (void *)n_frame_new},
+ {"nFrameColInt", "(J[BLjava/nio/Buffer;JIZJI)V", (void *)n_frame_col_int},
+ {"nFrameColFloat", "(J[BLjava/nio/Buffer;JI)V", (void *)n_frame_col_float},
+ {"nFrameColBool", "(J[BLjava/nio/Buffer;J)V", (void *)n_frame_col_bool},
+ {"nFrameColStr", "(J[BLjava/nio/Buffer;ZLjava/nio/Buffer;JJ)V", (void *)n_frame_col_str},
+ {"nFrameColView", "(J[BLjava/nio/Buffer;[Ljava/nio/Buffer;[JJ)V",
+ (void *)n_frame_col_view},
+ {"nFrameFree", "(J)V", (void *)n_frame_free},
+ {"nConnRegister", "(JJ)V", (void *)n_conn_register},
+ {"nConnUnregister", "(J[B)Z", (void *)n_conn_unregister},
+ {"nConnRegisteredCount", "(J)J", (void *)n_conn_registered_count},
+ {"nConnRegisteredName", "(JJ)[B", (void *)n_conn_registered_name},
+};
+
+JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
+ (void)reserved;
+ vm = jvm;
+ return JNI_VERSION_1_8;
+}
+
+/*
+ * The one method bound by name, which binds the rest.
+ *
+ * It is called from the static initialiser of JniBinding, so the class
+ * it is handed is the one to register against and the loader it can
+ * find dev.zudb classes through is the right one. Doing this in
+ * JNI_OnLoad instead would mean FindClass against the system loader,
+ * which does not see a client on the module path or under an
+ * application server's loader of its own.
+ */
+JNIEXPORT jboolean JNICALL Java_dev_zudb_jni_JniBinding_nRegister(JNIEnv *env, jclass self) {
+ jclass diagnostic;
+ jclass progress;
+ jclass runnable;
+
+ diagnostic = (*env)->FindClass(env, "dev/zudb/Diagnostic");
+ if (diagnostic == NULL) {
+ return JNI_FALSE;
+ }
+ progress = (*env)->FindClass(env, "dev/zudb/Progress");
+ if (progress == NULL) {
+ return JNI_FALSE;
+ }
+ runnable = (*env)->FindClass(env, "java/lang/Runnable");
+ if (runnable == NULL) {
+ return JNI_FALSE;
+ }
+
+ m_diagnostic = (*env)->GetStaticMethodID(env, self, "diagnostic",
+ "(I[B[B[BIIII[B[BZ)Ldev/zudb/Diagnostic;");
+ m_misuse =
+ (*env)->GetStaticMethodID(env, self, "misuse", "(ILjava/lang/String;)Ldev/zudb/ZuException;");
+ m_to_exception =
+ (*env)->GetMethodID(env, diagnostic, "toException", "()Ldev/zudb/ZuException;");
+ m_progress_at = (*env)->GetMethodID(env, progress, "at", "(JJ)Z");
+ m_runnable_run = (*env)->GetMethodID(env, runnable, "run", "()V");
+ if (m_diagnostic == NULL || m_misuse == NULL || m_to_exception == NULL ||
+ m_progress_at == NULL || m_runnable_run == NULL) {
+ return JNI_FALSE;
+ }
+
+ c_binding = (*env)->NewGlobalRef(env, self);
+ if (c_binding == NULL) {
+ return JNI_FALSE;
+ }
+
+ return (*env)->RegisterNatives(env, self, methods,
+ (jint)(sizeof methods / sizeof methods[0])) == 0
+ ? JNI_TRUE
+ : JNI_FALSE;
+}
diff --git a/zudb-jni/src/main/java/dev/zudb/jni/JniBinding.java b/zudb-jni/src/main/java/dev/zudb/jni/JniBinding.java
new file mode 100644
index 0000000..875b322
--- /dev/null
+++ b/zudb-jni/src/main/java/dev/zudb/jni/JniBinding.java
@@ -0,0 +1,1061 @@
+package dev.zudb.jni;
+
+import dev.zudb.Diagnostic;
+import dev.zudb.Progress;
+import dev.zudb.Status;
+import dev.zudb.ZuException;
+import dev.zudb.spi.ZuBinding;
+import java.nio.Buffer;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.CharBuffer;
+import java.nio.DoubleBuffer;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+import java.nio.LongBuffer;
+import java.nio.ShortBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * The C ABI, called through JNI.
+ *
+ * This is the provider for the JDKs that have no Panama: 17 through 21,
+ * which is still most of what is deployed. It costs a small native shim, one
+ * per platform, that the artifact carries and unpacks; everything it does is
+ * the same ABI with the same handles and the same rules, so a program that
+ * runs on one provider runs on the other without knowing which it got.
+ *
+ *
Strings
+ *
+ * Every string crosses as a byte array rather than as a {@code String}.
+ * JNI's own conversions speak modified UTF-8, which spells a character outside
+ * the basic multilingual plane as a surrogate pair in six bytes and a NUL in
+ * two. The engine validates real UTF-8, so the first emoji anybody stored
+ * would be refused as a bad encoding and the first one anybody read back would
+ * come out mangled. Encoding here costs an array per call on the string paths,
+ * which is the price of being right, and no column path touches it.
+ *
+ *
Buffers
+ *
+ * A column comes back as a direct buffer over the engine's own memory,
+ * made by {@code NewDirectByteBuffer}, so reading a column of a million
+ * integers allocates nothing here either. A buffer handed the other way has to
+ * have an address, so one on the heap is copied into a direct buffer for the
+ * calls that read and are done, and refused for a frame, which keeps the
+ * pointer.
+ */
+final class JniBinding implements ZuBinding {
+
+ /**
+ * The watch cookie each connection is watching through, which is the one
+ * thing this binding has to remember about a connection.
+ *
+ *
It is the address of a cell in the shim holding a global reference to
+ * the watcher. Keyed by the handle, which is safe because the entry goes
+ * when the connection closes and not a moment later, so an address the
+ * allocator hands out again cannot find an old one.
+ */
+ private final Map watches = new ConcurrentHashMap<>();
+
+ JniBinding() {}
+
+ // ---- strings ----
+
+ private static byte[] u8(String s) {
+ return s == null ? null : s.getBytes(StandardCharsets.UTF_8);
+ }
+
+ private static String str(byte[] b) {
+ return b == null ? null : new String(b, StandardCharsets.UTF_8);
+ }
+
+ // ---- buffers ----
+
+ /**
+ * A window onto a run of the engine's memory, as the buffer the API module
+ * hands a caller.
+ *
+ * The size is checked here rather than in the shim so that a column too
+ * big to address says what to do about it. A {@code java.nio} buffer counts
+ * in {@code int}, which runs out at two gigabytes, and the chunked accessors
+ * are exactly the way round that.
+ */
+ private static ByteBuffer window(ByteBuffer raw) {
+ return raw == null ? null : raw.asReadOnlyBuffer().order(ByteOrder.nativeOrder());
+ }
+
+ private static void addressable(long rows, long width, String what) {
+ long size = rows * width;
+ if (size > Integer.MAX_VALUE) {
+ throw Diagnostic.misuse(
+ Status.UNSUPPORTED,
+ "this column is "
+ + size
+ + " bytes, and a java.nio buffer addresses at most "
+ + Integer.MAX_VALUE
+ + ": read it a chunk at a time")
+ .toException();
+ }
+ if (rows < 0) {
+ throw Diagnostic.misuse(Status.MISUSE, what + " was asked for " + rows + " rows")
+ .toException();
+ }
+ }
+
+ /** How many bytes of a buffer are between its position and its limit. */
+ private static long byteSize(Buffer b) {
+ int width;
+ if (b instanceof ByteBuffer) {
+ width = 1;
+ } else if (b instanceof ShortBuffer || b instanceof CharBuffer) {
+ width = 2;
+ } else if (b instanceof IntBuffer || b instanceof FloatBuffer) {
+ width = 4;
+ } else {
+ width = 8;
+ }
+ return (long) b.remaining() * width;
+ }
+
+ /**
+ * A buffer the engine may keep rather than read once.
+ *
+ *
This is the one place a copy is refused instead of made. Everywhere
+ * else a heap buffer costs a memcpy and nothing else, because the call reads
+ * it and is done. A frame keeps the pointer for as long as it is registered,
+ * and a heap buffer has no address anything outside the JVM can keep, so a
+ * copy here would mean the engine reading a copy for the rest of the frame's
+ * life. That is a frame that is not a frame, and quietly making one is worse
+ * than saying so.
+ */
+ private static Buffer lent(Buffer buffer, String name) {
+ if (buffer == null) {
+ throw Diagnostic.misuse(Status.MISUSE, "column " + name + " of a frame has no buffer")
+ .toException();
+ }
+ if (!buffer.isDirect()) {
+ throw Diagnostic.misuse(
+ Status.MISUSE,
+ "column "
+ + name
+ + " of a frame is on the heap, and a frame is read where it lies rather than"
+ + " copied, so it wants a buffer from ByteBuffer.allocateDirect")
+ .toException();
+ }
+ // The shim asks the JVM for the address of a buffer, and that answer
+ // is the start of the buffer rather than of what is left in it. A
+ // slice is the same memory from the position on, so the two agree.
+ return buffer.position() == 0 ? buffer : buffer.slice();
+ }
+
+ /** The same buffer if it is direct, and a direct copy of it if it is not. */
+ private static LongBuffer pass(LongBuffer values) {
+ if (values.isDirect()) {
+ return values.position() == 0 ? values : values.slice();
+ }
+ LongBuffer copy =
+ ByteBuffer.allocateDirect(values.remaining() * 8)
+ .order(ByteOrder.nativeOrder())
+ .asLongBuffer();
+ copy.put(values.duplicate());
+ copy.flip();
+ return copy;
+ }
+
+ private static DoubleBuffer pass(DoubleBuffer values) {
+ if (values.isDirect()) {
+ return values.position() == 0 ? values : values.slice();
+ }
+ DoubleBuffer copy =
+ ByteBuffer.allocateDirect(values.remaining() * 8)
+ .order(ByteOrder.nativeOrder())
+ .asDoubleBuffer();
+ copy.put(values.duplicate());
+ copy.flip();
+ return copy;
+ }
+
+ private static IntBuffer pass(IntBuffer values) {
+ if (values.isDirect()) {
+ return values.position() == 0 ? values : values.slice();
+ }
+ IntBuffer copy =
+ ByteBuffer.allocateDirect(values.remaining() * 4)
+ .order(ByteOrder.nativeOrder())
+ .asIntBuffer();
+ copy.put(values.duplicate());
+ copy.flip();
+ return copy;
+ }
+
+ private static ByteBuffer pass(ByteBuffer values) {
+ if (values.isDirect()) {
+ return values.position() == 0 ? values : values.slice();
+ }
+ ByteBuffer copy = ByteBuffer.allocateDirect(values.remaining()).order(ByteOrder.nativeOrder());
+ copy.put(values.duplicate());
+ copy.flip();
+ return copy;
+ }
+
+ // ---- what the shim calls back ----
+
+ /**
+ * A diagnostic, built here because the mapping from the ABI's numbers to
+ * this client's two enums belongs in one place and that place is the API
+ * module.
+ *
+ *
Called from the shim and from nowhere else, which is why it is private
+ * and why nothing in this file appears to use it.
+ */
+ private static Diagnostic diagnostic(
+ int status,
+ byte[] message,
+ byte[] code,
+ byte[] condition,
+ int severity,
+ int line,
+ int column,
+ int offset,
+ byte[] excerpt,
+ byte[] docUrl,
+ boolean retryable) {
+ return Diagnostic.of(
+ status,
+ str(message),
+ str(code),
+ str(condition),
+ severity,
+ line,
+ column,
+ offset,
+ str(excerpt),
+ str(docUrl),
+ retryable);
+ }
+
+ /** The exception a status that carries no error of its own is. */
+ private static ZuException misuse(int status, String what) {
+ return Diagnostic.misuse(Status.of(status), what + " answered " + Status.of(status))
+ .toException();
+ }
+
+ // ---- the interface ----
+
+ @Override
+ public String version() {
+ return str(nVersion());
+ }
+
+ @Override
+ public long[] configSet(
+ long memoryLimit, long threads, boolean readOnly, String key, String value) {
+ return nConfigSet(memoryLimit, threads, readOnly, u8(key), u8(value));
+ }
+
+ @Override
+ public long databaseOpen(String path, long memoryLimit, long threads, boolean readOnly) {
+ return nDatabaseOpen(u8(path), memoryLimit, threads, readOnly);
+ }
+
+ @Override
+ public long databaseCreate(String path, long memoryLimit, long threads, boolean readOnly) {
+ return nDatabaseCreate(u8(path), memoryLimit, threads, readOnly);
+ }
+
+ @Override
+ public long databaseMemory(long memoryLimit, long threads, boolean readOnly) {
+ return nDatabaseMemory(memoryLimit, threads, readOnly);
+ }
+
+ @Override
+ public boolean databaseIsMemory(long db) {
+ return nDatabaseIsMemory(db);
+ }
+
+ @Override
+ public String databasePath(long db) {
+ return str(nDatabasePath(db));
+ }
+
+ @Override
+ public void databaseClose(long db) {
+ nDatabaseClose(db);
+ }
+
+ @Override
+ public long connect(long db) {
+ return nConnect(db);
+ }
+
+ @Override
+ public long open(String path) {
+ return nOpen(u8(path));
+ }
+
+ @Override
+ public long create(String path) {
+ return nCreate(u8(path));
+ }
+
+ @Override
+ public long memory() {
+ return nMemory();
+ }
+
+ @Override
+ public long connDuplicate(long conn) {
+ return nConnDuplicate(conn);
+ }
+
+ @Override
+ public void connClose(long conn) {
+ nConnClose(conn);
+ spend(watches.remove(conn));
+ }
+
+ @Override
+ public void connInterrupt(long conn) {
+ nConnInterrupt(conn);
+ }
+
+ @Override
+ public long connRowsRead(long conn) {
+ return nConnRowsRead(conn);
+ }
+
+ @Override
+ public void connSetProgress(long conn, Progress watcher, long intervalMillis) {
+ long cookie = nConnSetProgress(conn, watcher, intervalMillis);
+ // The old arrangement goes only once the engine has the new one, so
+ // there is no moment at which the reference the engine holds is one
+ // this side has already given back.
+ Long before = cookie == 0 ? watches.remove(conn) : watches.put(conn, cookie);
+ spend(before);
+ }
+
+ private static void spend(Long cookie) {
+ if (cookie != null && cookie != 0) {
+ nWatchFree(cookie);
+ }
+ }
+
+ @Override
+ public boolean connInTransaction(long conn) {
+ return nConnInTransaction(conn);
+ }
+
+ @Override
+ public void begin(long conn, boolean readOnly) {
+ nBegin(conn, readOnly);
+ }
+
+ @Override
+ public void commit(long conn) {
+ nCommit(conn);
+ }
+
+ @Override
+ public void rollback(long conn) {
+ nRollback(conn);
+ }
+
+ @Override
+ public long query(long conn, String statement) {
+ return nQuery(conn, u8(statement));
+ }
+
+ @Override
+ public long prepare(long conn, String statement) {
+ return nPrepare(conn, u8(statement));
+ }
+
+ @Override
+ public void bindLong(long stmt, String name, long value) {
+ nBindLong(stmt, u8(name), value);
+ }
+
+ @Override
+ public void bindDouble(long stmt, String name, double value) {
+ nBindDouble(stmt, u8(name), value);
+ }
+
+ @Override
+ public void bindBoolean(long stmt, String name, boolean value) {
+ nBindBoolean(stmt, u8(name), value);
+ }
+
+ @Override
+ public void bindString(long stmt, String name, String value) {
+ nBindString(stmt, u8(name), u8(value));
+ }
+
+ @Override
+ public void bindTemporal(long stmt, String name, int kind, long count, int offsetMinutes) {
+ nBindTemporal(stmt, u8(name), kind, count, offsetMinutes);
+ }
+
+ @Override
+ public void bindNull(long stmt, String name) {
+ nBindNull(stmt, u8(name));
+ }
+
+ @Override
+ public long execute(long stmt) {
+ return nExecute(stmt);
+ }
+
+ @Override
+ public void stmtClose(long stmt) {
+ nStmtClose(stmt);
+ }
+
+ @Override
+ public long resultRows(long result) {
+ return nResultRows(result);
+ }
+
+ @Override
+ public int resultCols(long result) {
+ return nResultCols(result);
+ }
+
+ @Override
+ public String resultColName(long result, int col) {
+ return str(nResultColName(result, col));
+ }
+
+ @Override
+ public int resultCellType(long result, long row, int col) {
+ return nResultCellType(result, row, col);
+ }
+
+ @Override
+ public String resultCellString(long result, long row, int col) {
+ return str(nResultCellString(result, row, col));
+ }
+
+ @Override
+ public String resultGqlstatus(long result) {
+ return str(nResultGqlstatus(result));
+ }
+
+ @Override
+ public int resultNotices(long result) {
+ return nResultNotices(result);
+ }
+
+ @Override
+ public Diagnostic resultNotice(long result, int index) {
+ return nResultNotice(result, index);
+ }
+
+ @Override
+ public void resultFree(long result) {
+ nResultFree(result);
+ }
+
+ @Override
+ public LongBuffer colLongs(long result, int col, long rows) {
+ addressable(rows, 8, "zu_result_col_i64");
+ ByteBuffer raw = window(nColLongs(result, col, rows));
+ return raw == null ? null : raw.asLongBuffer();
+ }
+
+ @Override
+ public DoubleBuffer colDoubles(long result, int col, long rows) {
+ addressable(rows, 8, "zu_result_col_f64");
+ ByteBuffer raw = window(nColDoubles(result, col, rows));
+ return raw == null ? null : raw.asDoubleBuffer();
+ }
+
+ @Override
+ public LongBuffer colNodeOffsets(long result, int col, long rows) {
+ addressable(rows, 8, "zu_result_col_node_offset");
+ ByteBuffer raw = window(nColNodeOffsets(result, col, rows));
+ return raw == null ? null : raw.asLongBuffer();
+ }
+
+ @Override
+ public ByteBuffer colValid(long result, int col, long rows) {
+ addressable(rows, 1, "zu_result_col_valid");
+ return window(nColValid(result, col, rows));
+ }
+
+ @Override
+ public long chunkCount(long result) {
+ return nChunkCount(result);
+ }
+
+ @Override
+ public long[] chunk(long result, long chunk) {
+ return nChunk(result, chunk);
+ }
+
+ @Override
+ public LongBuffer chunkLongs(long result, long chunk, int col, long rows) {
+ addressable(rows, 8, "zu_result_chunk_col_i64");
+ ByteBuffer raw = window(nChunkLongs(result, chunk, col, rows));
+ return raw == null ? null : raw.asLongBuffer();
+ }
+
+ @Override
+ public DoubleBuffer chunkDoubles(long result, long chunk, int col, long rows) {
+ addressable(rows, 8, "zu_result_chunk_col_f64");
+ ByteBuffer raw = window(nChunkDoubles(result, chunk, col, rows));
+ return raw == null ? null : raw.asDoubleBuffer();
+ }
+
+ @Override
+ public LongBuffer chunkNodeOffsets(long result, long chunk, int col, long rows) {
+ addressable(rows, 8, "zu_result_chunk_col_node_offset");
+ ByteBuffer raw = window(nChunkNodeOffsets(result, chunk, col, rows));
+ return raw == null ? null : raw.asLongBuffer();
+ }
+
+ @Override
+ public ByteBuffer chunkValid(long result, long chunk, int col, long rows) {
+ addressable(rows, 1, "zu_result_chunk_col_valid");
+ return window(nChunkValid(result, chunk, col, rows));
+ }
+
+ @Override
+ public void resultArrow(long conn, long result, long rowsPerBatch, long stream) {
+ nResultArrow(conn, result, rowsPerBatch, stream);
+ }
+
+ @Override
+ public long resultCell(long result, long row, int col) {
+ return nResultCell(result, row, col);
+ }
+
+ @Override
+ public int valueType(long value) {
+ return nValueType(value);
+ }
+
+ @Override
+ public boolean valueBoolean(long value) {
+ return nValueBoolean(value);
+ }
+
+ @Override
+ public long valueLong(long value) {
+ return nValueLong(value);
+ }
+
+ @Override
+ public double valueDouble(long value) {
+ return nValueDouble(value);
+ }
+
+ @Override
+ public String valueString(long value) {
+ return str(nValueString(value));
+ }
+
+ @Override
+ public long[] valueTemporal(long value) {
+ return nValueTemporal(value);
+ }
+
+ @Override
+ public long[] valueNode(long value) {
+ return nValueNode(value);
+ }
+
+ @Override
+ public long[] valueRel(long value) {
+ return nValueRel(value);
+ }
+
+ @Override
+ public long valueLength(long value) {
+ return nValueLength(value);
+ }
+
+ @Override
+ public long valueAt(long value, long index) {
+ return nValueAt(value, index);
+ }
+
+ @Override
+ public String valueField(long value, long index) {
+ return str(nValueField(value, index));
+ }
+
+ @Override
+ public long loaderCreate(String path) {
+ return nLoaderCreate(u8(path));
+ }
+
+ @Override
+ public void loaderTable(long loader, String nodes, String edges, long rows) {
+ nLoaderTable(loader, u8(nodes), u8(edges), rows);
+ }
+
+ @Override
+ public void loaderEdges(long loader, IntBuffer from, IntBuffer to) {
+ int count = from.remaining();
+ if (to.remaining() != count) {
+ throw Diagnostic.misuse(
+ Status.MISUSE,
+ "an edge starts somewhere and ends somewhere, and there are "
+ + count
+ + " starts against "
+ + to.remaining()
+ + " ends")
+ .toException();
+ }
+ nLoaderEdges(loader, pass(from), pass(to), count);
+ }
+
+ @Override
+ public void loaderColumnLongs(long loader, String name, LongBuffer values) {
+ nLoaderColLongs(loader, u8(name), pass(values), values.remaining());
+ }
+
+ @Override
+ public void loaderColumnDoubles(long loader, String name, DoubleBuffer values) {
+ nLoaderColDoubles(loader, u8(name), pass(values), values.remaining());
+ }
+
+ @Override
+ public void loaderColumnBooleans(long loader, String name, IntBuffer values) {
+ nLoaderColBooleans(loader, u8(name), pass(values), values.remaining());
+ }
+
+ @Override
+ public void loaderColumnStrings(long loader, String name, List values) {
+ byte[][] encoded = new byte[values.size()][];
+ for (int i = 0; i < encoded.length; i++) {
+ String v = values.get(i);
+ if (v == null) {
+ throw Diagnostic.misuse(
+ Status.MISUSE,
+ "row "
+ + i
+ + " of column "
+ + name
+ + " is no value at all, and a loaded column holds a value a row")
+ .toException();
+ }
+ encoded[i] = u8(v);
+ }
+ nLoaderColStrings(loader, u8(name), encoded);
+ }
+
+ @Override
+ public void loaderColumnTemporal(long loader, String name, int kind, LongBuffer values) {
+ nLoaderColTemporal(loader, u8(name), kind, pass(values), values.remaining());
+ }
+
+ @Override
+ public void loaderFinish(long loader) {
+ nLoaderFinish(loader);
+ }
+
+ @Override
+ public void loaderFree(long loader) {
+ nLoaderFree(loader);
+ }
+
+ @Override
+ public long appenderOpen(long conn, String table) {
+ return nAppenderOpen(conn, u8(table));
+ }
+
+ @Override
+ public void appendBoolean(long appender, boolean value) {
+ nAppendBoolean(appender, value);
+ }
+
+ @Override
+ public void appendLong(long appender, long value) {
+ nAppendLong(appender, value);
+ }
+
+ @Override
+ public void appendDouble(long appender, double value) {
+ nAppendDouble(appender, value);
+ }
+
+ @Override
+ public void appendString(long appender, String value) {
+ nAppendString(appender, u8(value));
+ }
+
+ @Override
+ public void appendBytes(long appender, ByteBuffer value) {
+ nAppendBytes(appender, pass(value), value.remaining());
+ }
+
+ @Override
+ public void appendTemporal(long appender, int kind, long count) {
+ nAppendTemporal(appender, kind, count);
+ }
+
+ @Override
+ public void appendEndRow(long appender) {
+ nAppendEndRow(appender);
+ }
+
+ @Override
+ public void appenderFlush(long appender) {
+ nAppenderFlush(appender);
+ }
+
+ @Override
+ public long appenderBuffered(long appender) {
+ return nAppenderBuffered(appender);
+ }
+
+ @Override
+ public long appenderCommitted(long appender) {
+ return nAppenderCommitted(appender);
+ }
+
+ @Override
+ public int appenderColumns(long appender) {
+ return nAppenderColumns(appender);
+ }
+
+ @Override
+ public String appenderColumnName(long appender, int col) {
+ return str(nAppenderColumnName(appender, col));
+ }
+
+ @Override
+ public long appenderDiscard(long appender) {
+ return nAppenderDiscard(appender);
+ }
+
+ @Override
+ public long appenderClose(long appender) {
+ return nAppenderClose(appender);
+ }
+
+ @Override
+ public void appenderFree(long appender) {
+ nAppenderFree(appender);
+ }
+
+ @Override
+ public long frameNew(String name, long rows, Runnable release) {
+ return nFrameNew(u8(name), rows, release);
+ }
+
+ @Override
+ public void frameColumnInts(
+ long frame,
+ String name,
+ Buffer values,
+ long count,
+ int bits,
+ boolean signed,
+ long scale,
+ int temporal) {
+ nFrameColInt(frame, u8(name), lent(values, name), count, bits, signed, scale, temporal);
+ }
+
+ @Override
+ public void frameColumnFloats(long frame, String name, Buffer values, long count, int bits) {
+ nFrameColFloat(frame, u8(name), lent(values, name), count, bits);
+ }
+
+ @Override
+ public void frameColumnBooleans(long frame, String name, Buffer bitmap, long count) {
+ nFrameColBool(frame, u8(name), lent(bitmap, name), count);
+ }
+
+ @Override
+ public void frameColumnStrings(
+ long frame, String name, Buffer offsets, boolean wide, Buffer data, long count) {
+ Buffer characters = lent(data, name);
+ nFrameColStr(
+ frame, u8(name), lent(offsets, name), wide, characters, byteSize(characters), count);
+ }
+
+ @Override
+ public void frameColumnViews(
+ long frame, String name, Buffer views, List data, long count) {
+ Buffer[] buffers = new Buffer[data.size()];
+ long[] lengths = new long[buffers.length];
+ for (int i = 0; i < buffers.length; i++) {
+ buffers[i] = lent(data.get(i), name);
+ lengths[i] = byteSize(buffers[i]);
+ }
+ nFrameColView(frame, u8(name), lent(views, name), buffers, lengths, count);
+ }
+
+ @Override
+ public void frameFree(long frame) {
+ nFrameFree(frame);
+ }
+
+ @Override
+ public void connRegister(long conn, long frame) {
+ nConnRegister(conn, frame);
+ }
+
+ @Override
+ public boolean connUnregister(long conn, String name) {
+ return nConnUnregister(conn, u8(name));
+ }
+
+ @Override
+ public long connRegisteredCount(long conn) {
+ return nConnRegisteredCount(conn);
+ }
+
+ @Override
+ public String connRegisteredName(long conn, long index) {
+ return str(nConnRegisteredName(conn, index));
+ }
+
+ // ---- the shim ----
+
+ /**
+ * Binds every native below to the shim, and the shim to this class.
+ *
+ * One method with a name JNI derives, which registers the rest and looks
+ * up what it has to call back into. It is done from here rather than from
+ * the shim's own load hook because the hook cannot find a class a module
+ * path or an application server's loader holds, and the class handed to a
+ * static native is the right one by construction.
+ *
+ * @return whether it bound
+ */
+ static native boolean nRegister();
+
+ /**
+ * Opens libzu and resolves every call this client makes.
+ *
+ * @param path the library, as UTF-8 bytes
+ * @return null if it opened and had everything, and otherwise why not
+ */
+ static native byte[] nLoad(byte[] path);
+
+ private static native byte[] nVersion();
+
+ private static native long[] nConfigSet(
+ long memoryLimit, long threads, boolean readOnly, byte[] key, byte[] value);
+
+ private static native long nDatabaseOpen(
+ byte[] path, long memoryLimit, long threads, boolean readOnly);
+
+ private static native long nDatabaseCreate(
+ byte[] path, long memoryLimit, long threads, boolean readOnly);
+
+ private static native long nDatabaseMemory(long memoryLimit, long threads, boolean readOnly);
+
+ private static native boolean nDatabaseIsMemory(long db);
+
+ private static native byte[] nDatabasePath(long db);
+
+ private static native void nDatabaseClose(long db);
+
+ private static native long nConnect(long db);
+
+ private static native long nOpen(byte[] path);
+
+ private static native long nCreate(byte[] path);
+
+ private static native long nMemory();
+
+ private static native long nConnDuplicate(long conn);
+
+ private static native void nConnClose(long conn);
+
+ private static native void nConnInterrupt(long conn);
+
+ private static native long nConnRowsRead(long conn);
+
+ private static native long nConnSetProgress(long conn, Progress watcher, long intervalMillis);
+
+ private static native void nWatchFree(long cookie);
+
+ private static native boolean nConnInTransaction(long conn);
+
+ private static native void nBegin(long conn, boolean readOnly);
+
+ private static native void nCommit(long conn);
+
+ private static native void nRollback(long conn);
+
+ private static native long nQuery(long conn, byte[] statement);
+
+ private static native long nPrepare(long conn, byte[] statement);
+
+ private static native void nBindLong(long stmt, byte[] name, long value);
+
+ private static native void nBindDouble(long stmt, byte[] name, double value);
+
+ private static native void nBindBoolean(long stmt, byte[] name, boolean value);
+
+ private static native void nBindString(long stmt, byte[] name, byte[] value);
+
+ private static native void nBindTemporal(
+ long stmt, byte[] name, int kind, long count, int offsetMinutes);
+
+ private static native void nBindNull(long stmt, byte[] name);
+
+ private static native long nExecute(long stmt);
+
+ private static native void nStmtClose(long stmt);
+
+ private static native long nResultRows(long result);
+
+ private static native int nResultCols(long result);
+
+ private static native byte[] nResultColName(long result, int col);
+
+ private static native int nResultCellType(long result, long row, int col);
+
+ private static native byte[] nResultCellString(long result, long row, int col);
+
+ private static native byte[] nResultGqlstatus(long result);
+
+ private static native int nResultNotices(long result);
+
+ private static native Diagnostic nResultNotice(long result, int index);
+
+ private static native void nResultFree(long result);
+
+ private static native ByteBuffer nColLongs(long result, int col, long rows);
+
+ private static native ByteBuffer nColDoubles(long result, int col, long rows);
+
+ private static native ByteBuffer nColNodeOffsets(long result, int col, long rows);
+
+ private static native ByteBuffer nColValid(long result, int col, long rows);
+
+ private static native long nChunkCount(long result);
+
+ private static native long[] nChunk(long result, long chunk);
+
+ private static native ByteBuffer nChunkLongs(long result, long chunk, int col, long rows);
+
+ private static native ByteBuffer nChunkDoubles(long result, long chunk, int col, long rows);
+
+ private static native ByteBuffer nChunkNodeOffsets(long result, long chunk, int col, long rows);
+
+ private static native ByteBuffer nChunkValid(long result, long chunk, int col, long rows);
+
+ private static native void nResultArrow(long conn, long result, long rowsPerBatch, long stream);
+
+ private static native long nResultCell(long result, long row, int col);
+
+ private static native int nValueType(long value);
+
+ private static native boolean nValueBoolean(long value);
+
+ private static native long nValueLong(long value);
+
+ private static native double nValueDouble(long value);
+
+ private static native byte[] nValueString(long value);
+
+ private static native long[] nValueTemporal(long value);
+
+ private static native long[] nValueNode(long value);
+
+ private static native long[] nValueRel(long value);
+
+ private static native long nValueLength(long value);
+
+ private static native long nValueAt(long value, long index);
+
+ private static native byte[] nValueField(long value, long index);
+
+ private static native long nLoaderCreate(byte[] path);
+
+ private static native void nLoaderTable(long loader, byte[] nodes, byte[] edges, long rows);
+
+ private static native void nLoaderEdges(long loader, Buffer from, Buffer to, long count);
+
+ private static native void nLoaderColLongs(long loader, byte[] name, Buffer values, long count);
+
+ private static native void nLoaderColDoubles(long loader, byte[] name, Buffer values, long count);
+
+ private static native void nLoaderColBooleans(
+ long loader, byte[] name, Buffer values, long count);
+
+ private static native void nLoaderColStrings(long loader, byte[] name, byte[][] values);
+
+ private static native void nLoaderColTemporal(
+ long loader, byte[] name, int kind, Buffer values, long count);
+
+ private static native void nLoaderFinish(long loader);
+
+ private static native void nLoaderFree(long loader);
+
+ private static native long nAppenderOpen(long conn, byte[] table);
+
+ private static native void nAppendBoolean(long appender, boolean value);
+
+ private static native void nAppendLong(long appender, long value);
+
+ private static native void nAppendDouble(long appender, double value);
+
+ private static native void nAppendString(long appender, byte[] value);
+
+ private static native void nAppendBytes(long appender, Buffer value, long length);
+
+ private static native void nAppendTemporal(long appender, int kind, long count);
+
+ private static native void nAppendEndRow(long appender);
+
+ private static native void nAppenderFlush(long appender);
+
+ private static native long nAppenderBuffered(long appender);
+
+ private static native long nAppenderCommitted(long appender);
+
+ private static native int nAppenderColumns(long appender);
+
+ private static native byte[] nAppenderColumnName(long appender, int col);
+
+ private static native long nAppenderDiscard(long appender);
+
+ private static native long nAppenderClose(long appender);
+
+ private static native void nAppenderFree(long appender);
+
+ private static native long nFrameNew(byte[] name, long rows, Runnable release);
+
+ private static native void nFrameColInt(
+ long frame,
+ byte[] name,
+ Buffer values,
+ long count,
+ int bits,
+ boolean signed,
+ long scale,
+ int temporal);
+
+ private static native void nFrameColFloat(
+ long frame, byte[] name, Buffer values, long count, int bits);
+
+ private static native void nFrameColBool(long frame, byte[] name, Buffer bitmap, long count);
+
+ private static native void nFrameColStr(
+ long frame, byte[] name, Buffer offsets, boolean wide, Buffer data, long dataLength,
+ long count);
+
+ private static native void nFrameColView(
+ long frame, byte[] name, Buffer views, Buffer[] data, long[] lengths, long count);
+
+ private static native void nFrameFree(long frame);
+
+ private static native void nConnRegister(long conn, long frame);
+
+ private static native boolean nConnUnregister(long conn, byte[] name);
+
+ private static native long nConnRegisteredCount(long conn);
+
+ private static native byte[] nConnRegisteredName(long conn, long index);
+}
diff --git a/zudb-jni/src/main/java/dev/zudb/jni/JniProvider.java b/zudb-jni/src/main/java/dev/zudb/jni/JniProvider.java
new file mode 100644
index 0000000..a580a92
--- /dev/null
+++ b/zudb-jni/src/main/java/dev/zudb/jni/JniProvider.java
@@ -0,0 +1,51 @@
+package dev.zudb.jni;
+
+import dev.zudb.spi.ProviderUnavailableException;
+import dev.zudb.spi.ZuBinding;
+import dev.zudb.spi.ZuProvider;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Path;
+
+/**
+ * The provider that binds zu through JNI.
+ *
+ *
This is the one for JDK 17 through 21, which have no Foreign Function
+ * and Memory API and are most of what is running in production. It costs a
+ * native shim per platform, which this artifact carries, and it is a little
+ * slower on the string paths than Panama is. Everything else about it is the
+ * same: the same handles, the same failures, the same buffers over the
+ * engine's own memory.
+ *
+ *
Its priority is below Panama's, so a JDK that has both takes Panama and
+ * this one is never loaded. Nothing chooses it by name.
+ */
+public final class JniProvider implements ZuProvider {
+
+ /**
+ * What the service loader calls.
+ *
+ *
Public and taking nothing because {@link java.util.ServiceLoader} says
+ * so. Nothing else has a reason to make one.
+ */
+ public JniProvider() {}
+
+ @Override
+ public String name() {
+ return "jni";
+ }
+
+ @Override
+ public int priority() {
+ return 50;
+ }
+
+ @Override
+ public ZuBinding load(Path library) {
+ Shim.ensure();
+ byte[] why = JniBinding.nLoad(library.toAbsolutePath().toString().getBytes(StandardCharsets.UTF_8));
+ if (why != null) {
+ throw new ProviderUnavailableException(new String(why, StandardCharsets.UTF_8));
+ }
+ return new JniBinding();
+ }
+}
diff --git a/zudb-jni/src/main/java/dev/zudb/jni/Shim.java b/zudb-jni/src/main/java/dev/zudb/jni/Shim.java
new file mode 100644
index 0000000..52fd59f
--- /dev/null
+++ b/zudb-jni/src/main/java/dev/zudb/jni/Shim.java
@@ -0,0 +1,110 @@
+package dev.zudb.jni;
+
+import dev.zudb.spi.Natives;
+import dev.zudb.spi.ProviderUnavailableException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Finds and loads the native shim, once.
+ *
+ *
The shim is the second library this provider needs, and it is not libzu.
+ * libzu is the engine, it is large, and the API module already knows four
+ * places to look for it. The shim is a few tens of kilobytes of C that turns
+ * a JNI call into a call through a function pointer, one build per platform,
+ * and it ships inside this artifact for the platforms that are tier one.
+ *
+ *
The order below is the API module's order for libzu, for the same
+ * reasons and with the same escape hatch first: somebody who built their own
+ * shim, or who is running one out of a build directory, names it and is
+ * believed. Only then does the jar's own copy get used, and only then the
+ * platform's search, which is what a distribution packaging this would want.
+ *
+ *
Loading is done here rather than in a static initialiser on the binding
+ * because a failure in a static initialiser is an
+ * {@link ExceptionInInitializerError} the first time and a bare
+ * {@link NoClassDefFoundError} every time after, and neither says which of
+ * four places was looked in. This throws
+ * {@link ProviderUnavailableException} with all four.
+ */
+final class Shim {
+
+ private Shim() {}
+
+ /** What the shim is called, before the platform decorates it. */
+ private static final String NAME = "zudb_jni";
+
+ private static boolean loaded;
+
+ /**
+ * Loads the shim and binds it to the binding, or says why it could not.
+ *
+ *
Called under the class lock, which is enough: the work is done once
+ * and everything after it is a read of a boolean that the same lock
+ * published.
+ *
+ * @throws ProviderUnavailableException if there is no shim for this machine
+ */
+ static synchronized void ensure() {
+ if (loaded) {
+ return;
+ }
+ List looked = new ArrayList<>();
+ Path file = named(System.getProperty("zu.jni.library"), "the zu.jni.library property", looked);
+ if (file == null) {
+ file = named(System.getenv("ZU_JNI_LIBRARY"), "the ZU_JNI_LIBRARY variable", looked);
+ }
+
+ String library = System.mapLibraryName(NAME);
+ if (file == null) {
+ String resource = "dev/zudb/jni/" + Natives.flavour() + "/" + library;
+ file = Natives.unpack(resource, library, Shim.class);
+ if (file == null) {
+ looked.add("no " + resource + " on the classpath");
+ }
+ }
+
+ try {
+ if (file != null) {
+ System.load(file.toAbsolutePath().toString());
+ } else {
+ // The last resort, and the one a distribution that packages this
+ // properly would rely on: LD_LIBRARY_PATH, /usr/local/lib,
+ // java.library.path, whatever this platform calls its search.
+ System.loadLibrary(NAME);
+ }
+ } catch (UnsatisfiedLinkError e) {
+ if (file == null) {
+ looked.add("no " + library + " on java.library.path: " + e.getMessage());
+ } else {
+ looked.add(file + " did not load: " + e.getMessage());
+ }
+ throw new ProviderUnavailableException(
+ "the JNI provider needs its native shim, and " + String.join(", and ", looked), e);
+ }
+
+ if (!JniBinding.nRegister()) {
+ throw new ProviderUnavailableException(
+ "the JNI shim loaded and then could not bind itself to "
+ + JniBinding.class.getName()
+ + ", which means the shim and this jar are from different releases");
+ }
+ loaded = true;
+ }
+
+ /** A path somebody named, checked for being there so the reason is theirs. */
+ private static Path named(String value, String where, List looked) {
+ if (value == null || value.isEmpty()) {
+ return null;
+ }
+ Path path = Paths.get(value);
+ if (Files.isRegularFile(path)) {
+ return path;
+ }
+ looked.add(where + " names " + value + ", which is not a file");
+ return null;
+ }
+}
diff --git a/zudb-jni/src/main/java/module-info.java b/zudb-jni/src/main/java/module-info.java
new file mode 100644
index 0000000..86572e2
--- /dev/null
+++ b/zudb-jni/src/main/java/module-info.java
@@ -0,0 +1,15 @@
+/**
+ * The zu provider over JNI, for the JDKs that have no Panama.
+ *
+ * Nothing here is exported, the same as the Panama provider: a program
+ * depends on this module to have it, not to name it, and what it gets is a
+ * service {@code dev.zudb} finds on its own. Which of the two providers a
+ * program ended up on is a log line rather than a compile-time fact, which is
+ * the point of both.
+ */
+module dev.zudb.jni {
+ requires dev.zudb;
+
+ provides dev.zudb.spi.ZuProvider with
+ dev.zudb.jni.JniProvider;
+}
diff --git a/zudb-jni/src/main/resources/META-INF/services/dev.zudb.spi.ZuProvider b/zudb-jni/src/main/resources/META-INF/services/dev.zudb.spi.ZuProvider
new file mode 100644
index 0000000..349ce86
--- /dev/null
+++ b/zudb-jni/src/main/resources/META-INF/services/dev.zudb.spi.ZuProvider
@@ -0,0 +1 @@
+dev.zudb.jni.JniProvider
diff --git a/zudb-jni/src/test/java/dev/zudb/jni/JniTest.java b/zudb-jni/src/test/java/dev/zudb/jni/JniTest.java
new file mode 100644
index 0000000..c97c4cd
--- /dev/null
+++ b/zudb-jni/src/test/java/dev/zudb/jni/JniTest.java
@@ -0,0 +1,204 @@
+package dev.zudb.jni;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+import dev.zudb.Connection;
+import dev.zudb.Database;
+import dev.zudb.Frame;
+import dev.zudb.Result;
+import dev.zudb.Statement;
+import dev.zudb.ZuException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.LongBuffer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+/**
+ * The JNI provider, end to end.
+ *
+ *
The behaviour every provider owes is checked by the suite both of them
+ * run, and is not repeated here. What is here is the handful of things that
+ * are true of this provider and of no other: that a string survives a
+ * crossing JNI's own conversions would mangle, that a column is a window onto
+ * the engine's memory rather than a copy, that a callback arriving on a
+ * thread the JVM has never seen finds its way back into Java, and that a
+ * frame refuses a buffer it could not keep.
+ */
+class JniTest {
+
+ private static Database db;
+ private static Connection conn;
+
+ @BeforeAll
+ static void engine() {
+ Path library = locate();
+ assumeTrue(library != null, "no libzu: set -Dzu.library to run these");
+ System.setProperty("zu.library", library.toString());
+ // Panama wins on priority wherever it runs, and this suite is about
+ // the other one, so it is named rather than left to the contest.
+ System.setProperty("zu.provider", "jni");
+ db = Database.memory();
+ conn = db.connect();
+ }
+
+ @AfterAll
+ static void done() {
+ if (conn != null) {
+ conn.close();
+ }
+ if (db != null) {
+ db.close();
+ }
+ }
+
+ /**
+ * The reason every string in this provider is a byte array.
+ *
+ *
JNI's own {@code NewStringUTF} and {@code GetStringUTFChars} speak
+ * modified UTF-8, in which a character outside the basic multilingual plane
+ * is a surrogate pair in six bytes rather than the four the standard says.
+ * Hand one of those to an engine that validates real UTF-8 and it is
+ * refused; read one back through the same conversion and it comes out
+ * mangled. This is the test that would fail the day somebody decided a
+ * jstring would be tidier.
+ */
+ @Test
+ void aStringOutsideTheBasicMultilingualPlaneCrossesIntact() {
+ String[] awkward = {
+ "ada", // plain
+ "héllo wörld", // two bytes a character
+ "日本語", // three
+ "🜛 alchemy 🝗", // four, which is where modified UTF-8 goes wrong
+ "👩💻", // a surrogate pair inside a longer sequence
+ };
+ for (String s : awkward) {
+ try (Statement stmt = conn.prepare("RETURN $s AS echoed")) {
+ try (Result r = stmt.bind("s", s).execute()) {
+ assertEquals(s, r.row(0).getString(0), s);
+ }
+ }
+ }
+ }
+
+ @Test
+ void aColumnIsAWindowOntoTheEnginesOwnMemory() {
+ try (Result r = conn.query("UNWIND [1, 2, 3, 4, 5] AS n RETURN n")) {
+ LongBuffer column = r.longs(0);
+ assertNotNull(column, "an integer column reads as a buffer");
+ assertTrue(column.isDirect(), "and the buffer is the engine's, not a copy of it");
+ assertEquals(5, column.remaining());
+ long[] read = new long[5];
+ column.get(read);
+ assertArrayEquals(new long[] {1, 2, 3, 4, 5}, read);
+ }
+ }
+
+ /**
+ * A callback arrives on a thread of the engine's, which the JVM has never
+ * seen, so the shim attaches it for the call and detaches after. A thread
+ * that exits while attached takes the process with it, so a green run of
+ * this test is also the check that it does not.
+ */
+ @Test
+ void progressArrivesFromAThreadTheJvmHasNeverSeen() {
+ long rows = 3000;
+ ByteBuffer bytes = ByteBuffer.allocateDirect((int) rows * 8).order(ByteOrder.nativeOrder());
+ LongBuffer ids = bytes.asLongBuffer();
+ for (long i = 0; i < rows; i++) {
+ ids.put((int) i, i);
+ }
+ AtomicLong seen = new AtomicLong();
+ Set threads = ConcurrentHashMap.newKeySet();
+ Thread asked = Thread.currentThread();
+ try (Frame frame = Frame.of("Person", rows)) {
+ frame.column("id", ids);
+ conn.register(frame);
+ conn.onProgress(
+ Duration.ofMillis(1),
+ (read, millis) -> {
+ seen.incrementAndGet();
+ threads.add(Thread.currentThread());
+ return true;
+ });
+ try (Result r =
+ conn.query("MATCH (a:Person), (b:Person) WHERE a.id < b.id RETURN count(*)")) {
+ assertTrue(r.row(0).getLong(0) > 0);
+ } finally {
+ conn.clearProgress();
+ conn.unregister("Person");
+ }
+ }
+ assertTrue(seen.get() > 0, "a cross product of nine million rows went by without a word");
+ assertFalse(threads.contains(asked), "the callback ran on the thread that asked");
+ }
+
+ /**
+ * A frame keeps the pointer it is given for as long as it is registered,
+ * and a heap buffer has no pointer anything outside the JVM can keep. A
+ * copy would be a frame that is not a frame, so it is refused instead.
+ */
+ @Test
+ void aFrameRefusesABufferOnTheHeap() {
+ try (Frame frame = Frame.of("Heaped", 2)) {
+ ZuException e =
+ assertThrows(
+ ZuException.class,
+ () -> frame.column("n", ByteBuffer.allocate(16).asLongBuffer()));
+ assertTrue(e.getMessage().contains("allocateDirect"), e.getMessage());
+ }
+ }
+
+ @Test
+ void aFrameOverDirectMemoryIsQueriedWhereItLies() {
+ ByteBuffer bytes = ByteBuffer.allocateDirect(24).order(ByteOrder.nativeOrder());
+ LongBuffer values = bytes.asLongBuffer();
+ values.put(new long[] {7, 8, 9}).flip();
+ try (Frame frame = Frame.of("Lent", 3)) {
+ frame.column("n", values);
+ conn.register(frame);
+ try (Result r = conn.query("MATCH (l:Lent) RETURN l.n AS n ORDER BY n")) {
+ assertEquals(List.of(7L, 8L, 9L), r.stream().map(row -> row.getLong(0)).toList());
+ } finally {
+ conn.unregister("Lent");
+ }
+ }
+ }
+
+ private static Path locate() {
+ String named = System.getProperty("zu.library");
+ if (named == null || named.isBlank()) {
+ named = System.getenv("ZU_LIBRARY");
+ }
+ if (named != null && !named.isBlank()) {
+ Path p = Paths.get(named);
+ return Files.isRegularFile(p) ? p : null;
+ }
+ String name = System.mapLibraryName("zu");
+ Path here = Paths.get("").toAbsolutePath();
+ for (Path root = here; root != null; root = root.getParent()) {
+ for (String sibling : new String[] {"zu", "zu-dx", "zu-g0"}) {
+ Path candidate = root.resolveSibling(sibling).resolve("target/release").resolve(name);
+ if (Files.isRegularFile(candidate)) {
+ return candidate;
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/zudb-tck/pom.xml b/zudb-tck/pom.xml
new file mode 100644
index 0000000..1122a4b
--- /dev/null
+++ b/zudb-tck/pom.xml
@@ -0,0 +1,67 @@
+
+
+
+ 4.0.0
+
+
+ dev.zudb
+ zudb-parent
+ 0.11.0-SNAPSHOT
+
+
+ zudb-tck
+ zu for the JVM: the cases every provider owes
+ One suite, run against both providers, so that neither can quietly differ from the other.
+
+
+
+ dev.zudb
+ zudb
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ compile
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+ ${zu.release.api}
+
+
+ -Xlint:all,-requires-automatic,-requires-transitive-automatic,-missing-explicit-ctor
+ -Werror
+
+
+
+
+
+
diff --git a/zudb-ffm/src/test/java/dev/zudb/ffm/AppenderTest.java b/zudb-tck/src/main/java/dev/zudb/tck/AppenderTest.java
similarity index 99%
rename from zudb-ffm/src/test/java/dev/zudb/ffm/AppenderTest.java
rename to zudb-tck/src/main/java/dev/zudb/tck/AppenderTest.java
index 9232e18..0bc8fa2 100644
--- a/zudb-ffm/src/test/java/dev/zudb/ffm/AppenderTest.java
+++ b/zudb-tck/src/main/java/dev/zudb/tck/AppenderTest.java
@@ -1,4 +1,4 @@
-package dev.zudb.ffm;
+package dev.zudb.tck;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -27,7 +27,7 @@
* DDL and a bulk load is the only thing that makes a table for an appender to
* append to.
*/
-class AppenderTest {
+public class AppenderTest {
@TempDir Path dir;
diff --git a/zudb-ffm/src/test/java/dev/zudb/ffm/ColumnarTest.java b/zudb-tck/src/main/java/dev/zudb/tck/ColumnarTest.java
similarity index 99%
rename from zudb-ffm/src/test/java/dev/zudb/ffm/ColumnarTest.java
rename to zudb-tck/src/main/java/dev/zudb/tck/ColumnarTest.java
index 2241c82..66efa71 100644
--- a/zudb-ffm/src/test/java/dev/zudb/ffm/ColumnarTest.java
+++ b/zudb-tck/src/main/java/dev/zudb/tck/ColumnarTest.java
@@ -1,4 +1,4 @@
-package dev.zudb.ffm;
+package dev.zudb.tck;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -25,7 +25,7 @@
* tests are for is that it is the right memory, in the right order, and that
* it is read-only so that nobody writes into the result by accident.
*/
-class ColumnarTest {
+public class ColumnarTest {
private static Database db;
private static Connection conn;
diff --git a/zudb-ffm/src/test/java/dev/zudb/ffm/DatabaseTest.java b/zudb-tck/src/main/java/dev/zudb/tck/DatabaseTest.java
similarity index 98%
rename from zudb-ffm/src/test/java/dev/zudb/ffm/DatabaseTest.java
rename to zudb-tck/src/main/java/dev/zudb/tck/DatabaseTest.java
index 0198cac..f68ec8d 100644
--- a/zudb-ffm/src/test/java/dev/zudb/ffm/DatabaseTest.java
+++ b/zudb-tck/src/main/java/dev/zudb/tck/DatabaseTest.java
@@ -1,4 +1,4 @@
-package dev.zudb.ffm;
+package dev.zudb.tck;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -18,7 +18,7 @@
import org.junit.jupiter.api.io.TempDir;
/** Opening, connecting, and closing, in that order and the reverse. */
-class DatabaseTest {
+public class DatabaseTest {
@BeforeAll
static void engine() {
diff --git a/zudb-ffm/src/test/java/dev/zudb/ffm/ErrorTest.java b/zudb-tck/src/main/java/dev/zudb/tck/ErrorTest.java
similarity index 98%
rename from zudb-ffm/src/test/java/dev/zudb/ffm/ErrorTest.java
rename to zudb-tck/src/main/java/dev/zudb/tck/ErrorTest.java
index 1f82c5c..6d36ec3 100644
--- a/zudb-ffm/src/test/java/dev/zudb/ffm/ErrorTest.java
+++ b/zudb-tck/src/main/java/dev/zudb/tck/ErrorTest.java
@@ -1,4 +1,4 @@
-package dev.zudb.ffm;
+package dev.zudb.tck;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -21,7 +21,7 @@
* The whole point of the error model is that a caller reads fields rather
* than a message. These tests are what says the fields actually arrive.
*/
-class ErrorTest {
+public class ErrorTest {
private static Database db;
private static Connection conn;
diff --git a/zudb-ffm/src/test/java/dev/zudb/ffm/FrameTest.java b/zudb-tck/src/main/java/dev/zudb/tck/FrameTest.java
similarity index 99%
rename from zudb-ffm/src/test/java/dev/zudb/ffm/FrameTest.java
rename to zudb-tck/src/main/java/dev/zudb/tck/FrameTest.java
index 958cc18..bd8c150 100644
--- a/zudb-ffm/src/test/java/dev/zudb/ffm/FrameTest.java
+++ b/zudb-tck/src/main/java/dev/zudb/tck/FrameTest.java
@@ -1,4 +1,4 @@
-package dev.zudb.ffm;
+package dev.zudb.tck;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -33,7 +33,7 @@
* Querying memory the test already holds, without any of it getting into a
* database.
*/
-class FrameTest {
+public class FrameTest {
@TempDir Path dir;
diff --git a/zudb-ffm/src/test/java/dev/zudb/ffm/Libzu.java b/zudb-tck/src/main/java/dev/zudb/tck/Libzu.java
similarity index 78%
rename from zudb-ffm/src/test/java/dev/zudb/ffm/Libzu.java
rename to zudb-tck/src/main/java/dev/zudb/tck/Libzu.java
index ad86b89..c9631f8 100644
--- a/zudb-ffm/src/test/java/dev/zudb/ffm/Libzu.java
+++ b/zudb-tck/src/main/java/dev/zudb/tck/Libzu.java
@@ -1,4 +1,4 @@
-package dev.zudb.ffm;
+package dev.zudb.tck;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
@@ -17,15 +17,20 @@
*
Point them at one with {@code -Dzu.library=/path/to/libzu.dylib}, or set
* {@code ZU_LIBRARY}. A sibling checkout of the engine with a release build in
* it is found on its own.
+ *
+ *
Which provider the suite runs on is not decided here. Each provider
+ * module names its own with {@code -Dzu.provider}, so that the same cases are
+ * run twice and a difference between the two is a red test rather than
+ * something a user finds.
*/
-final class Libzu {
+public final class Libzu {
private Libzu() {}
private static final Path FOUND = locate();
/** Skips the calling test when there is no engine to call. */
- static void require() {
+ public static void require() {
assumeTrue(FOUND != null, "no libzu: set -Dzu.library to run these");
if (System.getProperty("zu.library") == null) {
System.setProperty("zu.library", FOUND.toString());
@@ -42,8 +47,8 @@ private static Path locate() {
return Files.isRegularFile(p) ? p : null;
}
String name = System.mapLibraryName("zu");
- // Up out of zudb-ffm, out of the repository, and into whichever
- // checkout of the engine is beside it.
+ // Up out of whichever provider module is running this, out of the
+ // repository, and into whichever checkout of the engine is beside it.
Path here = Paths.get("").toAbsolutePath();
for (Path root = here; root != null; root = root.getParent()) {
for (String sibling : new String[] {"zu", "zu-dx", "zu-g0"}) {
diff --git a/zudb-ffm/src/test/java/dev/zudb/ffm/LoaderTest.java b/zudb-tck/src/main/java/dev/zudb/tck/LoaderTest.java
similarity index 99%
rename from zudb-ffm/src/test/java/dev/zudb/ffm/LoaderTest.java
rename to zudb-tck/src/main/java/dev/zudb/tck/LoaderTest.java
index 671fb9f..a831492 100644
--- a/zudb-ffm/src/test/java/dev/zudb/ffm/LoaderTest.java
+++ b/zudb-tck/src/main/java/dev/zudb/tck/LoaderTest.java
@@ -1,4 +1,4 @@
-package dev.zudb.ffm;
+package dev.zudb.tck;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -26,7 +26,7 @@
import org.junit.jupiter.api.io.TempDir;
/** Building a database out of columns, which is the only way a table comes into being. */
-class LoaderTest {
+public class LoaderTest {
@TempDir Path dir;
diff --git a/zudb-ffm/src/test/java/dev/zudb/ffm/ProgressTest.java b/zudb-tck/src/main/java/dev/zudb/tck/ProgressTest.java
similarity index 99%
rename from zudb-ffm/src/test/java/dev/zudb/ffm/ProgressTest.java
rename to zudb-tck/src/main/java/dev/zudb/tck/ProgressTest.java
index 6f62d6c..539b79f 100644
--- a/zudb-ffm/src/test/java/dev/zudb/ffm/ProgressTest.java
+++ b/zudb-tck/src/main/java/dev/zudb/tck/ProgressTest.java
@@ -1,4 +1,4 @@
-package dev.zudb.ffm;
+package dev.zudb.tck;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -32,7 +32,7 @@
* thousand rows against themselves, which is a third of a second and is not
* something the planner can fold into a count.
*/
-class ProgressTest {
+public class ProgressTest {
private static final int ROWS = 3_000;
diff --git a/zudb-ffm/src/test/java/dev/zudb/ffm/QueryTest.java b/zudb-tck/src/main/java/dev/zudb/tck/QueryTest.java
similarity index 99%
rename from zudb-ffm/src/test/java/dev/zudb/ffm/QueryTest.java
rename to zudb-tck/src/main/java/dev/zudb/tck/QueryTest.java
index 050c207..34e6774 100644
--- a/zudb-ffm/src/test/java/dev/zudb/ffm/QueryTest.java
+++ b/zudb-tck/src/main/java/dev/zudb/tck/QueryTest.java
@@ -1,4 +1,4 @@
-package dev.zudb.ffm;
+package dev.zudb.tck;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -21,7 +21,7 @@
import org.junit.jupiter.api.Test;
/** Reading rows, which is what almost every program does with this client. */
-class QueryTest {
+public class QueryTest {
private static Database db;
private static Connection conn;
diff --git a/zudb-ffm/src/test/java/dev/zudb/ffm/ShorthandTest.java b/zudb-tck/src/main/java/dev/zudb/tck/ShorthandTest.java
similarity index 98%
rename from zudb-ffm/src/test/java/dev/zudb/ffm/ShorthandTest.java
rename to zudb-tck/src/main/java/dev/zudb/tck/ShorthandTest.java
index 64b5951..afcd671 100644
--- a/zudb-ffm/src/test/java/dev/zudb/ffm/ShorthandTest.java
+++ b/zudb-tck/src/main/java/dev/zudb/tck/ShorthandTest.java
@@ -1,4 +1,4 @@
-package dev.zudb.ffm;
+package dev.zudb.tck;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -26,7 +26,7 @@
* objects, and a program reading its settings out of a file should not have to
* know which of three fields a key lands in.
*/
-class ShorthandTest {
+public class ShorthandTest {
@BeforeAll
static void engine() {
diff --git a/zudb-ffm/src/test/java/dev/zudb/ffm/StatementTest.java b/zudb-tck/src/main/java/dev/zudb/tck/StatementTest.java
similarity index 99%
rename from zudb-ffm/src/test/java/dev/zudb/ffm/StatementTest.java
rename to zudb-tck/src/main/java/dev/zudb/tck/StatementTest.java
index 1f4449e..a9169e7 100644
--- a/zudb-ffm/src/test/java/dev/zudb/ffm/StatementTest.java
+++ b/zudb-tck/src/main/java/dev/zudb/tck/StatementTest.java
@@ -1,4 +1,4 @@
-package dev.zudb.ffm;
+package dev.zudb.tck;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -26,7 +26,7 @@
import org.junit.jupiter.api.Test;
/** Preparing once and running many times, which is what a loop wants. */
-class StatementTest {
+public class StatementTest {
private static Database db;
private static Connection conn;
diff --git a/zudb-ffm/src/test/java/dev/zudb/ffm/TransactionTest.java b/zudb-tck/src/main/java/dev/zudb/tck/TransactionTest.java
similarity index 98%
rename from zudb-ffm/src/test/java/dev/zudb/ffm/TransactionTest.java
rename to zudb-tck/src/main/java/dev/zudb/tck/TransactionTest.java
index 6f3c1de..7a81a6f 100644
--- a/zudb-ffm/src/test/java/dev/zudb/ffm/TransactionTest.java
+++ b/zudb-tck/src/main/java/dev/zudb/tck/TransactionTest.java
@@ -1,4 +1,4 @@
-package dev.zudb.ffm;
+package dev.zudb.tck;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -12,7 +12,7 @@
import org.junit.jupiter.api.Test;
/** Beginning, committing and rolling back, and the block that does all three. */
-class TransactionTest {
+public class TransactionTest {
@BeforeAll
static void engine() {
diff --git a/zudb-ffm/src/test/java/dev/zudb/ffm/ValueTest.java b/zudb-tck/src/main/java/dev/zudb/tck/ValueTest.java
similarity index 98%
rename from zudb-ffm/src/test/java/dev/zudb/ffm/ValueTest.java
rename to zudb-tck/src/main/java/dev/zudb/tck/ValueTest.java
index 12ae8a1..d108328 100644
--- a/zudb-ffm/src/test/java/dev/zudb/ffm/ValueTest.java
+++ b/zudb-tck/src/main/java/dev/zudb/tck/ValueTest.java
@@ -1,4 +1,4 @@
-package dev.zudb.ffm;
+package dev.zudb.tck;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
@@ -17,7 +17,7 @@
* The values that have no column to be read into: lists, records and the
* trees they make.
*/
-class ValueTest {
+public class ValueTest {
private static Database db;
private static Connection conn;
diff --git a/zudb-ffm/src/test/java/dev/zudb/ffm/ZuTest.java b/zudb-tck/src/main/java/dev/zudb/tck/ZuTest.java
similarity index 70%
rename from zudb-ffm/src/test/java/dev/zudb/ffm/ZuTest.java
rename to zudb-tck/src/main/java/dev/zudb/tck/ZuTest.java
index a2551d3..2f22602 100644
--- a/zudb-ffm/src/test/java/dev/zudb/ffm/ZuTest.java
+++ b/zudb-tck/src/main/java/dev/zudb/tck/ZuTest.java
@@ -1,4 +1,4 @@
-package dev.zudb.ffm;
+package dev.zudb.tck;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -9,7 +9,7 @@
import org.junit.jupiter.api.Test;
/** What loaded, and what it says it is. */
-class ZuTest {
+public class ZuTest {
@BeforeAll
static void engine() {
@@ -17,9 +17,14 @@ static void engine() {
}
@Test
- void theProviderIsTheOneThisArtifactShips() {
- assertEquals("ffm", Zu.availableProvider().orElseThrow());
- assertEquals("ffm", Zu.provider());
+ void theProviderIsTheOneThisRunNamed() {
+ // These cases are run once per provider and the run says which, so
+ // this checks that the naming took rather than that a particular one
+ // won. A run in which it did not take would be a run of the same
+ // provider twice, which is the failure this catches.
+ String named = System.getProperty("zu.provider", "ffm");
+ assertEquals(named, Zu.availableProvider().orElseThrow());
+ assertEquals(named, Zu.provider());
}
@Test
diff --git a/zudb-tck/src/main/java/module-info.java b/zudb-tck/src/main/java/module-info.java
new file mode 100644
index 0000000..778e9c5
--- /dev/null
+++ b/zudb-tck/src/main/java/module-info.java
@@ -0,0 +1,20 @@
+/**
+ * The cases every provider owes, and the fixture that finds an engine to run
+ * them against.
+ *
+ *
These are main sources rather than test sources because two other
+ * modules run them, and a test source set is not a thing another module can
+ * depend on without a test jar and the trouble that comes with one.
+ *
+ *
The module is open rather than exporting to a list, because the
+ * reflection that reaches a test method comes from the platform, from the
+ * engine and from an extension, and a list of three would be a list that goes
+ * stale. The package is exported as well so that a provider module can name
+ * the fixture in a test of its own.
+ */
+open module dev.zudb.tck {
+ requires dev.zudb;
+ requires transitive org.junit.jupiter.api;
+
+ exports dev.zudb.tck;
+}
diff --git a/zudb/src/main/java/dev/zudb/Library.java b/zudb/src/main/java/dev/zudb/Library.java
index bd8308c..8dcfc88 100644
--- a/zudb/src/main/java/dev/zudb/Library.java
+++ b/zudb/src/main/java/dev/zudb/Library.java
@@ -1,15 +1,11 @@
package dev.zudb;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UncheckedIOException;
+import dev.zudb.spi.Natives;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.List;
-import java.util.Locale;
/**
* Where libzu is.
@@ -76,9 +72,10 @@ static Found find() {
}
looked.add(ENVIRONMENT);
- String flavour = flavour();
- String resource = "dev/zudb/native/" + flavour + "/" + System.mapLibraryName("zu");
- Path unpacked = unpack(resource);
+ String flavour = Natives.flavour();
+ String library = System.mapLibraryName("zu");
+ String resource = "dev/zudb/native/" + flavour + "/" + library;
+ Path unpacked = Natives.unpack(resource, library, Library.class);
if (unpacked != null) {
return new Found(unpacked, "the zudb-native artifact, " + flavour, looked);
}
@@ -99,100 +96,4 @@ static Found find() {
Paths.get(System.mapLibraryName("zu")), "the platform library path", looked);
}
- /**
- * The name this client gives the operating system and the instruction set,
- * which is Go's spelling of both, because that is what the library
- * artifacts in every other client of this engine are named after.
- *
- * @return for example {@code darwin-arm64}
- */
- static String platform() {
- String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
- String arch = System.getProperty("os.arch", "").toLowerCase(Locale.ROOT);
-
- String goos;
- if (os.startsWith("mac") || os.startsWith("darwin")) {
- goos = "darwin";
- } else if (os.startsWith("win")) {
- goos = "windows";
- } else if (os.startsWith("linux")) {
- goos = "linux";
- } else {
- goos = os.split("\\s")[0];
- }
-
- String goarch;
- if (arch.equals("x86_64") || arch.equals("amd64")) {
- goarch = "amd64";
- } else if (arch.equals("aarch64") || arch.equals("arm64")) {
- goarch = "arm64";
- } else {
- goarch = arch;
- }
-
- return goos + "-" + goarch;
- }
-
- /**
- * The same, and which C library on the platform where there are two.
- *
- *
Alpine is not a smaller Linux, it is a different one: a shared
- * object built against glibc does not load on musl and says so in a message
- * about an interpreter rather than about a database. The two builds are two
- * artifacts everywhere else this engine ships, so they are two here as well,
- * and the choice is made by looking for musl's own loader, which is the one
- * file whose path is fixed by the ABI rather than by a distribution.
- *
- *
Nowhere but Linux has a second answer, so nowhere but Linux is asked.
- *
- * @return for example {@code darwin-arm64} or {@code linux-amd64-musl}
- */
- static String flavour() {
- String platform = platform();
- if (!platform.startsWith("linux-")) {
- return platform;
- }
- return musl() ? platform + "-musl" : platform;
- }
-
- /** Whether this is a musl system, by its loader rather than by its name. */
- private static boolean musl() {
- for (String loader :
- new String[] {"/lib/ld-musl-x86_64.so.1", "/lib/ld-musl-aarch64.so.1"}) {
- if (Files.exists(Paths.get(loader))) {
- return true;
- }
- }
- return false;
- }
-
- /**
- * Copies a library out of the classpath, because a library inside a jar is
- * not a file and every loader on every platform wants a file.
- *
- * @param resource where it is
- * @return the copy, or null if there is no such resource
- */
- static Path unpack(String resource) {
- ClassLoader loader = Library.class.getClassLoader();
- try (InputStream in =
- loader == null
- ? ClassLoader.getSystemResourceAsStream(resource)
- : loader.getResourceAsStream(resource)) {
- if (in == null) {
- return null;
- }
- Path dir = Files.createTempDirectory("zudb");
- Path file = dir.resolve(System.mapLibraryName("zu"));
- Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING);
- // Best effort, and it fails on Windows for a library still mapped
- // into the process. A file in the temp directory is what the
- // operating system already cleans up after.
- file.toFile().deleteOnExit();
- dir.toFile().deleteOnExit();
- return file;
- } catch (IOException e) {
- throw new UncheckedIOException("could not unpack " + resource, e);
- }
- }
}
diff --git a/zudb/src/main/java/dev/zudb/spi/Natives.java b/zudb/src/main/java/dev/zudb/spi/Natives.java
new file mode 100644
index 0000000..2a3944c
--- /dev/null
+++ b/zudb/src/main/java/dev/zudb/spi/Natives.java
@@ -0,0 +1,129 @@
+package dev.zudb.spi;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.Locale;
+
+/**
+ * What a provider needs to know about the machine it is on.
+ *
+ *
Two of the three questions here are asked twice in this repository: the
+ * API module asks them to find libzu, and the JNI provider asks them again to
+ * find its own shim, which is a second library and a different file with the
+ * same seven platforms under it. They are answered here so that the two
+ * cannot come to disagree about what a platform is called, which would show up
+ * as a jar that has the library for the machine it is on and cannot find it.
+ *
+ *
Like everything else in this package, this is not part of the supported
+ * surface.
+ */
+public final class Natives {
+
+ private Natives() {}
+
+ /**
+ * The name this client gives the operating system and the instruction set,
+ * which is Go's spelling of both, because that is what the library
+ * artifacts in every other client of this engine are named after.
+ *
+ * @return for example {@code darwin-arm64}
+ */
+ public static String platform() {
+ String os = System.getProperty("os.name", "").toLowerCase(Locale.ROOT);
+ String arch = System.getProperty("os.arch", "").toLowerCase(Locale.ROOT);
+
+ String goos;
+ if (os.startsWith("mac") || os.startsWith("darwin")) {
+ goos = "darwin";
+ } else if (os.startsWith("win")) {
+ goos = "windows";
+ } else if (os.startsWith("linux")) {
+ goos = "linux";
+ } else {
+ goos = os.split("\\s")[0];
+ }
+
+ String goarch;
+ if (arch.equals("x86_64") || arch.equals("amd64")) {
+ goarch = "amd64";
+ } else if (arch.equals("aarch64") || arch.equals("arm64")) {
+ goarch = "arm64";
+ } else {
+ goarch = arch;
+ }
+
+ return goos + "-" + goarch;
+ }
+
+ /**
+ * The same, and which C library on the platform where there are two.
+ *
+ *
Alpine is not a smaller Linux, it is a different one: a shared object
+ * built against glibc does not load on musl and says so in a message about
+ * an interpreter rather than about a database. The two builds are two
+ * artifacts everywhere else this engine ships, so they are two here as well,
+ * and the choice is made by looking for musl's own loader, which is the one
+ * file whose path is fixed by the ABI rather than by a distribution.
+ *
+ *
Nowhere but Linux has a second answer, so nowhere but Linux is asked.
+ *
+ * @return for example {@code darwin-arm64} or {@code linux-amd64-musl}
+ */
+ public static String flavour() {
+ String platform = platform();
+ if (!platform.startsWith("linux-")) {
+ return platform;
+ }
+ return musl() ? platform + "-musl" : platform;
+ }
+
+ /** Whether this is a musl system, by its loader rather than by its name. */
+ private static boolean musl() {
+ for (String loader : new String[] {"/lib/ld-musl-x86_64.so.1", "/lib/ld-musl-aarch64.so.1"}) {
+ if (Files.exists(Paths.get(loader))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Copies a library out of the classpath, because a library inside a jar is
+ * not a file and every loader on every platform wants a file.
+ *
+ * @param resource where it is
+ * @param name what to call the copy, which matters because a loader reports
+ * the file name it failed on and a temporary name would be no help
+ * @param near a class of the artifact the resource is in, whose loader is
+ * the one asked, because the API module and the JNI provider are two
+ * artifacts and on a module path they are two loaders as well
+ * @return the copy, or null if there is no such resource
+ */
+ public static Path unpack(String resource, String name, Class> near) {
+ ClassLoader loader = near.getClassLoader();
+ try (InputStream in =
+ loader == null
+ ? ClassLoader.getSystemResourceAsStream(resource)
+ : loader.getResourceAsStream(resource)) {
+ if (in == null) {
+ return null;
+ }
+ Path dir = Files.createTempDirectory("zudb");
+ Path file = dir.resolve(name);
+ Files.copy(in, file, StandardCopyOption.REPLACE_EXISTING);
+ // Best effort, and it fails on Windows for a library still mapped
+ // into the process. A file in the temp directory is what the
+ // operating system already cleans up after.
+ file.toFile().deleteOnExit();
+ dir.toFile().deleteOnExit();
+ return file;
+ } catch (IOException e) {
+ throw new UncheckedIOException("could not unpack " + resource, e);
+ }
+ }
+}
diff --git a/zudb/src/test/java/dev/zudb/LibraryTest.java b/zudb/src/test/java/dev/zudb/LibraryTest.java
index 9057e1b..7ea2028 100644
--- a/zudb/src/test/java/dev/zudb/LibraryTest.java
+++ b/zudb/src/test/java/dev/zudb/LibraryTest.java
@@ -8,6 +8,7 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
+import dev.zudb.spi.Natives;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@@ -48,7 +49,7 @@ void thePlatformIsSpelledTheWayTheArtifactsAre() {
// Go's spelling, because every library artifact this engine publishes is
// named after it, and two spellings of one platform is how a client ends
// up unable to find its own jar.
- String platform = Library.platform();
+ String platform = Natives.platform();
assertTrue(
platform.matches("(darwin|linux|windows|[a-z0-9]+)-(amd64|arm64|[a-z0-9_]+)"),
platform + " is not a goos-goarch pair");
@@ -60,8 +61,8 @@ void theCLibraryIsPartOfTheAnswerOnLinuxAndNowhereElse() {
// A shared object built against glibc does not load on musl, so the two
// are two artifacts. Everywhere else there is one C library and nothing
// to say about it.
- String platform = Library.platform();
- String flavour = Library.flavour();
+ String platform = Natives.platform();
+ String flavour = Natives.flavour();
if (platform.startsWith("linux-")) {
assertTrue(
flavour.equals(platform) || flavour.equals(platform + "-musl"),
@@ -78,7 +79,7 @@ void aLibraryInAJarBecomesAFileWithTheSameBytes() throws Exception {
// a library, deliberately: what is being checked is the copy, and a real
// one would only make the test slower and platform-specific.
String resource = "dev/zudb/native/a-platform-that-is-not-one/libzu.stand-in";
- Path unpacked = Library.unpack(resource);
+ Path unpacked = Natives.unpack(resource, "libzu.stand-in", Library.class);
assertTrue(unpacked != null, "the stand-in is not on the test classpath");
assertTrue(Files.isRegularFile(unpacked));
byte[] want;
@@ -88,7 +89,7 @@ void aLibraryInAJarBecomesAFileWithTheSameBytes() throws Exception {
assertArrayEquals(want, Files.readAllBytes(unpacked));
// A directory of its own each time, so two callers cannot land on one
// file and so a copy cannot be made over a library already mapped.
- Path again = Library.unpack(resource);
+ Path again = Natives.unpack(resource, "libzu.stand-in", Library.class);
assertNotEquals(unpacked, again);
}
@@ -116,7 +117,7 @@ void theSearchSaysWhatItRuledOutOnTheWay() {
void aPlatformWithNoArtifactIsNotAFailure() {
// The classpath is the third of four places, so nothing there means the
// search carries on to the platform's own rather than stopping.
- assertNull(Library.unpack("dev/zudb/native/vax-11-780/libzu.so"));
+ assertNull(Natives.unpack("dev/zudb/native/vax-11-780/libzu.so", "libzu.so", Library.class));
}
private static void restore(String before) {