astonbitecode / j4rs

Java for Rust
Apache License 2.0
643 stars 36 forks source link

Android calls JvmBuilder::new() with an error. #102

Closed mzdk100 closed 4 months ago

mzdk100 commented 7 months ago

I have created a new project:

cargo new test --lib
cd test

File src/lib.rs:

use android_activity::AndroidApp;
use j4rs::jni_sys::{JavaVM, jint, jobject};
use j4rs::JvmBuilder;

#[no_mangle]
fn android_main(app: AndroidApp) {
    let jvm = JvmBuilder::new().build().unwrap();
    println!("{:?}", app);
}

const JNI_VERSION_1_6: jint = 0x00010006;

#[allow(non_snake_case)]
#[no_mangle]
pub extern fn jni_onload(env: *mut JavaVM, _reserved: jobject) -> jint {
    j4rs::set_java_vm(env);
    JNI_VERSION_1_6
}

File cargo.toml:

[package]
name = "test"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[lib]
name = "test"
crate-type = ["cdylib"]

[dependencies]

[dependencies.android-activity]
features = ["native-activity"]
version = "0.5.2"

[dependencies.j4rs]
version = "0.18.0"

Then use cargo-apk to build:

cargo apk run

Output:

...
04-27 21:17:40.671 20603 20677 I RustStdoutStderr: thread '<unnamed>' panicked at src\lib.rs:7:41:
04-27 21:17:40.672 20603 20677 I RustStdoutStderr: called `Result::unwrap()` on an `Err` value: GeneralError("Error { kind: PermissionDenied, message: \"Permission denied (os error 13)\" }")
04-27 21:17:40.672 20603 20677 I RustStdoutStderr: note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
04-27 21:17:40.673 20603 20678 E RustPanic: called `Result::unwrap()` on an `Err` value: GeneralError("Error { kind: PermissionDenied, message: \"Permission denied (os error 13)\" }")
...

May I ask if I missed anything?

astonbitecode commented 7 months ago

You should create the Jvm defining detach_thread_on_drop(false):

let jvm = JvmBuilder::new().detach_thread_on_drop(false).build().unwrap();

in order not to detach the main thread on Jvm drop.

mzdk100 commented 7 months ago

I modified src/lib.rs:

use android_activity::AndroidApp;
use j4rs::jni_sys::{JavaVM, jint, jobject};
use j4rs::JvmBuilder;

#[no_mangle]
fn android_main(app: AndroidApp) {
    let jvm = JvmBuilder::new().detach_thread_on_drop(false).build().unwrap();
    println!("{:?}", app);
}

const JNI_VERSION_1_6: jint = 0x00010006;

#[allow(non_snake_case)]
#[no_mangle]
pub extern fn jni_onload(env: *mut JavaVM, _reserved: jobject) -> jint {
    j4rs::set_java_vm(env);
    JNI_VERSION_1_6
}

But it still hasn't solved the problem:

...
04-29 23:44:21.356 12123 12278 I RustStdoutStderr: thread '<unnamed>' panicked at src\lib.rs:7:70:
04-29 23:44:21.356 12123 12278 I RustStdoutStderr: called `Result::unwrap()` on an `Err` value: GeneralError("Error { kind: PermissionDenied, message: \"Permission denied (os error 13)\" }")
04-29 23:44:21.356 12123 12278 I RustStdoutStderr: note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
04-29 23:44:21.357 12123 12279 E RustPanic: called `Result::unwrap()` on an `Err` value: GeneralError("Error { kind: PermissionDenied, message: \"Permission denied (os error 13)\" }")
...
astonbitecode commented 7 months ago

I overlooked your question, sorry.

The main problem with this approach is that j4rs cannot work without the j4rs jar file (currently j4rs-0.18.0-jar-with-dependencies.jar). This means that the jar should be available in the classpath of the created JavaVM.

Using the android_activity, the JavaVM is created by this crate and I am not familiar with it in order to tell if it is possible to add something in the classpath it creates.

You could have a look at this example which shows how you can use j4rs in Android.

mzdk100 commented 7 months ago

I may have made a mistake, perhaps I should use 'jni-rs' crate directly.

astonbitecode commented 7 months ago

Well, it depends what you want to do. Please correct me if I am wrong, but I guess things can get clunky if you decide to have everything in native code...

I am aware that someone could have a full android app without writing Java code, but from the other side, even the examples of android-activity include at least some Java/Gradle setup...

mzdk100 commented 6 months ago

