Rust JNI Android async callback

I'm testing Rust with JNI async execution. I want to do execute requests in Rust and return the result to Android asynchronously with callback. I'm testing code to execute the request in the command line and it works fine.

That is how it works on command line:

Callback struck:

struct Processor {
    pub(crate) callback: Box<dyn FnMut(String)>,
}

impl Processor {

    fn set_callback(&mut self, c: impl FnMut(String) + 'static) {
        self.callback = Box::new(c);
    }

    fn process_events(&mut self, result: String) {
        (self.callback)(result);
    }
}

Tokio/reqwest:

const DATA_URL: &str = "https://pokeapi.co/api/v2/pokemon/1/";

#[tokio::main]
pub async fn load_swapi_async_with_cb(callback: Box<dyn FnMut(String)>) -> Result<(), Box<dyn std::error::Error>> {
    println!("load_swload_swapi_async_with_cbapi_async started");
    let mut cb = Processor {
        callback: Box::new(callback),
    };
    let body = reqwest::get(DATA_URL)
        .await?
        .json::<HashMap<String, String>>()
        .await?;
    //println!("{:#?}", body);
    let name = match body.get("name") {
        Some(name) => name,
        None => "Failed to parse"
    }.to_string();

    println!("Name is: {} ", name);
    cb.process_events(name);
    Ok(())
}

And JNI part:

    #[no_mangle]
    #[allow(non_snake_case)]
    pub extern "C" fn Java_com_omg_app_greetings_MainActivity_callback(env: JNIEnv,
                                                                       _class: JClass,
                                                                       callback: JObject) {

        static callback: dyn FnMut(String) + 'static = |name| {
        let response = env.new_string(&name).expect("Couldn't create java string!");
            env.call_method(callback, "rustCallbackResult", "(Ljava/lang/String;)V",
                        &[JValue::from(JObject::from(response))]).unwrap();
        };

        pokemon_api(callback);
    }

And pokemon API method:

#[no_mangle]
pub extern fn pokemon_api(callback: impl FnMut(String) + 'static) {
    let cb_box = Box::new(callback);
    swapi::load_swapi_async_with_cb(cb_box);
}

The error I'm facing:

  • JNI ENV env non-constant value:
let response = env.new_string(&name).expect("Couldn't create java string!");
   |                        ^^^ non-constant value

  • callback - doesn't have a size known at compile-time:
static callback: dyn FnMut(String) + 'static = |name| {
   |                          ^^^^^^^^^^^^^^^^^^^^^^^^^^^ doesn't have a size known at compile-time

I was checking how this working, but example seems to be out of date:

Posted same question on StackOverflow:
https://stackoverflow.com/questions/61220627/rust-jni-async-callback-with-tokio-and-reqwest-for-android

1 Like

You probably want to be using channels instead of callbacks.

Hi,
Thanks for the hint, checking this.

Will it also help with?

env non-constant value

I don't know anything about JNI.

I suppose there are two parts of question:

  1. How to call Java code from rust, you can look at Rust code that generate my project,
    for this particular case: flapigen-rs/java_glue.rs.in at b48b8b67b2e578e0fc85a4ec6013b32d94c4bf2b · Dushistov/flapigen-rs · GitHub
  2. Ok there is call, how Java code that running inside tokio thread should pass value to the rest
    of program, I suppose in Android case LiveData is not bad case in that case.

Hi,
Right now I'm using JNI to call callback and it is working fine in main thread, but I cannot path JNI to another thread.
In your example you are using swig as I see, can you be more specific how you I can do this with it:

  1. From Java I want to call Rust function.
  2. From Rust I'l running a new thread
  3. from new thread I return result to Java

Can I send closure to Java? Something like this:
callback: Box<dyn FnMut(String)>,

Hi,
I tried to use channels, but JNI doesn't implement Send.
I had to create a new struct, compiler asked for static lifetime everywhere, and I made it compile - but it doesn't work. Also, I had to add *mut and <'static> to JNIEnv in the signature.
What I done:

#[no_mangle]
    #[allow(non_snake_case)]
    pub extern "C" fn Java_com_omg_app_greetings_MainActivity_startRequestFromJniChannel(env: *mut JNIEnv<'static>,
                                                                                  _class: JClass,
                                                                                         callback: *mut JObject<'static>) {
        let mut cb;
        unsafe {
            cb = NoSendStruct {
                j_env: &*env,
                j_object: &*callback,
            };
        }

        let (tx, rx): (Sender<NoSendStruct>, Receiver<NoSendStruct>) = mpsc::channel();
        let clone = tx.clone();
        clone.send(cb);
        let child = thread::spawn(move || {
            if let name = swapi::load_swapi_blocking() {
                match rx.recv() {
                    Ok(jni_struct) =>  {
                        let l_env = jni_struct.j_env;
                        let l_object = *jni_struct.j_object;
                        let response = l_env.new_string(&name.unwrap()).expect("Couldn't create java string!");
                        l_env.call_method(l_object, "rustCallbackResult", "(Ljava/lang/String;)V",
                                        &[JValue::from(JObject::from(response))]).unwrap();
                    },
                    _ => println!("Callback Name from channel: Error "),
                }
            } else {
                println!("Callback Name from channel: Error ");
            }
        });

    }

Any ideas?

Maybe just convert the values to things that are Send before sending them?

JNI is called on the UI thread from Android. That mean - if I call request from it, it will block UI. So I thought to send JNI to another thread and call callback when network request is done

download code in my link bellow, run cargo check inside jni_tests and look at java_glue.rs file in somewhere bellow target directory. It will contain exactly code that you want to write.

Actually I already run your Android example before posting this question - a very cool project!
In the file that you mentioned I see:

#[derive(Default)]
struct Observable {
    observers: Vec<Box<dyn OnEvent>>,
}

I assume this seems what I need, but I didn't found mapping of Box<dyn OnEvent> with JNI Env, I think it is inside of the macroslib. Can you help explain how I can do it on my own?
If I understood correctly, with this approach I think instead of sending callback from JAVA, I can return callback from Rust and listen to it.
Just for learning purposes, I want to do it on my own.

BTW, I was not able to run cargo check :

/Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/include/jni.h:39:10: fatal error: 'stdio.h' file not found

I checked for requirements in Readme - all fine. Xcode 11.

I post link bellow, it contains link to exact line:

foreign_interface!(interface ThreadSafeObserver {
    self_type OnThreadEvent + Send;
    onStateChanged = OnThreadEvent::something_change_in_other_thread(&self, x: i32, s: &str);
});

This is part of test for callback from "not-main thread", have no idea why are you talking about
struct Obserbale.

jni_tests is host only. In other words you need install JDK for your host OS,
in your case JDK for Mac OS.

You just need to look at generated code, that's all. If you already built android_example,
you can just copy/paste related part of jni_tests to android_example. The macrolib and stuff
may be complex, but generated code is just plain Rust code.
To make it more clear.
Let's suppose you install JDK on Mac OS and run

cd rust_swig/jni_tests
cargo check --release
find ../target/release/ -name java_glue.rs
../target/release/build/rust_swig_test_jni-4fce3fd4e44e1909/out/java_glue.rs

and look at java_glue.rs

You see:

struct JavaCallback {
    java_vm: *mut JavaVM,
    this: jobject,
    methods: Vec<jmethodID>,
}

trait OnThreadEvent {
    fn something_change_in_other_thread(&self, x: i32, s: &str);
}

impl SwigFrom<jobject> for Box<dyn OnThreadEvent + Send> {
    fn swig_from(this: jobject, env: *mut JNIEnv) -> Self {
        let mut cb = JavaCallback::new(this, env);
        cb.methods.reserve(1);
        let class = unsafe { (**env).GetObjectClass.unwrap()(env, cb.this) };
        assert!(
            !class.is_null(),
            "GetObjectClass return null class for ThreadSafeObserver"
        );
        let method_id: jmethodID = unsafe {
            (**env).GetMethodID.unwrap()(
                env,
                class,
                swig_c_str!("onStateChanged"),
                swig_c_str!("(ILjava/lang/String;)V"),
            )
        };
        assert!(!method_id.is_null(), "Can not find onStateChanged id");
        cb.methods.push(method_id);
        Box::new(cb)
    }
}

impl OnThreadEvent for JavaCallback {
    #[allow(unused_mut)]
    fn something_change_in_other_thread(&self, a0: i32, a1: &str) {
        swig_assert_eq_size!(::std::os::raw::c_uint, u32);
        swig_assert_eq_size!(::std::os::raw::c_int, i32);
        let env = self.get_jni_env();
        if let Some(env) = env.env {
            let a0: jint = a0;
            let mut a1: jstring = <jstring>::swig_from(a1, env);
            unsafe {
                (**env).CallVoidMethod.unwrap()(env, self.this, self.methods[0usize], a0, a1);
                if (**env).ExceptionCheck.unwrap()(env) != 0 {
                    log::error!(concat!(
                        stringify!(something_change_in_other_thread),
                        ": java throw exception"
                    ));
                    (**env).ExceptionDescribe.unwrap()(env);
                    (**env).ExceptionClear.unwrap()(env);
                }
            };
        }
    }
}

So if you somehow get jobject then code convert it to Box<dyn OnThreadEvent + Send>,
and you can pass it around and then call OnThreadEvent::something_change_in_other_thread
which calls Java code and pass data to Java.

