diff --git a/ext/jni/GNUmakefile b/ext/jni/GNUmakefile index 1be4c04439..e3dfb53c1d 100644 --- a/ext/jni/GNUmakefile +++ b/ext/jni/GNUmakefile @@ -68,6 +68,7 @@ JAVA_FILES.main := $(patsubst %,$(dir.src.jni)/%,\ ResultCode.java \ RollbackHook.java \ SQLFunction.java \ + SQLLog.java \ sqlite3_context.java \ sqlite3.java \ SQLite3Jni.java \ @@ -160,17 +161,16 @@ SQLITE_OPT = \ -DSQLITE_ENABLE_DBSTAT_VTAB \ -DSQLITE_ENABLE_BYTECODE_VTAB \ -DSQLITE_ENABLE_OFFSET_SQL_FUNC \ + -DSQLITE_ENABLE_PREUPDATE_HOOK \ + -DSQLITE_ENABLE_SQLLOG \ -DSQLITE_OMIT_LOAD_EXTENSION \ -DSQLITE_OMIT_DEPRECATED \ -DSQLITE_OMIT_SHARED_CACHE \ - -DSQLITE_THREADSAFE=0 \ + -DSQLITE_THREADSAFE=1 \ -DSQLITE_TEMP_STORE=2 \ -DSQLITE_USE_URI=1 \ - -DSQLITE_C=$(sqlite3.c) -# -DSQLITE_DEBUG -# -DSQLITE_DEBUG is just to work around a -Wall warning -# for a var which gets set in all builds but only read -# via assert(). + -DSQLITE_C=$(sqlite3.c) \ + -DSQLITE_DEBUG SQLITE_OPT += -g -DDEBUG -UNDEBUG @@ -226,12 +226,16 @@ $(sqlite3-jni.dll): $(dir.bld.c) $(sqlite3-jni.c) $(SQLite3Jni.java) $(MAKEFILE) $(sqlite3-jni.c) -shared -o $@ all: $(sqlite3-jni.dll) -.PHONY: test -test.flags ?= -v -test: $(SQLite3Jni.class) $(sqlite3-jni.dll) - $(bin.java) -ea -Djava.library.path=$(dir.bld.c) \ - $(java.flags) -cp $(classpath) \ - org.sqlite.jni.Tester1 $(if $(test.flags),-- $(test.flags),) +.PHONY: test test-one +test.flags ?= +test.main.flags = -ea -Djava.library.path=$(dir.bld.c) \ + $(java.flags) -cp $(classpath) \ + org.sqlite.jni.Tester1 +test-one: $(SQLite3Jni.class) $(sqlite3-jni.dll) + $(bin.java) $(test.main.flags) $(test.flags) +test: test-one + @echo "Again in multi-threaded mode:"; + $(bin.java) $(test.main.flags) -t 5 -r 20 -shuffle $(test.flags) tester.scripts := $(sort $(wildcard $(dir.src)/tests/*.test)) tester.flags ?= # --verbose @@ -272,8 +276,7 @@ package.jar.in := $(abspath $(dir.src)/jar.in) CLEAN_FILES += $(package.jar.in) $(package.jar.in): $(MAKEFILE) $(CLASS_FILES.main) cd $(dir.src); ls -1 org/sqlite/jni/*.java org/sqlite/jni/*.class > $@ - @ls -la $@ - @echo "To use this jar you will need the -Djava.library.path=DIR/WITH/libsqlite3-jni.so flag." + @echo "To use this jar you will need the -Djava.library.path=DIR/CONTAINING/libsqlite3-jni.so flag." @echo "e.g. java -jar $@ -Djava.library.path=bld" $(package.jar): $(CLASS_FILES) $(MAKEFILE) $(package.jar.in) @@ -317,6 +320,10 @@ dist: \ $(bin.version-info) $(sqlite3.canonical.c) \ $(package.jar) $(MAKEFILE) @echo "Making end-user deliverables..." + @echo "****************************************************************************"; \ + echo "*** WARNING: be sure to build this with JDK8 (javac 1.8) for compatibility."; \ + echo "*** reasons!"; $$($(bin.javac) -version); \ + echo "****************************************************************************" @rm -fr $(dist-dir.top) @mkdir -p $(dist-dir.src) @cp -p $(dist.top.extras) $(dist-dir.top)/. diff --git a/ext/jni/README.md b/ext/jni/README.md index cb51a21cd3..6e5e07cc03 100644 --- a/ext/jni/README.md +++ b/ext/jni/README.md @@ -15,7 +15,10 @@ Technical support is available in the forum: > **FOREWARNING:** this subproject is very much in development and subject to any number of changes. Please do not rely on any - information about its API until this disclaimer is removed. + information about its API until this disclaimer is removed. The JNI + bindings released with version 3.43 are a "tech preview" and 3.44 + will be "final," at which point strong backward compatibility + guarantees will apply. Project goals/requirements: @@ -41,13 +44,34 @@ Non-goals: them off of the C-style API. -Significant TODOs -======================================================================== +Hello World +----------------------------------------------------------------------- -- The initial beta release with version 3.43 has severe threading - limitations. Namely, two threads cannot call into the JNI-bound API - at once. This limitation will be remove in a subsequent release. +```java +import org.sqlite.jni.*; +import static SQLite3Jni.*; +... + +final sqlite3 db = sqlite3_open(":memory:"); +try { + final int rc = sqlite3_errcode(db); + if( 0 != rc ){ + if( null != db ){ + System.out.print("Error opening db: "+sqlite3_errmsg(db)); + }else{ + System.out.print("Error opening db: rc="+rc); + } + ... handle error ... + } + // ... else use the db ... +}finally{ + // ALWAYS close databases using sqlite3_close() or sqlite3_close_v2() + // when done with them. All of their active statement handles must + // first have been passed to sqlite3_finalize(). + sqlite3_close_v2(db); +} +``` Building ======================================================================== @@ -60,55 +84,108 @@ The canonical builds assumes a Linux-like environment and requires: Put simply: -``` +```console $ export JAVA_HOME=/path/to/jdk/root $ make $ make test $ make clean ``` +The jar distribution can be created with `make jar`, but note that it +does not contain the binary DLL file. A different DLL is needed for +each target platform. + + One-to-One(-ish) Mapping to C ======================================================================== This JNI binding aims to provide as close to a 1-to-1 experience with -the C API as cross-language semantics allow. Exceptions are +the C API as cross-language semantics allow. Interface changes are necessarily made where cross-language semantics do not allow a 1-to-1, and judiciously made where a 1-to-1 mapping would be unduly cumbersome -to use in Java. +to use in Java. In all cases, this binding makes every effort to +provide semantics compatible with the C API documentation even if the +interface to those semantics is slightly different. Any cases which +deviate from those semantics (either removing or adding semantics) are +clearly documented. -Golden Rule: _Never_ Throw from Callbacks +Where it makes sense to do so for usability, Java-side overloads are +provided which accept or return data in alternative forms or provide +sensible default argument values. In all such cases they are thin +proxies around the corresponding C APIs and do not introduce new +semantics. + +In some very few cases, Java-specific capabilities have been added in +new APIs, all of which have "_java" somewhere in their names. +Examples include: + +- `sqlite3_result_java_object()` +- `sqlite3_column_java_object()` +- `sqlite3_column_java_casted()` +- `sqlite3_value_java_object()` +- `sqlite3_value_java_casted()` + +which, as one might surmise, collectively enable the passing of +arbitrary Java objects from user-defined SQL functions through to the +caller. + + +Golden Rule: Garbage Collection Cannot Free SQLite Resources ------------------------------------------------------------------------ -JNI bindings which accept client-defined functions _must never throw -exceptions_ unless _very explicitly documented_ as being -throw-safe. Exceptions are generally reserved for higher-level -bindings which are constructed to specifically deal with them and -ensure that they do not leak C-level resources. Some of the JNI -bindings are provided as Java functions which expect this rule to -always hold. +It is important that all databases and prepared statement handles get +cleaned up by client code. A database cannot be closed if it has open +statement handles. `sqlite3_close()` fails if the db cannot be closed +whereas `sqlite3_close_v2()` recognizes that case and marks the db as +a "zombie," pending finalization when the library detects that all +pending statements have been closed. Be aware that Java garbage +collection _cannot_ close a database or finalize a prepared statement. +Those things require explicit API calls. -UTF-8(-ish) + +Golden Rule #2: _Never_ Throw from Callbacks (Unless...) ------------------------------------------------------------------------ -SQLite internally uses UTF-8 encoding, whereas Java natively uses -UTF-16. Java JNI has routines for converting to and from UTF-8, _but_ -Java uses what its docs call "[modified UTF-8][modutf8]." Care must be -taken when converting Java strings to UTF-8 to ensure that the proper -conversion is performed. In short, -`String.getBytes(StandardCharsets.UTF_8)` performs the proper -conversion in Java, and there is no JNI C API for that conversion -(JNI's `NewStringUTF()` returns MUTF-8). +All routines in this API, barring explicitly documented exceptions, +retain C-like semantics. For example, they are not permitted to throw +or propagate exceptions and must return error information (if any) via +result codes or `null`. The only cases where the C-style APIs may +throw is through client-side misuse, e.g. passing in a null where it +shouldn't be used. The APIs clearly mark function parameters which +should not be null, but does not actively defend itself against such +misuse. Some C-style APIs explicitly accept `null` as a no-op for +usability's sake, and some of the JNI APIs deliberately return an +error code, instead of segfaulting, when passed a `null`. -Known consequences and limitations of this discrepancy include: +Client-defined callbacks _must never throw exceptions_ unless _very +explicitly documented_ as being throw-safe. Exceptions are generally +reserved for higher-level bindings which are constructed to +specifically deal with them and ensure that they do not leak C-level +resources. In some cases, callback handlers are permitted to throw, in +which cases they get translated to C-level result codes and/or +messages. If a callback which is not permitted to throw throws, its +exception may trigger debug output but will otherwise be suppressed. -- Names of databases, tables, and collations must not contain - characters which differ in MUTF-8 and UTF-8, or certain APIs will - mis-translate them on their way between languages. APIs which - transfer other client-side data to Java take extra care to - convert the data at the cost of performance. +The reason some callbacks are permitted to throw and others not is +because all such callbacks act as proxies for C function callback +interfaces and some of those interfaces have no error-reporting +mechanism. Those which are capable of propagating errors back through +the library convert exceptions from callbacks into corresponding +C-level error information. Those which cannot propagate errors +necessarily suppress any exceptions in order to maintain the C-style +semantics of the APIs. -[modutf8]: https://docs.oracle.com/javase/8/docs/api/java/io/DataInput.html#modified-utf-8 + +Awkward Callback Names +------------------------------------------------------------------------ + +In places where the Java interface uses callbacks (see below), those +callbacks often have what might fairly be labeled as awkward names, +e.g. `sqlScalarFunction.xFunc()` and `preupdateHook.xPreUpdate()`. +Those names were chosen because they match the corresponding arguments +in the C-level API docs. If they were renamed to be more Java-esque, +documentation transparency would suffer. Unwieldy Constructs are Re-mapped @@ -126,7 +203,7 @@ A prime example of where interface changes for Java are necessary for usability is [registration of a custom collation](https://sqlite.org/c3ref/create_collation.html): -``` +```c // C: int sqlite3_create_collation(sqlite3 * db, const char * name, int eTextRep, void *pUserData, @@ -145,7 +222,7 @@ passed that object as their first argument. That data is passed around bind that part as-is to Java, the result would be awkward to use (^Yes, we tried this.): -``` +```java // Java: int sqlite3_create_collation(sqlite3 db, String name, int eTextRep, Object pUserData, xCompareType xCompare); @@ -160,7 +237,7 @@ for callbacks and (B) having their internal state provided separately, which is ill-fitting in Java. For the sake of usability, C APIs which follow that pattern use a slightly different Java interface: -``` +```java int sqlite3_create_collation(sqlite3 db, String name, int eTextRep, Collation collation); ``` @@ -169,7 +246,7 @@ Where the `Collation` class has an abstract `xCompare()` method and no-op `xDestroy()` method which can be overridden if needed, leading to a much more Java-esque usage: -``` +```java int rc = sqlite3_create_collation(db, "mycollation", SQLITE_UTF8, new Collation(){ // Required comparison function: @@ -188,8 +265,8 @@ int rc = sqlite3_create_collation(db, "mycollation", SQLITE_UTF8, new Collation( Noting that: -- It is still possible to bind in call-scope-local state via closures, - if desired. +- It is possible to bind in call-scope-local state via closures, if + desired, as opposed to packing it into the Collation object. - No capabilities of the C API are lost or unduly obscured via the above API reshaping, so power users need not make any compromises. @@ -200,6 +277,7 @@ Noting that: overriding the `xDestroy()` method effectively gives it v2 semantics. + ### User-defined SQL Functions (a.k.a. UDFs) The [`sqlite3_create_function()`](https://sqlite.org/c3ref/create_function.html) @@ -207,12 +285,13 @@ family of APIs make heavy use of function pointers to provide client-defined callbacks, necessitating interface changes in the JNI binding. The Java API has only one core function-registration function: -``` +```java int sqlite3_create_function(sqlite3 db, String funcName, int nArgs, int encoding, SQLFunction func); ``` -> Design question: does the encoding argument serve any purpose in JS? +> Design question: does the encoding argument serve any purpose in + Java? That's as-yet undetermined. If not, it will be removed. `SQLFunction` is not used directly, but is instead instantiated via one of its three subclasses: @@ -230,5 +309,9 @@ Search [`Tester1.java`](/file/ext/jni/src/org/sqlite/jni/Tester1.java) for Reminder: see the disclaimer at the top of this document regarding the in-flux nature of this API. -[jsrc]: /file/ -[www]: https://sqlite.org +### And so on... + +Various APIs which accept callbacks, e.g. `sqlite3_trace_v2()` and +`sqlite3_update_hook()`, use interfaces similar to those shown above. +Despite the changes in signature, the JNI layer makes every effort to +provide the same semantics as the C API documentation suggests. diff --git a/ext/jni/jar-dist.make b/ext/jni/jar-dist.make index 23a26e4a87..7596c99f3f 100644 --- a/ext/jni/jar-dist.make +++ b/ext/jni/jar-dist.make @@ -6,7 +6,9 @@ # proper top-level JDK directory and, depending on the platform, add a # platform-specific -I directory. It should build as-is with any # 2020s-era version of gcc or clang. It requires JDK version 8 or -# higher. +# higher and that JAVA_HOME points to the top-most installation +# directory of that JDK. On Ubuntu-style systems the JDK is typically +# installed under /usr/lib/jvm/java-VERSION-PLATFORM. default: all @@ -31,10 +33,11 @@ SQLITE_OPT = \ -DSQLITE_OMIT_LOAD_EXTENSION \ -DSQLITE_OMIT_DEPRECATED \ -DSQLITE_OMIT_SHARED_CACHE \ - -DSQLITE_THREADSAFE=0 \ + -DSQLITE_THREADSAFE=1 \ -DSQLITE_TEMP_STORE=2 \ -DSQLITE_USE_URI=1 \ - -DSQLITE_ENABLE_FTS5 + -DSQLITE_ENABLE_FTS5 \ + -DSQLITE_DEBUG sqlite3-jni.dll = libsqlite3-jni.so $(sqlite3-jni.dll): @@ -46,8 +49,10 @@ $(sqlite3-jni.dll): src/sqlite3-jni.c -shared -o $@ @echo "Now try running it with: make test" +test.flags = -Djava.library.path=. sqlite3-jni-*.jar test: $(sqlite3-jni.dll) - java -jar -Djava.library.path=. sqlite3-jni-*.jar + java -jar $(test.flags) + java -jar $(test.flags) -t 7 -r 10 -shuffle clean: -rm -f $(sqlite3-jni.dll) diff --git a/ext/jni/src/c/sqlite3-jni.c b/ext/jni/src/c/sqlite3-jni.c index b28ea71144..7c4aa7c28b 100644 --- a/ext/jni/src/c/sqlite3-jni.c +++ b/ext/jni/src/c/sqlite3-jni.c @@ -13,15 +13,15 @@ ** org.sqlite.jni.SQLiteJni (from which sqlite3-jni.h is generated). */ -/** - If you found this comment by searching the code for - CallStaticObjectMethod then you're the victim of an OpenJDK bug: - - https://bugs.openjdk.org/browse/JDK-8130659 - - It's known to happen with OpenJDK v8 but not with v19. - - This code does not use JNI's CallStaticObjectMethod(). +/* +** If you found this comment by searching the code for +** CallStaticObjectMethod then you're the victim of an OpenJDK bug: +** +** https://bugs.openjdk.org/browse/JDK-8130659 +** +** It's known to happen with OpenJDK v8 but not with v19. +** +** This code does not use JNI's CallStaticObjectMethod(). */ /* @@ -61,9 +61,6 @@ #ifndef SQLITE_ENABLE_OFFSET_SQL_FUNC # define SQLITE_ENABLE_OFFSET_SQL_FUNC 1 #endif -#ifndef SQLITE_ENABLE_PREUPDATE_HOOK -# define SQLITE_ENABLE_PREUPDATE_HOOK 1 /*required by session extension*/ -#endif #ifndef SQLITE_ENABLE_RTREE # define SQLITE_ENABLE_RTREE 1 #endif @@ -105,7 +102,7 @@ # define SQLITE_TEMP_STORE 2 #endif #ifndef SQLITE_THREADSAFE -# define SQLITE_THREADSAFE 0 +# define SQLITE_THREADSAFE 1 #endif /**********************************************************************/ @@ -133,9 +130,13 @@ #undef INC__STRINGIFY #undef SQLITE_C +/* +** End of the sqlite3 lib setup. What follows is JNI-specific. +*/ + #include "sqlite3-jni.h" -#include /* only for testing/debugging */ #include +#include /* only for testing/debugging */ /* Only for debugging */ #define MARKER(pfexp) \ @@ -151,28 +152,30 @@ #define JDECL(ReturnType,Suffix) \ JNIEXPORT ReturnType JNICALL \ JFuncName(Suffix) -/** - Shortcuts for the first 2 parameters to all JNI bindings. - - The type of the jSelf arg differs, but no docs seem to mention - this: for static methods it's of type jclass and for non-static - it's jobject. jobject actually works for all funcs, in the sense - that it compiles and runs so long as we don't use jSelf (which is - only rarely needed in this code), but to be pedantically correct we - need the proper type in the signature. - - Not even the official docs mention this discrepancy: - - https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#jni_interface_functions_and_pointers +/* +** Shortcuts for the first 2 parameters to all JNI bindings. +** +** The type of the jSelf arg differs, but no docs seem to mention +** this: for static methods it's of type jclass and for non-static +** it's jobject. jobject actually works for all funcs, in the sense +** that it compiles and runs so long as we don't use jSelf (which is +** only rarely needed in this code), but to be pedantically correct we +** need the proper type in the signature. +** +** https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html#jni_interface_functions_and_pointers */ #define JENV_OSELF JNIEnv * const env, jobject jSelf #define JENV_CSELF JNIEnv * const env, jclass jKlazz -/* Helpers to account for -Xcheck:jni warnings about not having - checked for exceptions. */ +/* +** Helpers to account for -Xcheck:jni warnings about not having +** checked for exceptions. +*/ #define IFTHREW if((*env)->ExceptionCheck(env)) -#define EXCEPTION_IGNORE (void)((*env)->ExceptionCheck(env)) #define EXCEPTION_CLEAR (*env)->ExceptionClear(env) #define EXCEPTION_REPORT (*env)->ExceptionDescribe(env) +#define EXCEPTION_IGNORE IFTHREW EXCEPTION_CLEAR +#define EXCEPTION_WARN_IGNORE \ + IFTHREW {EXCEPTION_REPORT; EXCEPTION_CLEAR;}(void)0 #define EXCEPTION_WARN_CALLBACK_THREW(STR) \ MARKER(("WARNING: " STR " MUST NOT THROW.\n")); \ (*env)->ExceptionDescribe(env) @@ -189,10 +192,10 @@ /** Helpers for extracting pointers from jobjects, noting that the corresponding Java interfaces have already done the type-checking. */ -#define PtrGet_sqlite3(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.sqlite3) -#define PtrGet_sqlite3_stmt(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.sqlite3_stmt) -#define PtrGet_sqlite3_value(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.sqlite3_value) -#define PtrGet_sqlite3_context(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.sqlite3_context) +#define PtrGet_sqlite3(OBJ) NativePointerHolder_get(env,OBJ,&S3NphRefs.sqlite3) +#define PtrGet_sqlite3_stmt(OBJ) NativePointerHolder_get(env,OBJ,&S3NphRefs.sqlite3_stmt) +#define PtrGet_sqlite3_value(OBJ) NativePointerHolder_get(env,OBJ,&S3NphRefs.sqlite3_value) +#define PtrGet_sqlite3_context(OBJ) NativePointerHolder_get(env,OBJ,&S3NphRefs.sqlite3_context) /* Helpers for Java value reference management. */ static inline jobject new_global_ref(JNIEnv * const env, jobject const v){ return v ? (*env)->NewGlobalRef(env, v) : NULL; @@ -209,47 +212,59 @@ static inline void delete_local_ref(JNIEnv * const env, jobject const v){ #define UNREF_L(VAR) delete_local_ref(env,(VAR)) /** - Constant string class names used as keys for S3JniGlobal_nph_cache(), -S3Jni - and - friends. + Keys for use with S3JniGlobal_nph_cache(). +*/ +typedef struct S3NphRef S3NphRef; +struct S3NphRef { + const int index /* index into S3JniGlobal.nph[] */; + const char * const zName /* Full Java name of the class */; +}; + +/** + Keys for each concrete NativePointerHolder subclass. These are to + be used with S3JniGlobal_nph_cache() and friends. These are + initialized on-demand by S3JniGlobal_nph_cache(). */ static const struct { - const char * const sqlite3; - const char * const sqlite3_stmt; - const char * const sqlite3_context; - const char * const sqlite3_value; - const char * const OutputPointer_Int32; - const char * const OutputPointer_Int64; - const char * const OutputPointer_String; - const char * const OutputPointer_ByteArray; - const char * const OutputPointer_sqlite3; - const char * const OutputPointer_sqlite3_stmt; + const S3NphRef sqlite3; + const S3NphRef sqlite3_stmt; + const S3NphRef sqlite3_context; + const S3NphRef sqlite3_value; + const S3NphRef OutputPointer_Int32; + const S3NphRef OutputPointer_Int64; + const S3NphRef OutputPointer_sqlite3; + const S3NphRef OutputPointer_sqlite3_stmt; + const S3NphRef OutputPointer_sqlite3_value; #ifdef SQLITE_ENABLE_FTS5 - const char * const Fts5Context; - const char * const Fts5ExtensionApi; - const char * const fts5_api; - const char * const fts5_tokenizer; - const char * const Fts5Tokenizer; + const S3NphRef OutputPointer_String; + const S3NphRef OutputPointer_ByteArray; + const S3NphRef Fts5Context; + const S3NphRef Fts5ExtensionApi; + const S3NphRef fts5_api; + const S3NphRef fts5_tokenizer; + const S3NphRef Fts5Tokenizer; #endif -} S3JniClassNames = { - "org/sqlite/jni/sqlite3", - "org/sqlite/jni/sqlite3_stmt", - "org/sqlite/jni/sqlite3_context", - "org/sqlite/jni/sqlite3_value", - "org/sqlite/jni/OutputPointer$Int32", - "org/sqlite/jni/OutputPointer$Int64", - "org/sqlite/jni/OutputPointer$String", - "org/sqlite/jni/OutputPointer$ByteArray", - "org/sqlite/jni/OutputPointer$sqlite3", - "org/sqlite/jni/OutputPointer$sqlite3_stmt", +} S3NphRefs = { +#define NREF(INDEX, JAVANAME) { INDEX, "org/sqlite/jni/" JAVANAME } + NREF(0, "sqlite3"), + NREF(1, "sqlite3_stmt"), + NREF(2, "sqlite3_context"), + NREF(3, "sqlite3_value"), + NREF(4, "OutputPointer$Int32"), + NREF(5, "OutputPointer$Int64"), + NREF(6, "OutputPointer$sqlite3"), + NREF(7, "OutputPointer$sqlite3_stmt"), + NREF(8, "OutputPointer$sqlite3_value"), #ifdef SQLITE_ENABLE_FTS5 - "org/sqlite/jni/Fts5Context", - "org/sqlite/jni/Fts5ExtensionApi", - "org/sqlite/jni/fts5_api", - "org/sqlite/jni/fts5_tokenizer", - "org/sqlite/jni/Fts5Tokenizer" + NREF(9, "OutputPointer$String"), + NREF(10, "OutputPointer$ByteArray"), + NREF(11, "Fts5Context"), + NREF(12, "Fts5ExtensionApi"), + NREF(13, "fts5_api"), + NREF(14, "fts5_tokenizer"), + NREF(15, "Fts5Tokenizer") #endif +#undef NREF }; /** Create a trivial JNI wrapper for (int CName(void)). */ @@ -264,9 +279,11 @@ static const struct { return (jint)CName((int)arg); \ } -/** Create a trivial JNI wrapper for (const mutf8_string * - CName(void)). This is only valid for functions which are known to - return ASCII or text which is equivalent in UTF-8 and MUTF-8. */ +/* +** Create a trivial JNI wrapper for (const mutf8_string * +** CName(void)). This is only valid for functions which are known to +** return ASCII or text which is equivalent in UTF-8 and MUTF-8. + */ #define WRAP_MUTF8_VOID(JniNameSuffix,CName) \ JDECL(jstring,JniNameSuffix)(JENV_CSELF){ \ return (*env)->NewStringUTF( env, CName() ); \ @@ -284,9 +301,11 @@ static const struct { return (jint)CName(PtrGet_sqlite3_stmt(pStmt), (int)n); \ } /** Create a trivial JNI wrapper for (jstring CName(sqlite3_stmt*,int)). */ -#define WRAP_STR_STMT_INT(JniNameSuffix,CName) \ - JDECL(jstring,JniNameSuffix)(JENV_CSELF, jobject pStmt, jint ndx){ \ - return (*env)->NewStringUTF(env, CName(PtrGet_sqlite3_stmt(pStmt), (int)ndx)); \ +#define WRAP_STR_STMT_INT(JniNameSuffix,CName) \ + JDECL(jstring,JniNameSuffix)(JENV_CSELF, jobject pStmt, jint ndx){ \ + return s3jni_utf8_to_jstring(env, \ + CName(PtrGet_sqlite3_stmt(pStmt), (int)ndx), \ + -1); \ } /** Create a trivial JNI wrapper for (int CName(sqlite3*)). */ #define WRAP_INT_DB(JniNameSuffix,CName) \ @@ -305,65 +324,190 @@ static const struct { } /* Helpers for jstring and jbyteArray. */ -#define JSTR_TOC(ARG) (*env)->GetStringUTFChars(env, ARG, NULL) -#define JSTR_RELEASE(ARG,VAR) if(VAR) (*env)->ReleaseStringUTFChars(env, ARG, VAR) -#define JBA_TOC(ARG) (*env)->GetByteArrayElements(env,ARG, NULL) -#define JBA_RELEASE(ARG,VAR) if(VAR) (*env)->ReleaseByteArrayElements(env, ARG, VAR, JNI_ABORT) - -/* Marker for code which needs(?) to be made thread-safe. REASON is a - terse reminder about why that function requires a mutex. -*/ -#define FIXME_THREADING(REASON) +#define s3jni_jstring_to_mutf8(ARG) (*env)->GetStringUTFChars(env, ARG, NULL) +#define s3jni_mutf8_release(ARG,VAR) if(VAR) (*env)->ReleaseStringUTFChars(env, ARG, VAR) +#define s3jni_jbytearray_bytes(ARG) (*env)->GetByteArrayElements(env,ARG, NULL) +#define s3jni_jbytearray_release(ARG,VAR) if(VAR) (*env)->ReleaseByteArrayElements(env, ARG, VAR, JNI_ABORT) enum { - /** - Size of the NativePointerHolder cache. Need enough space for - (only) the library's NativePointerHolder types, a fixed count - known at build-time. If we add more than this a fatal error will - be triggered with a reminder to increase this. This value needs - to be exactly the number of entries in the S3JniClassNames - object. The S3JniClassNames entries are the keys for this particular - cache. + /* + ** Size of the NativePointerHolder cache. Need enough space for + ** (only) the library's NativePointerHolder types, a fixed count + ** known at build-time. If we add more than this a fatal error will + ** be triggered with a reminder to increase this. This value needs + ** to be exactly the number of entries in the S3NphRefs object. The + ** index field of those entries are the keys for this particular + ** cache. */ - NphCache_SIZE = sizeof(S3JniClassNames) / sizeof(char const *) + NphCache_SIZE = sizeof(S3NphRefs) / sizeof(S3NphRef) }; -/** - Cache entry for NativePointerHolder lookups. +/* +** Cache entry for NativePointerHolder subclasses and OutputPointer +** types. */ -typedef struct S3JniNphCache S3JniNphCache; -struct S3JniNphCache { - const char * zClassName /* "full/class/Name". Must be a static - string pointer from the S3JniClassNames - struct. */; - jclass klazz /* global ref to the concrete - NativePointerHolder subclass represented by - zClassName */; - jmethodID midCtor /* klazz's no-arg constructor. Used by - new_NativePointerHolder_object(). */; - jfieldID fidValue /* NativePointerHolder.nativePointer and - OutputPointer.X.value */; - jfieldID fidSetAgg /* sqlite3_context::aggregateContext. Used only - by the sqlite3_context binding. */; +typedef struct S3JniNphClass S3JniNphClass; +struct S3JniNphClass { + volatile const S3NphRef * pRef /* Entry from S3NphRefs. */; + jclass klazz /* global ref to the concrete + ** NativePointerHolder subclass represented by + ** zClassName */; + volatile jmethodID midCtor /* klazz's no-arg constructor. Used by + ** new_NativePointerHolder_object(). */; + volatile jfieldID fidValue /* NativePointerHolder.nativePointer or + ** OutputPointer.T.value */; + volatile jfieldID fidAggCtx /* sqlite3_context::aggregateContext. Used only + ** by the sqlite3_context binding. */; }; -/** - Cache for per-JNIEnv data. +/** State for various hook callbacks. */ +typedef struct S3JniHook S3JniHook; +struct S3JniHook{ + jobject jObj /* global ref to Java instance */; + jmethodID midCallback /* callback method. Signature depends on + ** jObj's type */; +}; - Potential TODO: move the jclass entries to global space because, - per https://developer.android.com/training/articles/perf-jni: - - > once you have a valid jclass global reference you can use it from - any attached thread. - - Whereas we cache new refs for each thread. +/* +** Per-(sqlite3*) state for various JNI bindings. This state is +** allocated as needed, cleaned up in sqlite3_close(_v2)(), and +** recycled when possible. */ -typedef struct S3JniEnvCache S3JniEnvCache; -struct S3JniEnvCache { +typedef struct S3JniDb S3JniDb; +struct S3JniDb { + sqlite3 *pDb /* The associated db handle */; + jobject jDb /* A global ref of the output object which gets + returned from sqlite3_open(_v2)(). We need this in + order to have an object to pass to routines like + sqlite3_collation_needed()'s callback, or else we + have to dynamically create one for that purpose, + which would be fine except that it would be a + different instance (and maybe even a different + class) than the one the user may expect to + receive. */; + char * zMainDbName /* Holds the string allocated on behalf of + SQLITE_DBCONFIG_MAINDBNAME. */; + struct { + S3JniHook busyHandler; + S3JniHook collation; + S3JniHook collationNeeded; + S3JniHook commit; + S3JniHook progress; + S3JniHook rollback; + S3JniHook trace; + S3JniHook update; + S3JniHook auth; +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + S3JniHook preUpdate; +#endif + } hooks; +#ifdef SQLITE_ENABLE_FTS5 + jobject jFtsApi /* global ref to s3jni_fts5_api_from_db() */; +#endif + S3JniDb * pNext /* Next entry in the available/free list */; + S3JniDb * pPrev /* Previous entry in the available/free list */; +}; + +/* +** Cache for per-JNIEnv (i.e. per-thread) data. +*/ +typedef struct S3JniEnv S3JniEnv; +struct S3JniEnv { JNIEnv *env /* env in which this cache entry was created */; - //! The various refs to global classes might be cacheable a single - // time globally. Information online seems inconsistent on that - // point. + /* + ** pdbOpening is used to coordinate the Java/DB connection of a + ** being-open()'d db in the face of auto-extensions. "The problem" + ** is that auto-extensions run before we can bind the C db to its + ** Java representation, but auto-extensions require that binding. We + ** handle this as follows: + ** + ** - In the JNI side of sqlite3_open(), allocate the Java side of + ** that connection and set pdbOpening to point to that + ** object. + ** + ** - Call sqlite3_open(), which triggers the auto-extension + ** handler. That handler uses pdbOpening to connect the native + ** db handle which it receives with pdbOpening. + ** + ** - When sqlite3_open() returns, check whether pdbOpening->pDb is + ** NULL. If it isn't, auto-extension handling set it up. If it + ** is, complete the Java/C binding unless sqlite3_open() returns + ** a NULL db, in which case free pdbOpening. + */ + S3JniDb * pdbOpening; + S3JniEnv * pPrev /* Previous entry in the linked list */; + S3JniEnv * pNext /* Next entry in the linked list */; +}; + +/* +** State for proxying sqlite3_auto_extension() in Java. +*/ +typedef struct S3JniAutoExtension S3JniAutoExtension; +struct S3JniAutoExtension { + jobject jObj /* Java object */; + jmethodID midFunc /* xEntryPoint() callback */; +}; + +/* +** If true, modifying S3JniGlobal.metrics is protected by a mutex, +** else it isn't. +*/ +#ifdef SQLITE_DEBUG +#define S3JNI_METRICS_MUTEX 1 +#else +#define S3JNI_METRICS_MUTEX 0 +#endif + +/* +** Global state, e.g. caches and metrics. +*/ +typedef struct S3JniGlobalType S3JniGlobalType; +struct S3JniGlobalType { + /* + ** According to: https://developer.ibm.com/articles/j-jni/ + ** + ** > A thread can get a JNIEnv by calling GetEnv() using the JNI + ** invocation interface through a JavaVM object. The JavaVM object + ** itself can be obtained by calling the JNI GetJavaVM() method + ** using a JNIEnv object and can be cached and shared across + ** threads. Caching a copy of the JavaVM object enables any thread + ** with access to the cached object to get access to its own + ** JNIEnv when necessary. + */ + JavaVM * jvm; + /* Cache of Java refs/IDs for NativePointerHolder subclasses. + ** Initialized on demand. + */ + S3JniNphClass nph[NphCache_SIZE]; + /* + ** Cache of per-thread state. + */ + struct { + S3JniEnv * aHead /* Linked list of in-use instances */; + S3JniEnv * aFree /* Linked list of free instances */; + sqlite3_mutex * mutex /* mutex for aHead and aFree as well for + first-time inits of nph members. */; + void const * locker /* env mutex is held on this object's behalf. + Used only for sanity checking. */; + } envCache; + struct { + S3JniDb * aUsed /* Linked list of in-use instances */; + S3JniDb * aFree /* Linked list of free instances */; + sqlite3_mutex * mutex /* mutex for aUsed and aFree */; + void const * locker /* perDb mutex is held on this object's + behalf. Unlike envCache.locker, we cannot + always have this set to the current JNIEnv + object. Used only for sanity checking. */; + } perDb; +#ifdef SQLITE_ENABLE_SQLLOG + struct { + S3JniHook sqllog /* sqlite3_config(SQLITE_CONFIG_SQLLOG) callback */; + } hooks; +#endif + /* + ** Refs to global classes and methods. Obtained during static init + ** and never released. + */ struct { jclass cObj /* global ref to java.lang.Object */; jclass cLong /* global ref to java.lang.Long */; @@ -374,170 +518,119 @@ struct S3JniEnvCache { jmethodID stringGetBytes /* the String.getBytes(Charset) method */; } g /* refs to global Java state */; #ifdef SQLITE_ENABLE_FTS5 - jobject jFtsExt /* Global ref to Java singleton for the - Fts5ExtensionApi instance. */; struct { - jclass klazz; - jfieldID fidA; - jfieldID fidB; - } jPhraseIter; + volatile jobject jFtsExt /* Global ref to Java singleton for the + Fts5ExtensionApi instance. */; + struct { + jfieldID fidA /* Fts5Phrase::a member */; + jfieldID fidB /* Fts5Phrase::b member */; + } jPhraseIter; + } fts5; #endif - S3JniEnvCache * pPrev /* Previous entry in the linked list */; - S3JniEnvCache * pNext /* Next entry in the linked list */; - /** TODO?: S3JniNphCache *pNphHit; - - and always set it to the most recent cache search result. - - The intent would be to help fast-track cache lookups and would - speed up, e.g., the sqlite3_value-to-Java-array loop in a - multi-threaded app. - */ - S3JniNphCache nph[NphCache_SIZE]; -}; - -static void S3JniNphCache_clear(JNIEnv * const env, S3JniNphCache * const p){ - UNREF_G(p->klazz); - memset(p, 0, sizeof(S3JniNphCache)); -} - -#define S3JNI_ENABLE_AUTOEXT 1 -#if S3JNI_ENABLE_AUTOEXT -/* - Whether auto extensions are feasible here is currently unknown due - to... - - 1) JNIEnv/threading issues. A db instance is mapped to a specific - JNIEnv object but auto extensions may be added from any thread. In - such contexts, which JNIEnv do we use for the JNI APIs? - - 2) a chicken/egg problem involving the Java/C mapping of the db: - when auto extensions are run, the db has not yet been connected to - Java. If we do that during the auto-ext, sqlite3_open(_v2)() will not behave - properly because they have a different jobject and the API - guarantees the user that _that_ object is the one the API will bind - the native to. - - If we change the open(_v2()) interfaces to use OutputPointer.sqlite3 - instead of the client passing in an instance, we could work around - (2). -*/ -typedef struct S3JniAutoExtension S3JniAutoExtension; -typedef void (*S3JniAutoExtension_xEntryPoint)(sqlite3*); -struct S3JniAutoExtension { - jobject jObj; - jmethodID midFunc; - S3JniAutoExtension_xEntryPoint xEntryPoint; - S3JniAutoExtension *pNext /* next linked-list entry */; - S3JniAutoExtension *pPrev /* previous linked-list entry */; -}; -#endif - -/** State for various hook callbacks. */ -typedef struct S3JniHook S3JniHook; -struct S3JniHook{ - jobject jObj /* global ref to Java instance */; - jmethodID midCallback /* callback method. Signature depends on - jObj's type */; - jclass klazz /* global ref to jObj's class. Only needed - by hooks which have an xDestroy() method, - as lookup of that method is deferred - until the object requires cleanup. */; -}; - -/** - Per-(sqlite3*) state for various JNI bindings. This state is - allocated as needed, cleaned up in sqlite3_close(_v2)(), and - recycled when possible. It is freed during sqlite3_shutdown(). -*/ -typedef struct S3JniDb S3JniDb; -struct S3JniDb { - JNIEnv *env /* The associated JNIEnv handle */; - sqlite3 *pDb /* The associated db handle */; - jobject jDb /* A global ref of the object which was passed to - sqlite3_open(_v2)(). We need this in order to have - an object to pass to sqlite3_collation_needed()'s - callback, or else we have to dynamically create one - for that purpose, which would be fine except that - it would be a different instance (and maybe even a - different class) than the one the user may expect - to receive. */; - char * zMainDbName /* Holds any string allocated on behave of - SQLITE_DBCONFIG_MAINDBNAME. */; - S3JniHook busyHandler; - S3JniHook collation; - S3JniHook collationNeeded; - S3JniHook commitHook; - S3JniHook progress; - S3JniHook rollbackHook; - S3JniHook trace; - S3JniHook updateHook; - S3JniHook authHook; -#ifdef SQLITE_ENABLE_FTS5 - jobject jFtsApi /* global ref to s3jni_fts5_api_from_db() */; -#endif - S3JniDb * pNext /* Next entry in the available/free list */; - S3JniDb * pPrev /* Previous entry in the available/free list */; -}; - -/** - Global state, e.g. caches and metrics. -*/ -static struct { - /** - According to: https://developer.ibm.com/articles/j-jni/ - - > A thread can get a JNIEnv by calling GetEnv() using the JNI - invocation interface through a JavaVM object. The JavaVM object - itself can be obtained by calling the JNI GetJavaVM() method - using a JNIEnv object and can be cached and shared across - threads. Caching a copy of the JavaVM object enables any thread - with access to the cached object to get access to its own - JNIEnv when necessary. - */ - JavaVM * jvm; + /* Internal metrics. */ struct { - S3JniEnvCache * aHead /* Linked list of in-use instances */; - S3JniEnvCache * aFree /* Linked list of free instances */; - } envCache; - struct { - S3JniDb * aUsed /* Linked list of in-use instances */; - S3JniDb * aFree /* Linked list of free instances */; - } perDb; - struct { - unsigned nphCacheHits; - unsigned nphCacheMisses; - unsigned envCacheHits; - unsigned envCacheMisses; - unsigned nDestroy /* xDestroy() calls across all types */; + volatile unsigned envCacheHits; + volatile unsigned envCacheMisses; + volatile unsigned envCacheAllocs; + volatile unsigned nMutexEnv /* number of times envCache.mutex was entered for + a S3JniEnv operation. */; + volatile unsigned nMutexEnv2 /* number of times envCache.mutex was entered for + a S3JniNphClass operation. */; + volatile unsigned nMutexPerDb /* number of times perDb.mutex was entered */; + volatile unsigned nMutexAutoExt /* number of times autoExt.mutex was entered */; + volatile unsigned nDestroy /* xDestroy() calls across all types */; + volatile unsigned nPdbAlloc /* Number of S3JniDb alloced. */; + volatile unsigned nPdbRecycled /* Number of S3JniDb reused. */; struct { /* Number of calls for each type of UDF callback. */ - unsigned nFunc; - unsigned nStep; - unsigned nFinal; - unsigned nValue; - unsigned nInverse; + volatile unsigned nFunc; + volatile unsigned nStep; + volatile unsigned nFinal; + volatile unsigned nValue; + volatile unsigned nInverse; } udf; - } metrics; -#if S3JNI_ENABLE_AUTOEXT - struct { - S3JniAutoExtension *pHead /* Head of the auto-extension list */; - S3JniDb * psOpening /* handle to the being-opened db. We - need this so that auto extensions - can have a consistent view of the - cross-language db connection and - behave property if they call further - db APIs. */; - int isRunning /* True while auto extensions are - running. This is used to prohibit - manipulation of the auto-extension - list while extensions are - running. */; - } autoExt; + unsigned nMetrics /* Total number of mutex-locked metrics increments. */; +#if S3JNI_METRICS_MUTEX + sqlite3_mutex * mutex; #endif -} S3JniGlobal; + } metrics; + /** + The list of bound auto-extensions (Java-side: + org.sqlite.jni.AutoExtension objects). + */ + struct { + S3JniAutoExtension *pExt /* Head of the auto-extension list */; + int nAlloc /* number of entries allocated for pExt, + as distinct from the number of active + entries. */; + int nExt /* number of active entries in pExt. */; + sqlite3_mutex * mutex /* mutex for manipulation/traversal of pExt */; + } autoExt; +}; +static S3JniGlobalType S3JniGlobal = {}; +#define SJG S3JniGlobal -#define OOM_CHECK(VAR) if(!(VAR)) s3jni_oom(env) -static void s3jni_oom(JNIEnv * const env){ +/* Increments *p, possibly protected by a mutex. */ +#if S3JNI_METRICS_MUTEX +static void s3jni_incr( volatile unsigned int * const p ){ + sqlite3_mutex * const m = SJG.metrics.mutex; + sqlite3_mutex_enter(m); + ++SJG.metrics.nMetrics; + ++(*p); + sqlite3_mutex_leave(m); +} +#else +#define s3jni_incr(PTR) ++(*(PTR)) +#endif + +/* Helpers for working with specific mutexes. */ +#define MUTEX_ENV_ASSERT_LOCKED \ + assert( 0 != SJG.envCache.locker && "Misuse of S3JniGlobal.envCache.mutex" ) +#define MUTEX_ENV_ASSERT_LOCKER \ + assert( (env) == SJG.envCache.locker && "Misuse of S3JniGlobal.envCache.mutex" ) +#define MUTEX_ENV_ASSERT_NOTLOCKER \ + assert( (env) != SJG.envCache.locker && "Misuse of S3JniGlobal.envCache.mutex" ) +#define MUTEX_ENV_ENTER \ + MUTEX_ENV_ASSERT_NOTLOCKER; \ + /*MARKER(("Entering ENV mutex@%p %s.\n", env));*/ \ + sqlite3_mutex_enter( SJG.envCache.mutex ); \ + ++SJG.metrics.nMutexEnv; \ + SJG.envCache.locker = env +#define MUTEX_ENV_LEAVE \ + /*MARKER(("Leaving ENV mutex @%p %s.\n", env));*/ \ + MUTEX_ENV_ASSERT_LOCKER; \ + SJG.envCache.locker = 0; \ + sqlite3_mutex_leave( SJG.envCache.mutex ) +#define MUTEX_EXT_ENTER \ + /*MARKER(("Entering autoExt mutex@%p %s.\n", env));*/ \ + sqlite3_mutex_enter( SJG.autoExt.mutex ); \ + ++SJG.metrics.nMutexAutoExt +#define MUTEX_EXT_LEAVE \ + /*MARKER(("Leaving autoExt mutex@%p %s.\n", env));*/ \ + sqlite3_mutex_leave( SJG.autoExt.mutex ) +#define MUTEX_NPH_ENTER \ + MUTEX_ENV_ASSERT_NOTLOCKER; \ + /*MARKER(("Entering NPH mutex@%p %s.\n", env));*/ \ + sqlite3_mutex_enter( SJG.envCache.mutex ); \ + ++SJG.metrics.nMutexEnv2; \ + SJG.envCache.locker = env +#define MUTEX_NPH_LEAVE \ + /*MARKER(("Leaving NPH mutex @%p %s.\n", env));*/ \ + MUTEX_ENV_ASSERT_LOCKER; \ + SJG.envCache.locker = 0; \ + sqlite3_mutex_leave( SJG.envCache.mutex ) +#define MUTEX_PDB_ENTER \ + /*MARKER(("Entering PerDb mutex@%p %s.\n", env));*/ \ + sqlite3_mutex_enter( SJG.perDb.mutex ); \ + ++SJG.metrics.nMutexPerDb; \ + SJG.perDb.locker = env; +#define MUTEX_PDB_LEAVE \ + /*MARKER(("Leaving PerDb mutex@%p %s.\n", env));*/ \ + SJG.perDb.locker = 0; \ + sqlite3_mutex_leave( SJG.perDb.mutex ) + +#define s3jni_oom_check(VAR) if(!(VAR)) s3jni_oom(env) +static inline void s3jni_oom(JNIEnv * const env){ (*env)->FatalError(env, "Out of memory.") /* does not return */; } @@ -559,68 +652,34 @@ static void * s3jni_malloc(JNIEnv * const env, size_t n){ insofar as possible. Calls (*env)->FatalError() if allocation of an entry fails. That's hypothetically possible but "shouldn't happen." */ -FIXME_THREADING(S3JniEnvCache) -static S3JniEnvCache * S3JniGlobal_env_cache(JNIEnv * const env){ - struct S3JniEnvCache * row = S3JniGlobal.envCache.aHead; +static S3JniEnv * S3JniGlobal_env_cache(JNIEnv * const env){ + struct S3JniEnv * row; + MUTEX_ENV_ENTER; + row = SJG.envCache.aHead; for( ; row; row = row->pNext ){ if( row->env == env ){ - ++S3JniGlobal.metrics.envCacheHits; + s3jni_incr( &SJG.metrics.envCacheHits ); + MUTEX_ENV_LEAVE; return row; } } - ++S3JniGlobal.metrics.envCacheMisses; - row = S3JniGlobal.envCache.aFree; + s3jni_incr( &SJG.metrics.envCacheMisses ); + row = SJG.envCache.aFree; if( row ){ assert(!row->pPrev); - S3JniGlobal.envCache.aFree = row->pNext; + SJG.envCache.aFree = row->pNext; if( row->pNext ) row->pNext->pPrev = 0; }else{ - row = sqlite3_malloc(sizeof(S3JniEnvCache)); - if( !row ){ - (*env)->FatalError(env, "Maintenance required: S3JniEnvCache is full.") - /* Does not return, but cc doesn't know that */; - return NULL; - } + row = s3jni_malloc(env, sizeof(S3JniEnv)); + s3jni_incr( &SJG.metrics.envCacheAllocs ); } memset(row, 0, sizeof(*row)); - row->pNext = S3JniGlobal.envCache.aHead; + row->pNext = SJG.envCache.aHead; if(row->pNext) row->pNext->pPrev = row; - S3JniGlobal.envCache.aHead = row; + SJG.envCache.aHead = row; row->env = env; - /* Grab references to various global classes and objects... */ - row->g.cObj = REF_G((*env)->FindClass(env,"java/lang/Object")); - EXCEPTION_IS_FATAL("Error getting reference to Object class."); - - row->g.cLong = REF_G((*env)->FindClass(env,"java/lang/Long")); - EXCEPTION_IS_FATAL("Error getting reference to Long class."); - row->g.ctorLong1 = (*env)->GetMethodID(env, row->g.cLong, - "", "(J)V"); - EXCEPTION_IS_FATAL("Error getting reference to Long constructor."); - - row->g.cString = REF_G((*env)->FindClass(env,"java/lang/String")); - EXCEPTION_IS_FATAL("Error getting reference to String class."); - row->g.ctorStringBA = - (*env)->GetMethodID(env, row->g.cString, - "", "([BLjava/nio/charset/Charset;)V"); - EXCEPTION_IS_FATAL("Error getting reference to String(byte[],Charset) ctor."); - row->g.stringGetBytes = - (*env)->GetMethodID(env, row->g.cString, - "getBytes", "(Ljava/nio/charset/Charset;)[B"); - EXCEPTION_IS_FATAL("Error getting reference to String.getBytes(Charset)."); - - { /* StandardCharsets.UTF_8 */ - jfieldID fUtf8; - jclass const klazzSC = - (*env)->FindClass(env,"java/nio/charset/StandardCharsets"); - EXCEPTION_IS_FATAL("Error getting reference to StndardCharsets class."); - fUtf8 = (*env)->GetStaticFieldID(env, klazzSC, "UTF_8", - "Ljava/nio/charset/Charset;"); - EXCEPTION_IS_FATAL("Error getting StndardCharsets.UTF_8 field."); - row->g.oCharsetUtf8 = - REF_G((*env)->GetStaticObjectField(env, klazzSC, fUtf8)); - EXCEPTION_IS_FATAL("Error getting reference to StandardCharsets.UTF_8."); - } + MUTEX_ENV_LEAVE; return row; } @@ -638,23 +697,27 @@ static S3JniEnvCache * S3JniGlobal_env_cache(JNIEnv * const env){ ** ** Returns err_code. */ -static int s3jni_db_error(sqlite3* const db, int err_code, const char * const zMsg){ +static int s3jni_db_error(sqlite3* const db, int err_code, + const char * const zMsg){ if( db!=0 ){ if( 0==zMsg ){ sqlite3Error(db, err_code); }else{ const int nMsg = sqlite3Strlen30(zMsg); + sqlite3_mutex_enter(sqlite3_db_mutex(db)); sqlite3ErrorWithMsg(db, err_code, "%.*s", nMsg, zMsg); + sqlite3_mutex_leave(sqlite3_db_mutex(db)); } } return err_code; } -/** - Creates a new jByteArray of length nP, copies p's contents into it, and - returns that byte array. - */ -static jbyteArray s3jni_new_jbyteArray(JNIEnv * const env, const unsigned char * const p, int nP){ +/* +** Creates a new jByteArray of length nP, copies p's contents into it, and +** returns that byte array (NULL on OOM). +*/ +static jbyteArray s3jni_new_jbyteArray(JNIEnv * const env, + const unsigned char * const p, int nP){ jbyteArray jba = (*env)->NewByteArray(env, (jint)nP); if(jba){ (*env)->SetByteArrayRegion(env, jba, 0, (jint)nP, (const jbyte*)p); @@ -662,22 +725,37 @@ static jbyteArray s3jni_new_jbyteArray(JNIEnv * const env, const unsigned char * return jba; } -/** - Uses the java.lang.String(byte[],Charset) constructor to create a - new String from UTF-8 string z. n is the number of bytes to - copy. If n<0 then sqlite3Strlen30() is used to calculate it. - - Returns NULL if z is NULL or on OOM, else returns a new jstring - owned by the caller. - - Sidebar: this is a painfully inefficient way to convert from - standard UTF-8 to a Java string, but JNI offers only algorithms for - working with MUTF-8, not UTF-8. +/* +** Returns the current JNIEnv object. Fails fatally if it cannot find +** it. */ -static jstring s3jni_utf8_to_jstring(S3JniEnvCache * const jc, +static JNIEnv * s3jni_get_env(void){ + JNIEnv * env = 0; + if( (*SJG.jvm)->GetEnv(SJG.jvm, (void **)&env, + JNI_VERSION_1_8) ){ + fprintf(stderr, "Fatal error: cannot get current JNIEnv.\n"); + abort(); + } + return env; +} +/* Declares local var env = s3jni_get_env(). */ +#define LocalJniGetEnv JNIEnv * const env = s3jni_get_env() + +/* +** Uses the java.lang.String(byte[],Charset) constructor to create a +** new String from UTF-8 string z. n is the number of bytes to +** copy. If n<0 then sqlite3Strlen30() is used to calculate it. +** +** Returns NULL if z is NULL or on OOM, else returns a new jstring +** owned by the caller. +** +** Sidebar: this is a painfully inefficient way to convert from +** standard UTF-8 to a Java string, but JNI offers only algorithms for +** working with MUTF-8, not UTF-8. +*/ +static jstring s3jni_utf8_to_jstring(JNIEnv * const env, const char * const z, int n){ jstring rv = NULL; - JNIEnv * const env = jc->env; if( 0==n || (n<0 && z && !z[0]) ){ /* Fast-track the empty-string case via the MUTF-8 API. We could hypothetically do this for any strings where n<4 and z is @@ -688,39 +766,38 @@ static jstring s3jni_utf8_to_jstring(S3JniEnvCache * const jc, if( n<0 ) n = sqlite3Strlen30(z); jba = s3jni_new_jbyteArray(env, (unsigned const char *)z, (jsize)n); if( jba ){ - rv = (*env)->NewObject(env, jc->g.cString, jc->g.ctorStringBA, - jba, jc->g.oCharsetUtf8); + rv = (*env)->NewObject(env, SJG.g.cString, SJG.g.ctorStringBA, + jba, SJG.g.oCharsetUtf8); UNREF_L(jba); } } return rv; } -/** - Converts the given java.lang.String object into a NUL-terminated - UTF-8 C-string by calling jstr.getBytes(StandardCharset.UTF_8). - Returns NULL if jstr is NULL or on allocation error. If jstr is not - NULL and nLen is not NULL then nLen is set to the length of the - returned string, not including the terminating NUL. If jstr is not - NULL and it returns NULL, this indicates an allocation error. In - that case, if nLen is not NULL then it is either set to 0 (if - fetching of jstr's bytes fails to allocate) or set to what would - have been the length of the string had C-string allocation - succeeded. - - The returned memory is allocated from sqlite3_malloc() and - ownership is transferred to the caller. +/* +** Converts the given java.lang.String object into a NUL-terminated +** UTF-8 C-string by calling jstr.getBytes(StandardCharset.UTF_8). +** Returns NULL if jstr is NULL or on allocation error. If jstr is not +** NULL and nLen is not NULL then nLen is set to the length of the +** returned string, not including the terminating NUL. If jstr is not +** NULL and it returns NULL, this indicates an allocation error. In +** that case, if nLen is not NULL then it is either set to 0 (if +** fetching of jstr's bytes fails to allocate) or set to what would +** have been the length of the string had C-string allocation +** succeeded. +** +** The returned memory is allocated from sqlite3_malloc() and +** ownership is transferred to the caller. */ -static char * s3jni_jstring_to_utf8(S3JniEnvCache * const jc, +static char * s3jni_jstring_to_utf8(JNIEnv * const env, jstring jstr, int *nLen){ - JNIEnv * const env = jc->env; jbyteArray jba; jsize nBa; char *rv; if(!jstr) return 0; - jba = (*env)->CallObjectMethod(env, jstr, jc->g.stringGetBytes, - jc->g.oCharsetUtf8); + jba = (*env)->CallObjectMethod(env, jstr, SJG.g.stringGetBytes, + SJG.g.oCharsetUtf8); if( (*env)->ExceptionCheck(env) || !jba /* order of these checks is significant for -Xlint:jni */ ) { EXCEPTION_REPORT; @@ -738,12 +815,12 @@ static char * s3jni_jstring_to_utf8(S3JniEnvCache * const jc, return rv; } -/** - Expects to be passed a pointer from sqlite3_column_text16() or - sqlite3_value_text16() and a byte-length value from - sqlite3_column_bytes16() or sqlite3_value_bytes16(). It creates a - Java String of exactly half that character length, returning NULL - if !p or (*env)->NewString() fails. +/* +** Expects to be passed a pointer from sqlite3_column_text16() or +** sqlite3_value_text16() and a byte-length value from +** sqlite3_column_bytes16() or sqlite3_value_bytes16(). It creates a +** Java String of exactly half that character length, returning NULL +** if !p or (*env)->NewString() fails. */ static jstring s3jni_text16_to_jstring(JNIEnv * const env, const void * const p, int nP){ return p @@ -751,28 +828,26 @@ static jstring s3jni_text16_to_jstring(JNIEnv * const env, const void * const p, : NULL; } -/** - Requires jx to be a Throwable. Calls its toString() method and - returns its value converted to a UTF-8 string. The caller owns the - returned string and must eventually sqlite3_free() it. Returns 0 - if there is a problem fetching the info or on OOM. - - Design note: we use toString() instead of getMessage() because the - former includes the exception type's name: - - Exception e = new RuntimeException("Hi"); - System.out.println(e.toString()); // java.lang.RuntimeException: Hi - System.out.println(e.getMessage()); // Hi - } +/* +** Requires jx to be a Throwable. Calls its toString() method and +** returns its value converted to a UTF-8 string. The caller owns the +** returned string and must eventually sqlite3_free() it. Returns 0 +** if there is a problem fetching the info or on OOM. +** +** Design note: we use toString() instead of getMessage() because the +** former includes the exception type's name: +** +** Exception e = new RuntimeException("Hi"); +** System.out.println(e.toString()); // java.lang.RuntimeException: Hi +** System.out.println(e.getMessage()); // Hi */ -FIXME_THREADING(S3JniEnvCache) static char * s3jni_exception_error_msg(JNIEnv * const env, jthrowable jx ){ - S3JniEnvCache * const jc = S3JniGlobal_env_cache(env); jmethodID mid; jstring msg; char * zMsg; jclass const klazz = (*env)->GetObjectClass(env, jx); mid = (*env)->GetMethodID(env, klazz, "toString", "()Ljava/lang/String;"); + UNREF_L(klazz); IFTHREW{ EXCEPTION_REPORT; EXCEPTION_CLEAR; @@ -784,22 +859,22 @@ static char * s3jni_exception_error_msg(JNIEnv * const env, jthrowable jx ){ EXCEPTION_CLEAR; return 0; } - zMsg = s3jni_jstring_to_utf8(jc, msg, 0); + zMsg = s3jni_jstring_to_utf8(env, msg, 0); UNREF_L(msg); return zMsg; } -/** - Extracts the current JNI exception, sets ps->pDb's error message to - its message string, and clears the exception. If errCode is non-0, - it is used as-is, else SQLITE_ERROR is assumed. If there's a - problem extracting the exception's message, it's treated as - non-fatal and zDfltMsg is used in its place. - - This must only be called if a JNI exception is pending. - - Returns errCode unless it is 0, in which case SQLITE_ERROR is - returned. +/* +** Extracts the current JNI exception, sets ps->pDb's error message to +** its message string, and clears the exception. If errCode is non-0, +** it is used as-is, else SQLITE_ERROR is assumed. If there's a +** problem extracting the exception's message, it's treated as +** non-fatal and zDfltMsg is used in its place. +** +** This must only be called if a JNI exception is pending. +** +** Returns errCode unless it is 0, in which case SQLITE_ERROR is +** returned. */ static int s3jni_db_exception(JNIEnv * const env, S3JniDb * const ps, int errCode, const char *zDfltMsg){ @@ -817,195 +892,131 @@ static int s3jni_db_exception(JNIEnv * const env, S3JniDb * const ps, return errCode; } -/** - Extracts the (void xDestroy()) method from the given jclass and - applies it to jobj. If jObj is NULL, this is a no-op. If klazz is - NULL then it's derived from jobj. The lack of an xDestroy() method - is silently ignored and any exceptions thrown by the method trigger - a warning to stdout or stderr and then the exception is suppressed. +/* +** Extracts the (void xDestroy()) method from jObj and applies it to +** jObj. If jObj is NULL, this is a no-op. The lack of an xDestroy() +** method is silently ignored and any exceptions thrown by xDestroy() +** trigger a warning to stdout or stderr and then the exception is +** suppressed. */ -static void s3jni_call_xDestroy(JNIEnv * const env, jobject jObj, jclass klazz){ +static void s3jni_call_xDestroy(JNIEnv * const env, jobject jObj){ if(jObj){ - jmethodID method; - if(!klazz){ - klazz = (*env)->GetObjectClass(env, jObj); - assert(klazz); - } - method = (*env)->GetMethodID(env, klazz, "xDestroy", "()V"); - //MARKER(("jObj=%p, klazz=%p, method=%p\n", jObj, klazz, method)); + jclass const klazz = (*env)->GetObjectClass(env, jObj); + jmethodID method = (*env)->GetMethodID(env, klazz, "xDestroy", "()V"); + + UNREF_L(klazz); if(method){ - ++S3JniGlobal.metrics.nDestroy; + s3jni_incr( &SJG.metrics.nDestroy ); (*env)->CallVoidMethod(env, jObj, method); IFTHREW{ EXCEPTION_WARN_CALLBACK_THREW("xDestroy() callback"); EXCEPTION_CLEAR; } }else{ + /* Non-fatal. */ EXCEPTION_CLEAR; } } } -/** - Removes any Java references from s and clears its state. If - doXDestroy is true and s->klazz and s->jObj are not NULL, s->jObj's - s is passed to s3jni_call_xDestroy() before any references are - cleared. It is legal to call this when the object has no Java - references. +/* +** Removes any Java references from s and clears its state. If +** doXDestroy is true and s->jObj is not NULL, s->jObj's +** s is passed to s3jni_call_xDestroy() before any references are +** cleared. It is legal to call this when the object has no Java +** references. */ static void S3JniHook_unref(JNIEnv * const env, S3JniHook * const s, int doXDestroy){ - if(doXDestroy && s->klazz && s->jObj){ - s3jni_call_xDestroy(env, s->jObj, s->klazz); + if(doXDestroy && s->jObj){ + s3jni_call_xDestroy(env, s->jObj); } UNREF_G(s->jObj); - UNREF_G(s->klazz); memset(s, 0, sizeof(*s)); } -/** - Clears s's state and moves it to the free-list. +/* +** Clears s's state and moves it to the free-list. */ -FIXME_THREADING(perDb) -static void S3JniDb_set_aside(S3JniDb * const s){ +static void S3JniDb_set_aside(JNIEnv * env, S3JniDb * const s){ if(s){ - JNIEnv * const env = s->env; - assert(s->pDb && "Else this object is already in the free-list."); - //MARKER(("state@%p for db@%p setting aside\n", s, s->pDb)); + MUTEX_PDB_ENTER; assert(s->pPrev != s); assert(s->pNext != s); assert(s->pPrev ? (s->pPrev!=s->pNext) : 1); if(s->pNext) s->pNext->pPrev = s->pPrev; if(s->pPrev) s->pPrev->pNext = s->pNext; - else if(S3JniGlobal.perDb.aUsed == s){ + else if(SJG.perDb.aUsed == s){ assert(!s->pPrev); - S3JniGlobal.perDb.aUsed = s->pNext; + SJG.perDb.aUsed = s->pNext; } sqlite3_free( s->zMainDbName ); -#define UNHOOK(MEMBER,XDESTROY) S3JniHook_unref(env, &s->MEMBER, XDESTROY) +#define UNHOOK(MEMBER,XDESTROY) S3JniHook_unref(env, &s->hooks.MEMBER, XDESTROY) UNHOOK(trace, 0); UNHOOK(progress, 0); - UNHOOK(commitHook, 0); - UNHOOK(rollbackHook, 0); - UNHOOK(updateHook, 0); - UNHOOK(authHook, 0); + UNHOOK(commit, 0); + UNHOOK(rollback, 0); + UNHOOK(update, 0); + UNHOOK(auth, 0); +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + UNHOOK(preUpdate, 0); +#endif UNHOOK(collation, 1); UNHOOK(collationNeeded, 1); UNHOOK(busyHandler, 1); #undef UNHOOK UNREF_G(s->jDb); -#ifdef SQLITE_ENABLE_FTS5 - UNREF_G(s->jFtsApi); -#endif memset(s, 0, sizeof(S3JniDb)); - s->pNext = S3JniGlobal.perDb.aFree; + s->pNext = SJG.perDb.aFree; if(s->pNext) s->pNext->pPrev = s; - S3JniGlobal.perDb.aFree = s; - //MARKER(("%p->pPrev@%p, pNext@%p\n", s, s->pPrev, s->pNext)); - //if(s->pNext) MARKER(("next: %p->pPrev@%p\n", s->pNext, s->pNext->pPrev)); + SJG.perDb.aFree = s; + MUTEX_PDB_LEAVE; } } -/** - Requires that p has been snipped from any linked list it is - in. Clears all Java refs p holds and zeroes out p. -*/ -static void S3JniEnvCache_clear(S3JniEnvCache * const p){ - JNIEnv * const env = p->env; - if(env){ - int i; - UNREF_G(p->g.cObj); - UNREF_G(p->g.cLong); - UNREF_G(p->g.cString); - UNREF_G(p->g.oCharsetUtf8); -#ifdef SQLITE_ENABLE_FTS5 - UNREF_G(p->jFtsExt); - UNREF_G(p->jPhraseIter.klazz); -#endif - for( i = 0; i < NphCache_SIZE; ++i ){ - S3JniNphCache_clear(env, &p->nph[i]); - } - memset(p, 0, sizeof(S3JniEnvCache)); - } -} - -/** - Cleans up all state in S3JniGlobal.perDb for th given JNIEnv. - Results are undefined if a Java-side db uses the API - from the given JNIEnv after this call. -*/ -FIXME_THREADING(perDb) -static void S3JniDb_free_for_env(JNIEnv *env){ - S3JniDb * ps = S3JniGlobal.perDb.aUsed; - S3JniDb * pNext = 0; - for( ; ps; ps = pNext ){ - pNext = ps->pNext; - if(ps->env == env){ - S3JniDb * const pPrev = ps->pPrev; - S3JniDb_set_aside(ps); - assert(pPrev ? pPrev->pNext!=ps : 1); - pNext = pPrev; - } - } -} - -/** - Uncache any state for the given JNIEnv, clearing all Java - references the cache owns. Returns true if env was cached and false - if it was not found in the cache. - - Also passes env to S3JniDb_free_for_env() to free up - what would otherwise be stale references. +/* +** Uncache any state for the given JNIEnv, clearing all Java +** references the cache owns. Returns true if env was cached and false +** if it was not found in the cache. */ static int S3JniGlobal_env_uncache(JNIEnv * const env){ - struct S3JniEnvCache * row = S3JniGlobal.envCache.aHead; + struct S3JniEnv * row; + MUTEX_ENV_ASSERT_LOCKED; + row = SJG.envCache.aHead; for( ; row; row = row->pNext ){ if( row->env == env ){ break; } } - if( !row ) return 0; + if( !row ){ + return 0; + } if( row->pNext ) row->pNext->pPrev = row->pPrev; if( row->pPrev ) row->pPrev->pNext = row->pNext; - if( S3JniGlobal.envCache.aHead == row ){ + if( SJG.envCache.aHead == row ){ assert( !row->pPrev ); - S3JniGlobal.envCache.aHead = row->pNext; + SJG.envCache.aHead = row->pNext; } - S3JniEnvCache_clear(row); - assert( !row->pNext ); - assert( !row->pPrev ); - row->pNext = S3JniGlobal.envCache.aFree; + memset(row, 0, sizeof(S3JniEnv)); + row->pNext = SJG.envCache.aFree; if( row->pNext ) row->pNext->pPrev = row; - S3JniGlobal.envCache.aFree = row; - S3JniDb_free_for_env(env); + SJG.envCache.aFree = row; return 1; } -static void S3JniGlobal_S3JniEnvCache_clear(void){ - while( S3JniGlobal.envCache.aHead ){ - S3JniGlobal_env_uncache( S3JniGlobal.envCache.aHead->env ); - } -} - -/** - Searches the NativePointerHolder cache for the given combination. - If it finds one, it returns it as-is. If it doesn't AND the cache - has a free slot, it populates that slot with (env, zClassName, - klazz) and returns it. If the cache is full with no match it - returns NULL. - - It is up to the caller to populate the other members of the returned - object if needed. - - zClassName must be a static string so we can use its address as a - cache key. - - This simple cache catches >99% of searches in the current - (2023-07-31) tests. +/* +** Searches the NativePointerHolder cache for the given combination of +** args. It returns a cache entry with its klazz member set. +** +** It is up to the caller to populate the other members of the +** returned object if needed, taking care to lock the population with +** MUTEX_NPH_ENTER/LEAVE. +** +** This simple cache catches >99% of searches in the current +** (2023-07-31) tests. */ -FIXME_THREADING(S3JniEnvCache) -static S3JniNphCache * S3JniGlobal_nph_cache(JNIEnv * const env, const char *zClassName){ +static S3JniNphClass * S3JniGlobal_nph_cache(JNIEnv * const env, S3NphRef const* pRef){ /** - According to: + According to: https://developer.ibm.com/articles/j-jni/ @@ -1019,128 +1030,79 @@ static S3JniNphCache * S3JniGlobal_nph_cache(JNIEnv * const env, const char *zCl looking up class objects can be expensive, so they should be cached as well. */ - struct S3JniEnvCache * const envRow = S3JniGlobal_env_cache(env); - S3JniNphCache * freeSlot = 0; - S3JniNphCache * pCache = 0; - int i; - assert(envRow); - for( i = 0; i < NphCache_SIZE; ++i ){ - pCache = &envRow->nph[i]; - if(zClassName == pCache->zClassName){ - ++S3JniGlobal.metrics.nphCacheHits; -#define DUMP_NPH_CACHES 0 -#if DUMP_NPH_CACHES - MARKER(("Cache hit #%u %s klazz@%p nativePointer field@%p, ctor@%p\n", - S3JniGlobal.metrics.nphCacheHits, zClassName, pCache->klazz, pCache->fidValue, - pCache->midCtor)); -#endif - assert(pCache->klazz); - return pCache; - }else if(!freeSlot && !pCache->zClassName){ - freeSlot = pCache; + S3JniNphClass * const pNC = &SJG.nph[pRef->index]; + if( !pNC->pRef ){ + MUTEX_NPH_ENTER; + if( !pNC->pRef ){ + pNC->pRef = pRef; + pNC->klazz = (*env)->FindClass(env, pRef->zName); + EXCEPTION_IS_FATAL("FindClass() unexpectedly threw"); + pNC->klazz = REF_G(pNC->klazz); } + MUTEX_NPH_LEAVE; } - if(freeSlot){ - freeSlot->zClassName = zClassName; - freeSlot->klazz = (*env)->FindClass(env, zClassName); - EXCEPTION_IS_FATAL("FindClass() unexpectedly threw"); - freeSlot->klazz = REF_G(freeSlot->klazz); - ++S3JniGlobal.metrics.nphCacheMisses; -#if DUMP_NPH_CACHES - static unsigned int cacheMisses = 0; - MARKER(("Cache miss #%u %s klazz@%p nativePointer field@%p, ctor@%p\n", - S3JniGlobal.metrics.nphCacheMisses, zClassName, freeSlot->klazz, - freeSlot->fidValue, freeSlot->midCtor)); -#endif -#undef DUMP_NPH_CACHES - }else{ - (*env)->FatalError(env, "MAINTENANCE REQUIRED: NphCache_SIZE is too low."); - } - return freeSlot; + return pNC; } -/** - Returns the ID of the "nativePointer" field from the given - NativePointerHolder class. - */ -static jfieldID NativePointerHolder_getField(JNIEnv * const env, jclass klazz){ - jfieldID rv = (*env)->GetFieldID(env, klazz, "nativePointer", "J"); - EXCEPTION_IS_FATAL("Code maintenance required: missing nativePointer field."); - return rv; +/* +** Returns the ID of the "nativePointer" field from the given +** NativePointerHolder class. +*/ +static jfieldID NativePointerHolder_getField(JNIEnv * const env, S3NphRef const* pRef){ + S3JniNphClass * const pNC = S3JniGlobal_nph_cache(env, pRef); + if(!pNC->fidValue){ + MUTEX_NPH_ENTER; + if(!pNC->fidValue){ + pNC->fidValue = (*env)->GetFieldID(env, pNC->klazz, "nativePointer", "J"); + EXCEPTION_IS_FATAL("Code maintenance required: missing nativePointer field."); + } + MUTEX_NPH_LEAVE; + } + return pNC->fidValue; } -/** - Sets a native ptr value in NativePointerHolder object ppOut. - zClassName must be a static string so we can use its address - as a cache key. +/* +** Sets a native ptr value in NativePointerHolder object ppOut. +** zClassName must be a static string so we can use its address +** as a cache key. */ static void NativePointerHolder_set(JNIEnv * env, jobject ppOut, const void * p, - const char *zClassName){ - jfieldID setter = 0; - S3JniNphCache * const pCache = S3JniGlobal_nph_cache(env, zClassName); - if(pCache && pCache->klazz && pCache->fidValue){ - assert(zClassName == pCache->zClassName); - setter = pCache->fidValue; - assert(setter); - }else{ - jclass const klazz = - pCache ? pCache->klazz : (*env)->GetObjectClass(env, ppOut); - setter = NativePointerHolder_getField(env, klazz); - if(pCache){ - assert(pCache->klazz); - assert(!pCache->fidValue); - assert(zClassName == pCache->zClassName); - pCache->fidValue = setter; - } - } - (*env)->SetLongField(env, ppOut, setter, (jlong)p); + S3NphRef const* pRef){ + (*env)->SetLongField(env, ppOut, NativePointerHolder_getField(env, pRef), + (jlong)p); EXCEPTION_IS_FATAL("Could not set NativePointerHolder.nativePointer."); } -/** - Fetches a native ptr value from NativePointerHolder object ppOut. - zClassName must be a static string so we can use its address as a - cache key. +/* +** Fetches a native ptr value from NativePointerHolder object ppOut. +** zClassName must be a static string so we can use its address as a +** cache key. This is a no-op if pObj is NULL. */ -static void * NativePointerHolder_get(JNIEnv * env, jobject pObj, const char *zClassName){ +static void * NativePointerHolder_get(JNIEnv * env, jobject pObj, S3NphRef const* pRef){ if( pObj ){ - jfieldID getter = 0; - void * rv = 0; - S3JniNphCache * const pCache = S3JniGlobal_nph_cache(env, zClassName); - if(pCache && pCache->fidValue){ - getter = pCache->fidValue; - }else{ - jclass const klazz = - pCache ? pCache->klazz : (*env)->GetObjectClass(env, pObj); - getter = NativePointerHolder_getField(env, klazz); - if(pCache){ - assert(pCache->klazz); - assert(zClassName == pCache->zClassName); - pCache->fidValue = getter; - } - } - rv = (void*)(*env)->GetLongField(env, pObj, getter); - IFTHREW_REPORT; + void * rv = (void*)(*env)->GetLongField( + env, pObj, NativePointerHolder_getField(env, pRef) + ); + EXCEPTION_IS_FATAL("Cannot fetch NativePointerHolder.nativePointer."); return rv; }else{ return 0; } } -/** - Extracts the new S3JniDb instance from the free-list, or - allocates one if needed, associats it with pDb, and returns. - Returns NULL on OOM. pDb MUST be associated with jDb via - NativePointerHolder_set(). +/* +** Extracts the new S3JniDb instance from the free-list, or allocates +** one if needed, associats it with pDb, and returns. Returns NULL on +** OOM. pDb MUST, on success of the calling operation, subsequently be +** associated with jDb via NativePointerHolder_set(). */ static S3JniDb * S3JniDb_alloc(JNIEnv * const env, sqlite3 *pDb, jobject jDb){ S3JniDb * rv; - if(S3JniGlobal.perDb.aFree){ - rv = S3JniGlobal.perDb.aFree; - //MARKER(("state@%p for db allocating for db@%p from free-list\n", rv, pDb)); - //MARKER(("%p->pPrev@%p, pNext@%p\n", rv, rv->pPrev, rv->pNext)); - S3JniGlobal.perDb.aFree = rv->pNext; + MUTEX_PDB_ENTER; + if( SJG.perDb.aFree ){ + rv = SJG.perDb.aFree; + SJG.perDb.aFree = rv->pNext; assert(rv->pNext != rv); assert(rv->pPrev != rv); assert(rv->pPrev ? (rv->pPrev!=rv->pNext) : 1); @@ -1150,198 +1112,129 @@ static S3JniDb * S3JniDb_alloc(JNIEnv * const env, sqlite3 *pDb, rv->pNext->pPrev = 0; rv->pNext = 0; } + s3jni_incr( &SJG.metrics.nPdbRecycled ); }else{ rv = s3jni_malloc(env, sizeof(S3JniDb)); - //MARKER(("state@%p for db allocating for db@%p from heap\n", rv, pDb)); - if(rv){ + if( rv ){ memset(rv, 0, sizeof(S3JniDb)); + s3jni_incr( &SJG.metrics.nPdbAlloc ); } } - if(rv){ - rv->pNext = S3JniGlobal.perDb.aUsed; - S3JniGlobal.perDb.aUsed = rv; + if( rv ){ + rv->pNext = SJG.perDb.aUsed; + SJG.perDb.aUsed = rv; if(rv->pNext){ assert(!rv->pNext->pPrev); rv->pNext->pPrev = rv; } rv->jDb = REF_G(jDb); rv->pDb = pDb; - rv->env = env; } + MUTEX_PDB_LEAVE; return rv; } -#if 0 -static void S3JniDb_dump(S3JniDb *s){ - MARKER(("S3JniDb->env @ %p\n", s->env)); - MARKER(("S3JniDb->pDb @ %p\n", s->pDb)); - MARKER(("S3JniDb->trace.jObj @ %p\n", s->trace.jObj)); - MARKER(("S3JniDb->progress.jObj @ %p\n", s->progress.jObj)); - MARKER(("S3JniDb->commitHook.jObj @ %p\n", s->commitHook.jObj)); - MARKER(("S3JniDb->rollbackHook.jObj @ %p\n", s->rollbackHook.jObj)); - MARKER(("S3JniDb->busyHandler.jObj @ %p\n", s->busyHandler.jObj)); - MARKER(("S3JniDb->env @ %p\n", s->env)); -} -#endif - -/** - Returns the S3JniDb object for the given db. If allocIfNeeded is - true then a new instance will be allocated if no mapping currently - exists, else NULL is returned if no mapping is found. - - The 3rd and 4th args should normally only be non-0 for - sqlite3_open(_v2)(). For most other cases, they must be 0, in which - case the db handle will be fished out of the jDb object and NULL is - returned if jDb does not have any associated S3JniDb. - - If called with a NULL jDb and non-NULL pDb then allocIfNeeded MUST - be false and it will look for a matching db object. That usage is - required for functions, like sqlite3_context_db_handle(), which - return a (sqlite3*) but do not take one as an argument. +/* +** Returns the S3JniDb object for the given db. +** +** The 3rd argument should normally only be non-0 for routines which +** are called from the C library and pass a native db handle instead of +** a Java handle. In normal usage, the 2nd argument is a Java-side sqlite3 +** object, from which the db is fished out. +** +** Returns NULL if jDb and pDb are both NULL or if there is no +** matching S3JniDb entry for pDb or the pointer fished out of jDb. */ -FIXME_THREADING(S3JniEnvCache) -FIXME_THREADING(perDb) -static S3JniDb * S3JniDb_for_db(JNIEnv * const env, jobject jDb, - sqlite3 *pDb, int allocIfNeeded){ - S3JniDb * s = S3JniGlobal.perDb.aUsed; - if(!jDb){ - if(pDb){ - assert(!allocIfNeeded); - }else{ - return 0; +static S3JniDb * S3JniDb_for_db(JNIEnv * const env, jobject jDb, sqlite3 *pDb){ + S3JniDb * s = 0; + if(jDb || pDb){ + MUTEX_PDB_ENTER; + s = SJG.perDb.aUsed; + if(!pDb){ + assert( jDb ); + pDb = PtrGet_sqlite3(jDb); } - } - assert(allocIfNeeded ? !!pDb : 1); - if(!allocIfNeeded && !pDb){ - pDb = PtrGet_sqlite3(jDb); - } - for( ; pDb && s; s = s->pNext){ - if(s->pDb == pDb) return s; - } - if(allocIfNeeded){ - s = S3JniDb_alloc(env, pDb, jDb); + for( ; pDb && s; s = s->pNext){ + if(s->pDb == pDb){ + break; + } + } + MUTEX_PDB_LEAVE; } return s; } -#if 0 -/** - An alternative form which searches for the S3JniDb instance for - pDb with no JNIEnv-specific info. This can be (but _should_ it be?) - called from the context of a separate JNIEnv than the one mapped - to in the returned object. Returns 0 if no match is found. +/* +** Unref any Java-side state in ax and zero out ax. */ -FIXME_THREADING(perDb) -static S3JniDb * S3JniDb_for_db2(sqlite3 *pDb){ - S3JniDb * s = S3JniGlobal.perDb.aUsed; - for( ; pDb && s; s = s->pNext){ - if(s->pDb == pDb) return s; +static void S3JniAutoExtension_clear(JNIEnv * const env, + S3JniAutoExtension * const ax){ + if( ax->jObj ){ + UNREF_G(ax->jObj); + memset(ax, 0, sizeof(*ax)); } +} + +/* +** Initializes a pre-allocated S3JniAutoExtension object. Returns +** non-0 if there is an error collecting the required state from +** jAutoExt (which must be an AutoExtension object). On error, it +** passes ax to S3JniAutoExtension_clear(). +*/ +static int S3JniAutoExtension_init(JNIEnv *const env, + S3JniAutoExtension * const ax, + jobject const jAutoExt){ + jclass const klazz = (*env)->GetObjectClass(env, jAutoExt); + + ax->midFunc = (*env)->GetMethodID(env, klazz, "xEntryPoint", + "(Lorg/sqlite/jni/sqlite3;)I"); + UNREF_L(klazz); + EXCEPTION_WARN_IGNORE; + if(!ax->midFunc){ + MARKER(("Error getting xEntryPoint(sqlite3) from AutoExtension object.")); + S3JniAutoExtension_clear(env, ax); + return SQLITE_ERROR; + } + ax->jObj = REF_G(jAutoExt); return 0; } -#endif -#if S3JNI_ENABLE_AUTOEXT -/** - Unlink ax from S3JniGlobal.autoExt and free it. -*/ -static void S3JniAutoExtension_free(JNIEnv * const env, - S3JniAutoExtension * const ax){ - if( ax ){ - if( ax->pNext ) ax->pNext->pPrev = ax->pPrev; - if( ax == S3JniGlobal.autoExt.pHead ){ - assert( !ax->pNext ); - S3JniGlobal.autoExt.pHead = ax->pNext; - }else if( ax->pPrev ){ - ax->pPrev->pNext = ax->pNext; - } - ax->pNext = ax->pPrev = 0; - UNREF_G(ax->jObj); - sqlite3_free(ax); - } -} - -/** - Allocates a new auto extension and plugs it in to S3JniGlobal.autoExt. - Returns 0 on OOM or if there is an error collecting the required - state from jAutoExt (which must be an AutoExtension object). -*/ -static S3JniAutoExtension * S3JniAutoExtension_alloc(JNIEnv *const env, - jobject const jAutoExt){ - S3JniAutoExtension * const ax = sqlite3_malloc(sizeof(*ax)); - if( ax ){ - jclass klazz; - memset(ax, 0, sizeof(*ax)); - klazz = (*env)->GetObjectClass(env, jAutoExt); - if(!klazz){ - S3JniAutoExtension_free(env, ax); - return 0; - } - ax->midFunc = (*env)->GetMethodID(env, klazz, "xEntryPoint", - "(Lorg/sqlite/jni/sqlite3;)I"); - if(!ax->midFunc){ - MARKER(("Error getting xEntryPoint(sqlite3) from object.")); - S3JniAutoExtension_free(env, ax); - return 0; - } - ax->jObj = REF_G(jAutoExt); - ax->pNext = S3JniGlobal.autoExt.pHead; - if( ax->pNext ) ax->pNext->pPrev = ax; - S3JniGlobal.autoExt.pHead = ax; - } - return ax; -} -#endif /* S3JNI_ENABLE_AUTOEXT */ - -/** - Requires that jCx be a Java-side sqlite3_context wrapper for pCx. - This function calls sqlite3_aggregate_context() to allocate a tiny - sliver of memory, the address of which is set in - jCx->aggregateContext. The memory is only used as a key for - mapping client-side results of aggregate result sets across - calls to the UDF's callbacks. - - isFinal must be 1 for xFinal() calls and 0 for all others, the - difference being that the xFinal() invocation will not allocate - new memory if it was not already, resulting in a value of 0 - for jCx->aggregateContext. - - Returns 0 on success. Returns SQLITE_NOMEM on allocation error, - noting that it will not allocate when isFinal is true. It returns - SQLITE_ERROR if there's a serious internal error in dealing with - the JNI state. +/* +** Requires that jCx be a Java-side sqlite3_context wrapper for pCx. +** This function calls sqlite3_aggregate_context() to allocate a tiny +** sliver of memory, the address of which is set in +** jCx->aggregateContext. The memory is only used as a key for +** mapping client-side results of aggregate result sets across +** calls to the UDF's callbacks. +** +** isFinal must be 1 for xFinal() calls and 0 for all others, the +** difference being that the xFinal() invocation will not allocate +** new memory if it was not already, resulting in a value of 0 +** for jCx->aggregateContext. +** +** Returns 0 on success. Returns SQLITE_NOMEM on allocation error, +** noting that it will not allocate when isFinal is true. It returns +** SQLITE_ERROR if there's a serious internal error in dealing with +** the JNI state. */ static int udf_setAggregateContext(JNIEnv * env, jobject jCx, sqlite3_context * pCx, int isFinal){ - jfieldID member; void * pAgg; int rc = 0; - S3JniNphCache * const pCache = - S3JniGlobal_nph_cache(env, S3JniClassNames.sqlite3_context); - if(pCache && pCache->klazz && pCache->fidSetAgg){ - member = pCache->fidSetAgg; - assert(member); - }else{ - jclass const klazz = - pCache ? pCache->klazz : (*env)->GetObjectClass(env, jCx); - member = (*env)->GetFieldID(env, klazz, "aggregateContext", "J"); - if( !member ){ - IFTHREW{ EXCEPTION_REPORT; EXCEPTION_CLEAR; } - return s3jni_db_error(sqlite3_context_db_handle(pCx), - SQLITE_ERROR, - "Internal error: cannot find " - "sqlite3_context::aggregateContext field."); - } - if(pCache){ - assert(pCache->klazz); - assert(!pCache->fidSetAgg); - pCache->fidSetAgg = member; + S3JniNphClass * const pNC = + S3JniGlobal_nph_cache(env, &S3NphRefs.sqlite3_context); + if(!pNC->fidAggCtx){ + MUTEX_NPH_ENTER; + if(!pNC->fidAggCtx){ + pNC->fidAggCtx = (*env)->GetFieldID(env, pNC->klazz, "aggregateContext", "J"); + EXCEPTION_IS_FATAL("Cannot get sqlite3_contex.aggregateContext member."); } + MUTEX_NPH_LEAVE; } - pAgg = sqlite3_aggregate_context(pCx, isFinal ? 0 : 4); + pAgg = sqlite3_aggregate_context(pCx, isFinal ? 0 : sizeof(void*)); if( pAgg || isFinal ){ - (*env)->SetLongField(env, jCx, member, (jlong)pAgg); + (*env)->SetLongField(env, jCx, pNC->fidAggCtx, (jlong)pAgg); }else{ assert(!pAgg); rc = SQLITE_NOMEM; @@ -1349,100 +1242,135 @@ static int udf_setAggregateContext(JNIEnv * env, jobject jCx, return rc; } -/** - Common init for OutputPointer_set_Int32() and friends. zClassName must be a - pointer from S3JniClassNames. jOut must be an instance of that - class. Fetches the jfieldID for jOut's [value] property, which must - be of the type represented by the JNI type signature zTypeSig, and - stores it in pFieldId. Fails fatally if the property is not found, - as that presents a serious internal misuse. - - Property lookups are cached on a per-zClassName basis. Do not use - this routine with the same zClassName but different zTypeSig: it - will misbehave. +/* +** Common init for OutputPointer_set_Int32() and friends. pRef must be +** a pointer from S3NphRefs. jOut must be an instance of that +** class. If necessary, this fetches the jfieldID for jOut's [value] +** property, which must be of the type represented by the JNI type +** signature zTypeSig, and stores it in pRef's S3JniGlobal.nph entry. +** Fails fatally if the property is not found, as that presents a +** serious internal misuse. +** +** Property lookups are cached on a per-pRef basis. Do not use this +** routine with the same pRef but different zTypeSig: it will +** misbehave. */ -static void setupOutputPointer(JNIEnv * const env, const char *zClassName, - const char * const zTypeSig, - jobject const jOut, jfieldID * const pFieldId){ - jfieldID setter = 0; - S3JniNphCache * const pCache = - S3JniGlobal_nph_cache(env, zClassName); - if(pCache && pCache->klazz && pCache->fidValue){ - setter = pCache->fidValue; - }else{ - const jclass klazz = (*env)->GetObjectClass(env, jOut); - /*MARKER(("%s => %s\n", zClassName, zTypeSig));*/ - setter = (*env)->GetFieldID(env, klazz, "value", zTypeSig); - EXCEPTION_IS_FATAL("setupOutputPointer() could not find OutputPointer.*.value"); - if(pCache){ - assert(!pCache->fidValue); - pCache->fidValue = setter; +static jfieldID setupOutputPointer(JNIEnv * const env, S3NphRef const * pRef, + const char * const zTypeSig, + jobject const jOut){ + S3JniNphClass * const pNC = S3JniGlobal_nph_cache(env, pRef); + if(!pNC->fidValue){ + MUTEX_NPH_ENTER; + if(!pNC->fidValue){ + pNC->fidValue = (*env)->GetFieldID(env, pNC->klazz, "value", zTypeSig); + EXCEPTION_IS_FATAL("setupOutputPointer() could not find OutputPointer.*.value"); } + MUTEX_NPH_LEAVE; } - *pFieldId = setter; + return pNC->fidValue; } -/* Sets the value property of the OutputPointer.Int32 jOut object - to v. */ +/* +** Sets the value property of the OutputPointer.Int32 jOut object to +** v. +*/ static void OutputPointer_set_Int32(JNIEnv * const env, jobject const jOut, int v){ - jfieldID setter = 0; - setupOutputPointer(env, S3JniClassNames.OutputPointer_Int32, "I", jOut, &setter); + jfieldID const setter = setupOutputPointer( + env, &S3NphRefs.OutputPointer_Int32, "I", jOut + ); (*env)->SetIntField(env, jOut, setter, (jint)v); EXCEPTION_IS_FATAL("Cannot set OutputPointer.Int32.value"); } -/* Sets the value property of the OutputPointer.Int64 jOut object - to v. */ +/* +** Sets the value property of the OutputPointer.Int64 jOut object to +** v. +*/ static void OutputPointer_set_Int64(JNIEnv * const env, jobject const jOut, jlong v){ - jfieldID setter = 0; - setupOutputPointer(env, S3JniClassNames.OutputPointer_Int64, "J", jOut, &setter); + jfieldID const setter = setupOutputPointer( + env, &S3NphRefs.OutputPointer_Int64, "J", jOut + ); (*env)->SetLongField(env, jOut, setter, v); EXCEPTION_IS_FATAL("Cannot set OutputPointer.Int64.value"); } +/* +** Sets the value property of the OutputPointer.sqlite3 jOut object to +** v. +*/ static void OutputPointer_set_sqlite3(JNIEnv * const env, jobject const jOut, - jobject jDb){ - jfieldID setter = 0; - setupOutputPointer(env, S3JniClassNames.OutputPointer_sqlite3, - "Lorg/sqlite/jni/sqlite3;", jOut, &setter); + jobject jDb){ + jfieldID const setter = setupOutputPointer( + env, &S3NphRefs.OutputPointer_sqlite3, "Lorg/sqlite/jni/sqlite3;", jOut + ); (*env)->SetObjectField(env, jOut, setter, jDb); EXCEPTION_IS_FATAL("Cannot set OutputPointer.sqlite3.value"); } +/* +** Sets the value property of the OutputPointer.sqlite3_stmt jOut object to +** v. +*/ static void OutputPointer_set_sqlite3_stmt(JNIEnv * const env, jobject const jOut, - jobject jStmt){ - jfieldID setter = 0; - setupOutputPointer(env, S3JniClassNames.OutputPointer_sqlite3_stmt, - "Lorg/sqlite/jni/sqlite3_stmt;", jOut, &setter); + jobject jStmt){ + jfieldID const setter = setupOutputPointer( + env, &S3NphRefs.OutputPointer_sqlite3_stmt, + "Lorg/sqlite/jni/sqlite3_stmt;", jOut + ); (*env)->SetObjectField(env, jOut, setter, jStmt); EXCEPTION_IS_FATAL("Cannot set OutputPointer.sqlite3_stmt.value"); } +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +/* +** Sets the value property of the OutputPointer.sqlite3_value jOut object to +** v. +*/ +static void OutputPointer_set_sqlite3_value(JNIEnv * const env, jobject const jOut, + jobject jValue){ + jfieldID const setter = setupOutputPointer( + env, &S3NphRefs.OutputPointer_sqlite3_value, + "Lorg/sqlite/jni/sqlite3_value;", jOut + ); + (*env)->SetObjectField(env, jOut, setter, jValue); + EXCEPTION_IS_FATAL("Cannot set OutputPointer.sqlite3_value.value"); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + #ifdef SQLITE_ENABLE_FTS5 #if 0 -/* Sets the value property of the OutputPointer.ByteArray jOut object - to v. */ +/* +** Sets the value property of the OutputPointer.ByteArray jOut object +** to v. +*/ static void OutputPointer_set_ByteArray(JNIEnv * const env, jobject const jOut, jbyteArray const v){ - jfieldID setter = 0; - setupOutputPointer(env, S3JniClassNames.OutputPointer_ByteArray, "[B", - jOut, &setter); + jfieldID const setter = setupOutputPointer( + env, &S3NphRefs.OutputPointer_ByteArray, "[B", jOut + ); (*env)->SetObjectField(env, jOut, setter, v); EXCEPTION_IS_FATAL("Cannot set OutputPointer.ByteArray.value"); } #endif -/* Sets the value property of the OutputPointer.String jOut object - to v. */ + +/* +** Sets the value property of the OutputPointer.String jOut object to +** v. +*/ static void OutputPointer_set_String(JNIEnv * const env, jobject const jOut, jstring const v){ - jfieldID setter = 0; - setupOutputPointer(env, S3JniClassNames.OutputPointer_String, - "Ljava/lang/String;", jOut, &setter); + jfieldID const setter = setupOutputPointer( + env, &S3NphRefs.OutputPointer_String, "Ljava/lang/String;", jOut + ); (*env)->SetObjectField(env, jOut, setter, v); EXCEPTION_IS_FATAL("Cannot set OutputPointer.String.value"); } #endif /* SQLITE_ENABLE_FTS5 */ +/* +** Returns true if eTextRep is a valid sqlite3 encoding constant, else +** returns false. +*/ static int encodingTypeIsValid(int eTextRep){ switch(eTextRep){ case SQLITE_UTF8: case SQLITE_UTF16: @@ -1453,22 +1381,25 @@ static int encodingTypeIsValid(int eTextRep){ } } +/* +** Proxy for Java-side Collation.xCompare() callbacks. +*/ static int CollationState_xCompare(void *pArg, int nLhs, const void *lhs, int nRhs, const void *rhs){ S3JniDb * const ps = pArg; - JNIEnv * env = ps->env; + LocalJniGetEnv; jint rc = 0; jbyteArray jbaLhs = (*env)->NewByteArray(env, (jint)nLhs); jbyteArray jbaRhs = jbaLhs ? (*env)->NewByteArray(env, (jint)nRhs) : NULL; - //MARKER(("native xCompare nLhs=%d nRhs=%d\n", nLhs, nRhs)); if(!jbaRhs){ + UNREF_L(jbaLhs); s3jni_db_error(ps->pDb, SQLITE_NOMEM, 0); return 0; - //(*env)->FatalError(env, "Out of memory. Cannot allocate arrays for collation."); } (*env)->SetByteArrayRegion(env, jbaLhs, 0, (jint)nLhs, (const jbyte*)lhs); (*env)->SetByteArrayRegion(env, jbaRhs, 0, (jint)nRhs, (const jbyte*)rhs); - rc = (*env)->CallIntMethod(env, ps->collation.jObj, ps->collation.midCallback, + rc = (*env)->CallIntMethod(env, ps->hooks.collation.jObj, + ps->hooks.collation.midCallback, jbaLhs, jbaRhs); EXCEPTION_IGNORE; UNREF_L(jbaLhs); @@ -1479,137 +1410,98 @@ static int CollationState_xCompare(void *pArg, int nLhs, const void *lhs, /* Collation finalizer for use by the sqlite3 internals. */ static void CollationState_xDestroy(void *pArg){ S3JniDb * const ps = pArg; - S3JniHook_unref( ps->env, &ps->collation, 1 ); + S3JniHook_unref( s3jni_get_env(), &ps->hooks.collation, 1 ); } -/* State for sqlite3_result_java_object() and - sqlite3_value_java_object(). */ +/* +** State for sqlite3_result_java_object() and +** sqlite3_value_java_object(). +** +** TODO: this middle-man struct is no longer necessary. Conider +** removing it and passing around jObj itself instead. OTOH, we might +** find more state to pack in here. +*/ typedef struct { - /* The JNI docs say: - - https://docs.oracle.com/javase/8/docs/technotes/guides/jni/spec/design.html - - > The VM is guaranteed to pass the same interface pointer to a - native method when it makes multiple calls to the native method - from the same Java thread. - - Per the accompanying diagram, the "interface pointer" is the - pointer-to-pointer which is passed to all JNI calls - (`JNIEnv *env`), implying that we need to be caching that. The - verbiage "interface pointer" implies, however, that we should be - storing the dereferenced `(*env)` pointer. - - This posts claims it's unsafe to cache JNIEnv at all, even when - it's always used in the same thread: - - https://stackoverflow.com/questions/12420463 - - And this one seems to contradict that: - - https://stackoverflow.com/questions/13964608 - - For later reference: - - https://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/design.html#wp1242 - - https://developer.android.com/training/articles/perf-jni - - The later has the following say about caching: - - > If performance is important, it's useful to look the - [class/method ID] values up once and cache the results in your - native code. Because there is a limit of one JavaVM per - process, it's reasonable to store this data in a static local - structure. ... The class references, field IDs, and method IDs - are guaranteed valid until the class is unloaded. Classes are - only unloaded if all classes associated with a ClassLoader can - be garbage collected, which is rare but will not be impossible - in Android. Note however that the jclass is a class reference - and must be protected with a call to NewGlobalRef (see the next - section). - */ - JNIEnv * env; jobject jObj; } ResultJavaVal; /* For use with sqlite3_result/value_pointer() */ -#define RESULT_JAVA_VAL_STRING "ResultJavaVal" +#define ResultJavaValuePtrStr "org.sqlite.jni.ResultJavaVal" +/* +** Allocate a new ResultJavaVal and assign it a new global ref of +** jObj. Caller owns the returned object and must eventually pass it +** to ResultJavaVal_finalizer(). +*/ static ResultJavaVal * ResultJavaVal_alloc(JNIEnv * const env, jobject jObj){ ResultJavaVal * rv = sqlite3_malloc(sizeof(ResultJavaVal)); if(rv){ - rv->env = env; rv->jObj = jObj ? REF_G(jObj) : 0; } return rv; } +/* +** If v is not NULL, it must point to a a ResultJavaVal object. Its +** object reference is relinquished and v is freed. +*/ static void ResultJavaVal_finalizer(void *v){ if(v){ ResultJavaVal * const rv = (ResultJavaVal*)v; - if(rv->jObj) (*(rv->env))->DeleteGlobalRef(rv->env, rv->jObj); + LocalJniGetEnv; + UNREF_G(rv->jObj); sqlite3_free(rv); } } -/** - Returns a new Java instance of the class named by zClassName, which - MUST be interface-compatible with NativePointerHolder and MUST have - a no-arg constructor. The NativePointerHolder_set() method is - passed the new Java object and pNative. Hypothetically returns NULL - if Java fails to allocate, but the JNI docs are not entirely clear - on that detail. - - Always use a static string pointer from S3JniClassNames for the 2nd - argument so that we can use its address as a cache key. +/* +** Returns a new Java instance of the class named by zClassName, which +** MUST be interface-compatible with NativePointerHolder and MUST have +** a no-arg constructor. The NativePointerHolder_set() method is +** passed the new Java object and pNative. Hypothetically returns NULL +** if Java fails to allocate, but the JNI docs are not entirely clear +** on that detail. +** +** Always use a static pointer from the S3NphRefs struct for the 2nd +** argument so that we can use pRef->index as an O(1) cache key. */ -static jobject new_NativePointerHolder_object(JNIEnv * const env, const char *zClassName, +static jobject new_NativePointerHolder_object(JNIEnv * const env, S3NphRef const * pRef, const void * pNative){ jobject rv = 0; - jclass klazz = 0; - jmethodID ctor = 0; - S3JniNphCache * const pCache = - S3JniGlobal_nph_cache(env, zClassName); - if(pCache && pCache->midCtor){ - assert( pCache->klazz ); - klazz = pCache->klazz; - ctor = pCache->midCtor; - }else{ - klazz = pCache - ? pCache->klazz - : (*env)->FindClass(env, zClassName); - ctor = klazz ? (*env)->GetMethodID(env, klazz, "", "()V") : 0; - EXCEPTION_IS_FATAL("Cannot find constructor for class."); - if(pCache){ - assert(zClassName == pCache->zClassName); - assert(pCache->klazz); - assert(!pCache->midCtor); - pCache->midCtor = ctor; + S3JniNphClass * const pNC = S3JniGlobal_nph_cache(env, pRef); + if(!pNC->midCtor){ + MUTEX_NPH_ENTER; + if(!pNC->midCtor){ + pNC->midCtor = (*env)->GetMethodID(env, pNC->klazz, "", "()V"); + EXCEPTION_IS_FATAL("Cannot find constructor for class."); } + MUTEX_NPH_LEAVE; } - assert(klazz); - assert(ctor); - rv = (*env)->NewObject(env, klazz, ctor); + rv = (*env)->NewObject(env, pNC->klazz, pNC->midCtor); EXCEPTION_IS_FATAL("No-arg constructor threw."); - if(rv) NativePointerHolder_set(env, rv, pNative, zClassName); + s3jni_oom_check(rv); + if(rv) NativePointerHolder_set(env, rv, pNative, pRef); return rv; } static inline jobject new_sqlite3_wrapper(JNIEnv * const env, sqlite3 *sv){ - return new_NativePointerHolder_object(env, S3JniClassNames.sqlite3, sv); + return new_NativePointerHolder_object(env, &S3NphRefs.sqlite3, sv); } static inline jobject new_sqlite3_context_wrapper(JNIEnv * const env, sqlite3_context *sv){ - return new_NativePointerHolder_object(env, S3JniClassNames.sqlite3_context, sv); + return new_NativePointerHolder_object(env, &S3NphRefs.sqlite3_context, sv); } static inline jobject new_sqlite3_stmt_wrapper(JNIEnv * const env, sqlite3_stmt *sv){ - return new_NativePointerHolder_object(env, S3JniClassNames.sqlite3_stmt, sv); + return new_NativePointerHolder_object(env, &S3NphRefs.sqlite3_stmt, sv); } static inline jobject new_sqlite3_value_wrapper(JNIEnv * const env, sqlite3_value *sv){ - return new_NativePointerHolder_object(env, S3JniClassNames.sqlite3_value, sv); + return new_NativePointerHolder_object(env, &S3NphRefs.sqlite3_value, sv); } +/* +** Type IDs for SQL function categories. +*/ enum UDFType { UDF_SCALAR = 1, UDF_AGGREGATE, @@ -1628,9 +1520,7 @@ typedef void (*udf_xFinal_f)(sqlite3_context*); */ typedef struct S3JniUdf S3JniUdf; struct S3JniUdf { - JNIEnv * env; /* env registered from */; jobject jObj /* SQLFunction instance */; - jclass klazz /* jObj's class */; char * zFuncName /* Only for error reporting and debug logging */; enum UDFType type; /** Method IDs for the various UDF methods. */ @@ -1648,12 +1538,12 @@ static S3JniUdf * S3JniUdf_alloc(JNIEnv * const env, jobject jObj){ "(Lorg/sqlite/jni/sqlite3_context;[Lorg/sqlite/jni/sqlite3_value;)V"; const char * zFV = /* signature for xFinal, xValue */ "(Lorg/sqlite/jni/sqlite3_context;)V"; + jclass const klazz = (*env)->GetObjectClass(env, jObj); + memset(s, 0, sizeof(S3JniUdf)); - s->env = env; s->jObj = REF_G(jObj); - s->klazz = REF_G((*env)->GetObjectClass(env, jObj)); #define FGET(FuncName,FuncType,Field) \ - s->Field = (*env)->GetMethodID(env, s->klazz, FuncName, FuncType); \ + s->Field = (*env)->GetMethodID(env, klazz, FuncName, FuncType); \ if(!s->Field) (*env)->ExceptionClear(env) FGET("xFunc", zFSI, jmidxFunc); FGET("xStep", zFSI, jmidxStep); @@ -1661,6 +1551,7 @@ static S3JniUdf * S3JniUdf_alloc(JNIEnv * const env, jobject jObj){ FGET("xValue", zFV, jmidxValue); FGET("xInverse", zFSI, jmidxInverse); #undef FGET + UNREF_L(klazz); if(s->jmidxFunc) s->type = UDF_SCALAR; else if(s->jmidxStep && s->jmidxFinal){ s->type = s->jmidxValue ? UDF_WINDOW : UDF_AGGREGATE; @@ -1672,12 +1563,11 @@ static S3JniUdf * S3JniUdf_alloc(JNIEnv * const env, jobject jObj){ } static void S3JniUdf_free(S3JniUdf * s){ - JNIEnv * const env = s->env; + LocalJniGetEnv; if(env){ //MARKER(("UDF cleanup: %s\n", s->zFuncName)); - s3jni_call_xDestroy(env, s->jObj, s->klazz); + s3jni_call_xDestroy(env, s->jObj); UNREF_G(s->jObj); - UNREF_G(s->klazz); } sqlite3_free(s->zFuncName); sqlite3_free(s); @@ -1701,10 +1591,6 @@ typedef struct { Converts the given (cx, argc, argv) into arguments for the given UDF, placing the result in the final argument. Returns 0 on success, SQLITE_NOMEM on allocation error. - - TODO: see what we can do to optimize the - new_sqlite3_value_wrapper() call. e.g. find the ctor a single time - and call it here, rather than looking it up repeatedly. */ static int udf_args(JNIEnv *env, sqlite3_context * const cx, @@ -1717,7 +1603,7 @@ static int udf_args(JNIEnv *env, *jArgv = 0; if(!jcx) goto error_oom; ja = (*env)->NewObjectArray(env, argc, - S3JniGlobal_env_cache(env)->g.cObj, + SJG.g.cObj, NULL); if(!ja) goto error_oom; for(i = 0; i < argc; ++i){ @@ -1736,47 +1622,73 @@ error_oom: return SQLITE_NOMEM; } -static int udf_report_exception(sqlite3_context * cx, - const char *zFuncName, - const char *zFuncType){ - int rc; - char * z = - sqlite3_mprintf("Client-defined function %s.%s() threw. It should " - "not do that.", - zFuncName ? zFuncName : "", zFuncType); - if(z){ - sqlite3_result_error(cx, z, -1); - sqlite3_free(z); - rc = SQLITE_ERROR; +/* +** Must be called immediately after a Java-side UDF callback throws. +** If translateToErr is true then it sets the exception's message in +** the result error using sqlite3_result_error(). If translateToErr is +** false then it emits a warning that the function threw but should +** not do so. In either case, it clears the exception state. +** +** Returns SQLITE_NOMEM if an allocation fails, else SQLITE_ERROR. In +** the latter case it calls sqlite3_result_error_nomem(). +*/ +static int udf_report_exception(JNIEnv * const env, int translateToErr, + sqlite3_context * cx, + const char *zFuncName, const char *zFuncType ){ + jthrowable const ex = (*env)->ExceptionOccurred(env); + int rc = SQLITE_ERROR; + + assert(ex && "This must only be called when a Java exception is pending."); + if( translateToErr ){ + char * zMsg; + char * z; + + EXCEPTION_CLEAR; + zMsg = s3jni_exception_error_msg(env, ex); + z = sqlite3_mprintf("Client-defined SQL function %s.%s() threw: %s", + zFuncName ? zFuncName : "", zFuncType, + zMsg ? zMsg : "Unknown exception" ); + sqlite3_free(zMsg); + if( z ){ + sqlite3_result_error(cx, z, -1); + sqlite3_free(z); + }else{ + sqlite3_result_error_nomem(cx); + rc = SQLITE_NOMEM; + } }else{ - sqlite3_result_error_nomem(cx); - rc = SQLITE_NOMEM; + MARKER(("Client-defined SQL function %s.%s() threw. " + "It should not do that.\n", + zFuncName ? zFuncName : "", zFuncType)); + (*env)->ExceptionDescribe( env ); + EXCEPTION_CLEAR; } return rc; } -/** - Sets up the state for calling a Java-side xFunc/xStep/xInverse() - UDF, calls it, and returns 0 on success. +/* +** Sets up the state for calling a Java-side xFunc/xStep/xInverse() +** UDF, calls it, and returns 0 on success. */ -static int udf_xFSI(sqlite3_context* pCx, int argc, - sqlite3_value** argv, - S3JniUdf * s, +static int udf_xFSI(sqlite3_context* const pCx, int argc, + sqlite3_value** const argv, + S3JniUdf * const s, jmethodID xMethodID, - const char * zFuncType){ - JNIEnv * const env = s->env; + const char * const zFuncType){ + LocalJniGetEnv; udf_jargs args = {0,0}; - int rc = udf_args(s->env, pCx, argc, argv, &args.jcx, &args.jargv); - //MARKER(("%s.%s() pCx = %p\n", s->zFuncName, zFuncType, pCx)); - if(rc) return rc; + int rc = udf_args(env, pCx, argc, argv, &args.jcx, &args.jargv); + //MARKER(("UDF::%s.%s()\n", s->zFuncName, zFuncType)); + if(rc) return rc; if( UDF_SCALAR != s->type ){ rc = udf_setAggregateContext(env, args.jcx, pCx, 0); } if( 0 == rc ){ (*env)->CallVoidMethod(env, s->jObj, xMethodID, args.jcx, args.jargv); IFTHREW{ - rc = udf_report_exception(pCx, s->zFuncName, zFuncType); + rc = udf_report_exception(env, 'F'==zFuncType[1]/*xFunc*/, pCx, + s->zFuncName, zFuncType); } } UNREF_L(args.jcx); @@ -1784,69 +1696,76 @@ static int udf_xFSI(sqlite3_context* pCx, int argc, return rc; } -/** - Sets up the state for calling a Java-side xFinal/xValue() UDF, - calls it, and returns 0 on success. +/* +** Sets up the state for calling a Java-side xFinal/xValue() UDF, +** calls it, and returns 0 on success. */ static int udf_xFV(sqlite3_context* cx, S3JniUdf * s, jmethodID xMethodID, const char *zFuncType){ - JNIEnv * const env = s->env; - jobject jcx = new_sqlite3_context_wrapper(s->env, cx); + LocalJniGetEnv; + jobject jcx = new_sqlite3_context_wrapper(env, cx); int rc = 0; + int const isFinal = 'F'==zFuncType[1]/*xFinal*/; //MARKER(("%s.%s() cx = %p\n", s->zFuncName, zFuncType, cx)); if(!jcx){ - sqlite3_result_error_nomem(cx); + if( isFinal ) sqlite3_result_error_nomem(cx); return SQLITE_NOMEM; } //MARKER(("UDF::%s.%s()\n", s->zFuncName, zFuncType)); if( UDF_SCALAR != s->type ){ - rc = udf_setAggregateContext(env, jcx, cx, 1); + rc = udf_setAggregateContext(env, jcx, cx, isFinal); } if( 0 == rc ){ (*env)->CallVoidMethod(env, s->jObj, xMethodID, jcx); IFTHREW{ - rc = udf_report_exception(cx,s->zFuncName, zFuncType); + rc = udf_report_exception(env, isFinal, cx, s->zFuncName, + zFuncType); } } UNREF_L(jcx); return rc; } +/* Proxy for C-to-Java xFunc. */ static void udf_xFunc(sqlite3_context* cx, int argc, sqlite3_value** argv){ S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx); - ++S3JniGlobal.metrics.udf.nFunc; + s3jni_incr( &SJG.metrics.udf.nFunc ); udf_xFSI(cx, argc, argv, s, s->jmidxFunc, "xFunc"); } +/* Proxy for C-to-Java xStep. */ static void udf_xStep(sqlite3_context* cx, int argc, sqlite3_value** argv){ S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx); - ++S3JniGlobal.metrics.udf.nStep; + s3jni_incr( &SJG.metrics.udf.nStep ); udf_xFSI(cx, argc, argv, s, s->jmidxStep, "xStep"); } +/* Proxy for C-to-Java xFinal. */ static void udf_xFinal(sqlite3_context* cx){ S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx); - ++S3JniGlobal.metrics.udf.nFinal; + s3jni_incr( &SJG.metrics.udf.nFinal ); udf_xFV(cx, s, s->jmidxFinal, "xFinal"); } +/* Proxy for C-to-Java xValue. */ static void udf_xValue(sqlite3_context* cx){ S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx); - ++S3JniGlobal.metrics.udf.nValue; + s3jni_incr( &SJG.metrics.udf.nValue ); udf_xFV(cx, s, s->jmidxValue, "xValue"); } +/* Proxy for C-to-Java xInverse. */ static void udf_xInverse(sqlite3_context* cx, int argc, sqlite3_value** argv){ S3JniUdf * const s = (S3JniUdf*)sqlite3_user_data(cx); - ++S3JniGlobal.metrics.udf.nInverse; + s3jni_incr( &SJG.metrics.udf.nInverse ); udf_xFSI(cx, argc, argv, s, s->jmidxInverse, "xInverse"); } //////////////////////////////////////////////////////////////////////// // What follows is the JNI/C bindings. They are in alphabetical order -// except for this macro-generated subset which are kept together here -// at the front... +// except for this macro-generated subset which are kept together +// (alphabetized) here at the front... //////////////////////////////////////////////////////////////////////// WRAP_INT_STMT(1bind_1parameter_1count, sqlite3_bind_parameter_count) WRAP_INT_DB(1changes, sqlite3_changes) @@ -1866,6 +1785,11 @@ WRAP_INT_DB(1error_1offset, sqlite3_error_offset) WRAP_INT_DB(1extended_1errcode, sqlite3_extended_errcode) WRAP_MUTF8_VOID(1libversion, sqlite3_libversion) WRAP_INT_VOID(1libversion_1number, sqlite3_libversion_number) +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +WRAP_INT_DB(1preupdate_1blobwrite, sqlite3_preupdate_blobwrite) +WRAP_INT_DB(1preupdate_1count, sqlite3_preupdate_count) +WRAP_INT_DB(1preupdate_1depth, sqlite3_preupdate_depth) +#endif WRAP_INT_INT(1sleep, sqlite3_sleep) WRAP_MUTF8_VOID(1sourceid, sqlite3_sourceid) WRAP_INT_VOID(1threadsafe, sqlite3_threadsafe) @@ -1880,187 +1804,224 @@ WRAP_INT_SVALUE(1value_1numeric_1type, sqlite3_value_numeric_type) WRAP_INT_SVALUE(1value_1subtype, sqlite3_value_subtype) WRAP_INT_SVALUE(1value_1type, sqlite3_value_type) -#if S3JNI_ENABLE_AUTOEXT +#undef WRAP_INT64_DB +#undef WRAP_INT_DB +#undef WRAP_INT_INT +#undef WRAP_INT_STMT +#undef WRAP_INT_STMT_INT +#undef WRAP_INT_SVALUE +#undef WRAP_INT_VOID +#undef WRAP_MUTF8_VOID +#undef WRAP_STR_STMT_INT + /* Central auto-extension handler. */ -FIXME_THREADING(autoExt) -static int s3jni_auto_extension(sqlite3 *pDb, const char **pzErr, - const struct sqlite3_api_routines *ignored){ - S3JniAutoExtension const * pAX = S3JniGlobal.autoExt.pHead; - int rc; +static int s3jni_run_java_auto_extensions(sqlite3 *pDb, const char **pzErr, + const struct sqlite3_api_routines *ignored){ + int rc = 0; + unsigned i, go = 1; JNIEnv * env = 0; - S3JniDb * const ps = S3JniGlobal.autoExt.psOpening; - //MARKER(("auto-extension on open()ing ps@%p db@%p\n", ps, pDb)); - S3JniGlobal.autoExt.psOpening = 0; - if( !pAX ){ - assert( 0==S3JniGlobal.autoExt.isRunning ); - return 0; - } - else if( S3JniGlobal.autoExt.isRunning ){ - /* Necessary to avoid certain endless loop/stack overflow cases. */ - *pzErr = sqlite3_mprintf("Auto-extensions must not be triggered while " - "auto-extensions are running."); - return SQLITE_MISUSE; - } - else if(!ps){ - MARKER(("Internal error: cannot find S3JniDb for auto-extension\n")); - return SQLITE_ERROR; - }else if( (*S3JniGlobal.jvm)->GetEnv(S3JniGlobal.jvm, (void **)&env, JNI_VERSION_1_8) ){ - assert(!"Cannot get JNIEnv"); - *pzErr = sqlite3_mprintf("Could not get current JNIEnv."); + S3JniDb * ps; + S3JniEnv * jc; + if( 0==SJG.autoExt.nExt ) return 0; + env = s3jni_get_env(); + jc = S3JniGlobal_env_cache(env); + ps = jc->pdbOpening; + if( !ps ){ + MARKER(("Unexpected arrival of null S3JniDb in auto-extension runner.\n")); + *pzErr = sqlite3_mprintf("Unexpected arrival of null S3JniDb in auto-extension runner."); return SQLITE_ERROR; } - assert( !ps->pDb /* it's still being opened */ ); + jc->pdbOpening = 0; + assert( !ps->pDb && "it's still being opened" ); ps->pDb = pDb; assert( ps->jDb ); - NativePointerHolder_set(env, ps->jDb, pDb, S3JniClassNames.sqlite3); - ++S3JniGlobal.autoExt.isRunning; - for( ; pAX; pAX = pAX->pNext ){ - rc = (*env)->CallIntMethod(env, pAX->jObj, pAX->midFunc, ps->jDb); - IFTHREW { - jthrowable const ex = (*env)->ExceptionOccurred(env); - char * zMsg; - EXCEPTION_CLEAR; - zMsg = s3jni_exception_error_msg(env, ex); - UNREF_L(ex); - *pzErr = sqlite3_mprintf("auto-extension threw: %s", zMsg); - sqlite3_free(zMsg); - rc = rc ? rc : SQLITE_ERROR; - break; - }else if( rc ){ - break; + NativePointerHolder_set(env, ps->jDb, pDb, &S3NphRefs.sqlite3); + for( i = 0; go && 0==rc; ++i ){ + S3JniAutoExtension ax = {0,0} + /* We need a copy of the auto-extension object, with our own + ** local reference to it, to avoid a race condition with another + ** thread manipulating the list during the call and invaliding + ** what ax points to. */; + MUTEX_EXT_ENTER; + if( i >= SJG.autoExt.nExt ){ + go = 0; + }else{ + ax.jObj = REF_L(SJG.autoExt.pExt[i].jObj); + ax.midFunc = SJG.autoExt.pExt[i].midFunc; + } + MUTEX_EXT_LEAVE; + if( ax.jObj ){ + rc = (*env)->CallIntMethod(env, ax.jObj, ax.midFunc, ps->jDb); + IFTHREW { + jthrowable const ex = (*env)->ExceptionOccurred(env); + char * zMsg; + EXCEPTION_CLEAR; + zMsg = s3jni_exception_error_msg(env, ex); + UNREF_L(ex); + *pzErr = sqlite3_mprintf("auto-extension threw: %s", zMsg); + sqlite3_free(zMsg); + if( !rc ) rc = SQLITE_ERROR; + } + UNREF_L(ax.jObj); } } - --S3JniGlobal.autoExt.isRunning; return rc; } -FIXME_THREADING(autoExt) -JDECL(jint,1auto_1extension)(JENV_OSELF, jobject jAutoExt){ +JDECL(jint,1auto_1extension)(JENV_CSELF, jobject jAutoExt){ static int once = 0; + int i; S3JniAutoExtension * ax; + int rc = 0; if( !jAutoExt ) return SQLITE_MISUSE; - else if( 0==once && ++once ){ - sqlite3_auto_extension( (void(*)(void))s3jni_auto_extension ); - } - ax = S3JniGlobal.autoExt.pHead; - for( ; ax; ax = ax->pNext ){ - if( (*env)->IsSameObject(env, ax->jObj, jAutoExt) ){ - return 0 /* C API treats this as a no-op. */; + MUTEX_EXT_ENTER; + for( i = 0; i < SJG.autoExt.nExt; ++i ){ + /* Look for match or first empty slot. */ + ax = &SJG.autoExt.pExt[i]; + if( ax->jObj && (*env)->IsSameObject(env, ax->jObj, jAutoExt) ){ + MUTEX_EXT_LEAVE; + return 0 /* this as a no-op. */; } } - return S3JniAutoExtension_alloc(env, jAutoExt) ? 0 : SQLITE_NOMEM; + if(i == SJG.autoExt.nExt ){ + assert( SJG.autoExt.nExt <= SJG.autoExt.nAlloc ); + if( SJG.autoExt.nExt == SJG.autoExt.nAlloc ){ + unsigned n = 1 + SJG.autoExt.nAlloc; + S3JniAutoExtension * const aNew = + sqlite3_realloc( SJG.autoExt.pExt, + n * sizeof(S3JniAutoExtension) ); + if( !aNew ){ + rc = SQLITE_NOMEM; + }else{ + SJG.autoExt.pExt = aNew; + ++SJG.autoExt.nAlloc; + } + } + if( 0==rc ){ + ax = &SJG.autoExt.pExt[SJG.autoExt.nExt]; + rc = S3JniAutoExtension_init(env, ax, jAutoExt); + assert( rc ? 0==ax->jObj : 0!=ax->jObj ); + } + } + if( 0==rc ){ + if( 0==once && ++once ){ + rc = sqlite3_auto_extension( (void(*)(void))s3jni_run_java_auto_extensions ); + if( rc ){ + assert( ax ); + S3JniAutoExtension_clear(env, ax); + } + } + if( 0==rc ){ + ++SJG.autoExt.nExt; + } + } + MUTEX_EXT_LEAVE; + return rc; } -#endif /* S3JNI_ENABLE_AUTOEXT */ -FIXME_THREADING(S3JniEnvCache) JDECL(jint,1bind_1blob)(JENV_CSELF, jobject jpStmt, jint ndx, jbyteArray baData, jint nMax){ - int rc; - if(!baData){ - rc = sqlite3_bind_null(PtrGet_sqlite3_stmt(jpStmt), ndx); - }else{ - jbyte * const pBuf = JBA_TOC(baData); - rc = sqlite3_bind_blob(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, pBuf, (int)nMax, - SQLITE_TRANSIENT); - JBA_RELEASE(baData,pBuf); - } + jbyte * const pBuf = baData ? s3jni_jbytearray_bytes(baData) : 0; + int const rc = sqlite3_bind_blob(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, + pBuf, (int)nMax, SQLITE_TRANSIENT); + s3jni_jbytearray_release(baData,pBuf); return (jint)rc; } -FIXME_THREADING(S3JniEnvCache) JDECL(jint,1bind_1double)(JENV_CSELF, jobject jpStmt, jint ndx, jdouble val){ return (jint)sqlite3_bind_double(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, (double)val); } -FIXME_THREADING(S3JniEnvCache) JDECL(jint,1bind_1int)(JENV_CSELF, jobject jpStmt, jint ndx, jint val){ return (jint)sqlite3_bind_int(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, (int)val); } -FIXME_THREADING(S3JniEnvCache) JDECL(jint,1bind_1int64)(JENV_CSELF, jobject jpStmt, jint ndx, jlong val){ return (jint)sqlite3_bind_int64(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, (sqlite3_int64)val); } -FIXME_THREADING(S3JniEnvCache) JDECL(jint,1bind_1null)(JENV_CSELF, jobject jpStmt, jint ndx){ return (jint)sqlite3_bind_null(PtrGet_sqlite3_stmt(jpStmt), (int)ndx); } -FIXME_THREADING(S3JniEnvCache) JDECL(jint,1bind_1parameter_1index)(JENV_CSELF, jobject jpStmt, jbyteArray jName){ int rc = 0; - jbyte * const pBuf = JBA_TOC(jName); + jbyte * const pBuf = s3jni_jbytearray_bytes(jName); if(pBuf){ rc = sqlite3_bind_parameter_index(PtrGet_sqlite3_stmt(jpStmt), (const char *)pBuf); - JBA_RELEASE(jName, pBuf); + s3jni_jbytearray_release(jName, pBuf); } return rc; } -FIXME_THREADING(S3JniEnvCache) JDECL(jint,1bind_1text)(JENV_CSELF, jobject jpStmt, jint ndx, jbyteArray baData, jint nMax){ - if(baData){ - jbyte * const pBuf = JBA_TOC(baData); - int rc = sqlite3_bind_text(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, (const char *)pBuf, - (int)nMax, SQLITE_TRANSIENT); - JBA_RELEASE(baData, pBuf); - return (jint)rc; - }else{ - return sqlite3_bind_null(PtrGet_sqlite3_stmt(jpStmt), (int)ndx); - } + jbyte * const pBuf = baData ? s3jni_jbytearray_bytes(baData) : 0; + int const rc = sqlite3_bind_text(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, + (const char *)pBuf, + (int)nMax, SQLITE_TRANSIENT); + s3jni_jbytearray_release(baData, pBuf); + return (jint)rc; +} + +JDECL(jint,1bind_1text16)(JENV_CSELF, jobject jpStmt, + jint ndx, jbyteArray baData, jint nMax){ + jbyte * const pBuf = baData ? s3jni_jbytearray_bytes(baData) : 0; + int const rc = sqlite3_bind_text16(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, + pBuf, (int)nMax, SQLITE_TRANSIENT); + s3jni_jbytearray_release(baData, pBuf); + return (jint)rc; } -FIXME_THREADING(S3JniEnvCache) JDECL(jint,1bind_1zeroblob)(JENV_CSELF, jobject jpStmt, jint ndx, jint n){ return (jint)sqlite3_bind_zeroblob(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, (int)n); } -FIXME_THREADING(S3JniEnvCache) JDECL(jint,1bind_1zeroblob64)(JENV_CSELF, jobject jpStmt, jint ndx, jlong n){ return (jint)sqlite3_bind_zeroblob(PtrGet_sqlite3_stmt(jpStmt), (int)ndx, (sqlite3_uint64)n); } +/* Central C-to-Java busy handler proxy. */ static int s3jni_busy_handler(void* pState, int n){ S3JniDb * const ps = (S3JniDb *)pState; int rc = 0; - if( ps->busyHandler.jObj ){ - JNIEnv * const env = ps->env; - rc = (*env)->CallIntMethod(env, ps->busyHandler.jObj, - ps->busyHandler.midCallback, (jint)n); + if( ps->hooks.busyHandler.jObj ){ + LocalJniGetEnv; + rc = (*env)->CallIntMethod(env, ps->hooks.busyHandler.jObj, + ps->hooks.busyHandler.midCallback, (jint)n); IFTHREW{ - EXCEPTION_WARN_CALLBACK_THREW("busy-handler callback"); - EXCEPTION_CLEAR; - rc = s3jni_db_error(ps->pDb, SQLITE_ERROR, "busy-handle callback threw."); + EXCEPTION_WARN_CALLBACK_THREW("sqlite3_busy_handler() callback"); + rc = s3jni_db_exception(env, ps, SQLITE_ERROR, + "sqlite3_busy_handler() callback threw."); } } return rc; } -FIXME_THREADING(S3JniEnvCache) JDECL(jint,1busy_1handler)(JENV_CSELF, jobject jDb, jobject jBusy){ - S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0); + S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0); int rc = 0; if(!ps) return (jint)SQLITE_NOMEM; if(jBusy){ - S3JniHook * const pHook = &ps->busyHandler; + S3JniHook * const pHook = &ps->hooks.busyHandler; if(pHook->jObj && (*env)->IsSameObject(env, pHook->jObj, jBusy)){ /* Same object - this is a no-op. */ return 0; } + jclass klazz; S3JniHook_unref(env, pHook, 1); pHook->jObj = REF_G(jBusy); - pHook->klazz = REF_G((*env)->GetObjectClass(env, jBusy)); - pHook->midCallback = (*env)->GetMethodID(env, pHook->klazz, "xCallback", "(I)I"); + klazz = (*env)->GetObjectClass(env, jBusy); + pHook->midCallback = (*env)->GetMethodID(env, klazz, "xCallback", "(I)I"); + UNREF_L(klazz); IFTHREW { S3JniHook_unref(env, pHook, 0); rc = SQLITE_ERROR; @@ -2069,73 +2030,75 @@ JDECL(jint,1busy_1handler)(JENV_CSELF, jobject jDb, jobject jBusy){ return rc; } }else{ - S3JniHook_unref(env, &ps->busyHandler, 1); + S3JniHook_unref(env, &ps->hooks.busyHandler, 1); } return jBusy ? sqlite3_busy_handler(ps->pDb, s3jni_busy_handler, ps) : sqlite3_busy_handler(ps->pDb, 0, 0); } -FIXME_THREADING(S3JniEnvCache) -FIXME_THREADING(perDb) JDECL(jint,1busy_1timeout)(JENV_CSELF, jobject jDb, jint ms){ - S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0); + S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0); if( ps ){ - S3JniHook_unref(env, &ps->busyHandler, 1); + S3JniHook_unref(env, &ps->hooks.busyHandler, 1); return sqlite3_busy_timeout(ps->pDb, (int)ms); } return SQLITE_MISUSE; } -#if S3JNI_ENABLE_AUTOEXT -FIXME_THREADING(autoExt) JDECL(jboolean,1cancel_1auto_1extension)(JENV_CSELF, jobject jAutoExt){ - S3JniAutoExtension * ax;; - if( S3JniGlobal.autoExt.isRunning ) return JNI_FALSE; - for( ax = S3JniGlobal.autoExt.pHead; ax; ax = ax->pNext ){ - if( (*env)->IsSameObject(env, ax->jObj, jAutoExt) ){ - S3JniAutoExtension_free(env, ax); - return JNI_TRUE; + S3JniAutoExtension * ax; + jboolean rc = JNI_FALSE; + int i; + MUTEX_EXT_ENTER; + /* This algo mirrors the one in the core. */ + for( i = SJG.autoExt.nExt-1; i >= 0; --i ){ + ax = &SJG.autoExt.pExt[i]; + if( ax->jObj && (*env)->IsSameObject(env, ax->jObj, jAutoExt) ){ + S3JniAutoExtension_clear(env, ax); + /* Move final entry into this slot. */ + --SJG.autoExt.nExt; + *ax = SJG.autoExt.pExt[SJG.autoExt.nExt]; + memset(&SJG.autoExt.pExt[SJG.autoExt.nExt], 0, + sizeof(S3JniAutoExtension)); + assert(! SJG.autoExt.pExt[SJG.autoExt.nExt].jObj ); + rc = JNI_TRUE; + break; } } - return JNI_FALSE; + MUTEX_EXT_LEAVE; + return rc; } -#endif /* S3JNI_ENABLE_AUTOEXT */ -/** - Wrapper for sqlite3_close(_v2)(). -*/ +/* Wrapper for sqlite3_close(_v2)(). */ static jint s3jni_close_db(JNIEnv * const env, jobject jDb, int version){ int rc = 0; S3JniDb * ps = 0; assert(version == 1 || version == 2); - ps = S3JniDb_for_db(env, jDb, 0, 0); + ps = S3JniDb_for_db(env, jDb, 0); if(ps){ - //MARKER(("close()ing db@%p\n", ps->pDb)); rc = 1==version ? (jint)sqlite3_close(ps->pDb) : (jint)sqlite3_close_v2(ps->pDb); - S3JniDb_set_aside(ps) - /* MUST come after close() because of ps->trace. */; - NativePointerHolder_set(env, jDb, 0, S3JniClassNames.sqlite3); + if( 0==rc ){ + S3JniDb_set_aside(env, ps) + /* MUST come after close() because of ps->trace. */; + NativePointerHolder_set(env, jDb, 0, &S3NphRefs.sqlite3); + } } return (jint)rc; } -FIXME_THREADING(S3JniEnvCache) -FIXME_THREADING(perDb) JDECL(jint,1close_1v2)(JENV_CSELF, jobject pDb){ return s3jni_close_db(env, pDb, 2); } -FIXME_THREADING(S3JniEnvCache) -FIXME_THREADING(perDb) JDECL(jint,1close)(JENV_CSELF, jobject pDb){ return s3jni_close_db(env, pDb, 1); } -/** - Assumes z is an array of unsigned short and returns the index in - that array of the first element with the value 0. +/* +** Assumes z is an array of unsigned short and returns the index in +** that array of the first element with the value 0. */ static unsigned int s3jni_utf16_strlen(void const * z){ unsigned int i = 0; @@ -2144,37 +2107,33 @@ static unsigned int s3jni_utf16_strlen(void const * z){ return i; } -/** - sqlite3_collation_needed16() hook impl. - */ +/* Central C-to-Java sqlite3_collation_needed16() hook impl. */ static void s3jni_collation_needed_impl16(void *pState, sqlite3 *pDb, int eTextRep, const void * z16Name){ S3JniDb * const ps = pState; - JNIEnv * const env = ps->env; + LocalJniGetEnv; unsigned int const nName = s3jni_utf16_strlen(z16Name); jstring jName = (*env)->NewString(env, (jchar const *)z16Name, nName); IFTHREW{ s3jni_db_error(ps->pDb, SQLITE_NOMEM, 0); EXCEPTION_CLEAR; }else{ - (*env)->CallVoidMethod(env, ps->collationNeeded.jObj, - ps->collationNeeded.midCallback, + (*env)->CallVoidMethod(env, ps->hooks.collationNeeded.jObj, + ps->hooks.collationNeeded.midCallback, ps->jDb, (jint)eTextRep, jName); IFTHREW{ - s3jni_db_exception(env, ps, 0, "collation-needed callback threw"); + s3jni_db_exception(env, ps, 0, "sqlite3_collation_needed() callback threw"); } + UNREF_L(jName); } - UNREF_L(jName); } -FIXME_THREADING(S3JniEnvCache) -FIXME_THREADING(perDb) JDECL(jint,1collation_1needed)(JENV_CSELF, jobject jDb, jobject jHook){ - S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0); + S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0); jclass klazz; jobject pOld = 0; jmethodID xCallback; - S3JniHook * const pHook = &ps->collationNeeded; + S3JniHook * const pHook = &ps->hooks.collationNeeded; int rc; if( !ps ) return SQLITE_MISUSE; @@ -2192,6 +2151,7 @@ JDECL(jint,1collation_1needed)(JENV_CSELF, jobject jDb, jobject jHook){ klazz = (*env)->GetObjectClass(env, jHook); xCallback = (*env)->GetMethodID(env, klazz, "xCollationNeeded", "(Lorg/sqlite/jni/sqlite3;ILjava/lang/String;)I"); + UNREF_L(klazz); IFTHREW { rc = s3jni_db_exception(env, ps, SQLITE_MISUSE, "Cannot not find matching callback on " @@ -2205,7 +2165,6 @@ JDECL(jint,1collation_1needed)(JENV_CSELF, jobject jDb, jobject jHook){ return rc; } -FIXME_THREADING(S3JniEnvCache) JDECL(jbyteArray,1column_1blob)(JENV_CSELF, jobject jpStmt, jint ndx){ sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt); @@ -2219,34 +2178,29 @@ JDECL(jbyteArray,1column_1blob)(JENV_CSELF, jobject jpStmt, } } -FIXME_THREADING(S3JniEnvCache) JDECL(jdouble,1column_1double)(JENV_CSELF, jobject jpStmt, jint ndx){ return (jdouble)sqlite3_column_double(PtrGet_sqlite3_stmt(jpStmt), (int)ndx); } -FIXME_THREADING(S3JniEnvCache) JDECL(jint,1column_1int)(JENV_CSELF, jobject jpStmt, jint ndx){ return (jint)sqlite3_column_int(PtrGet_sqlite3_stmt(jpStmt), (int)ndx); } -FIXME_THREADING(S3JniEnvCache) JDECL(jlong,1column_1int64)(JENV_CSELF, jobject jpStmt, jint ndx){ return (jlong)sqlite3_column_int64(PtrGet_sqlite3_stmt(jpStmt), (int)ndx); } -FIXME_THREADING(S3JniEnvCache) -JDECL(jbyteArray,1column_1text)(JENV_CSELF, jobject jpStmt, +JDECL(jbyteArray,1column_1text_1utf8)(JENV_CSELF, jobject jpStmt, jint ndx){ sqlite3_stmt * const stmt = PtrGet_sqlite3_stmt(jpStmt); const int n = sqlite3_column_bytes(stmt, (int)ndx); const unsigned char * const p = sqlite3_column_text(stmt, (int)ndx); - return s3jni_new_jbyteArray(env, p, n); + return p ? s3jni_new_jbyteArray(env, p, n) : NULL; } -FIXME_THREADING(S3JniEnvCache) JDECL(jstring,1column_1text16)(JENV_CSELF, jobject jpStmt, jint ndx){ sqlite3_stmt * const stmt = PtrGet_sqlite3_stmt(jpStmt); @@ -2255,7 +2209,6 @@ JDECL(jstring,1column_1text16)(JENV_CSELF, jobject jpStmt, return s3jni_text16_to_jstring(env, p, n); } -FIXME_THREADING(S3JniEnvCache) JDECL(jobject,1column_1value)(JENV_CSELF, jobject jpStmt, jint ndx){ sqlite3_value * const sv = sqlite3_column_value(PtrGet_sqlite3_stmt(jpStmt), (int)ndx); @@ -2264,12 +2217,12 @@ JDECL(jobject,1column_1value)(JENV_CSELF, jobject jpStmt, static int s3jni_commit_rollback_hook_impl(int isCommit, S3JniDb * const ps){ - JNIEnv * const env = ps->env; + LocalJniGetEnv; int rc = isCommit - ? (int)(*env)->CallIntMethod(env, ps->commitHook.jObj, - ps->commitHook.midCallback) - : (int)((*env)->CallVoidMethod(env, ps->rollbackHook.jObj, - ps->rollbackHook.midCallback), 0); + ? (int)(*env)->CallIntMethod(env, ps->hooks.commit.jObj, + ps->hooks.commit.midCallback) + : (int)((*env)->CallVoidMethod(env, ps->hooks.rollback.jObj, + ps->hooks.rollback.midCallback), 0); IFTHREW{ EXCEPTION_CLEAR; rc = s3jni_db_error(ps->pDb, SQLITE_ERROR, "hook callback threw."); @@ -2285,14 +2238,14 @@ static void s3jni_rollback_hook_impl(void *pP){ (void)s3jni_commit_rollback_hook_impl(0, pP); } -FIXME_THREADING(perDb) -static jobject s3jni_commit_rollback_hook(int isCommit, JNIEnv * const env,jobject jDb, - jobject jHook){ - S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0); +static jobject s3jni_commit_rollback_hook(int isCommit, JNIEnv * const env, + jobject jDb, jobject jHook){ + S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0); jclass klazz; jobject pOld = 0; jmethodID xCallback; - S3JniHook * const pHook = isCommit ? &ps->commitHook : &ps->rollbackHook; + S3JniHook * const pHook = + isCommit ? &ps->hooks.commit : &ps->hooks.rollback; if(!ps){ s3jni_db_error(ps->pDb, SQLITE_NOMEM, 0); return 0; @@ -2317,6 +2270,7 @@ static jobject s3jni_commit_rollback_hook(int isCommit, JNIEnv * const env,jobje xCallback = (*env)->GetMethodID(env, klazz, isCommit ? "xCommitHook" : "xRollbackHook", isCommit ? "()I" : "()V"); + UNREF_L(klazz); IFTHREW { EXCEPTION_REPORT; EXCEPTION_CLEAR; @@ -2343,66 +2297,158 @@ JDECL(jobject,1commit_1hook)(JENV_CSELF,jobject jDb, jobject jHook){ JDECL(jstring,1compileoption_1get)(JENV_CSELF, jint n){ - return (*env)->NewStringUTF( env, sqlite3_compileoption_get(n) ); + return (*env)->NewStringUTF( env, sqlite3_compileoption_get(n) ) + /* We know these to be ASCII, so MUTF-8 is fine. */; } JDECL(jboolean,1compileoption_1used)(JENV_CSELF, jstring name){ - const char *zUtf8 = JSTR_TOC(name); + const char *zUtf8 = s3jni_jstring_to_mutf8(name); const jboolean rc = 0==sqlite3_compileoption_used(zUtf8) ? JNI_FALSE : JNI_TRUE; - JSTR_RELEASE(name, zUtf8); + s3jni_mutf8_release(name, zUtf8); return rc; } -FIXME_THREADING(perDb) +/* +** sqlite3_config(SQLITE_CONFIG_...) wrapper for a small subset of +** options. +*/ +JDECL(jint,1config__I)(JENV_CSELF, jint n){ + switch(n){ + case SQLITE_CONFIG_SINGLETHREAD: + case SQLITE_CONFIG_MULTITHREAD: + case SQLITE_CONFIG_SERIALIZED: + return sqlite3_config( n ); + default: + return SQLITE_MISUSE; + } +} + +#ifdef SQLITE_ENABLE_SQLLOG +/* C-to-Java SQLITE_CONFIG_SQLLOG wrapper. */ +static void s3jni_config_sqllog(void *ignored, sqlite3 *pDb, const char *z, int op){ + jobject jArg0 = 0; + jstring jArg1 = 0; + LocalJniGetEnv; + S3JniDb * const ps = S3JniDb_for_db(env, 0, pDb); + S3JniHook * const hook = &SJG.hooks.sqllog; + + if( !ps || !hook->jObj ) return; + jArg0 = REF_L(ps->jDb); + switch(op){ + case 0: /* db opened */ + case 1: /* SQL executed */ + jArg1 = s3jni_utf8_to_jstring(env, z, -1); + break; + case 2: /* db closed */ + break; + default: + (*env)->FatalError(env, "Unhandled 4th arg to SQLITE_CONFIG_SQLLOG."); + break; + } + (*env)->CallVoidMethod(env, hook->jObj, hook->midCallback, jArg0, jArg1, op); + IFTHREW{ + EXCEPTION_WARN_CALLBACK_THREW("SQLITE_CONFIG_SQLLOG callback"); + EXCEPTION_CLEAR; + } + UNREF_L(jArg0); + UNREF_L(jArg1); +} +//! Requirement of SQLITE_CONFIG_SQLLOG. +void sqlite3_init_sqllog(void){ + sqlite3_config( SQLITE_CONFIG_SQLLOG, s3jni_config_sqllog, 0 ); +} +#endif + +/* sqlite3_config(SQLITE_CONFIG_SQLLOG) wrapper. */ +JDECL(jint,1config__Lorg_sqlite_jni_SQLLog_2)(JENV_CSELF, jobject jLog){ +#ifdef SQLITE_ENABLE_SQLLOG + S3JniHook tmpHook; + S3JniHook * const hook = &tmpHook; + S3JniHook * const hookOld = & SJG.hooks.sqllog; + jclass klazz; + int rc = 0; + if( !jLog ){ + S3JniHook_unref(env, hookOld, 0); + return 0; + } + if( hookOld->jObj && (*env)->IsSameObject(env, jLog, hookOld->jObj) ){ + return 0; + } + klazz = (*env)->GetObjectClass(env, jLog); + hook->midCallback = (*env)->GetMethodID(env, klazz, "xSqllog", + "(Lorg/sqlite/jni/sqlite3;" + "Ljava/lang/String;" + "I)V"); + UNREF_L(klazz); + if( !hook->midCallback ){ + EXCEPTION_WARN_IGNORE; + S3JniHook_unref(env, hook, 0); + return SQLITE_ERROR; + } + hook->jObj = REF_G(jLog); + rc = sqlite3_config( SQLITE_CONFIG_SQLLOG, s3jni_config_sqllog, 0 ); + if( rc ){ + S3JniHook_unref(env, hook, 0); + }else{ + S3JniHook_unref(env, hookOld, 0); + *hookOld = *hook; + } + return rc; +#else + MARKER(("Warning: built without SQLITE_ENABLE_SQLLOG.\n")); + return SQLITE_MISUSE; +#endif +} + JDECL(jobject,1context_1db_1handle)(JENV_CSELF, jobject jpCx){ sqlite3 * const pDb = sqlite3_context_db_handle(PtrGet_sqlite3_context(jpCx)); - S3JniDb * const ps = pDb ? S3JniDb_for_db(env, 0, pDb, 0) : 0; + S3JniDb * const ps = pDb ? S3JniDb_for_db(env, 0, pDb) : 0; return ps ? ps->jDb : 0; } JDECL(jint,1create_1collation)(JENV_CSELF, jobject jDb, jstring name, jint eTextRep, jobject oCollation){ - S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0); - jclass klazz; int rc; const char *zName; - S3JniHook * pHook; - if(!ps) return (jint)SQLITE_NOMEM; - pHook = &ps->collation; + jclass klazz; + S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0); + S3JniHook * const pHook = ps ? &ps->hooks.collation : 0; + + if( !pHook ) return SQLITE_MISUSE; klazz = (*env)->GetObjectClass(env, oCollation); pHook->midCallback = (*env)->GetMethodID(env, klazz, "xCompare", "([B[B)I"); + UNREF_L(klazz); IFTHREW{ - EXCEPTION_REPORT; + UNREF_L(klazz); return s3jni_db_error(ps->pDb, SQLITE_ERROR, "Could not get xCompare() method for object."); } - zName = JSTR_TOC(name); + zName = s3jni_jstring_to_mutf8(name); rc = sqlite3_create_collation_v2(ps->pDb, zName, (int)eTextRep, ps, CollationState_xCompare, CollationState_xDestroy); - JSTR_RELEASE(name, zName); + s3jni_mutf8_release(name, zName); if( 0==rc ){ pHook->jObj = REF_G(oCollation); - pHook->klazz = REF_G(klazz); }else{ S3JniHook_unref(env, pHook, 1); } return (jint)rc; } -static jint create_function(JNIEnv * env, jobject jDb, jstring jFuncName, - jint nArg, jint eTextRep, jobject jFunctor){ +JDECL(jint,1create_1function)(JENV_CSELF, jobject jDb, jstring jFuncName, + jint nArg, jint eTextRep, jobject jFunctor){ S3JniUdf * s = 0; int rc; sqlite3 * const pDb = PtrGet_sqlite3(jDb); - const char * zFuncName = 0; + char * zFuncName = 0; if( !encodingTypeIsValid(eTextRep) ){ return s3jni_db_error(pDb, SQLITE_FORMAT, - "Invalid function encoding option."); + "Invalid function encoding option."); } s = S3JniUdf_alloc(env, jFunctor); if( !s ) return SQLITE_NOMEM; @@ -2412,7 +2458,7 @@ static jint create_function(JNIEnv * env, jobject jDb, jstring jFuncName, S3JniUdf_free(s); goto error_cleanup; } - zFuncName = JSTR_TOC(jFuncName); + zFuncName = s3jni_jstring_to_utf8(env,jFuncName,0); if(!zFuncName){ rc = SQLITE_NOMEM; S3JniUdf_free(s); @@ -2436,34 +2482,24 @@ static jint create_function(JNIEnv * env, jobject jDb, jstring jFuncName, rc = sqlite3_create_function_v2(pDb, zFuncName, nArg, eTextRep, s, xFunc, xStep, xFinal, S3JniUdf_finalizer); } - if( 0==rc ){ - s->zFuncName = sqlite3_mprintf("%s", zFuncName) - /* OOM here is non-fatal. Ignore it. Handling it would require - re-calling the appropriate create_function() func with 0 - for all xAbc args so that s would be finalized. */; - } error_cleanup: - JSTR_RELEASE(jFuncName, zFuncName); - /* on create_function() error, s will be destroyed via create_function() */ + sqlite3_free(zFuncName); + /* on sqlite3_create_function() error, s will be destroyed via + ** create_function(), so we're not leaking s. */ return (jint)rc; } -JDECL(jint,1create_1function)(JENV_CSELF, jobject jDb, jstring jFuncName, - jint nArg, jint eTextRep, jobject jFunctor){ - return create_function(env, jDb, jFuncName, nArg, eTextRep, jFunctor); -} - /* sqlite3_db_config() for (int,const char *) */ JDECL(int,1db_1config__Lorg_sqlite_jni_sqlite3_2ILjava_lang_String_2)( JENV_CSELF, jobject jDb, jint op, jstring jStr ){ - S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0); + S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0); int rc; char *zStr; switch( (ps && jStr) ? op : 0 ){ case SQLITE_DBCONFIG_MAINDBNAME: - zStr = s3jni_jstring_to_utf8(S3JniGlobal_env_cache(env), jStr, 0); + zStr = s3jni_jstring_to_utf8(env, jStr, 0); if( zStr ){ rc = sqlite3_db_config(ps->pDb, (int)op, zStr); if( rc ){ @@ -2482,14 +2518,16 @@ JDECL(int,1db_1config__Lorg_sqlite_jni_sqlite3_2ILjava_lang_String_2)( return rc; } -FIXME_THREADING(perDb) /* sqlite3_db_config() for (int,int*) */ -/* ACHTUNG: openjdk v19 creates a different mangled name for this - function than openjdk v8 does. */ +/* +** ACHTUNG: openjdk v19 creates a different mangled name for this +** function than openjdk v8 does. We account for that by exporting +** both versions of the name. +*/ JDECL(jint,1db_1config__Lorg_sqlite_jni_sqlite3_2IILorg_sqlite_jni_OutputPointer_Int32_2)( JENV_CSELF, jobject jDb, jint op, jint onOff, jobject jOut ){ - S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0); + S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0); int rc; switch( ps ? op : 0 ){ case SQLITE_DBCONFIG_ENABLE_FKEY: @@ -2523,11 +2561,11 @@ JDECL(jint,1db_1config__Lorg_sqlite_jni_sqlite3_2IILorg_sqlite_jni_OutputPointer return (jint)rc; } -/** - This is a workaround for openjdk v19 (and possibly others) encoding - this function's name differently than JDK v8 does. If we do not - install both names for this function then Java will not be able to - find the function in both environments. +/* +** This is a workaround for openjdk v19 (and possibly others) encoding +** this function's name differently than JDK v8 does. If we do not +** install both names for this function then Java will not be able to +** find the function in both environments. */ JDECL(jint,1db_1config__Lorg_sqlite_jni_sqlite3_2IILorg_sqlite_jni_OutputPointer_00024Int32_2)( JENV_CSELF, jobject jDb, jint op, jint onOff, jobject jOut @@ -2538,8 +2576,7 @@ JDECL(jint,1db_1config__Lorg_sqlite_jni_sqlite3_2IILorg_sqlite_jni_OutputPointer } JDECL(jstring,1db_1filename)(JENV_CSELF, jobject jDb, jstring jDbName){ - S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0); - S3JniEnvCache * const jc = S3JniGlobal_env_cache(env); + S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0); char *zDbName; jstring jRv = 0; int nStr = 0; @@ -2547,12 +2584,12 @@ JDECL(jstring,1db_1filename)(JENV_CSELF, jobject jDb, jstring jDbName){ if( !ps || !jDbName ){ return 0; } - zDbName = s3jni_jstring_to_utf8(jc, jDbName, &nStr); + zDbName = s3jni_jstring_to_utf8(env, jDbName, &nStr); if( zDbName ){ char const * zRv = sqlite3_db_filename(ps->pDb, zDbName); sqlite3_free(zDbName); if( zRv ){ - jRv = s3jni_utf8_to_jstring(jc, zRv, -1); + jRv = s3jni_utf8_to_jstring(env, zRv, -1); } } return jRv; @@ -2579,25 +2616,23 @@ JDECL(jint,1errcode)(JENV_CSELF, jobject jpDb){ JDECL(jstring,1errmsg)(JENV_CSELF, jobject jpDb){ sqlite3 * const pDb = PtrGet_sqlite3(jpDb); - S3JniEnvCache * const jc = pDb ? S3JniGlobal_env_cache(env) : 0; - return jc ? s3jni_utf8_to_jstring(jc, sqlite3_errmsg(pDb), -1) : 0; + return pDb ? s3jni_utf8_to_jstring(env, sqlite3_errmsg(pDb), -1) : 0; } JDECL(jstring,1errstr)(JENV_CSELF, jint rcCode){ return (*env)->NewStringUTF(env, sqlite3_errstr((int)rcCode)) - /* We know these values to be plain ASCII, so pose no - MUTF-8 incompatibility */; + /* We know these values to be plain ASCII, so pose no MUTF-8 + ** incompatibility */; } JDECL(jstring,1expanded_1sql)(JENV_CSELF, jobject jpStmt){ sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt); jstring rv = 0; if( pStmt ){ - S3JniEnvCache * const jc = S3JniGlobal_env_cache(env); char * zSql = sqlite3_expanded_sql(pStmt); - OOM_CHECK(zSql); + s3jni_oom_check(zSql); if( zSql ){ - rv = s3jni_utf8_to_jstring(jc, zSql, -1); + rv = s3jni_utf8_to_jstring(env, zSql, -1); sqlite3_free(zSql); } } @@ -2619,70 +2654,89 @@ JDECL(jint,1finalize)(JENV_CSELF, jobject jpStmt){ sqlite3_stmt * const pStmt = PtrGet_sqlite3_stmt(jpStmt); if( pStmt ){ rc = sqlite3_finalize(pStmt); - NativePointerHolder_set(env, jpStmt, 0, S3JniClassNames.sqlite3_stmt); + NativePointerHolder_set(env, jpStmt, 0, &S3NphRefs.sqlite3_stmt); } return rc; } +JDECL(void,1interrupt)(JENV_CSELF, jobject jpDb){ + sqlite3 * const pDb = PtrGet_sqlite3(jpDb); + if( pDb ) sqlite3_interrupt(pDb); +} + +JDECL(jboolean,1is_1interrupted)(JENV_CSELF, jobject jpDb){ + int rc = 0; + sqlite3 * const pDb = PtrGet_sqlite3(jpDb); + if( pDb ){ + rc = sqlite3_is_interrupted(pDb); + } + return rc ? JNI_TRUE : JNI_FALSE; +} + JDECL(jlong,1last_1insert_1rowid)(JENV_CSELF, jobject jpDb){ return (jlong)sqlite3_last_insert_rowid(PtrGet_sqlite3(jpDb)); } -//! Pre-open() code common to sqlite3_open(_v2)(). -static int s3jni_open_pre(JNIEnv * const env, S3JniEnvCache **jc, +/* Pre-open() code common to sqlite3_open(_v2)(). */ +static int s3jni_open_pre(JNIEnv * const env, S3JniEnv **jc, jstring jDbName, char **zDbName, - S3JniDb ** ps, jobject *jDb){ + S3JniDb ** ps){ + int rc = 0; + jobject jDb = 0; *jc = S3JniGlobal_env_cache(env); - if(!*jc) return SQLITE_NOMEM; - *zDbName = jDbName ? s3jni_jstring_to_utf8(*jc, jDbName, 0) : 0; - if(jDbName && !*zDbName) return SQLITE_NOMEM; - *jDb = new_sqlite3_wrapper(env, 0); - if( !*jDb ){ + if(!*jc){ + rc = SQLITE_NOMEM; + goto end; + } + *zDbName = jDbName ? s3jni_jstring_to_utf8(env, jDbName, 0) : 0; + if(jDbName && !*zDbName){ + rc = SQLITE_NOMEM; + goto end; + } + jDb = new_sqlite3_wrapper(env, 0); + if( !jDb ){ sqlite3_free(*zDbName); *zDbName = 0; - return SQLITE_NOMEM; + rc = SQLITE_NOMEM; + goto end; } - *ps = S3JniDb_alloc(env, 0, *jDb); -#if S3JNI_ENABLE_AUTOEXT + *ps = S3JniDb_alloc(env, 0, jDb); if(*ps){ - assert(!S3JniGlobal.autoExt.psOpening); - S3JniGlobal.autoExt.psOpening = *ps; + (*jc)->pdbOpening = *ps; + }else{ + UNREF_L(jDb); + rc = SQLITE_NOMEM; } -#endif - //MARKER(("pre-open ps@%p\n", *ps)); - return *ps ? 0 : SQLITE_NOMEM; +end: + return rc; } -/** - Post-open() code common to both the sqlite3_open() and - sqlite3_open_v2() bindings. ps->jDb must be the - org.sqlite.jni.sqlite3 object which will hold the db's native - pointer. theRc must be the result code of the open() op. If - *ppDb is NULL then ps is set aside and its state cleared, - else ps is associated with *ppDb. If *ppDb is not NULL then - ps->jDb is stored in jOut (an OutputPointer.sqlite3 instance). - - Returns theRc. +/* +** Post-open() code common to both the sqlite3_open() and +** sqlite3_open_v2() bindings. ps->jDb must be the +** org.sqlite.jni.sqlite3 object which will hold the db's native +** pointer. theRc must be the result code of the open() op. If +** *ppDb is NULL then ps is set aside and its state cleared, +** else ps is associated with *ppDb. If *ppDb is not NULL then +** ps->jDb is stored in jOut (an OutputPointer.sqlite3 instance). +** +** Returns theRc. */ -static int s3jni_open_post(JNIEnv * const env, S3JniDb * ps, - sqlite3 **ppDb, jobject jOut, int theRc){ - //MARKER(("post-open() ps@%p db@%p\n", ps, *ppDb)); -#if S3JNI_ENABLE_AUTOEXT - assert( S3JniGlobal.autoExt.pHead ? ps!=S3JniGlobal.autoExt.psOpening : 1 ); - S3JniGlobal.autoExt.psOpening = 0; -#endif +static int s3jni_open_post(JNIEnv * const env, S3JniEnv * const jc, + S3JniDb * ps, sqlite3 **ppDb, + jobject jOut, int theRc){ + jc->pdbOpening = 0; if(*ppDb){ assert(ps->jDb); -#if S3JNI_ENABLE_AUTOEXT - //MARKER(("*autoExt.pHead=%p, ppDb=%p, ps->pDb=%p\n", S3JniGlobal.autoExt.pHead, *ppDb, ps->pDb)); - // invalid when an autoext triggers another open(): - // assert( S3JniGlobal.autoExt.pHead ? *ppDb==ps->pDb : 0==ps->pDb ); -#endif - ps->pDb = *ppDb; - NativePointerHolder_set(env, ps->jDb, *ppDb, S3JniClassNames.sqlite3); + if( 0==ps->pDb ){ + ps->pDb = *ppDb; + NativePointerHolder_set(env, ps->jDb, *ppDb, &S3NphRefs.sqlite3); + }else{ + assert( ps->pDb == *ppDb /* set up via s3jni_run_java_auto_extensions() */); + } }else{ - S3JniDb_set_aside(ps); + S3JniDb_set_aside(env, ps); ps = 0; } OutputPointer_set_sqlite3(env, jOut, ps ? ps->jDb : 0); @@ -2692,20 +2746,16 @@ static int s3jni_open_post(JNIEnv * const env, S3JniDb * ps, JDECL(jint,1open)(JENV_CSELF, jstring strName, jobject jOut){ sqlite3 * pOut = 0; char *zName = 0; - jobject jDb = 0; S3JniDb * ps = 0; - S3JniEnvCache * jc = 0; - S3JniDb * const prevOpening = S3JniGlobal.autoExt.psOpening; - int rc = s3jni_open_pre(env, &jc, strName, &zName, &ps, &jDb); + S3JniEnv * jc = 0; + int rc; + rc = s3jni_open_pre(env, &jc, strName, &zName, &ps); if( 0==rc ){ rc = sqlite3_open(zName, &pOut); - //MARKER(("env=%p, *env=%p\n", env, *env)); - //MARKER(("open() ps@%p db@%p\n", ps, pOut)); - rc = s3jni_open_post(env, ps, &pOut, jOut, rc); + rc = s3jni_open_post(env, jc, ps, &pOut, jOut, rc); assert(rc==0 ? pOut!=0 : 1); sqlite3_free(zName); } - S3JniGlobal.autoExt.psOpening = prevOpening; return (jint)rc; } @@ -2713,14 +2763,12 @@ JDECL(jint,1open_1v2)(JENV_CSELF, jstring strName, jobject jOut, jint flags, jstring strVfs){ sqlite3 * pOut = 0; char *zName = 0; - jobject jDb = 0; S3JniDb * ps = 0; - S3JniEnvCache * jc = 0; + S3JniEnv * jc = 0; char *zVfs = 0; - S3JniDb * const prevOpening = S3JniGlobal.autoExt.psOpening; - int rc = s3jni_open_pre(env, &jc, strName, &zName, &ps, &jDb); + int rc = s3jni_open_pre(env, &jc, strName, &zName, &ps); if( 0==rc && strVfs ){ - zVfs = s3jni_jstring_to_utf8(jc, strVfs, 0); + zVfs = s3jni_jstring_to_utf8(env, strVfs, 0); if( !zVfs ){ rc = SQLITE_NOMEM; } @@ -2728,14 +2776,10 @@ JDECL(jint,1open_1v2)(JENV_CSELF, jstring strName, if( 0==rc ){ rc = sqlite3_open_v2(zName, &pOut, (int)flags, zVfs); } - //MARKER(("open_v2() ps@%p db@%p\n", ps, pOut)); - /*MARKER(("zName=%s, zVfs=%s, pOut=%p, flags=%d, nrc=%d\n", - zName, zVfs, pOut, (int)flags, nrc));*/ - rc = s3jni_open_post(env, ps, &pOut, jOut, rc); + rc = s3jni_open_post(env, jc, ps, &pOut, jOut, rc); assert(rc==0 ? pOut!=0 : 1); sqlite3_free(zName); sqlite3_free(zVfs); - S3JniGlobal.autoExt.psOpening = prevOpening; return (jint)rc; } @@ -2747,7 +2791,7 @@ static jint sqlite3_jni_prepare_v123(int prepVersion, JNIEnv * const env, jclass sqlite3_stmt * pStmt = 0; jobject jStmt = 0; const char * zTail = 0; - jbyte * const pBuf = JBA_TOC(baSql); + jbyte * const pBuf = s3jni_jbytearray_bytes(baSql); int rc = SQLITE_ERROR; assert(prepVersion==1 || prepVersion==2 || prepVersion==3); if( !pBuf ){ @@ -2774,18 +2818,19 @@ static jint sqlite3_jni_prepare_v123(int prepVersion, JNIEnv * const env, jclass assert(0 && "Invalid prepare() version"); } end: - JBA_RELEASE(baSql,pBuf); + s3jni_jbytearray_release(baSql,pBuf); if( 0==rc ){ if( 0!=outTail ){ - /* Noting that pBuf is deallocated now but its address is all we need. */ + /* Noting that pBuf is deallocated now but its address is all we need for + ** what follows... */ assert(zTail ? ((void*)zTail>=(void*)pBuf) : 1); assert(zTail ? (((int)((void*)zTail - (void*)pBuf)) >= 0) : 1); OutputPointer_set_Int32(env, outTail, (int)(zTail ? (zTail - (const char *)pBuf) : 0)); } if( pStmt ){ - NativePointerHolder_set(env, jStmt, pStmt, S3JniClassNames.sqlite3_stmt); + NativePointerHolder_set(env, jStmt, pStmt, &S3NphRefs.sqlite3_stmt); }else{ - /* Happens for comments and whitespace */ + /* Happens for comments and whitespace. */ UNREF_L(jStmt); jStmt = 0; } @@ -2817,12 +2862,201 @@ JDECL(jint,1prepare_1v3)(JNIEnv * const env, jclass self, jobject jDb, jbyteArra prepFlags, jOutStmt, outTail); } +/* +** Impl for C-to-Java of the callbacks for both sqlite3_update_hook() +** and sqlite3_preupdate_hook(). The differences are that for +** update_hook(): +** +** - pDb is NULL +** - iKey1 is the row ID +** - iKey2 is unused +*/ +static void s3jni_updatepre_hook_impl(void * pState, sqlite3 *pDb, int opId, + const char *zDb, const char *zTable, + sqlite3_int64 iKey1, sqlite3_int64 iKey2){ + S3JniDb * const ps = pState; + LocalJniGetEnv; + jstring jDbName; + jstring jTable; + S3JniHook * pHook; + const int isPre = 0!=pDb; + pHook = isPre ? +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + &ps->hooks.preUpdate +#else + 0 +#endif + : &ps->hooks.update; + + assert( pHook ); + jDbName = s3jni_utf8_to_jstring(env, zDb, -1); + jTable = jDbName ? s3jni_utf8_to_jstring(env, zTable, -1) : 0; + IFTHREW { + EXCEPTION_CLEAR; + s3jni_db_error(ps->pDb, SQLITE_NOMEM, 0); + }else{ + assert( pHook->jObj ); + assert( pHook->midCallback ); + assert( ps->jDb ); +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + if( isPre ) (*env)->CallVoidMethod(env, pHook->jObj, pHook->midCallback, + ps->jDb, (jint)opId, jDbName, jTable, + (jlong)iKey1, (jlong)iKey2); + else +#endif + (*env)->CallVoidMethod(env, pHook->jObj, pHook->midCallback, + (jint)opId, jDbName, jTable, (jlong)iKey1); + IFTHREW{ + EXCEPTION_WARN_CALLBACK_THREW("sqlite3_(pre)update_hook() callback"); + s3jni_db_exception(env, ps, 0, + "sqlite3_(pre)update_hook() callback threw"); + } + } + UNREF_L(jDbName); + UNREF_L(jTable); +} + +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK +static void s3jni_preupdate_hook_impl(void * pState, sqlite3 *pDb, int opId, + const char *zDb, const char *zTable, + sqlite3_int64 iKey1, sqlite3_int64 iKey2){ + return s3jni_updatepre_hook_impl(pState, pDb, opId, zDb, zTable, + iKey1, iKey2); +} +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ + +static void s3jni_update_hook_impl(void * pState, int opId, const char *zDb, + const char *zTable, sqlite3_int64 nRowid){ + return s3jni_updatepre_hook_impl(pState, NULL, opId, zDb, zTable, nRowid, 0); +} + +#ifndef SQLITE_ENABLE_PREUPDATE_HOOK +/* We need no-op impls for preupdate_{count,depth,blobwrite}() */ +JDECL(int,1preupdate_1blobwrite)(JENV_CSELF, jobject jDb){ return SQLITE_MISUSE; } +JDECL(int,1preupdate_1count)(JENV_CSELF, jobject jDb){ return SQLITE_MISUSE; } +JDECL(int,1preupdate_1depth)(JENV_CSELF, jobject jDb){ return SQLITE_MISUSE; } +#endif /* !SQLITE_ENABLE_PREUPDATE_HOOK */ + +/* +** JNI wrapper for both sqlite3_update_hook() and +** sqlite3_preupdate_hook() (if isPre is true). +*/ +static jobject s3jni_updatepre_hook(JNIEnv * env, int isPre, jobject jDb, jobject jHook){ + S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0); + jclass klazz; + jobject pOld = 0; + jmethodID xCallback; + S3JniHook * pHook = ps ? ( + isPre ? +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + &ps->hooks.preUpdate +#else + 0 +#endif + : &ps->hooks.update) : 0; + + if(!pHook){ + return 0; + } + pOld = pHook->jObj; + if( pOld && jHook && (*env)->IsSameObject(env, pOld, jHook) ){ + return pOld; + } + if( !jHook ){ + if( pOld ){ + jobject tmp = REF_L(pOld); + UNREF_G(pOld); + pOld = tmp; + } + memset(pHook, 0, sizeof(S3JniHook)); +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + if( isPre ) sqlite3_preupdate_hook(ps->pDb, 0, 0); + else +#endif + sqlite3_update_hook(ps->pDb, 0, 0); + return pOld; + } + klazz = (*env)->GetObjectClass(env, jHook); + xCallback = isPre + ? (*env)->GetMethodID(env, klazz, "xPreUpdate", + "(Lorg/sqlite/jni/sqlite3;" + "I" + "Ljava/lang/String;" + "Ljava/lang/String;" + "JJ)V") + : (*env)->GetMethodID(env, klazz, "xUpdateHook", + "(ILjava/lang/String;Ljava/lang/String;J)V"); + UNREF_L(klazz); + IFTHREW { + EXCEPTION_CLEAR; + s3jni_db_error(ps->pDb, SQLITE_ERROR, + "Cannot not find matching callback on " + "(pre)update hook object."); + }else{ + pHook->midCallback = xCallback; + pHook->jObj = REF_G(jHook); +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + if( isPre ) sqlite3_preupdate_hook(ps->pDb, s3jni_preupdate_hook_impl, ps); + else +#endif + sqlite3_update_hook(ps->pDb, s3jni_update_hook_impl, ps); + if(pOld){ + jobject tmp = REF_L(pOld); + UNREF_G(pOld); + pOld = tmp; + } + } + return pOld; +} + + +JDECL(jobject,1preupdate_1hook)(JENV_CSELF, jobject jDb, jobject jHook){ +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + return s3jni_updatepre_hook(env, 1, jDb, jHook); +#else + return NULL; +#endif /* SQLITE_ENABLE_PREUPDATE_HOOK */ +} + +/* Impl for sqlite3_preupdate_{new,old}(). */ +static int s3jni_preupdate_newold(JNIEnv * const env, int isNew, jobject jDb, + jint iCol, jobject jOut){ +#ifdef SQLITE_ENABLE_PREUPDATE_HOOK + sqlite3_value * pOut = 0; + sqlite3 * const pDb = PtrGet_sqlite3(jDb); + int rc; + int (*fOrig)(sqlite3*,int,sqlite3_value**) = + isNew ? sqlite3_preupdate_new : sqlite3_preupdate_old; + rc = fOrig(pDb, (int)iCol, &pOut); + if( 0==rc ){ + jobject pWrap = new_sqlite3_value_wrapper(env, pOut); + if( pWrap ){ + OutputPointer_set_sqlite3_value(env, jOut, pWrap); + UNREF_L(pWrap); + }else{ + rc = SQLITE_NOMEM; + } + } + return rc; +#else + return SQLITE_MISUSE; +#endif +} +JDECL(jint,1preupdate_1new)(JENV_CSELF, jobject jDb, jint iCol, jobject jOut){ + return s3jni_preupdate_newold(env, 1, jDb, iCol, jOut); +} +JDECL(jint,1preupdate_1old)(JENV_CSELF, jobject jDb, jint iCol, jobject jOut){ + return s3jni_preupdate_newold(env, 0, jDb, iCol, jOut); +} + + +/* Central C-to-Java sqlite3_progress_handler() proxy. */ static int s3jni_progress_handler_impl(void *pP){ S3JniDb * const ps = (S3JniDb *)pP; - JNIEnv * const env = ps->env; - int rc = (int)(*env)->CallIntMethod(env, ps->progress.jObj, - ps->progress.midCallback); + LocalJniGetEnv; + int rc = (int)(*env)->CallIntMethod(env, ps->hooks.progress.jObj, + ps->hooks.progress.midCallback); IFTHREW{ rc = s3jni_db_exception(env, ps, rc, "sqlite3_progress_handler() callback threw"); @@ -2831,13 +3065,13 @@ static int s3jni_progress_handler_impl(void *pP){ } JDECL(void,1progress_1handler)(JENV_CSELF,jobject jDb, jint n, jobject jProgress){ - S3JniDb * ps = S3JniDb_for_db(env, jDb, 0, 0); + S3JniDb * ps = S3JniDb_for_db(env, jDb, 0); jclass klazz; jmethodID xCallback; if( n<1 || !jProgress ){ if(ps){ - UNREF_G(ps->progress.jObj); - memset(&ps->progress, 0, sizeof(ps->progress)); + UNREF_G(ps->hooks.progress.jObj); + memset(&ps->hooks.progress, 0, sizeof(ps->hooks.progress)); } sqlite3_progress_handler(ps->pDb, 0, 0, 0); return; @@ -2848,15 +3082,16 @@ JDECL(void,1progress_1handler)(JENV_CSELF,jobject jDb, jint n, jobject jProgress } klazz = (*env)->GetObjectClass(env, jProgress); xCallback = (*env)->GetMethodID(env, klazz, "xCallback", "()I"); + UNREF_L(klazz); IFTHREW { EXCEPTION_CLEAR; s3jni_db_error(ps->pDb, SQLITE_ERROR, "Cannot not find matching xCallback() on " "ProgressHandler object."); }else{ - UNREF_G(ps->progress.jObj); - ps->progress.midCallback = xCallback; - ps->progress.jObj = REF_G(jProgress); + UNREF_G(ps->hooks.progress.jObj); + ps->hooks.progress.midCallback = xCallback; + ps->hooks.progress.jObj = REF_G(jProgress); sqlite3_progress_handler(ps->pDb, (int)n, s3jni_progress_handler_impl, ps); } } @@ -2871,15 +3106,20 @@ JDECL(jint,1reset)(JENV_CSELF, jobject jpStmt){ return rc; } -#if S3JNI_ENABLE_AUTOEXT -JDECL(void,1reset_1auto_1extension)(JENV_CSELF){ - if(!S3JniGlobal.autoExt.isRunning){ - while( S3JniGlobal.autoExt.pHead ){ - S3JniAutoExtension_free(env, S3JniGlobal.autoExt.pHead); - } +/* Clears all entries from S3JniGlobal.autoExt. */ +static void s3jni_reset_auto_extension(JNIEnv *env){ + int i; + MUTEX_EXT_ENTER; + for( i = 0; i < SJG.autoExt.nExt; ++i ){ + S3JniAutoExtension_clear( env, &SJG.autoExt.pExt[i] ); } + SJG.autoExt.nExt = 0; + MUTEX_EXT_LEAVE; +} + +JDECL(void,1reset_1auto_1extension)(JENV_CSELF){ + s3jni_reset_auto_extension(env); } -#endif /* S3JNI_ENABLE_AUTOEXT */ /* sqlite3_result_text/blob() and friends. */ static void result_blob_text(int asBlob, int as64, @@ -2887,7 +3127,7 @@ static void result_blob_text(int asBlob, int as64, JNIEnv * const env, sqlite3_context *pCx, jbyteArray jBa, jlong nMax){ if(jBa){ - jbyte * const pBuf = JBA_TOC(jBa); + jbyte * const pBuf = s3jni_jbytearray_bytes(jBa); jsize nBa = (*env)->GetArrayLength(env, jBa); if( nMax>=0 && nBa>(jsize)nMax ){ nBa = (jsize)nMax; @@ -2952,7 +3192,7 @@ static void result_blob_text(int asBlob, int as64, break; } } - JBA_RELEASE(jBa, pBuf); + s3jni_jbytearray_release(jBa, pBuf); } }else{ sqlite3_result_null(pCx); @@ -2975,7 +3215,7 @@ JDECL(void,1result_1error)(JENV_CSELF, jobject jpCx, jbyteArray baMsg, int eTextRep){ const char * zUnspecified = "Unspecified error."; jsize const baLen = (*env)->GetArrayLength(env, baMsg); - jbyte * const pjBuf = baMsg ? JBA_TOC(baMsg) : NULL; + jbyte * const pjBuf = baMsg ? s3jni_jbytearray_bytes(baMsg) : NULL; switch(pjBuf ? eTextRep : SQLITE_UTF8){ case SQLITE_UTF8: { const char *zMsg = pjBuf ? (const char *)pjBuf : zUnspecified; @@ -2994,7 +3234,7 @@ JDECL(void,1result_1error)(JENV_CSELF, jobject jpCx, jbyteArray baMsg, "to sqlite3_result_error().", -1); break; } - JBA_RELEASE(baMsg,pjBuf); + s3jni_jbytearray_release(baMsg,pjBuf); } JDECL(void,1result_1error_1code)(JENV_CSELF, jobject jpCx, jint v){ @@ -3021,8 +3261,8 @@ JDECL(void,1result_1java_1object)(JENV_CSELF, jobject jpCx, jobject v){ if(v){ ResultJavaVal * const rjv = ResultJavaVal_alloc(env, v); if(rjv){ - sqlite3_result_pointer(PtrGet_sqlite3_context(jpCx), rjv, RESULT_JAVA_VAL_STRING, - ResultJavaVal_finalizer); + sqlite3_result_pointer(PtrGet_sqlite3_context(jpCx), rjv, + ResultJavaValuePtrStr, ResultJavaVal_finalizer); }else{ sqlite3_result_error_nomem(PtrGet_sqlite3_context(jpCx)); } @@ -3036,16 +3276,19 @@ JDECL(void,1result_1null)(JENV_CSELF, jobject jpCx){ } JDECL(void,1result_1text)(JENV_CSELF, jobject jpCx, jbyteArray jBa, jint nMax){ - return result_blob_text(0, 0, SQLITE_UTF8, env, PtrGet_sqlite3_context(jpCx), jBa, nMax); + return result_blob_text(0, 0, SQLITE_UTF8, env, + PtrGet_sqlite3_context(jpCx), jBa, nMax); } JDECL(void,1result_1text64)(JENV_CSELF, jobject jpCx, jbyteArray jBa, jlong nMax, jint eTextRep){ - return result_blob_text(0, 1, eTextRep, env, PtrGet_sqlite3_context(jpCx), jBa, nMax); + return result_blob_text(0, 1, eTextRep, env, + PtrGet_sqlite3_context(jpCx), jBa, nMax); } JDECL(void,1result_1value)(JENV_CSELF, jobject jpCx, jobject jpSVal){ - sqlite3_result_value(PtrGet_sqlite3_context(jpCx), PtrGet_sqlite3_value(jpSVal)); + sqlite3_result_value(PtrGet_sqlite3_context(jpCx), + PtrGet_sqlite3_value(jpSVal)); } JDECL(void,1result_1zeroblob)(JENV_CSELF, jobject jpCx, jint v){ @@ -3053,10 +3296,11 @@ JDECL(void,1result_1zeroblob)(JENV_CSELF, jobject jpCx, jint v){ } JDECL(jint,1result_1zeroblob64)(JENV_CSELF, jobject jpCx, jlong v){ - return (jint)sqlite3_result_zeroblob64(PtrGet_sqlite3_context(jpCx), (sqlite3_int64)v); + return (jint)sqlite3_result_zeroblob64(PtrGet_sqlite3_context(jpCx), + (sqlite3_int64)v); } -JDECL(jobject,1rollback_1hook)(JENV_CSELF,jobject jDb, jobject jHook){ +JDECL(jobject,1rollback_1hook)(JENV_CSELF, jobject jDb, jobject jHook){ return s3jni_commit_rollback_hook(0, env, jDb, jHook); } @@ -3064,20 +3308,19 @@ JDECL(jobject,1rollback_1hook)(JENV_CSELF,jobject jDb, jobject jHook){ static int s3jni_xAuth(void* pState, int op,const char*z0, const char*z1, const char*z2,const char*z3){ S3JniDb * const ps = pState; - JNIEnv * const env = ps->env; - jstring const s0 = z0 ? (*env)->NewStringUTF(env, z0) : 0; - jstring const s1 = z1 ? (*env)->NewStringUTF(env, z1) : 0; - jstring const s2 = z2 ? (*env)->NewStringUTF(env, z2) : 0; - jstring const s3 = z3 ? (*env)->NewStringUTF(env, z3) : 0; - S3JniHook const * const pHook = &ps->authHook; + LocalJniGetEnv; + S3JniHook const * const pHook = &ps->hooks.auth; + jstring const s0 = z0 ? s3jni_utf8_to_jstring(env, z0, -1) : 0; + jstring const s1 = z1 ? s3jni_utf8_to_jstring(env, z1, -1) : 0; + jstring const s2 = z2 ? s3jni_utf8_to_jstring(env, z2, -1) : 0; + jstring const s3 = z3 ? s3jni_utf8_to_jstring(env, z3, -1) : 0; int rc; assert( pHook->jObj ); rc = (*env)->CallIntMethod(env, pHook->jObj, pHook->midCallback, (jint)op, s0, s1, s3, s3); IFTHREW{ - EXCEPTION_WARN_CALLBACK_THREW("sqlite3_set_authorizer() callback"); - EXCEPTION_CLEAR; + rc = s3jni_db_exception(env, ps, rc, "sqlite3_set_authorizer() callback"); } UNREF_L(s0); UNREF_L(s1); @@ -3087,8 +3330,8 @@ static int s3jni_xAuth(void* pState, int op,const char*z0, const char*z1, } JDECL(jint,1set_1authorizer)(JENV_CSELF,jobject jDb, jobject jHook){ - S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0); - S3JniHook * const pHook = ps ? &ps->authHook : 0; + S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0); + S3JniHook * const pHook = ps ? &ps->hooks.auth : 0; if( !ps ) return SQLITE_MISUSE; else if( !jHook ){ @@ -3096,6 +3339,7 @@ JDECL(jint,1set_1authorizer)(JENV_CSELF,jobject jDb, jobject jHook){ return (jint)sqlite3_set_authorizer( ps->pDb, 0, 0 ); }else{ int rc = 0; + jclass klazz; if( pHook->jObj ){ if( (*env)->IsSameObject(env, pHook->jObj, jHook) ){ /* Same object - this is a no-op. */ @@ -3104,8 +3348,8 @@ JDECL(jint,1set_1authorizer)(JENV_CSELF,jobject jDb, jobject jHook){ S3JniHook_unref(env, pHook, 0); } pHook->jObj = REF_G(jHook); - pHook->klazz = REF_G((*env)->GetObjectClass(env, jHook)); - pHook->midCallback = (*env)->GetMethodID(env, pHook->klazz, + klazz = (*env)->GetObjectClass(env, jHook); + pHook->midCallback = (*env)->GetMethodID(env, klazz, "xAuth", "(I" "Ljava/lang/String;" @@ -3113,6 +3357,7 @@ JDECL(jint,1set_1authorizer)(JENV_CSELF,jobject jDb, jobject jHook){ "Ljava/lang/String;" "Ljava/lang/String;" ")I"); + UNREF_L(klazz); IFTHREW { S3JniHook_unref(env, pHook, 0); return s3jni_db_error(ps->pDb, SQLITE_ERROR, @@ -3130,7 +3375,6 @@ JDECL(void,1set_1last_1insert_1rowid)(JENV_CSELF, jobject jpDb, jlong rowId){ (sqlite3_int64)rowId); } -FIXME_THREADING(nphCache) JDECL(jint,1status)(JENV_CSELF, jint op, jobject jOutCurrent, jobject jOutHigh, jboolean reset ){ int iCur = 0, iHigh = 0; @@ -3142,7 +3386,6 @@ JDECL(jint,1status)(JENV_CSELF, jint op, jobject jOutCurrent, jobject jOutHigh, return (jint)rc; } -FIXME_THREADING(nphCache) JDECL(jint,1status64)(JENV_CSELF, jint op, jobject jOutCurrent, jobject jOutHigh, jboolean reset ){ sqlite3_int64 iCur = 0, iHigh = 0; @@ -3157,18 +3400,18 @@ JDECL(jint,1status64)(JENV_CSELF, jint op, jobject jOutCurrent, jobject jOutHigh static int s3jni_strlike_glob(int isLike, JNIEnv *const env, jbyteArray baG, jbyteArray baT, jint escLike){ int rc = 0; - jbyte * const pG = JBA_TOC(baG); - jbyte * const pT = pG ? JBA_TOC(baT) : 0; - OOM_CHECK(pT); + jbyte * const pG = s3jni_jbytearray_bytes(baG); + jbyte * const pT = pG ? s3jni_jbytearray_bytes(baT) : 0; + s3jni_oom_check(pT); /* Note that we're relying on the byte arrays having been NUL-terminated on the Java side. */ rc = isLike ? sqlite3_strlike((const char *)pG, (const char *)pT, (unsigned int)escLike) : sqlite3_strglob((const char *)pG, (const char *)pT); - JBA_RELEASE(baG, pG); - JBA_RELEASE(baT, pT); + s3jni_jbytearray_release(baG, pG); + s3jni_jbytearray_release(baT, pT); return rc; } @@ -3181,7 +3424,12 @@ JDECL(jint,1strlike)(JENV_CSELF, jbyteArray baG, jbyteArray baT, jint escChar){ } JDECL(jint,1shutdown)(JENV_CSELF){ - S3JniGlobal_S3JniEnvCache_clear(); + s3jni_reset_auto_extension(env); + MUTEX_ENV_ENTER; + while( SJG.envCache.aHead ){ + S3JniGlobal_env_uncache( SJG.envCache.aHead->env ); + } + MUTEX_ENV_LEAVE; /* Do not clear S3JniGlobal.jvm: it's legal to call sqlite3_initialize() again to restart the lib. */ return sqlite3_shutdown(); @@ -3192,10 +3440,9 @@ JDECL(jstring,1sql)(JENV_CSELF, jobject jpStmt){ jstring rv = 0; if( pStmt ){ const char * zSql = 0; - S3JniEnvCache * const jc = S3JniGlobal_env_cache(env); zSql = sqlite3_sql(pStmt); - rv = s3jni_utf8_to_jstring(jc, zSql, -1); - OOM_CHECK(rv); + rv = s3jni_utf8_to_jstring(env, zSql, -1); + s3jni_oom_check(rv); } return rv; } @@ -3211,22 +3458,21 @@ JDECL(jint,1step)(JENV_CSELF,jobject jStmt){ static int s3jni_trace_impl(unsigned traceflag, void *pC, void *pP, void *pX){ S3JniDb * const ps = (S3JniDb *)pC; - JNIEnv * const env = ps->env; + LocalJniGetEnv; jobject jX = NULL /* the tracer's X arg */; jobject jP = NULL /* the tracer's P arg */; jobject jPUnref = NULL /* potentially a local ref to jP */; - S3JniEnvCache * const jc = S3JniGlobal_env_cache(env); int rc; int createStmt = 0; switch(traceflag){ case SQLITE_TRACE_STMT: - jX = s3jni_utf8_to_jstring(jc, (const char *)pX, -1); + jX = s3jni_utf8_to_jstring(env, (const char *)pX, -1); if(!jX) return SQLITE_NOMEM; /*MARKER(("TRACE_STMT@%p SQL=%p / %s\n", pP, jX, (const char *)pX));*/ createStmt = 1; break; case SQLITE_TRACE_PROFILE: - jX = (*env)->NewObject(env, jc->g.cLong, jc->g.ctorLong1, + jX = (*env)->NewObject(env, SJG.g.cLong, SJG.g.ctorLong1, (jlong)*((sqlite3_int64*)pX)); // hmm. ^^^ (*pX) really is zero. // MARKER(("profile time = %llu\n", *((sqlite3_int64*)pX))); @@ -3251,13 +3497,13 @@ static int s3jni_trace_impl(unsigned traceflag, void *pC, void *pP, void *pX){ } } assert(jP); - rc = (int)(*env)->CallIntMethod(env, ps->trace.jObj, - ps->trace.midCallback, + rc = (int)(*env)->CallIntMethod(env, ps->hooks.trace.jObj, + ps->hooks.trace.midCallback, (jint)traceflag, jP, jX); IFTHREW{ EXCEPTION_WARN_CALLBACK_THREW("sqlite3_trace_v2() callback"); - EXCEPTION_CLEAR; - rc = SQLITE_ERROR; + rc = s3jni_db_exception(env, ps, SQLITE_ERROR, + "sqlite3_trace_v2() callback threw."); } UNREF_L(jPUnref); UNREF_L(jX); @@ -3265,100 +3511,31 @@ static int s3jni_trace_impl(unsigned traceflag, void *pC, void *pP, void *pX){ } JDECL(jint,1trace_1v2)(JENV_CSELF,jobject jDb, jint traceMask, jobject jTracer){ - S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0); + S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0); jclass klazz; + if( !traceMask || !jTracer ){ if(ps){ - UNREF_G(ps->trace.jObj); - memset(&ps->trace, 0, sizeof(ps->trace)); + S3JniHook_unref(env, &ps->hooks.trace, 0); } return (jint)sqlite3_trace_v2(ps->pDb, 0, 0, 0); } if(!ps) return SQLITE_NOMEM; klazz = (*env)->GetObjectClass(env, jTracer); - ps->trace.midCallback = (*env)->GetMethodID(env, klazz, "xCallback", + ps->hooks.trace.midCallback = (*env)->GetMethodID(env, klazz, "xCallback", "(ILjava/lang/Object;Ljava/lang/Object;)I"); + UNREF_L(klazz); IFTHREW { EXCEPTION_CLEAR; return s3jni_db_error(ps->pDb, SQLITE_ERROR, "Cannot not find matching xCallback() on Tracer object."); } - ps->trace.jObj = REF_G(jTracer); + ps->hooks.trace.jObj = REF_G(jTracer); return sqlite3_trace_v2(ps->pDb, (unsigned)traceMask, s3jni_trace_impl, ps); } -static void s3jni_update_hook_impl(void * pState, int opId, const char *zDb, - const char *zTable, sqlite3_int64 nRowid){ - S3JniDb * const ps = pState; - JNIEnv * const env = ps->env; - /* ACHTUNG: this will break if zDb or zTable contain chars which are - different in MUTF-8 than UTF-8. That seems like a low risk, - but it's possible. */ - jstring jDbName; - jstring jTable; - jDbName = (*env)->NewStringUTF(env, zDb); - jTable = jDbName ? (*env)->NewStringUTF(env, zTable) : 0; - IFTHREW { - s3jni_db_error(ps->pDb, SQLITE_NOMEM, 0); - }else{ - (*env)->CallVoidMethod(env, ps->updateHook.jObj, - ps->updateHook.midCallback, - (jint)opId, jDbName, jTable, (jlong)nRowid); - IFTHREW{ - EXCEPTION_WARN_CALLBACK_THREW("update hook"); - EXCEPTION_CLEAR; - s3jni_db_error(ps->pDb, SQLITE_ERROR, "update hook callback threw."); - } - } - UNREF_L(jDbName); - UNREF_L(jTable); -} - - JDECL(jobject,1update_1hook)(JENV_CSELF, jobject jDb, jobject jHook){ - S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0); - jclass klazz; - jobject pOld = 0; - jmethodID xCallback; - S3JniHook * const pHook = &ps->updateHook; - if(!ps){ - s3jni_db_error(ps->pDb, SQLITE_MISUSE, 0); - return 0; - } - pOld = pHook->jObj; - if(pOld && jHook && - (*env)->IsSameObject(env, pOld, jHook)){ - return pOld; - } - if( !jHook ){ - if(pOld){ - jobject tmp = REF_L(pOld); - UNREF_G(pOld); - pOld = tmp; - } - memset(pHook, 0, sizeof(S3JniHook)); - sqlite3_update_hook(ps->pDb, 0, 0); - return pOld; - } - klazz = (*env)->GetObjectClass(env, jHook); - xCallback = (*env)->GetMethodID(env, klazz, "xUpdateHook", - "(ILjava/lang/String;Ljava/lang/String;J)V"); - IFTHREW { - EXCEPTION_CLEAR; - s3jni_db_error(ps->pDb, SQLITE_ERROR, - "Cannot not find matching callback on " - "update hook object."); - }else{ - pHook->midCallback = xCallback; - pHook->jObj = REF_G(jHook); - sqlite3_update_hook(ps->pDb, s3jni_update_hook_impl, ps); - if(pOld){ - jobject tmp = REF_L(pOld); - UNREF_G(pOld); - pOld = tmp; - } - } - return pOld; + return s3jni_updatepre_hook(env, 0, jDb, jHook); } @@ -3399,37 +3576,32 @@ JDECL(jlong,1value_1int64)(JENV_CSELF, jobject jpSVal){ } JDECL(jobject,1value_1java_1object)(JENV_CSELF, jobject jpSVal){ - ResultJavaVal * const rv = sqlite3_value_pointer(PtrGet_sqlite3_value(jpSVal), RESULT_JAVA_VAL_STRING); + ResultJavaVal * const rv = sqlite3_value_pointer(PtrGet_sqlite3_value(jpSVal), + ResultJavaValuePtrStr); return rv ? rv->jObj : NULL; } -JDECL(jstring,1value_1text)(JENV_CSELF, jobject jpSVal){ - sqlite3_value * const sv = PtrGet_sqlite3_value(jpSVal); - int const n = sqlite3_value_bytes16(sv); - const void * const p = sqlite3_value_text16(sv); - return s3jni_text16_to_jstring(env, p, n); -} - JDECL(jbyteArray,1value_1text_1utf8)(JENV_CSELF, jobject jpSVal){ sqlite3_value * const sv = PtrGet_sqlite3_value(jpSVal); int const n = sqlite3_value_bytes(sv); const unsigned char * const p = sqlite3_value_text(sv); - return s3jni_new_jbyteArray(env, p, n); + return p ? s3jni_new_jbyteArray(env, p, n) : 0; } static jbyteArray value_text16(int mode, JNIEnv * const env, jobject jpSVal){ - int const nLen = sqlite3_value_bytes16(PtrGet_sqlite3_value(jpSVal)); + sqlite3_value * const sv = PtrGet_sqlite3_value(jpSVal); + int const nLen = sqlite3_value_bytes16(sv); jbyteArray jba; const jbyte * pBytes; switch(mode){ case SQLITE_UTF16: - pBytes = sqlite3_value_text16(PtrGet_sqlite3_value(jpSVal)); + pBytes = sqlite3_value_text16(sv); break; case SQLITE_UTF16LE: - pBytes = sqlite3_value_text16le(PtrGet_sqlite3_value(jpSVal)); + pBytes = sqlite3_value_text16le(sv); break; case SQLITE_UTF16BE: - pBytes = sqlite3_value_text16be(PtrGet_sqlite3_value(jpSVal)); + pBytes = sqlite3_value_text16be(sv); break; default: assert(!"not possible"); @@ -3469,28 +3641,41 @@ JDECL(void,1do_1something_1for_1developer)(JENV_CSELF){ puts("sizeofs:"); #define SO(T) printf("\tsizeof(" #T ") = %u\n", (unsigned)sizeof(T)) SO(void*); - SO(S3JniEnvCache); + SO(jmethodID); + SO(jfieldID); + SO(S3JniEnv); SO(S3JniHook); SO(S3JniDb); - SO(S3JniClassNames); + SO(S3NphRefs); printf("\t(^^^ %u NativePointerHolder subclasses)\n", - (unsigned)(sizeof(S3JniClassNames) / sizeof(const char *))); + (unsigned)NphCache_SIZE); SO(S3JniGlobal); SO(S3JniAutoExtension); SO(S3JniUdf); printf("Cache info:\n"); - printf("\tNativePointerHolder cache: %u misses, %u hits\n", - S3JniGlobal.metrics.nphCacheMisses, - S3JniGlobal.metrics.nphCacheHits); - printf("\tJNIEnv cache %u misses, %u hits\n", - S3JniGlobal.metrics.envCacheMisses, - S3JniGlobal.metrics.envCacheHits); + printf("\tJNIEnv cache: %u allocs, %u misses, %u hits\n", + SJG.metrics.envCacheAllocs, + SJG.metrics.envCacheMisses, + SJG.metrics.envCacheHits); + printf("Mutex entry:" + "\n\tenv %u" + "\n\tnph inits %u" + "\n\tperDb %u" + "\n\tautoExt %u list accesses" + "\n\tmetrics %u\n", + SJG.metrics.nMutexEnv, SJG.metrics.nMutexEnv2, + SJG.metrics.nMutexPerDb, SJG.metrics.nMutexAutoExt, + SJG.metrics.nMetrics); + printf("S3JniDb: %u alloced (*%u = %u bytes), %u recycled\n", + SJG.metrics.nPdbAlloc, (unsigned) sizeof(S3JniDb), + (unsigned)(SJG.metrics.nPdbAlloc * sizeof(S3JniDb)), + SJG.metrics.nPdbRecycled); puts("Java-side UDF calls:"); -#define UDF(T) printf("\t%-8s = %u\n", "x" #T, S3JniGlobal.metrics.udf.n##T) +#define UDF(T) printf("\t%-8s = %u\n", "x" #T, SJG.metrics.udf.n##T) UDF(Func); UDF(Step); UDF(Final); UDF(Value); UDF(Inverse); #undef UDF printf("xDestroy calls across all callback types: %u\n", - S3JniGlobal.metrics.nDestroy); + SJG.metrics.nDestroy); #undef SO } @@ -3517,19 +3702,17 @@ JDECL(void,1do_1something_1for_1developer)(JENV_CSELF){ JNIEXPORT ReturnType JNICALL \ JFuncNameFtsTok(Suffix) -#define PtrGet_fts5_api(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.fts5_api) -#define PtrGet_fts5_tokenizer(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.fts5_tokenizer) -#define PtrGet_Fts5Context(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.Fts5Context) -#define PtrGet_Fts5Tokenizer(OBJ) NativePointerHolder_get(env,OBJ,S3JniClassNames.Fts5Tokenizer) +#define PtrGet_fts5_api(OBJ) NativePointerHolder_get(env,OBJ,&S3NphRefs.fts5_api) +#define PtrGet_fts5_tokenizer(OBJ) NativePointerHolder_get(env,OBJ,&S3NphRefs.fts5_tokenizer) +#define PtrGet_Fts5Context(OBJ) NativePointerHolder_get(env,OBJ,&S3NphRefs.Fts5Context) +#define PtrGet_Fts5Tokenizer(OBJ) NativePointerHolder_get(env,OBJ,&S3NphRefs.Fts5Tokenizer) #define Fts5ExtDecl Fts5ExtensionApi const * const fext = s3jni_ftsext() /** State for binding Java-side FTS5 auxiliary functions. */ typedef struct { - JNIEnv * env; /* env registered from */; jobject jObj /* functor instance */; - jclass klazz /* jObj's class */; jobject jUserData /* 2nd arg to JNI binding of xCreateFunction(), ostensibly the 3rd arg to the lib-level xCreateFunction(), except @@ -3540,12 +3723,11 @@ typedef struct { } Fts5JniAux; static void Fts5JniAux_free(Fts5JniAux * const s){ - JNIEnv * const env = s->env; + LocalJniGetEnv; if(env){ /*MARKER(("FTS5 aux function cleanup: %s\n", s->zFuncName));*/ - s3jni_call_xDestroy(env, s->jObj, s->klazz); + s3jni_call_xDestroy(env, s->jObj); UNREF_G(s->jObj); - UNREF_G(s->klazz); UNREF_G(s->jUserData); } sqlite3_free(s->zFuncName); @@ -3559,16 +3741,16 @@ static void Fts5JniAux_xDestroy(void *p){ static Fts5JniAux * Fts5JniAux_alloc(JNIEnv * const env, jobject jObj){ Fts5JniAux * s = sqlite3_malloc(sizeof(Fts5JniAux)); if(s){ - const char * zSig = - "(Lorg/sqlite/jni/Fts5ExtensionApi;" - "Lorg/sqlite/jni/Fts5Context;" - "Lorg/sqlite/jni/sqlite3_context;" - "[Lorg/sqlite/jni/sqlite3_value;)V"; + jclass klazz; memset(s, 0, sizeof(Fts5JniAux)); - s->env = env; s->jObj = REF_G(jObj); - s->klazz = REF_G((*env)->GetObjectClass(env, jObj)); - s->jmid = (*env)->GetMethodID(env, s->klazz, "xFunction", zSig); + klazz = (*env)->GetObjectClass(env, jObj); + s->jmid = (*env)->GetMethodID(env, klazz, "xFunction", + "(Lorg/sqlite/jni/Fts5ExtensionApi;" + "Lorg/sqlite/jni/Fts5Context;" + "Lorg/sqlite/jni/sqlite3_context;" + "[Lorg/sqlite/jni/sqlite3_value;)V"); + UNREF_L(klazz); IFTHREW{ EXCEPTION_REPORT; EXCEPTION_CLEAR; @@ -3584,10 +3766,10 @@ static inline Fts5ExtensionApi const * s3jni_ftsext(void){ } static inline jobject new_Fts5Context_wrapper(JNIEnv * const env, Fts5Context *sv){ - return new_NativePointerHolder_object(env, S3JniClassNames.Fts5Context, sv); + return new_NativePointerHolder_object(env, &S3NphRefs.Fts5Context, sv); } static inline jobject new_fts5_api_wrapper(JNIEnv * const env, fts5_api *sv){ - return new_NativePointerHolder_object(env, S3JniClassNames.fts5_api, sv); + return new_NativePointerHolder_object(env, &S3NphRefs.fts5_api, sv); } /** @@ -3595,13 +3777,20 @@ static inline jobject new_fts5_api_wrapper(JNIEnv * const env, fts5_api *sv){ instance, or NULL on OOM. */ static jobject s3jni_getFts5ExensionApi(JNIEnv * const env){ - S3JniEnvCache * const row = S3JniGlobal_env_cache(env); - if( !row->jFtsExt ){ - row->jFtsExt = new_NativePointerHolder_object(env, S3JniClassNames.Fts5ExtensionApi, - s3jni_ftsext()); - if(row->jFtsExt) row->jFtsExt = REF_G(row->jFtsExt); + if( !SJG.fts5.jFtsExt ){ + jobject pNPH = new_NativePointerHolder_object( + env, &S3NphRefs.Fts5ExtensionApi, s3jni_ftsext() + ); + MUTEX_ENV_ENTER; + if( pNPH ){ + if( !SJG.fts5.jFtsExt ){ + SJG.fts5.jFtsExt = REF_G(pNPH); + } + UNREF_L(pNPH); + } + MUTEX_ENV_LEAVE; } - return row->jFtsExt; + return SJG.fts5.jFtsExt; } /* @@ -3621,7 +3810,7 @@ static fts5_api *s3jni_fts5_api_from_db(sqlite3 *db){ } JDECLFtsApi(jobject,getInstanceForDb)(JENV_CSELF,jobject jDb){ - S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0, 0); + S3JniDb * const ps = S3JniDb_for_db(env, jDb, 0); jobject rv = 0; if(!ps) return 0; else if(ps->jFtsApi){ @@ -3662,8 +3851,7 @@ JDECLFtsXA(jint,xColumnText)(JENV_OSELF,jobject jCtx, jint iCol, int rc = fext->xColumnText(PtrGet_Fts5Context(jCtx), (int)iCol, &pz, &pn); if( 0==rc ){ - S3JniEnvCache * const jc = S3JniGlobal_env_cache(env); - jstring jstr = pz ? s3jni_utf8_to_jstring(jc, pz, pn) : 0; + jstring jstr = pz ? s3jni_utf8_to_jstring(env, pz, pn) : 0; if( pz ){ if( jstr ){ OutputPointer_set_String(env, jOut, jstr); @@ -3684,9 +3872,9 @@ JDECLFtsXA(jint,xColumnTotalSize)(JENV_OSELF,jobject jCtx, jint iCol, jobject jO return (jint)rc; } -/** - Proxy for fts5_extension_function instances plugged in via - fts5_api::xCreateFunction(). +/* +** Proxy for fts5_extension_function instances plugged in via +** fts5_api::xCreateFunction(). */ static void s3jni_fts5_extension_function(Fts5ExtensionApi const *pApi, Fts5Context *pFts, @@ -3694,14 +3882,14 @@ static void s3jni_fts5_extension_function(Fts5ExtensionApi const *pApi, int argc, sqlite3_value **argv){ Fts5JniAux * const pAux = pApi->xUserData(pFts); - JNIEnv *env; jobject jpCx = 0; jobjectArray jArgv = 0; jobject jpFts = 0; jobject jFXA; int rc; + LocalJniGetEnv; + assert(pAux); - env = pAux->env; jFXA = s3jni_getFts5ExensionApi(env); if( !jFXA ) goto error_oom; jpFts = new_Fts5Context_wrapper(env, pFts); @@ -3711,8 +3899,7 @@ static void s3jni_fts5_extension_function(Fts5ExtensionApi const *pApi, (*env)->CallVoidMethod(env, pAux->jObj, pAux->jmid, jFXA, jpFts, jpCx, jArgv); IFTHREW{ - EXCEPTION_CLEAR; - udf_report_exception(pCx, pAux->zFuncName, "xFunction"); + udf_report_exception(env, 1, pCx, pAux->zFuncName, "xFunction"); } UNREF_L(jpFts); UNREF_L(jpCx); @@ -3732,8 +3919,9 @@ JDECLFtsApi(jint,xCreateFunction)(JENV_OSELF, jstring jName, int rc; char const * zName; Fts5JniAux * pAux; + assert(pApi); - zName = JSTR_TOC(jName); + zName = s3jni_jstring_to_mutf8(jName); if(!zName) return SQLITE_NOMEM; pAux = Fts5JniAux_alloc(env, jFunc); if( pAux ){ @@ -3748,23 +3936,26 @@ JDECLFtsApi(jint,xCreateFunction)(JENV_OSELF, jstring jName, pAux->zFuncName = sqlite3_mprintf("%s", zName) /* OOM here is non-fatal. Ignore it. */; } - JSTR_RELEASE(jName, zName); + s3jni_mutf8_release(jName, zName); return (jint)rc; } -typedef struct s3jni_fts5AuxData s3jni_fts5AuxData; -struct s3jni_fts5AuxData { - JNIEnv *env; +typedef struct S3JniFts5AuxData S3JniFts5AuxData; +/* +** TODO: this middle-man struct is no longer necessary. Conider +** removing it and passing around jObj itself instead. +*/ +struct S3JniFts5AuxData { jobject jObj; }; -static void s3jni_fts5AuxData_xDestroy(void *x){ +static void S3JniFts5AuxData_xDestroy(void *x){ if(x){ - s3jni_fts5AuxData * const p = x; + S3JniFts5AuxData * const p = x; if(p->jObj){ - JNIEnv *env = p->env; - s3jni_call_xDestroy(env, p->jObj, 0); + LocalJniGetEnv; + s3jni_call_xDestroy(env, p->jObj); UNREF_G(p->jObj); } sqlite3_free(x); @@ -3774,7 +3965,7 @@ static void s3jni_fts5AuxData_xDestroy(void *x){ JDECLFtsXA(jobject,xGetAuxdata)(JENV_OSELF,jobject jCtx, jboolean bClear){ Fts5ExtDecl; jobject rv = 0; - s3jni_fts5AuxData * const pAux = fext->xGetAuxdata(PtrGet_Fts5Context(jCtx), bClear); + S3JniFts5AuxData * const pAux = fext->xGetAuxdata(PtrGet_Fts5Context(jCtx), bClear); if(pAux){ if(bClear){ if( pAux->jObj ){ @@ -3816,41 +4007,28 @@ JDECLFtsXA(jint,xPhraseCount)(JENV_OSELF,jobject jCtx){ return (jint)fext->xPhraseCount(PtrGet_Fts5Context(jCtx)); } -/** - Initializes jc->jPhraseIter if it needed it. -*/ -static void s3jni_phraseIter_init(JNIEnv *const env, S3JniEnvCache * const jc, - jobject jIter){ - if(!jc->jPhraseIter.klazz){ - jclass klazz = (*env)->GetObjectClass(env, jIter); - jc->jPhraseIter.klazz = REF_G(klazz); - jc->jPhraseIter.fidA = (*env)->GetFieldID(env, klazz, "a", "J"); - EXCEPTION_IS_FATAL("Cannot get Fts5PhraseIter.a field."); - jc->jPhraseIter.fidB = (*env)->GetFieldID(env, klazz, "a", "J"); - EXCEPTION_IS_FATAL("Cannot get Fts5PhraseIter.b field."); - } -} - /* Copy the 'a' and 'b' fields from pSrc to Fts5PhraseIter object jIter. */ -static void s3jni_phraseIter_NToJ(JNIEnv *const env, S3JniEnvCache const * const jc, - Fts5PhraseIter const * const pSrc, - jobject jIter){ - assert(jc->jPhraseIter.klazz); - (*env)->SetLongField(env, jIter, jc->jPhraseIter.fidA, (jlong)pSrc->a); +static void s3jni_phraseIter_NToJ(JNIEnv *const env, + Fts5PhraseIter const * const pSrc, + jobject jIter){ + S3JniGlobalType * const g = &S3JniGlobal; + assert(g->fts5.jPhraseIter.fidA); + (*env)->SetLongField(env, jIter, g->fts5.jPhraseIter.fidA, (jlong)pSrc->a); EXCEPTION_IS_FATAL("Cannot set Fts5PhraseIter.a field."); - (*env)->SetLongField(env, jIter, jc->jPhraseIter.fidB, (jlong)pSrc->b); + (*env)->SetLongField(env, jIter, g->fts5.jPhraseIter.fidB, (jlong)pSrc->b); EXCEPTION_IS_FATAL("Cannot set Fts5PhraseIter.b field."); } /* Copy the 'a' and 'b' fields from Fts5PhraseIter object jIter to pDest. */ -static void s3jni_phraseIter_JToN(JNIEnv *const env, S3JniEnvCache const * const jc, - jobject jIter, Fts5PhraseIter * const pDest){ - assert(jc->jPhraseIter.klazz); +static void s3jni_phraseIter_JToN(JNIEnv *const env, jobject jIter, + Fts5PhraseIter * const pDest){ + S3JniGlobalType * const g = &S3JniGlobal; + assert(g->fts5.jPhraseIter.fidA); pDest->a = - (const unsigned char *)(*env)->GetLongField(env, jIter, jc->jPhraseIter.fidA); + (const unsigned char *)(*env)->GetLongField(env, jIter, g->fts5.jPhraseIter.fidA); EXCEPTION_IS_FATAL("Cannot get Fts5PhraseIter.a field."); pDest->b = - (const unsigned char *)(*env)->GetLongField(env, jIter, jc->jPhraseIter.fidB); + (const unsigned char *)(*env)->GetLongField(env, jIter, g->fts5.jPhraseIter.fidB); EXCEPTION_IS_FATAL("Cannot get Fts5PhraseIter.b field."); } @@ -3858,16 +4036,14 @@ JDECLFtsXA(jint,xPhraseFirst)(JENV_OSELF,jobject jCtx, jint iPhrase, jobject jIter, jobject jOutCol, jobject jOutOff){ Fts5ExtDecl; - S3JniEnvCache * const jc = S3JniGlobal_env_cache(env); Fts5PhraseIter iter; int rc, iCol = 0, iOff = 0; - s3jni_phraseIter_init(env, jc, jIter); rc = fext->xPhraseFirst(PtrGet_Fts5Context(jCtx), (int)iPhrase, &iter, &iCol, &iOff); if( 0==rc ){ OutputPointer_set_Int32(env, jOutCol, iCol); OutputPointer_set_Int32(env, jOutOff, iOff); - s3jni_phraseIter_NToJ(env, jc, &iter, jIter); + s3jni_phraseIter_NToJ(env, &iter, jIter); } return rc; } @@ -3875,15 +4051,13 @@ JDECLFtsXA(jint,xPhraseFirst)(JENV_OSELF,jobject jCtx, jint iPhrase, JDECLFtsXA(jint,xPhraseFirstColumn)(JENV_OSELF,jobject jCtx, jint iPhrase, jobject jIter, jobject jOutCol){ Fts5ExtDecl; - S3JniEnvCache * const jc = S3JniGlobal_env_cache(env); Fts5PhraseIter iter; int rc, iCol = 0; - s3jni_phraseIter_init(env, jc, jIter); rc = fext->xPhraseFirstColumn(PtrGet_Fts5Context(jCtx), (int)iPhrase, &iter, &iCol); if( 0==rc ){ OutputPointer_set_Int32(env, jOutCol, iCol); - s3jni_phraseIter_NToJ(env, jc, &iter, jIter); + s3jni_phraseIter_NToJ(env, &iter, jIter); } return rc; } @@ -3891,29 +4065,24 @@ JDECLFtsXA(jint,xPhraseFirstColumn)(JENV_OSELF,jobject jCtx, jint iPhrase, JDECLFtsXA(void,xPhraseNext)(JENV_OSELF,jobject jCtx, jobject jIter, jobject jOutCol, jobject jOutOff){ Fts5ExtDecl; - S3JniEnvCache * const jc = S3JniGlobal_env_cache(env); Fts5PhraseIter iter; int iCol = 0, iOff = 0; - if(!jc->jPhraseIter.klazz) return /*SQLITE_MISUSE*/; - s3jni_phraseIter_JToN(env, jc, jIter, &iter); - fext->xPhraseNext(PtrGet_Fts5Context(jCtx), - &iter, &iCol, &iOff); + s3jni_phraseIter_JToN(env, jIter, &iter); + fext->xPhraseNext(PtrGet_Fts5Context(jCtx), &iter, &iCol, &iOff); OutputPointer_set_Int32(env, jOutCol, iCol); OutputPointer_set_Int32(env, jOutOff, iOff); - s3jni_phraseIter_NToJ(env, jc, &iter, jIter); + s3jni_phraseIter_NToJ(env, &iter, jIter); } JDECLFtsXA(void,xPhraseNextColumn)(JENV_OSELF,jobject jCtx, jobject jIter, jobject jOutCol){ Fts5ExtDecl; - S3JniEnvCache * const jc = S3JniGlobal_env_cache(env); Fts5PhraseIter iter; int iCol = 0; - if(!jc->jPhraseIter.klazz) return /*SQLITE_MISUSE*/; - s3jni_phraseIter_JToN(env, jc, jIter, &iter); + s3jni_phraseIter_JToN(env, jIter, &iter); fext->xPhraseNextColumn(PtrGet_Fts5Context(jCtx), &iter, &iCol); OutputPointer_set_Int32(env, jOutCol, iCol); - s3jni_phraseIter_NToJ(env, jc, &iter, jIter); + s3jni_phraseIter_NToJ(env, &iter, jIter); } @@ -3922,13 +4091,10 @@ JDECLFtsXA(jint,xPhraseSize)(JENV_OSELF,jobject jCtx, jint iPhrase){ return (jint)fext->xPhraseSize(PtrGet_Fts5Context(jCtx), (int)iPhrase); } -/** - State for use with xQueryPhrase() and xTokenize(). -*/ +/* State for use with xQueryPhrase() and xTokenize(). */ struct s3jni_xQueryPhraseState { - JNIEnv *env; Fts5ExtensionApi const * fext; - S3JniEnvCache const * jc; + S3JniEnv const * jc; jmethodID midCallback; jobject jCallback; jobject jFcx; @@ -3946,11 +4112,11 @@ static int s3jni_xQueryPhrase(const Fts5ExtensionApi *xapi, guaranteed to be the same one passed to xQueryPhrase(). If it's not, we'll have to create a new wrapper object on every call. */ struct s3jni_xQueryPhraseState const * s = pData; - JNIEnv * const env = s->env; + LocalJniGetEnv; int rc = (int)(*env)->CallIntMethod(env, s->jCallback, s->midCallback, - s->jc->jFtsExt, s->jFcx); + SJG.fts5.jFtsExt, s->jFcx); IFTHREW{ - EXCEPTION_WARN_CALLBACK_THREW("xQueryPhrase callback"); + EXCEPTION_WARN_CALLBACK_THREW("xQueryPhrase() callback"); EXCEPTION_CLEAR; rc = SQLITE_ERROR; } @@ -3960,11 +4126,11 @@ static int s3jni_xQueryPhrase(const Fts5ExtensionApi *xapi, JDECLFtsXA(jint,xQueryPhrase)(JENV_OSELF,jobject jFcx, jint iPhrase, jobject jCallback){ Fts5ExtDecl; - S3JniEnvCache * const jc = S3JniGlobal_env_cache(env); + S3JniEnv * const jc = S3JniGlobal_env_cache(env); struct s3jni_xQueryPhraseState s; jclass klazz = jCallback ? (*env)->GetObjectClass(env, jCallback) : NULL; + if( !klazz ) return SQLITE_MISUSE; - s.env = env; s.jc = jc; s.jCallback = jCallback; s.jFcx = jFcx; @@ -3972,6 +4138,7 @@ JDECLFtsXA(jint,xQueryPhrase)(JENV_OSELF,jobject jFcx, jint iPhrase, s.midCallback = (*env)->GetMethodID(env, klazz, "xCallback", "(Lorg.sqlite.jni.Fts5ExtensionApi;" "Lorg.sqlite.jni.Fts5Context;)I"); + UNREF_L(klazz); EXCEPTION_IS_FATAL("Could not extract xQueryPhraseCallback.xCallback method."); return (jint)fext->xQueryPhrase(PtrGet_Fts5Context(jFcx), iPhrase, &s, s3jni_xQueryPhrase); @@ -3994,31 +4161,28 @@ JDECLFtsXA(jlong,xRowid)(JENV_OSELF,jobject jCtx){ JDECLFtsXA(int,xSetAuxdata)(JENV_OSELF,jobject jCtx, jobject jAux){ Fts5ExtDecl; int rc; - s3jni_fts5AuxData * pAux; + S3JniFts5AuxData * pAux; pAux = sqlite3_malloc(sizeof(*pAux)); if(!pAux){ if(jAux){ - // Emulate how xSetAuxdata() behaves when it cannot alloc - // its auxdata wrapper. - s3jni_call_xDestroy(env, jAux, 0); + /* Emulate how xSetAuxdata() behaves when it cannot alloc + ** its auxdata wrapper. */ + s3jni_call_xDestroy(env, jAux); } return SQLITE_NOMEM; } - pAux->env = env; pAux->jObj = REF_G(jAux); rc = fext->xSetAuxdata(PtrGet_Fts5Context(jCtx), pAux, - s3jni_fts5AuxData_xDestroy); + S3JniFts5AuxData_xDestroy); return rc; } -/** - xToken() impl for xTokenize(). -*/ +/* xToken() impl for xTokenize(). */ static int s3jni_xTokenize_xToken(void *p, int tFlags, const char* z, int nZ, int iStart, int iEnd){ int rc; + LocalJniGetEnv; struct s3jni_xQueryPhraseState * const s = p; - JNIEnv * const env = s->env; jbyteArray jba; if( s->tok.zPrev == z && s->tok.nPrev == nZ ){ jba = s->tok.jba; @@ -4039,41 +4203,43 @@ static int s3jni_xTokenize_xToken(void *p, int tFlags, const char* z, return rc; } -/** - Proxy for Fts5ExtensionApi.xTokenize() and fts5_tokenizer.xTokenize() +/* +** Proxy for Fts5ExtensionApi.xTokenize() and +** fts5_tokenizer.xTokenize() */ -static jint s3jni_fts5_xTokenize(JENV_OSELF, const char *zClassName, +static jint s3jni_fts5_xTokenize(JENV_OSELF, S3NphRef const *pRef, jint tokFlags, jobject jFcx, jbyteArray jbaText, jobject jCallback){ Fts5ExtDecl; - S3JniEnvCache * const jc = S3JniGlobal_env_cache(env); + S3JniEnv * const jc = S3JniGlobal_env_cache(env); struct s3jni_xQueryPhraseState s; int rc = 0; - jbyte * const pText = jCallback ? JBA_TOC(jbaText) : 0; + jbyte * const pText = jCallback ? s3jni_jbytearray_bytes(jbaText) : 0; jsize nText = pText ? (*env)->GetArrayLength(env, jbaText) : 0; jclass const klazz = jCallback ? (*env)->GetObjectClass(env, jCallback) : NULL; + if( !klazz ) return SQLITE_MISUSE; memset(&s, 0, sizeof(s)); - s.env = env; s.jc = jc; s.jCallback = jCallback; s.jFcx = jFcx; s.fext = fext; s.midCallback = (*env)->GetMethodID(env, klazz, "xToken", "(I[BII)I"); + UNREF_L(klazz); IFTHREW { EXCEPTION_REPORT; EXCEPTION_CLEAR; - JBA_RELEASE(jbaText, pText); + s3jni_jbytearray_release(jbaText, pText); return SQLITE_ERROR; } s.tok.jba = REF_L(jbaText); s.tok.zPrev = (const char *)pText; s.tok.nPrev = (int)nText; - if( zClassName == S3JniClassNames.Fts5ExtensionApi ){ + if( pRef == &S3NphRefs.Fts5ExtensionApi ){ rc = fext->xTokenize(PtrGet_Fts5Context(jFcx), (const char *)pText, (int)nText, &s, s3jni_xTokenize_xToken); - }else if( zClassName == S3JniClassNames.fts5_tokenizer ){ + }else if( pRef == &S3NphRefs.fts5_tokenizer ){ fts5_tokenizer * const pTok = PtrGet_fts5_tokenizer(jSelf); rc = pTok->xTokenize(PtrGet_Fts5Tokenizer(jFcx), &s, tokFlags, (const char *)pText, (int)nText, @@ -4085,19 +4251,19 @@ static jint s3jni_fts5_xTokenize(JENV_OSELF, const char *zClassName, assert( s.tok.zPrev ); UNREF_L(s.tok.jba); } - JBA_RELEASE(jbaText, pText); + s3jni_jbytearray_release(jbaText, pText); return (jint)rc; } JDECLFtsXA(jint,xTokenize)(JENV_OSELF,jobject jFcx, jbyteArray jbaText, jobject jCallback){ - return s3jni_fts5_xTokenize(env, jSelf, S3JniClassNames.Fts5ExtensionApi, + return s3jni_fts5_xTokenize(env, jSelf, &S3NphRefs.Fts5ExtensionApi, 0, jFcx, jbaText, jCallback); } JDECLFtsTok(jint,xTokenize)(JENV_OSELF,jobject jFcx, jint tokFlags, jbyteArray jbaText, jobject jCallback){ - return s3jni_fts5_xTokenize(env, jSelf, S3JniClassNames.Fts5Tokenizer, + return s3jni_fts5_xTokenize(env, jSelf, &S3NphRefs.Fts5Tokenizer, tokFlags, jFcx, jbaText, jCallback); } @@ -4283,15 +4449,15 @@ Java_org_sqlite_jni_tester_SQLTester_strglob( JENV_CSELF, jbyteArray baG, jbyteArray baT ){ int rc = 0; - jbyte * const pG = JBA_TOC(baG); - jbyte * const pT = pG ? JBA_TOC(baT) : 0; - OOM_CHECK(pT); + jbyte * const pG = s3jni_jbytearray_bytes(baG); + jbyte * const pT = pG ? s3jni_jbytearray_bytes(baT) : 0; + s3jni_oom_check(pT); /* Note that we're relying on the byte arrays having been NUL-terminated on the Java side. */ rc = !SQLTester_strnotglob((const char *)pG, (const char *)pT); - JBA_RELEASE(baG, pG); - JBA_RELEASE(baT, pT); + s3jni_jbytearray_release(baG, pG); + s3jni_jbytearray_release(baT, pT); return rc; } @@ -4316,23 +4482,27 @@ Java_org_sqlite_jni_tester_SQLTester_installCustomExtensions(JENV_CSELF){ //////////////////////////////////////////////////////////////////////// -/** - Uncaches the current JNIEnv from the S3JniGlobal state, clearing any - resources owned by that cache entry and making that slot available - for re-use. It is important that the Java-side decl of this - function be declared as synchronous. +/* +** Uncaches the current JNIEnv from the S3JniGlobal state, clearing any +** resources owned by that cache entry and making that slot available +** for re-use. It is important that the Java-side decl of this +** function be declared as synchronous. */ JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_SQLite3Jni_uncacheJniEnv(JENV_CSELF){ - return S3JniGlobal_env_uncache(env) ? JNI_TRUE : JNI_FALSE; + int rc; + MUTEX_ENV_ENTER; + rc = S3JniGlobal_env_uncache(env); + MUTEX_ENV_LEAVE; + return rc ? JNI_TRUE : JNI_FALSE; } -/** - Called during static init of the SQLite3Jni class to sync certain - compile-time constants to Java-space. - - This routine is part of the reason why we have to #include - sqlite3.c instead of sqlite3.h. +/* +** Called during static init of the SQLite3Jni class to sync certain +** compile-time constants to Java-space. +** +** This routine is part of the reason why we have to #include +** sqlite3.c instead of sqlite3.h. */ JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_init(JENV_CSELF){ @@ -4346,13 +4516,6 @@ Java_org_sqlite_jni_SQLite3Jni_init(JENV_CSELF){ int value; } ConfigFlagEntry; const ConfigFlagEntry aLimits[] = { - {"SQLITE_ENABLE_FTS5", JTYPE_BOOL, -#ifdef SQLITE_ENABLE_FTS5 - 1 -#else - 0 -#endif - }, {"SQLITE_MAX_ALLOCATION_SIZE", JTYPE_INT, SQLITE_MAX_ALLOCATION_SIZE}, {"SQLITE_LIMIT_LENGTH", JTYPE_INT, SQLITE_LIMIT_LENGTH}, {"SQLITE_MAX_LENGTH", JTYPE_INT, SQLITE_MAX_LENGTH}, @@ -4381,30 +4544,84 @@ Java_org_sqlite_jni_SQLite3Jni_init(JENV_CSELF){ {0,0} }; jfieldID fieldId; + jclass klazz; const ConfigFlagEntry * pConfFlag; + if( 0==sqlite3_threadsafe() ){ + (*env)->FatalError(env, "sqlite3 was not built with SQLITE_THREADSAFE."); + return; + } memset(&S3JniGlobal, 0, sizeof(S3JniGlobal)); - if( (*env)->GetJavaVM(env, &S3JniGlobal.jvm) ){ + if( (*env)->GetJavaVM(env, &SJG.jvm) ){ (*env)->FatalError(env, "GetJavaVM() failure shouldn't be possible."); return; } -#if 0 - /* Just for sanity checking... */ - (void)S3JniGlobal_env_cache(env); - if( !S3JniGlobal.envCache.aHead ){ - (*env)->FatalError(env, "Could not allocate JNIEnv-specific cache."); - return; + + /* Grab references to various global classes and objects... */ + SJG.g.cObj = REF_G((*env)->FindClass(env,"java/lang/Object")); + EXCEPTION_IS_FATAL("Error getting reference to Object class."); + + SJG.g.cLong = REF_G((*env)->FindClass(env,"java/lang/Long")); + EXCEPTION_IS_FATAL("Error getting reference to Long class."); + SJG.g.ctorLong1 = (*env)->GetMethodID(env, SJG.g.cLong, + "", "(J)V"); + EXCEPTION_IS_FATAL("Error getting reference to Long constructor."); + + SJG.g.cString = REF_G((*env)->FindClass(env,"java/lang/String")); + EXCEPTION_IS_FATAL("Error getting reference to String class."); + SJG.g.ctorStringBA = + (*env)->GetMethodID(env, SJG.g.cString, + "", "([BLjava/nio/charset/Charset;)V"); + EXCEPTION_IS_FATAL("Error getting reference to String(byte[],Charset) ctor."); + SJG.g.stringGetBytes = + (*env)->GetMethodID(env, SJG.g.cString, + "getBytes", "(Ljava/nio/charset/Charset;)[B"); + EXCEPTION_IS_FATAL("Error getting reference to String.getBytes(Charset)."); + + { /* StandardCharsets.UTF_8 */ + jfieldID fUtf8; + klazz = (*env)->FindClass(env,"java/nio/charset/StandardCharsets"); + EXCEPTION_IS_FATAL("Error getting reference to StandardCharsets class."); + fUtf8 = (*env)->GetStaticFieldID(env, klazz, "UTF_8", + "Ljava/nio/charset/Charset;"); + EXCEPTION_IS_FATAL("Error getting StandardCharsets.UTF_8 field."); + SJG.g.oCharsetUtf8 = + REF_G((*env)->GetStaticObjectField(env, klazz, fUtf8)); + EXCEPTION_IS_FATAL("Error getting reference to StandardCharsets.UTF_8."); + UNREF_L(klazz); } - assert( 1 == S3JniGlobal.metrics.envCacheMisses ); - assert( env == S3JniGlobal.envCache.aHead->env ); - assert( 0 != S3JniGlobal.envCache.aHead->g.cObj ); + +#ifdef SQLITE_ENABLE_FTS5 + klazz = (*env)->FindClass(env, "org/sqlite/jni/Fts5PhraseIter"); + EXCEPTION_IS_FATAL("Error getting reference to org.sqlite.jni.Fts5PhraseIter."); + SJG.fts5.jPhraseIter.fidA = (*env)->GetFieldID(env, klazz, "a", "J"); + EXCEPTION_IS_FATAL("Cannot get Fts5PhraseIter.a field."); + SJG.fts5.jPhraseIter.fidB = (*env)->GetFieldID(env, klazz, "b", "J"); + EXCEPTION_IS_FATAL("Cannot get Fts5PhraseIter.b field."); + UNREF_L(klazz); #endif + SJG.envCache.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST); + s3jni_oom_check( SJG.envCache.mutex ); + SJG.perDb.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST); + s3jni_oom_check( SJG.perDb.mutex ); + SJG.autoExt.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST); + s3jni_oom_check( SJG.autoExt.mutex ); + +#if S3JNI_METRICS_MUTEX + SJG.metrics.mutex = sqlite3_mutex_alloc(SQLITE_MUTEX_FAST); + s3jni_oom_check( SJG.metrics.mutex ); +#endif + + sqlite3_shutdown() + /* So that it becomes legal for Java-level code to call + ** sqlite3_config(), if it's ever implemented. */; + + /* Set up static "consts" of the SQLite3Jni class. */ for( pConfFlag = &aLimits[0]; pConfFlag->zName; ++pConfFlag ){ char const * zSig = (JTYPE_BOOL == pConfFlag->jtype) ? "Z" : "I"; fieldId = (*env)->GetStaticFieldID(env, jKlazz, pConfFlag->zName, zSig); EXCEPTION_IS_FATAL("Missing an expected static member of the SQLite3Jni class."); - //MARKER(("Setting %s (field=%p) = %d\n", pConfFlag->zName, fieldId, pConfFlag->value)); assert(fieldId); switch(pConfFlag->jtype){ case JTYPE_INT: diff --git a/ext/jni/src/c/sqlite3-jni.h b/ext/jni/src/c/sqlite3-jni.h index bcb55c4f1c..8052a6027e 100644 --- a/ext/jni/src/c/sqlite3-jni.h +++ b/ext/jni/src/c/sqlite3-jni.h @@ -843,6 +843,14 @@ JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1parameter_1 JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1text (JNIEnv *, jclass, jobject, jint, jbyteArray, jint); +/* + * Class: org_sqlite_jni_SQLite3Jni + * Method: sqlite3_bind_text16 + * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I[BI)I + */ +JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1bind_1text16 + (JNIEnv *, jclass, jobject, jint, jbyteArray, jint); + /* * Class: org_sqlite_jni_SQLite3Jni * Method: sqlite3_bind_zeroblob @@ -1013,18 +1021,18 @@ JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1table_ /* * Class: org_sqlite_jni_SQLite3Jni - * Method: sqlite3_column_text16 - * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)Ljava/lang/String; + * Method: sqlite3_column_text_utf8 + * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)[B */ -JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1text16 +JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1text_1utf8 (JNIEnv *, jclass, jobject, jint); /* * Class: org_sqlite_jni_SQLite3Jni - * Method: sqlite3_column_text - * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)[B + * Method: sqlite3_column_text16 + * Signature: (Lorg/sqlite/jni/sqlite3_stmt;I)Ljava/lang/String; */ -JNIEXPORT jbyteArray JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1text +JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1column_1text16 (JNIEnv *, jclass, jobject, jint); /* @@ -1083,6 +1091,22 @@ JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1compileoption_ JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1compileoption_1used (JNIEnv *, jclass, jstring); +/* + * Class: org_sqlite_jni_SQLite3Jni + * Method: sqlite3_config + * Signature: (I)I + */ +JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1config__I + (JNIEnv *, jclass, jint); + +/* + * Class: org_sqlite_jni_SQLite3Jni + * Method: sqlite3_config + * Signature: (Lorg/sqlite/jni/SQLLog;)I + */ +JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1config__Lorg_sqlite_jni_SQLLog_2 + (JNIEnv *, jclass, jobject); + /* * Class: org_sqlite_jni_SQLite3Jni * Method: sqlite3_create_collation @@ -1211,6 +1235,22 @@ JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1finalize JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1initialize (JNIEnv *, jclass); +/* + * Class: org_sqlite_jni_SQLite3Jni + * Method: sqlite3_interrupt + * Signature: (Lorg/sqlite/jni/sqlite3;)V + */ +JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1interrupt + (JNIEnv *, jclass, jobject); + +/* + * Class: org_sqlite_jni_SQLite3Jni + * Method: sqlite3_is_interrupted + * Signature: (Lorg/sqlite/jni/sqlite3;)Z + */ +JNIEXPORT jboolean JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1is_1interrupted + (JNIEnv *, jclass, jobject); + /* * Class: org_sqlite_jni_SQLite3Jni * Method: sqlite3_last_insert_rowid @@ -1275,6 +1315,54 @@ JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1prepare_1v2 JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1prepare_1v3 (JNIEnv *, jclass, jobject, jbyteArray, jint, jint, jobject, jobject); +/* + * Class: org_sqlite_jni_SQLite3Jni + * Method: sqlite3_preupdate_blobwrite + * Signature: (Lorg/sqlite/jni/sqlite3;)I + */ +JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1preupdate_1blobwrite + (JNIEnv *, jclass, jobject); + +/* + * Class: org_sqlite_jni_SQLite3Jni + * Method: sqlite3_preupdate_count + * Signature: (Lorg/sqlite/jni/sqlite3;)I + */ +JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1preupdate_1count + (JNIEnv *, jclass, jobject); + +/* + * Class: org_sqlite_jni_SQLite3Jni + * Method: sqlite3_preupdate_depth + * Signature: (Lorg/sqlite/jni/sqlite3;)I + */ +JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1preupdate_1depth + (JNIEnv *, jclass, jobject); + +/* + * Class: org_sqlite_jni_SQLite3Jni + * Method: sqlite3_preupdate_hook + * Signature: (Lorg/sqlite/jni/sqlite3;Lorg/sqlite/jni/PreUpdateHook;)Lorg/sqlite/jni/PreUpdateHook; + */ +JNIEXPORT jobject JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1preupdate_1hook + (JNIEnv *, jclass, jobject, jobject); + +/* + * Class: org_sqlite_jni_SQLite3Jni + * Method: sqlite3_preupdate_new + * Signature: (Lorg/sqlite/jni/sqlite3;ILorg/sqlite/jni/OutputPointer/sqlite3_value;)I + */ +JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1preupdate_1new + (JNIEnv *, jclass, jobject, jint, jobject); + +/* + * Class: org_sqlite_jni_SQLite3Jni + * Method: sqlite3_preupdate_old + * Signature: (Lorg/sqlite/jni/sqlite3;ILorg/sqlite/jni/OutputPointer/sqlite3_value;)I + */ +JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1preupdate_1old + (JNIEnv *, jclass, jobject, jint, jobject); + /* * Class: org_sqlite_jni_SQLite3Jni * Method: sqlite3_progress_handler @@ -1427,6 +1515,14 @@ JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1text JNIEXPORT void JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1result_1text64 (JNIEnv *, jclass, jobject, jbyteArray, jlong, jint); +/* + * Class: org_sqlite_jni_SQLite3Jni + * Method: sqlite3_shutdown + * Signature: ()I + */ +JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1shutdown + (JNIEnv *, jclass); + /* * Class: org_sqlite_jni_SQLite3Jni * Method: sqlite3_status @@ -1635,14 +1731,6 @@ JNIEXPORT jlong JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1int64 JNIEXPORT jobject JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1java_1object (JNIEnv *, jclass, jobject); -/* - * Class: org_sqlite_jni_SQLite3Jni - * Method: sqlite3_value_text - * Signature: (Lorg/sqlite/jni/sqlite3_value;)Ljava/lang/String; - */ -JNIEXPORT jstring JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1text - (JNIEnv *, jclass, jobject); - /* * Class: org_sqlite_jni_SQLite3Jni * Method: sqlite3_value_text_utf8 @@ -1715,14 +1803,6 @@ JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1frombind JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1value_1subtype (JNIEnv *, jclass, jobject); -/* - * Class: org_sqlite_jni_SQLite3Jni - * Method: sqlite3_shutdown - * Signature: ()I - */ -JNIEXPORT jint JNICALL Java_org_sqlite_jni_SQLite3Jni_sqlite3_1shutdown - (JNIEnv *, jclass); - /* * Class: org_sqlite_jni_SQLite3Jni * Method: sqlite3_do_something_for_developer diff --git a/ext/jni/src/org/sqlite/jni/AutoExtension.java b/ext/jni/src/org/sqlite/jni/AutoExtension.java index a2ab6a0f75..443345fde4 100644 --- a/ext/jni/src/org/sqlite/jni/AutoExtension.java +++ b/ext/jni/src/org/sqlite/jni/AutoExtension.java @@ -14,18 +14,27 @@ package org.sqlite.jni; /** - A callback for use with sqlite3_auto_extension(). + A callback for use with the sqlite3_auto_extension() family of + APIs. */ public interface AutoExtension { /** - Must function as described for the sqlite3_auto_extension(), - with the caveat that the signature is more limited. + Must function as described for a sqlite3_auto_extension() + callback, with the caveat that the signature is shorter. - As an exception (as it were) to the callbacks-must-not-throw - rule, AutoExtensions may do so and the exception's error message + AutoExtensions may throw and the exception's error message will be set as the db's error string. - Results are undefined if db is closed by an auto-extension. + Tips for implementations: + + - Opening a database from an auto-extension handler will lead to + an endless recursion of the auto-handler triggering itself + indirectly for each newly-opened database. + + - If this routine is stateful, it may be useful to make the + overridden method synchronized. + + - Results are undefined if db is closed by an auto-extension. */ int xEntryPoint(sqlite3 db); } diff --git a/ext/jni/src/org/sqlite/jni/Fts5.java b/ext/jni/src/org/sqlite/jni/Fts5.java index 102cf575a8..443a69a409 100644 --- a/ext/jni/src/org/sqlite/jni/Fts5.java +++ b/ext/jni/src/org/sqlite/jni/Fts5.java @@ -27,7 +27,7 @@ public final class Fts5 { //! Callback type for use with xTokenize() variants public static interface xTokenizeCallback { - int xToken(int tFlags, byte txt[], int iStart, int iEnd); + int xToken(int tFlags, byte[] txt, int iStart, int iEnd); } public static final int FTS5_TOKENIZE_QUERY = 0x0001; diff --git a/ext/jni/src/org/sqlite/jni/Fts5ExtensionApi.java b/ext/jni/src/org/sqlite/jni/Fts5ExtensionApi.java index ac041e3001..205f110f41 100644 --- a/ext/jni/src/org/sqlite/jni/Fts5ExtensionApi.java +++ b/ext/jni/src/org/sqlite/jni/Fts5ExtensionApi.java @@ -23,7 +23,7 @@ import java.nio.charset.StandardCharsets; public final class Fts5ExtensionApi extends NativePointerHolder { //! Only called from JNI private Fts5ExtensionApi(){} - private int iVersion = 2; + private final int iVersion = 2; /* Callback type for used by xQueryPhrase(). */ public static interface xQueryPhraseCallback { @@ -33,54 +33,54 @@ public final class Fts5ExtensionApi extends NativePointerHolder { //! Only set from JNI, where access permissions don't matter. - private long nativePointer = 0; + private volatile long nativePointer = 0; public final long getNativePointer(){ return nativePointer; } } diff --git a/ext/jni/src/org/sqlite/jni/OutputPointer.java b/ext/jni/src/org/sqlite/jni/OutputPointer.java index 82a90c9185..bf61656dd5 100644 --- a/ext/jni/src/org/sqlite/jni/OutputPointer.java +++ b/ext/jni/src/org/sqlite/jni/OutputPointer.java @@ -36,6 +36,9 @@ package org.sqlite.jni; access to the object's value via the `value` property, whereas the JNI-level opaque types do not permit client-level code to set that property. + + Warning: do not share instances of these classes across + threads. Doing so may lead to corrupting sqlite3-internal state. */ public final class OutputPointer { @@ -83,6 +86,28 @@ public final class OutputPointer { } } + /** + Output pointer for use with routines, such as sqlite3_prepupdate_new(), + which return a sqlite3_value handle via an output pointer. These + pointers can only be set by the JNI layer, not by client-level + code. + */ + public static final class sqlite3_value { + private org.sqlite.jni.sqlite3_value value; + //! Initializes with a null value. + public sqlite3_value(){value = null;} + //! Sets the current value to null. + public void clear(){value = null;} + //! Returns the current value. + public final org.sqlite.jni.sqlite3_value get(){return value;} + //! Equivalent to calling get() then clear(). + public final org.sqlite.jni.sqlite3_value take(){ + final org.sqlite.jni.sqlite3_value v = value; + value = null; + return v; + } + } + /** Output pointer for use with native routines which return integers via output pointers. diff --git a/ext/jni/src/org/sqlite/jni/PreUpdateHook.java b/ext/jni/src/org/sqlite/jni/PreUpdateHook.java new file mode 100644 index 0000000000..d5d82c72bc --- /dev/null +++ b/ext/jni/src/org/sqlite/jni/PreUpdateHook.java @@ -0,0 +1,29 @@ +/* +** 2023-08-23 +** +** The author disclaims copyright to this source code. In place of +** a legal notice, here is a blessing: +** +** May you do good and not evil. +** May you find forgiveness for yourself and forgive others. +** May you share freely, never taking more than you give. +** +************************************************************************* +** This file is part of the JNI bindings for the sqlite3 C API. +*/ +package org.sqlite.jni; + +/** + A callback for use with sqlite3_preupdate_hook(). +*/ +public interface PreUpdateHook { + /** + Must function as described for the sqlite3_preupdate_hook(). + callback, with the slight signature change. + + Must not throw. Any exceptions may emit debugging messages and + will be suppressed. + */ + void xPreUpdate(sqlite3 db, int op, String dbName, String dbTable, + long iKey1, long iKey2 ); +} diff --git a/ext/jni/src/org/sqlite/jni/SQLFunction.java b/ext/jni/src/org/sqlite/jni/SQLFunction.java index 21e5fe622a..c8e87ff827 100644 --- a/ext/jni/src/org/sqlite/jni/SQLFunction.java +++ b/ext/jni/src/org/sqlite/jni/SQLFunction.java @@ -21,17 +21,19 @@ package org.sqlite.jni; This class is not used by itself, but is a marker base class. The three UDF types are modelled by the inner classes Scalar, - Aggregate, and Window. Most simply, clients may create - anonymous classes from those to implement UDFs. Clients are free to - create their own classes for use with UDFs, so long as they conform - to the public interfaces defined by those three classes. The JNI - layer only actively relies on the SQLFunction base class. + Aggregate, and Window. Most simply, clients may subclass + those, or create anonymous classes from them, to implement + UDFs. Clients are free to create their own classes for use with + UDFs, so long as they conform to the public interfaces defined by + those three classes. The JNI layer only actively relies on the + SQLFunction base class and the method names and signatures used by + the UDF callback interfaces. */ public abstract class SQLFunction { /** PerContextState assists aggregate and window functions in - managinga their accumulator state across calls to the UDF's + managing their accumulator state across calls to the UDF's callbacks. If a given aggregate or window function is called multiple times @@ -96,7 +98,11 @@ public abstract class SQLFunction { //! Subclass for creating scalar functions. public static abstract class Scalar extends SQLFunction { - //! As for the xFunc() argument of the C API's sqlite3_create_function() + /** + As for the xFunc() argument of the C API's + sqlite3_create_function(). If this function throws, it is + translated into an sqlite3_result_error(). + */ public abstract void xFunc(sqlite3_context cx, sqlite3_value[] args); /** @@ -114,10 +120,18 @@ public abstract class SQLFunction { */ public static abstract class Aggregate extends SQLFunction { - //! As for the xStep() argument of the C API's sqlite3_create_function() + /** + As for the xStep() argument of the C API's + sqlite3_create_function(). If this function throws, the + exception is not propagated and a warning might be emitted to a + debugging channel. + */ public abstract void xStep(sqlite3_context cx, sqlite3_value[] args); - //! As for the xFinal() argument of the C API's sqlite3_create_function() + /** + As for the xFinal() argument of the C API's sqlite3_create_function(). + If this function throws, it is translated into an sqlite3_result_error(). + */ public abstract void xFinal(sqlite3_context cx); /** @@ -163,10 +177,18 @@ public abstract class SQLFunction { */ public static abstract class Window extends Aggregate { - //! As for the xInverse() argument of the C API's sqlite3_create_window_function() + /** + As for the xInverse() argument of the C API's + sqlite3_create_window_function(). If this function throws, the + exception is not propagated and a warning might be emitted + to a debugging channel. + */ public abstract void xInverse(sqlite3_context cx, sqlite3_value[] args); - //! As for the xValue() argument of the C API's sqlite3_create_window_function() + /** + As for the xValue() argument of the C API's sqlite3_create_window_function(). + See xInverse() for the fate of any exceptions this throws. + */ public abstract void xValue(sqlite3_context cx); } } diff --git a/ext/jni/src/org/sqlite/jni/SQLLog.java b/ext/jni/src/org/sqlite/jni/SQLLog.java new file mode 100644 index 0000000000..c1bc0aab61 --- /dev/null +++ b/ext/jni/src/org/sqlite/jni/SQLLog.java @@ -0,0 +1,25 @@ +/* +** 2023-08-23 +** +** The author disclaims copyright to this source code. In place of +** a legal notice, here is a blessing: +** +** May you do good and not evil. +** May you find forgiveness for yourself and forgive others. +** May you share freely, never taking more than you give. +** +************************************************************************* +** This file is part of the JNI bindings for the sqlite3 C API. +*/ +package org.sqlite.jni; + +/** + A callback for use with sqlite3_config(SQLLog). +*/ +public interface SQLLog { + /** + Must function as described for sqlite3_config(SQLITE_CONFIG_SQLLOG) + callback, with the slight signature change. + */ + void xSqllog(sqlite3 db, String msg, int msgType ); +} diff --git a/ext/jni/src/org/sqlite/jni/SQLite3Jni.java b/ext/jni/src/org/sqlite/jni/SQLite3Jni.java index 9b2a176504..ef67890918 100644 --- a/ext/jni/src/org/sqlite/jni/SQLite3Jni.java +++ b/ext/jni/src/org/sqlite/jni/SQLite3Jni.java @@ -82,27 +82,16 @@ import java.lang.annotation.ElementType; The known consequences and limitations this discrepancy places on the SQLite3 JNI binding include: - - Any functions which return client-side data from a database - take extra care to perform proper conversion, at the cost of - efficiency. - - - Functions which return database identifiers require those - identifiers to have identical representations in UTF-8 and - MUTF-8. They do not perform such conversions (A) because of the - much lower risk of an encoding discrepancy and (B) to avoid - significant extra code involved (see both the Java- and C-side - implementations of sqlite3_db_filename() for an example). Names - of databases, tables, columns, collations, and functions MUST NOT - contain characters which differ in MUTF-8 and UTF-8, or certain - APIs will mis-translate them on their way between languages - (possibly leading to a crash). + - Any functions which return state from a database take extra care + to perform proper conversion, at the cost of efficiency. - C functions which take C-style strings without a length argument require special care when taking input from Java. In particular, Java strings converted to byte arrays for encoding purposes are not NUL-terminated, and conversion to a Java byte array must be careful to add one. Functions which take a length do not require - this. Search the SQLite3Jni class for "\0" for many examples. + this so long as the length is provided. Search the SQLite3Jni + class for "\0" for many examples. - Similarly, C-side code which deals with strings which might not be NUL-terminated (e.g. while tokenizing in FTS5-related code) cannot @@ -133,21 +122,13 @@ public final class SQLite3Jni { uncacheJniEnv() when it is done with the library - either right before it terminates or when it is finished using the SQLite API. This will clean up any cached per-JNIEnv info. Calling into the - library again after that "should" re-initialize the cache on - demand, but that's untested. + library will re-initialize the cache on demand. - This call forcibly wipes out all cached information for the - current JNIEnv, a side-effect of which is that behavior is - undefined if any database objects are (A) still active at the - time it is called _and_ (B) calls are subsequently made into the - library with such a database. Doing so will, at best, lead to a - crash. Azt worst, it will lead to the db possibly misbehaving - because some of its Java-bound state has been cleared. There is - no immediate harm in (A) so long as condition (B) is not met. - This process does _not_ actually close any databases or finalize - any prepared statements. For proper library behavior, and to - avoid C-side leaks, be sure to close them before calling this - function. + This process does _not_ close any databases or finalize + any prepared statements because their ownership does not depend on + a given thread. For proper library behavior, and to + avoid C-side leaks, be sure to finalize all statements and close + all databases before calling this function. Calling this from the main application thread is not strictly required but is "polite." Additional threads must call this @@ -173,67 +154,59 @@ public final class SQLite3Jni { Functions almost as documented for the C API, with these exceptions: - - The callback interface is more limited because of - cross-language differences. Specifically, auto-extensions do - not have access to the sqlite3_api object which native - auto-extensions do. + - The callback interface is is shorter because of cross-language + differences. Specifically, 3rd argument to the C auto-extension + callback interface is unnecessary here. - - If an auto-extension opens a db, thereby triggering recursion - in the auto-extension handler, it will fail with a message - explaining that recursion is not permitted. - - All of the other auto extension routines will fail without side - effects if invoked from within the execution of an - auto-extension. i.e. auto extensions can neither be added, - removed, nor cleared while one registered with this function is - running. Auto-extensions registered directly with the library - via C code, as opposed to indirectly via Java, do not have that - limitation. + The C API docs do not specifically say so, if the list of + auto-extensions is manipulated from an auto-extension, it is + undefined which, if any, auto-extensions will subsequently + execute for the current database. See the AutoExtension class docs for more information. - - Achtung: it is as yet unknown whether auto extensions registered - from one JNIEnv (thread) can be safely called from another. */ - public static synchronized native int sqlite3_auto_extension(@NotNull AutoExtension callback); + public static native int sqlite3_auto_extension(@NotNull AutoExtension callback); + + /** + Results are undefined if data is not null and n<0 || n>=data.length. + */ + public static native int sqlite3_bind_blob( + @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data, int n + ); public static int sqlite3_bind_blob( @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data ){ - return (null == data) + return (null==data) ? sqlite3_bind_null(stmt, ndx) : sqlite3_bind_blob(stmt, ndx, data, data.length); } - private static synchronized native int sqlite3_bind_blob( - @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data, int n - ); - - public static synchronized native int sqlite3_bind_double( + public static native int sqlite3_bind_double( @NotNull sqlite3_stmt stmt, int ndx, double v ); - public static synchronized native int sqlite3_bind_int( + public static native int sqlite3_bind_int( @NotNull sqlite3_stmt stmt, int ndx, int v ); - public static synchronized native int sqlite3_bind_int64( + public static native int sqlite3_bind_int64( @NotNull sqlite3_stmt stmt, int ndx, long v ); - public static synchronized native int sqlite3_bind_null( + public static native int sqlite3_bind_null( @NotNull sqlite3_stmt stmt, int ndx ); - public static synchronized native int sqlite3_bind_parameter_count( + public static native int sqlite3_bind_parameter_count( @NotNull sqlite3_stmt stmt ); - - /** A level of indirection required to ensure that the input to the - C-level function of the same name is a NUL-terminated UTF-8 - string. */ - private static synchronized native int sqlite3_bind_parameter_index( + /** + Requires that paramName be a NUL-terminated UTF-8 string. + */ + public static native int sqlite3_bind_parameter_index( @NotNull sqlite3_stmt stmt, byte[] paramName ); @@ -244,6 +217,23 @@ public final class SQLite3Jni { return sqlite3_bind_parameter_index(stmt, utf8); } + /** + Works like the C-level sqlite3_bind_text() but assumes + SQLITE_TRANSIENT for the final C API parameter. + + Results are undefined if data is not null and + maxBytes>=data.length. If maxBytes is negative then results are + undefined if data is not null and does not contain a NUL byte. + */ + private static native int sqlite3_bind_text( + @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data, int maxBytes + ); + + /** + Converts data, if not null, to a UTF-8-encoded byte array and + binds it as such, returning the result of the C-level + sqlite3_bind_null() or sqlite3_bind_text(). + */ public static int sqlite3_bind_text( @NotNull sqlite3_stmt stmt, int ndx, @Nullable String data ){ @@ -252,6 +242,9 @@ public final class SQLite3Jni { return sqlite3_bind_text(stmt, ndx, utf8, utf8.length); } + /** + Requires that data be null or in UTF-8 encoding. + */ public static int sqlite3_bind_text( @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data ){ @@ -261,98 +254,118 @@ public final class SQLite3Jni { } /** - Works like the C-level sqlite3_bind_text() but (A) assumes - SQLITE_TRANSIENT for the final parameter and (B) behaves like - sqlite3_bind_null() if the data argument is null. + Identical to the sqlite3_bind_text() overload with the same + signature but requires that its input be encoded in UTF-16 in + platform byte order. */ - private static synchronized native int sqlite3_bind_text( + private static native int sqlite3_bind_text16( @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data, int maxBytes ); - public static synchronized native int sqlite3_bind_zeroblob( + /** + Converts its string argument to UTF-16 and binds it as such, returning + the result of the C-side function of the same name. The 3rd argument + may be null. + */ + public static int sqlite3_bind_text16( + @NotNull sqlite3_stmt stmt, int ndx, @Nullable String data + ){ + if(null == data) return sqlite3_bind_null(stmt, ndx); + final byte[] bytes = data.getBytes(StandardCharsets.UTF_16); + return sqlite3_bind_text16(stmt, ndx, bytes, bytes.length); + } + + /** + Requires that data be null or in UTF-16 encoding in platform byte + order. Returns the result of the C-level sqlite3_bind_null() or + sqlite3_bind_text(). + */ + public static int sqlite3_bind_text16( + @NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data + ){ + return (null == data) + ? sqlite3_bind_null(stmt, ndx) + : sqlite3_bind_text16(stmt, ndx, data, data.length); + } + + public static native int sqlite3_bind_zeroblob( @NotNull sqlite3_stmt stmt, int ndx, int n ); - public static synchronized native int sqlite3_bind_zeroblob64( + public static native int sqlite3_bind_zeroblob64( @NotNull sqlite3_stmt stmt, int ndx, long n ); /** As for the C-level function of the same name, with a BusyHandler instance in place of a callback function. Pass it a null handler - to clear the busy handler. Calling this multiple times with the - same object is a no-op on the second and subsequent calls. + to clear the busy handler. */ - public static synchronized native int sqlite3_busy_handler( + public static native int sqlite3_busy_handler( @NotNull sqlite3 db, @Nullable BusyHandler handler ); - public static synchronized native int sqlite3_busy_timeout( + public static native int sqlite3_busy_timeout( @NotNull sqlite3 db, int ms ); - /** - Works like the C API except that it returns false, without side - effects, if auto extensions are currently running. (The JNI-level - list of extensions cannot be manipulated while it is being traversed.) - */ - public static synchronized native boolean sqlite3_cancel_auto_extension( + public static native boolean sqlite3_cancel_auto_extension( @NotNull AutoExtension ax ); - public static synchronized native int sqlite3_changes( + public static native int sqlite3_changes( @NotNull sqlite3 db ); - public static synchronized native long sqlite3_changes64( + public static native long sqlite3_changes64( @NotNull sqlite3 db ); - public static synchronized native int sqlite3_clear_bindings( + public static native int sqlite3_clear_bindings( @NotNull sqlite3_stmt stmt ); - public static synchronized native int sqlite3_close( - @NotNull sqlite3 db + public static native int sqlite3_close( + @Nullable sqlite3 db ); - public static synchronized native int sqlite3_close_v2( - @NotNull sqlite3 db + public static native int sqlite3_close_v2( + @Nullable sqlite3 db ); - public static synchronized native byte[] sqlite3_column_blob( + public static native byte[] sqlite3_column_blob( @NotNull sqlite3_stmt stmt, int ndx ); - public static synchronized native int sqlite3_column_bytes( + public static native int sqlite3_column_bytes( @NotNull sqlite3_stmt stmt, int ndx ); - public static synchronized native int sqlite3_column_bytes16( + public static native int sqlite3_column_bytes16( @NotNull sqlite3_stmt stmt, int ndx ); - public static synchronized native int sqlite3_column_count( + public static native int sqlite3_column_count( @NotNull sqlite3_stmt stmt ); - public static synchronized native double sqlite3_column_double( + public static native double sqlite3_column_double( @NotNull sqlite3_stmt stmt, int ndx ); - public static synchronized native int sqlite3_column_int( + public static native int sqlite3_column_int( @NotNull sqlite3_stmt stmt, int ndx ); - public static synchronized native long sqlite3_column_int64( + public static native long sqlite3_column_int64( @NotNull sqlite3_stmt stmt, int ndx ); - public static synchronized native String sqlite3_column_name( + public static native String sqlite3_column_name( @NotNull sqlite3_stmt stmt, int ndx ); - public static synchronized native String sqlite3_column_database_name( + public static native String sqlite3_column_database_name( @NotNull sqlite3_stmt stmt, int ndx ); @@ -385,28 +398,31 @@ public final class SQLite3Jni { return type.isInstance(o) ? (T)o : null; } - public static synchronized native String sqlite3_column_origin_name( + public static native String sqlite3_column_origin_name( @NotNull sqlite3_stmt stmt, int ndx ); - public static synchronized native String sqlite3_column_table_name( + public static native String sqlite3_column_table_name( @NotNull sqlite3_stmt stmt, int ndx ); /** - To extract _standard_ UTF-8, use sqlite3_column_text(). - This API includes no functions for working with Java's Modified - UTF-8. + Returns the given column's contents as UTF-8-encoded (not MUTF-8) + text. Returns null if the C-level sqlite3_column_text() returns + NULL. */ - public static synchronized native String sqlite3_column_text16( + public static native byte[] sqlite3_column_text_utf8( @NotNull sqlite3_stmt stmt, int ndx ); - /** - Returns the given column's contents as UTF-8-encoded (not MUTF-8) text. - Use sqlite3_column_text16() to fetch the text - */ - public static synchronized native byte[] sqlite3_column_text( + public static String sqlite3_column_text( + @NotNull sqlite3_stmt stmt, int ndx + ){ + final byte[] ba = sqlite3_column_text_utf8(stmt, ndx); + return ba==null ? null : new String(ba, StandardCharsets.UTF_8); + } + + public static native String sqlite3_column_text16( @NotNull sqlite3_stmt stmt, int ndx ); @@ -447,11 +463,11 @@ public final class SQLite3Jni { // return rv; // } - public static synchronized native int sqlite3_column_type( + public static native int sqlite3_column_type( @NotNull sqlite3_stmt stmt, int ndx ); - public static synchronized native sqlite3_value sqlite3_column_value( + public static native sqlite3_value sqlite3_column_value( @NotNull sqlite3_stmt stmt, int ndx ); @@ -459,7 +475,7 @@ public final class SQLite3Jni { This functions like C's sqlite3_collation_needed16() because Java's string type is compatible with that interface. */ - public static synchronized native int sqlite3_collation_needed( + public static native int sqlite3_collation_needed( @NotNull sqlite3 db, @Nullable CollationNeeded callback ); @@ -467,11 +483,11 @@ public final class SQLite3Jni { Returns the db handle passed to sqlite3_open() or sqlite3_open_v2(), as opposed to a new wrapper object. */ - public static synchronized native sqlite3 sqlite3_context_db_handle( + public static native sqlite3 sqlite3_context_db_handle( @NotNull sqlite3_context cx ); - public static synchronized native CommitHook sqlite3_commit_hook( + public static native CommitHook sqlite3_commit_hook( @NotNull sqlite3 db, @Nullable CommitHook hook ); @@ -483,7 +499,32 @@ public final class SQLite3Jni { @NotNull String optName ); - public static synchronized native int sqlite3_create_collation( + /* + ** Works like in the C API with the exception that it only supports + ** the following subset of configution flags: + ** + ** - SQLITE_CONFIG_SINGLETHREAD + ** - SQLITE_CONFIG_MULTITHREAD + ** - SQLITE_CONFIG_SERIALIZED + ** + ** Others may be added in the future. It returns SQLITE_MISUSE if + ** given an argument it does not handle. + */ + public static native int sqlite3_config(int op); + + /* + ** If the native library was built with SQLITE_ENABLE_SQLLOG defined + ** then this acts as a proxy for C's + ** sqlite3_config(SQLITE_ENABLE_SQLLOG,...). This sets or clears the + ** logger. If installation of a logger fails, any previous logger is + ** retained. + ** + ** If not built with SQLITE_ENABLE_SQLLOG defined, this returns + ** SQLITE_MISUSE. + */ + public static native int sqlite3_config( @Nullable SQLLog logger ); + + public static native int sqlite3_create_collation( @NotNull sqlite3 db, @NotNull String name, int eTextRep, @NotNull Collation col ); @@ -496,16 +537,16 @@ public final class SQLite3Jni { SQLFunction's inner classes (Scalar, Aggregate, and Window) for details. */ - public static synchronized native int sqlite3_create_function( + public static native int sqlite3_create_function( @NotNull sqlite3 db, @NotNull String functionName, int nArg, int eTextRep, @NotNull SQLFunction func ); - public static synchronized native int sqlite3_data_count( + public static native int sqlite3_data_count( @NotNull sqlite3_stmt stmt ); - public static synchronized native String sqlite3_db_filename( + public static native String sqlite3_db_filename( @NotNull sqlite3 db, @NotNull String dbName ); @@ -514,7 +555,7 @@ public final class SQLite3Jni { variadic arguments. Returns SQLITE_MISUSE if op is not one of the SQLITE_DBCONFIG_... options which uses this call form. */ - public static synchronized native int sqlite3_db_config( + public static native int sqlite3_db_config( @NotNull sqlite3 db, int op, int onOff, @Nullable OutputPointer.Int32 out ); @@ -525,43 +566,62 @@ public final class SQLite3Jni { SQLITE_DBCONFIG_MAINDBNAME, but that set of options may be extended in future versions. */ - public static synchronized native int sqlite3_db_config( + public static native int sqlite3_db_config( @NotNull sqlite3 db, int op, @NotNull String val ); - public static synchronized native int sqlite3_db_status( + public static native int sqlite3_db_status( @NotNull sqlite3 db, int op, @NotNull OutputPointer.Int32 pCurrent, @NotNull OutputPointer.Int32 pHighwater, boolean reset ); - public static synchronized native int sqlite3_errcode(@NotNull sqlite3 db); + public static native int sqlite3_errcode(@NotNull sqlite3 db); - public static synchronized native String sqlite3_expanded_sql(@NotNull sqlite3_stmt stmt); + public static native String sqlite3_expanded_sql(@NotNull sqlite3_stmt stmt); - public static synchronized native int sqlite3_extended_errcode(@NotNull sqlite3 db); + public static native int sqlite3_extended_errcode(@NotNull sqlite3 db); - public static synchronized native boolean sqlite3_extended_result_codes( + public static native boolean sqlite3_extended_result_codes( @NotNull sqlite3 db, boolean onoff ); - public static synchronized native String sqlite3_errmsg(@NotNull sqlite3 db); + public static native String sqlite3_errmsg(@NotNull sqlite3 db); - public static synchronized native String sqlite3_errstr(int resultCode); + public static native String sqlite3_errstr(int resultCode); /** Note that the offset values assume UTF-8-encoded SQL. */ - public static synchronized native int sqlite3_error_offset(@NotNull sqlite3 db); + public static native int sqlite3_error_offset(@NotNull sqlite3 db); - public static synchronized native int sqlite3_finalize(@NotNull sqlite3_stmt stmt); + public static native int sqlite3_finalize(@NotNull sqlite3_stmt stmt); - public static synchronized native int sqlite3_initialize(); + public static native int sqlite3_initialize(); - public static synchronized native long sqlite3_last_insert_rowid(@NotNull sqlite3 db); + /** + Design note/FIXME: we have a problem vis-a-vis 'synchronized' + here: we specifically want other threads to be able to cancel a + long-running thread, but this routine requires access to C-side + global state which does not have a mutex. Making this function + synchronized would make it impossible for a long-running job to + be cancelled from another thread. - public static synchronized native String sqlite3_libversion(); + The mutexing problem here is not within the core lib or Java, but + within the cached data held by the JNI binding. The cache holds + per-thread state, used by all but a tiny fraction of the JNI + binding layer, and access to that state needs to be + mutex-protected. + */ + public static native void sqlite3_interrupt(@NotNull sqlite3 db); - public static synchronized native int sqlite3_libversion_number(); + //! See sqlite3_interrupt() for threading concerns. + public static native boolean sqlite3_is_interrupted(@NotNull sqlite3 db); + + public static native long sqlite3_last_insert_rowid(@NotNull sqlite3 db); + + public static native String sqlite3_libversion(); + + public static native int sqlite3_libversion_number(); /** Works like its C counterpart and makes the native pointer of the @@ -575,26 +635,46 @@ public final class SQLite3Jni { non-null. Any error message about the failure will be in that object and it is up to the caller to sqlite3_close() that db handle. - - Pedantic note: though any number of Java-level sqlite3 objects - may refer to/wrap a single C-level (sqlite3*), the JNI internals - take a reference to the object which is passed to sqlite3_open() - or sqlite3_open_v2() so that they have a predictible object to - pass to, e.g., the sqlite3_collation_needed() callback. */ - public static synchronized native int sqlite3_open( + public static native int sqlite3_open( @Nullable String filename, @NotNull OutputPointer.sqlite3 ppDb ); - public static synchronized native int sqlite3_open_v2( + /** + Convenience overload which returns its db handle directly. The returned + object might not have been successfully opened: use sqlite3_errcode() to + check whether it is in an error state. + + Ownership of the returned value is passed to the caller, who must eventually + pass it to sqlite3_close() or sqlite3_close_v2(). + */ + public static sqlite3 sqlite3_open(@Nullable String filename){ + final OutputPointer.sqlite3 out = new OutputPointer.sqlite3(); + sqlite3_open(filename, out); + return out.take(); + }; + + public static native int sqlite3_open_v2( @Nullable String filename, @NotNull OutputPointer.sqlite3 ppDb, int flags, @Nullable String zVfs ); + /** + Has the same semantics as the sqlite3-returning sqlite3_open() + but uses sqlite3_open_v2() instead of sqlite3_open(). + */ + public static sqlite3 sqlite3_open_v2(@Nullable String filename, int flags, + @Nullable String zVfs){ + final OutputPointer.sqlite3 out = new OutputPointer.sqlite3(); + sqlite3_open_v2(filename, out, flags, zVfs); + return out.take(); + }; + /** The sqlite3_prepare() family of functions require slightly - different signatures than their native counterparts, but - overloading allows us to install several convenience forms. + different signatures than their native counterparts, but (A) they + retain functionally equivalent semantics and (B) overloading + allows us to install several convenience forms. All of them which take their SQL in the form of a byte[] require that it be in UTF-8 encoding unless explicitly noted otherwise. @@ -610,7 +690,7 @@ public final class SQLite3Jni { necessary, however, and overloads are provided which gloss over that. */ - private static synchronized native int sqlite3_prepare( + private static native int sqlite3_prepare( @NotNull sqlite3 db, @NotNull byte[] sqlUtf8, int maxBytes, @NotNull OutputPointer.sqlite3_stmt outStmt, @Nullable OutputPointer.Int32 pTailOffset @@ -639,7 +719,27 @@ public final class SQLite3Jni { return sqlite3_prepare(db, utf8, utf8.length, outStmt, null); } - private static synchronized native int sqlite3_prepare_v2( + /** + Convenience overload which returns its statement handle directly, + or null on error or when reading only whitespace or + comments. sqlite3_errcode() can be used to determine whether + there was an error or the input was empty. Ownership of the + returned object is passed to the caller, who must eventually pass + it to sqlite3_finalize(). + */ + public static sqlite3_stmt sqlite3_prepare( + @NotNull sqlite3 db, @NotNull String sql + ){ + final OutputPointer.sqlite3_stmt out = new OutputPointer.sqlite3_stmt(); + sqlite3_prepare(db, sql, out); + return out.take(); + } + + /** + See sqlite3_prepare() for details about the slight API differences + from the C API. + */ + private static native int sqlite3_prepare_v2( @NotNull sqlite3 db, @NotNull byte[] sqlUtf8, int maxBytes, @NotNull OutputPointer.sqlite3_stmt outStmt, @Nullable OutputPointer.Int32 pTailOffset @@ -668,7 +768,19 @@ public final class SQLite3Jni { return sqlite3_prepare_v2(db, utf8, utf8.length, outStmt, null); } - private static synchronized native int sqlite3_prepare_v3( + /** + Works identically to the sqlite3_stmt-returning sqlite3_prepare() + but uses sqlite3_prepare_v2(). + */ + public static sqlite3_stmt sqlite3_prepare_v2( + @NotNull sqlite3 db, @NotNull String sql + ){ + final OutputPointer.sqlite3_stmt out = new OutputPointer.sqlite3_stmt(); + sqlite3_prepare_v2(db, sql, out); + return out.take(); + } + + private static native int sqlite3_prepare_v3( @NotNull sqlite3 db, @NotNull byte[] sqlUtf8, int maxBytes, int prepFlags, @NotNull OutputPointer.sqlite3_stmt outStmt, @Nullable OutputPointer.Int32 pTailOffset @@ -697,22 +809,97 @@ public final class SQLite3Jni { return sqlite3_prepare_v3(db, utf8, utf8.length, prepFlags, outStmt, null); } - public static synchronized native void sqlite3_progress_handler( + /** + Works identically to the sqlite3_stmt-returning sqlite3_prepare() + but uses sqlite3_prepare_v3(). + */ + public static sqlite3_stmt sqlite3_prepare_v3( + @NotNull sqlite3 db, @NotNull String sql, int prepFlags + ){ + final OutputPointer.sqlite3_stmt out = new OutputPointer.sqlite3_stmt(); + sqlite3_prepare_v3(db, sql, prepFlags, out); + return out.take(); + } + + /** + If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined, this + acts as a proxy for C's sqlite3_preupdate_blobwrite(), else it returns + SQLITE_MISUSE with no side effects. + */ + public static native int sqlite3_preupdate_blobwrite(@NotNull sqlite3 db); + /** + If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined, this + acts as a proxy for C's sqlite3_preupdate_count(), else it returns + SQLITE_MISUSE with no side effects. + */ + public static native int sqlite3_preupdate_count(@NotNull sqlite3 db); + /** + If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined, this + acts as a proxy for C's sqlite3_preupdate_depth(), else it returns + SQLITE_MISUSE with no side effects. + */ + public static native int sqlite3_preupdate_depth(@NotNull sqlite3 db); + + /** + If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined, this + acts as a proxy for C's sqlite3_preupdate_hook(), else it returns null + with no side effects. + */ + public static native PreUpdateHook sqlite3_preupdate_hook(@NotNull sqlite3 db, + @Nullable PreUpdateHook hook); + + /** + If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined, + this acts as a proxy for C's sqlite3_preupdate_new(), else it + returns SQLITE_MISUSE with no side effects. + */ + public static native int sqlite3_preupdate_new(@NotNull sqlite3 db, int col, + @NotNull OutputPointer.sqlite3_value out); + + /** + Convenience wrapper for the 3-arg sqlite3_preupdate_new() which returns + null on error. + */ + public static sqlite3_value sqlite3_preupdate_new(@NotNull sqlite3 db, int col){ + final OutputPointer.sqlite3_value out = new OutputPointer.sqlite3_value(); + sqlite3_preupdate_new(db, col, out); + return out.take(); + } + + /** + If the C API was built with SQLITE_ENABLE_PREUPDATE_HOOK defined, + this acts as a proxy for C's sqlite3_preupdate_old(), else it + returns SQLITE_MISUSE with no side effects. + */ + public static native int sqlite3_preupdate_old(@NotNull sqlite3 db, int col, + @NotNull OutputPointer.sqlite3_value out); + + /** + Convenience wrapper for the 3-arg sqlite3_preupdate_old() which returns + null on error. + */ + public static sqlite3_value sqlite3_preupdate_old(@NotNull sqlite3 db, int col){ + final OutputPointer.sqlite3_value out = new OutputPointer.sqlite3_value(); + sqlite3_preupdate_old(db, col, out); + return out.take(); + } + + public static native void sqlite3_progress_handler( @NotNull sqlite3 db, int n, @Nullable ProgressHandler h ); //TODO??? void *sqlite3_preupdate_hook(...) and friends - public static synchronized native int sqlite3_reset(@NotNull sqlite3_stmt stmt); + public static native int sqlite3_reset(@NotNull sqlite3_stmt stmt); /** Works like the C API except that it has no side effects if auto extensions are currently running. (The JNI-level list of extensions cannot be manipulated while it is being traversed.) */ - public static synchronized native void sqlite3_reset_auto_extension(); + public static native void sqlite3_reset_auto_extension(); - public static synchronized native void sqlite3_result_double( + public static native void sqlite3_result_double( @NotNull sqlite3_context cx, double v ); @@ -723,7 +910,7 @@ public final class SQLite3Jni { results in the C-level sqlite3_result_error() being called with a complaint about the invalid argument. */ - private static synchronized native void sqlite3_result_error( + private static native void sqlite3_result_error( @NotNull sqlite3_context cx, @Nullable byte[] msg, int eTextRep ); @@ -766,27 +953,27 @@ public final class SQLite3Jni { sqlite3_result_error16(cx, e.getMessage()); } - public static synchronized native void sqlite3_result_error_toobig( + public static native void sqlite3_result_error_toobig( @NotNull sqlite3_context cx ); - public static synchronized native void sqlite3_result_error_nomem( + public static native void sqlite3_result_error_nomem( @NotNull sqlite3_context cx ); - public static synchronized native void sqlite3_result_error_code( + public static native void sqlite3_result_error_code( @NotNull sqlite3_context cx, int c ); - public static synchronized native void sqlite3_result_null( + public static native void sqlite3_result_null( @NotNull sqlite3_context cx ); - public static synchronized native void sqlite3_result_int( + public static native void sqlite3_result_int( @NotNull sqlite3_context cx, int v ); - public static synchronized native void sqlite3_result_int64( + public static native void sqlite3_result_int64( @NotNull sqlite3_context cx, long v ); @@ -806,7 +993,7 @@ public final class SQLite3Jni { Note that there is no sqlite3_bind_java_object() counterpart. */ - public static synchronized native void sqlite3_result_java_object( + public static native void sqlite3_result_java_object( @NotNull sqlite3_context cx, @NotNull Object o ); @@ -864,19 +1051,19 @@ public final class SQLite3Jni { sqlite3_result_text(cx, v); } - public static synchronized native void sqlite3_result_value( + public static native void sqlite3_result_value( @NotNull sqlite3_context cx, @NotNull sqlite3_value v ); - public static synchronized native void sqlite3_result_zeroblob( + public static native void sqlite3_result_zeroblob( @NotNull sqlite3_context cx, int n ); - public static synchronized native int sqlite3_result_zeroblob64( + public static native int sqlite3_result_zeroblob64( @NotNull sqlite3_context cx, long n ); - private static synchronized native void sqlite3_result_blob( + private static native void sqlite3_result_blob( @NotNull sqlite3_context cx, @Nullable byte[] blob, int maxLen ); @@ -896,7 +1083,7 @@ public final class SQLite3Jni { If maxLen is larger than blob.length, it is truncated to that value. If it is negative, results are undefined. */ - private static synchronized native void sqlite3_result_blob64( + private static native void sqlite3_result_blob64( @NotNull sqlite3_context cx, @Nullable byte[] blob, long maxLen ); @@ -906,7 +1093,7 @@ public final class SQLite3Jni { sqlite3_result_blob64(cx, blob, (long)(null==blob ? 0 : blob.length)); } - private static synchronized native void sqlite3_result_text( + private static native void sqlite3_result_text( @NotNull sqlite3_context cx, @Nullable byte[] text, int maxLen ); @@ -938,19 +1125,27 @@ public final class SQLite3Jni { If maxLength (in bytes, not characters) is larger than text.length, it is silently truncated to text.length. If it is - negative, results are undefined. + negative, results are undefined. If text is null, the following + arguments are ignored. */ - private static synchronized native void sqlite3_result_text64( + private static native void sqlite3_result_text64( @NotNull sqlite3_context cx, @Nullable byte[] text, long maxLength, int encoding ); - public static synchronized native int sqlite3_status( + /** + Cleans up all per-JNIEnv and per-db state managed by the library, + as well as any registered auto-extensions, then calls the + C-native sqlite3_shutdown(). + */ + public static synchronized native int sqlite3_shutdown(); + + public static native int sqlite3_status( int op, @NotNull OutputPointer.Int32 pCurrent, @NotNull OutputPointer.Int32 pHighwater, boolean reset ); - public static synchronized native int sqlite3_status64( + public static native int sqlite3_status64( int op, @NotNull OutputPointer.Int64 pCurrent, @NotNull OutputPointer.Int64 pHighwater, boolean reset ); @@ -1006,32 +1201,32 @@ public final class SQLite3Jni { sqlite3_result_text64(cx, b, b.length, SQLITE_UTF16BE); } - public static synchronized native RollbackHook sqlite3_rollback_hook( + public static native RollbackHook sqlite3_rollback_hook( @NotNull sqlite3 db, @Nullable RollbackHook hook ); //! Sets or unsets (if auth is null) the current authorizer. - public static synchronized native int sqlite3_set_authorizer( + public static native int sqlite3_set_authorizer( @NotNull sqlite3 db, @Nullable Authorizer auth ); - public static synchronized native void sqlite3_set_last_insert_rowid( + public static native void sqlite3_set_last_insert_rowid( @NotNull sqlite3 db, long rowid ); - public static synchronized native int sqlite3_sleep(int ms); + public static native int sqlite3_sleep(int ms); - public static synchronized native String sqlite3_sourceid(); + public static native String sqlite3_sourceid(); - public static synchronized native String sqlite3_sql(@NotNull sqlite3_stmt stmt); + public static native String sqlite3_sql(@NotNull sqlite3_stmt stmt); - public static synchronized native int sqlite3_step(@NotNull sqlite3_stmt stmt); + public static native int sqlite3_step(@NotNull sqlite3_stmt stmt); /** Internal impl of the public sqlite3_strglob() method. Neither argument may be NULL and both _MUST_ be NUL-terminated. */ - private static synchronized native int sqlite3_strglob( + private static native int sqlite3_strglob( @NotNull byte[] glob, @NotNull byte[] txt ); @@ -1048,7 +1243,7 @@ public final class SQLite3Jni { Internal impl of the public sqlite3_strlike() method. Neither argument may be NULL and both _MUST_ be NUL-terminated. */ - private static synchronized native int sqlite3_strlike( + private static native int sqlite3_strlike( @NotNull byte[] glob, @NotNull byte[] txt, int escChar ); @@ -1062,11 +1257,11 @@ public final class SQLite3Jni { ); } - public static synchronized native int sqlite3_threadsafe(); + public static native int sqlite3_threadsafe(); - public static synchronized native int sqlite3_total_changes(@NotNull sqlite3 db); + public static native int sqlite3_total_changes(@NotNull sqlite3 db); - public static synchronized native long sqlite3_total_changes64(@NotNull sqlite3 db); + public static native long sqlite3_total_changes64(@NotNull sqlite3 db); /** Works like C's sqlite3_trace_v2() except that the 3rd argument to that @@ -1078,33 +1273,33 @@ public final class SQLite3Jni { mapping state fails and SQLITE_ERROR if the given callback object cannot be processed propertly (i.e. an internal error). */ - public static synchronized native int sqlite3_trace_v2( + public static native int sqlite3_trace_v2( @NotNull sqlite3 db, int traceMask, @Nullable Tracer tracer ); - public static synchronized native UpdateHook sqlite3_update_hook( + public static native UpdateHook sqlite3_update_hook( sqlite3 db, UpdateHook hook ); - public static synchronized native byte[] sqlite3_value_blob(@NotNull sqlite3_value v); + public static native byte[] sqlite3_value_blob(@NotNull sqlite3_value v); - public static synchronized native int sqlite3_value_bytes(@NotNull sqlite3_value v); + public static native int sqlite3_value_bytes(@NotNull sqlite3_value v); - public static synchronized native int sqlite3_value_bytes16(@NotNull sqlite3_value v); + public static native int sqlite3_value_bytes16(@NotNull sqlite3_value v); - public static synchronized native double sqlite3_value_double(@NotNull sqlite3_value v); + public static native double sqlite3_value_double(@NotNull sqlite3_value v); - public static synchronized native sqlite3_value sqlite3_value_dupe( + public static native sqlite3_value sqlite3_value_dupe( @NotNull sqlite3_value v ); - public static synchronized native int sqlite3_value_encoding(@NotNull sqlite3_value v); + public static native int sqlite3_value_encoding(@NotNull sqlite3_value v); - public static synchronized native void sqlite3_value_free(@Nullable sqlite3_value v); + public static native void sqlite3_value_free(@Nullable sqlite3_value v); - public static synchronized native int sqlite3_value_int(@NotNull sqlite3_value v); + public static native int sqlite3_value_int(@NotNull sqlite3_value v); - public static synchronized native long sqlite3_value_int64(@NotNull sqlite3_value v); + public static native long sqlite3_value_int64(@NotNull sqlite3_value v); /** If the given value was set using sqlite3_result_java_value() then @@ -1113,7 +1308,7 @@ public final class SQLite3Jni { It is up to the caller to inspect the object to determine its type, and cast it if necessary. */ - public static synchronized native Object sqlite3_value_java_object( + public static native Object sqlite3_value_java_object( @NotNull sqlite3_value v ); @@ -1130,45 +1325,38 @@ public final class SQLite3Jni { } /** - See sqlite3_column_text() for notes about encoding conversions. - See sqlite3_value_text_utf8() for how to extract text in standard - UTF-8. + Returns the given value as UTF-8-encoded bytes, or null if the + underlying C API returns null for sqlite3_value_text(). */ - public static synchronized native String sqlite3_value_text(@NotNull sqlite3_value v); + public static native byte[] sqlite3_value_text_utf8(@NotNull sqlite3_value v); - /** - The sqlite3_value counterpart of sqlite3_column_text_utf8(). - */ - public static synchronized native byte[] sqlite3_value_text_utf8(@NotNull sqlite3_value v); + public static String sqlite3_value_text(@NotNull sqlite3_value v){ + final byte[] ba = sqlite3_value_text_utf8(v); + return null==ba ? null : new String(ba, StandardCharsets.UTF_8); + } - public static synchronized native byte[] sqlite3_value_text16(@NotNull sqlite3_value v); + public static native byte[] sqlite3_value_text16(@NotNull sqlite3_value v); - public static synchronized native byte[] sqlite3_value_text16le(@NotNull sqlite3_value v); + public static native byte[] sqlite3_value_text16le(@NotNull sqlite3_value v); - public static synchronized native byte[] sqlite3_value_text16be(@NotNull sqlite3_value v); + public static native byte[] sqlite3_value_text16be(@NotNull sqlite3_value v); - public static synchronized native int sqlite3_value_type(@NotNull sqlite3_value v); + public static native int sqlite3_value_type(@NotNull sqlite3_value v); - public static synchronized native int sqlite3_value_numeric_type(@NotNull sqlite3_value v); + public static native int sqlite3_value_numeric_type(@NotNull sqlite3_value v); - public static synchronized native int sqlite3_value_nochange(@NotNull sqlite3_value v); + public static native int sqlite3_value_nochange(@NotNull sqlite3_value v); - public static synchronized native int sqlite3_value_frombind(@NotNull sqlite3_value v); + public static native int sqlite3_value_frombind(@NotNull sqlite3_value v); - public static synchronized native int sqlite3_value_subtype(@NotNull sqlite3_value v); - - /** - Cleans up all per-JNIEnv and per-db state managed by the library - then calls the C-native sqlite3_shutdown(). - */ - public static synchronized native int sqlite3_shutdown(); + public static native int sqlite3_value_subtype(@NotNull sqlite3_value v); /** This is NOT part of the public API. It exists solely as a place to hook in arbitrary C-side code during development and testing of this library. */ - public static synchronized native void sqlite3_do_something_for_developer(); + public static native void sqlite3_do_something_for_developer(); ////////////////////////////////////////////////////////////////////// // SQLITE_... constants follow... @@ -1178,11 +1366,6 @@ public final class SQLite3Jni { public static final String SQLITE_VERSION = sqlite3_libversion(); public static final String SQLITE_SOURCE_ID = sqlite3_sourceid(); - //! Feature flags which are initialized at lib startup. Necessarily - // non-final so that lib init can fill out the proper values, - // but modifying them from client code has no effect. - public static boolean SQLITE_ENABLE_FTS5 = false; - // access public static final int SQLITE_ACCESS_EXISTS = 0; public static final int SQLITE_ACCESS_READWRITE = 1; diff --git a/ext/jni/src/org/sqlite/jni/Tester1.java b/ext/jni/src/org/sqlite/jni/Tester1.java index dca49faf66..cdbf860666 100644 --- a/ext/jni/src/org/sqlite/jni/Tester1.java +++ b/ext/jni/src/org/sqlite/jni/Tester1.java @@ -15,65 +15,116 @@ package org.sqlite.jni; import static org.sqlite.jni.SQLite3Jni.*; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; -public class Tester1 { +/** + An annotation for Tester1 tests which we do not want to run in + reflection-driven test mode because either they are not suitable + for multi-threaded threaded mode or we have to control their execution + order. +*/ +@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME) +@java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD}) +@interface ManualTest{} + +public class Tester1 implements Runnable { + //! True when running in multi-threaded mode. + private static boolean mtMode = false; + //! True to sleep briefly between tests. + private static boolean takeNaps = false; + //! True to shuffle the order of the tests. + private static boolean shuffle = false; + //! True to dump the list of to-run tests to stdout. + private static boolean listRunTests = false; + //! True to squelch all out() and outln() output. + private static boolean quietMode = false; + //! Total number of runTests() calls. + private static int nTestRuns = 0; + //! List of test*() methods to run. + private static List testMethods = null; + //! List of exceptions collected by run() + private static List listErrors = new ArrayList<>(); private static final class Metrics { - int dbOpen; + //! Number of times createNewDb() (or equivalent) is invoked. + volatile int dbOpen = 0; + } + + private Integer tId; + + Tester1(Integer id){ + tId = id; } static final Metrics metrics = new Metrics(); - private static final OutputPointer.sqlite3_stmt outStmt - = new OutputPointer.sqlite3_stmt(); - public static void out(Object val){ - System.out.print(val); + public synchronized static void outln(){ + if( !quietMode ){ + System.out.println(""); + } } - public static void outln(Object val){ - System.out.println(val); + public synchronized static void outln(Object val){ + if( !quietMode ){ + System.out.print(Thread.currentThread().getName()+": "); + System.out.println(val); + } + } + + public synchronized static void out(Object val){ + if( !quietMode ){ + System.out.print(val); + } } @SuppressWarnings("unchecked") - public static void out(Object... vals){ - int n = 0; - for(Object v : vals) out((n++>0 ? " " : "")+v); + public synchronized static void out(Object... vals){ + if( !quietMode ){ + System.out.print(Thread.currentThread().getName()+": "); + for(Object v : vals) out(v); + } } @SuppressWarnings("unchecked") - public static void outln(Object... vals){ - out(vals); out("\n"); + public synchronized static void outln(Object... vals){ + if( !quietMode ){ + out(vals); out("\n"); + } } - static int affirmCount = 0; - public static void affirm(Boolean v){ + static volatile int affirmCount = 0; + public synchronized static void affirm(Boolean v, String comment){ ++affirmCount; - assert( v /* prefer assert over exception if it's enabled because - the JNI layer sometimes has to suppress exceptions. */); - if( !v ) throw new RuntimeException("Assertion failed."); + if( false ) assert( v /* prefer assert over exception if it's enabled because + the JNI layer sometimes has to suppress exceptions, + so they might be squelched on their way back to the + top. */); + if( !v ) throw new RuntimeException(comment); } - private static void test1(){ - outln("libversion_number:", - sqlite3_libversion_number() - + "\n" - + sqlite3_libversion() - + "\n" - + SQLITE_SOURCE_ID); + public static void affirm(Boolean v){ + affirm(v, "Affirmation failed."); + } + + @ManualTest /* because testing this for threading is pointless */ + private void test1(){ affirm(sqlite3_libversion_number() == SQLITE_VERSION_NUMBER); - //outln("threadsafe = "+sqlite3_threadsafe()); affirm(SQLITE_MAX_LENGTH > 0); affirm(SQLITE_MAX_TRIGGER_DEPTH>0); } - public static sqlite3 createNewDb(){ + static sqlite3 createNewDb(){ final OutputPointer.sqlite3 out = new OutputPointer.sqlite3(); int rc = sqlite3_open(":memory:", out); ++metrics.dbOpen; sqlite3 db = out.take(); if( 0!=rc ){ - final String msg = db.getNativePointer()==0 - ? sqlite3_errstr(rc) - : sqlite3_errmsg(db); + final String msg = + null==db ? sqlite3_errstr(rc) : sqlite3_errmsg(db); + sqlite3_close(db); throw new RuntimeException("Opening db failed: "+msg); } affirm( null == out.get() ); @@ -83,17 +134,18 @@ public class Tester1 { return db; } - public static void execSql(sqlite3 db, String[] sql){ + static void execSql(sqlite3 db, String[] sql){ execSql(db, String.join("", sql)); } - public static int execSql(sqlite3 db, boolean throwOnError, String sql){ + static int execSql(sqlite3 db, boolean throwOnError, String sql){ OutputPointer.Int32 oTail = new OutputPointer.Int32(); final byte[] sqlUtf8 = sql.getBytes(StandardCharsets.UTF_8); int pos = 0, n = 1; byte[] sqlChunk = sqlUtf8; int rc = 0; sqlite3_stmt stmt = null; + final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt(); while(pos < sqlChunk.length){ if(pos > 0){ sqlChunk = Arrays.copyOfRange(sqlChunk, pos, @@ -127,21 +179,27 @@ public class Tester1 { return rc; } - public static void execSql(sqlite3 db, String sql){ + static void execSql(sqlite3 db, String sql){ execSql(db, true, sql); } - public static sqlite3_stmt prepare(sqlite3 db, String sql){ - outStmt.clear(); + static sqlite3_stmt prepare(sqlite3 db, boolean throwOnError, String sql){ + final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt(); int rc = sqlite3_prepare(db, sql, outStmt); - affirm( 0 == rc ); + if( throwOnError ){ + affirm( 0 == rc ); + } final sqlite3_stmt rv = outStmt.take(); affirm( null == outStmt.get() ); - affirm( 0 != rv.getNativePointer() ); + if( throwOnError ){ + affirm( 0 != rv.getNativePointer() ); + } return rv; } - - private static void testCompileOption(){ + static sqlite3_stmt prepare(sqlite3 db, String sql){ + return prepare(db, true, sql); + } + private void showCompileOption(){ int i = 0; String optName; outln("compile options:"); @@ -152,7 +210,7 @@ public class Tester1 { } - private static void testOpenDb1(){ + private void testOpenDb1(){ final OutputPointer.sqlite3 out = new OutputPointer.sqlite3(); int rc = sqlite3_open(":memory:", out); ++metrics.dbOpen; @@ -163,11 +221,18 @@ public class Tester1 { /* This function has different mangled names in jdk8 vs jdk19, and this call is here to ensure that the build fails if it cannot find both names. */; + + // These interrupt checks are only to make sure that the JNI binding + // has the proper exported symbol names. They don't actually test + // anything useful. + affirm( !sqlite3_is_interrupted(db) ); + sqlite3_interrupt(db); + affirm( sqlite3_is_interrupted(db) ); sqlite3_close_v2(db); affirm(0 == db.getNativePointer()); } - private static void testOpenDb2(){ + private void testOpenDb2(){ final OutputPointer.sqlite3 out = new OutputPointer.sqlite3(); int rc = sqlite3_open_v2(":memory:", out, SQLITE_OPEN_READWRITE @@ -180,14 +245,18 @@ public class Tester1 { affirm(0 == db.getNativePointer()); } - private static void testPrepare123(){ + private void testPrepare123(){ sqlite3 db = createNewDb(); int rc; + final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt(); rc = sqlite3_prepare(db, "CREATE TABLE t1(a);", outStmt); affirm(0 == rc); - sqlite3_stmt stmt = outStmt.get(); + sqlite3_stmt stmt = outStmt.take(); affirm(0 != stmt.getNativePointer()); rc = sqlite3_step(stmt); + if( SQLITE_DONE != rc ){ + outln("step failed ??? ",rc, " ",sqlite3_errmsg(db)); + } affirm(SQLITE_DONE == rc); sqlite3_finalize(stmt); affirm(0 == stmt.getNativePointer()); @@ -236,10 +305,20 @@ public class Tester1 { affirm(0 != stmt.getNativePointer()); sqlite3_finalize(stmt); affirm(0 == stmt.getNativePointer() ); + + affirm( 0==sqlite3_errcode(db) ); + stmt = sqlite3_prepare(db, "intentional error"); + affirm( null==stmt ); + affirm( 0!=sqlite3_errcode(db) ); + affirm( 0==sqlite3_errmsg(db).indexOf("near \"intentional\"") ); + sqlite3_finalize(stmt); + stmt = sqlite3_prepare(db, "/* empty input*/\n-- comments only"); + affirm( null==stmt ); + affirm( 0==sqlite3_errcode(db) ); sqlite3_close_v2(db); } - private static void testBindFetchInt(){ + private void testBindFetchInt(){ sqlite3 db = createNewDb(); execSql(db, "CREATE TABLE t(a)"); @@ -286,7 +365,7 @@ public class Tester1 { affirm(0 == db.getNativePointer()); } - private static void testBindFetchInt64(){ + private void testBindFetchInt64(){ sqlite3 db = createNewDb(); execSql(db, "CREATE TABLE t(a)"); sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);"); @@ -308,7 +387,7 @@ public class Tester1 { sqlite3_close_v2(db); } - private static void testBindFetchDouble(){ + private void testBindFetchDouble(){ sqlite3 db = createNewDb(); execSql(db, "CREATE TABLE t(a)"); sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);"); @@ -333,14 +412,17 @@ public class Tester1 { sqlite3_close_v2(db); } - private static void testBindFetchText(){ + private void testBindFetchText(){ sqlite3 db = createNewDb(); execSql(db, "CREATE TABLE t(a)"); sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);"); String[] list1 = { "hell🤩", "w😃rld", "!" }; int rc; + int n = 0; for( String e : list1 ){ - rc = sqlite3_bind_text(stmt, 1, e); + rc = (0==n) + ? sqlite3_bind_text(stmt, 1, e) + : sqlite3_bind_text16(stmt, 1, e); affirm(0 == rc); rc = sqlite3_step(stmt); affirm(SQLITE_DONE==rc); @@ -349,7 +431,7 @@ public class Tester1 { sqlite3_finalize(stmt); stmt = prepare(db, "SELECT a FROM t ORDER BY a DESC;"); StringBuilder sbuf = new StringBuilder(); - int n = 0; + n = 0; while( SQLITE_ROW == sqlite3_step(stmt) ){ String txt = sqlite3_column_text16(stmt, 0); //outln("txt = "+txt); @@ -362,7 +444,7 @@ public class Tester1 { sqlite3_close_v2(db); } - private static void testBindFetchBlob(){ + private void testBindFetchBlob(){ sqlite3 db = createNewDb(); execSql(db, "CREATE TABLE t(a)"); sqlite3_stmt stmt = prepare(db, "INSERT INTO t(a) VALUES(?);"); @@ -391,7 +473,7 @@ public class Tester1 { sqlite3_close_v2(db); } - private static void testSql(){ + private void testSql(){ sqlite3 db = createNewDb(); sqlite3_stmt stmt = prepare(db, "SELECT 1"); affirm( "SELECT 1".equals(sqlite3_sql(stmt)) ); @@ -400,9 +482,10 @@ public class Tester1 { sqlite3_bind_text(stmt, 1, "hell😃"); affirm( "SELECT 'hell😃'".equals(sqlite3_expanded_sql(stmt)) ); sqlite3_finalize(stmt); + sqlite3_close(db); } - private static void testCollation(){ + private void testCollation(){ final sqlite3 db = createNewDb(); execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')"); final ValueHolder xDestroyCalled = new ValueHolder<>(false); @@ -472,14 +555,13 @@ public class Tester1 { rc = sqlite3_collation_needed(db, null); affirm( 0 == rc ); sqlite3_close_v2(db); + affirm( 0 == db.getNativePointer() ); affirm(xDestroyCalled.value); } - private static void testToUtf8(){ + @ManualTest /* because threading is meaningless here */ + private void testToUtf8(){ /** - Java docs seem contradictory, claiming to use "modified UTF-8" - encoding while also claiming to export using RFC 2279: - https://docs.oracle.com/javase/8/docs/api/java/nio/charset/Charset.html Let's ensure that we can convert to standard UTF-8 in Java code @@ -489,7 +571,7 @@ public class Tester1 { affirm( 5 == ba.length /* as opposed to 6 in modified utf-8 */); } - private static void testStatus(){ + private void testStatus(){ final OutputPointer.Int64 cur64 = new OutputPointer.Int64(); final OutputPointer.Int64 high64 = new OutputPointer.Int64(); final OutputPointer.Int32 cur32 = new OutputPointer.Int32(); @@ -517,7 +599,7 @@ public class Tester1 { sqlite3_close_v2(db); } - private static void testUdf1(){ + private void testUdf1(){ final sqlite3 db = createNewDb(); // These ValueHolders are just to confirm that the func did what we want... final ValueHolder xDestroyCalled = new ValueHolder<>(false); @@ -561,7 +643,49 @@ public class Tester1 { affirm( xDestroyCalled.value ); } - private static void testUdfJavaObject(){ + private void testUdfThrows(){ + final sqlite3 db = createNewDb(); + final ValueHolder xFuncAccum = new ValueHolder<>(0); + + SQLFunction funcAgg = new SQLFunction.Aggregate(){ + @Override public void xStep(sqlite3_context cx, sqlite3_value[] args){ + /** Throwing from here should emit loud noise on stdout or stderr + but the exception is supressed because we have no way to inform + sqlite about it from these callbacks. */ + //throw new RuntimeException("Throwing from an xStep"); + } + @Override public void xFinal(sqlite3_context cx){ + throw new RuntimeException("Throwing from an xFinal"); + } + }; + int rc = sqlite3_create_function(db, "myagg", 1, SQLITE_UTF8, funcAgg); + affirm(0 == rc); + affirm(0 == xFuncAccum.value); + sqlite3_stmt stmt = prepare(db, "SELECT myagg(1)"); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + affirm( 0 != rc ); + affirm( sqlite3_errmsg(db).indexOf("an xFinal") > 0 ); + + SQLFunction funcSc = new SQLFunction.Scalar(){ + @Override public void xFunc(sqlite3_context cx, sqlite3_value[] args){ + throw new RuntimeException("Throwing from an xFunc"); + } + }; + rc = sqlite3_create_function(db, "mysca", 0, SQLITE_UTF8, funcSc); + affirm(0 == rc); + affirm(0 == xFuncAccum.value); + stmt = prepare(db, "SELECT mysca()"); + rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + affirm( 0 != rc ); + affirm( sqlite3_errmsg(db).indexOf("an xFunc") > 0 ); + rc = sqlite3_create_function(db, "mysca", 1, -1, funcSc); + affirm( SQLITE_FORMAT==rc, "invalid encoding value." ); + sqlite3_close_v2(db); + } + + private void testUdfJavaObject(){ final sqlite3 db = createNewDb(); final ValueHolder testResult = new ValueHolder<>(db); final SQLFunction func = new SQLFunction.Scalar(){ @@ -590,7 +714,7 @@ public class Tester1 { sqlite3_close_v2(db); } - private static void testUdfAggregate(){ + private void testUdfAggregate(){ final sqlite3 db = createNewDb(); final ValueHolder xFinalNull = // To confirm that xFinal() is called with no aggregate state @@ -652,7 +776,7 @@ public class Tester1 { sqlite3_close_v2(db); } - private static void testUdfWindow(){ + private void testUdfWindow(){ final sqlite3 db = createNewDb(); /* Example window function, table, and results taken from: https://sqlite.org/windowfunctions.html#udfwinfunc */ @@ -709,14 +833,14 @@ public class Tester1 { sqlite3_close_v2(db); } - private static void listBoundMethods(){ + private void listBoundMethods(){ if(false){ final java.lang.reflect.Field[] declaredFields = SQLite3Jni.class.getDeclaredFields(); outln("Bound constants:\n"); for(java.lang.reflect.Field field : declaredFields) { if(java.lang.reflect.Modifier.isStatic(field.getModifiers())) { - outln("\t"+field.getName()); + outln("\t",field.getName()); } } } @@ -735,12 +859,12 @@ public class Tester1 { java.util.Collections.sort(funcList); for(String n : funcList){ ++count; - outln("\t"+n+"()"); + outln("\t",n,"()"); } - outln(count+" functions named sqlite3_*."); + outln(count," functions named sqlite3_*."); } - private static void testTrace(){ + private void testTrace(){ final sqlite3 db = createNewDb(); final ValueHolder counter = new ValueHolder<>(0); /* Ensure that characters outside of the UTF BMP survive the trip @@ -789,9 +913,11 @@ public class Tester1 { affirm( 7 == counter.value ); } - private static void testBusy(){ + @ManualTest /* because threads inherently break this test */ + private void testBusy(){ final String dbName = "_busy-handler.db"; final OutputPointer.sqlite3 outDb = new OutputPointer.sqlite3(); + final OutputPointer.sqlite3_stmt outStmt = new OutputPointer.sqlite3_stmt(); int rc = sqlite3_open(dbName, outDb); ++metrics.dbOpen; @@ -839,7 +965,7 @@ public class Tester1 { } } - private static void testProgress(){ + private void testProgress(){ final sqlite3 db = createNewDb(); final ValueHolder counter = new ValueHolder<>(0); sqlite3_progress_handler(db, 1, new ProgressHandler(){ @@ -857,7 +983,7 @@ public class Tester1 { sqlite3_close_v2(db); } - private static void testCommitHook(){ + private void testCommitHook(){ final sqlite3 db = createNewDb(); final ValueHolder counter = new ValueHolder<>(0); final ValueHolder hookResult = new ValueHolder<>(0); @@ -906,12 +1032,13 @@ public class Tester1 { sqlite3_close_v2(db); } - private static void testUpdateHook(){ + private void testUpdateHook(){ final sqlite3 db = createNewDb(); final ValueHolder counter = new ValueHolder<>(0); final ValueHolder expectedOp = new ValueHolder<>(0); final UpdateHook theHook = new UpdateHook(){ @SuppressWarnings("unchecked") + @Override public void xUpdateHook(int opId, String dbName, String tableName, long rowId){ ++counter.value; if( 0!=expectedOp.value ){ @@ -955,7 +1082,84 @@ public class Tester1 { sqlite3_close_v2(db); } - private static void testRollbackHook(){ + /** + This test is functionally identical to testUpdateHook(), only with a + different callback type. + */ + private synchronized void testPreUpdateHook(){ + if( !sqlite3_compileoption_used("ENABLE_PREUPDATE_HOOK") ){ + //outln("Skipping testPreUpdateHook(): no pre-update hook support."); + return; + } + final sqlite3 db = createNewDb(); + final ValueHolder counter = new ValueHolder<>(0); + final ValueHolder expectedOp = new ValueHolder<>(0); + final PreUpdateHook theHook = new PreUpdateHook(){ + @SuppressWarnings("unchecked") + @Override + public void xPreUpdate(sqlite3 db, int opId, String dbName, String dbTable, + long iKey1, long iKey2 ){ + ++counter.value; + switch( opId ){ + case SQLITE_UPDATE: + affirm( 0 < sqlite3_preupdate_count(db) ); + affirm( null != sqlite3_preupdate_new(db, 0) ); + affirm( null != sqlite3_preupdate_old(db, 0) ); + break; + case SQLITE_INSERT: + affirm( null != sqlite3_preupdate_new(db, 0) ); + break; + case SQLITE_DELETE: + affirm( null != sqlite3_preupdate_old(db, 0) ); + break; + default: + break; + } + if( 0!=expectedOp.value ){ + affirm( expectedOp.value == opId ); + } + } + }; + PreUpdateHook oldHook = sqlite3_preupdate_hook(db, theHook); + affirm( null == oldHook ); + expectedOp.value = SQLITE_INSERT; + execSql(db, "CREATE TABLE t(a); INSERT INTO t(a) VALUES('a'),('b'),('c')"); + affirm( 3 == counter.value ); + expectedOp.value = SQLITE_UPDATE; + execSql(db, "update t set a='d' where a='c';"); + affirm( 4 == counter.value ); + oldHook = sqlite3_preupdate_hook(db, theHook); + affirm( theHook == oldHook ); + expectedOp.value = SQLITE_DELETE; + execSql(db, "DELETE FROM t where a='d'"); + affirm( 5 == counter.value ); + oldHook = sqlite3_preupdate_hook(db, null); + affirm( theHook == oldHook ); + execSql(db, "update t set a='e' where a='b';"); + affirm( 5 == counter.value ); + oldHook = sqlite3_preupdate_hook(db, null); + affirm( null == oldHook ); + + final PreUpdateHook newHook = new PreUpdateHook(){ + @Override + public void xPreUpdate(sqlite3 db, int opId, String dbName, + String tableName, long iKey1, long iKey2){ + } + }; + oldHook = sqlite3_preupdate_hook(db, newHook); + affirm( null == oldHook ); + execSql(db, "update t set a='h' where a='a'"); + affirm( 5 == counter.value ); + oldHook = sqlite3_preupdate_hook(db, theHook); + affirm( newHook == oldHook ); + expectedOp.value = SQLITE_UPDATE; + execSql(db, "update t set a='i' where a='h'"); + affirm( 6 == counter.value ); + + sqlite3_close_v2(db); + } + + private void testRollbackHook(){ final sqlite3 db = createNewDb(); final ValueHolder counter = new ValueHolder<>(0); final RollbackHook theHook = new RollbackHook(){ @@ -993,9 +1197,11 @@ public class Tester1 { it throws. */ @SuppressWarnings("unchecked") - private static void testFts5() throws Exception { - if( !SQLITE_ENABLE_FTS5 ){ - outln("SQLITE_ENABLE_FTS5 is not set. Skipping FTS5 tests."); + @ManualTest /* because the Fts5 parts are not yet known to be + thread-safe */ + private void testFts5() throws Exception { + if( !sqlite3_compileoption_used("ENABLE_FTS5") ){ + //outln("SQLITE_ENABLE_FTS5 is not set. Skipping FTS5 tests."); return; } Exception err = null; @@ -1021,7 +1227,7 @@ public class Tester1 { } } - private static void testAuthorizer(){ + private void testAuthorizer(){ final sqlite3 db = createNewDb(); final ValueHolder counter = new ValueHolder<>(0); final ValueHolder authRc = new ValueHolder<>(0); @@ -1043,7 +1249,9 @@ public class Tester1 { sqlite3_close(db); } - private static void testAutoExtension(){ + @ManualTest/* because multiple threads legitimately make these + results unpredictable */ + private synchronized void testAutoExtension(){ final ValueHolder val = new ValueHolder<>(0); final ValueHolder toss = new ValueHolder<>(null); final AutoExtension ax = new AutoExtension(){ @@ -1071,73 +1279,305 @@ public class Tester1 { affirm( 0==rc ); sqlite3_close( createNewDb() ); affirm( 3==val.value ); + + sqlite3 db = createNewDb(); + affirm( 4==val.value ); + execSql(db, "ATTACH ':memory' as foo"); + affirm( 4==val.value, "ATTACH uses the same connection, not sub-connections." ); + sqlite3_close(db); + db = null; + affirm( sqlite3_cancel_auto_extension(ax) ); affirm( !sqlite3_cancel_auto_extension(ax) ); sqlite3_close(createNewDb()); - affirm( 3==val.value ); + affirm( 4==val.value ); rc = sqlite3_auto_extension( ax ); affirm( 0==rc ); Exception err = null; toss.value = "Throwing from AutoExtension."; try{ - createNewDb(); + sqlite3_close(createNewDb()); }catch(Exception e){ err = e; } affirm( err!=null ); affirm( err.getMessage().indexOf(toss.value)>0 ); + toss.value = null; + + val.value = 0; + final AutoExtension ax2 = new AutoExtension(){ + public synchronized int xEntryPoint(sqlite3 db){ + ++val.value; + return 0; + } + }; + rc = sqlite3_auto_extension( ax2 ); + affirm( 0 == rc ); + sqlite3_close(createNewDb()); + affirm( 2 == val.value ); affirm( sqlite3_cancel_auto_extension(ax) ); + affirm( !sqlite3_cancel_auto_extension(ax) ); + sqlite3_close(createNewDb()); + affirm( 3 == val.value ); + rc = sqlite3_auto_extension( ax ); + affirm( 0 == rc ); + sqlite3_close(createNewDb()); + affirm( 5 == val.value ); + affirm( sqlite3_cancel_auto_extension(ax2) ); + affirm( !sqlite3_cancel_auto_extension(ax2) ); + sqlite3_close(createNewDb()); + affirm( 6 == val.value ); + rc = sqlite3_auto_extension( ax2 ); + affirm( 0 == rc ); + sqlite3_close(createNewDb()); + affirm( 8 == val.value ); + + sqlite3_reset_auto_extension(); + sqlite3_close(createNewDb()); + affirm( 8 == val.value ); + affirm( !sqlite3_cancel_auto_extension(ax) ); + affirm( !sqlite3_cancel_auto_extension(ax2) ); + sqlite3_close(createNewDb()); + affirm( 8 == val.value ); } - private static void testSleep(){ + @ManualTest /* because we only want to run this test manually */ + private void testSleep(){ out("Sleeping briefly... "); sqlite3_sleep(600); outln("Woke up."); } - public static void main(String[] args) throws Exception { - final long timeStart = System.nanoTime(); - test1(); - if(false) testCompileOption(); - final java.util.List liArgs = - java.util.Arrays.asList(args); - testOpenDb1(); - testOpenDb2(); - testPrepare123(); - testBindFetchInt(); - testBindFetchInt64(); - testBindFetchDouble(); - testBindFetchText(); - testBindFetchBlob(); - testSql(); - testCollation(); - testToUtf8(); - testStatus(); - testUdf1(); - testUdfJavaObject(); - testUdfAggregate(); - testUdfWindow(); - testTrace(); - testBusy(); - testProgress(); - testCommitHook(); - testRollbackHook(); - testUpdateHook(); - testAuthorizer(); - testFts5(); - testAutoExtension(); - //testSleep(); - if(liArgs.indexOf("-v")>0){ - sqlite3_do_something_for_developer(); - //listBoundMethods(); + private void nap() throws InterruptedException { + if( takeNaps ){ + Thread.sleep(java.util.concurrent.ThreadLocalRandom.current().nextInt(3, 17), 0); } - final long timeEnd = System.nanoTime(); - affirm( SQLite3Jni.uncacheJniEnv() ); - affirm( !SQLite3Jni.uncacheJniEnv() ); - outln("Tests done. Metrics:"); - outln("\tAssertions checked: "+affirmCount); - outln("\tDatabases opened: "+metrics.dbOpen); + } + @ManualTest /* because we only want to run this test on demand */ + private void testFail(){ + affirm( false, "Intentional failure." ); + } + + private void runTests(boolean fromThread) throws Exception { + if(false) showCompileOption(); + List mlist = testMethods; + affirm( null!=mlist ); + if( shuffle ){ + mlist = new ArrayList<>( testMethods.subList(0, testMethods.size()) ); + java.util.Collections.shuffle(mlist); + } + if( listRunTests ){ + synchronized(this.getClass()){ + if( !fromThread ){ + out("Initial test"," list: "); + for(java.lang.reflect.Method m : testMethods){ + out(m.getName()+" "); + } + outln(); + outln("(That list excludes some which are hard-coded to run.)"); + } + out("Running"," tests: "); + for(java.lang.reflect.Method m : mlist){ + out(m.getName()+" "); + } + outln(); + } + } + testToUtf8(); + test1(); + for(java.lang.reflect.Method m : mlist){ + nap(); + try{ + m.invoke(this); + }catch(java.lang.reflect.InvocationTargetException e){ + outln("FAILURE: ",m.getName(),"(): ", e.getCause()); + throw e; + } + } + if( !fromThread ){ + testBusy(); + if( !mtMode ){ + testAutoExtension() /* threads rightfully muck up these results */; + testFts5(); + } + } + synchronized( this.getClass() ){ + ++nTestRuns; + } + } + + public void run() { + try { + runTests(0!=this.tId); + }catch(Exception e){ + synchronized( listErrors ){ + listErrors.add(e); + } + }finally{ + affirm( SQLite3Jni.uncacheJniEnv() ); + affirm( !SQLite3Jni.uncacheJniEnv() ); + } + } + + /** + Runs the basic sqlite3 JNI binding sanity-check suite. + + CLI flags: + + -q|-quiet: disables most test output. + + -t|-thread N: runs the tests in N threads + concurrently. Default=1. + + -r|-repeat N: repeats the tests in a loop N times, each one + consisting of the -thread value's threads. + + -shuffle: randomizes the order of most of the test functions. + + -naps: sleep small random intervals between tests in order to add + some chaos for cross-thread contention. + + -list-tests: outputs the list of tests being run, minus some + which are hard-coded. This is noisy in multi-threaded mode. + + -fail: forces an exception to be thrown during the test run. Use + with -shuffle to make its appearance unpredictable. + + -v: emit some developer-mode info at the end. + */ + public static void main(String[] args) throws Exception { + Integer nThread = null; + boolean doSomethingForDev = false; + Integer nRepeat = 1; + boolean forceFail = false; + boolean sqlLog = false; + boolean squelchTestOutput = false; + for( int i = 0; i < args.length; ){ + String arg = args[i++]; + if(arg.startsWith("-")){ + arg = arg.replaceFirst("-+",""); + if(arg.equals("v")){ + doSomethingForDev = true; + //listBoundMethods(); + }else if(arg.equals("t") || arg.equals("thread")){ + nThread = Integer.parseInt(args[i++]); + }else if(arg.equals("r") || arg.equals("repeat")){ + nRepeat = Integer.parseInt(args[i++]); + }else if(arg.equals("shuffle")){ + shuffle = true; + }else if(arg.equals("list-tests")){ + listRunTests = true; + }else if(arg.equals("fail")){ + forceFail = true; + }else if(arg.equals("sqllog")){ + sqlLog = true; + }else if(arg.equals("naps")){ + takeNaps = true; + }else if(arg.equals("q") || arg.equals("quiet")){ + squelchTestOutput = true; + }else{ + throw new IllegalArgumentException("Unhandled flag:"+arg); + } + } + } + + { + // Build list of tests to run from the methods named test*(). + testMethods = new ArrayList<>(); + for(final java.lang.reflect.Method m : Tester1.class.getDeclaredMethods()){ + final String name = m.getName(); + if( name.equals("testFail") ){ + if( forceFail ){ + testMethods.add(m); + } + }else if( !m.isAnnotationPresent( ManualTest.class ) ){ + if( name.startsWith("test") ){ + testMethods.add(m); + } + } + } + } + + if( sqlLog ){ + if( sqlite3_compileoption_used("ENABLE_SQLLOG") ){ + int rc = sqlite3_config( new SQLLog() { + @Override public void xSqllog(sqlite3 db, String msg, int op){ + switch(op){ + case 0: outln("Opening db: ",db); break; + case 1: outln(db,": ",msg); break; + case 2: outln("Closing db: ",db); break; + } + } + }); + affirm( 0==rc ); + }else{ + outln("WARNING: -sqllog is not active because library was built ", + "without SQLITE_ENABLE_SQLLOG."); + } + } + + quietMode = squelchTestOutput; + outln("If you just saw warning messages regarding CallStaticObjectMethod, ", + "you are very likely seeing the side effects of a known openjdk8 ", + "bug. It is unsightly but does not affect the library."); + + final long timeStart = System.currentTimeMillis(); + int nLoop = 0; + affirm( 0==sqlite3_config( SQLITE_CONFIG_SINGLETHREAD ), + "Could not switch to single-thread mode." ); + affirm( 0==sqlite3_config( SQLITE_CONFIG_MULTITHREAD ), + "Could not switch to multithread mode." ); + affirm( 0==sqlite3_config( SQLITE_CONFIG_SERIALIZED ), + "Could not switch to serialized threading mode." ); + outln("libversion_number: ", + sqlite3_libversion_number(),"\n", + sqlite3_libversion(),"\n",SQLITE_SOURCE_ID); + outln("Running ",nRepeat," loop(s) with ",nThread," thread(s) each."); + if( takeNaps ) outln("Napping between tests is enabled."); + for( int n = 0; n < nRepeat; ++n ){ + if( nThread==null || nThread<=1 ){ + new Tester1(0).runTests(false); + continue; + } + Tester1.mtMode = true; + final ExecutorService ex = Executors.newFixedThreadPool( nThread ); + //final List> futures = new ArrayList<>(); + ++nLoop; + out((1==nLoop ? "" : " ")+nLoop); + for( int i = 0; i < nThread; ++i ){ + ex.submit( new Tester1(i), i ); + } + ex.shutdown(); + try{ + ex.awaitTermination(nThread*200, java.util.concurrent.TimeUnit.MILLISECONDS); + ex.shutdownNow(); + }catch (InterruptedException ie){ + ex.shutdownNow(); + Thread.currentThread().interrupt(); + } + if( !listErrors.isEmpty() ){ + quietMode = false; + outln("TEST ERRORS:"); + Exception err = null; + for( Exception e : listErrors ){ + e.printStackTrace(); + if( null==err ) err = e; + } + if( null!=err ) throw err; + } + } + outln(); + quietMode = false; + + final long timeEnd = System.currentTimeMillis(); + outln("Tests done. Metrics across ",nTestRuns," total iteration(s):"); + outln("\tAssertions checked: ",affirmCount); + outln("\tDatabases opened: ",metrics.dbOpen); + if( doSomethingForDev ){ + sqlite3_do_something_for_developer(); + } + sqlite3_shutdown(); int nMethods = 0; int nNatives = 0; final java.lang.reflect.Method[] declaredMethods = @@ -1154,10 +1594,10 @@ public class Tester1 { } } } - outln("\tSQLite3Jni sqlite3_*() methods: "+ + outln("\tSQLite3Jni.sqlite3_*() methods: "+ nNatives+" native methods and "+ (nMethods - nNatives)+" Java impls"); - outln("\tTotal time = " - +((timeEnd - timeStart)/1000000.0)+"ms"); + outln("\tTotal test time = " + +(timeEnd - timeStart)+"ms"); } } diff --git a/ext/jni/src/org/sqlite/jni/TesterFts5.java b/ext/jni/src/org/sqlite/jni/TesterFts5.java index 6439768e29..feb6d6303d 100644 --- a/ext/jni/src/org/sqlite/jni/TesterFts5.java +++ b/ext/jni/src/org/sqlite/jni/TesterFts5.java @@ -72,16 +72,18 @@ public class TesterFts5 { affirm( xDestroyCalled.value ); } - public TesterFts5(){ - int oldAffirmCount = Tester1.affirmCount; - Tester1.affirmCount = 0; - final long timeStart = System.nanoTime(); - test1(); - final long timeEnd = System.nanoTime(); - outln("FTS5 Tests done. Metrics:"); - outln("\tAssertions checked: "+Tester1.affirmCount); - outln("\tTotal time = " - +((timeEnd - timeStart)/1000000.0)+"ms"); - Tester1.affirmCount = oldAffirmCount; + public TesterFts5(boolean verbose){ + if(verbose){ + final long timeStart = System.currentTimeMillis(); + final int oldAffirmCount = Tester1.affirmCount; + test1(); + final int affirmCount = Tester1.affirmCount - oldAffirmCount; + final long timeEnd = System.currentTimeMillis(); + outln("FTS5 Tests done. Assertions checked = ",affirmCount, + ", Total time = ",(timeEnd - timeStart),"ms"); + }else{ + test1(); + } } + public TesterFts5(){ this(false); } } diff --git a/ext/jni/src/org/sqlite/jni/sqlite3_context.java b/ext/jni/src/org/sqlite/jni/sqlite3_context.java index a61ff21c7e..d582df7838 100644 --- a/ext/jni/src/org/sqlite/jni/sqlite3_context.java +++ b/ext/jni/src/org/sqlite/jni/sqlite3_context.java @@ -19,8 +19,7 @@ package org.sqlite.jni; */ public final class sqlite3_context extends NativePointerHolder { /** - For use only by the JNI layer. It's permitted to set this even - though it's private. + Only set by the JNI layer. */ private long aggregateContext = 0; diff --git a/ext/jni/src/org/sqlite/jni/tester/SQLTester.java b/ext/jni/src/org/sqlite/jni/tester/SQLTester.java index ffdb867d9b..ef3b839bc1 100644 --- a/ext/jni/src/org/sqlite/jni/tester/SQLTester.java +++ b/ext/jni/src/org/sqlite/jni/tester/SQLTester.java @@ -250,14 +250,14 @@ public class SQLTester { } public void runTests() throws Exception { - final long tStart = System.nanoTime(); + final long tStart = System.currentTimeMillis(); for(String f : listInFiles){ reset(); ++nTestFile; final TestScript ts = new TestScript(f); outln(nextStartEmoji(), " starting [",f,"]"); boolean threw = false; - final long timeStart = System.nanoTime(); + final long timeStart = System.currentTimeMillis(); try{ ts.run(this); }catch(SQLTesterException e){ @@ -267,14 +267,14 @@ public class SQLTester { if( keepGoing ) outln("Continuing anyway becaure of the keep-going option."); else if( e.isFatal() ) throw e; }finally{ - final long timeEnd = System.nanoTime(); + final long timeEnd = System.currentTimeMillis(); outln("🏁",(threw ? "❌" : "✅")," ",nTest," test(s) in ", - ((timeEnd-timeStart)/1000000.0),"ms."); + (timeEnd-timeStart),"ms."); //ts.getFilename()); } } - final long tEnd = System.nanoTime(); - outln("Total run-time: ",((tEnd-tStart)/1000000.0),"ms"); + final long tEnd = System.currentTimeMillis(); + outln("Total run-time: ",(tEnd-tStart),"ms"); Util.unlink(initialDbName); } diff --git a/manifest b/manifest index 1df7b6f2ee..b03cdd8592 100644 --- a/manifest +++ b/manifest @@ -1,5 +1,5 @@ -C Merge\sinto\strunk\simprovements\smade\sto\sthe\swasm\sAPIs\swhich\swere\stoo\slate\sfor\s3.43. -D 2023-08-24T14:43:30.216 +C Merge\sthe\sjni-threading\sbranch\sinto\strunk,\seliminating\sthe\sJNI\sAPI's\sprior\sthreading\slimitations. +D 2023-08-24T14:49:29.051 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 @@ -232,32 +232,34 @@ F ext/fts5/tool/showfts5.tcl d54da0e067306663e2d5d523965ca487698e722c F ext/icu/README.txt 7ab7ced8ae78e3a645b57e78570ff589d4c672b71370f5aa9e1cd7024f400fc9 F ext/icu/icu.c c074519b46baa484bb5396c7e01e051034da8884bad1a1cb7f09bbe6be3f0282 F ext/icu/sqliteicu.h fa373836ed5a1ee7478bdf8a1650689294e41d0c89c1daab26e9ae78a32075a8 -F ext/jni/GNUmakefile 3deba6bc0bf37c1ee5f15d1ff3c3512ae2f3cf44a2b8ae7b4af92690514b0cb4 -F ext/jni/README.md 5c60e4580aa5c94ff74d7bef1fb6231e578f7764e831a07b5981b6ab62b35560 -F ext/jni/jar-dist.make 93da95f8fe01ef22fccacc27f2e805938058e91e8c72c0532558d3a812a42e74 -F ext/jni/src/c/sqlite3-jni.c bea6b8691a5fa3a8626a771757bb261208d3c5fc6598266d3b0ee23d88e35632 -F ext/jni/src/c/sqlite3-jni.h 28565de9efc971195c684095ba0d184b90401290698c987f7ea3f54e47ff4f2f +F ext/jni/GNUmakefile 2e17aae8debf0b0ee12010500eae7bd9557f8ad5554f0161c39a41f229e84e3e +F ext/jni/README.md 64bf1da0d562d051207ca1c5cfa52e8b7a69120533cc034a3da7670ef920cbef +F ext/jni/jar-dist.make 030aaa4ae71dd86e4ec5e7c1e6cd86f9dfa47c4592c070d2e35157e42498e1fa +F ext/jni/src/c/sqlite3-jni.c d7d6d420f2a13d55828cee19ba17a37c4244532dafbc5822582d7fd52ae2aaf0 +F ext/jni/src/c/sqlite3-jni.h cc24d6742b29a52338ffd3b47caf923facb8ae77f9c2fc9c2de82673bf339ea2 F ext/jni/src/org/sqlite/jni/Authorizer.java 1308988f7f40579ea0e4deeaec3c6be971630566bd021c31367fe3f5140db892 -F ext/jni/src/org/sqlite/jni/AutoExtension.java 18e83f6f463e306df60b2dceb65247d32af1f78af4bbbae9155411a8c6cdb093 +F ext/jni/src/org/sqlite/jni/AutoExtension.java bcc1849b2fccbe5e2d7ac9e9ac7f8d05a6d7088a8fedbaad90e39569745a61e6 F ext/jni/src/org/sqlite/jni/BusyHandler.java 1b1d3e5c86cd796a0580c81b6af6550ad943baa25e47ada0dcca3aff3ebe978c F ext/jni/src/org/sqlite/jni/Collation.java 8dffbb00938007ad0967b2ab424d3c908413af1bbd3d212b9c9899910f1218d1 F ext/jni/src/org/sqlite/jni/CollationNeeded.java ad67843b6dd1c06b6b0a1dc72887b7c48e2a98042fcf6cacf14d42444037eab8 F ext/jni/src/org/sqlite/jni/CommitHook.java 87c6a8e5138c61a8eeff018fe16d23f29219150239746032687f245938baca1a -F ext/jni/src/org/sqlite/jni/Fts5.java 13844685231e8b4840a706db3bed84d5dfcf15be0ae7e809eac40420dba24901 +F ext/jni/src/org/sqlite/jni/Fts5.java a45cd890202d72c3bfe8aea69b57b02b6dd588361af81d8b921954c37940b2f7 F ext/jni/src/org/sqlite/jni/Fts5Context.java 0a5a02047a6a1dd3e4a38b0e542a8dd2de365033ba30e6ae019a676305959890 -F ext/jni/src/org/sqlite/jni/Fts5ExtensionApi.java 01f890105c6b7edbbad1c0f5635f783cea62c4b2ae694a71e76514a936ee03ec +F ext/jni/src/org/sqlite/jni/Fts5ExtensionApi.java 10cb2e0eb4dc5cf4241a7ccc0442a680f14a3ce6ecbb726552f2b5e026e521e0 F ext/jni/src/org/sqlite/jni/Fts5Function.java 65cde7151e441fee012250a5e03277de7babcd11a0c308a832b7940574259bcc F ext/jni/src/org/sqlite/jni/Fts5PhraseIter.java 6642beda341c0b1b46af4e2d7f6f9ab03a7aede43277b2c92859176d6bce3be9 F ext/jni/src/org/sqlite/jni/Fts5Tokenizer.java 91489893596b6528c0df5cd7180bd5b55809c26e2b797fb321dfcdbc1298c060 -F ext/jni/src/org/sqlite/jni/NativePointerHolder.java 9c5d901cce4f7e57c3d623f4e2476f9f79a8eed6e51b2a603f37866018e040ee -F ext/jni/src/org/sqlite/jni/OutputPointer.java d81f8bd43d2296ae373692370cfad16ddde76f5c14cd2760f7b4e1113ef56d4c +F ext/jni/src/org/sqlite/jni/NativePointerHolder.java 8110d4cfb20884e8ed241de7420c615b040a9f9c441d9cff06f34833399244a8 +F ext/jni/src/org/sqlite/jni/OutputPointer.java bb09fee5ad51d10e58075de000f8c1a3622a6c4b6a390ef134b6add1bfb32ca1 +F ext/jni/src/org/sqlite/jni/PreUpdateHook.java dec00a706b58c67989f0ff56c4f0a703821d25b45c62dd7fed1b462049f15c26 F ext/jni/src/org/sqlite/jni/ProgressHandler.java 6f62053a828a572de809828b1ee495380677e87daa29a1c57a0e2c06b0a131dc F ext/jni/src/org/sqlite/jni/ResultCode.java ba701f20213a5f259e94cfbfdd36eb7ac7ce7797f2c6c7fca2004ff12ce20f86 F ext/jni/src/org/sqlite/jni/RollbackHook.java b04c8abcc6ade44a8a57129e33765793f69df0ba909e49ba18d73f4268d92564 -F ext/jni/src/org/sqlite/jni/SQLFunction.java 09ce81c1c637e31c3a830d4c859cce95d65f5e02ff45f8bd1985b3479381bc46 -F ext/jni/src/org/sqlite/jni/SQLite3Jni.java 4b6fd22e04e63eb65d8e4e38fda39ecf15ce244d034607517627ce2e766e7e65 -F ext/jni/src/org/sqlite/jni/Tester1.java 4253dc7bcff64500a9388f1a17d3d39dbe4eb9d7db9fc035ce6e2380d45ad5fc -F ext/jni/src/org/sqlite/jni/TesterFts5.java 59e22dd24af033ea8827d36225a2f3297908fb6af8818ead8850c6c6847557b1 +F ext/jni/src/org/sqlite/jni/SQLFunction.java f697cf2a81c4119f2baf0682af689686f0466f1dd83dba00885f5603e693fe16 +F ext/jni/src/org/sqlite/jni/SQLLog.java c60610b35208416940822e834d61f08fbbe5d6e06b374b541b49e41fd56c9798 +F ext/jni/src/org/sqlite/jni/SQLite3Jni.java 956063c854c4f662183c41c65d0ab48b5e2127824b8053eeb05b9fc40f0d09e3 +F ext/jni/src/org/sqlite/jni/Tester1.java 9b6ec0ae299a56822e82e7dc2cf7ef1031ae87bcb595065bef84b7edac7114f5 +F ext/jni/src/org/sqlite/jni/TesterFts5.java 6f135c60e24c89e8eecb9fe61dde0f3bb2906de668ca6c9186bcf34bdaf94629 F ext/jni/src/org/sqlite/jni/Tracer.java a5cece9f947b0af27669b8baec300b6dd7ff859c3e6a6e4a1bd8b50f9714775d F ext/jni/src/org/sqlite/jni/UpdateHook.java e58645a1727f8a9bbe72dc072ec5b40d9f9362cb0aa24acfe93f49ff56a9016d F ext/jni/src/org/sqlite/jni/ValueHolder.java f022873abaabf64f3dd71ab0d6037c6e71cece3b8819fa10bf26a5461dc973ee @@ -265,10 +267,10 @@ F ext/jni/src/org/sqlite/jni/fts5_api.java 5198be71c162e3e0cb1f4962a7cdf0d7596e8 F ext/jni/src/org/sqlite/jni/fts5_extension_function.java ac825035d7d83fc7fd960347abfa6803e1614334a21533302041823ad5fc894c F ext/jni/src/org/sqlite/jni/fts5_tokenizer.java e530b36e6437fcc500e95d5d75fbffe272bdea20d2fac6be2e1336c578fba98b F ext/jni/src/org/sqlite/jni/sqlite3.java 62b1b81935ccf3393472d17cb883dc5ff39c388ec3bc1de547f098a0217158fc -F ext/jni/src/org/sqlite/jni/sqlite3_context.java d26573fc7b309228cb49786e9078597d96232257defa955a3425d10897bca810 +F ext/jni/src/org/sqlite/jni/sqlite3_context.java fe7797a696978f057528a57b7a11e7797ed41fd7afcf100c5ebb67055d9f706f F ext/jni/src/org/sqlite/jni/sqlite3_stmt.java 78e6d1b95ac600a9475e9db4623f69449322b0c93d1bd4e1616e76ed547ed9fc F ext/jni/src/org/sqlite/jni/sqlite3_value.java 3d1d4903e267bc0bc81d57d21f5e85978eff389a1a6ed46726dbe75f85e6914a -F ext/jni/src/org/sqlite/jni/tester/SQLTester.java 1f1286428fab38dfefe328e72b5735f533b19af8dd17712dd3df7e044d21c8b8 +F ext/jni/src/org/sqlite/jni/tester/SQLTester.java 2835eb3dd1e14767ca49354c224150c70300d8013d6d51dd875f7d9380faa278 F ext/jni/src/org/sqlite/jni/tester/test-script-interpreter.md f9f25126127045d051e918fe59004a1485311c50a13edbf18c79a6ff9160030e F ext/jni/src/tests/000-000-sanity.test cfe6dc1b950751d6096e3f5695becaadcdaa048bfe9567209d6eb676e693366d F ext/jni/src/tests/000-001-ignored.test e17e874c6ab3c437f1293d88093cf06286083b65bf162317f91bbfd92f961b70 @@ -2092,9 +2094,9 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93 F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 -P 0f80b798b3f4b81a7bb4233c58294edd0f1156f36b6ecf5ab8e83631d468778c b8f6a50a4bf9478324f0272d79f2fe6992a49b753e79e39a268c6afb261bb01e -R 7ae5b52e7df2292254903c2a58e5680f -T +closed b8f6a50a4bf9478324f0272d79f2fe6992a49b753e79e39a268c6afb261bb01e Closed\sby\sintegrate-merge. +P ac9da5c79a4f56d25202d50974e16e2a463d77c99b1907aee2605d5a3e50a565 1f46ba8d3bc61af771c1e33d09ad25f0da4fc4f915f7a9f6223ebfd99526d81d +R 77949ce12a43210a6d0344be95da4143 +T +closed 1f46ba8d3bc61af771c1e33d09ad25f0da4fc4f915f7a9f6223ebfd99526d81d Closed\sby\sintegrate-merge. U stephan -Z 43037d70eaecfc3116c1946ba3ee4fe8 +Z 8355fada3860b53acd7e17e529df543a # Remove this line to create a well-formed Fossil manifest. diff --git a/manifest.uuid b/manifest.uuid index 11b51edd1d..220fc74d64 100644 --- a/manifest.uuid +++ b/manifest.uuid @@ -1 +1 @@ -ac9da5c79a4f56d25202d50974e16e2a463d77c99b1907aee2605d5a3e50a565 \ No newline at end of file +3739c8aa7080d8e1044ca51ab7b699b50da4d29620b35acfcea2745059b65bac \ No newline at end of file