diff --git a/build.gradle.kts b/build.gradle.kts index 0ee5e15..20c034a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -184,6 +184,7 @@ val dockcrossOutputDir: Directory = project.layout.buildDirectory.get().dir("doc val nativeForHostOutputDir: Directory = dockcrossOutputDir.dir("host") val compileNativeForHost by tasks.registering(DockcrossRunTask::class) { baseConfigure(nativeForHostOutputDir, BuildTarget(image = null, family = "host", classifier = "host")) + extraEnv.put("BUILD_JNI_TESTS", "ON") unsafeWritableMountSource = true runner(NonContainerRunner) } diff --git a/conventions/src/main/kotlin/tel.schich.libdatachannel.convention.common.gradle.kts b/conventions/src/main/kotlin/tel.schich.libdatachannel.convention.common.gradle.kts index f3f8d1b..bb74dfa 100644 --- a/conventions/src/main/kotlin/tel.schich.libdatachannel.convention.common.gradle.kts +++ b/conventions/src/main/kotlin/tel.schich.libdatachannel.convention.common.gradle.kts @@ -44,6 +44,7 @@ dependencies { implementation(libs.slf4j) testImplementation(libs.junitJupiter) + testRuntimeOnly(libs.junitPlatformLauncher) testImplementation(libs.logbackClassic) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 932513b..f5c0c09 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ jniAccessGenerator = { module = "tel.schich:jni-access-generator", version.ref = jdtAnnotations = { module = "org.eclipse.jdt:org.eclipse.jdt.annotation", version.ref = "jdtAnnotations" } slf4j = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } logbackClassic = { module = "ch.qos.logback:logback-classic", version.ref = "logback" } +junitPlatformLauncher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junitJupiter" } junitJupiter = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junitJupiter" } [plugins] diff --git a/jni/CMakeLists.txt b/jni/CMakeLists.txt index 92b23f8..4b4d9c3 100644 --- a/jni/CMakeLists.txt +++ b/jni/CMakeLists.txt @@ -5,6 +5,7 @@ set(CMAKE_C_STANDARD 11) set(CMAKE_CXX_STANDARD 11) option(PROJECT_VERSION "The version of the project" "unspecified") +option(BUILD_JNI_TESTS "Build JNI test support" OFF) set(NO_WEBSOCKET ON CACHE BOOL "configure libdatachannel build") set(NO_MEDIA ON CACHE BOOL "configure libdatachannel build") @@ -54,6 +55,19 @@ add_library(datachannel-java SHARED src/util.c src/native_channel.c src/native_peer.c + src/peer_connection_lifecycle.c src/native_track.c src/callback.c) +if(BUILD_JNI_TESTS) + target_sources(datachannel-java PRIVATE test/thread_lifecycle.c) + + enable_testing() + find_package(Threads REQUIRED) + add_executable(datachannel-java-native-tests + test/native_lifecycle_test.c + src/init.c + src/peer_connection_lifecycle.c) + target_link_libraries(datachannel-java-native-tests PRIVATE Threads::Threads) + add_test(NAME datachannel-java-native-tests COMMAND datachannel-java-native-tests) +endif() target_link_libraries(datachannel-java PRIVATE datachannel-static) diff --git a/jni/build.sh b/jni/build.sh index 01e0761..68fba82 100755 --- a/jni/build.sh +++ b/jni/build.sh @@ -27,6 +27,7 @@ cmake_options=( "-DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=${MOUNT_SOURCE}/jni/cmake-conan/conan_provider.cmake" "-DPROJECT_VERSION=${PROJECT_VERSION}" "-DCMAKE_BUILD_TYPE=${PROJECT_BUILD_TYPE}" + "-DBUILD_JNI_TESTS=${BUILD_JNI_TESTS:-OFF}" ) if [ "$TARGET_FAMILY" = 'android' ] @@ -46,4 +47,8 @@ then fi cmake "$RELATIVE_PROJECT_PATH" "${cmake_options[@]}" -make -j"${JOBS:-1}" \ No newline at end of file +make -j"${JOBS:-1}" +if [ "${BUILD_JNI_TESTS:-OFF}" = 'ON' ] +then + ctest --output-on-failure +fi diff --git a/jni/src/init.c b/jni/src/init.c index ddf16ae..72154a5 100644 --- a/jni/src/init.c +++ b/jni/src/init.c @@ -3,40 +3,66 @@ #include #include #include +#include #define JNI_VERSION JNI_VERSION_1_6 -static JavaVM* global_JVM; +static pthread_mutex_t lifecycle_mutex = PTHREAD_MUTEX_INITIALIZER; +static JavaVM* global_jvm; static pthread_key_t thread_key; +static bool thread_key_initialized; +static bool jvm_unloading = true; -void detach_thread() { - JavaVM* jvm = pthread_getspecific(thread_key); +static void stop_jvm_access(void) { + pthread_mutex_lock(&lifecycle_mutex); + jvm_unloading = true; + global_jvm = NULL; + pthread_mutex_unlock(&lifecycle_mutex); +} + +static void delete_thread_key(void) { + pthread_mutex_lock(&lifecycle_mutex); + if (thread_key_initialized) { + pthread_key_delete(thread_key); + thread_key_initialized = false; + } + pthread_mutex_unlock(&lifecycle_mutex); +} + +static void detach_thread(void* value) { + JavaVM* jvm = value; if (jvm != NULL) { (*jvm)->DetachCurrentThread(jvm); } } -JNIEnv* get_jni_env_from_jvm(JavaVM* jvm) { - JNIEnv* env; +JNIEnv* get_jni_env(void) { + pthread_mutex_lock(&lifecycle_mutex); + if (global_jvm == NULL || jvm_unloading || !thread_key_initialized) { + pthread_mutex_unlock(&lifecycle_mutex); + return NULL; + } + + JNIEnv* env = NULL; + JavaVM* jvm = global_jvm; jint result = (*jvm)->GetEnv(jvm, (void**) &env, JNI_VERSION); if (result == JNI_EDETACHED) { result = (*jvm)->AttachCurrentThreadAsDaemon(jvm, (void**) &env, NULL); if (result == JNI_OK) { - pthread_setspecific(thread_key, jvm); + if (pthread_setspecific(thread_key, jvm) != 0) { + (*jvm)->DetachCurrentThread(jvm); + pthread_mutex_unlock(&lifecycle_mutex); + return NULL; + } } } - if (result != JNI_OK) { + if (result != JNI_OK || env == NULL) { + pthread_mutex_unlock(&lifecycle_mutex); return NULL; } - return env; -} -JNIEnv* get_jni_env() { - // make sure it's initialized - if (global_JVM == NULL) { - return NULL; - } - return get_jni_env_from_jvm(global_JVM); + pthread_mutex_unlock(&lifecycle_mutex); + return env; } void logger_callback(rtcLogLevel level, const char* message) { @@ -50,18 +76,52 @@ void logger_callback(rtcLogLevel level, const char* message) { } JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* jvm, void* reserved) { - pthread_key_create(&thread_key, detach_thread); - global_JVM = jvm; - JNIEnv* env = get_jni_env_from_jvm(jvm); + if (jvm == NULL || pthread_key_create(&thread_key, detach_thread) != 0) { + return JNI_ERR; + } + pthread_mutex_lock(&lifecycle_mutex); + thread_key_initialized = true; + pthread_mutex_unlock(&lifecycle_mutex); + + JNIEnv* env = NULL; + if ((*jvm)->GetEnv(jvm, (void**) &env, JNI_VERSION) != JNI_OK || env == NULL) { + delete_thread_key(); + return JNI_ERR; + } + module_OnLoad(env); + if ((*env)->ExceptionCheck(env)) { + module_OnUnload(env); + delete_thread_key(); + return JNI_ERR; + } + + pthread_mutex_lock(&lifecycle_mutex); + global_jvm = jvm; + jvm_unloading = false; + pthread_mutex_unlock(&lifecycle_mutex); + rtcInitLogger(RTC_LOG_VERBOSE, &logger_callback); rtcPreload(); + if ((*env)->ExceptionCheck(env)) { + stop_jvm_access(); + rtcInitLogger(RTC_LOG_NONE, NULL); + rtcCleanup(); + module_OnUnload(env); + delete_thread_key(); + return JNI_ERR; + } return JNI_VERSION; } JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* jvm, void* reserved) { + stop_jvm_access(); + rtcInitLogger(RTC_LOG_NONE, NULL); rtcCleanup(); - JNIEnv* env = get_jni_env(); - module_OnUnload(env); - global_JVM = NULL; -} \ No newline at end of file + + JNIEnv* env = NULL; + if (jvm != NULL && (*jvm)->GetEnv(jvm, (void**) &env, JNI_VERSION) == JNI_OK && env != NULL) { + module_OnUnload(env); + } + delete_thread_key(); +} diff --git a/jni/src/native_peer.c b/jni/src/native_peer.c index 9feb7b3..2b326af 100644 --- a/jni/src/native_peer.c +++ b/jni/src/native_peer.c @@ -1,4 +1,5 @@ #include "callback.h" +#include "peer_connection_lifecycle.h" #include "util.h" #include #include @@ -138,12 +139,7 @@ Java_tel_schich_libdatachannel_LibDataChannelNative_rtcClosePeerConnection(JNIEn JNIEXPORT jint JNICALL Java_tel_schich_libdatachannel_LibDataChannelNative_rtcDeletePeerConnection(JNIEnv* env, jclass clazz, jint peerHandle) { - struct jvm_callback* callback = rtcGetUserPointer(peerHandle); - if (callback != NULL) { - free_callback(env, callback); - } - - return rtcDeletePeerConnection(peerHandle); + return delete_peer_connection(env, peerHandle); } @@ -270,4 +266,4 @@ JNIEXPORT jint JNICALL Java_tel_schich_libdatachannel_LibDataChannelNative_setup rtcSetUserPointer(peerHandle, jvm_callback); return RTC_ERR_SUCCESS; -} \ No newline at end of file +} diff --git a/jni/src/peer_connection_lifecycle.c b/jni/src/peer_connection_lifecycle.c new file mode 100644 index 0000000..6cd2e5e --- /dev/null +++ b/jni/src/peer_connection_lifecycle.c @@ -0,0 +1,14 @@ +#include "peer_connection_lifecycle.h" + +#include "callback.h" +#include + +jint delete_peer_connection(JNIEnv* env, jint peer_handle) { + struct jvm_callback* callback = rtcGetUserPointer(peer_handle); + jint result = rtcDeletePeerConnection(peer_handle); + if (result == RTC_ERR_SUCCESS && callback != NULL) { + free_callback(env, callback); + } + + return result; +} diff --git a/jni/src/peer_connection_lifecycle.h b/jni/src/peer_connection_lifecycle.h new file mode 100644 index 0000000..d30fe81 --- /dev/null +++ b/jni/src/peer_connection_lifecycle.h @@ -0,0 +1,8 @@ +#ifndef LIBDATACHANNEL_JNI_PEER_CONNECTION_LIFECYCLE_H +#define LIBDATACHANNEL_JNI_PEER_CONNECTION_LIFECYCLE_H + +#include + +jint delete_peer_connection(JNIEnv* env, jint peer_handle); + +#endif//LIBDATACHANNEL_JNI_PEER_CONNECTION_LIFECYCLE_H diff --git a/jni/test/native_lifecycle_test.c b/jni/test/native_lifecycle_test.c new file mode 100644 index 0000000..59a6465 --- /dev/null +++ b/jni/test/native_lifecycle_test.c @@ -0,0 +1,362 @@ +#include "../src/callback.h" +#include "../src/global_jvm.h" +#include "../src/peer_connection_lifecycle.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* jvm, void* reserved); +JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* jvm, void* reserved); +void logger_callback(rtcLogLevel level, const char* message); + +enum lifecycle_event { + MODULE_LOAD, + MODULE_UNLOAD, + LOGGER_ENABLED, + LOGGER_DISABLED, + RTC_PRELOAD, + RTC_CLEANUP, +}; + +enum peer_delete_event { + GET_USER_POINTER, + DELETE_PEER, + FREE_CALLBACK, +}; + +static enum lifecycle_event lifecycle_events[16]; +static size_t lifecycle_event_count; +static enum peer_delete_event peer_delete_events[4]; +static size_t peer_delete_event_count; + +static pthread_t main_thread; +static jint main_get_env_result; +static bool exception_pending; +static bool fail_module_load; +static bool fail_rtc_preload; +static atomic_int attach_count; +static atomic_int detach_count; +static atomic_bool received_unexpected_vm; +static int java_log_count; + +static int configured_peer_handle; +static int configured_delete_result; +static struct jvm_callback* configured_callback; +static JNIEnv* freed_with_env; +static struct jvm_callback* freed_callback; + +static void record_lifecycle_event(enum lifecycle_event event) { + lifecycle_events[lifecycle_event_count++] = event; +} + +static void record_peer_delete_event(enum peer_delete_event event) { + peer_delete_events[peer_delete_event_count++] = event; +} + +static jboolean JNICALL mock_exception_check(JNIEnv* env) { + return exception_pending ? JNI_TRUE : JNI_FALSE; +} + +static const struct JNINativeInterface_ mock_native_interface = { + .ExceptionCheck = mock_exception_check, +}; +static JNIEnv mock_env = &mock_native_interface; + +static jint JNICALL mock_get_env(JavaVM* vm, void** env, jint version) { + if (!pthread_equal(pthread_self(), main_thread)) { + *env = NULL; + return JNI_EDETACHED; + } + if (main_get_env_result != JNI_OK) { + *env = NULL; + return main_get_env_result; + } + *env = &mock_env; + return JNI_OK; +} + +static jint JNICALL mock_attach_current_thread_as_daemon(JavaVM* vm, void** env, void* args) { + atomic_fetch_add(&attach_count, 1); + *env = &mock_env; + return JNI_OK; +} + +static JavaVM mock_vm; + +static jint JNICALL mock_detach_current_thread(JavaVM* vm) { + if (vm != &mock_vm) { + atomic_store(&received_unexpected_vm, true); + } + atomic_fetch_add(&detach_count, 1); + return JNI_OK; +} + +static const struct JNIInvokeInterface_ mock_invoke_interface = { + .DetachCurrentThread = mock_detach_current_thread, + .GetEnv = mock_get_env, + .AttachCurrentThreadAsDaemon = mock_attach_current_thread_as_daemon, +}; +static JavaVM mock_vm = &mock_invoke_interface; + +void module_OnLoad(JNIEnv* env) { + record_lifecycle_event(MODULE_LOAD); + if (fail_module_load) { + exception_pending = true; + } +} + +void module_OnUnload(JNIEnv* env) { + record_lifecycle_event(MODULE_UNLOAD); +} + +void call_tel_schich_libdatachannel_LibDataChannel_log_cstr(JNIEnv* env, jint level, const char* message) { + java_log_count++; +} + +void rtcInitLogger(rtcLogLevel level, rtcLogCallbackFunc callback) { + record_lifecycle_event(callback == NULL && level == RTC_LOG_NONE ? LOGGER_DISABLED : LOGGER_ENABLED); +} + +void rtcPreload(void) { + record_lifecycle_event(RTC_PRELOAD); + if (fail_rtc_preload) { + exception_pending = true; + } +} + +void rtcCleanup(void) { + record_lifecycle_event(RTC_CLEANUP); +} + +void* rtcGetUserPointer(int peer_handle) { + record_peer_delete_event(GET_USER_POINTER); + if (peer_handle != configured_peer_handle) { + return NULL; + } + return configured_callback; +} + +int rtcDeletePeerConnection(int peer_handle) { + record_peer_delete_event(DELETE_PEER); + return peer_handle == configured_peer_handle ? configured_delete_result : RTC_ERR_INVALID; +} + +void free_callback(JNIEnv* env, struct jvm_callback* callback) { + record_peer_delete_event(FREE_CALLBACK); + freed_with_env = env; + freed_callback = callback; +} + +static void reset_mocks(void) { + memset(lifecycle_events, 0, sizeof(lifecycle_events)); + lifecycle_event_count = 0; + memset(peer_delete_events, 0, sizeof(peer_delete_events)); + peer_delete_event_count = 0; + main_thread = pthread_self(); + main_get_env_result = JNI_OK; + exception_pending = false; + fail_module_load = false; + fail_rtc_preload = false; + atomic_store(&attach_count, 0); + atomic_store(&detach_count, 0); + atomic_store(&received_unexpected_vm, false); + java_log_count = 0; + configured_peer_handle = 42; + configured_delete_result = RTC_ERR_SUCCESS; + configured_callback = NULL; + freed_with_env = NULL; + freed_callback = NULL; +} + +#define CHECK(condition, message) \ + do { \ + if (!(condition)) { \ + fprintf(stderr, "%s:%d: %s\n", __FILE__, __LINE__, message); \ + return false; \ + } \ + } while (false) + +static bool events_equal(const enum lifecycle_event* expected, size_t expected_count) { + return lifecycle_event_count == expected_count + && memcmp(lifecycle_events, expected, expected_count * sizeof(*expected)) == 0; +} + +static bool peer_delete_events_equal(const enum peer_delete_event* expected, size_t expected_count) { + return peer_delete_event_count == expected_count + && memcmp(peer_delete_events, expected, expected_count * sizeof(*expected)) == 0; +} + +static bool rejects_missing_jvm(void) { + reset_mocks(); + + CHECK(JNI_OnLoad(NULL, NULL) == JNI_ERR, "JNI_OnLoad should reject a missing JVM"); + CHECK(lifecycle_event_count == 0, "a rejected load should not initialize native modules"); + CHECK(get_jni_env() == NULL, "a rejected load should not publish JNI access"); + return true; +} + +static bool cleans_up_failed_get_env(void) { + reset_mocks(); + main_get_env_result = JNI_ERR; + + CHECK(JNI_OnLoad(&mock_vm, NULL) == JNI_ERR, "JNI_OnLoad should propagate GetEnv failure"); + CHECK(lifecycle_event_count == 0, "GetEnv failure should stop before module initialization"); + CHECK(get_jni_env() == NULL, "GetEnv failure should not publish JNI access"); + return true; +} + +static bool cleans_up_failed_module_load(void) { + static const enum lifecycle_event expected[] = {MODULE_LOAD, MODULE_UNLOAD}; + reset_mocks(); + fail_module_load = true; + + CHECK(JNI_OnLoad(&mock_vm, NULL) == JNI_ERR, "JNI_OnLoad should reject a module initialization exception"); + CHECK(events_equal(expected, sizeof(expected) / sizeof(*expected)), + "module initialization failure should release module state"); + CHECK(get_jni_env() == NULL, "module initialization failure should not publish JNI access"); + return true; +} + +static bool cleans_up_failed_rtc_preload(void) { + static const enum lifecycle_event expected[] = { + MODULE_LOAD, + LOGGER_ENABLED, + RTC_PRELOAD, + LOGGER_DISABLED, + RTC_CLEANUP, + MODULE_UNLOAD, + }; + reset_mocks(); + fail_rtc_preload = true; + + CHECK(JNI_OnLoad(&mock_vm, NULL) == JNI_ERR, "JNI_OnLoad should reject an RTC preload exception"); + CHECK(events_equal(expected, sizeof(expected) / sizeof(*expected)), + "RTC preload failure should clean up in reverse initialization order"); + CHECK(get_jni_env() == NULL, "RTC preload failure should revoke JNI access"); + return true; +} + +static void* attach_worker(void* data) { + JNIEnv** result = data; + *result = get_jni_env(); + return NULL; +} + +static bool detaches_attached_native_threads(void) { + enum { THREAD_COUNT = 8 }; + pthread_t threads[THREAD_COUNT]; + JNIEnv* results[THREAD_COUNT] = {0}; + reset_mocks(); + + CHECK(JNI_OnLoad(&mock_vm, NULL) == JNI_VERSION_1_6, "JNI_OnLoad should initialize lifecycle state"); + for (size_t index = 0; index < THREAD_COUNT; index++) { + CHECK(pthread_create(&threads[index], NULL, attach_worker, &results[index]) == 0, + "failed to create native test thread"); + } + for (size_t index = 0; index < THREAD_COUNT; index++) { + CHECK(pthread_join(threads[index], NULL) == 0, "failed to join native test thread"); + CHECK(results[index] == &mock_env, "native test thread did not receive the attached JNI environment"); + } + + CHECK(atomic_load(&attach_count) == THREAD_COUNT, "each detached native thread should attach once"); + CHECK(atomic_load(&detach_count) == THREAD_COUNT, "the TLS destructor should detach each native thread"); + CHECK(!atomic_load(&received_unexpected_vm), "the TLS destructor should use its supplied JVM value"); + JNI_OnUnload(&mock_vm, NULL); + return true; +} + +static bool unloads_in_safe_order(void) { + static const enum lifecycle_event expected[] = { + MODULE_LOAD, + LOGGER_ENABLED, + RTC_PRELOAD, + LOGGER_DISABLED, + RTC_CLEANUP, + MODULE_UNLOAD, + }; + reset_mocks(); + + CHECK(JNI_OnLoad(&mock_vm, NULL) == JNI_VERSION_1_6, "JNI_OnLoad should initialize lifecycle state"); + CHECK(get_jni_env() == &mock_env, "initialized lifecycle should expose the current JNI environment"); + logger_callback(RTC_LOG_INFO, "before unload"); + CHECK(java_log_count == 1, "logger should dispatch while JNI access is available"); + + JNI_OnUnload(&mock_vm, NULL); + + CHECK(events_equal(expected, sizeof(expected) / sizeof(*expected)), + "unload should disable callbacks and drain RTC before releasing module state"); + CHECK(get_jni_env() == NULL, "unload should revoke JNI access"); + logger_callback(RTC_LOG_INFO, "after unload"); + CHECK(java_log_count == 1, "logger should not dispatch after JNI access is revoked"); + return true; +} + +static bool frees_callback_after_successful_peer_deletion(void) { + static const enum peer_delete_event expected[] = {GET_USER_POINTER, DELETE_PEER, FREE_CALLBACK}; + static struct jvm_callback callback; + reset_mocks(); + configured_callback = &callback; + + CHECK(delete_peer_connection(&mock_env, configured_peer_handle) == RTC_ERR_SUCCESS, + "peer deletion should return the libdatachannel result"); + CHECK(peer_delete_events_equal(expected, sizeof(expected) / sizeof(*expected)), + "callback state should be freed only after peer deletion returns"); + CHECK(freed_with_env == &mock_env, "callback cleanup should receive the calling JNI environment"); + CHECK(freed_callback == &callback, "callback cleanup should release the peer callback state"); + return true; +} + +static bool retains_callback_when_peer_deletion_fails(void) { + static const enum peer_delete_event expected[] = {GET_USER_POINTER, DELETE_PEER}; + static struct jvm_callback callback; + reset_mocks(); + configured_callback = &callback; + configured_delete_result = RTC_ERR_FAILURE; + + CHECK(delete_peer_connection(&mock_env, configured_peer_handle) == RTC_ERR_FAILURE, + "peer deletion should preserve a failure result"); + CHECK(peer_delete_events_equal(expected, sizeof(expected) / sizeof(*expected)), + "failed peer deletion should retain callback state"); + CHECK(freed_callback == NULL, "failed peer deletion should not free callback state"); + return true; +} + +static bool deletes_peer_without_callback_state(void) { + static const enum peer_delete_event expected[] = {GET_USER_POINTER, DELETE_PEER}; + reset_mocks(); + + CHECK(delete_peer_connection(&mock_env, configured_peer_handle) == RTC_ERR_SUCCESS, + "peer deletion without callback state should succeed"); + CHECK(peer_delete_events_equal(expected, sizeof(expected) / sizeof(*expected)), + "peer deletion should not attempt to free missing callback state"); + return true; +} + +static bool run_test(const char* name, bool (*test)(void)) { + if (!test()) { + fprintf(stderr, "FAIL %s\n", name); + return false; + } + printf("PASS %s\n", name); + return true; +} + +int main(void) { + bool passed = true; + passed &= run_test("rejects missing JVM", rejects_missing_jvm); + passed &= run_test("cleans up failed GetEnv", cleans_up_failed_get_env); + passed &= run_test("cleans up failed module load", cleans_up_failed_module_load); + passed &= run_test("cleans up failed RTC preload", cleans_up_failed_rtc_preload); + passed &= run_test("detaches attached native threads", detaches_attached_native_threads); + passed &= run_test("unloads in safe order", unloads_in_safe_order); + passed &= run_test("frees callback after successful peer deletion", frees_callback_after_successful_peer_deletion); + passed &= run_test("retains callback when peer deletion fails", retains_callback_when_peer_deletion_fails); + passed &= run_test("deletes peer without callback state", deletes_peer_without_callback_state); + return passed ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/jni/test/thread_lifecycle.c b/jni/test/thread_lifecycle.c new file mode 100644 index 0000000..8d7b618 --- /dev/null +++ b/jni/test/thread_lifecycle.c @@ -0,0 +1,71 @@ +#include "../src/global_jvm.h" +#include "../src/util.h" + +#include +#include + +struct thread_result { + jobject thread; + char* error; +}; + +static void* attach_and_terminate(void* data) { + struct thread_result* result = data; + JNIEnv* env = get_jni_env(); + if (env == NULL) { + result->error = "Failed to attach native test thread"; + return NULL; + } + + jclass thread_class = (*env)->FindClass(env, "java/lang/Thread"); + if (thread_class == NULL) { + (*env)->ExceptionClear(env); + result->error = "Failed to find java.lang.Thread"; + return NULL; + } + + jmethodID current_thread = (*env)->GetStaticMethodID(env, thread_class, "currentThread", "()Ljava/lang/Thread;"); + if (current_thread == NULL) { + (*env)->ExceptionClear(env); + result->error = "Failed to find Thread.currentThread"; + return NULL; + } + + jobject thread = (*env)->CallStaticObjectMethod(env, thread_class, current_thread); + if (thread == NULL || (*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + result->error = "Failed to get current native thread"; + return NULL; + } + + result->thread = (*env)->NewGlobalRef(env, thread); + if (result->thread == NULL) { + if ((*env)->ExceptionCheck(env)) { + (*env)->ExceptionClear(env); + } + result->error = "Failed to retain current native thread"; + } + return NULL; +} + +JNIEXPORT jobject JNICALL +Java_tel_schich_libdatachannel_NativeThreadLifecycleTest_attachAndTerminateNativeThread(JNIEnv* env, jclass clazz) { + struct thread_result result = {0}; + pthread_t thread; + if (pthread_create(&thread, NULL, attach_and_terminate, &result) != 0) { + throw_native_exception(env, "Failed to create native test thread"); + return NULL; + } + if (pthread_join(thread, NULL) != 0) { + throw_native_exception(env, "Failed to join native test thread"); + return NULL; + } + if (result.error != NULL) { + throw_native_exception(env, result.error); + return NULL; + } + + jobject java_thread = (*env)->NewLocalRef(env, result.thread); + (*env)->DeleteGlobalRef(env, result.thread); + return java_thread; +} diff --git a/src/test/java/tel/schich/libdatachannel/NativeThreadLifecycleTest.java b/src/test/java/tel/schich/libdatachannel/NativeThreadLifecycleTest.java new file mode 100644 index 0000000..908f442 --- /dev/null +++ b/src/test/java/tel/schich/libdatachannel/NativeThreadLifecycleTest.java @@ -0,0 +1,24 @@ +package tel.schich.libdatachannel; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class NativeThreadLifecycleTest { + static { + LibDataChannel.initialize(); + } + + private static native Thread attachAndTerminateNativeThread(); + + @Test + void detachesTerminatedNativeThread() { + Thread thread = attachAndTerminateNativeThread(); + + assertNotNull(thread); + assertFalse(thread.isAlive()); + assertEquals(Thread.State.TERMINATED, thread.getState()); + } +}