diff --git a/packages/manganis/manganis-macro/src/ffi.rs b/packages/manganis/manganis-macro/src/ffi.rs index 7a570212c..26f7357b0 100644 --- a/packages/manganis/manganis-macro/src/ffi.rs +++ b/packages/manganis/manganis-macro/src/ffi.rs @@ -398,7 +398,7 @@ impl FfiBridgeParser { // Use with_activity which returns Option, wrapping our Result in the Option let inner_result: Option> = manganis::android::with_activity(|mut env, activity| { // Find the class - let class_result = env.find_class(#class_name_lit); + let class_result = manganis::android::load_class_from_classloader(env, activity, #class_name_lit); let class = match class_result { Ok(c) => c, Err(e) => return Some(Err(format!("Failed to find class {}: {:?}", #class_name_lit, e))), diff --git a/packages/manganis/manganis/src/android/java.rs b/packages/manganis/manganis/src/android/java.rs index fd44731ee..2e16cb812 100644 --- a/packages/manganis/manganis/src/android/java.rs +++ b/packages/manganis/manganis/src/android/java.rs @@ -111,17 +111,56 @@ pub fn request_permissions_via_helper( Ok(()) } -/// Load a Java class from the APK's classloader +/// Load a Java class from the APK's classloader. +/// +/// `JNIEnv::find_class` resolves against the *system* classloader on a thread that +/// was attached from native code (`JavaVM::attach_current_thread`), which cannot see +/// classes packaged in the APK, so it raises `ClassNotFoundException`. The activity's +/// own classloader can, so we ask the activity for it and call `loadClass` on it. +/// +/// `class_name` may be given in either JNI form (`com/example/Foo`) or binary form +/// (`com.example.Foo`); `loadClass` requires the binary form, so slashes are converted. pub fn load_class_from_classloader<'env>( env: &mut JNIEnv<'env>, + activity: &JObject<'_>, class_name: &str, ) -> Result> { - let class_name_jstring = new_string(env, class_name)?; - let class = env.call_static_method( - "java/lang/Class", - "forName", + let binary_name = class_name.replace('/', "."); + + // Ask the activity INSTANCE for its classloader. `Activity` is a `Context`, and + // `Context.getClassLoader()` is documented to return a loader for the application + // package unconditionally. Do not route this through `get_object_class(activity)`: + // that calls `Class.getClassLoader()`, which asks which loader loaded the Activity's + // own class and is specified to return null for anything the bootstrap loader + // loaded -- a null receiver that then fails obscurely at the `loadClass` below. + let loader = env.call_method(activity, "getClassLoader", "()Ljava/lang/ClassLoader;", &[]); + let loader = describe_and_clear(env, loader)?.l()?; + + let class_name_jstring = new_string(env, &binary_name); + let class_name_jstring = describe_and_clear(env, class_name_jstring)?; + + let class = env.call_method( + &loader, + "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;", &[JValue::Object(&class_name_jstring)], - )?; + ); + let class = describe_and_clear(env, class)?; Ok(class.l()?.into()) } + +/// Log and clear a pending Java exception before returning an `Err`. +/// +/// jni 0.21 does NOT clear the pending exception when a call returns `Err` +/// (see its `JNIEnv` docs), and any later JNI call made while one is pending +/// aborts the process with "JNI DETECTED ERROR IN APPLICATION: ... called with +/// pending exception". That is what turns a recoverable `ClassNotFoundException` +/// into an unreadable ART abort. Describe it to logcat so the cause is visible, +/// then clear it so the `Err` can travel normally. +fn describe_and_clear(env: &mut JNIEnv<'_>, result: Result) -> Result { + if result.is_err() && env.exception_check().unwrap_or(false) { + let _ = env.exception_describe(); + let _ = env.exception_clear(); + } + result +}