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

[Java.Interop] Use Class.forName() as fallback to load Java classes #1326

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

jonpryor
Copy link
Member

Fixes: #23

Context: dotnet/android@aba2726
Context: b4d44e4
Context: https://github.com/xamarin/monodroid/commit/ed984a3a0bfbe71f6499a7855c647b5bc2b28466
Context: dotnet/android#7616

Commit b4d44e4 noted:

Android is..."special", in that not all threads get the same
ClassLoader behavior. Specifically, managed threads --
System.Threading.Thread instances -- get a different ClassLoader than
the main/UI thread on Android. (Untested, but the ClassLoader may
behave sanely if you use a java.lang.Thread instance instead. But who
wants to use java.lang.Thread instances...?)

dotnet/android#7616 provided additional context: JNIEnv::FindClass() behavior is tied to the thread that calls it, and one of the knock-on effects is that Java.Lang.JavaSystem.LoadLibrary("MyLib") doesn't work properly when invoked from a new System.Threading.Thread thread.

Which brings us to xamarin/mondroid@ed984a3a, which updated then Xamarin.Android to use java.lang.Class.forName(String) instead of ClassLoader.loadClass(String) because the "real" JDK cannot use ClassLoader.loadClass(String) to load array types:

Class c1 = Class.forName("[Ljava.lang.String;");    // works
Class c2 = ClassLoader.getSystemClassLoader()
    .loadClass("[Ljava.lang.String;");              // throws java.lang.ClassNotFoundException

Class c3 = Class.forName("[I");                     // works; array of int
Class c4 = ClassLoader.getSystemClassLoader()
    .loadClass("[I");                               // throws java.lang.ClassNotFoundException

Using ClassLoader.loadClass(String) to load array types works on Android, presumably as an undocumented implementation detail, but as xamarin/monodroid@ed984a3a was trying to get things working within the (now dead) Android Designer -- which ran using a Desktop JDK -- Android-specific extensions were not available.

Update JniEnvironment.Types to use Class.forName(String) instead of ClassLoader.loadClass(String) to load Java classes, as a fallback for when JNIEnv::FindClass() fails to find the class.

Fixes: #23

Context: dotnet/android@aba2726
Context: b4d44e4
Context: xamarin/monodroid@ed984a3
Context: dotnet/android#7616

Commit b4d44e4 noted:

> Android is..."special", in that not all threads get the same
> ClassLoader behavior. Specifically, *managed* threads --
> System.Threading.Thread instances -- get a different ClassLoader than
> the main/UI thread on Android. (Untested, but the ClassLoader *may*
> behave sanely if you use a java.lang.Thread instance instead. But who
> wants to use java.lang.Thread instances...?)

dotnet/android#7616 provided additional context: `JNIEnv::FindClass()`
behavior *is* tied to the thread that calls it, and one of the knock-on
effects is that `Java.Lang.JavaSystem.LoadLibrary("MyLib")` doesn't
work properly when invoked from a new `System.Threading.Thread` thread.

Which brings us to xamarin/mondroid@ed984a3a, which updated then
Xamarin.Android to use [`java.lang.Class.forName(String)`][0] instead
of [`ClassLoader.loadClass(String)`][1] because the "real" JDK cannot
use `ClassLoader.loadClass(String)` to load array types:

	Class c1 = Class.forName("[Ljava.lang.String;");    // works
	Class c2 = ClassLoader.getSystemClassLoader()
	    .loadClass("[Ljava.lang.String;");              // throws java.lang.ClassNotFoundException

	Class c3 = Class.forName("[I");                     // works; array of int
	Class c4 = ClassLoader.getSystemClassLoader()
	    .loadClass("[I");                               // throws java.lang.ClassNotFoundException

Using `ClassLoader.loadClass(String)` to load array types works on
Android, presumably as an undocumented implementation detail, but as
xamarin/monodroid@ed984a3a was trying to get things working within
the (now dead) Android Designer -- which ran using a Desktop JDK --
Android-specific extensions were not available.

Update `JniEnvironment.Types` to use `Class.forName(String)` instead
of `ClassLoader.loadClass(String)` to load Java classes, as a fallback
for when `JNIEnv::FindClass()` fails to find the class.

[0]: https://developer.android.com/reference/java/lang/Class#forName(java.lang.String)
[1]: https://developer.android.com/reference/java/lang/ClassLoader#loadClass(java.lang.String)
jonpryor added a commit to dotnet/android that referenced this pull request Mar 19, 2025
Context: dotnet/android#9931
Context: dotnet/android#9931 (comment)

The attempted integration test crashed!

	F mono-rt : [ERROR] FATAL UNHANDLED EXCEPTION: Java.Lang.ClassNotFoundException: Didn't find class "crc641855b07eca6dcc03.MyCb" on path: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /system/product/lib64, /system/lib64, /system/product/lib64]]
	F mono-rt :    at Java.Interop.JniEnvironment.Types.TryFindClass(String classname, Boolean throwOnError)
	F mono-rt :    at Java.Interop.JniEnvironment.Types.FindClass(String classname)
	F mono-rt :    at Java.Interop.JniType..ctor(String classname)
	F mono-rt :    at Java.Interop.JniPeerMembers.JniInstanceMethods..ctor(Type declaringType)
	F mono-rt :    at Java.Interop.JniPeerMembers.JniInstanceMethods.GetConstructorsForType(Type declaringType)
	F mono-rt :    at Java.Interop.JniPeerMembers.JniInstanceMethods.StartCreateInstance(String constructorSignature, Type declaringType, JniArgumentValue* parameters)
	F mono-rt :    at Java.Lang.Object..ctor()
	F mono-rt :    at Java.InteropTests.MyCb..ctor()
	F mono-rt :    at Java.InteropTests.JnienvTest.<>c__DisplayClass29_0.<DoNotLeakWeakReferences>b__1()
	F mono-rt :    at System.Threading.Thread.StartHelper.Callback(Object state)
	F mono-rt :    at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
	F mono-rt : --- End of stack trace from previous location ---
	F mono-rt :    at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state)
	F mono-rt :   --- End of managed Java.Lang.ClassNotFoundException stack trace ---
	F mono-rt : java.lang.ClassNotFoundException: Didn't find class "crc641855b07eca6dcc03.MyCb" on path: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /system/product/lib64, /system/lib64, /system/product/lib64]]
	F mono-rt :      at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:196)
	F mono-rt :      at java.lang.ClassLoader.loadClass(ClassLoader.java:379)
	F mono-rt :      at java.lang.ClassLoader.loadClass(ClassLoader.java:312)

Perhaps unsurprisingly, we need to provide the system ClassLoader
in order to load types from outside the default "classpath", which is
why we used `ClassLoader.loadClass()` in the first place.

Instead of `Class.forName()`, use the
[`Class.forName(String name, boolean initialize, ClassLoader loader)`][0]
overload, so that we can provide a `ClassLoader` instance.

This *should* fix the crash.

[0]: https://developer.android.com/reference/java/lang/Class#forName(java.lang.String,%20boolean,%20java.lang.ClassLoader)
Copy link
Contributor

@jpobst jpobst left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems perfectly cromulent.

@garuma
Copy link

garuma commented Mar 20, 2025

🫡

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

Successfully merging this pull request may close these issues.

Switch JniEnvironment.FindClass to use Class.forName instead of ClassLoader.loadClass
3 participants