diff --git a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs index d8b9f652d..32e6b6254 100644 --- a/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs +++ b/src/Java.Interop/Java.Interop/JniEnvironment.Types.cs @@ -2,6 +2,7 @@ using System; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Collections.Generic; using System.Runtime.ExceptionServices; using System.Runtime.InteropServices; @@ -251,12 +252,14 @@ static string JavaClassToJniType (string value) return value.Replace ('.', '/'); } + [RequiresDynamicCode ("Native method registration via JniNativeMethodRegistration[] requires dynamic code generation. Use the blittable RegisterNatives(JniObjectReference, ReadOnlySpan) overload with statically-compiled function pointers for Native AOT compatibility.")] public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegistration [] methods) { RegisterNatives (type, methods, methods == null ? 0 : methods.Length); } - public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegistration [] methods, int numMethods) + [RequiresDynamicCode ("Native method registration via JniNativeMethodRegistration[] requires dynamic code generation. Use the blittable RegisterNatives(JniObjectReference, ReadOnlySpan) overload with statically-compiled function pointers for Native AOT compatibility.")] + public static unsafe void RegisterNatives (JniObjectReference type, JniNativeMethodRegistration [] methods, int numMethods) { if ((numMethods < 0) || (numMethods > (methods?.Length ?? 0))) { @@ -275,11 +278,45 @@ public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegi } #endif // DEBUG - int r = _RegisterNatives (type, methods ?? Array.Empty(), numMethods); + if (numMethods == 0 || methods == null) { + return; + } - if (r != 0) { - throw new InvalidOperationException ( - string.Format ("Could not register native methods for class '{0}'; JNIEnv::RegisterNatives() returned {1}.", GetJniTypeNameFromClass (type), r)); + // Marshal the non-blittable JniNativeMethodRegistration[] into blittable JniNativeMethod + // values and dispatch to the blittable overload, instead of invoking the JNI + // `RegisterNatives` function pointer with a non-blittable managed-array parameter. + // The runtime marshalling stub synthesized for such a `delegate* unmanaged<>` call is + // miscompiled by crossgen2 under composite ReadyToRun + PGO: the JniNativeMethod `name` + // pointers end up referencing the managed `string` objects instead of marshalled UTF-8 + // data, which corrupts the registered method names. See https://github.com/dotnet/android/issues/11633. + const int MaxStackAllocatedNativeMethods = 32; + bool useStackAllocatedBuffers = numMethods <= MaxStackAllocatedNativeMethods; + Span natives = useStackAllocatedBuffers + ? stackalloc JniNativeMethod [numMethods] + : new JniNativeMethod [numMethods]; + Span unmanagedStrings = useStackAllocatedBuffers + ? stackalloc IntPtr [numMethods * 2] + : new IntPtr [numMethods * 2]; + unmanagedStrings.Clear (); + try { + for (int i = 0; i < numMethods; ++i) { + var m = methods [i]; + if (m.Marshaler == null) + throw new ArgumentException ($"JniNativeMethodRegistration[{i}] ({m.Name}{m.Signature}) has a null Marshaler delegate.", nameof (methods)); + IntPtr name = Marshal.StringToCoTaskMemUTF8 (m.Name); + unmanagedStrings [i * 2] = name; + IntPtr sig = Marshal.StringToCoTaskMemUTF8 (m.Signature); + unmanagedStrings [i * 2 + 1] = sig; + natives [i] = new JniNativeMethod ((byte*) name, (byte*) sig, Marshal.GetFunctionPointerForDelegate (m.Marshaler)); + } + RegisterNatives (type, natives); + // Keep the Marshaler delegates alive at least until JNI has consumed the function pointers. + GC.KeepAlive (methods); + } finally { + for (int i = 0; i < unmanagedStrings.Length; ++i) { + if (unmanagedStrings [i] != IntPtr.Zero) + Marshal.ZeroFreeCoTaskMemUTF8 (unmanagedStrings [i]); + } } } @@ -290,7 +327,11 @@ public static void RegisterNatives (JniObjectReference type, JniNativeMethodRegi /// public static unsafe void RegisterNatives (JniObjectReference type, ReadOnlySpan methods) { + if (!type.IsValid) + throw new ArgumentException ("Handle must be valid.", nameof (type)); + IntPtr env = JniEnvironment.EnvironmentPointer; + int r; fixed (JniNativeMethod* methodsPtr = methods) { #if FEATURE_JNIENVIRONMENT_JI_FUNCTION_POINTERS var registerNatives = (delegate* unmanaged) @@ -299,10 +340,19 @@ public static unsafe void RegisterNatives (JniObjectReference type, ReadOnlySpan var registerNatives = (delegate* unmanaged) JniEnvironment.CurrentInfo.Invoker.env.RegisterNatives; #endif - int r = registerNatives (env, type.Handle, methodsPtr, methods.Length); - if (r != 0) { - throw new InvalidOperationException ($"Could not register native methods for class '{GetJniTypeNameFromClass (type)}'; JNIEnv::RegisterNatives() returned {r}."); - } + r = registerNatives (env, type.Handle, methodsPtr, methods.Length); + } + + // Surface (and clear) any pending Java exception raised by JNI::RegisterNatives() + // — e.g. NoSuchMethodError — before falling back to the return-code check, matching + // the behavior of the prior JniNativeMethodRegistration[] registration path. Leaving a pending + // exception in the JNIEnv would make subsequent JNI calls fail or abort. + var thrown = JniEnvironment.GetExceptionForLastThrowable (); + if (thrown != null) + ExceptionDispatchInfo.Capture (thrown).Throw (); + + if (r != 0) { + throw new InvalidOperationException ($"Could not register native methods for class '{GetJniTypeNameFromClass (type)}'; JNIEnv::RegisterNatives() returned {r}."); } } diff --git a/src/Java.Interop/Java.Interop/JniType.cs b/src/Java.Interop/Java.Interop/JniType.cs index c17488e45..204c5630a 100644 --- a/src/Java.Interop/Java.Interop/JniType.cs +++ b/src/Java.Interop/Java.Interop/JniType.cs @@ -153,6 +153,7 @@ public bool IsInstanceOfType (JniObjectReference value) JniNativeMethodRegistration[]? methods; #pragma warning restore 0414 + [RequiresDynamicCode ("Native method registration via JniNativeMethodRegistration[] requires dynamic code generation. Use the blittable RegisterNatives(JniObjectReference, ReadOnlySpan) overload with statically-compiled function pointers for Native AOT compatibility.")] public void RegisterNativeMethods (params JniNativeMethodRegistration[] methods) { AssertValid (); diff --git a/src/Java.Interop/Java.Interop/ManagedPeer.cs b/src/Java.Interop/Java.Interop/ManagedPeer.cs index 0a03e916c..f41a06200 100644 --- a/src/Java.Interop/Java.Interop/ManagedPeer.cs +++ b/src/Java.Interop/Java.Interop/ManagedPeer.cs @@ -23,6 +23,7 @@ namespace Java.Interop { static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (ManagedPeer)); + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "ManagedPeer is not compatible with Native AOT; it's only used by reflection-based JniRuntime.JniValueManager implementations.")] static ManagedPeer () { _members.JniPeerType.RegisterNativeMethods ( diff --git a/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs b/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs index 6996442d4..9120d4003 100644 --- a/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs +++ b/tests/Java.Interop-Tests/Java.Interop/JniTypeTest.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Runtime.InteropServices; using Java.Interop; @@ -42,6 +43,7 @@ public void Ctor_ThrowsIfTypeNotFound () } [Test] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Test exercises non-AOT-compatible JniType.RegisterNativeMethods API.")] public unsafe void Dispose_Exceptions () { var t = new JniType ("java/lang/Object"); @@ -175,6 +177,7 @@ public void RegisterWithRuntime () } [Test] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Test exercises non-AOT-compatible JniType.RegisterNativeMethods API.")] public void RegisterNativeMethods () { using (var TestType_class = new JniType ("net/dot/jni/test/CallNonvirtualBase")) { @@ -214,6 +217,73 @@ public unsafe void RegisterNativeMethods_JniNativeMethod () [UnmanagedCallersOnly] static int NativeAdd (IntPtr jnienv, IntPtr self, int a, int b) => a + b; + + [Test] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Test exercises non-AOT-compatible JniNativeMethodRegistration[] registration path.")] + public unsafe void RegisterNativeMethods_JniNativeMethodRegistration () + { + using (var nativeClass = new JniType ("net/dot/jni/test/RegisterNativesTestType")) { + // Test the JniNativeMethodRegistration[] registration path (the one that marshals to blittable) + var methods = new JniNativeMethodRegistration [1]; + methods [0] = new JniNativeMethodRegistration ( + "add", + "(II)I", + new AddDelegate (ManagedAdd)); + + JniEnvironment.Types.RegisterNatives (nativeClass.PeerReference, methods, methods.Length); + + // Call the native method from Java to verify registration worked + var ctor = JniEnvironment.InstanceMethods.GetMethodID (nativeClass.PeerReference, "", "()V"); + var obj = JniEnvironment.Object.NewObject (nativeClass.PeerReference, ctor); + try { + var addMethod = JniEnvironment.InstanceMethods.GetMethodID (nativeClass.PeerReference, "add", "(II)I"); + var args = stackalloc JniArgumentValue [2]; + args [0] = new JniArgumentValue (5); + args [1] = new JniArgumentValue (6); + int result = JniEnvironment.InstanceMethods.CallIntMethod (obj, addMethod, args); + Assert.AreEqual (11, result); + } finally { + JniObjectReference.Dispose (ref obj); + } + } + } + + delegate int AddDelegate (IntPtr jnienv, IntPtr self, int a, int b); + + static int ManagedAdd (IntPtr jnienv, IntPtr self, int a, int b) => a + b; + + [Test] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Test exercises non-AOT-compatible JniNativeMethodRegistration[] registration path with many methods.")] + public unsafe void RegisterNativeMethods_JniNativeMethodRegistration_ManyMethods () + { + using (var nativeClass = new JniType ("net/dot/jni/test/RegisterNativesTestType")) { + // Test the heap allocation path (> 32 methods) to ensure the marshalling loop handles it + var methods = new JniNativeMethodRegistration [40]; + for (int i = 0; i < methods.Length; ++i) { + methods [i] = new JniNativeMethodRegistration ( + "add", + "(II)I", + new AddDelegate (ManagedAdd)); + } + + // This should not crash even with > 32 methods + JniEnvironment.Types.RegisterNatives (nativeClass.PeerReference, methods, methods.Length); + + // Verify the registration worked by calling the method + var ctor = JniEnvironment.InstanceMethods.GetMethodID (nativeClass.PeerReference, "", "()V"); + var obj = JniEnvironment.Object.NewObject (nativeClass.PeerReference, ctor); + try { + var addMethod = JniEnvironment.InstanceMethods.GetMethodID (nativeClass.PeerReference, "add", "(II)I"); + var args = stackalloc JniArgumentValue [2]; + args [0] = new JniArgumentValue (10); + args [1] = new JniArgumentValue (20); + int result = JniEnvironment.InstanceMethods.CallIntMethod (obj, addMethod, args); + Assert.AreEqual (30, result); + } finally { + JniObjectReference.Dispose (ref obj); + } + } + } } }