I understand what you mean, but I would like to use only cargo to build Android applications. But currently, the cargo-apk crate cannot compile Java or specify a jar path, which is regrettable.

astonbitecode commented 6 months ago

Meanwhile, I have asked the android-activity devs whether it is possible to add any java classes or jars to the classpath of the JavaVM being created.

If the answer is positive, then I guess we can make j4rs usable in this context too.

mzdk100 commented 6 months ago

To the best of my knowledge, the AndroidApp struct of android-activity provides a method called vm_as_ptr. I tried using j4rs::set_java_vm (app.vm_as_ptr()); Writing this way also has no effect. I found that the jni_onload function required by j4rs has not been run at all.

astonbitecode commented 6 months ago

Yes, I also saw this... the jni_onload is called when the JVM is initialized by Android when we have a Java/Kotlin app. Here, as you also understood, we need to

  1. Set the created JavaVM for j4rs:
    j4rs::set_java_vm (app.vm_as_ptr());
  2. Create a j4rs Jvm like:
    let mut jvm = Jvm::attach_thread();
    jvm.detach_thread_on_drop(false);

    However, we cannot get any further than that yet, because as mentioned earlier, we need the JavaVM that is supplied by app.vm_as_ptr() to have the j4rs-0.18.0-jar-with-dependencies.jar in the classpath.

mzdk100 commented 6 months ago

Understood, but I haven't found a place to set the jar path either.

MarijnS95 commented 6 months ago

But currently, the cargo-apk crate cannot compile Java or specify a jar path, which is regrettable.

Maybe xbuild can? The two crates are in a bit of a disconnected state, but xbuild allows you to include arbitrary files a bit more easily at least.

~As far as I'm aware Android has a default class search path inside the APK, so embedding it should be "as easy as" embedding it in the right folder.~

~This is after all what android-activity requires for the game-activity backend which provides a portion of Java code, but it's all set up via a gradle project: https://github.com/rust-mobile/android-activity/tree/main/examples/agdk-mainloop~

EDIT: A more correct reply is in https://github.com/rust-mobile/android-activity/issues/159#issuecomment-2089124701. Android / gradle "dex" your class and jar files into classes.dex, if that file is in the APK root (unsure if cargo-apk/xbuild allow you to do that easily now), Android should be able to find the class file.

You can look into https://developer.android.com/tools/d8 to "dex" the file by hand.

MarijnS95 commented 6 months ago

I've quickly hacked together a feature on xbuild to put the classes.dex file in the root via the android: dexes: [] manifest property, and pushed a little repo to start testing it out. The precompiled .class file is easily replaced with a .jar file when invoking d8: https://github.com/MarijnS95/android-support

mzdk100 commented 6 months ago

Compared to cargo-apk, I tested that the xbuild tool is not very useful, at least in my Windows 10 environment, I have not successfully built Android applications. Compared to xbuild, a better option would be cargo-mobile2. At least the code repository for cargo-mobile2 is more active, but it is also complex and requires configuration of the Gradle environment.

astonbitecode commented 6 months ago

I've quickly hacked together a feature on xbuild to put the classes.dex file in the root via the android: dexes: [] manifest property, and pushed a little repo to start testing it out. The precompiled .class file is easily replaced with a .jar file when invoking d8: https://github.com/MarijnS95/android-support

Thanks very much for this @MarijnS95 ! However, the link returns a 404.

mzdk100 commented 6 months ago

That's right, it shows page not found.

MarijnS95 commented 6 months ago

That tends to happen, the default visibility for new repositories is "private" and I hadn't noticed when uploading late at night. Should be fixed now.

Compared to cargo-apk, I tested that the xbuild tool is not very useful, at least in my Windows 10 environment, I have not successfully built Android applications. Compared to xbuild, a better option would be cargo-mobile2. At least the code repository for cargo-mobile2 is more active, but it is also complex and requires configuration of the Gradle environment.

Unfortunately the Rust Android ecosystem is plagued heavily by NIH syndrome, there are far too many crates and tools attempting to do the same thing, all failing or lacking in certain areas rather than bundling their strength in one coherent system/crate/tool/approach.

However, cargo-mobile2 is a completely different beast that serves a completely different purpose. cargo-apk and xbuild allow you to build applications natively (with the help of a varying number of system tools, and even leveraging gradle when enabling AAB support in xbuild). cargo-mobile(2) generates a gradle Android Studio project to continue your development from, rather than being able to build from an almost-bare-bones Cargo.toml project.

astonbitecode commented 6 months ago

I forked your repo @MarijnS95 and made some changes in order o bring it closer to using j4rs. I updated the README with some more details on how to create the classes.dex.

However, during the apk generation, I get the following error:

[2/3] Build rust example [628ms] [3/3] Create apk Error: Failed to collect all required libraries for /git/android-support-j4rs/target/x/debug/android/x64/cargo/x86_64-linux-android/debug/libexample.so with [ "/.cache/x/Android.ndk/usr/lib/x86_64-linux-android", "/.cache/x/Android.ndk/usr/lib/x86_64-linux-android/21" ] available libraries and [ "/git/android-support-j4rs/target/x/debug/android/x64/cargo/x86_64-linux-android/debug/deps" ] shippable libraries

Can you maybe understand what I miss?

mzdk100 commented 6 months ago

Actually, my goal is to use pure rust to build Android applications. Cargo-apk is the best build tool I have ever used, but unfortunately it has been marked as deprecated.

mzdk100 commented 6 months ago
PS E:\repositories\rust\android-support> x run -p example --device adb:rsga9xzlhmbecelv [1/3] Fetch precompiled artifacts info: component 'rust-std' for target 'aarch64-linux-android' is up to date Android.ndk.tar.zst [27s] ██████████████████████████████████████████████████████████ 66.73 MiB/66.73 MiB 📥 downloadedDownloading platforms;android-33 Extracting platforms;android-33 [1/3] Fetch precompiled artifacts [75627ms] [2/3] Build rust example Updating crates.io index Downloaded ndk v0.9.0 Downloaded android-activity v0.6.0 Downloaded ndk-sys v0.6.0+11769913 Downloaded 3 crates (673.4 KB) in 3.11s Compiling proc-macro2 v1.0.81 Compiling unicode-ident v1.0.12 Compiling equivalent v1.0.1 Compiling hashbrown v0.14.5 Compiling winnow v0.5.40 Compiling toml_datetime v0.6.5 Compiling thiserror v1.0.59 Compiling indexmap v2.2.6 Compiling quote v1.0.36 Compiling syn v2.0.60 Compiling once_cell v1.19.0 Compiling jobserver v0.1.31 Compiling jni-sys v0.3.0 Compiling toml_edit v0.21.1 Compiling cc v1.0.96 Compiling log v0.4.21 Compiling memchr v2.7.2 Compiling proc-macro-crate v3.1.0 Compiling libc v0.2.154 Compiling bytes v1.6.0 Compiling combine v4.6.7 Compiling android-activity v0.6.0 Compiling ndk-sys v0.6.0+11769913 Compiling thiserror-impl v1.0.59 Compiling num_enum_derive v0.7.2 Compiling cfg-if v1.0.0 Compiling bitflags v2.5.0 Compiling cesu8 v1.1.0 Compiling ndk-context v0.1.1 Compiling android-properties v0.2.2 Compiling num_enum v0.7.2 Compiling ndk v0.9.0 Compiling jni v0.21.1 Compiling android-support v0.1.0 (E:\repositories\rust\android-support) warning: unused Result that must be used --> src\lib.rs:22:5 22 dbg!(r.v()); ^^^^^^^^^^^

= note: this Result may be an Err variant, which should be handled = note: #[warn(unused_must_use)] on by default = note: this warning originates in the macro dbg (in Nightly builds, run with -Z macro-backtrace for more info)

warning: android-support (lib) generated 1 warning Compiling example v0.1.0 (E:\repositories\rust\android-support\example) error: linker clang not found | = note: program not found

error: could not compile example (lib) due to 1 previous error PS E:\repositories\rust\android-support>

astonbitecode commented 6 months ago

@mzdk100 , use x doctor to see what dependencies you miss.

mzdk100 commented 6 months ago
x run -p example --device adb:rsga9xzlhmbecelv

Output with errors:

28054 28102 I RustStdoutStderr: thread '<unnamed>' panicked at src\lib.rs:21:10:
28054 28102 I RustStdoutStderr: called `Result::unwrap()` on an `Err` value: JavaException
28054 28102 I RustStdoutStderr: note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
28054 28103 E RustPanic: called `Result::unwrap()` on an `Err` value: JavaException
--------- beginning of crash

28054 28103 E AndroidRuntime: FATAL EXCEPTION: Thread-5
28054 28103 E AndroidRuntime: Process: com.example.example, PID: 28054
28054 28103 E AndroidRuntime: java.lang.ClassNotFoundException: Didn't find class "com.example.example.Activity" on path
: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64, /system/lib64, /system_ext/lib
64]]
28054 28103 E AndroidRuntime:   at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:259)
28054 28103 E AndroidRuntime:   at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
28054 28103 E AndroidRuntime:   at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
28054 28103 I Process : Process is going to kill itself!
28054 28103 I Process : java.lang.Exception
28054 28103 I Process :         at android.os.Process.killProcess(Process.java:1330)
28054 28103 I Process :         at com.android.internal.os.RuntimeInit$KillApplicationHandler.uncaughtException(RuntimeI
nit.java:195)
28054 28103 I Process :         at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1073)
28054 28103 I Process :         at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1068)
28054 28103 I Process :         at java.lang.Thread.dispatchUncaughtException(Thread.java:2306)
28054 28103 I Process : Sending signal. PID: 28054 SIG: 9
MarijnS95 commented 4 months ago

@mzdk100 heya, sorry for leaving this hanging. https://github.com/MarijnS95/android-support had a bug where it relied on the JVM to provide the class, but as seen above that fails with java.lang.ClassNotFoundException: Didn't find class "com.example.example.Activity" on path : DexPathList[[directory "."],...]. The search path for dex files (containing Java classes) here is empty, while for an APK it should be something like DexPathList[[zip file "/data/app/~~somerandomstring==/com.example.example-morerandomness==/base.apk"],...].

Looking at how Android spawns Activitys from an APK, it's simply taking the ClassCloader from android.content.Context.getClassLoader() and calling java.lang.ClassLoader.loadClass("the/class/we/Need") on it. I've pushed a commit now that does that (via the NativeActivity instance, which is a subclass of Context), and it can finally find and call into the rust/android_support/Activity class now!

mzdk100 commented 4 months ago

Thank you for your work, but I have already resolved these issues. You can take a look at my work droid-wrap. It can easily and elegantly access the APIs of the Android ecosystem. In droid-wrap, I use InMemoryDexClassLoader to dynamically load classesfull code at:

fn load_rust_call_method_hook_class<'a>() -> &'a GlobalRef {
    #[cfg(target_os = "android")]
    const BYTECODE: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/classes.dex"));
    #[cfg(not(target_os = "android"))]
    const BYTECODE: &[u8] = &[];
    const LOADER_CLASS: &str = "dalvik/system/InMemoryDexClassLoader";
    static INSTANCE: OnceLock<GlobalRef> = OnceLock::new();
    INSTANCE.get_or_init(|| {
        vm_attach!(mut env);
        let byte_buffer = unsafe { env.new_direct_byte_buffer(BYTECODE.as_ptr() as *mut u8, BYTECODE.len()) }.unwrap();
        let dex_class_loader = env
            .new_object(
                LOADER_CLASS,
                "(Ljava/nio/ByteBuffer;Ljava/lang/ClassLoader;)V",
                &[
                    JValueGen::Object(&JObject::from(byte_buffer)),
                    JValueGen::Object(&JObject::null()),
                ],
            ).unwrap();
        let class = env.new_string("rust/CallMethodHook").unwrap();
        let class = env
            .call_method(
                &dex_class_loader,
                "loadClass",
                "(Ljava/lang/String;)Ljava/lang/Class;",
                &[(&class).into()],
            )
            .unwrap()
            .l()
            .unwrap();
        let m = NativeMethod {
            name: "invoke".into(),
            sig: "(Ljava/lang/Object;Ljava/lang/reflect/Method;[Ljava/lang/Object;)Ljava/lang/Object;".into(),
            fn_ptr: rust_callback as *mut _,
        };
        env.register_native_methods(Into::<&JClass<'_>>::into(&class), &[m]).unwrap();
        env.new_global_ref(&class).unwrap()
    })
}

By using this method, it is not necessary to package .class into the apk, so it can be easily built using the cargo-apk tool.

MarijnS95 commented 4 months ago

@mzdk100 then this issue should have been closed if you already figured out how to fix this. Note that I cannot read your crate at all :wink:

By using this method, it is not necessary to package .class into the apk, so it can be easily built using the cargo-apk tool.

This isn't very extensible if users also want to embed and use managed Activities written in Java/Kotlin, that's why I'll be adding the mentioned functionality to build tools either way.

mzdk100 commented 4 months ago

Yes, this issue can be closed, thanks.

MarijnS95 commented 4 months ago

@mzdk100 as the author of this issue you should also have the rights to close it.