Java code looks like:

$ cat `find . -name ThreadSafeObserver.java`
// Automatically generated by rust_swig
package com.example.rust;


public interface ThreadSafeObserver {


    void onStateChanged(int x, String s);

}

And also there is JavaCallback::get_jni_env:

    fn get_jni_env(&self) -> JniEnvHolder {
        assert!(!self.java_vm.is_null());
        let mut env: *mut JNIEnv = ::std::ptr::null_mut();
        let res = unsafe {
            (**self.java_vm).GetEnv.unwrap()(
                self.java_vm,
                (&mut env) as *mut *mut JNIEnv as *mut *mut ::std::os::raw::c_void,
                SWIG_JNI_VERSION,
            )
        };
        if res == (JNI_OK as jint) {
            return JniEnvHolder {
                env: Some(env),
                callback: self,
                need_detach: false,
            };
        }
        if res != (JNI_EDETACHED as jint) {
            panic!("get_jni_env: GetEnv return error `{}`", res);
        }
        trait ConvertPtr<T> {
            fn convert_ptr(self) -> T;
        }
        impl ConvertPtr<*mut *mut ::std::os::raw::c_void> for *mut *mut JNIEnv {
            fn convert_ptr(self) -> *mut *mut ::std::os::raw::c_void {
                self as *mut *mut ::std::os::raw::c_void
            }
        }
        impl ConvertPtr<*mut *mut JNIEnv> for *mut *mut JNIEnv {
            fn convert_ptr(self) -> *mut *mut JNIEnv {
                self
            }
        }
        let res = unsafe {
            (**self.java_vm).AttachCurrentThread.unwrap()(
                self.java_vm,
                (&mut env as *mut *mut JNIEnv).convert_ptr(),
                ::std::ptr::null_mut(),
            )
        };
        if res != 0 {
            log::error!(
                "JavaCallback::get_jnienv: AttachCurrentThread failed: {}",
                res
            );
            JniEnvHolder {
                env: None,
                callback: self,
                need_detach: false,
            }
        } else {
            assert!(!env.is_null());
            JniEnvHolder {
                env: Some(env),
                callback: self,
                need_detach: true,
            }
        }
    }

I suppose I missed some details, all of them you can find in generated code.

2 Likes

Thanks for the detailed description, I will try to follow your suggestions.
Marking as resolved.

About the error, it is a known issue in Mac OS, I forgot I already faced it before. Without JDK I would not be able to build a project.

Hi
I tried to follow your suggestions: added multithreading to your Android example - it is working fine, checked generated code and tried to copy it to the new project.
But I faced many errors, I could not compile the code:

  1. set static lifecycle for JNIenv
  2. jmethodID cannot be passed between threads, and other

When I added it rust_swig as a dependency to the new project with the same glue

[build-dependencies]
rust_swig="*"

I'm getting an error:

 failed to run custom build command for `cargo v0.1.0 (/Users/Project/Rust/greetings/cargo)`

Caused by:
  process didn't exit successfully: `/Users/Project/Rust/greetings/cargo/target/release/build/cargo-970e4c126cf88ee7/build-script-build` (exit code: 101)
--- stderr
error in android bindings: src/java_glue.rs.in
parsing of android bindings: src/java_glue.rs.in failed
error: Do not know conversation from Java type to such rust type 'Box < dyn OnThreadEvent + Send >'
    fn f(cb: Box<dyn OnThreadEvent + Send>) {
             ^^^

At android bindings: src/java_glue.rs.in:13:13
thread 'main' panicked at 'explicit panic', /Users/SDK/Rust/cargo/registry/src/github.com-1ecc6299db9ec823/rust_swig-0.4.0/src/error.rs:88:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

This example is working:

// ANCHOR: api
foreign_class!(class Session {
    self_type Session;
    constructor Session::new() -> Session;
    fn Session::add_and1(&self, val: i32) -> i32;
    fn Session::greet(to: &str) -> String;
});
// ANCHOR_END: api


Env:

cargo 1.42.0 (86334295e 2020-01-31)
rustc 1.42.0 (b8cedc004 2020-03-09)
rust_swig= "0.4.0"
run e.g.: CC=aarch64-linux-android21-clang cargo build --target aarch64-linux-android --release

Does it depends on Rust versions or anything else?

I suppose the problem in dyn keyword. Its support there is only in github, so the version from crates.io have no support for it. unsafe impl Send for JavaCallback {} I suppose is also github only stuff. So you need:

[build-dependencies]
rust_swig = { git = "https://github.com/Dushistov/rust_swig.git", rev = "1835ebd90a0feef5ee74b0fbe69b47de36d30c03" }
1 Like

Working, thanks!
Looking forward to 0.5 :slight_smile:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.