Skip to content

Avoiding crashing on NoSuchMethodError is possible #13

@wuwbobo2021

Description

@wuwbobo2021

New methods for various classes are continuously added in the Android API, and the programmer may forget to check SDK_INT before calling some method added in a brand new Android version if its android.jar is used for the bindgen. How about checking for the existence of the method with Java reflection in Env::require_method? Currently the usage of each method involves require_method for once (then it is cached), so there would be no significant performance issue.

Initial draft of method existence checking
impl<'env> Env<'env> {
    /// Checks if a method exists. If not, JNI `GetMethodID` or `RegisterNatives` will crash the program.
    pub(crate) unsafe fn ensure_method_exists(self, class: &JClass, method: &CStr, descriptor: &CStr) -> bool {
        static GET_METHODS: OnceLock<JMethodID> = OnceLock::new();
        let get_methods = GET_METHODS.get_or_init(|| {
            let class_class = self.require_class_jni(c"java/lang/Class").unwrap();
            self.require_method(&class_class, c"getMethods", c"()[Ljava/lang/reflect/Method;")
        });

        let jnienv = self.as_raw();
        let arr_methods = ((**jnienv).v1_2.CallObjectMethod)(jnienv, class.as_raw(), get_methods.as_raw());
        if self.exception_check_raw().is_err() || arr_methods.is_null() {
            return false;
        }
        let found = Self::find_in_method_array(self, arr_methods, method, descriptor);
        ((**jnienv).v1_2.DeleteLocalRef)(jnienv, arr_methods);
        if found {
            return true;
        }

        static GET_DECLARED_METHODS: OnceLock<JMethodID> = OnceLock::new();
        let get_declared_methods = GET_DECLARED_METHODS.get_or_init(|| {
            let class_class = self.require_class_jni(c"java/lang/Class").unwrap();
            self.require_method(&class_class, c"getDeclaredMethods", c"()[Ljava/lang/reflect/Method;")
        });
        let arr_methods = ((**jnienv).v1_2.CallObjectMethod)(jnienv, class.as_raw(), get_declared_methods.as_raw());
        if self.exception_check_raw().is_err() || arr_methods.is_null() {
            return false;
        }
        let found = Self::find_in_method_array(self, arr_methods, method, descriptor);
        ((**jnienv).v1_2.DeleteLocalRef)(jnienv, arr_methods);
        return found;
    }

    unsafe fn find_in_method_array(self, arr_methods: jobject, method: &CStr, descriptor: &CStr) -> bool {
        static TO_STRING: OnceLock<JMethodID> = OnceLock::new();
        let to_string = TO_STRING.get_or_init(|| {
            // Prevent class "java.lang.reflect.Method" from being unloaded.
            let method_class = Box::leak(Box::new(self.require_class_jni(c"java/lang/reflect/Method").unwrap()));
            self.require_method(method_class, c"toString", c"()Ljava/lang/String;")
        });

        let jnienv = self.as_raw();
        let len = ((**jnienv).v1_2.GetArrayLength)(jnienv, arr_methods);
        let mut found = false;
        for i in 0..len {
            let method_obj = ((**jnienv).v1_2.GetObjectArrayElement)(jnienv, arr_methods, i);
            if self.exception_check_raw().is_err() || method_obj.is_null() {
                continue;
            }
            let method_jstring = ((**jnienv).v1_2.CallObjectMethod)(jnienv, method_obj, to_string.as_raw());
            if self.exception_check_raw().is_err() || method_jstring.is_null() {
                ((**jnienv).v1_2.DeleteLocalRef)(jnienv, method_obj);
                continue;
            }
            let method_string = StringChars::from_env_jstring(self, method_jstring).to_string_lossy();
            ((**jnienv).v1_2.DeleteLocalRef)(jnienv, method_jstring);
            ((**jnienv).v1_2.DeleteLocalRef)(jnienv, method_obj);

            let (name, desc) = Self::java_method_string_to_name_desc(&method_string);
            if name == method.to_string_lossy() && desc == descriptor.to_string_lossy() {
                found = true;
                break;
            }
        }
        found
    }

    // See <https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Method.html#toString-->.
    fn java_method_string_to_name_desc(method_string: &str) -> (&str, String) {
        let (Some(i_par_l), Some(i_par_r)) = (method_string.find('('), method_string.rfind(')')) else {
            return ("", String::new());
        };

        let params_str = &method_string[i_par_l + 1..i_par_r];
        let mut parts: Vec<_> = method_string[..i_par_l].split_whitespace().collect();
        let method_name = parts.pop().unwrap_or("").trim().split('.').next_back().unwrap_or("");
        let return_type = parts.pop().unwrap_or("").trim();

        let mut desc = "(".to_string();
        for param_type in params_str.split(',') {
            Self::write_type_desc(param_type, &mut desc);
        }
        desc.push(')');
        Self::write_type_desc(return_type, &mut desc);

        (method_name, desc)
    }

    fn write_type_desc(java_name: &str, desc: &mut String) {
        let mut param = java_name.trim();
        while param.ends_with("[]") {
            desc.push('[');
            param = &param[..param.len() - 2];
        }
        let mut param_class = None;
        #[rustfmt::skip]
        let param_prim = match param {
            "boolean" => "Z", "byte" => "B", "char" => "C", "short" => "S",
            "int" => "I", "long" => "J", "float" => "F", "double" => "D",
            "void" => "V", cls => { param_class.replace(cls.replace('.', "/")); "" },
        };
        if let Some(cls) = param_class {
            desc.push('L');
            desc.push_str(&cls);
            desc.push(';');
        } else {
            desc.push_str(param_prim);
        }
    }
}

#[test]
fn java_method_string_to_name_desc_test() {
    let method = "public final void java.lang.Object.wait(long) throws java.lang.InterruptedException";
    let (name, desc) = Env::java_method_string_to_name_desc(method);
    assert!(name == "wait" && desc == "(J)V");

    let method = "public java.lang.String[][] com.test.SomeType.someMethod(boolean,java.lang.Object[],long,com.test.SomeType$Sub) ";
    let (name, desc) = Env::java_method_string_to_name_desc(method);
    assert!(name == "someMethod" && desc == "(Z[Ljava/lang/Object;JLcom/test/SomeType$Sub;)[[Ljava/lang/String;");
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions