Skip to content

Conversation

@jonpryor
Copy link
Contributor

@jonpryor jonpryor commented May 7, 2025

Fixes: #10081
Fixes: #10118

Context: 18ca528
Context: dotnet/java-interop@5852e6e

dotnet/java-interop@5852e6e3 updated JniEnviroment.Types.FindClass() to begin using Class.forName(String, bool, ClassLoader) to load Java types instead of ClassLoader.loadClass().

This broke type registration on non-Java threads under NativeAOT; attempting to load a non-Android Java type from a managed thread:

var t = new System.Threading.Thread(() => {
    using var c = new ClassFromThisAssembly();
});
t.Start();
t.Join();

would fail with a ClassNotFoundException:

E NUnit   : Java.Lang.ClassNotFoundException: Didn't find class "from.NewManagedThreadOne" on path: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64, /system/lib64, /system_ext/lib64]]
E NUnit   :    at Java.Interop.JniEnvironment.Types.TryFindClass(String, Boolean) + 0x3f4
E NUnit   :    at Java.Interop.JniPeerMembers.JniInstanceMethods..ctor(Type) + 0x130
E NUnit   :    at Java.Interop.JniPeerMembers.JniInstanceMethods.GetConstructorsForType(Type) + 0x94
E NUnit   :    at Java.Interop.JniPeerMembers.JniInstanceMethods.StartCreateInstance(String, Type, JniArgumentValue*) + 0x1c
E NUnit   :    at Java.Lang.Object..ctor() + 0x108
E NUnit   :    at Java.InteropTests.JnienvTest.<>c__DisplayClass6_0.<RegisterTypeOnNewManagedThread>b__0() + 0x24
E NUnit   :   --- End of managed Java.Lang.ClassNotFoundException stack trace ---
E NUnit   : java.lang.ClassNotFoundException: Didn't find class "from.NewManagedThreadOne" on path: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64, /system/lib64, /system_ext/lib64]]
E NUnit   : 	at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:259)
E NUnit   : 	at java.lang.ClassLoader.loadClass(ClassLoader.java:637)
E NUnit   : 	at java.lang.ClassLoader.loadClass(ClassLoader.java:573)

Fix this by setting NativeAotRuntimeOptions.ClassLoader to the context.getClassLoader() value within
NativeAotRuntimeProvider.attachInfo(). This ensures that we use a ClassLoader that knows about the app's classes.dex.

Fixes: #10081
Fixes: #10118

Context: 18ca528
Context: dotnet/java-interop@5852e6e

dotnet/java-interop@5852e6e3 updated `JniEnviroment.Types.FindClass()`
to begin using [`Class.forName(String, bool, ClassLoader)`][0] to load
Java types instead of `ClassLoader.loadClass()`.

This broke type registration on non-Java threads under NativeAOT;
attempting to load a non-Android Java type from a managed thread:

	var t = new System.Threading.Thread(() => {
	    using var c = new ClassFromThisAssembly();
	});
	t.Start();
	t.Join();

would fail with a `ClassNotFoundException`:

	E NUnit   : Java.Lang.ClassNotFoundException: Didn't find class "from.NewManagedThreadOne" on path: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64, /system/lib64, /system_ext/lib64]]
	E NUnit   :    at Java.Interop.JniEnvironment.Types.TryFindClass(String, Boolean) + 0x3f4
	E NUnit   :    at Java.Interop.JniPeerMembers.JniInstanceMethods..ctor(Type) + 0x130
	E NUnit   :    at Java.Interop.JniPeerMembers.JniInstanceMethods.GetConstructorsForType(Type) + 0x94
	E NUnit   :    at Java.Interop.JniPeerMembers.JniInstanceMethods.StartCreateInstance(String, Type, JniArgumentValue*) + 0x1c
	E NUnit   :    at Java.Lang.Object..ctor() + 0x108
	E NUnit   :    at Java.InteropTests.JnienvTest.<>c__DisplayClass6_0.<RegisterTypeOnNewManagedThread>b__0() + 0x24
	E NUnit   :   --- End of managed Java.Lang.ClassNotFoundException stack trace ---
	E NUnit   : java.lang.ClassNotFoundException: Didn't find class "from.NewManagedThreadOne" on path: DexPathList[[directory "."],nativeLibraryDirectories=[/system/lib64, /system_ext/lib64, /system/lib64, /system_ext/lib64]]
	E NUnit   : 	at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:259)
	E NUnit   : 	at java.lang.ClassLoader.loadClass(ClassLoader.java:637)
	E NUnit   : 	at java.lang.ClassLoader.loadClass(ClassLoader.java:573)

Fix this by setting `NativeAotRuntimeOptions.ClassLoader` to the
`context.getClassLoader()` value within
`NativeAotRuntimeProvider.attachInfo()`.  This ensures that we use
a `ClassLoader` that knows about the app's `classes.dex`.

[0]: https://developer.android.com/reference/java/lang/Class#forName(java.lang.String,%20boolean,%20java.lang.ClassLoader)
@jonpryor jonpryor requested a review from jonathanpeppers as a code owner May 7, 2025 18:46
@jonpryor jonpryor requested review from Copilot and jonathanpeppers and removed request for jonathanpeppers May 7, 2025 18:46
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR fixes issues with Java type registration on non-Java threads under NativeAOT by ensuring that a proper Android ClassLoader is passed during initialization. The changes include updating the tests to use new class registration names for native and managed threads, modifying the JavaInteropRuntime API to accept a ClassLoader parameter, and updating the native initialization method on the C# side.

Reviewed Changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs Updated tests to use new registration strings and added a new test for managed thread type registration.
src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java Retrieves the app ClassLoader and passes it to the JavaInteropRuntime initializer.
src/Xamarin.Android.Build.Tasks/Resources/JavaInteropRuntime.java Updated native method signature to accept a ClassLoader argument.
src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs Modified native initialization to accept and store the ClassLoader for proper type registration.

}
});
thread.Start ();
thread.Join (5000);
Copy link

Copilot AI May 7, 2025

Choose a reason for hiding this comment

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

[nitpick] Consider extracting the magic number 5000 into a named constant for improved clarity and maintainability in test code.

Suggested change
thread.Join (5000);
thread.Join (ThreadJoinTimeoutMs);

Copilot uses AI. Check for mistakes.
Copy link
Member

@jonathanpeppers jonathanpeppers left a comment

Choose a reason for hiding this comment

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

The new test passes:

image

@jonathanpeppers
Copy link
Member

Note that I'm not sure if this actually fixes:

I think their sample was running on Mono, but I asked to double-check.

@jonpryor jonpryor merged commit 199efa8 into main May 8, 2025
59 checks passed
@jonpryor jonpryor deleted the dev/jonp/jonp-NativeAOT-set-ClassLoader branch May 8, 2025 12:48
@github-actions github-actions bot locked and limited conversation to collaborators Jun 8, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

3 participants