manganis: load FFI classes from the activity classloader, not FindClass

JNIEnv::find_class resolves against the system classloader on a thread
attached from native code, which cannot see APK classes (#5629). Route
the generated #[manganis::ffi] type lookup through
load_class_from_classloader, and reimplement that helper to ask the
activity's own ClassLoader.loadClass instead of Class.forName.

Ask the activity INSTANCE, not its Class: Context.getClassLoader() is
documented to return a loader for the application package
unconditionally, whereas Class.getClassLoader() returns null for classes
loaded by the bootstrap loader.

Describe and clear the pending Java exception on every error path. jni
0.21 leaves the exception pending when a call returns Err, and the next
JNI call then aborts the process with 'JNI DETECTED ERROR IN
APPLICATION: ... called with pending exception' -- which is what turns a
recoverable ClassNotFoundException into an unreadable ART abort.
This commit is contained in:
swaits committed 2026-09-05 21:31:12 -06:00
1 parent 07dddd569e
commit 48baafc3ff
2 files changed
+46 -7

No files matched your search

+1 -1
View File
@@ -398,7 +398,7 @@ impl FfiBridgeParser {
// Use with_activity which returns Option<R>, wrapping our Result in the Option // Use with_activity which returns Option<R>, wrapping our Result in the Option
let inner_result: Option<Result<Self, String>> = manganis::android::with_activity(|mut env, activity| { let inner_result: Option<Result<Self, String>> = manganis::android::with_activity(|mut env, activity| {
// Find the class // 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 { let class = match class_result {
Ok(c) => c, Ok(c) => c,
Err(e) => return Some(Err(format!("Failed to find class {}: {:?}", #class_name_lit, e))), Err(e) => return Some(Err(format!("Failed to find class {}: {:?}", #class_name_lit, e))),
+45 -6
View File
@@ -111,17 +111,56 @@ pub fn request_permissions_via_helper(
Ok(()) 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>( pub fn load_class_from_classloader<'env>(
env: &mut JNIEnv<'env>, env: &mut JNIEnv<'env>,
activity: &JObject<'_>,
class_name: &str, class_name: &str,
) -> Result<JClass<'env>> { ) -> Result<JClass<'env>> {
let class_name_jstring = new_string(env, class_name)?; let binary_name = class_name.replace('/', ".");
let class = env.call_static_method(
"java/lang/Class", // Ask the activity INSTANCE for its classloader. `Activity` is a `Context`, and
"forName", // `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;", "(Ljava/lang/String;)Ljava/lang/Class;",
&[JValue::Object(&class_name_jstring)], &[JValue::Object(&class_name_jstring)],
)?; );
let class = describe_and_clear(env, class)?;
Ok(class.l()?.into()) 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<T>(env: &mut JNIEnv<'_>, result: Result<T>) -> Result<T> {
if result.is_err() && env.exception_check().unwrap_or(false) {
let _ = env.exception_describe();
let _ = env.exception_clear();
}
result
}