Skip to content

Track ANativeActivity pointer lifetime more carefully#234

Merged
rib merged 1 commit intomainfrom
rib/pr/anative-activity-ptr-invalidation
Mar 17, 2026
Merged

Track ANativeActivity pointer lifetime more carefully#234
rib merged 1 commit intomainfrom
rib/pr/anative-activity-ptr-invalidation

Conversation

@rib
Copy link
Member

@rib rib commented Mar 17, 2026

Once on_destroy() returns then the NativeActivity.java code will call an unloadNativeCode native method that will delete the ANativeActivity and invalidate any pointers we hold.

Considering the possibility that an AndroidApp could be retained beyond the lifetime of the original NativeActivity, this ensures we always hold the WaitableNativeActivityState::mutex before dereferencing this pointer and ensures we clear the pointer before returning from on_destroy so we're also able to perform null pointer checks before dereferencing.

Considering that AndroidApp::vm_as_ptr previously depended on dereferencing the ANativeActivity, this updates it to instead use JavaVM::singleton() which we guarantee will be initialized before the AndroidApp is created.

Considering that AndroidApp::activity_as_ptr() promises to return a global reference that remains valid for the lifetime of the AndroidApp, but the ANativeActivity::clazz reference is deleted after on_destroy() returns, we now create our own Global reference for the Activity that is owned by AndroidAppInner.

Addresses: #196

Once `on_destroy()` returns then the `NativeActivity.java` code will call an
`unloadNativeCode` native method that will `delete` the `ANativeActivity`
and invalidate any pointers we hold.

Considering the possibility that an `AndroidApp` could be retained
beyond the lifetime of the original `NativeActivity`, this ensures we
always hold the `WaitableNativeActivityState::mutex` before
dereferencing this pointer and ensures we clear the pointer before
returning from `on_destroy` so we're also able to perform `null` pointer
checks before dereferencing.

Considering that `AndroidApp::vm_as_ptr` previously depended on
dereferencing the `ANativeActivity`, this updates it to instead use
`JavaVM::singleton()` which we guarantee will be initialized before the
`AndroidApp` is created.

Considering that `AndroidApp::activity_as_ptr()` promises to return a
global reference that remains valid for the lifetime of the
`AndroidApp`, but the `ANativeActivity::clazz` reference is deleted
after `on_destroy()` returns, we now create our own `Global` reference
for the `Activity` that is owned by `AndroidAppInner`.
@rib rib force-pushed the rib/pr/anative-activity-ptr-invalidation branch from f19460c to c60af42 Compare March 17, 2026 13:25
@rib rib merged commit 91cf9d7 into main Mar 17, 2026
7 checks passed
@rib rib deleted the rib/pr/anative-activity-ptr-invalidation branch March 17, 2026 13:38
rib added a commit that referenced this pull request Mar 18, 2026
Most of the same issues found in the native-activity backend when
working on #234 (to safely drop ANativeActivity via onDestroy callback)
also apply to the game-activity backend, which this PR addresses.

This ensures that the game-activity backend cleanly drops its
`android_app` pointer once we're notified that the `GameActivity` is being
destroyed and adds a mutex around the pointer that guarantees that
it can't be freed while it's being dereferenced (because the same
lock is required to respond to the onDestroy callback where the
state gets freed).

This makes a number of backend details consistent with the
native-activity backend:
- The backend retains its own Looper reference instead of relying on
  the android_app reference.
- The backend allocates its own JNI global reference for the Activity,
  instead of relying on the android_app reference.

Since this needed to add a hook to clear the android_app pointer after
dispatching the callback for `MainEvent::Destroy` it also made sense to
fix the MainEvent::TerminateWindow hook for clearing our `NativeWindow`
so it also happens _after_ the callback (as the API docs state).

Testing these changes with a minimal agdk-mainloop and agdk-egui example
I see it's now possible to cleanly handle repeated activity start ->
destroy -> start -> destroy cycles (e.g. due to config changes
triggering a recreation of the activity). (When testing egui I did also
have to patch Winit to ensure it exits the loop when receiving a Destroy
event)

Fixes: #235
Fixes: #162
rib added a commit that referenced this pull request Mar 18, 2026
Most of the same issues found in the native-activity backend when
working on #234 (to safely drop ANativeActivity via onDestroy callback)
also apply to the game-activity backend, which this PR addresses.

This ensures that the game-activity backend cleanly drops its
`android_app` pointer once we're notified that the `GameActivity` is being
destroyed and adds a mutex around the pointer that guarantees that
it can't be freed while it's being dereferenced (because the same
lock is required to respond to the onDestroy callback where the
state gets freed).

This makes a number of backend details consistent with the
native-activity backend:
- The backend retains its own Looper reference instead of relying on
  the android_app reference.
- The backend allocates its own JNI global reference for the Activity,
  instead of relying on the android_app reference.

Since this needed to add a hook to clear the android_app pointer after
dispatching the callback for `MainEvent::Destroy` it also made sense to
fix the MainEvent::TerminateWindow hook for clearing our `NativeWindow`
so it also happens _after_ the callback (as the API docs state).

Testing these changes with a minimal agdk-mainloop and agdk-egui example
I see it's now possible to cleanly handle repeated activity start ->
destroy -> start -> destroy cycles (e.g. due to config changes
triggering a recreation of the activity). (When testing egui I did also
have to patch Winit to ensure it exits the loop when receiving a Destroy
event)

Fixes: #235
Fixes: #162
rib added a commit that referenced this pull request Mar 18, 2026
Most of the same issues found in the native-activity backend when
working on #234 (to safely drop ANativeActivity via onDestroy callback)
also apply to the game-activity backend, which this PR addresses.

This ensures that the game-activity backend cleanly drops its
`android_app` pointer once we're notified that the `GameActivity` is being
destroyed and adds a mutex around the pointer that guarantees that
it can't be freed while it's being dereferenced (because the same
lock is required to respond to the onDestroy callback where the
state gets freed).

This makes a number of backend details consistent with the
native-activity backend:
- The backend retains its own Looper reference instead of relying on
  the android_app reference.
- The backend allocates its own JNI global reference for the Activity,
  instead of relying on the android_app reference.

Since this needed to add a hook to clear the android_app pointer after
dispatching the callback for `MainEvent::Destroy` it also made sense to
fix the MainEvent::TerminateWindow hook for clearing our `NativeWindow`
so it also happens _after_ the callback (as the API docs state).

Testing these changes with a minimal agdk-mainloop and agdk-egui example
I see it's now possible to cleanly handle repeated activity start ->
destroy -> start -> destroy cycles (e.g. due to config changes
triggering a recreation of the activity). (When testing egui I did also
have to patch Winit to ensure it exits the loop when receiving a Destroy
event)

Fixes: #235
Fixes: #162
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant