From 269d09815b9eadbdd61d51e491d651748cf43499 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 28 Oct 2026 12:00:43 +0100 Subject: [PATCH] Document JNI host checks and their platform requirements --- tools/chatgui-android/tests/jni_log/README.md | 34 +++++++++++++++++++ tools/chatgui-android/tests/jni_log/run.py | 7 ++-- 2 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 tools/chatgui-android/tests/jni_log/README.md diff --git a/tools/chatgui-android/tests/jni_log/README.md b/tools/chatgui-android/tests/jni_log/README.md new file mode 100644 index 000000000..bbe07ad5c --- /dev/null +++ b/tools/chatgui-android/tests/jni_log/README.md @@ -0,0 +1,34 @@ +# Проверка JNI-доставки логов + +Linux host, без телефона и запущенного ядра. Нужны Python 3, C99-компилятор, +заголовки OpenSSL и JDK с `include/jni.h`. Используется настоящий +`jni_bridge/android_jni_bridge.c`: неиспользуемые функции отсекает ELF linker. +JNI function tables моделируют JVM, локальные ссылки и принудительные отказы; +Android logcat заменён только проверяющим приёмником диагностики. + +Из корня репозитория: + +```sh +python3 tools/chatgui-android/tests/jni_log/run.py +# Явный JDK / другой компилятор: +CC=clang python3 tools/chatgui-android/tests/jni_log/run.py --java-home /path/to/jdk +``` + +В Android Gradle на Linux задача `:app:testJniLog` запускается перед JVM-тестами +приложения, включая `:app:testDebugUnitTest`. На других ОС эта host-задача пропускается. +Android `AttachCurrentThread` принимает `JNIEnv**`, JDK — `void**` с тем же ABI; +только это различие типов допускается host-компилятором. Настоящая NDK-сборка +проверяет Android-заголовки отдельно. Host-модели не включаются в APK. + +22 сценария проверяют GetEnv/attach/detach, получение callback/class/method, +создание/копирование массивов, исходный UTF-8 с кириллицей/emoji, освобождение +всех локальных ссылок и продолжение после ошибки. Отдельно проверяются +Java-исключение callback, сохранение чужого pending exception, подавление +рекурсии и удержание local ref при замене global callback. +Для операций, допускающих `NULL` без Java-исключения, проверяются оба отказа. + +Тест не запускает ART и не проверяет сборщик мусора Android; JVM-тест +`NativeLogContractTest` дополнительно сопоставляет C lookup с дескриптором +скомпилированного Kotlin `onNativeLog(Int, ByteArray?, ByteArray?)`. +Правила JNI: [Android JNI tips](https://developer.android.com/ndk/guides/jni-tips), +[JNI function specification](https://docs.oracle.com/en/java/javase/17/docs/specs/jni/functions.html). diff --git a/tools/chatgui-android/tests/jni_log/run.py b/tools/chatgui-android/tests/jni_log/run.py index 34f748ef5..bcb7a4fe0 100644 --- a/tools/chatgui-android/tests/jni_log/run.py +++ b/tools/chatgui-android/tests/jni_log/run.py @@ -3,6 +3,7 @@ import argparse import os from pathlib import Path +import shlex import shutil import subprocess import tempfile @@ -18,19 +19,19 @@ def main(): java_home = args.java_home or (Path(javac).resolve().parents[1] if javac else None) if java_home is None or not (java_home / "include/jni.h").is_file(): parser.error("JDK JNI headers missing; pass --java-home") - compiler = os.environ.get("CC", "cc") + compiler = shlex.split(os.environ.get("CC", "cc")) includes = [directory, java_home / "include", java_home / "include/linux", root / "src", root / "src/transport_layer", root / "src/routing_layer", root / "src/chat", root / "src/media_async", root / "lib"] with tempfile.TemporaryDirectory(prefix="utun-jni-log-") as output: binary = Path(output) / "test_jni_log" - command = [compiler, "-std=c99", "-D_DEFAULT_SOURCE", "-D__ANDROID__", "-DUSE_OPENSSL", + command = compiler + ["-std=c99", "-D_DEFAULT_SOURCE", "-D__ANDROID__", "-DUSE_OPENSSL", "-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections", "-pthread", # Android JNI принимает JNIEnv**, JDK — void**; ABI одинаков, чужие callbacks здесь не вызываются. "-Wno-incompatible-pointer-types"] command += [f"-I{path}" for path in includes] command += [str(directory / "test_jni_log.c"), "-o", str(binary)] - subprocess.run(command, check=True) + subprocess.run(command, check=True, timeout=90) subprocess.run([str(binary)], check=True, timeout=15)