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

Switch JniEnvironment.FindClass to use Class.forName instead of ClassLoader.loadClass #23

Closed
garuma opened this issue Apr 11, 2016 · 2 comments · Fixed by #1326 or dotnet/android#9931
Labels
enhancement Proposed change to current functionality java-interop Runtime bridge between .NET and Java

Comments

@garuma
Copy link

garuma commented Apr 11, 2016

See equivalent commit in monodroid: https://github.com/xamarin/monodroid/commit/ed984a3a0bfbe71f6499a7855c647b5bc2b28466

@jpobst jpobst added enhancement Proposed change to current functionality java-interop Runtime bridge between .NET and Java labels Apr 16, 2020
jonpryor pushed a commit to dotnet/android that referenced this issue Feb 8, 2025
…ass()` (#9769)

Context: xamarin/monodroid@ed984a3
Context: dotnet/java-interop#23

Update `JNIEnv.FindClass()` to use `JniEnvironment.Types.FindClass()`.

This avoids the problem of `JNIEnvInit.mid_Class_forName` being
`null` in a NativeAOT context:

	E AndroidRuntime: Process: net.dot.hellonativeaot, PID: 30744
	E AndroidRuntime: net.dot.jni.internal.JavaProxyThrowable: System.TypeInitializationException: TypeInitialization_Type_NoTypeAvailable
	E AndroidRuntime:  ---> System.ArgumentNullException: ArgumentNull_Generic Arg_ParamName_Name, method
	E AndroidRuntime:    at Java.Interop.JniEnvironment.StaticMethods.CallStaticObjectMethod(JniObjectReference, JniMethodInfo, JniArgumentValue*) + 0x1c8
	E AndroidRuntime:    at Android.Runtime.JNIEnv.FindClass(String) + 0xb4
	E AndroidRuntime:    at Java.Lang.Class..cctor() + 0x7c
	E AndroidRuntime:    at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0xb4
	E AndroidRuntime:    Exception_EndOfInnerExceptionStack
	E AndroidRuntime:    at System.Runtime.CompilerServices.ClassConstructorRunner.EnsureClassConstructorRun(StaticClassConstructionContext*) + 0x160
	E AndroidRuntime:    at System.Runtime.CompilerServices.ClassConstructorRunner.CheckStaticClassConstructionReturnNonGCStaticBase(StaticClassConstructionContext*, IntPtr) + 0x14
	E AndroidRuntime:    at Android.Runtime.JNIEnv.FindClass(String) + 0xc0
	E AndroidRuntime:    at Android.App.Application.get_Context() + 0x50
	E AndroidRuntime:    at Microsoft.Maui.Hosting.EssentialsExtensions.<>c.<UseEssentials>b__0_0(ILifecycleBuilder life) + 0x18
	E AndroidRuntime:    at Microsoft.Maui.LifecycleEvents.LifecycleEventService..ctor(IEnumerable`1) + 0x94
	E AndroidRuntime:    at Microsoft.Maui.LifecycleEvents.MauiAppHostBuilderExtensions.<>c.<ConfigureLifecycleEvents>b__0_0(IServiceProvider sp) + 0x3c
	E AndroidRuntime:    at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite, RuntimeResolverContext) + 0x68
	E AndroidRuntime:    at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier) + 0x180
	E AndroidRuntime:    at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey, Func`2) + 0x11c
	E AndroidRuntime:    at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier, ServiceProviderEngineScope) + 0x44
	E AndroidRuntime:    at Microsoft.Maui.MauiContext.WrappedServiceProvider.GetService(Type) + 0x48
	E AndroidRuntime:    at Microsoft.Maui.MauiContext.WrappedServiceProvider.GetService(Type) + 0x48
	E AndroidRuntime:    at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetService[T](IServiceProvider) + 0x3c
	E AndroidRuntime:    at Microsoft.Maui.LifecycleEvents.LifecycleEventServiceExtensions.<GetLifecycleEventDelegates>d__3`1.MoveNext() + 0x38
	E AndroidRuntime:    at Microsoft.Maui.LifecycleEvents.LifecycleEventServiceExtensions.InvokeLifecycleEvents[TDelegate](IServiceProvider, Action`1) + 0x68
	E AndroidRuntime:    at Microsoft.Maui.MauiApplication.OnCreate() + 0xb0
	E AndroidRuntime:    at Android.App.Application.n_OnCreate(IntPtr jnienv, IntPtr native__this) + 0xb0
	E AndroidRuntime: at my.MainApplication.n_onCreate(Native Method)
	E AndroidRuntime: at my.MainApplication.onCreate(MainApplication.java:24)
	E AndroidRuntime: at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1386)
	E AndroidRuntime: at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7398)
	E AndroidRuntime: at android.app.ActivityThread.-$$Nest$mhandleBindApplication(Unknown Source:0)
	E AndroidRuntime: at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2379)
	E AndroidRuntime: at android.os.Handler.dispatchMessage(Handler.java:107)
	E AndroidRuntime: at android.os.Looper.loopOnce(Looper.java:232)
	E AndroidRuntime: at android.os.Looper.loop(Looper.java:317)
	E AndroidRuntime: at android.app.ActivityThread.main(ActivityThread.java:8592)
	E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
	E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580)
	E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:878)

This also allows us to remove these fields that are now unused:

  * `JnienvInitializeArgs.Class_forName`
  * `JNIEnv.BinaryName()`

TODO?  While `JNIEnv.FindClass()` was using [`Class.forName()`][0],
`JniEnvironment.Types.FindClass()` uses [`ClassLoader.loadClass()`][1].
`JNIEnv.FindClass()` originally used `ClassLoader.loadClass()`, but
was changed to use `Class.forName()` in xamarin/monodroid@ed984a3a as
part of supporting the Xamarin.Android Designer.

Updating `JNIEnv.FindClass()` to use `JniEnvironment.Types.FindClass()`
effectively reverts that original change.  This is "fine" for now --
the Xamarin.Android Designer is no longer supported -- but in the
future we may want to update `JniEnvironment.Types.FindClass()` to
use `Class.forName()`; see dotnet/java-interop#23.

[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
Copy link
Member

The commit message of xamarin/monodroid@ed984a provides some useful context:

Previously, JNIEnv.FindClass(string) would defer to ClassLoader.loadClass() to load a Java type.

Because loading some classes on Java is a bit of an arcane art, you have to use a special syntax for primitive types. This syntax seemed to have been supported just fine on (Dalvik|ART) when using the loadClass method but on a desktop java like the Oracle VM this is not the case anymore.

Instead, passing by the Class.forName() static method seems to handle such case that involves primitive types so this commit supply the necessary handles to JNIEnv from native and switch the FindClass method to use them.

For instance, a type that used to trip the runtime beforehand with a ClassNotFoundException but doesn't
after this commit is [I (an array of primitive Java integer).

Which raises an interesting aside: JNIEnv::FindClass() is used to obtain the java.lang.Class for everything, including arrays of types such as "array of int" aka [I and "array of String" aka [Ljava/lang/String;. How does this work with Class.forName(), which doesn't accept JNI / syntax? (It's apparently always worked, I just didn't understand the array side of things.)

Simply replacing / with . is enough: Class.forName("[Ljava.lang.String;") works! This is likewise true for nested types: leave the $ along, replace / with .: Class.forName("[Ljava.lang.Thread$UncaughtExceptionHandler;") works!

jonpryor added a commit that referenced this issue Mar 19, 2025
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
Copy link
Member

This is a fun demo app:

class Example {
    public static void main(String[] args) throws Throwable {
        String[] types = {
            "java.lang.String", "java/lang/String",
            "[Ljava/lang/String;", "[Ljava.lang.String;", "[java.lang.String", "java.lang.String[]",
            "I", "int", "[I", "[int", "int[]",
            "[Ljava.lang.Thread$UncaughtExceptionHandler;",
            // "int", "long", "float", "double", "char", "byte", "short", "boolean"
        };
        ClassLoader cl = ClassLoader.getSystemClassLoader();
        for (String type : types) {
            try {
                Class<?> c = Class.forName(type);
                System.out.println("Class.forName(" + type + ") found: " + c.getName()); // prints "int", "long", etc.
            } catch (ClassNotFoundException e) {
                System.out.println("error: Class.forName(" + type + ") failed: " + e.toString());
            }
            try {
                Class<?> c = cl.loadClass(type);
                System.out.println("ClassLoader.loadClass(" + type + ") found: " + c.getName()); // prints "int", "long", etc.
            } catch (ClassNotFoundException e) {
                System.out.println("error: ClassLoader.loadClass(" + type + ") failed: " + e.toString());
            }
        }
        Class as = String[].class;
        System.out.println("String[].class.getName()=" + as.getName()); // prints "[Ljava.lang.String;"
        System.out.println("Thread.UncaughtExceptionHandler[].class.getCanonicalName()=" + Thread.UncaughtExceptionHandler[].class.getName()); // prints "java.lang.String[]"
    }
}

output:

Class.forName(java.lang.String) found: java.lang.String
ClassLoader.loadClass(java.lang.String) found: java.lang.String
error: Class.forName(java/lang/String) failed: java.lang.ClassNotFoundException: java/lang/String
error: ClassLoader.loadClass(java/lang/String) failed: java.lang.ClassNotFoundException: java/lang/String
error: Class.forName([Ljava/lang/String;) failed: java.lang.ClassNotFoundException: [Ljava/lang/String;
error: ClassLoader.loadClass([Ljava/lang/String;) failed: java.lang.ClassNotFoundException: [Ljava/lang/String;
Class.forName([Ljava.lang.String;) found: [Ljava.lang.String;
error: ClassLoader.loadClass([Ljava.lang.String;) failed: java.lang.ClassNotFoundException: [Ljava.lang.String;
error: Class.forName([java.lang.String) failed: java.lang.ClassNotFoundException: [java/lang/String
error: ClassLoader.loadClass([java.lang.String) failed: java.lang.ClassNotFoundException: [java.lang.String
error: Class.forName(java.lang.String[]) failed: java.lang.ClassNotFoundException: java/lang/String[]
error: ClassLoader.loadClass(java.lang.String[]) failed: java.lang.ClassNotFoundException: java.lang.String[]
error: Class.forName(I) failed: java.lang.ClassNotFoundException: I
error: ClassLoader.loadClass(I) failed: java.lang.ClassNotFoundException: I
error: Class.forName(int) failed: java.lang.ClassNotFoundException: int
error: ClassLoader.loadClass(int) failed: java.lang.ClassNotFoundException: int
Class.forName([I) found: [I
error: ClassLoader.loadClass([I) failed: java.lang.ClassNotFoundException: [I
error: Class.forName([int) failed: java.lang.ClassNotFoundException: [int
error: ClassLoader.loadClass([int) failed: java.lang.ClassNotFoundException: [int
error: Class.forName(int[]) failed: java.lang.ClassNotFoundException: int[]
error: ClassLoader.loadClass(int[]) failed: java.lang.ClassNotFoundException: int[]
Class.forName([Ljava.lang.Thread$UncaughtExceptionHandler;) found: [Ljava.lang.Thread$UncaughtExceptionHandler;
error: ClassLoader.loadClass([Ljava.lang.Thread$UncaughtExceptionHandler;) failed: java.lang.ClassNotFoundException: [Ljava.lang.Thread$UncaughtExceptionHandler;
String[].class.getName()=[Ljava.lang.String;
Thread.UncaughtExceptionHandler[].class.getCanonicalName()=[Ljava.lang.Thread$UncaughtExceptionHandler;

The TL;DR:

ClassLoader.loadClass() doesn't like arrays at all, at least not with the same syntax that Class.forName() supports. Which is why xamarin/monodroid@ed984a3a was needed in the first place.

jonpryor added a commit that referenced this issue Mar 20, 2025
…#1326)

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.
(This is still the case, by the way.)

Which brings us to xamarin/mondroid@ed984a3a, which updated then
Xamarin.Android to use
[`Class.forName(String name, boolean initialize, ClassLoader loader)`][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. 

`[Obsolete]` the `JniRuntime.CreationOptions.ClassLoader_LoadClass_id`
property as it is no longer used.

[0]: https://developer.android.com/reference/java/lang/Class#forName(java.lang.String,%20boolean,%20java.lang.ClassLoader)
[1]: https://developer.android.com/reference/java/lang/ClassLoader#loadClass(java.lang.String)
jonpryor added a commit to dotnet/android that referenced this issue Mar 20, 2025
jonpryor added a commit to dotnet/android that referenced this issue Mar 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement Proposed change to current functionality java-interop Runtime bridge between .NET and Java
Projects
None yet
3 participants