1
0
mirror of https://github.com/sqlite/sqlite.git synced 2025-07-29 08:01:23 +03:00

JNI internal cleanups and correct two leaked db handles in test code.

FossilOrigin-Name: f927a30b5bba35991f472084ebaf02779e84c343a4e84f0efb3df7679ff212f8
This commit is contained in:
stephan
2023-08-22 17:36:59 +00:00
parent 9828aa223a
commit a7e3a1c09b
6 changed files with 122 additions and 148 deletions

View File

@ -348,15 +348,15 @@ enum {
typedef struct S3JniNphClass S3JniNphClass; typedef struct S3JniNphClass S3JniNphClass;
struct S3JniNphClass { struct S3JniNphClass {
volatile const S3NphRef * pRef /* Entry from S3NphRefs. */; volatile const S3NphRef * pRef /* Entry from S3NphRefs. */;
jclass klazz /* global ref to the concrete jclass klazz /* global ref to the concrete
NativePointerHolder subclass represented by ** NativePointerHolder subclass represented by
zClassName */; ** zClassName */;
volatile jmethodID midCtor /* klazz's no-arg constructor. Used by volatile jmethodID midCtor /* klazz's no-arg constructor. Used by
new_NativePointerHolder_object(). */; ** new_NativePointerHolder_object(). */;
volatile jfieldID fidValue /* NativePointerHolder.nativePointer or volatile jfieldID fidValue /* NativePointerHolder.nativePointer or
OutputPointer.T.value */; ** OutputPointer.T.value */;
volatile jfieldID fidAggCtx /* sqlite3_context::aggregateContext. Used only volatile jfieldID fidAggCtx /* sqlite3_context::aggregateContext. Used only
by the sqlite3_context binding. */; ** by the sqlite3_context binding. */;
}; };
/** State for various hook callbacks. */ /** State for various hook callbacks. */
@ -364,13 +364,13 @@ typedef struct S3JniHook S3JniHook;
struct S3JniHook{ struct S3JniHook{
jobject jObj /* global ref to Java instance */; jobject jObj /* global ref to Java instance */;
jmethodID midCallback /* callback method. Signature depends on jmethodID midCallback /* callback method. Signature depends on
jObj's type */; ** jObj's type */;
jclass klazz /* global ref to jObj's class. Only needed jclass klazz /* global ref to jObj's class. Only needed
by hooks which have an xDestroy() method. ** by hooks which have an xDestroy() method.
We can probably eliminate this and simply ** We can probably eliminate this and simply
do the class lookup at the same ** do the class lookup at the same
(deferred) time we do the xDestroy() ** (deferred) time we do the xDestroy()
lookup. */; ** lookup. */;
}; };
/* /*
@ -380,7 +380,10 @@ struct S3JniHook{
*/ */
typedef struct S3JniDb S3JniDb; typedef struct S3JniDb S3JniDb;
struct S3JniDb { struct S3JniDb {
JNIEnv *env /* The associated JNIEnv handle */; JNIEnv *env /* Used for cleaning up all dbs owned by a given
** thread, noting that this ownership is an artificial
** one imposed by our threading constraints, not by
** the core library. */;
sqlite3 *pDb /* The associated db handle */; sqlite3 *pDb /* The associated db handle */;
jobject jDb /* A global ref of the output object which gets jobject jDb /* A global ref of the output object which gets
returned from sqlite3_open(_v2)(). We need this in returned from sqlite3_open(_v2)(). We need this in
@ -528,6 +531,8 @@ struct S3JniGlobalType {
volatile unsigned nMutexPerDb /* number of times perDb.mutex was entered */; volatile unsigned nMutexPerDb /* number of times perDb.mutex was entered */;
volatile unsigned nMutexAutoExt /* number of times autoExt.mutex was entered */; volatile unsigned nMutexAutoExt /* number of times autoExt.mutex was entered */;
volatile unsigned nDestroy /* xDestroy() calls across all types */; volatile unsigned nDestroy /* xDestroy() calls across all types */;
volatile unsigned nPdbAlloc /* Number of S3JniDb alloced. */;
volatile unsigned nPdbRecycled /* Number of S3JniDb reused. */;
struct { struct {
/* Number of calls for each type of UDF callback. */ /* Number of calls for each type of UDF callback. */
volatile unsigned nFunc; volatile unsigned nFunc;
@ -689,6 +694,16 @@ static jbyteArray s3jni_new_jbyteArray(JNIEnv * const env, const unsigned char *
} }
return jba; return jba;
} }
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;
}
#define LocalJniGetEnv JNIEnv * const env = s3jni_get_env()
/** /**
Uses the java.lang.String(byte[],Charset) constructor to create a Uses the java.lang.String(byte[],Charset) constructor to create a
@ -705,7 +720,7 @@ static jbyteArray s3jni_new_jbyteArray(JNIEnv * const env, const unsigned char *
static jstring s3jni_utf8_to_jstring(S3JniEnv * const jc, static jstring s3jni_utf8_to_jstring(S3JniEnv * const jc,
const char * const z, int n){ const char * const z, int n){
jstring rv = NULL; jstring rv = NULL;
JNIEnv * const env = jc->env; LocalJniGetEnv;
if( 0==n || (n<0 && z && !z[0]) ){ if( 0==n || (n<0 && z && !z[0]) ){
/* Fast-track the empty-string case via the MUTF-8 API. We could /* 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 hypothetically do this for any strings where n<4 and z is
@ -741,7 +756,7 @@ static jstring s3jni_utf8_to_jstring(S3JniEnv * const jc,
*/ */
static char * s3jni_jstring_to_utf8(S3JniEnv * const jc, static char * s3jni_jstring_to_utf8(S3JniEnv * const jc,
jstring jstr, int *nLen){ jstring jstr, int *nLen){
JNIEnv * const env = jc->env; LocalJniGetEnv;
jbyteArray jba; jbyteArray jba;
jsize nBa; jsize nBa;
char *rv; char *rv;
@ -893,7 +908,7 @@ static void S3JniHook_unref(JNIEnv * const env, S3JniHook * const s, int doXDest
*/ */
static void S3JniDb_set_aside(S3JniDb * const s){ static void S3JniDb_set_aside(S3JniDb * const s){
if(s){ if(s){
JNIEnv * const env = s->env; LocalJniGetEnv;
MUTEX_PDB_ASSERT_LOCKED; MUTEX_PDB_ASSERT_LOCKED;
//MARKER(("state@%p for db@%p setting aside\n", s, s->pDb)); //MARKER(("state@%p for db@%p setting aside\n", s, s->pDb));
assert(s->pPrev != s); assert(s->pPrev != s);
@ -1087,8 +1102,6 @@ static S3JniDb * S3JniDb_alloc(JNIEnv * const env, sqlite3 *pDb,
MUTEX_PDB_ENTER; MUTEX_PDB_ENTER;
if(SJG.perDb.aFree){ if(SJG.perDb.aFree){
rv = SJG.perDb.aFree; rv = SJG.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));
SJG.perDb.aFree = rv->pNext; SJG.perDb.aFree = rv->pNext;
assert(rv->pNext != rv); assert(rv->pNext != rv);
assert(rv->pPrev != rv); assert(rv->pPrev != rv);
@ -1099,11 +1112,13 @@ static S3JniDb * S3JniDb_alloc(JNIEnv * const env, sqlite3 *pDb,
rv->pNext->pPrev = 0; rv->pNext->pPrev = 0;
rv->pNext = 0; rv->pNext = 0;
} }
++SJG.metrics.nPdbRecycled;
}else{ }else{
rv = s3jni_malloc(env, sizeof(S3JniDb)); rv = s3jni_malloc(env, sizeof(S3JniDb));
//MARKER(("state@%p for db allocating for db@%p from heap\n", rv, pDb)); //MARKER(("state@%p for db allocating for db@%p from heap\n", rv, pDb));
if(rv){ if(rv){
memset(rv, 0, sizeof(S3JniDb)); memset(rv, 0, sizeof(S3JniDb));
++SJG.metrics.nPdbAlloc;
} }
} }
if(rv){ if(rv){
@ -1357,7 +1372,7 @@ static int encodingTypeIsValid(int eTextRep){
static int CollationState_xCompare(void *pArg, int nLhs, const void *lhs, static int CollationState_xCompare(void *pArg, int nLhs, const void *lhs,
int nRhs, const void *rhs){ int nRhs, const void *rhs){
S3JniDb * const ps = pArg; S3JniDb * const ps = pArg;
JNIEnv * env = ps->env; LocalJniGetEnv;
jint rc = 0; jint rc = 0;
jbyteArray jbaLhs = (*env)->NewByteArray(env, (jint)nLhs); jbyteArray jbaLhs = (*env)->NewByteArray(env, (jint)nLhs);
jbyteArray jbaRhs = jbaLhs ? (*env)->NewByteArray(env, (jint)nRhs) : NULL; jbyteArray jbaRhs = jbaLhs ? (*env)->NewByteArray(env, (jint)nRhs) : NULL;
@ -1380,56 +1395,17 @@ static int CollationState_xCompare(void *pArg, int nLhs, const void *lhs,
/* Collation finalizer for use by the sqlite3 internals. */ /* Collation finalizer for use by the sqlite3 internals. */
static void CollationState_xDestroy(void *pArg){ static void CollationState_xDestroy(void *pArg){
S3JniDb * const ps = pArg; S3JniDb * const ps = pArg;
S3JniHook_unref( ps->env, &ps->collation, 1 ); S3JniHook_unref( s3jni_get_env(), &ps->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.
*/
typedef struct { 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; jobject jObj;
} ResultJavaVal; } ResultJavaVal;
@ -1439,7 +1415,6 @@ typedef struct {
static ResultJavaVal * ResultJavaVal_alloc(JNIEnv * const env, jobject jObj){ static ResultJavaVal * ResultJavaVal_alloc(JNIEnv * const env, jobject jObj){
ResultJavaVal * rv = sqlite3_malloc(sizeof(ResultJavaVal)); ResultJavaVal * rv = sqlite3_malloc(sizeof(ResultJavaVal));
if(rv){ if(rv){
rv->env = env;
rv->jObj = jObj ? REF_G(jObj) : 0; rv->jObj = jObj ? REF_G(jObj) : 0;
} }
return rv; return rv;
@ -1448,7 +1423,8 @@ static ResultJavaVal * ResultJavaVal_alloc(JNIEnv * const env, jobject jObj){
static void ResultJavaVal_finalizer(void *v){ static void ResultJavaVal_finalizer(void *v){
if(v){ if(v){
ResultJavaVal * const rv = (ResultJavaVal*)v; ResultJavaVal * const rv = (ResultJavaVal*)v;
if(rv->jObj) (*(rv->env))->DeleteGlobalRef(rv->env, rv->jObj); LocalJniGetEnv;
UNREF_G(rv->jObj);
sqlite3_free(rv); sqlite3_free(rv);
} }
} }
@ -1516,7 +1492,6 @@ typedef void (*udf_xFinal_f)(sqlite3_context*);
*/ */
typedef struct S3JniUdf S3JniUdf; typedef struct S3JniUdf S3JniUdf;
struct S3JniUdf { struct S3JniUdf {
JNIEnv * env; /* env registered from */;
jobject jObj /* SQLFunction instance */; jobject jObj /* SQLFunction instance */;
jclass klazz /* jObj's class */; jclass klazz /* jObj's class */;
char * zFuncName /* Only for error reporting and debug logging */; char * zFuncName /* Only for error reporting and debug logging */;
@ -1537,7 +1512,6 @@ static S3JniUdf * S3JniUdf_alloc(JNIEnv * const env, jobject jObj){
const char * zFV = /* signature for xFinal, xValue */ const char * zFV = /* signature for xFinal, xValue */
"(Lorg/sqlite/jni/sqlite3_context;)V"; "(Lorg/sqlite/jni/sqlite3_context;)V";
memset(s, 0, sizeof(S3JniUdf)); memset(s, 0, sizeof(S3JniUdf));
s->env = env;
s->jObj = REF_G(jObj); s->jObj = REF_G(jObj);
s->klazz = REF_G((*env)->GetObjectClass(env, jObj)); s->klazz = REF_G((*env)->GetObjectClass(env, jObj));
#define FGET(FuncName,FuncType,Field) \ #define FGET(FuncName,FuncType,Field) \
@ -1560,7 +1534,7 @@ static S3JniUdf * S3JniUdf_alloc(JNIEnv * const env, jobject jObj){
} }
static void S3JniUdf_free(S3JniUdf * s){ static void S3JniUdf_free(S3JniUdf * s){
JNIEnv * const env = s->env; LocalJniGetEnv;
if(env){ if(env){
//MARKER(("UDF cleanup: %s\n", s->zFuncName)); //MARKER(("UDF cleanup: %s\n", s->zFuncName));
s3jni_call_xDestroy(env, s->jObj, s->klazz); s3jni_call_xDestroy(env, s->jObj, s->klazz);
@ -1589,10 +1563,6 @@ typedef struct {
Converts the given (cx, argc, argv) into arguments for the given Converts the given (cx, argc, argv) into arguments for the given
UDF, placing the result in the final argument. Returns 0 on UDF, placing the result in the final argument. Returns 0 on
success, SQLITE_NOMEM on allocation error. 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, static int udf_args(JNIEnv *env,
sqlite3_context * const cx, sqlite3_context * const cx,
@ -1652,9 +1622,9 @@ static int udf_xFSI(sqlite3_context* pCx, int argc,
S3JniUdf * s, S3JniUdf * s,
jmethodID xMethodID, jmethodID xMethodID,
const char * zFuncType){ const char * zFuncType){
JNIEnv * const env = s->env; LocalJniGetEnv;
udf_jargs args = {0,0}; udf_jargs args = {0,0};
int rc = udf_args(s->env, pCx, argc, argv, &args.jcx, &args.jargv); int rc = udf_args(env, pCx, argc, argv, &args.jcx, &args.jargv);
//MARKER(("%s.%s() pCx = %p\n", s->zFuncName, zFuncType, pCx)); //MARKER(("%s.%s() pCx = %p\n", s->zFuncName, zFuncType, pCx));
if(rc) return rc; if(rc) return rc;
//MARKER(("UDF::%s.%s()\n", s->zFuncName, zFuncType)); //MARKER(("UDF::%s.%s()\n", s->zFuncName, zFuncType));
@ -1679,8 +1649,8 @@ static int udf_xFSI(sqlite3_context* pCx, int argc,
static int udf_xFV(sqlite3_context* cx, S3JniUdf * s, static int udf_xFV(sqlite3_context* cx, S3JniUdf * s,
jmethodID xMethodID, jmethodID xMethodID,
const char *zFuncType){ const char *zFuncType){
JNIEnv * const env = s->env; LocalJniGetEnv;
jobject jcx = new_sqlite3_context_wrapper(s->env, cx); jobject jcx = new_sqlite3_context_wrapper(env, cx);
int rc = 0; int rc = 0;
//MARKER(("%s.%s() cx = %p\n", s->zFuncName, zFuncType, cx)); //MARKER(("%s.%s() cx = %p\n", s->zFuncName, zFuncType, cx));
if(!jcx){ if(!jcx){
@ -1768,16 +1738,6 @@ WRAP_INT_SVALUE(1value_1numeric_1type, sqlite3_value_numeric_type)
WRAP_INT_SVALUE(1value_1subtype, sqlite3_value_subtype) WRAP_INT_SVALUE(1value_1subtype, sqlite3_value_subtype)
WRAP_INT_SVALUE(1value_1type, sqlite3_value_type) WRAP_INT_SVALUE(1value_1type, sqlite3_value_type)
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;
}
/* Central auto-extension handler. */ /* Central auto-extension handler. */
static int s3jni_run_java_auto_extensions(sqlite3 *pDb, const char **pzErr, static int s3jni_run_java_auto_extensions(sqlite3 *pDb, const char **pzErr,
const struct sqlite3_api_routines *ignored){ const struct sqlite3_api_routines *ignored){
@ -1958,7 +1918,7 @@ static int s3jni_busy_handler(void* pState, int n){
S3JniDb * const ps = (S3JniDb *)pState; S3JniDb * const ps = (S3JniDb *)pState;
int rc = 0; int rc = 0;
if( ps->busyHandler.jObj ){ if( ps->busyHandler.jObj ){
JNIEnv * const env = ps->env; LocalJniGetEnv;
rc = (*env)->CallIntMethod(env, ps->busyHandler.jObj, rc = (*env)->CallIntMethod(env, ps->busyHandler.jObj,
ps->busyHandler.midCallback, (jint)n); ps->busyHandler.midCallback, (jint)n);
IFTHREW{ IFTHREW{
@ -2079,7 +2039,7 @@ static unsigned int s3jni_utf16_strlen(void const * z){
static void s3jni_collation_needed_impl16(void *pState, sqlite3 *pDb, static void s3jni_collation_needed_impl16(void *pState, sqlite3 *pDb,
int eTextRep, const void * z16Name){ int eTextRep, const void * z16Name){
S3JniDb * const ps = pState; S3JniDb * const ps = pState;
JNIEnv * const env = ps->env; LocalJniGetEnv;
unsigned int const nName = s3jni_utf16_strlen(z16Name); unsigned int const nName = s3jni_utf16_strlen(z16Name);
jstring jName = (*env)->NewString(env, (jchar const *)z16Name, nName); jstring jName = (*env)->NewString(env, (jchar const *)z16Name, nName);
IFTHREW{ IFTHREW{
@ -2184,7 +2144,7 @@ JDECL(jobject,1column_1value)(JENV_CSELF, jobject jpStmt,
static int s3jni_commit_rollback_hook_impl(int isCommit, S3JniDb * const ps){ static int s3jni_commit_rollback_hook_impl(int isCommit, S3JniDb * const ps){
JNIEnv * const env = ps->env; LocalJniGetEnv;
int rc = isCommit int rc = isCommit
? (int)(*env)->CallIntMethod(env, ps->commitHook.jObj, ? (int)(*env)->CallIntMethod(env, ps->commitHook.jObj,
ps->commitHook.midCallback) ps->commitHook.midCallback)
@ -2749,7 +2709,7 @@ JDECL(jint,1prepare_1v3)(JNIEnv * const env, jclass self, jobject jDb, jbyteArra
static int s3jni_progress_handler_impl(void *pP){ static int s3jni_progress_handler_impl(void *pP){
S3JniDb * const ps = (S3JniDb *)pP; S3JniDb * const ps = (S3JniDb *)pP;
JNIEnv * const env = ps->env; LocalJniGetEnv;
int rc = (int)(*env)->CallIntMethod(env, ps->progress.jObj, int rc = (int)(*env)->CallIntMethod(env, ps->progress.jObj,
ps->progress.midCallback); ps->progress.midCallback);
IFTHREW{ IFTHREW{
@ -3001,7 +2961,7 @@ JDECL(jobject,1rollback_1hook)(JENV_CSELF, jobject jDb, jobject jHook){
static int s3jni_xAuth(void* pState, int op,const char*z0, const char*z1, static int s3jni_xAuth(void* pState, int op,const char*z0, const char*z1,
const char*z2,const char*z3){ const char*z2,const char*z3){
S3JniDb * const ps = pState; S3JniDb * const ps = pState;
JNIEnv * const env = ps->env; LocalJniGetEnv;
S3JniEnv * const jc = S3JniGlobal_env_cache(env); S3JniEnv * const jc = S3JniGlobal_env_cache(env);
S3JniHook const * const pHook = &ps->authHook; S3JniHook const * const pHook = &ps->authHook;
jstring const s0 = z0 ? s3jni_utf8_to_jstring(jc, z0, -1) : 0; jstring const s0 = z0 ? s3jni_utf8_to_jstring(jc, z0, -1) : 0;
@ -3119,7 +3079,7 @@ JDECL(jint,1shutdown)(JENV_CSELF){
s3jni_reset_auto_extension(env); s3jni_reset_auto_extension(env);
MUTEX_ENV_ENTER; MUTEX_ENV_ENTER;
while( SJG.envCache.aHead ){ while( SJG.envCache.aHead ){
S3JniGlobal_env_uncache( SJG.envCache.aHead->env ); S3JniGlobal_env_uncache( env );//SJG.envCache.aHead->env );
} }
MUTEX_ENV_LEAVE; MUTEX_ENV_LEAVE;
/* Do not clear S3JniGlobal.jvm: it's legal to call /* Do not clear S3JniGlobal.jvm: it's legal to call
@ -3151,7 +3111,7 @@ JDECL(jint,1step)(JENV_CSELF,jobject jStmt){
static int s3jni_trace_impl(unsigned traceflag, void *pC, void *pP, void *pX){ static int s3jni_trace_impl(unsigned traceflag, void *pC, void *pP, void *pX){
S3JniDb * const ps = (S3JniDb *)pC; S3JniDb * const ps = (S3JniDb *)pC;
JNIEnv * const env = ps->env; LocalJniGetEnv;
jobject jX = NULL /* the tracer's X arg */; jobject jX = NULL /* the tracer's X arg */;
jobject jP = NULL /* the tracer's P arg */; jobject jP = NULL /* the tracer's P arg */;
jobject jPUnref = NULL /* potentially a local ref to jP */; jobject jPUnref = NULL /* potentially a local ref to jP */;
@ -3230,7 +3190,7 @@ JDECL(jint,1trace_1v2)(JENV_CSELF,jobject jDb, jint traceMask, jobject jTracer){
static void s3jni_update_hook_impl(void * pState, int opId, const char *zDb, static void s3jni_update_hook_impl(void * pState, int opId, const char *zDb,
const char *zTable, sqlite3_int64 nRowid){ const char *zTable, sqlite3_int64 nRowid){
S3JniDb * const ps = pState; S3JniDb * const ps = pState;
JNIEnv * const env = ps->env; LocalJniGetEnv;
S3JniEnv * const jc = S3JniGlobal_env_cache(env); S3JniEnv * const jc = S3JniGlobal_env_cache(env);
jstring jDbName; jstring jDbName;
jstring jTable; jstring jTable;
@ -3423,6 +3383,10 @@ JDECL(void,1do_1something_1for_1developer)(JENV_CSELF){
"\n\tautoExt %u container access\n", "\n\tautoExt %u container access\n",
SJG.metrics.nMutexEnv, SJG.metrics.nMutexEnv2, SJG.metrics.nMutexEnv, SJG.metrics.nMutexEnv2,
SJG.metrics.nMutexPerDb, SJG.metrics.nMutexAutoExt); SJG.metrics.nMutexPerDb, SJG.metrics.nMutexAutoExt);
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:"); puts("Java-side UDF calls:");
#define UDF(T) printf("\t%-8s = %u\n", "x" #T, SJG.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); UDF(Func); UDF(Step); UDF(Final); UDF(Value); UDF(Inverse);
@ -3465,7 +3429,6 @@ JDECL(void,1do_1something_1for_1developer)(JENV_CSELF){
State for binding Java-side FTS5 auxiliary functions. State for binding Java-side FTS5 auxiliary functions.
*/ */
typedef struct { typedef struct {
JNIEnv * env; /* env registered from */;
jobject jObj /* functor instance */; jobject jObj /* functor instance */;
jclass klazz /* jObj's class */; jclass klazz /* jObj's class */;
jobject jUserData /* 2nd arg to JNI binding of jobject jUserData /* 2nd arg to JNI binding of
@ -3478,7 +3441,7 @@ typedef struct {
} Fts5JniAux; } Fts5JniAux;
static void Fts5JniAux_free(Fts5JniAux * const s){ static void Fts5JniAux_free(Fts5JniAux * const s){
JNIEnv * const env = s->env; LocalJniGetEnv;
if(env){ if(env){
/*MARKER(("FTS5 aux function cleanup: %s\n", s->zFuncName));*/ /*MARKER(("FTS5 aux function cleanup: %s\n", s->zFuncName));*/
s3jni_call_xDestroy(env, s->jObj, s->klazz); s3jni_call_xDestroy(env, s->jObj, s->klazz);
@ -3503,7 +3466,6 @@ static Fts5JniAux * Fts5JniAux_alloc(JNIEnv * const env, jobject jObj){
"Lorg/sqlite/jni/sqlite3_context;" "Lorg/sqlite/jni/sqlite3_context;"
"[Lorg/sqlite/jni/sqlite3_value;)V"; "[Lorg/sqlite/jni/sqlite3_value;)V";
memset(s, 0, sizeof(Fts5JniAux)); memset(s, 0, sizeof(Fts5JniAux));
s->env = env;
s->jObj = REF_G(jObj); s->jObj = REF_G(jObj);
s->klazz = REF_G((*env)->GetObjectClass(env, jObj)); s->klazz = REF_G((*env)->GetObjectClass(env, jObj));
s->jmid = (*env)->GetMethodID(env, s->klazz, "xFunction", zSig); s->jmid = (*env)->GetMethodID(env, s->klazz, "xFunction", zSig);
@ -3639,14 +3601,14 @@ static void s3jni_fts5_extension_function(Fts5ExtensionApi const *pApi,
int argc, int argc,
sqlite3_value **argv){ sqlite3_value **argv){
Fts5JniAux * const pAux = pApi->xUserData(pFts); Fts5JniAux * const pAux = pApi->xUserData(pFts);
JNIEnv *env;
jobject jpCx = 0; jobject jpCx = 0;
jobjectArray jArgv = 0; jobjectArray jArgv = 0;
jobject jpFts = 0; jobject jpFts = 0;
jobject jFXA; jobject jFXA;
int rc; int rc;
LocalJniGetEnv;
assert(pAux); assert(pAux);
env = pAux->env;
jFXA = s3jni_getFts5ExensionApi(env); jFXA = s3jni_getFts5ExensionApi(env);
if( !jFXA ) goto error_oom; if( !jFXA ) goto error_oom;
jpFts = new_Fts5Context_wrapper(env, pFts); jpFts = new_Fts5Context_wrapper(env, pFts);
@ -3700,8 +3662,11 @@ JDECLFtsApi(jint,xCreateFunction)(JENV_OSELF, jstring jName,
typedef struct S3JniFts5AuxData S3JniFts5AuxData; typedef struct S3JniFts5AuxData S3JniFts5AuxData;
/*
** TODO: this middle-man struct is no longer necessary. Conider
** removing it and passing around jObj itself instead.
*/
struct S3JniFts5AuxData { struct S3JniFts5AuxData {
JNIEnv *env;
jobject jObj; jobject jObj;
}; };
@ -3709,7 +3674,7 @@ static void S3JniFts5AuxData_xDestroy(void *x){
if(x){ if(x){
S3JniFts5AuxData * const p = x; S3JniFts5AuxData * const p = x;
if(p->jObj){ if(p->jObj){
JNIEnv *env = p->env; LocalJniGetEnv;
s3jni_call_xDestroy(env, p->jObj, 0); s3jni_call_xDestroy(env, p->jObj, 0);
UNREF_G(p->jObj); UNREF_G(p->jObj);
} }
@ -3872,7 +3837,7 @@ static int s3jni_xQueryPhrase(const Fts5ExtensionApi *xapi,
guaranteed to be the same one passed to xQueryPhrase(). If it's 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. */ not, we'll have to create a new wrapper object on every call. */
struct s3jni_xQueryPhraseState const * s = pData; struct s3jni_xQueryPhraseState const * s = pData;
JNIEnv * const env = s->env; LocalJniGetEnv;
int rc = (int)(*env)->CallIntMethod(env, s->jCallback, s->midCallback, int rc = (int)(*env)->CallIntMethod(env, s->jCallback, s->midCallback,
SJG.fts5.jFtsExt, s->jFcx); SJG.fts5.jFtsExt, s->jFcx);
IFTHREW{ IFTHREW{
@ -3931,7 +3896,6 @@ JDECLFtsXA(int,xSetAuxdata)(JENV_OSELF,jobject jCtx, jobject jAux){
} }
return SQLITE_NOMEM; return SQLITE_NOMEM;
} }
pAux->env = env;
pAux->jObj = REF_G(jAux); pAux->jObj = REF_G(jAux);
rc = fext->xSetAuxdata(PtrGet_Fts5Context(jCtx), pAux, rc = fext->xSetAuxdata(PtrGet_Fts5Context(jCtx), pAux,
S3JniFts5AuxData_xDestroy); S3JniFts5AuxData_xDestroy);
@ -3944,8 +3908,8 @@ JDECLFtsXA(int,xSetAuxdata)(JENV_OSELF,jobject jCtx, jobject jAux){
static int s3jni_xTokenize_xToken(void *p, int tFlags, const char* z, static int s3jni_xTokenize_xToken(void *p, int tFlags, const char* z,
int nZ, int iStart, int iEnd){ int nZ, int iStart, int iEnd){
int rc; int rc;
LocalJniGetEnv;
struct s3jni_xQueryPhraseState * const s = p; struct s3jni_xQueryPhraseState * const s = p;
JNIEnv * const env = s->env;
jbyteArray jba; jbyteArray jba;
if( s->tok.zPrev == z && s->tok.nPrev == nZ ){ if( s->tok.zPrev == z && s->tok.nPrev == nZ ){
jba = s->tok.jba; jba = s->tok.jba;

View File

@ -18,14 +18,23 @@ package org.sqlite.jni;
*/ */
public interface AutoExtension { public interface AutoExtension {
/** /**
Must function as described for the sqlite3_auto_extension(), Must function as described for a sqlite3_auto_extension()
with the caveat that the signature is more limited. callback, with the caveat that the signature is more limited.
As an exception (as it were) to the callbacks-must-not-throw As an exception (as it were) to the callbacks-must-not-throw
rule, AutoExtensions may do so and the exception's error message rule, AutoExtensions may throw and the exception's error message
will be set as the db's error string. will be set as the db's error string.
Results are undefined if db is closed by an auto-extension. Hints 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 is a good idea to make the
overridden method synchronized.
- Results are undefined if db is closed by an auto-extension.
*/ */
int xEntryPoint(sqlite3 db); int xEntryPoint(sqlite3 db);
} }

View File

@ -173,9 +173,6 @@ public final class SQLite3Jni {
on multiple factors). on multiple factors).
See the AutoExtension class docs for more information. 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 native int sqlite3_auto_extension(@NotNull AutoExtension callback); public static native int sqlite3_auto_extension(@NotNull AutoExtension callback);
@ -212,9 +209,11 @@ public final class SQLite3Jni {
); );
/** 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 A level of indirection required to ensure that the input to the
string. */ C-level function of the same name is a NUL-terminated UTF-8
string.
*/
private static native int sqlite3_bind_parameter_index( private static native int sqlite3_bind_parameter_index(
@NotNull sqlite3_stmt stmt, byte[] paramName @NotNull sqlite3_stmt stmt, byte[] paramName
); );
@ -226,6 +225,15 @@ public final class SQLite3Jni {
return sqlite3_bind_parameter_index(stmt, utf8); return sqlite3_bind_parameter_index(stmt, utf8);
} }
/**
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.
*/
private static native int sqlite3_bind_text(
@NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data, int maxBytes
);
public static int sqlite3_bind_text( public static int sqlite3_bind_text(
@NotNull sqlite3_stmt stmt, int ndx, @Nullable String data @NotNull sqlite3_stmt stmt, int ndx, @Nullable String data
){ ){
@ -242,15 +250,6 @@ public final class SQLite3Jni {
: sqlite3_bind_text(stmt, ndx, data, data.length); : sqlite3_bind_text(stmt, ndx, data, data.length);
} }
/**
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.
*/
private static native int sqlite3_bind_text(
@NotNull sqlite3_stmt stmt, int ndx, @Nullable byte[] data, int maxBytes
);
public static native int sqlite3_bind_zeroblob( public static native int sqlite3_bind_zeroblob(
@NotNull sqlite3_stmt stmt, int ndx, int n @NotNull sqlite3_stmt stmt, int ndx, int n
); );
@ -931,7 +930,8 @@ public final class SQLite3Jni {
If maxLength (in bytes, not characters) is larger than If maxLength (in bytes, not characters) is larger than
text.length, it is silently truncated to text.length. If it is 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 native void sqlite3_result_text64( private static native void sqlite3_result_text64(
@NotNull sqlite3_context cx, @Nullable byte[] text, @NotNull sqlite3_context cx, @Nullable byte[] text,
@ -939,8 +939,9 @@ public final class SQLite3Jni {
); );
/** /**
Cleans up all per-JNIEnv and per-db state managed by the library Cleans up all per-JNIEnv and per-db state managed by the library,
then calls the C-native sqlite3_shutdown(). as well as any registered auto-extensions, then calls the
C-native sqlite3_shutdown().
*/ */
public static synchronized native int sqlite3_shutdown(); public static synchronized native int sqlite3_shutdown();

View File

@ -87,9 +87,9 @@ public class Tester1 implements Runnable {
++metrics.dbOpen; ++metrics.dbOpen;
sqlite3 db = out.take(); sqlite3 db = out.take();
if( 0!=rc ){ if( 0!=rc ){
final String msg = db.getNativePointer()==0 final String msg =
? sqlite3_errstr(rc) null==db ? sqlite3_errstr(rc) : sqlite3_errmsg(db);
: sqlite3_errmsg(db); sqlite3_close(db);
throw new RuntimeException("Opening db failed: "+msg); throw new RuntimeException("Opening db failed: "+msg);
} }
affirm( null == out.get() ); affirm( null == out.get() );
@ -428,6 +428,7 @@ public class Tester1 implements Runnable {
sqlite3_bind_text(stmt, 1, "hell😃"); sqlite3_bind_text(stmt, 1, "hell😃");
affirm( "SELECT 'hell😃'".equals(sqlite3_expanded_sql(stmt)) ); affirm( "SELECT 'hell😃'".equals(sqlite3_expanded_sql(stmt)) );
sqlite3_finalize(stmt); sqlite3_finalize(stmt);
sqlite3_close(db);
} }
private void testCollation(){ private void testCollation(){
@ -500,14 +501,12 @@ public class Tester1 implements Runnable {
rc = sqlite3_collation_needed(db, null); rc = sqlite3_collation_needed(db, null);
affirm( 0 == rc ); affirm( 0 == rc );
sqlite3_close_v2(db); sqlite3_close_v2(db);
affirm( 0 == db.getNativePointer() );
affirm(xDestroyCalled.value); affirm(xDestroyCalled.value);
} }
private void testToUtf8(){ 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 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 Let's ensure that we can convert to standard UTF-8 in Java code
@ -1106,6 +1105,7 @@ public class Tester1 implements Runnable {
execSql(db, "ATTACH ':memory' as foo"); execSql(db, "ATTACH ':memory' as foo");
affirm( 4==val.value /* ATTACH uses the same connection, not sub-connections. */ ); affirm( 4==val.value /* ATTACH uses the same connection, not sub-connections. */ );
sqlite3_close(db); sqlite3_close(db);
db = null;
affirm( sqlite3_cancel_auto_extension(ax) ); affirm( sqlite3_cancel_auto_extension(ax) );
affirm( !sqlite3_cancel_auto_extension(ax) ); affirm( !sqlite3_cancel_auto_extension(ax) );
@ -1116,7 +1116,7 @@ public class Tester1 implements Runnable {
Exception err = null; Exception err = null;
toss.value = "Throwing from AutoExtension."; toss.value = "Throwing from AutoExtension.";
try{ try{
createNewDb(); sqlite3_close(createNewDb());
}catch(Exception e){ }catch(Exception e){
err = e; err = e;
} }
@ -1169,9 +1169,11 @@ public class Tester1 implements Runnable {
private void runTests(boolean fromThread) throws Exception { private void runTests(boolean fromThread) throws Exception {
if(false) testCompileOption(); if(false) testCompileOption();
testToUtf8();
test1(); test1();
testOpenDb1(); testOpenDb1();
testOpenDb2(); testOpenDb2();
testCollation();
testPrepare123(); testPrepare123();
testBindFetchInt(); testBindFetchInt();
testBindFetchInt64(); testBindFetchInt64();
@ -1179,8 +1181,6 @@ public class Tester1 implements Runnable {
testBindFetchText(); testBindFetchText();
testBindFetchBlob(); testBindFetchBlob();
testSql(); testSql();
testCollation();
testToUtf8();
testStatus(); testStatus();
testUdf1(); testUdf1();
testUdfJavaObject(); testUdfJavaObject();

View File

@ -1,5 +1,5 @@
C Move\sthe\sJNI\sper-thread\scache\sof\sNativePointerHolder\srefs\sinto\sglobal\sspace.\sThis\sallows\sbetter-targeted\smutex\slocks\sand\sincidentally\seliminates\sthe\slagginess\sand\spost-run\shangs\sin\sTester1's\smulti-thread\smode\s(presumably\scaused\sby\sdeadlocks). C JNI\sinternal\scleanups\sand\scorrect\stwo\sleaked\sdb\shandles\sin\stest\scode.
D 2023-08-22T15:30:35.368 D 2023-08-22T17:36:59.339
F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1 F .fossil-settings/empty-dirs dbb81e8fc0401ac46a1491ab34a7f2c7c0452f2f06b54ebb845d024ca8283ef1
F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea F .fossil-settings/ignore-glob 35175cdfcf539b2318cb04a9901442804be81cd677d8b889fcc9149c21f239ea
F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724 F LICENSE.md df5091916dbb40e6e9686186587125e1b2ff51f022cc334e886c19a0e9982724
@ -235,10 +235,10 @@ F ext/icu/sqliteicu.h fa373836ed5a1ee7478bdf8a1650689294e41d0c89c1daab26e9ae78a3
F ext/jni/GNUmakefile 30f0926a69edbd9e9932283ec8e4cea02b785f373395f2093dbbc6d65866a196 F ext/jni/GNUmakefile 30f0926a69edbd9e9932283ec8e4cea02b785f373395f2093dbbc6d65866a196
F ext/jni/README.md 975b35173debbbf3a4ab7166e14d2ffa2bacff9b6850414f09cc919805e81ba4 F ext/jni/README.md 975b35173debbbf3a4ab7166e14d2ffa2bacff9b6850414f09cc919805e81ba4
F ext/jni/jar-dist.make 9a03d10dbb5a74c724bfec4b76fd9e4c9865cbbc858d731cb48f38ac897d73a3 F ext/jni/jar-dist.make 9a03d10dbb5a74c724bfec4b76fd9e4c9865cbbc858d731cb48f38ac897d73a3
F ext/jni/src/c/sqlite3-jni.c ad002976687e294936ce8f50b9efb1e531fa1d78a076b8a09089328082b48af4 F ext/jni/src/c/sqlite3-jni.c b54056176060ef68dba31baaee43ad90a8ac3d4e7a477224377026110bb213ac
F ext/jni/src/c/sqlite3-jni.h 8b0ab1a3f0f92b75d4ff50db4a88b66a137cfb561268eb15bb3993ed174dbb74 F ext/jni/src/c/sqlite3-jni.h 8b0ab1a3f0f92b75d4ff50db4a88b66a137cfb561268eb15bb3993ed174dbb74
F ext/jni/src/org/sqlite/jni/Authorizer.java 1308988f7f40579ea0e4deeaec3c6be971630566bd021c31367fe3f5140db892 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 3b62c915e45ce73f63343ca9195ec63592244d616a1908b7587bdd45de1b97dd
F ext/jni/src/org/sqlite/jni/BusyHandler.java 1b1d3e5c86cd796a0580c81b6af6550ad943baa25e47ada0dcca3aff3ebe978c 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/Collation.java 8dffbb00938007ad0967b2ab424d3c908413af1bbd3d212b9c9899910f1218d1
F ext/jni/src/org/sqlite/jni/CollationNeeded.java ad67843b6dd1c06b6b0a1dc72887b7c48e2a98042fcf6cacf14d42444037eab8 F ext/jni/src/org/sqlite/jni/CollationNeeded.java ad67843b6dd1c06b6b0a1dc72887b7c48e2a98042fcf6cacf14d42444037eab8
@ -255,8 +255,8 @@ F ext/jni/src/org/sqlite/jni/ProgressHandler.java 6f62053a828a572de809828b1ee495
F ext/jni/src/org/sqlite/jni/ResultCode.java ba701f20213a5f259e94cfbfdd36eb7ac7ce7797f2c6c7fca2004ff12ce20f86 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/RollbackHook.java b04c8abcc6ade44a8a57129e33765793f69df0ba909e49ba18d73f4268d92564
F ext/jni/src/org/sqlite/jni/SQLFunction.java 8c1ad92c35bcc1b2f7256cf6e229b31340ed6d1a404d487f0a9adb28ba7fc332 F ext/jni/src/org/sqlite/jni/SQLFunction.java 8c1ad92c35bcc1b2f7256cf6e229b31340ed6d1a404d487f0a9adb28ba7fc332
F ext/jni/src/org/sqlite/jni/SQLite3Jni.java 5c469585946b63592cafe134b01af0b9144a12131f22ea352e12f4c3ec70efb2 F ext/jni/src/org/sqlite/jni/SQLite3Jni.java 0eea21f1015704e495b1a47aa9c7c90d081f51777981fe3f07760486aed092d8
F ext/jni/src/org/sqlite/jni/Tester1.java 63e1e4285a0f050580490323f656809bdadc0f1f28c4454f5cca82a6ccdfaf0f F ext/jni/src/org/sqlite/jni/Tester1.java b6be63a8e80c7362073f2a799719315a1459d1eff97cebb944b1309522758de2
F ext/jni/src/org/sqlite/jni/TesterFts5.java c729d5b3cb91888b7e2a3a3ef450852f184697df78721574f6c0bf9043e4b84c F ext/jni/src/org/sqlite/jni/TesterFts5.java c729d5b3cb91888b7e2a3a3ef450852f184697df78721574f6c0bf9043e4b84c
F ext/jni/src/org/sqlite/jni/Tracer.java a5cece9f947b0af27669b8baec300b6dd7ff859c3e6a6e4a1bd8b50f9714775d 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/UpdateHook.java e58645a1727f8a9bbe72dc072ec5b40d9f9362cb0aa24acfe93f49ff56a9016d
@ -2092,8 +2092,8 @@ F vsixtest/vsixtest.tcl 6a9a6ab600c25a91a7acc6293828957a386a8a93
F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc F vsixtest/vsixtest.vcxproj.data 2ed517e100c66dc455b492e1a33350c1b20fbcdc
F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e F vsixtest/vsixtest.vcxproj.filters 37e51ffedcdb064aad6ff33b6148725226cd608e
F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0 F vsixtest/vsixtest_TemporaryKey.pfx e5b1b036facdb453873e7084e1cae9102ccc67a0
P 7342bf578790e1a87c128a7c1c7745fe2e7c442890370feb160d406597d4d8ec P e209f56a9745695aadc04418c7bebe62b79e38e5aee26c3248a30f73bfa460c2
R 2848048c8acd75d9850655e629905695 R 187f43ad9bb697fc679503d683587e56
U stephan U stephan
Z 76a56828e8164766c79107d6b6e04f3f Z 1a62043be3662d747eb273c051b3d9b0
# Remove this line to create a well-formed Fossil manifest. # Remove this line to create a well-formed Fossil manifest.

View File

@ -1 +1 @@
e209f56a9745695aadc04418c7bebe62b79e38e5aee26c3248a30f73bfa460c2 f927a30b5bba35991f472084ebaf02779e84c343a4e84f0efb3df7679ff212f8