jni-rs / jni-rs

Rust bindings to the Java Native Interface — JNI
Apache License 2.0
1.23k stars 158 forks source link

call_method overriding class reference (Classloader issue) #487

Open danbrownuk opened 1 year ago

danbrownuk commented 1 year ago

I'm working on a project with multiple classloaders and used JNI-rs to manually locate the classloader I need and then called the findClass function via JNI from that to get a reference to the class file I need.

Taking this class and calling call_method on it produces a JavaError, as the call_method checks pass, but then the function call to call_method_unchecked fails as two different class objects are passed to it, one of them being looked up again by the library, which uses the default classloader.

Specifically jnienv.rs: https://github.com/jni-rs/jni-rs/blob/cab39b239d46eb4367efdb1a3283340296eb42a6/src/wrapper/jnienv.rs#L1411 Here obj is the JClass passed directly to the call_method function (found via custom classloader) &class is a borrow of the Class, but is looked via the JVM and so when obj has been obtained from a different classloader than the default, obj != class https://github.com/jni-rs/jni-rs/blob/cab39b239d46eb4367efdb1a3283340296eb42a6/src/wrapper/jnienv.rs#L1404

Looking at call_static_method, the call to the unchecked version uses the same class reference for both parameters, and this is looked up again (which is perhaps the intended behaviour of this method, although it would be amazing to be able to pass a custom classloader somehow!) https://github.com/jni-rs/jni-rs/blob/cab39b239d46eb4367efdb1a3283340296eb42a6/src/wrapper/jnienv.rs#L1468

Here both the class file (first param class) and JMethodID obtained via Desc<> (second param with class) are the same.

argv-minus-one commented 1 year ago

I'm having a great deal of difficulty understanding your description of the problem.

JNIEnv::call_method calls an instance method on an object. The obj parameter is the object you're calling the method on, not the object's class. The class is looked up from the object using get_object_class, which works regardless of which class loader loaded the class. obj and class are never the same, regardless of class loaders, unless you're calling a method on the java.lang.Class class itself; this is correct behavior.

When you use JNIEnv::call_static_method, and you use a string as the class parameter, it loads the class with that name using a the class loader that loaded the class that the currently executing native method belongs to. For example, suppose the currently executing native method is native void doNativeStuff(); and it's declared on the class com/example/MyClass. When JNIEnv::call_static_method asks the JVM to find a class by the name com/example/SomeOtherClass, the JVM does the following:

  1. Look up which native method we're currently executing. That's native void doNativeStuff();.
  2. Look up which class that native method was declared on. That's com/example/MyClass.
  3. Get the class loader that loaded com/example/MyClass.
  4. Call the loadClass method on that class loader to load the class com/example/SomeOtherClass.

In most cases, this works fine, since when a native method needs to look up a class by name, that class will probably be found in the same jar as the native method itself is in, and therefore the same class loader should be used. But, if you want to load a class by name from a different class loader, this won't work, and you have to call loadClass yourself instead. That will look something like this:

let mut env: JNIEnv;

// This is a reference to whichever class loader you want to use.
let class_loader: JObject;

// This is the name of the class you want to call a static method on.
let the_class_name: JString = env.new_string("com/example/SomeOtherClass")?;

// Load the class with that name using that class loader.
let the_class: JClass = env.call_method(
    &class_loader,
    "loadClass",
    "(Ljava/lang/String;)Ljava/lang/Class;",
    &[
        (&the_class_name).into(),
    ],
)?;

// Now, call the static method.
env.call_static_method(
    &the_class,
    "someStaticMethod",
    "()V",
    &[],
)?;

Does that help? If not, please tell me more about what exactly you're trying to do.

danbrownuk commented 1 year ago

Thanks for the quick response! Apologies, I definitely struggled to get the issue cross clearly! Parking the call_static_method for now as I haven't tested and confirmed a problem there but was just adding the additional info as it's the two styles (static & instance) of calling a method. Thanks for the info on the static method though, that makes complete sense as there is no instance to call from and using the default classloader makes sense and agree that's by design.

The situation was when using call_method and passing the class obtained via another classLoader, I was unable to call any methods except standard inherited ones (getInterfaces, getDeclaredMethods etc from java/lang/Class). I confirmed these methods exist by calling getDeclaredMethods on the class file and dumping them out. When trying to call any of them by name via call_method, JVM returned an error (can replicate again & get the exact error, I believe it was MethodNotFound).

