From 661e26541c6c7d89649a88ff18232e90741d1a14 Mon Sep 17 00:00:00 2001 From: Jonathan Pryor Date: Wed, 7 May 2025 14:33:25 -0400 Subject: [PATCH] [NativeAOT] Provide the Android ClassLoader Fixes: https://github.com/dotnet/android/issues/10081 Fixes: https://github.com/dotnet/android/issues/10118 Context: 18ca52896f36bd756f6b9df801758ab4fe3dc31a Context: https://github.com/dotnet/java-interop/commit/5852e6e398a6c96d42fff3ed4b8a5d4b5a0abf97 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.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) --- .../JavaInteropRuntime.cs | 3 ++- .../Resources/JavaInteropRuntime.java | 2 +- .../Resources/NativeAotRuntimeProvider.java | 4 ++- .../Java.Interop/JnienvTest.cs | 27 ++++++++++++++++--- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs index 982f1c5a210..340078864e8 100644 --- a/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs +++ b/src/Microsoft.Android.Runtime.NativeAOT/Android.Runtime.NativeAOT/JavaInteropRuntime.cs @@ -31,7 +31,7 @@ static void JNI_OnUnload (IntPtr vm, IntPtr reserved) // symbol name from `$(IntermediateOutputPath)obj/Release/osx-arm64/h-classes/net_dot_jni_hello_JavaInteropRuntime.h` [UnmanagedCallersOnly (EntryPoint="Java_net_dot_jni_nativeaot_JavaInteropRuntime_init")] - static void init (IntPtr jnienv, IntPtr klass) + static void init (IntPtr jnienv, IntPtr klass, IntPtr classLoader) { JniTransition transition = default; try { @@ -41,6 +41,7 @@ static void init (IntPtr jnienv, IntPtr klass) var typeManager = new ManagedTypeManager (); var options = new NativeAotRuntimeOptions { EnvironmentPointer = jnienv, + ClassLoader = new JniObjectReference (classLoader), TypeManager = typeManager, ValueManager = new ManagedValueManager (), UseMarshalMemberBuilder = false, diff --git a/src/Xamarin.Android.Build.Tasks/Resources/JavaInteropRuntime.java b/src/Xamarin.Android.Build.Tasks/Resources/JavaInteropRuntime.java index cb0474682f9..194c8ddb03d 100644 --- a/src/Xamarin.Android.Build.Tasks/Resources/JavaInteropRuntime.java +++ b/src/Xamarin.Android.Build.Tasks/Resources/JavaInteropRuntime.java @@ -11,5 +11,5 @@ public class JavaInteropRuntime { private JavaInteropRuntime() { } - public static native void init(); + public static native void init(ClassLoader classLoader); } diff --git a/src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java b/src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java index a3acd8935af..f5db2335339 100644 --- a/src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java +++ b/src/Xamarin.Android.Build.Tasks/Resources/NativeAotRuntimeProvider.java @@ -37,8 +37,10 @@ public void attachInfo(android.content.Context context, android.content.pm.Provi Log.e(TAG, "Failed to set environment variables", e); } + ClassLoader loader = context.getClassLoader (); + // Initialize .NET runtime - JavaInteropRuntime.init(); + JavaInteropRuntime.init(loader); // NOTE: only required for custom applications ApplicationRegistration.registerApplications(); super.attachInfo (context, info); diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs index c97a8d6e1d3..a563f56fd3a 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs @@ -44,7 +44,7 @@ public void TestMyPaintColor () public void RegisterTypeOnNewNativeThread () { Java.Lang.JavaSystem.LoadLibrary ("reuse-threads"); - int ret = rt_register_type_on_new_thread ("from.NewThreadOne", Application.Context.ClassLoader.Handle); + int ret = rt_register_type_on_new_thread ("from.NewNativeThreadOne", Application.Context.ClassLoader.Handle); Assert.AreEqual (0, ret, $"Java type registration on a new thread failed with code {ret}"); } @@ -57,6 +57,23 @@ public void RegisterTypeOnNewJavaThread () Assert.AreNotEqual (null, thread.Instance, "Failed to register instance of a class on new thread"); } + [Test] + public void RegisterTypeOnNewManagedThread () + { + Exception? ex = null; + var thread = new System.Threading.Thread (() => { + try { + using var instance = new RegisterMeOnNewManagedThreadOne (); + } + catch (Exception e) { + ex = e; + } + }); + thread.Start (); + thread.Join (5000); + Assert.IsNull (ex, $"Failed to register instance of a class on new thread: {ex}"); + } + [Test] public void ThreadReuse () { @@ -459,8 +476,12 @@ public void DoNotLeakWeakReferences () } } - [Register ("from/NewThreadOne")] - class RegisterMeOnNewThreadOne : Java.Lang.Object + [Register ("from/NewNativeThreadOne")] + class RegisterMeOnNewNativeThreadOne : Java.Lang.Object + {} + + [Register ("from/NewManagedThreadOne")] + class RegisterMeOnNewManagedThreadOne : Java.Lang.Object {} [Register ("from/NewThreadTwo")]