Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Limitations of this library #1

Open
mzdk100 opened this issue Jan 1, 2025 · 16 comments
Open

Limitations of this library #1

mzdk100 opened this issue Jan 1, 2025 · 16 comments

Comments

@mzdk100
Copy link

mzdk100 commented Jan 1, 2025

I am planning to use some of the convert functions in this library and do not intend to use proxies and BroadcastReceiver. In this case, I do not want to run the build script(build.rs) provided by jni-min-helper.
So I hope to add some features to achieve this, thank you.

@wuwbobo2021
Copy link
Owner

I don't know how to disable the build script by some cargo feature, but it's possible to check enabled features and conditionally compile Java code in the build script.

However, the current build script doesn't slow down compilation significantly. I have removed the android-build (which requires complicated env variable configuration) from build dependency. So it should be done silently if the JDK and Android SDK are installed (JAVA_HOME and ANDROID_HOME were set).

@mzdk100
Copy link
Author

mzdk100 commented Jan 1, 2025

Then let's use this plan: check enabled features and conditionally compile Java code in the build script.

@wuwbobo2021
Copy link
Owner

I have updated this library for your requirement.

Still I wonder why you have came up with this idea. Actually, I wouldn't have built this crate without the need of the proxy feature. Waiting for your complaint about my proxy table implementation.

I have changed the comment "Based on" to "Inspired by" in the build script, avoiding possible GPL "pollution" from i-slint-backend-android-activity. I dare to do so, because I'm not just making a few modifications to that build script. I may ask one of the Slint authors to confirm this right.

