// Harness JNI sans VM Java : on dlopen libkazeia_tts.so, on récupère les Java_* // symbols, et on les appelle avec un JNIEnv mock minimal (juste les fonctions que // kazeia_tts_jni.cpp utilise : GetStringUTFChars, ReleaseStringUTFChars, NewIntArray, // SetIntArrayRegion). jstring est juste un alias opaque pour const char*. // // Permet de valider le bridge JNI sans installer l'app — utile pour CI / régression. #include #include #include #include // _exit #include #include #include namespace { // Pour notre mock, jstring = const char* tel quel (alias). const char * mock_GetStringUTFChars(JNIEnv*, jstring s, jboolean*) { return (const char*)s; } void mock_ReleaseStringUTFChars(JNIEnv*, jstring, const char*) {} // jintArray = pointeur vers une struct {int n; jint data[n]} qu'on alloue. struct IntArr { int n; jint data[1]; }; jintArray mock_NewIntArray(JNIEnv*, jsize len) { auto* a = (IntArr*)malloc(sizeof(int) + (size_t)len * sizeof(jint)); a->n = len; return (jintArray)a; } void mock_SetIntArrayRegion(JNIEnv*, jintArray arr, jsize start, jsize len, const jint* buf) { auto* a = (IntArr*)arr; memcpy(a->data + start, buf, (size_t)len * sizeof(jint)); } struct MockNativeInterface { // Les offsets et le layout doivent matcher jni.h JNINativeInterface. Plutôt que // de reproduire toute la struct, on alloue une zone large, et on pose les // function pointers AUX BONS OFFSETS (cf jni.h: GetStringUTFChars = entry 169, // ReleaseStringUTFChars = 170, NewIntArray = 187, SetIntArrayRegion = 207). // Pour rester portable, on utilise directement l'API via le struct JNINativeInterface // déclaré dans jni.h. JNINativeInterface iface{}; JNINativeInterface * ptr; MockNativeInterface() { // On laisse tout à nullptr, on remplit juste les méthodes utilisées. iface.GetStringUTFChars = mock_GetStringUTFChars; iface.ReleaseStringUTFChars = mock_ReleaseStringUTFChars; iface.NewIntArray = mock_NewIntArray; iface.SetIntArrayRegion = mock_SetIntArrayRegion; ptr = &iface; } }; } // namespace int main(int argc, char** argv) { if (argc < 6) { fprintf(stderr,"usage: %s \n", argv[0]); return 1; } const char* lib_path = argv[1]; const char* talker_gguf = argv[2]; const char* vocab_gguf = argv[3]; const char* dump_dir = argv[4]; const char* out_wav = argv[5]; void* dl = dlopen(lib_path, RTLD_NOW | RTLD_LOCAL); if (!dl) { fprintf(stderr, "dlopen FAIL %s : %s\n", lib_path, dlerror()); return 2; } using FnLoad = jlong (*)(JNIEnv*, jobject, jstring, jstring, jstring, jboolean, jint, jboolean); using FnSynth = jintArray (*)(JNIEnv*, jobject, jlong, jstring, jstring, jint, jint, jfloat, jint, jfloat, jfloat, jfloat, jint, jfloat, jfloat); using FnFree = void (*)(JNIEnv*, jobject, jlong); auto p_load = (FnLoad)dlsym(dl, "Java_com_kazeia_tts_TtsJni_nativeLoad"); auto p_synth = (FnSynth)dlsym(dl, "Java_com_kazeia_tts_TtsJni_nativeSynthesize"); auto p_free = (FnFree)dlsym(dl, "Java_com_kazeia_tts_TtsJni_nativeFree"); if (!p_load || !p_synth || !p_free) { fprintf(stderr, "dlsym FAIL : load=%p synth=%p free=%p\n", (void*)p_load, (void*)p_synth, (void*)p_free); return 3; } MockNativeInterface ifc; JNIEnv env_buf; env_buf.functions = ifc.ptr; JNIEnv* env = &env_buf; jobject self = nullptr; // pas utilisé dans nos handlers jlong h = p_load(env, self, (jstring)talker_gguf, (jstring)vocab_gguf, (jstring)dump_dir, /*useHtp=*/JNI_FALSE, /*nThreads=*/6, /*cpUseCache=*/JNI_TRUE); if (h == 0) { fprintf(stderr, "nativeLoad returned 0\n"); dlclose(dl); return 4; } fprintf(stderr,"nativeLoad OK -> handle=%p\n", (void*)(uintptr_t)h); // 2 appels : même phrase puis phrase différente. auto run = [&](const char* text, const char* out_path) { auto* arr = p_synth(env, self, h, (jstring)text, (jstring)out_path, /*maxSteps=*/256, /*seed=*/42, /*cpTemp=*/0.9f, /*cpTopK=*/50, /*cpTopP=*/1.0f, /*cpRepPenalty=*/1.05f, /*talkerTemp=*/0.9f, /*talkerTopK=*/50, /*talkerTopP=*/1.0f, /*talkerRepPenalty=*/1.05f); IntArr* a = (IntArr*)arr; fprintf(stderr,"[%s] err=%d frames=%d total=%dms (pf=%d talker=%d cp=%d dec=%d) -> %s\n", text, a->data[0], a->data[1], a->data[2], a->data[3], a->data[4], a->data[5], a->data[6], out_path); int err = a->data[0]; free(a); return err; }; std::string out1 = out_wav; out1 += ".1.wav"; std::string out2 = out_wav; out2 += ".2.wav"; int e1 = run("Bonjour je m'appelle Kazeia", out1.c_str()); int e2 = run("Bonsoir, comment tu te sens ce soir ?", out2.c_str()); p_free(env, self, h); fprintf(stderr,"OK e1=%d e2=%d\n", e1, e2); fflush(stderr); fflush(stdout); // Pas de dlclose ni return : destructors statiques de libllama/ggml peuvent segfaulter // après unmap (atexit/global dtors). En contexte app Android, la .so reste chargée // pour toute la vie du process, donc ce path n'existe pas. _exit court-circuite. _exit((e1 == 0 && e2 == 0) ? 0 : 5); }