From 6a3da6e8bcd7f54bf856795932ae121f4bf2ead7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Sun, 14 Jun 2026 19:24:45 +0200 Subject: [PATCH 1/5] [Mono.Android] Support legacy RegisterNativeMembers in trimmable typemap Add an opt-in feature switch, `Microsoft.Android.Runtime.RuntimeFeature.LegacyJniRegistration` (MSBuild `_AndroidEnableLegacyJniRegistration`, default `false`), that re-enables the legacy, reflection-based JNI native-method registration in the trimmable type map path. By default the trimmable type map registers native methods via the fast path (JCW static initializers -> `mono.android.Runtime.registerNatives`), and `TrimmableTypeMapTypeManager.RegisterNativeMembers` throws. That makes two legacy mechanisms unusable with the trimmable type map: * legacy precompiled Java Callable Wrappers (from binding jars/aars) whose static initializers call `mono.android.Runtime.register("Type, Asm", Class, "methods")`, and * `Java.Interop.ManagedPeer.registerNativeMembers`. When the switch is enabled: * `JNIEnvInit.Initialize` wires `registerJniNativesFn` even in the trimmable path, so the native `register(...)` call flows back into managed code; and * `TrimmableTypeMapTypeManager.RegisterNativeMembers` performs the reflection-based registration instead of throwing. The reflection parsing logic is extracted from `ManagedTypeManager` into a shared `NativeMethodRegistrar` helper and reused by both managers (removing a duplicate copy). With the switch off (the default), the helper and the `register(...)` callback are unreferenced and trimmed away. No native changes are required: `Java_mono_android_Runtime_register` is a name-exported JNICALL that is always present and only acts when the managed `registerJniNativesFn` pointer is set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/JNIEnvInit.cs | 5 +- .../ManagedTypeManager.cs | 96 +----------- .../NativeMethodRegistrar.cs | 140 ++++++++++++++++++ .../RuntimeFeature.cs | 10 ++ .../TrimmableTypeMapTypeManager.cs | 18 ++- src/Mono.Android/Mono.Android.csproj | 1 + ...icrosoft.Android.Sdk.RuntimeConfig.targets | 10 ++ 7 files changed, 181 insertions(+), 99 deletions(-) create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistrar.cs diff --git a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs index c2e7ea913ca..70d97d0bc18 100644 --- a/src/Mono.Android/Android.Runtime/JNIEnvInit.cs +++ b/src/Mono.Android/Android.Runtime/JNIEnvInit.cs @@ -157,7 +157,10 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) args->propagateUncaughtExceptionFn = (IntPtr)(delegate* unmanaged)&PropagateUncaughtException; - if (!RuntimeFeature.TrimmableTypeMap) { + if (!RuntimeFeature.TrimmableTypeMap || RuntimeFeature.LegacyJniRegistration) { + // Non-trimmable typemap: always needed. Trimmable typemap: only needed when the + // legacy reflection-based registration is opted into (e.g. legacy precompiled jars + // whose JCWs call `mono.android.Runtime.register(...)`). args->registerJniNativesFn = (IntPtr)(delegate* unmanaged)&RegisterJniNatives; } RunStartupHooksIfNeeded (); diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs index 454bab0e1bb..687a3cd09ad 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs @@ -59,76 +59,17 @@ static Type MakeGenericType ( return MakeGenericType (suffixDefinition, arguments); } - // NOTE: suppressions below also in `src/Mono.Android/Android.Runtime/AndroidRuntime.cs` - [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, ReadOnlySpan methods) { - if (methods.IsEmpty) { + if (!NativeMethodRegistrar.TryRegisterNativeMembers (nativeClass, type, methods)) { base.RegisterNativeMembers (nativeClass, type, methods); - return; - } - - int methodCount = CountMethods (methods); - if (methodCount < 1) { - base.RegisterNativeMembers (nativeClass, type, methods); - return; - } - - JniNativeMethodRegistration [] natives = new JniNativeMethodRegistration [methodCount]; - int nativesIndex = 0; - - 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__")) { - throw new InvalidOperationException (FormattableString.Invariant ($"Methods such as {callbackString.ToString ()} are not implemented!")); - } 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); } } - protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { // Base class contains built-in mappings (e.g. java/lang/String → System.String) @@ -156,39 +97,4 @@ protected override IEnumerable GetSimpleReferences (Type type) { return JniRemappingLookup.GetStaticMethodFallbackTypes (jniSimpleReference, useReplacementTypes: false); } - - 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; - } - - delegate Delegate GetCallbackHandler (); } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistrar.cs b/src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistrar.cs new file mode 100644 index 00000000000..063067ac428 --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistrar.cs @@ -0,0 +1,140 @@ +#nullable enable + +using System; +using System.Diagnostics.CodeAnalysis; +using Java.Interop; + +namespace Microsoft.Android.Runtime; + +/// +/// Shared implementation of the legacy, reflection-based JNI native method registration. +/// Parses the methods metadata string (produced for Java Callable Wrappers and +/// net/dot/jni/ManagedPeer.registerNativeMembers) and registers the native callbacks +/// via . +/// +/// This is the "slow" path: it relies on and +/// , so it is not trim-friendly. It is used directly by +/// and, behind , +/// by . +/// +static class NativeMethodRegistrar +{ + const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = + DynamicallyAccessedMemberTypes.PublicMethods | + DynamicallyAccessedMemberTypes.NonPublicMethods | + DynamicallyAccessedMemberTypes.NonPublicNestedTypes; + + // NOTE: suppressions below also in `src/Mono.Android/Android.Runtime/AndroidRuntime.cs` + // and historically in `ManagedTypeManager`. + /// + /// Registers the native members described by . + /// + /// + /// if the string was parsed (whether or not + /// any natives were registered); if it was empty / contained no method + /// lines, in which case the caller should fall back to + /// base.RegisterNativeMembers (the marshal-methods path). + /// + [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 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; + + 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__")) { + throw new InvalidOperationException (FormattableString.Invariant ($"Methods such as {callbackString.ToString ()} are not implemented!")); + } 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); + } + + 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; + } + + delegate Delegate GetCallbackHandler (); +} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs index 5d20f9e5ac4..0950b8b4d37 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 LegacyJniRegistrationEnabledByDefault = false; const string FeatureSwitchPrefix = "Microsoft.Android.Runtime.RuntimeFeature."; const string StartupHookProviderSwitch = "System.StartupHookProvider.IsSupported"; @@ -49,4 +50,13 @@ static class RuntimeFeature [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (ObjectReferenceLogging)}")] internal static bool ObjectReferenceLogging { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (ObjectReferenceLogging)}", out bool isEnabled) ? isEnabled : ObjectReferenceLoggingEnabledByDefault; + + // When enabled (together with the trimmable typemap), re-enables the legacy, reflection-based + // JNI native method registration (NativeMethodRegistrar / Java.Interop.ManagedPeer). This is + // required to support legacy precompiled Java Callable Wrappers (from binding jars/aars) whose + // static initializers call `mono.android.Runtime.register(...)`, and `ManagedPeer`. Disabled by + // default so the reflection-based path can be trimmed away when it is not needed. + [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (LegacyJniRegistration)}")] + internal static bool LegacyJniRegistration { get; } = + AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (LegacyJniRegistration)}", out bool isEnabled) ? isEnabled : LegacyJniRegistrationEnabledByDefault; } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index dea8cb2dcb8..24d58162d0a 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -114,8 +114,20 @@ public override void RegisterNativeMembers ( 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."); + // By default, native methods in the trimmable typemap path are registered by JCW static + // initializer blocks via the fast path (mono.android.Runtime.registerNatives), so this + // reflection-based overload should never be reached. The legacy, reflection-based path is + // only supported when explicitly opted into via RuntimeFeature.LegacyJniRegistration (e.g. + // to support legacy precompiled jars or Java.Interop.ManagedPeer). + if (!RuntimeFeature.LegacyJniRegistration) { + throw new UnreachableException ( + $"RegisterNativeMembers should not be called in the trimmable typemap path " + + $"unless RuntimeFeature.LegacyJniRegistration is enabled. Native methods for " + + $"'{type.FullName}' should be registered by JCW static initializer blocks."); + } + + if (!NativeMethodRegistrar.TryRegisterNativeMembers (nativeClass, type, methods)) { + base.RegisterNativeMembers (nativeClass, type, methods); + } } } diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 611ef7edf07..9d7094d40fb 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -365,6 +365,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..80216bd5756 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,12 @@ See: https://github.com/dotnet/runtime/blob/b13715b6984889a709ba29ea8a1961db469f <_AndroidEnableDiagnosticCrashReporting Condition=" '$(_AndroidEnableDiagnosticCrashReporting)' == '' ">true + + <_AndroidEnableLegacyJniRegistration Condition=" '$(_AndroidEnableLegacyJniRegistration)' == '' ">false @@ -68,6 +74,10 @@ See: https://github.com/dotnet/runtime/blob/b13715b6984889a709ba29ea8a1961db469f Value="$(_AndroidEnableObjectReferenceLogging)" Trim="true" /> + Date: Sun, 14 Jun 2026 19:52:38 +0200 Subject: [PATCH 2/5] [Mono.Android] Reuse fast path for legacy RegisterNativeMembers (trimmable) Refine the trimmable legacy-registration prototype to reuse the generated fast path instead of the slow, reflection-based registration. The legacy `RegisterNativeMembers(JniType, Type, ReadOnlySpan methods)` already carries the managed `Type`, which keys directly into the generated `IAndroidCallableWrapper` proxy. So when `RuntimeFeature.LegacyJniRegistration` is enabled, `TrimmableTypeMapTypeManager.RegisterNativeMembers` now calls `TrimmableTypeMap.TryRegisterNativeMembers`, which resolves the proxy via `GetProxyForManagedType(type)` and invokes `acw.RegisterNatives(nativeClass)` -- the same trim-safe primitive used by `OnRegisterNatives` (`mono.android.Runtime.registerNatives`). `type` selects the proxy; the `methods` metadata string is redundant and is used only for a `[Conditional("DEBUG")]` validation (JNI-name match and non-empty methods). This keeps the legacy entry points (legacy precompiled JCWs calling `mono.android.Runtime.register(...)`, and `Java.Interop.ManagedPeer`) reflection-free, so there is nothing extra to trim. Because the trimmable path no longer needs shared reflection parsing, the previous `NativeMethodRegistrar` extraction is reverted and `ManagedTypeManager` is restored to its original form. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ManagedTypeManager.cs | 96 +++++++++++- .../NativeMethodRegistrar.cs | 140 ------------------ .../TrimmableTypeMap.cs | 43 ++++++ .../TrimmableTypeMapTypeManager.cs | 18 ++- src/Mono.Android/Mono.Android.csproj | 1 - 5 files changed, 151 insertions(+), 147 deletions(-) delete mode 100644 src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistrar.cs diff --git a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs index 687a3cd09ad..454bab0e1bb 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/ManagedTypeManager.cs @@ -59,17 +59,76 @@ static Type MakeGenericType ( return MakeGenericType (suffixDefinition, arguments); } + // NOTE: suppressions below also in `src/Mono.Android/Android.Runtime/AndroidRuntime.cs` + [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, ReadOnlySpan methods) { - if (!NativeMethodRegistrar.TryRegisterNativeMembers (nativeClass, type, methods)) { + if (methods.IsEmpty) { base.RegisterNativeMembers (nativeClass, type, methods); + return; + } + + int methodCount = CountMethods (methods); + if (methodCount < 1) { + base.RegisterNativeMembers (nativeClass, type, methods); + return; + } + + JniNativeMethodRegistration [] natives = new JniNativeMethodRegistration [methodCount]; + int nativesIndex = 0; + + 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__")) { + throw new InvalidOperationException (FormattableString.Invariant ($"Methods such as {callbackString.ToString ()} are not implemented!")); + } 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); } } + protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { // Base class contains built-in mappings (e.g. java/lang/String → System.String) @@ -97,4 +156,39 @@ protected override IEnumerable GetSimpleReferences (Type type) { return JniRemappingLookup.GetStaticMethodFallbackTypes (jniSimpleReference, useReplacementTypes: false); } + + 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; + } + + delegate Delegate GetCallbackHandler (); } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistrar.cs b/src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistrar.cs deleted file mode 100644 index 063067ac428..00000000000 --- a/src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistrar.cs +++ /dev/null @@ -1,140 +0,0 @@ -#nullable enable - -using System; -using System.Diagnostics.CodeAnalysis; -using Java.Interop; - -namespace Microsoft.Android.Runtime; - -/// -/// Shared implementation of the legacy, reflection-based JNI native method registration. -/// Parses the methods metadata string (produced for Java Callable Wrappers and -/// net/dot/jni/ManagedPeer.registerNativeMembers) and registers the native callbacks -/// via . -/// -/// This is the "slow" path: it relies on and -/// , so it is not trim-friendly. It is used directly by -/// and, behind , -/// by . -/// -static class NativeMethodRegistrar -{ - const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = - DynamicallyAccessedMemberTypes.PublicMethods | - DynamicallyAccessedMemberTypes.NonPublicMethods | - DynamicallyAccessedMemberTypes.NonPublicNestedTypes; - - // NOTE: suppressions below also in `src/Mono.Android/Android.Runtime/AndroidRuntime.cs` - // and historically in `ManagedTypeManager`. - /// - /// Registers the native members described by . - /// - /// - /// if the string was parsed (whether or not - /// any natives were registered); if it was empty / contained no method - /// lines, in which case the caller should fall back to - /// base.RegisterNativeMembers (the marshal-methods path). - /// - [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 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; - - 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__")) { - throw new InvalidOperationException (FormattableString.Invariant ($"Methods such as {callbackString.ToString ()} are not implemented!")); - } 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); - } - - 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; - } - - delegate Delegate GetCallbackHandler (); -} diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 915daa0e248..8e836dbb259 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.InteropServices; @@ -233,6 +234,48 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return jniName is not null; } + /// + /// Registers the JNI native methods for by reusing the generated + /// fast path — the same trim-safe registration performed + /// by the mono.android.Runtime.registerNatives path (see ). + /// + /// This lets the legacy entry points that flow through + /// JniRuntime.JniTypeManager.RegisterNativeMembers (a legacy precompiled JCW calling + /// mono.android.Runtime.register(...), or Java.Interop.ManagedPeer) register their + /// natives without the slow, reflection-based path: the generated proxy keyed off + /// already knows the native callbacks. The + /// metadata string carried by those entry points is redundant here and is used only for + /// validation. + /// + /// + /// if a generated ACW proxy was found for and used; + /// otherwise (the caller should fall back). + /// + internal bool TryRegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) + { + var proxy = GetProxyForManagedType (type); + if (proxy is not IAndroidCallableWrapper acw) { + return false; + } + + ValidateLegacyRegistration (proxy, nativeClass, methods); + acw.RegisterNatives (nativeClass); + return true; + } + + // The legacy `methods` string and the managed `type` carry no information the generated fast + // path needs; they are only cross-checked here to catch mismatches during development. + [Conditional ("DEBUG")] + static void ValidateLegacyRegistration (JavaPeerProxy proxy, JniType nativeClass, ReadOnlySpan methods) + { + Debug.Assert ( + string.Equals (proxy.JniName, nativeClass.Name, StringComparison.Ordinal), + $"Legacy RegisterNativeMembers JNI name mismatch: proxy '{proxy.JniName}' vs class '{nativeClass.Name}'."); + Debug.Assert ( + !methods.IsEmpty, + $"Legacy RegisterNativeMembers called for '{proxy.JniName}' with an empty methods string."); + } + internal JavaPeerProxy? GetProxyForJavaObject (IntPtr handle, Type? targetType = null) { if (handle == IntPtr.Zero) { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs index 24d58162d0a..1a8a1bd03fd 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMapTypeManager.cs @@ -116,9 +116,10 @@ public override void RegisterNativeMembers ( { // By default, native methods in the trimmable typemap path are registered by JCW static // initializer blocks via the fast path (mono.android.Runtime.registerNatives), so this - // reflection-based overload should never be reached. The legacy, reflection-based path is - // only supported when explicitly opted into via RuntimeFeature.LegacyJniRegistration (e.g. - // to support legacy precompiled jars or Java.Interop.ManagedPeer). + // reflection-based overload should never be reached. The legacy entry points that do call + // it (a legacy precompiled JCW's mono.android.Runtime.register(...), or + // Java.Interop.ManagedPeer) are only supported when explicitly opted into via + // RuntimeFeature.LegacyJniRegistration. if (!RuntimeFeature.LegacyJniRegistration) { throw new UnreachableException ( $"RegisterNativeMembers should not be called in the trimmable typemap path " + @@ -126,8 +127,15 @@ public override void RegisterNativeMembers ( $"'{type.FullName}' should be registered by JCW static initializer blocks."); } - if (!NativeMethodRegistrar.TryRegisterNativeMembers (nativeClass, type, methods)) { - base.RegisterNativeMembers (nativeClass, type, methods); + // Reuse the trimmable fast path rather than the slow, reflection-based registration: the + // generated ACW proxy keyed off `type` already knows the native callbacks, so `methods` is + // redundant (used only for validation inside TryRegisterNativeMembers). + if (TrimmableTypeMap.Instance.TryRegisterNativeMembers (nativeClass, type, methods)) { + return; } + + // No generated ACW proxy for this type (e.g. nothing to register / empty methods). Fall back + // to the base marshal-method path, which is a safe no-op in trimmed builds. + base.RegisterNativeMembers (nativeClass, type, methods); } } diff --git a/src/Mono.Android/Mono.Android.csproj b/src/Mono.Android/Mono.Android.csproj index 9d7094d40fb..611ef7edf07 100644 --- a/src/Mono.Android/Mono.Android.csproj +++ b/src/Mono.Android/Mono.Android.csproj @@ -365,7 +365,6 @@ - From 6f7f6c08340f23c6a77a8b8841db2d8d957e7fae Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 15 Jun 2026 12:49:51 +0200 Subject: [PATCH 3/5] [Mono.Android] Share string-based JNI native registration Extract the string-based JNI native method registration logic from AndroidTypeManager.RegisterNativeMembers into a shared NativeMethodRegistration helper. The helper owns parsing the method metadata string, resolving callback delegates, handling [Export] dynamic callbacks, rooting those callbacks, and registering natives with JNI. Use the helper from AndroidTypeManager after the linker-generated fast registration map, preserving the existing fallback to Java.Interop's marshal-method registration when applicable. Also use the same helper from TrimmableTypeMapTypeManager when the new string-based registration feature switch is enabled. Rename the feature to RuntimeFeature.StringBasedJniRegistration and guard it with [FeatureGuard(typeof(RequiresUnreferencedCodeAttribute))], because this path relies on reflection over method metadata strings. The switch is true by default for non-trimmable typemaps and false by default for the trimmable typemap, where native registration normally happens through mono.android.Runtime.registerNatives(Class). Move the trimmable registerNatives JNI callback wiring out of TrimmableTypeMap and into JNIEnvInit, so responsibilities are clearer: JNIEnvInit wires JNI callbacks, TrimmableTypeMap resolves generated wrappers, and TrimmableTypeMapTypeManager handles JniTypeManager behavior. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Android.Runtime/AndroidRuntime.cs | 145 +------------ .../Android.Runtime/JNIEnvInit.cs | 33 ++- .../NativeMethodRegistration.cs | 197 ++++++++++++++++++ .../RuntimeFeature.cs | 14 +- .../TrimmableTypeMap.cs | 116 ++--------- .../TrimmableTypeMapTypeManager.cs | 54 +++-- src/Mono.Android/Mono.Android.csproj | 1 + ...icrosoft.Android.Sdk.RuntimeConfig.targets | 18 +- 8 files changed, 288 insertions(+), 290 deletions(-) create mode 100644 src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistration.cs diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 918b8377b54..d0db95b8191 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,18 @@ 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 ("String-based JNI registration is disabled."); } 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 70d97d0bc18..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,12 +169,10 @@ internal static unsafe void Initialize (JnienvInitializeArgs* args) args->propagateUncaughtExceptionFn = (IntPtr)(delegate* unmanaged)&PropagateUncaughtException; - if (!RuntimeFeature.TrimmableTypeMap || RuntimeFeature.LegacyJniRegistration) { - // Non-trimmable typemap: always needed. Trimmable typemap: only needed when the - // legacy reflection-based registration is opted into (e.g. legacy precompiled jars - // whose JCWs call `mono.android.Runtime.register(...)`). + if (RuntimeFeature.StringBasedJniRegistration) { args->registerJniNativesFn = (IntPtr)(delegate* unmanaged)&RegisterJniNatives; } + RunStartupHooksIfNeeded (); SetSynchronizationContext (); } @@ -227,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..234934ef822 --- /dev/null +++ b/src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistration.cs @@ -0,0 +1,197 @@ +#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-friendly and is +/// marked . It is shared by +/// AndroidTypeManager (the default llvm-ir/MonoVM path) and, gated behind +/// , by TrimmableTypeMapTypeManager. +/// +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. + /// + [RequiresUnreferencedCode ("Parses the 'methods' metadata string and resolves JNI callbacks via reflection (Type.GetType / Delegate.CreateDelegate).")] + 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; + } + + // 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."); + } + 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 0950b8b4d37..f937cef62c9 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs @@ -13,7 +13,7 @@ static class RuntimeFeature const bool StartupHookSupportEnabledByDefault = true; const bool TrimmableTypeMapEnabledByDefault = false; const bool ObjectReferenceLoggingEnabledByDefault = false; - const bool LegacyJniRegistrationEnabledByDefault = false; + const bool StringBasedJniRegistrationEnabledByDefault = true; const string FeatureSwitchPrefix = "Microsoft.Android.Runtime.RuntimeFeature."; const string StartupHookProviderSwitch = "System.StartupHookProvider.IsSupported"; @@ -51,12 +51,8 @@ static class RuntimeFeature internal static bool ObjectReferenceLogging { get; } = AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (ObjectReferenceLogging)}", out bool isEnabled) ? isEnabled : ObjectReferenceLoggingEnabledByDefault; - // When enabled (together with the trimmable typemap), re-enables the legacy, reflection-based - // JNI native method registration (NativeMethodRegistrar / Java.Interop.ManagedPeer). This is - // required to support legacy precompiled Java Callable Wrappers (from binding jars/aars) whose - // static initializers call `mono.android.Runtime.register(...)`, and `ManagedPeer`. Disabled by - // default so the reflection-based path can be trimmed away when it is not needed. - [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (LegacyJniRegistration)}")] - internal static bool LegacyJniRegistration { get; } = - AppContext.TryGetSwitch ($"{FeatureSwitchPrefix}{nameof (LegacyJniRegistration)}", out bool isEnabled) ? isEnabled : LegacyJniRegistrationEnabledByDefault; + [FeatureSwitchDefinition ($"{FeatureSwitchPrefix}{nameof (StringBasedJniRegistration)}")] + [FeatureGuard (typeof (RequiresUnreferencedCodeAttribute))] + 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 8e836dbb259..6541b1b94d7 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -3,10 +3,8 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Reflection; -using System.Runtime.InteropServices; using System.Threading; using Android.Runtime; using Java.Interop; @@ -23,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 => @@ -130,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 @@ -234,46 +209,27 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return jniName is not null; } - /// - /// Registers the JNI native methods for by reusing the generated - /// fast path — the same trim-safe registration performed - /// by the mono.android.Runtime.registerNatives path (see ). - /// - /// This lets the legacy entry points that flow through - /// JniRuntime.JniTypeManager.RegisterNativeMembers (a legacy precompiled JCW calling - /// mono.android.Runtime.register(...), or Java.Interop.ManagedPeer) register their - /// natives without the slow, reflection-based path: the generated proxy keyed off - /// already knows the native callbacks. The - /// metadata string carried by those entry points is redundant here and is used only for - /// validation. - /// - /// - /// if a generated ACW proxy was found for and used; - /// otherwise (the caller should fall back). - /// - internal bool TryRegisterNativeMembers (JniType nativeClass, Type type, ReadOnlySpan methods) + internal void RegisterNatives (IntPtr nativeClassHandle) { - var proxy = GetProxyForManagedType (type); - if (proxy is not IAndroidCallableWrapper acw) { - return false; - } + 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); - ValidateLegacyRegistration (proxy, nativeClass, methods); - acw.RegisterNatives (nativeClass); - return true; - } - - // The legacy `methods` string and the managed `type` carry no information the generated fast - // path needs; they are only cross-checked here to catch mismatches during development. - [Conditional ("DEBUG")] - static void ValidateLegacyRegistration (JavaPeerProxy proxy, JniType nativeClass, ReadOnlySpan methods) - { - Debug.Assert ( - string.Equals (proxy.JniName, nativeClass.Name, StringComparison.Ordinal), - $"Legacy RegisterNativeMembers JNI name mismatch: proxy '{proxy.JniName}' vs class '{nativeClass.Name}'."); - Debug.Assert ( - !methods.IsEmpty, - $"Legacy RegisterNativeMembers called for '{proxy.JniName}' with an empty methods string."); + 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) @@ -612,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 1a8a1bd03fd..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,33 +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) { - // By default, native methods in the trimmable typemap path are registered by JCW static - // initializer blocks via the fast path (mono.android.Runtime.registerNatives), so this - // reflection-based overload should never be reached. The legacy entry points that do call - // it (a legacy precompiled JCW's mono.android.Runtime.register(...), or - // Java.Interop.ManagedPeer) are only supported when explicitly opted into via - // RuntimeFeature.LegacyJniRegistration. - if (!RuntimeFeature.LegacyJniRegistration) { - throw new UnreachableException ( - $"RegisterNativeMembers should not be called in the trimmable typemap path " + - $"unless RuntimeFeature.LegacyJniRegistration is enabled. Native methods for " + - $"'{type.FullName}' should be registered by JCW static initializer blocks."); - } - - // Reuse the trimmable fast path rather than the slow, reflection-based registration: the - // generated ACW proxy keyed off `type` already knows the native callbacks, so `methods` is - // redundant (used only for validation inside TryRegisterNativeMembers). - if (TrimmableTypeMap.Instance.TryRegisterNativeMembers (nativeClass, type, methods)) { - return; + // 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. + """); } - - // No generated ACW proxy for this type (e.g. nothing to register / empty methods). Fall back - // to the base marshal-method path, which is a safe no-op in trimmed builds. - base.RegisterNativeMembers (nativeClass, type, methods); } } 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 80216bd5756..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,12 +20,14 @@ See: https://github.com/dotnet/runtime/blob/b13715b6984889a709ba29ea8a1961db469f <_AndroidEnableDiagnosticCrashReporting Condition=" '$(_AndroidEnableDiagnosticCrashReporting)' == '' ">true - - <_AndroidEnableLegacyJniRegistration Condition=" '$(_AndroidEnableLegacyJniRegistration)' == '' ">false + + <_AndroidEnableStringBasedJniRegistration Condition=" '$(_AndroidEnableStringBasedJniRegistration)' == '' and '$(_AndroidTypeMapImplementation)' == 'trimmable' ">false + <_AndroidEnableStringBasedJniRegistration Condition=" '$(_AndroidEnableStringBasedJniRegistration)' == '' ">true @@ -74,8 +76,8 @@ See: https://github.com/dotnet/runtime/blob/b13715b6984889a709ba29ea8a1961db469f Value="$(_AndroidEnableObjectReferenceLogging)" Trim="true" /> - From c32bd7722d499cdbb1d9a3567ae4adb8c0d039ba Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 15 Jun 2026 13:42:33 +0200 Subject: [PATCH 4/5] [Mono.Android] Mark string JNI registration dynamic-code unsafe Mark NativeMethodRegistration with both RequiresUnreferencedCode and RequiresDynamicCode. This makes the intent explicit: the string-based JNI registration path parses method metadata strings and resolves callbacks via reflection/dynamic delegate creation, so it is suitable for MonoVM and CoreCLR but not NativeAOT. Remove the previous UnconditionalSuppressMessage attributes from the helper so callers must flow through the feature switch instead of hiding trim/dynamic-code usage locally. Add a RequiresDynamicCode FeatureGuard to RuntimeFeature.StringBasedJniRegistration alongside the existing RequiresUnreferencedCode guard. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/AndroidRuntime.cs | 3 --- .../NativeMethodRegistration.cs | 10 ++++------ .../Microsoft.Android.Runtime/RuntimeFeature.cs | 1 + 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index d0db95b8191..4b79d9af78a 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -478,10 +478,7 @@ public override void RegisterNativeMembers ( jniAddNativeMethodRegistrationAttributePresent) { base.RegisterNativeMembers (nativeClass, type, methods.ToString ()); } - return; } - - throw new NotSupportedException ("String-based JNI registration is disabled."); } catch (Exception e) { JniEnvironment.Runtime.RaisePendingException (e); } diff --git a/src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistration.cs b/src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistration.cs index 234934ef822..23bb4a2a64c 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistration.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/NativeMethodRegistration.cs @@ -18,11 +18,13 @@ namespace Microsoft.Android.Runtime; /// . /// /// This relies on and -/// , so it is not trim-friendly and is -/// marked . It is shared by +/// , 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 = @@ -49,7 +51,6 @@ static class NativeMethodRegistration /// or not any natives were registered); if it was empty, in which case the /// caller may fall back to the marshal-methods path. /// - [RequiresUnreferencedCode ("Parses the 'methods' metadata string and resolves JNI callbacks via reflection (Type.GetType / Delegate.CreateDelegate).")] internal static bool TryRegisterNativeMembers ( JniType nativeClass, [DynamicallyAccessedMembers (MethodsAndPrivateNested)] @@ -172,9 +173,6 @@ static void SplitMethodLine ( callbackDeclaringType = colonIndex != -1 ? methodLine.Slice (colonIndex + 1) : default; } - // 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) { diff --git a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs index f937cef62c9..3afe20a398b 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/RuntimeFeature.cs @@ -53,6 +53,7 @@ static class RuntimeFeature [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; } From 39477d61c1d750f9f6b0e1e897c9a2b05af10acc Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 15 Jun 2026 14:46:33 +0200 Subject: [PATCH 5/5] [Mono.Android] Fail fast when string JNI registration is disabled Make AndroidTypeManager.RegisterNativeMembers raise a clear exception when string-based JNI registration is disabled and the linker-generated fast registration map did not handle the type. Previously this path silently returned, leaving native methods unregistered and deferring the failure to a later JNI call. The exception is caught by RegisterNativeMembers and surfaced through JniEnvironment.Runtime like other registration failures. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Mono.Android/Android.Runtime/AndroidRuntime.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs index 4b79d9af78a..5bea1e3f50a 100644 --- a/src/Mono.Android/Android.Runtime/AndroidRuntime.cs +++ b/src/Mono.Android/Android.Runtime/AndroidRuntime.cs @@ -478,7 +478,13 @@ public override void RegisterNativeMembers ( jniAddNativeMethodRegistrationAttributePresent) { base.RegisterNativeMembers (nativeClass, type, methods.ToString ()); } + return; } + + 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); }