Manually fetching the method_id myself with get_method_id, passing it a JClass from the jobject obtained from the secondary classloader, and passing it to call_method_unchecked, I was able to successfully call the java function. Given call_method ultimately calls call_method_unchecked, I then compared how the library called it vs how I called it and concluded the difference was in defining the JMethodID. I was passing it as a result from get_method_id and the library defined it as a Desc. The implementation for JMethodID used in the Desc just calls get_method_id similar to how I call it, so figured the issue is in the &class param being passed in the tuple for the Desc, which is ultimately defined as: https://github.com/jni-rs/jni-rs/blob/cab39b239d46eb4367efdb1a3283340296eb42a6/src/wrapper/jnienv.rs#L1404

This led me to the conclusion that there's an issue because obj is derived from a jobject with class X from classloader 1, but class is derived from class X which looks to be from the default classloader.

I'm not super-sure, but I'm wondering if this could occur if two classloaders loaded the same class name, but they differ (either on-disk, or modified during load). Happy to share some code snippets & error logs if that helps here too, appreciate it's not easy to understand what's going on in my project & head!

argv-minus-one commented 1 year ago

The situation was when using call_method and passing the class obtained via another classLoader, I was unable to call any methods except standard inherited ones (getInterfaces, getDeclaredMethods etc from java/lang/Class).

java/lang/Class is final, so yes, those are the only methods you'll ever see on an instance of java/lang/Class.

This led me to the conclusion that there's an issue because obj is derived from a jobject with class X from classloader 1, but class is derived from class X which looks to be from the default classloader.

I'm not super-sure, but I'm wondering if this could occur if two classloaders loaded the same class name, but they differ (either on-disk, or modified during load).

I'm still struggling to understand you, sorry.

Each Java object has exactly one class. Each class has exactly one class loader. Instance methods are called on objects, not on classes. You can see this in the Java language, where you call an instance method with the syntax instance.method(), but this crate's JNIEnv::call_method works that way too.

If you're passing the object's class as the first parameter of JNIEnv::call_method, then you're calling a method on the java/lang/Class instance that represents the object's class, not on the object itself.

Happy to share some code snippets

Please do. That may help me understand.

danbrownuk commented 1 year ago

That makes sense, but in this case I have a copy of the object, an instance of a class called Client (which by definition has java/lang/Class as a parent) but it must implement these other methods otherwise getDeclaredMethods wouldn't return them all.

Here's my code snippet & associated debug log that shows call_method failing but manually fetching the method ID and calling call_method_unchecked succeeding.

let final_classLoader = env.call_method(classLoader, "getClassLoader", "()Ljava/lang/ClassLoader;", &[]).expect("oops").l().expect("oops");
dbg!(final_classLoader);

let val4 = env.call_method(final_classLoader, "loadClass", "(Ljava/lang/String;Z)Ljava/lang/Class;", &[JValue::from(*env.new_string("client").unwrap()),JValue::from(true)]).expect("Couldn't find class with class loader");

let myfunc2 = env.get_method_id(JClass::from(val4.l().unwrap()), "refreshChat", "()V").unwrap();
dbg!(myfunc2);

let ret_val = env.call_method_unchecked(val4.l().unwrap(), myfunc2, jni::signature::JavaType::Primitive(jni::signature::Primitive::Void), &[]).unwrap();
dbg!(ret_val);

let val3 = env.call_method(val4.l().unwrap(), "refreshChat", "()V", &[]).expect("Didn't call method safely").i().unwrap();
dbg!(val3);
[java-lib\src\lib.rs:141] final_classLoader = JObject {
    internal: 0x000000001caca070,
    lifetime: PhantomData,
}
[java-lib\src\lib.rs:150] myfunc2 = JMethodID {
    internal: 0x0000000027dee0a0,
    lifetime: PhantomData,
}
[java-lib\src\lib.rs:152] ret_val = Void
thread '<unnamed>' panicked at 'Didn't call method safely: JavaException', java-lib\src\lib.rs:154:82
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
2023-08-29 01:03:03 PDT [Thread-12] ERROR <class name hidden> - Uncaught exception:
java.lang.NoSuchMethodError: refreshChat
argv-minus-one commented 1 year ago

That makes sense, but in this case I have a copy of the object, an instance of a class called Client (which by definition has java/lang/Class as a parent)

No, it doesn't. java/lang/Class is final; nothing has it as a superclass. If class Client does not have an extends clause, then its superclass is java/lang/Object.

If Client.refreshChat is declared static in the Java source code, then you must call it with JNIEnv::call_static_method. If not, you must call it on an object of class Client, not on the java/lang/Class representing the Client class itself.

The call_method_unchecked in your code snippet has undefined behavior because it is calling a method on an object of the wrong class (java/lang/Class rather than …/Client). The only reason your program doesn't get an error at that point is because the JVM doesn't check whether it's valid before calling it, which, again, results in undefined behavior. That's why call_method_unchecked is unsafe.