Open danbrownuk opened 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:
native void doNativeStuff();
.com/example/MyClass
.com/example/MyClass
.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.
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!
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.
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
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
.
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.