The usage of JNI forced by Android means a lot of pain for me even in Rust (with the current ecosystem):

  • The jni-rs library has many unsound flaws beside the few common JNI pitfalls that can be managed by this helper crate (check the issues list of jni-rs), it should be considered "half-unsafe". While managing to be "transparent" with the JNI C interface for the Java-to-Rust callback passthrough without help of intermediate mappers and macros, it keeps making breaking changes in recent versions. It looks unmaintained for several months, though. (the author rib is busy working in his private repositories.)

  • On the other hand, j4rs claims itself to be a higher level crate, but it uses GlobalRef for almost all JNI references, which means bad performance (JNIEnv::{push,with}_local_frame can free JObjects before their lifetime ends (use-after-free) jni-rs/jni-rs#392); currently it exposes some ugly JNI callback functions in the API, which made me doubt about the reliability of this crate; The Android support relies on Gradle, and probably xbuild which looks unmaintained (JNI_OnLoad-like callback to load APK classes with env.FindClass()? rust-mobile/android-activity#169).

I think there should be another crate that locates itself between jni and j4rs (or maybe a future version of jni), ensuring soundness with minimum cost. I am not able to do this enormous task by myself, but made this helper crate available as a compromise (which is a painful decision). After that, I realized the BLE support crate bluest is using java-spaghetti for Android, and it's receiving callbacks. I'm don't know if it's a perfect solution, though.

I have to admit that the dex embedding mechanism learnt from i-slint-backend-android-activity means difficulties of importing dependencies like androidx or javassist (the later is probably capable of building objects of abstract classes at runtime). I'm hesitating about issue rust-mobile/android-activity#174, wondering about the possibility of building proxy-based class (like BroadcastRec) for any abstract class in the build script (there should be a new crate to be used as a build dependency). The ownership of this repo will be transferred to you if you're willing to do it.

I would have been using Flutter (instead of Rust) if Dart had become more popular. Currently Dart is becoming less popular on TIOBE.

It seems like you have started developing droid-wrap in the hopes of supporting your "RigelA" screen reader on Android, which will be difficult when it comes to permission issues and all sorts of custom renderers...

PS: can you help out with https://github.com/slint-ui/slint/issues/7203, or make an OHOS backend for Slint? I had considered the advantage of this OS again, after realizing the fact that Android creates a JVM instance even for each native application. Note that I had disliked the HarmonyOS, thinking it's merely better than iOS. But if OHOS-based systems were to become popular enough around the world with its advantages, then efforts made for Rust cross-platform support would be worthless without having them in the support list. (However, I'm losing interest in mobile development, and my repos should find their new owners.)

@mzdk100
Copy link
Author

mzdk100 commented Jan 3, 2025

Yes, I am trying to build a macro based on a series of features of droid-wrap to generate Java class files at compile time, so that some Java classes can be embedded into the apk before packaging, making it more closely integrated with the Android ecosystem.
In this case, declaring a Java class in Rust becomes much easier, for example:

use droid_wrap::android::{app::Activity, os::Bundle};
#[java_class("com/sscn/MainActivity", extends = Activity)]
struct MainActivity;
impl MainActivity {
    #(java_method(override = true)]
    fn on_create(&self, state: &Bundle) {
        self.super().on_create(state);
        println!("Hello world")
    }
}

During the build, a dex file containing the MainActivity class will be automatically generated. This work will gradually be carried out in 2025, but there are still many preparatory tasks to be done, such as I am trying to implement a Java syntax parser to automatically generate Rust code based on the droid-wrap library from Java source files.
Although this seems particularly complex, Rust has given me many surprises and the future looks very promising, and I firmly believe that all this is necessary.
However, I am sorry that you do not plan to maintain your library. If possible, would you agree to migrate your code to the droid-wrap-utils library?
For the many problems with the jni-rs library that you mentioned, I also have no plans to optimize and solve them temporarily. I don't have much time, thank you for understanding.

@wuwbobo2021
Copy link
Owner

Feel free to migrate any parts of my code into your droid-wrap-utils. Please note about the original source of the code if you're not going to make significant modifications, in case of it were to cause any confusion.

My suggestion: the usage of java-spaghetti is worth studying, it can be a replacement of jni-rs.

@mzdk100
Copy link
Author

mzdk100 commented Jan 4, 2025

Thank you.

@mzdk100
Copy link
Author

mzdk100 commented Jan 4, 2025

Hi, I took a look at the java-spaghetti project and it's well-designed. It's a fork of jni-bindgen, so the main issue I have with it is that it only supports generating .rs files from .jar or .class files, which is not what I'm satisfied with. This is because it doesn't scan Javadoc.

My goal is to generate .rs files from .java source files, which would capture more Java details. So, I plan to implement everything myself.

However, I've noticed that the jni-sys version used by jni-rs is quite outdated, and the current version should be 0.4. So, I plan to build droid-wrap directly from jni-sys.

Regardless, thank you for your support in my work. If possible, I would also appreciate it if you continue to follow the development of droid-wrap.

@wuwbobo2021
Copy link
Owner

wuwbobo2021 commented Jan 6, 2025

Macro implementations seem to be complicated, but I'm thinking about this: classes needed by the native code should eventually be converted to dex data for the runtime class loader. If D8 is invoked by some build dependency, then these macros need to communicate with that dependency. If the dex data should be generated at runtime, we need a crate similar to noak which outputs dex format data. There's a dex parser written in Rust: https://crates.io/crates/dex. To build the dex file in memory, there are https://mavenlibs.com/jar/file/com.jakewharton.android.repackaged/dalvik-dx (this one is used by https://github.com/linkedin/dexmaker) and https://github.com/LSPosed/DexBuilder, both aren't written in Rust.

@wuwbobo2021 wuwbobo2021 reopened this Jan 6, 2025
@wuwbobo2021 wuwbobo2021 changed the title I hope to compile InvocHdl.java and BroadcastRec.java as optional steps. Limitations of this library Jan 6, 2025
@mzdk100
Copy link
Author

mzdk100 commented Jan 6, 2025

I don't intend to build dex in memory because this method is far more complicated than macro implementation. Moreover, there's a significant issue: Android systems cannot recognize some classes, such as <activity android:name="com.sscn.MainActivity"/>. If MainActivity is built in memory, the system will not be able to find our class correctly. Therefore, generating MainActivity at compile time is a pressing matter.

@wuwbobo2021
Copy link
Owner

wuwbobo2021 commented Jan 7, 2025

I cannot understand it. I guess you mean that you cannot get the class object of MainActivity at runtime. Why did you think MainActivity needs to be built by macros? NativeActivity with a native UI renderer is probably enough for me; for handling activity callbacks (e.g. a permission request result receiver), I may define a class that extends Activity and overrides some callbacks, some dialog should be opened to block this empty Activity, then wait until the result is received from the callback; eventually, the dialog is hidden, and I should close the new activity, switching back to the native activity. I don't want to load some Java activity in some existing Android application, merely overriding some methods of the activity.

"this method is far more complicated than macro implementation." I think the dalvik-dx Java component can be turned into a dex file by the jni-min-helper build script (this should be removed when a Rust dex builder becomes available), then it can be used to generate dex data at runtime, after dalvik-dx.dex is loaded by the dex class loader; extended classes should call the invocation handler (to be registered on object creation) in every overrided method. On PC platforms, using noak and JNI DefineClass is enough.

A comment from dexmaker/src/main/java/com/android/dx/stock/ProxyBuilder.java: Only non-private, non-final, non-static methods will be dispatched to the invocation handler. Private, static or final methods will always call through to the superclass as normal. (I'm not going to use dexmaker in my crate, but this can be a reference)

@mzdk100
Copy link
Author

mzdk100 commented Jan 7, 2025

My intention is that the MainActivity defined in the Manifest is directly called by the system, in this case, our memory dex has not been loaded yet, so the system cannot find this class. I thought it would be simpler to build MainActivity using macros because my droid-wrap library already has a similar macro, which can be slightly modified to achieve this effect; secondly, my true intention is to build this class at compile time and write it into the apk file as a classes.dex file, thereby solving the problem I mentioned at the beginning. Macros can generate java class files at compile time, and then be packaged using cargo-apk2, resulting in an apk file that includes classes.dex, which is perfect. In fact, as you said, we should not use Rust to build MainActivity, I just used it as an example because I am solving the problem of building a Service (for example, MyService inherits from android.accessibilityservice.AccessibilityService, and then this MyService needs to be declared in the Manifest).

@wuwbobo2021
Copy link
Owner

@mzdk100 Would you like to check it: jni-rs/jni-rs#560

@wuwbobo2021
Copy link
Owner

wuwbobo2021 commented Jan 23, 2025

Would you please check jni-rs/jni-rs#558 (comment)?

@mzdk100
Copy link
Author

mzdk100 commented Jan 23, 2025

Thank you, I will take note and explore this issue.

@wuwbobo2021
Copy link
Owner

wuwbobo2021 commented Mar 10, 2025

Note: the disadvantage of native implementations of Java abstract classes backed by dynamic proxies is its bad performance.

The test result on OpenJDK 8 shows that the cost of using Java proxy is about 180% greater than direct JNI call (2.8x), and the cost of using jni-min-helper proxy is about 250x greater than direct JNI call. I don't think it is possible to improve it significantly.

This is bad for power saving on mobile devices, if such callbacks are invoked frequently.

The possible advantage is about safety, but it seems like a minor issue which should be managed by the programmer: jni-rs/jni-rs#526. The jni-min-helper proxy builder API shouldn't introduce any UB, and an ClassCastException or NullPointerException will be thrown on invocation if the backing Rust closure doesn't return correctly, see https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/InvocationHandler.html.

Here's my test case, in which mylib is used by the Java main program:

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

[dependencies]
jni-min-helper = "0.3.0"

[lib]
crate-type = ["cdylib"]
use jni_min_helper::*;
use jni::JNIEnv;
use jni::objects::JClass;
use jni::sys::{jint, jobject};

#[no_mangle]
pub extern "system" fn Java_JniTest_plusOne<'local>(
    _env: JNIEnv<'local>,
    _class: JClass<'local>,
    num: jint
) -> jint {
    num + 1
}

#[no_mangle]
pub extern "system" fn Java_JniTest_getProxy<'local>(
    ref mut env: JNIEnv<'local>,
    _class: JClass<'local>,
) -> jobject {
    let class_loader = JniClassLoader::app_loader().unwrap();
    let intr = class_loader.load_class("JniTest$PlusOne").unwrap();
    let proxy = JniProxy::build(
        env,
        Some(&class_loader),
        [intr.as_class()],
        |env, method, args| {
            if method.get_method_name(env)? == "plusOne" {
                (args[0].get_int(env)? + 1).new_jobject(env)
            } else {
                JniProxy::void(env)
            }
        }
    ).unwrap();
    
    let proxy_local = env.new_local_ref(&proxy).unwrap();
    let _ = proxy.forget();
    proxy_local.into_raw()
}

