I have an Android app (written in Kotlin), with the processing code running in Rust. I'm trying to send List<String>
data to the Rust back-end and interpret it as a Vec<String>
. The size of the list is variable. I'm using JNI (Java Native Interface) for this.
Android (Kotlin)
On the Kotlin side I have the following function signature:
package eu.mypackage.rust
class SomeName {
@Throws(IllegalArgumentException::class)
external fun checkAnswer(answerInput: String, answerList: List<String>): Boolean
}
Which I call with:
val isCorrect = sn.checkAnswer(answerInput, answerList)
Rust
On the Rust side I have this draft function:
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
pub mod android {
extern crate jni;
// This is the interface to the JVM that we'll call the majority of our methods on.
// @See /
use self::jni::JNIEnv;
// These objects are what you should use as arguments to your native function.
// They carry extra lifetime information to prevent them escaping this context
// and getting used after being GC'd.
use self::jni::objects::{JClass, JString, JObject, JObjectArray}; // Not sure what is required
// This is just a pointer. We'll be returning it from our function.
// We can't return one of the objects with lifetime information
// because the lifetime checker won't let us.
use self::jni::sys::{jstring, jboolean, jobjectArray, jsize}; // Not sure what is required
fn kotlin_list_to_rust_vec_string(env: &JNIEnv, java_array: &jobjectArray) -> Vec<String> {
// TODO function to convert
}
#[no_mangle] // This keeps Rust from "mangling" the name so it is unique (crate).
pub extern "system" fn Java_eu_mycompany_rust_MyPackage_checkAnswer<'local>(
mut env: JNIEnv<'local>,
// This is the class that owns our static method. It's not going to be used,
// but still must be present to match the expected signature of a static native method.
_class: JClass<'local>,
answerInput: JString<'local>,
answerList: JObjectArray<'local>, // JObjectArray correct?
) -> jboolean
// Keep in mind that arrays of jboolean values should only ever hold values of 0 or 1 because any other value could lead to undefined behaviour within the JVM.
// .JNIEnv.html#jboolean-elements
{
let answer_input: String = env.get_string(&answerInput).expect("Couldn't get java string!").into();
let answer_list = kotlin_list_to_rust_vec_string(&env, &answerList);
// ... code to process answer_list
return 1 // true or 0 for false
}
}
Question
- It seems there is no direct conversion for Java
List<String>
-> RustVec<String>
, so a function likekotlin_list_to_rust_vec_string
is required? - What code should be in
kotlin_list_to_rust_vec_string
? I assumeenv
andanswerList
should be a reference to avoid screwing up the data on the Kotlin side? (The amount of data is small, so copying the data is fine)
I have an Android app (written in Kotlin), with the processing code running in Rust. I'm trying to send List<String>
data to the Rust back-end and interpret it as a Vec<String>
. The size of the list is variable. I'm using JNI (Java Native Interface) for this.
Android (Kotlin)
On the Kotlin side I have the following function signature:
package eu.mypackage.rust
class SomeName {
@Throws(IllegalArgumentException::class)
external fun checkAnswer(answerInput: String, answerList: List<String>): Boolean
}
Which I call with:
val isCorrect = sn.checkAnswer(answerInput, answerList)
Rust
On the Rust side I have this draft function:
#[cfg(target_os = "android")]
#[allow(non_snake_case)]
pub mod android {
extern crate jni;
// This is the interface to the JVM that we'll call the majority of our methods on.
// @See https://docs.rs/jni/latest/jni/
use self::jni::JNIEnv;
// These objects are what you should use as arguments to your native function.
// They carry extra lifetime information to prevent them escaping this context
// and getting used after being GC'd.
use self::jni::objects::{JClass, JString, JObject, JObjectArray}; // Not sure what is required
// This is just a pointer. We'll be returning it from our function.
// We can't return one of the objects with lifetime information
// because the lifetime checker won't let us.
use self::jni::sys::{jstring, jboolean, jobjectArray, jsize}; // Not sure what is required
fn kotlin_list_to_rust_vec_string(env: &JNIEnv, java_array: &jobjectArray) -> Vec<String> {
// TODO function to convert
}
#[no_mangle] // This keeps Rust from "mangling" the name so it is unique (crate).
pub extern "system" fn Java_eu_mycompany_rust_MyPackage_checkAnswer<'local>(
mut env: JNIEnv<'local>,
// This is the class that owns our static method. It's not going to be used,
// but still must be present to match the expected signature of a static native method.
_class: JClass<'local>,
answerInput: JString<'local>,
answerList: JObjectArray<'local>, // JObjectArray correct?
) -> jboolean
// Keep in mind that arrays of jboolean values should only ever hold values of 0 or 1 because any other value could lead to undefined behaviour within the JVM.
// https://docs.rs/jni/latest/jni/struct.JNIEnv.html#jboolean-elements
{
let answer_input: String = env.get_string(&answerInput).expect("Couldn't get java string!").into();
let answer_list = kotlin_list_to_rust_vec_string(&env, &answerList);
// ... code to process answer_list
return 1 // true or 0 for false
}
}
Question
- It seems there is no direct conversion for Java
List<String>
-> RustVec<String>
, so a function likekotlin_list_to_rust_vec_string
is required? - What code should be in
kotlin_list_to_rust_vec_string
? I assumeenv
andanswerList
should be a reference to avoid screwing up the data on the Kotlin side? (The amount of data is small, so copying the data is fine)
1 Answer
Reset to default 0Rust
With the help of a LLM, I was able to produce the following solution:
fn kotlin_list_to_rust_vec_string(env: &mut JNIEnv, java_array: &JObjectArray) -> Vec<String> {
let string_count = env.get_array_length(java_array).unwrap();
let mut rust_strings = Vec::with_capacity(string_count as usize);
for i in 0..string_count {
let string_element = env.get_object_array_element(&java_array, i).unwrap();
let java_string: JString = string_element.into();
let rust_string = env.get_string(&java_string).unwrap().into();
rust_strings.push(rust_string);
}
rust_strings
}
The env
variable had to be mutable for this to work though (not sure if this is a safe way of doing it?), so call the function with:
let answer_list = kotlin_list_to_rust_vec_string(&mut env, &answerList);
Android (Kotlin)
While the code compiled on the Rust side, my Android app crashed with the error:
java_vm_ext:598] JNI DETECTED ERROR IN APPLICATION: jarray argument has non-array type: java.util.ArrayList
java_vm_ext:598] in call to GetArrayLength
java_vm_ext:598] from boolean eu.mypackage.rust.
The LLM explained the error as:
In Kotlin, when you pass a
List<String>
to a native method, it remains ajava.util.ArrayList
, which is not a JNI-compatible array. Solution: Convert theList<String>
to aString[]
before passing it to Rust
So updating the function signature:
@Throws(IllegalArgumentException::class)
// in Kotlin, when you pass a List<String> to a native method, it remains a java.util.ArrayList,
// which is not a JNI-compatible array. Convert the List<String> to a String[]
external fun checkAnswer(answerInput: String, answerList: Array<String>): Boolean // List<String>
and adding .toTypedArray()
to my function call:
val isCorrect = sn.checkAnswer(answerInput, answerList.toTypedArray()) // convert to Array
Solved the final issue.