diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 918b8377b54..5bea1e3f50a 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -2,13 +2,12 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Threading; -using System.Reflection; using Java.Interop; -using Java.Interop.Tools.TypeNameMappings; using Microsoft.Android.Runtime; using System.Diagnostics.CodeAnalysis; @@ -380,34 +379,6 @@ protected override IEnumerable GetSimpleReferences (Type type) return null; } - delegate Delegate GetCallbackHandler (); - - static MethodInfo? dynamic_callback_gen; - - // See ExportAttribute.cs - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Mono.Android.Export.dll is preserved when [Export] is used via [DynamicDependency].")] - [UnconditionalSuppressMessage ("Trimming", "IL2075", Justification = "Mono.Android.Export.dll is preserved when [Export] is used via [DynamicDependency].")] - static Delegate CreateDynamicCallback (MethodInfo method) - { - if (dynamic_callback_gen == null) { - var assembly = Assembly.Load ("Mono.Android.Export"); - if (assembly == null) - throw new InvalidOperationException ("To use methods marked with ExportAttribute, Mono.Android.Export.dll needs to be referenced in the application"); - var type = assembly.GetType ("Java.Interop.DynamicCallbackCodeGenerator"); - if (type == null) - throw new InvalidOperationException ("The referenced Mono.Android.Export.dll does not match the expected version. The required type was not found."); - dynamic_callback_gen = type.GetMethod ("Create"); - if (dynamic_callback_gen == null) - throw new InvalidOperationException ("The referenced Mono.Android.Export.dll does not match the expected version. The required method was not found."); - } - return (Delegate)dynamic_callback_gen.Invoke (null, new object [] { method })!; - } - - // [Export] callback delegates are created dynamically via DynamicCallbackCodeGenerator and are not - // cached in static fields (unlike non-[Export] connector delegates). Without rooting them here, - // CoreCLR's GC can collect them between JNI registration and first invocation, causing a crash. - static readonly Lock prevent_delegate_gc_lock = new Lock (); - static readonly List prevent_delegate_gc = new List (); static List sharedRegistrations = new List (); static bool FastRegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) @@ -488,9 +459,6 @@ public override void RegisterNativeMembers ( string? methods) => RegisterNativeMembers (nativeClass, type, methods.AsSpan ()); - [UnconditionalSuppressMessage ("Trimming", "IL2057", Justification = "Type.GetType() can never statically know the string value parsed from parameter 'methods'.")] - [UnconditionalSuppressMessage ("Trimming", "IL2067", Justification = "Delegate.CreateDelegate() can never statically know the string value parsed from parameter 'methods'.")] - [UnconditionalSuppressMessage ("Trimming", "IL2072", Justification = "Delegate.CreateDelegate() can never statically know the string value parsed from parameter 'methods'.")] public override void RegisterNativeMembers ( JniType nativeClass, [DynamicallyAccessedMembers (MethodsAndPrivateNested)] Type type, @@ -505,121 +473,21 @@ public override void RegisterNativeMembers ( return; } - int methodCount = CountMethods (methods); - if (methodCount < 1) { - if (jniAddNativeMethodRegistrationAttributePresent) { + if (RuntimeFeature.StringBasedJniRegistration) { + if (!NativeMethodRegistration.TryRegisterNativeMembers (nativeClass, type, methods) && + jniAddNativeMethodRegistrationAttributePresent) { base.RegisterNativeMembers (nativeClass, type, methods.ToString ()); } return; } - JniNativeMethodRegistration [] natives = new JniNativeMethodRegistration [methodCount]; - int nativesIndex = 0; - MethodInfo []? typeMethods = null; - - ReadOnlySpan methodsSpan = methods; - bool needToRegisterNatives = false; - - while (!methodsSpan.IsEmpty) { - int newLineIndex = methodsSpan.IndexOf ('\n'); - - ReadOnlySpan methodLine = methodsSpan.Slice (0, newLineIndex != -1 ? newLineIndex : methodsSpan.Length); - if (!methodLine.IsEmpty) { - SplitMethodLine (methodLine, - out ReadOnlySpan name, - out ReadOnlySpan signature, - out ReadOnlySpan callbackString, - out ReadOnlySpan callbackDeclaringTypeString); - - Delegate? callback = null; - if (callbackString.SequenceEqual ("__export__")) { - var mname = name.Slice (2); - MethodInfo? minfo = null; - typeMethods ??= type.GetMethods (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); - foreach (var mi in typeMethods) - if (mname.SequenceEqual (mi.Name) && signature.SequenceEqual (JavaNativeTypeManager.GetJniSignature (mi))) { - minfo = mi; - break; - } - - if (minfo == null) - throw new InvalidOperationException (FormattableString.Invariant ($"Specified managed method '{mname.ToString ()}' was not found. Signature: {signature.ToString ()}")); - callback = CreateDynamicCallback (minfo); - lock (prevent_delegate_gc_lock) { - prevent_delegate_gc.Add (callback); - } - needToRegisterNatives = true; - } else { - Type callbackDeclaringType = type; - if (!callbackDeclaringTypeString.IsEmpty) { - callbackDeclaringType = Type.GetType (callbackDeclaringTypeString.ToString (), throwOnError: true)!; - } - while (callbackDeclaringType.ContainsGenericParameters) { - callbackDeclaringType = callbackDeclaringType.BaseType!; - } - - GetCallbackHandler connector = (GetCallbackHandler) Delegate.CreateDelegate (typeof (GetCallbackHandler), - callbackDeclaringType, callbackString.ToString ()); - callback = connector (); - } - - if (callback != null) { - needToRegisterNatives = true; - natives [nativesIndex++] = new JniNativeMethodRegistration (name.ToString (), signature.ToString (), callback); - } - } - - methodsSpan = newLineIndex != -1 ? methodsSpan.Slice (newLineIndex + 1) : default; - } - - if (needToRegisterNatives) { - JniEnvironment.Types.RegisterNatives (nativeClass.PeerReference, natives, nativesIndex); - } + throw new NotSupportedException ( + $"Unable to register native methods for '{type.FullName}': string-based JNI registration is disabled " + + $"and no fast native registration entry was found. Re-enable it by setting " + + $"_AndroidEnableStringBasedJniRegistration=true."); } catch (Exception e) { JniEnvironment.Runtime.RaisePendingException (e); } - - bool ShouldRegisterDynamically (string callbackTypeName, string callbackString, string typeName, string callbackName) - { - if (String.Compare (typeName, callbackTypeName, StringComparison.Ordinal) != 0) { - return false; - } - - return String.Compare (callbackName, callbackString, StringComparison.Ordinal) == 0; - } - } - - static int CountMethods (ReadOnlySpan methodsSpan) - { - int count = 0; - while (!methodsSpan.IsEmpty) { - count++; - - int newLineIndex = methodsSpan.IndexOf ('\n'); - methodsSpan = newLineIndex != -1 ? methodsSpan.Slice (newLineIndex + 1) : default; - } - return count; - } - - static void SplitMethodLine ( - ReadOnlySpan methodLine, - out ReadOnlySpan name, - out ReadOnlySpan signature, - out ReadOnlySpan callback, - out ReadOnlySpan callbackDeclaringType) - { - int colonIndex = methodLine.IndexOf (':'); - name = methodLine.Slice (0, colonIndex); - methodLine = methodLine.Slice (colonIndex + 1); - - colonIndex = methodLine.IndexOf (':'); - signature = methodLine.Slice (0, colonIndex); - methodLine = methodLine.Slice (colonIndex + 1); - - colonIndex = methodLine.IndexOf (':'); - callback = methodLine.Slice (0, colonIndex != -1 ? colonIndex : methodLine.Length); - - callbackDeclaringType = colonIndex != -1 ? methodLine.Slice (colonIndex + 1) : default; } } diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index c2e7ea913ca..9f3a67883fe 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -88,6 +88,18 @@ static Type TypeGetType (string typeName) => androidRuntime.TypeManager.RegisterNativeMembers (jniType, type, methods); } + [UnmanagedCallersOnly] + static void RegisterNativesViaTrimmableTypeMap (IntPtr env, IntPtr klass, IntPtr nativeClassHandle) + { + try { + TrimmableTypeMap.Instance.RegisterNatives (nativeClassHandle); + } catch (Exception ex) { + var classRef = new JniObjectReference (nativeClassHandle); + var className = JniEnvironment.Types.GetJniTypeNameFromClass (classRef); + Environment.FailFast ($"TrimmableTypeMap: Failed to register natives for class '{className}'.", ex); + } + } + // This must be called by NativeAOT before InitializeJniRuntime, as early as possible internal static void NativeAotInitializeMaxGrefGet () { @@ -157,9 +169,10 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) args->propagateUncaughtExceptionFn = (IntPtr)(delegate* unmanaged)&PropagateUncaughtException; - if (!RuntimeFeature.TrimmableTypeMap) { + if (RuntimeFeature.StringBasedJniRegistration) { args->registerJniNativesFn = (IntPtr)(delegate* unmanaged)&RegisterJniNatives; } + RunStartupHooksIfNeeded (); SetSynchronizationContext (); } @@ -224,10 +237,17 @@ static void InitializeTrimmableTypeMapDataIfNeeded () static void RegisterTrimmableTypeMapNativeMethodsIfNeeded () { - if (RuntimeFeature.TrimmableTypeMap) { - // TypeMapLoader.Initialize() only loads managed typemap data. Registering - // mono.android.Runtime natives requires JniRuntime.Current and its ClassLoader. - TrimmableTypeMap.RegisterNativeMethods (); + if (!RuntimeFeature.TrimmableTypeMap) { + return; + } + + using var runtimeClass = new JniType ("mono/android/Runtime"u8); + unsafe { + fixed (byte* name = "registerNatives"u8, sig = "(Ljava/lang/Class;)V"u8) { + var onRegisterNatives = (IntPtr)(delegate* unmanaged)&RegisterNativesViaTrimmableTypeMap; + var method = new JniNativeMethod (name, sig, onRegisterNatives); + JniEnvironment.Types.RegisterNatives (runtimeClass.PeerReference, [method]); + } } } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistration.cs b/src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistration.cs new file mode 100644 index 00000000000..23bb4a2a64c --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistration.cs @@ -0,0 +1,195 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Threading; +using Java.Interop; +using Java.Interop.Tools.TypeNameMappings; + +namespace Microsoft.Android.Runtime; + +/// +/// Shared implementation of the "string-based" JNI native method registration. Parses the +/// methods metadata string that Java passes back to managed code (a Java Callable Wrapper +/// static initializer calling mono.android.Runtime.register(...), or +/// Java.Interop.ManagedPeer) and registers the native callbacks via +/// . +/// +/// This relies on and +/// , so it is not trim- or +/// NativeAOT-friendly. It works on MonoVM and CoreCLR and is shared by +/// AndroidTypeManager (the default llvm-ir/MonoVM path) and, gated behind +/// , by TrimmableTypeMapTypeManager. +/// +[RequiresUnreferencedCode ("Parses the 'methods' metadata string and resolves JNI callbacks via reflection (Type.GetType / Delegate.CreateDelegate).")] +[RequiresDynamicCode ("Resolves JNI callbacks via reflection and dynamic delegate creation, which is not compatible with NativeAOT.")] +static class NativeMethodRegistration +{ + const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = + DynamicallyAccessedMemberTypes.PublicMethods | + DynamicallyAccessedMemberTypes.NonPublicMethods | + DynamicallyAccessedMemberTypes.NonPublicNestedTypes; + + static MethodInfo? dynamic_callback_gen; + + // [Export] callback delegates are created dynamically via DynamicCallbackCodeGenerator and are not + // cached in static fields (unlike non-[Export] connector delegates). Without rooting them here, + // CoreCLR's GC can collect them between JNI registration and first invocation, causing a crash. + static readonly Lock prevent_delegate_gc_lock = new Lock (); + static readonly List prevent_delegate_gc = new List (); + + delegate Delegate GetCallbackHandler (); + + /// + /// Parses and registers the described native callbacks on + /// . + /// + /// + /// if contained at least one method line (whether + /// or not any natives were registered); if it was empty, in which case the + /// caller may fall back to the marshal-methods path. + /// + internal static bool TryRegisterNativeMembers ( + JniType nativeClass, + [DynamicallyAccessedMembers (MethodsAndPrivateNested)] + Type type, + ReadOnlySpan methods) + { + if (methods.IsEmpty) { + return false; + } + + int methodCount = CountMethods (methods); + if (methodCount < 1) { + return false; + } + + JniNativeMethodRegistration [] natives = new JniNativeMethodRegistration [methodCount]; + int nativesIndex = 0; + MethodInfo []? typeMethods = null; + + ReadOnlySpan methodsSpan = methods; + bool needToRegisterNatives = false; + + while (!methodsSpan.IsEmpty) { + int newLineIndex = methodsSpan.IndexOf ('\n'); + + ReadOnlySpan methodLine = methodsSpan.Slice (0, newLineIndex != -1 ? newLineIndex : methodsSpan.Length); + if (!methodLine.IsEmpty) { + SplitMethodLine (methodLine, + out ReadOnlySpan name, + out ReadOnlySpan signature, + out ReadOnlySpan callbackString, + out ReadOnlySpan callbackDeclaringTypeString); + + Delegate? callback = null; + if (callbackString.SequenceEqual ("__export__")) { + var mname = name.Slice (2); + MethodInfo? minfo = null; + typeMethods ??= type.GetMethods (BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static); + foreach (var mi in typeMethods) + if (mname.SequenceEqual (mi.Name) && signature.SequenceEqual (JavaNativeTypeManager.GetJniSignature (mi))) { + minfo = mi; + break; + } + + if (minfo == null) + throw new InvalidOperationException (FormattableString.Invariant ($"Specified managed method '{mname.ToString ()}' was not found. Signature: {signature.ToString ()}")); + callback = CreateDynamicCallback (minfo); + lock (prevent_delegate_gc_lock) { + prevent_delegate_gc.Add (callback); + } + needToRegisterNatives = true; + } else { + Type callbackDeclaringType = type; + if (!callbackDeclaringTypeString.IsEmpty) { + var resolvedType = Type.GetType (callbackDeclaringTypeString.ToString (), throwOnError: true); + if (resolvedType is null) { + throw new InvalidOperationException ($"Could not resolve JNI callback declaring type '{callbackDeclaringTypeString.ToString ()}'."); + } + callbackDeclaringType = resolvedType; + } + while (callbackDeclaringType.ContainsGenericParameters) { + var baseType = callbackDeclaringType.BaseType; + if (baseType is null) { + throw new InvalidOperationException ($"Could not resolve a closed JNI callback declaring type for '{callbackDeclaringType}'."); + } + callbackDeclaringType = baseType; + } + + GetCallbackHandler connector = (GetCallbackHandler) Delegate.CreateDelegate (typeof (GetCallbackHandler), + callbackDeclaringType, callbackString.ToString ()); + callback = connector (); + } + + if (callback != null) { + needToRegisterNatives = true; + natives [nativesIndex++] = new JniNativeMethodRegistration (name.ToString (), signature.ToString (), callback); + } + } + + methodsSpan = newLineIndex != -1 ? methodsSpan.Slice (newLineIndex + 1) : default; + } + + if (needToRegisterNatives) { + JniEnvironment.Types.RegisterNatives (nativeClass.PeerReference, natives, nativesIndex); + } + + return true; + } + + static int CountMethods (ReadOnlySpan methodsSpan) + { + int count = 0; + while (!methodsSpan.IsEmpty) { + count++; + + int newLineIndex = methodsSpan.IndexOf ('\n'); + methodsSpan = newLineIndex != -1 ? methodsSpan.Slice (newLineIndex + 1) : default; + } + return count; + } + + static void SplitMethodLine ( + ReadOnlySpan methodLine, + out ReadOnlySpan name, + out ReadOnlySpan signature, + out ReadOnlySpan callback, + out ReadOnlySpan callbackDeclaringType) + { + int colonIndex = methodLine.IndexOf (':'); + name = methodLine.Slice (0, colonIndex); + methodLine = methodLine.Slice (colonIndex + 1); + + colonIndex = methodLine.IndexOf (':'); + signature = methodLine.Slice (0, colonIndex); + methodLine = methodLine.Slice (colonIndex + 1); + + colonIndex = methodLine.IndexOf (':'); + callback = methodLine.Slice (0, colonIndex != -1 ? colonIndex : methodLine.Length); + + callbackDeclaringType = colonIndex != -1 ? methodLine.Slice (colonIndex + 1) : default; + } + + static Delegate CreateDynamicCallback (MethodInfo method) + { + if (dynamic_callback_gen == null) { + var assembly = Assembly.Load ("Mono.Android.Export"); + if (assembly == null) + throw new InvalidOperationException ("To use methods marked with ExportAttribute, Mono.Android.Export.dll needs to be referenced in the application"); + var type = assembly.GetType ("Java.Interop.DynamicCallbackCodeGenerator"); + if (type == null) + throw new InvalidOperationException ("The referenced Mono.Android.Export.dll does not match the expected version. The required type was not found."); + dynamic_callback_gen = type.GetMethod ("Create"); + if (dynamic_callback_gen == null) + throw new InvalidOperationException ("The referenced Mono.Android.Export.dll does not match the expected version. The required method was not found."); + } + var callback = dynamic_callback_gen.Invoke (null, [ method ]); + if (callback is not Delegate result) { + throw new InvalidOperationException ("The referenced Mono.Android.Export.dll returned an invalid dynamic callback."); + } + return result; + } +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs index 5d20f9e5ac4..3afe20a398b 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs @@ -13,6 +13,7 @@ static class RuntimeFeature const bool StartupHookSupportEnabledByDefault = true; const bool TrimmableTypeMapEnabledByDefault = false; const bool ObjectReferenceLoggingEnabledByDefault = false; + const bool StringBasedJniRegistrationEnabledByDefault = true; const string FeatureSwitchPrefix = "Microsoft.Android.Runtime.RuntimeFeature."; const string StartupHookProviderSwitch = "System.StartupHookProvider.IsSupported"; @@ -49,4 +50,10 @@ static class RuntimeFeature [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (ObjectReferenceLogging)}")] internal static bool ObjectReferenceLogging { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (ObjectReferenceLogging)}", out bool isEnabled) ? isEnabled : ObjectReferenceLoggingEnabledByDefault; + + [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (StringBasedJniRegistration)}")] + [FeatureGuard (typeof (RequiresUnreferencedCodeAttribute))] + [FeatureGuard (typeof (RequiresDynamicCodeAttribute))] + internal static bool StringBasedJniRegistration { get; } = + AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (StringBasedJniRegistration)}", out bool isEnabled) ? isEnabled : StringBasedJniRegistrationEnabledByDefault; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 915daa0e248..6541b1b94d7 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -5,7 +5,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Runtime.InteropServices; using System.Threading; using Android.Runtime; using Java.Interop; @@ -22,7 +21,6 @@ public class TrimmableTypeMap static readonly Lock s_initLock = new (); static readonly JavaPeerProxy s_noPeerSentinel = new MissingJavaPeerProxy (); static TrimmableTypeMap? s_instance; - static bool s_nativeMethodsRegistered; static JniMethodInfo? s_classGetInterfacesMethod; internal static TrimmableTypeMap Instance => @@ -129,28 +127,6 @@ static void InitializeCore (ITypeMap typeMap) } } - internal static unsafe void RegisterNativeMethods () - { - lock (s_initLock) { - if (s_nativeMethodsRegistered) { - throw new InvalidOperationException ("TrimmableTypeMap native methods have already been registered."); - } - - if (s_instance is null) { - throw new InvalidOperationException ( - "TrimmableTypeMap has not been initialized. Ensure RuntimeFeature.TrimmableTypeMap is enabled and the JNI runtime is initialized."); - } - - using var runtimeClass = new JniType ("mono/android/Runtime"u8); - fixed (byte* name = "registerNatives"u8, sig = "(Ljava/lang/Class;)V"u8) { - var onRegisterNatives = (IntPtr)(delegate* unmanaged)&OnRegisterNatives; - var method = new JniNativeMethod (name, sig, onRegisterNatives); - JniEnvironment.Types.RegisterNatives (runtimeClass.PeerReference, [method]); - } - s_nativeMethodsRegistered = true; - } - } - /// /// Returns all target types mapped to a JNI name. For non-alias entries, returns a /// single-element array. For alias groups, returns the surviving target types from @@ -233,6 +209,29 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return jniName is not null; } + internal void RegisterNatives (IntPtr nativeClassHandle) + { + var classRef = new JniObjectReference (nativeClassHandle, JniObjectReferenceType.Local); + var className = JniEnvironment.Types.GetJniTypeNameFromClass (classRef) ?? throw new InvalidOperationException ($"TrimmableTypeMap: Could not get JNI name for class {classRef.Handle:x8}"); + var wrapper = GetAndroidCallableWrapper (className); + using var jniType = new JniType (ref classRef, JniObjectReferenceOptions.Copy); + wrapper.RegisterNatives (jniType); + + IAndroidCallableWrapper GetAndroidCallableWrapper (string className) + { + var proxies = GetProxiesForJniName (className); + if (proxies.Length == 0) { + throw new InvalidOperationException ($"TrimmableTypeMap: No JavaPeerProxy found for Java type '{className}'"); + } else if (proxies.Length > 1) { + throw new InvalidOperationException ($"TrimmableTypeMap: Multiple JavaPeerProxies found for Java type '{className}'"); + } else if (proxies [0] is not IAndroidCallableWrapper acw) { + throw new InvalidOperationException ($"TrimmableTypeMap: JavaPeerProxy for Java type '{className}' does not implement IAndroidCallableWrapper and so cannot register native methods"); + } else { + return acw; + } + } + } + internal JavaPeerProxy? GetProxyForJavaObject (IntPtr handle, Type? targetType = null) { if (handle == IntPtr.Zero) { @@ -569,40 +568,6 @@ static bool TryGetPrimitiveJniName (Type primitive, [NotNullWhen (true)] out str return false; } - [UnmanagedCallersOnly] - static void OnRegisterNatives (IntPtr jnienv, IntPtr klass, IntPtr nativeClassHandle) - { - string? className = null; - try { - if (s_instance is null) { - return; - } - - var classRef = new JniObjectReference (nativeClassHandle); - className = JniEnvironment.Types.GetJniTypeNameFromClass (classRef); - if (className is null) { - return; - } - - var proxies = s_instance.GetProxiesForJniName (className); - if (proxies.Length == 0) { - return; - } - - // Use the class reference passed from Java (via C++) — not JniType(className) - // which resolves via FindClass and may get a different class from a different ClassLoader. - // Registering natives on that other instance is silently wrong. - using var jniType = new JniType (ref classRef, JniObjectReferenceOptions.Copy); - foreach (var proxy in proxies) { - if (proxy is IAndroidCallableWrapper acw) { - acw.RegisterNatives (jniType); - } - } - } catch (Exception ex) { - Environment.FailFast ($"TrimmableTypeMap: Failed to register natives for class '{className}'.", ex); - } - } - sealed class MissingJavaPeerProxy : JavaPeerProxy { public MissingJavaPeerProxy () : base ("", typeof (Java.Lang.Object), null) diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index dea8cb2dcb8..4dbbe60a895 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Reflection; using Java.Interop; namespace Microsoft.Android.Runtime; @@ -109,13 +107,33 @@ protected override IEnumerable GetSimpleReferences (Type type) } public override void RegisterNativeMembers ( - JniType nativeClass, - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes)] - Type type, - ReadOnlySpan methods) + JniType nativeClass, + [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes)] + Type type, + ReadOnlySpan methods) { - throw new UnreachableException ( - $"RegisterNativeMembers should not be called in the trimmable typemap path. " + - $"Native methods for '{type.FullName}' should be registered by JCW static initializer blocks."); + // In the trimmable type map, native methods are registered by Java Callable Wrapper static + // initializers via the fast path (mono.android.Runtime.registerNatives). The string-based + // entry points that reach this overload (a JCW calling mono.android.Runtime.register(...), + // or Java.Interop.ManagedPeer) are disabled by default and only honored when + // RuntimeFeature.StringBasedJniRegistration is enabled. + if (RuntimeFeature.StringBasedJniRegistration) { + if (!NativeMethodRegistration.TryRegisterNativeMembers (nativeClass, type, methods)) { + throw new InvalidOperationException ($"Unable to register native methods for '{type.FullName}'."); + } + } else { + throw new NotSupportedException ( + $""" + Java called back to register native methods for '{type.FullName}' using the string-based JNI registration path, which is disabled for the trimmable type map. + + This is either: + - A bug in .NET for Android - the trimmable type map should have registered these natives via 'mono.android.Runtime.registerNatives'. Please report it at https://github.com/dotnet/android/issues, quoting the type name above. + - Caused by an outdated/precompiled Java library whose Java Callable Wrappers call 'mono.android.Runtime.register(...)'. To keep using it, re-enable string-based JNI registration by adding this to your .csproj: + + <_AndroidEnableStringBasedJniRegistration>true + + Please also report the library at https://github.com/dotnet/android/issues so we can investigate further. + """); + } } } diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 611ef7edf07..5a9f67d15e6 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -366,6 +366,7 @@ + diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.RuntimeConfig.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.RuntimeConfig.targets index 276cc236459..7fbac911fc1 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.RuntimeConfig.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.RuntimeConfig.targets @@ -20,6 +20,14 @@ See: https://github.com/dotnet/runtime/blob/b13715b6984889a709ba29ea8a1961db469f <_AndroidEnableDiagnosticCrashReporting Condition=" '$(_AndroidEnableDiagnosticCrashReporting)' == '' ">true + + <_AndroidEnableStringBasedJniRegistration Condition=" '$(_AndroidEnableStringBasedJniRegistration)' == '' and '$(_AndroidTypeMapImplementation)' == 'trimmable' ">false + <_AndroidEnableStringBasedJniRegistration Condition=" '$(_AndroidEnableStringBasedJniRegistration)' == '' ">true @@ -68,6 +76,10 @@ See: https://github.com/dotnet/runtime/blob/b13715b6984889a709ba29ea8a1961db469f Value="$(_AndroidEnableObjectReferenceLogging)" Trim="true" /> +