#[no_mangle]
pub extern "system" fn JNI_OnLoad(jvm: jni::JavaVM, _: *mut ()) -> jint {
    unsafe { jni_set_vm(&jvm) };
    jni::sys::JNI_VERSION_1_2
}

method.get_method_name(env)? == "plusOne" is preserved here to emulate a realistic usage: there will be multiple methods in the interface. Actually, the method object can be obtained by the code below, but is_same_object returns false, and using equals to check them is even slower than get_method_name.

Get method object
    let method_plus_one = {
        let methods = env.call_method(&intr, "getMethods", "()[Ljava/lang/reflect/Method;", &[])
            .get_object(env)
            .unwrap();
        let methods: &JObjectArray = methods.as_ref().into();
        env.get_object_array_element(&methods, 0).global_ref(env).unwrap()
    };

The main program:

JniTest.java
import java.lang.ClassLoader;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

class JniTest {
    public interface PlusOne {
        int plusOne(int num);
    }
    
    private static native int plusOne(int num);
    private static native PlusOne getProxy();
    
    static {
        System.loadLibrary("mylib");
    }

    public static void main(String[] args) {
        long t1 = System.nanoTime();
        
        int i = 0;
        while (i < 1024*1024) {
            i = JniTest.plusOne(i);
        }

        long t2 = System.nanoTime();
        
        i = 0;
        PlusOne proxy = (PlusOne) JniTest.getProxy();
        while (i < 1024*1024) {
            i = proxy.plusOne(i);
        }

        long t3 = System.nanoTime();
        
        JavaHdl java_hdl = new JavaHdl();
        Class[] intrs = { PlusOne.class };
        PlusOne java_proxy = (PlusOne) Proxy.newProxyInstance(
            ClassLoader.getSystemClassLoader(), intrs, java_hdl);
        i = 0;
        while (i < 1024*1024) {
            i = java_proxy.plusOne(i);
        }
        
        long t4 = System.nanoTime();
        
        System.out.println("JNI Direct Call: " + (t2 - t1));
        System.out.println("JNI Dynamic Proxy: " + (t3 - t2));
        System.out.println("Java Dynamic Proxy: " + (t4 - t3));
    }
}

class JavaHdl implements InvocationHandler {
    public JavaHdl() {}

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        if (methodName.equals("plusOne")) {
            return ((Integer) args[0]).intValue() + 1;
        }
        return null;
    }
}

@mzdk100
Copy link
Author

mzdk100 commented Mar 10, 2025

My plan is to use a static proxy, which means that necessary '.class' will be automatically generated during Rust build, and then Java will call back the native method directly (your first case, jni direct call). This can achieve higher customization requirements and is not limited to interfaces. Regarding the performance issue you mentioned, I believe there is currently no better solution available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants