Converting unknown Java types to Rust types

Viewed 641

I am trying to take a HashMap from Java into Rust and convert it into an internal object, matching data types and all. The HashMap in java is a HashMap<String, Object> where the Object could be any data type including another HashMap<String, Object>.

I'm having a difficult time determining what type of object a JObject is, and I am blocked on how to convert a JObject to something like a JString.

As you can see I've barely gotten anywhere with it, but as a first pass I'd just like to return the string value of either a String, Integer, or Date (where Date would be the string value of Date.getTime()).

static DATE_CLASS: &str = "java/util/Date";
static INTEGER_CLASS: &str = "java/lang/Integer";
static STRING_CLASS: &str = "java/lang/String";

fn get_string_value(env: &JNIEnv, obj: JObject) -> String {
    let ret = String::new();

    if env.is_instance_of(obj, STRING_CLASS).unwrap() {
        let ret2 = env.call_method(obj, "toString", "()Ljava/lang/String;", &[]).unwrap();
        // Got a JValue- now what?
    }

    if env.is_instance_of(obj, INTEGER_CLASS).unwrap() {
        let ret2 = env.call_method(obj, "toString", "()Ljava/lang/String;", &[]).unwrap();
        // Got a JValue- now what?
    }

    if env.is_instance_of(obj, INTEGER_CLASS).unwrap() {
        let ret2 = env.call_method(obj, "getTime", "()Ljava/lang/String;", &[]).unwrap();
        // Got a JValue- now what?
    }

    ret
}

I think the way I'm supposed to do this (but please correct me if not), is to check if the JObject is an instance of any of these classes by their fully qualified paths. Once I know what type they are, I can call a method on them with env.call_method. The result is a JValue, which can return some primitive types.

enter image description here

I'm assuming I'm supposed to be using these unless I'm expecting another object to be returned, in which case I'd use-

let message_ref = env.auto_local(ret2).as_obj();

Then I could call another method if I needed to. But as far as I know I can't then convert that into a JString if I wanted to. However, that's also the only way I know how to get a rust string out of a JString so far, using:

let s: String = env.get_string(a_j_string).expect("Couldn't get java string").into();

Am I going about this right?

How do I convert a JValue to a String?

Am I correct that I should use auto_local to convert a JValue to a JObject, and that would allow me to call methods on it again?

If I'm supposed to use the primitive type jchar to get the value of toString- how do I convert that to &str or String?

Edit:

With @Chris Jester-Young's help I was able to get both a jobj_to_string and jobj_to_int functions working.

static INTEGER_CLASS: &str = "java/lang/Integer";
static STRING_CLASS: &str = "java/lang/String";

fn get_liquid_value(env: &JNIEnv, obj: JObject) -> LiquidValue {
    let mut value = LiquidValue::Nil;

    if env.is_instance_of(obj, STRING_CLASS).unwrap() {
        match jobj_to_string(env, obj) {
            Some(str) => {
                value = LiquidValue::Scalar(LiquidScalar::from(str));
            },
            None => {}
        }
    }

    if env.is_instance_of(obj, INTEGER_CLASS).unwrap() {
        match jobj_to_i32(env, obj) {
            Some(int) => {
                value = LiquidValue::Scalar(LiquidScalar::from(int));
            },
            None => {}
        }
    }

    value
}

fn jobj_to_string(env: &JNIEnv, obj: JObject) -> Option<String> {
    let mut result = Option::None;

    match env.call_method(obj, "toString", "()Ljava/lang/String;", &[]) {
        Result::Ok(jvalue) => {
            match jvalue.l() {
                Result::Ok(jobject) => {
                    let string = String::from(env.get_string(jobject.into()).unwrap().to_str().unwrap());
                    result = Option::Some(string);
                },
                _ => {}
            }
        },
        _ => {}
    };

    result
}

fn jobj_to_i32(env: &JNIEnv, obj: JObject) -> Option<i32> {
    let mut result = Option::None;

    match env.call_method(obj, "intValue", "()I", &[]) {
        Ok(jvalue) => {
            match jvalue.i() {
                Ok(int) => {
                    result = Option::Some(int.to_owned());
                },
                _ => {}
            }
        },
        _ => {}
    }

    result
}
1 Answers

In the jni crate, JObject and JString are just wrappers around JNI's jobject and jstring. You can "cast" a JObject into a JString using the From trait. e.g., JString::from(my_jobject) or my_jobject.into(). You do have to do a JNIEnv::is_instance_of check first, though, if you're at all unsure if the object is actually a string.

To your question about JValue, the result of toString is a string, which in the JVM universe, is an object type, not a primitive type. So you can just use the JValue::l accessor (you don't need to use auto_local from what I can tell). Since we know the result is a string, you can just cast the result to a JString directly (as described above), and then call JNIEnv::get_string to get a JavaStr object, that you can convert to a Rust string using the From trait.

Putting it all together, we get the following (not tested, but it compiles for me at least):

fn get_string_value(env: &JNIEnv, obj: JObject<'_>) -> Result<String> {
    let result = env.call_method(obj, "toString", "()Ljava/lang/String;", &[])?.l()?;
    Ok(env.get_string(result.into())?.into())
}
Related