From 2fabeb9927f73e5f4ab0abe4ee046d5749df11b5 Mon Sep 17 00:00:00 2001 From: Marijn Suijten Date: Wed, 12 Mar 2025 14:43:02 +0100 Subject: [PATCH 1/2] native_activity: Only wait for state to update while main thread is running We see that some Android callbacks like `onStart()` deadlock, specifically when returning out of the main thread before running any event loop (but likely also whenever terminating the event loop), because they don't check if the thread is still even running and are otherwise guaranteed receive an `activity_state` update or other state change to unblock themselves. This is a followup to [#94] which only concerned itself with a deadlock caused by a destructor not running because that very object was kept alive to poll on the `destroyed` field that destructor was supposed to set, but its new `thread_state` can be reused to disable these condvar waits when the "sending" thread has disappeared. Separately, that PR mentions `Activity` recreates because of configuration changes which isn't supported anyway because `Activity` is still wrongly assumed to be a global singleton. [#94]: https://togithub.com/rust-mobile/android-activity/pull/94 --- android-activity/src/native_activity/glue.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/android-activity/src/native_activity/glue.rs b/android-activity/src/native_activity/glue.rs index dab3e29..82672a5 100644 --- a/android-activity/src/native_activity/glue.rs +++ b/android-activity/src/native_activity/glue.rs @@ -447,7 +447,9 @@ impl WaitableNativeActivityState { guard.pending_input_queue = input_queue; guard.write_cmd(AppCmd::InputQueueChanged); - while guard.input_queue != guard.pending_input_queue { + while guard.thread_state == NativeThreadState::Running + && guard.input_queue != guard.pending_input_queue + { guard = self.cond.wait(guard).unwrap(); } guard.pending_input_queue = ptr::null_mut(); @@ -468,7 +470,9 @@ impl WaitableNativeActivityState { if guard.pending_window.is_some() { guard.write_cmd(AppCmd::InitWindow); } - while guard.window != guard.pending_window { + while guard.thread_state == NativeThreadState::Running + && guard.window != guard.pending_window + { guard = self.cond.wait(guard).unwrap(); } guard.pending_window = None; @@ -492,7 +496,7 @@ impl WaitableNativeActivityState { }; guard.write_cmd(cmd); - while guard.activity_state != state { + while guard.thread_state == NativeThreadState::Running && guard.activity_state != state { guard = self.cond.wait(guard).unwrap(); } } @@ -505,7 +509,7 @@ impl WaitableNativeActivityState { // this to be None debug_assert!(!guard.app_has_saved_state, "SaveState request clash"); guard.write_cmd(AppCmd::SaveState); - while !guard.app_has_saved_state { + while guard.thread_state == NativeThreadState::Running && !guard.app_has_saved_state { guard = self.cond.wait(guard).unwrap(); } guard.app_has_saved_state = false; @@ -560,7 +564,9 @@ impl WaitableNativeActivityState { pub fn notify_main_thread_stopped_running(&self) { let mut guard = self.mutex.lock().unwrap(); guard.thread_state = NativeThreadState::Stopped; - self.cond.notify_one(); + // Notify all waiters to unblock any Android callbacks that would otherwise be waiting + // indefinitely for the now-stopped (!) main thread. + self.cond.notify_all(); } pub unsafe fn pre_exec_cmd( From 9afaca815fe58b872abbb3ba5bd4b7e012400464 Mon Sep 17 00:00:00 2001 From: Marijn Suijten Date: Wed, 17 Dec 2025 13:43:52 +0100 Subject: [PATCH 2/2] Assert that the thread `Looper` matches the main one --- android-activity/src/game_activity/mod.rs | 7 ++++++- android-activity/src/native_activity/mod.rs | 5 +++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/android-activity/src/game_activity/mod.rs b/android-activity/src/game_activity/mod.rs index 70b6941..8873a32 100644 --- a/android-activity/src/game_activity/mod.rs +++ b/android-activity/src/game_activity/mod.rs @@ -295,6 +295,11 @@ impl AndroidAppInner { unsafe { let native_app = &self.native_app; + assert_eq!( + ndk_sys::ALooper_forThread(), + (*native_app.as_ptr()).looper, + "Application tried to poll events from non-main thread" + ); let mut fd: i32 = 0; let mut events: i32 = 0; @@ -590,7 +595,7 @@ impl AndroidAppInner { let mut guard = self.input_receiver.lock().unwrap(); // Make sure we don't hand out more than one receiver at a time because - // turning the reciever into an interator will perform a swap_buffers + // turning the receiver into an iterator will perform a swap_buffers // for the buffered input events which shouldn't happen while we're in // the middle of iterating events if let Some(receiver) = &*guard { diff --git a/android-activity/src/native_activity/mod.rs b/android-activity/src/native_activity/mod.rs index 88a9fe8..0346b98 100644 --- a/android-activity/src/native_activity/mod.rs +++ b/android-activity/src/native_activity/mod.rs @@ -201,8 +201,9 @@ impl AndroidAppInner { }; trace!("Calling ALooper_pollAll, timeout = {timeout_milliseconds}"); - assert!( - !ndk_sys::ALooper_forThread().is_null(), + assert_eq!( + ndk_sys::ALooper_forThread(), + self.looper.ptr, "Application tried to poll events from non-main thread" ); let id = ndk_sys::ALooper_pollAll(