Preserve event callback lifetime and contain JNI delivery exceptions

This commit is contained in:
evgeny committed 2026-10-29 13:35:41 +01:00
1 parent 17f79b16cd
commit 2f4cded218
2 files changed
+58 -24

No files matched your search

@@ -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;
}
@@ -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,