fix(android): stop crash when USB is first used off the main thread

Merged
BA
2mo ago
+17-13

On Android, our app crashed the moment it listed serial ports, because it calls available_ports from an async Tauri command (a tokio worker thread):

FATAL EXCEPTION: Thread-3
java.lang.ClassNotFoundException: app.tauri.serialplugin.UsbNative

fd_bridge::cache() looks up UsbNative with FindClass the first time it is used, on whatever thread gets there first. On a thread Rust attached with attach_current_thread, JNI uses the system class loader, which can't see app classes, so the lookup fails. The error is still pending when ? returns early, so ART kills the app when the thread detaches. It only works if the first USB call happens to come from the main thread (e.g. the sync JS commands). Any call from async commands, tokio tasks or spawned threads crashes, and so could the port IO that now runs off the main thread.

The fix: nativeInit is a static native method on UsbNative, so its jclass argument is the class itself, already resolved by the app class loader. The patch keeps a global ref to it there, next to where the JVM is stored, and drops the thread-dependent FindClass. The public API doesn't change. As before, calls that run before bind() return "JNI not initialized".

Testing

  • On a Galaxy Tab A9 (Android 16), release build: before the fix, the app crashed on the first available_ports from a tokio worker. After it, enumeration runs on tokio-rt-worker and returns normally, with nothing in the crash buffer.
  • cargo fmt --all -- --check passes. cargo clippy -p tauri-plugin-serialplugin --target aarch64-linux-android --lib -- -D warnings passes.
  • --all-targets for aarch64-linux-android fails to compile the lib tests (unresolved crate::AtResultFormat, etc.). That happens identically on main, so it's unrelated to this change.

🤖 Generated with Claude Code


Summary by cubic

Fixes an Android crash when serial ports are first used from a non-main thread by caching the UsbNative JNI class in nativeInit instead of looking it up with FindClass on first use.

  • On Rust-attached threads (async commands, tokio tasks), FindClass uses the system class loader, which can't see app classes, so the lookup fails with ClassNotFoundException and ART kills the app at thread detach.
  • nativeInit is a static native method on UsbNative, so its jclass argument is already resolved by the app class loader; the fix keeps a global ref there.
  • Public API is unchanged; calls before bind() still return "JNI not initialized".