Skip to content

Avoiding crashing on NoSuchMethodError is possible #13

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;");
}

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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