Repository navigation
android: Rework the JNI layer (Activity lifetime, exceptions, UI thread, class cache) - #1
Conversation
with_env claimed the thread is detached when the closure returns. jni 0.22's attach_current_thread attaches for good, runs the closure in its own local frame, and turns an exception still pending at the end into the returned error, replacing the closure's result. The doc now says so. obtain_env, an alias of with_env with no callers, is deprecated. The unsafe blocks in the JNI helpers and exports gain SAFETY comments, and a stale comment about dropping a JObject (it has no Drop in jni 0.22) goes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A call into Java that throws leaves the exception pending. jni 0.22 then refuses every later JNI call in the same frame, and the error only says "JavaException". Several calls in platform.rs and jni.rs converted the error with map_err(to_string) and left the exception pending. The new JniResultExt::or_clear clears the pending exception and returns its toString(), so errors read like "java.lang.SecurityException: ...". The core files' calls into Java use it. get_string now checks that the object is a java.lang.String before reading it; before, any object handed to it (nativeIme passes whatever Java sent) was read as one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The host-driven path kept the current Activity and the previous one as global references, and activity() wrapped the current one's raw pointer after releasing the lock. Two Activity creations during one JNI call (a double rotation) deleted the reference the call was still using, and an Activity that finished stayed the current one, kept alive and used as a Context, until the next onCreate. set_host_activity now adds the Activity to a list, and activity() returns a local reference to the newest one that is neither finishing nor destroyed, taken under the lock, and drops the references of those that are. activity() takes &mut Env for that (every caller already passes one). The new application_context() is for calls that only need a Context; the keyboard layout and thermal status queries use it. The accessibility bridge takes its Activity through activity() too. activity_as_ptr() is documented as not to be kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
find_app_class asked the Activity for its class loader and loaded the class again on every call, three JNI calls on top of the one it served, for each of the ~100 call sites in the platform packages. It also needed an Activity, so it failed between two host Activities. The class loader now comes from the application context, once, and each class found is kept as a global reference. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set_system_chrome (Window.setStatusBarColor and friends) and update_ime_position (InputMethodManager.updateCursorAnchorInfo with the decor view) ran on the render thread. Android requires the UI thread for View and Window calls; the chrome code already noted it could deadlock against it. The new run_on_ui_thread posts a closure to the UI thread's looper, woken through an eventfd, or runs it right away on the UI thread itself. The host-driven path registers the UI thread in set_host_activity, which runs in onCreate. The android-activity path cannot reach the UI thread without Java code, so the closure runs in place there, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pending Java exception makes jni 0.22 refuse every later JNI call in the frame, and the bare JNI error only said "JavaException". Convert calls into Java in the platform packages to `or_clear(env)`, which clears the exception and reports its toString(). Discarded results now log the failure at debug level, and swallowed errors (`.ok()`, `unwrap_or`, `if let Ok`) clear the exception before falling back to their default. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An Activity can finish while a package still holds it, whereas the application context lives for the process. Packages that only call Context methods (getSystemService, getPackageManager, getPackageName) now use `application_context` instead of `activity`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…from Rust The clipboard, deeplink and in-app review packages called static Java helpers (GpuiClipboard, GpuiDeepLink, GpuiInAppReview) that only forwarded to framework APIs. A host that did not copy those files from the example got a loadClass error at runtime. They now make the same framework calls through JNI, with the same behaviour, and the three Java files go. The clipboard and the Play Store check use the application context; opening the store still starts the Activity from the current Activity, in a new task. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review of the branch (core files read in full, packages swept, clippy clean on Fix
Description overclaims the sweep
Pre-existing, touched here Size / safety / perf
Verified: jni 0.22.4 |
|
Reviewed head
The PR could be smaller and safer to review. It mixes 1,062 additions and 1,032 deletions across 34 files. Split the existing commits into focused PRs for exception handling, Activity lifetime, class caching, UI dispatch, and Java helper removal. The helper removal can ship independently; it adds substantial handwritten JNI unrelated to fixing Activity references. Two further recommendations, separate from the confirmed regression:
Validation covered source review and the JVM reproduction. I did not run an Android build or device tests; TalkBack and Activity transitions deserve targeted verification before merging the lifecycle changes. |
The first pass converted only map_err(to_string). The rest of the core still cleared exceptions by hand (losing their description), dropped errors with let _ = and _ => arms, and in two keyboard calls cleared the exception before or_clear could describe it. Every call into Java in jni.rs, platform.rs, window.rs, platform_view.rs and document_picker.rs now goes through or_clear, and errors are returned or logged. The new or_catch makes an expected exception explicit: the system chrome falls back to the pre-API-30 flags only on NoSuchMethodError, where any failure of getInsetsController used to. The only direct exception_* calls left are in take_exception and or_catch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pt hits Review of the in-app review port: open_store_page returned early on any error before startActivity (Uri.parse, the Intent, no Activity), so the https fallback was never tried, where the removed Java helper caught everything and fell through. is_available treated every exception as "not installed", where Java only caught NameNotFoundException. Only ActivityNotFoundException and NameNotFoundException are now expected outcomes (or_catch); anything else is an error, and the market:// attempt falls through to the web page on any failure, logging why. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first pass converted map_err(to_string) and inserted or_clear before swallowed errors. The packages still cleared exceptions by hand in their Err(_) arms (losing the description), and the shared_preferences editor writes and commit() logged failures and returned Ok(()) where they used to return the error. - Every call into Java in the packages goes through or_clear, or or_catch for an expected exception: ActivityNotFoundException when no app takes an intent (url_launcher, maps_launcher; any other exception is now an error instead of "false"), NoClassDefFoundError for VibrationEffect. - shared_preferences: writes return their error again, and commit() returning false is one; getters and contains_key log what they hide. - Functions that return a plain value (connectivity, sensors, battery, can_vibrate) log the error before falling back to their default. - battery's sticky broadcast and the default SharedPreferences use the application context. No exception_clear/exception_check is left outside jni.rs's helpers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…updates From the review of the JNI rework: - activity() asked isFinishing and isDestroyed of every registered Activity on every call, the hot path of every package. It now checks only the newest (popping it if it is gone); older ones are pruned when the next Activity registers. - activity_as_ptr() is deprecated: it is the only way left to hold a raw global reference the registry may delete, and nothing in the crate uses it any more. - The system chrome cache was only reset on the android-activity path, so a recreated host Activity never had its bars styled. set_host_activity resets it. - run_latest_on_ui_thread replaces a pending task with the same key. The IME cursor position and the system chrome use it, so a busy UI thread runs the latest update instead of replaying every frame's. Inline runs of run_on_ui_thread log a leftover exception like queued ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks to both reviews. I pushed four commits on top ( Exception handling: the sweep is now complete. The first pass only rewrote one pattern.
Confirmed regression, fixed: shared_preferences writes and Review points:
Not done here, on purpose:
I'll re-test on the phone with the lab APK (Bombatomica64/Gpui-android#24) before merging. |
Refactor of how gpui-mobile talks to Java on Android, from an audit of
src/android/jni.rs, the core files and all 30packages/*/android.rs. Every commit stands alone, builds, and adds no clippy warnings overmain. Each is meant to become one small upstream PR later.Commits
with_envclaimed it detaches the thread on return. jni 0.22 attaches permanently, and an exception still pending at the end replaces the closure's result.obtain_envis deprecated, and the unsafe blocks get SAFETY comments.JniResultExt::or_clear(env)clears the pending exception and returns itstoString(), so errors readjava.lang.SecurityException: …instead ofJavaException.get_stringnow checks that the object is ajava.lang.String. Before, it read any object as one.onCreate.set_host_activityregisters Activities in a list.activity(env)returns a local ref, taken under the lock, to the newest Activity that is neither finishing nor destroyed, and drops the others.application_context(). The accessibility bridge goes throughactivity().find_app_classcost three extra JNI calls on each of about 110 call sites and needed an Activity. The loader now comes from the application context, once, and each class found is kept as a global ref.set_system_chromeandupdate_ime_positionran on the render thread; the code itself noted a possible deadlock.run_on_ui_threadposts to the UI thread's looper through aneventfd(NDK only, no Java). It is registered inset_host_activity.or_clearrule, applied mechanically across the packages. Discarded results are logged at debug level, and swallowed errors clear the exception before falling back.getSystemService,getPackageManager, …).loadClasserror.GpuiClipboard.java,GpuiDeepLink.javaandGpuiInAppReview.javaare removed from the example.Not in scope: behaviour bugs in individual packages, such as unbounded waits in the pickers, auth and permissions, dead webview controls, and
content://URIs reported as paths. They're listed in the audit for later.Testing
cargo fmt --all -- --checkandcargo clippy --target aarch64-linux-android --lib --all-featuresafter each commit, with the same warning list asmain.am start -f NEW_TASK|CLEAR_TASK): no JNI errors.🤖 Generated with Claude Code