Skip to content

Add AndroidApp::run_on_java_main_thread#232

Merged
rib merged 1 commit intomainfrom
rib/pr/run-on-java-main-thread
Mar 17, 2026
Merged

Add AndroidApp::run_on_java_main_thread#232
rib merged 1 commit intomainfrom
rib/pr/run-on-java-main-thread

Conversation

@rib
Copy link
Member

@rib rib commented Mar 12, 2026

This makes it easy to schedule boxed closures to be run on the Java main / ui thread.

When the closure is run:

  • Any panic will be caught, so we don't unwind into the Looper and abort the process
  • The JVM will be attached (for JNI) and any exceptions that are thrown will be caught and logged as errors.
  • A JNI stack frame will be pushed and popped before running your closure (so you don't have to worry about leaking local JNI references)

As an example, here's how the API could be used to send a Toast via JNI on the Java main / UI thread:

jni::bind_java_type! {
    Toast => "android.widget.Toast",
    type_map {
        Context => "android.content.Context",
    },
    methods {
        static fn make_text(context: Context, text: JCharSequence, duration: i32) -> Toast,
        fn show(),
    }
}

enum ToastDuration {
    Short = 0,
    Long = 1,
}

fn send_toast(outer_app: &AndroidApp, msg: impl AsRef<str>, duration: ToastDuration) {
    let app = outer_app.clone();
    let msg = msg.as_ref().to_string();
    outer_app.run_on_java_main_thread(Box::new(move || {
        // Assuming JavaVM::singleton was initialized at the start of `android_main`
        let jvm = jni::JavaVM::singleton().expect("Failed to get singleton JavaVM instance");
        // We use `with_top_local_frame` as a minor optimization because it's guaranteed by
        // `run_on_java_main_thread` that we already have an underlying JNI attachment and local
        // frame. It would also be perfectly reasonable to use `jvm.attach_current_thread()`.
        if let Err(err) = jvm.with_top_local_frame(|env| -> jni::errors::Result<()> {
            let activity: jni::sys::jobject = app.activity_as_ptr() as _;
            let activity = unsafe { env.as_cast_raw::<Global<Activity>>(&activity)? };

            let message = JString::new(env, &msg)?;
            let toast = Toast::make_text(env, activity.as_ref(), &message, duration as i32)?;
            log::info!("Showing Toast from Rust JNI callback: {msg}");
            toast.show(env)?;

            Ok(())
        }) {
            log::error!("Failed to execute callback on main thread: {err:?}");
        }
    }));
}

I've added the above example to the API documentation for run_on_java_main_thread and I also plan to update na-mainloop to demonstrate the same thing.

Note: I've tested the above snippet in a locally modified na-mainloop but since the in-tree example has got very out-of-date, I'll add this example usage of run_on_java_main_thread via a separate PR

@rib rib force-pushed the rib/pr/run-on-java-main-thread branch 2 times, most recently from 267c3e3 to 643a16d Compare March 12, 2026 23:50
@rib rib force-pushed the rib/pr/run-on-java-main-thread branch from 643a16d to a087094 Compare March 17, 2026 10:21
This makes it easy to schedule boxed closures to be run on the Java main
/ ui thread.

When the closure is run then:
- Any panic will be caught, so we don't unwind into the Looper and abort
  the process
- The JVM will be attached (for JNI) and any exceptions that are thrown
  will be caught and logged as errors.
- A JNI stack frame will be pushed and popped before running your closure
 (so you don't have to worry about leaking local JNI references)

This bumps the jni dependency to 0.22.4 because that version adds a
`JCharSequence` binding that we use in the `Toast` example in the
documentation.
@rib rib force-pushed the rib/pr/run-on-java-main-thread branch from a087094 to 49bbf6c Compare March 17, 2026 10:23
@rib rib merged commit 0c32e9d into main Mar 17, 2026
7 checks passed
@rib rib deleted the rib/pr/run-on-java-main-thread branch March 17, 2026 10:27
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