From 2f4cded2182d4e630032b8d0a3ccb031f0ed90e1 Mon Sep 17 00:00:00 2001 From: evgeny Date: Thu, 29 Oct 2026 12:01:09 +0100 Subject: [PATCH] Preserve event callback lifetime and contain JNI delivery exceptions --- .../jni_bridge/android_jni_bridge.c | 78 +++++++++++++------ .../jni_bridge/android_jni_bridge.h | 4 +- 2 files changed, 58 insertions(+), 24 deletions(-) diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.c b/tools/chatgui-android/jni_bridge/android_jni_bridge.c index 58605a0b..cdb2630f 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.c +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.c @@ -166,8 +166,9 @@ bool utun_bridge_init(const char* db_path, int control_port, bridge_log(BLEV_INFO, "bridge init: db=%s port=%d (stub, use nativeStart for real init)", db_path ? db_path : "(null)", control_port); - voice_recorder_init(db_path); - return true; + int result = voice_recorder_init(db_path); + if (result) bridge_log(BLEV_ERROR, "bridge init: voice recorder failed rc=%d", result); + return result == 0; } int utun_bridge_destroy(void) { @@ -1744,6 +1745,7 @@ static pthread_mutex_t g_log_callback_lock = PTHREAD_MUTEX_INITIALIZER; /* За static __thread int g_log_callback_active; /* Callback не может рекурсивно пересылать native-лог того же потока. */ static jobject g_event_callback = NULL; static jmethodID g_event_mid = NULL; +static pthread_mutex_t g_event_callback_lock = PTHREAD_MUTEX_INITIALIZER; enum bridge_query_kind { QUERY_CHANNELS, QUERY_MESSAGES, QUERY_MEMBERS, QUERY_DETAIL, QUERY_LINKS, QUERY_NODE, QUERY_SET_NICK, QUERY_OWNER, QUERY_MEMBERSHIP, QUERY_SETTING, QUERY_SOUND, QUERY_RADIO_SUBSCRIBERS, QUERY_RADIO_START }; @@ -1933,30 +1935,55 @@ static jstring jni_text(JNIEnv* env, const char* value) { /* ── JNI event callback (chat_event → Kotlin) ── */ static void jni_on_event(int type, const uint8_t* data, int len) { - if (!g_jvm) { bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: g_jvm=NULL"); return; } - if (!g_event_callback) { bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: g_event_callback=NULL"); return; } - if (!g_event_mid) { bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: g_event_mid=NULL"); return; } + if (!g_jvm || len < 0 || (len && !data)) { + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "JNI event: invalid input type=%d bytes=%d", type, len); return; + } JNIEnv* env = NULL; int attached = 0; + jobject callback = NULL; + jbyteArray jdata = NULL; jint res = (*g_jvm)->GetEnv(g_jvm, (void**)&env, JNI_VERSION_1_6); if (res == JNI_EDETACHED) { if ((*g_jvm)->AttachCurrentThread(g_jvm, &env, NULL) != JNI_OK) { - bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: AttachCurrentThread failed"); + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "JNI event: attach failed type=%d", type); return; } attached = 1; } else if (res != JNI_OK) { - bridge_log(BLEV_ERROR, "[EVT_DIAG] jni_on_event: GetEnv failed res=%d", (int)res); + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "JNI event: GetEnv failed rc=%d type=%d", res, type); return; } - jbyteArray jdata = NULL; - if (data && len > 0) { - jdata = (*env)->NewByteArray(env, len); - if (jdata) (*env)->SetByteArrayRegion(env, jdata, 0, len, (const jbyte*)data); + if ((*env)->ExceptionCheck(env)) { + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "JNI event: preserving existing exception type=%d", type); goto finish; } - (*env)->CallVoidMethod(env, g_event_callback, g_event_mid, (jint)type, jdata); + pthread_mutex_lock(&g_event_callback_lock); + jmethodID method = g_event_mid; + if (g_event_callback && method) callback = (*env)->NewLocalRef(env, g_event_callback); + pthread_mutex_unlock(&g_event_callback_lock); + if (!callback) { + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "JNI event: callback unavailable type=%d", type); goto cleanup; + } + if (len > 0) { + jdata = (*env)->NewByteArray(env, len); + if (!jdata) { + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "JNI event: payload allocation failed type=%d bytes=%d", type, len); goto cleanup; + } + (*env)->SetByteArrayRegion(env, jdata, 0, len, (const jbyte*)data); + if ((*env)->ExceptionCheck(env)) { + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "JNI event: payload copy failed type=%d bytes=%d", type, len); goto cleanup; + } + } + (*env)->CallVoidMethod(env, callback, method, (jint)type, jdata); + if ((*env)->ExceptionCheck(env)) { + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "JNI event: callback threw type=%d bytes=%d", type, len); + } +cleanup: + if ((*env)->ExceptionCheck(env)) { (*env)->ExceptionDescribe(env); (*env)->ExceptionClear(env); } if (jdata) (*env)->DeleteLocalRef(env, jdata); - if (attached) (*g_jvm)->DetachCurrentThread(g_jvm); + if (callback) (*env)->DeleteLocalRef(env, callback); +finish: + if (attached && (*g_jvm)->DetachCurrentThread(g_jvm) != JNI_OK) + __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "JNI event: detach failed type=%d", type); } void instance_event_handler(int type, const uint8_t* data, int len) { @@ -2214,15 +2241,22 @@ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeSendLocalInvi JNIEXPORT void JNICALL Java_com_utun_chat_data_NativeLib_nativeSetEventCallback( JNIEnv* env, jobject thiz, jobject callback) { (void)thiz; - if (g_event_callback) (*env)->DeleteGlobalRef(env, g_event_callback); - g_event_callback = callback ? (*env)->NewGlobalRef(env, callback) : NULL; - if (g_event_callback) { - jclass cls = (*env)->GetObjectClass(env, g_event_callback); - g_event_mid = (*env)->GetMethodID(env, cls, "onEvent", "(I[B)V"); - if (!g_event_mid) bridge_log(BLEV_ERROR, "onEvent method not found on callback"); - } else { - g_event_mid = NULL; + jobject next = callback ? (*env)->NewGlobalRef(env, callback) : NULL; + if (callback && !next) { bridge_log(BLEV_ERROR, "JNI event: callback allocation failed"); return; } + jmethodID method = NULL; + if (next) { + jclass cls = (*env)->GetObjectClass(env, next); + method = cls ? (*env)->GetMethodID(env, cls, "onEvent", "(I[B)V") : NULL; + if (cls) (*env)->DeleteLocalRef(env, cls); + if (!method) { + (*env)->DeleteGlobalRef(env, next); bridge_log(BLEV_ERROR, "JNI event: onEvent method lookup failed"); return; + } } + pthread_mutex_lock(&g_event_callback_lock); + jobject old = g_event_callback; g_event_callback = next; g_event_mid = method; + pthread_mutex_unlock(&g_event_callback_lock); + if (old) (*env)->DeleteGlobalRef(env, old); + bridge_log(BLEV_INFO, "JNI event callback configured=%d", next != NULL); } /* ── Pass-through JNI wrappers (unchanged from before) ── */ @@ -2240,7 +2274,7 @@ JNIEXPORT jboolean JNICALL Java_com_utun_chat_data_NativeLib_nativeDestroy( g_log_callback = NULL; pthread_mutex_unlock(&g_log_callback_lock); if (previous_log_callback) (*env)->DeleteGlobalRef(env, previous_log_callback); - if (g_event_callback) { (*env)->DeleteGlobalRef(env, g_event_callback); g_event_callback = NULL; } + Java_com_utun_chat_data_NativeLib_nativeSetEventCallback(env, thiz, NULL); return JNI_TRUE; } diff --git a/tools/chatgui-android/jni_bridge/android_jni_bridge.h b/tools/chatgui-android/jni_bridge/android_jni_bridge.h index dbd69487..11dc157a 100644 --- a/tools/chatgui-android/jni_bridge/android_jni_bridge.h +++ b/tools/chatgui-android/jni_bridge/android_jni_bridge.h @@ -47,8 +47,8 @@ struct utun_bridge_config { /* Сохранить callbacks, установить debug hook, инициализировать voice_recorder(db_path). * Таблицы callbacks копируются, config=NULL отключает прежний config-provider; функции/окружение остаются у caller. - * control_port только логируется: listener и ядро здесь не запускаются. Текущая реализация возвращает true - * независимо от результата voice_recorder_init; start нужен отдельно. */ + * control_port только логируется: listener и ядро здесь не запускаются. true — recorder инициализирован, + * false — его ошибка с логом; start нужен отдельно. Регистрация callbacks сохраняется для повторного init. */ bool utun_bridge_init(const char* db_path, int control_port, utun_log_fn on_log,