From b5d3834b51f66f3fdbc6ccba28b7f812aa1c4fdb Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 15 Jun 2026 18:40:56 +0200 Subject: [PATCH 1/9] [Java.Interop] Make JavaProxyObject.RegisterNativeMembers private again #1441 changed `JavaProxyObject.RegisterNativeMembers` from `private` to `internal` so the new reflection-free `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()` could call it directly. A side effect is that the `[JniAddNativeMethodRegistrationAttribute]`- annotated method now appears in Java.Interop's *reference* assembly. dotnet/android's trimmable typemap scanner reads reference assemblies and rejects any type carrying that attribute (XA4251), so every trimmable typemap build now fails on the built-in `JavaProxyObject` (see dotnet/android#11622). Restore the method to `private` (so it is stripped from the reference assembly) and expose a small attribute-free `internal` entry point, `RegisterBuiltInNativeMembers (JniType)`, for the built-in registration path to call. Runtime behavior is unchanged. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Java.Interop/Java.Interop/JavaProxyObject.cs | 15 ++++++++++++++- .../Java.Interop/JniRuntime.JniTypeManager.cs | 5 +---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JavaProxyObject.cs b/src/Java.Interop/Java.Interop/JavaProxyObject.cs index 0050cf291..ba52ab85a 100644 --- a/src/Java.Interop/Java.Interop/JavaProxyObject.cs +++ b/src/Java.Interop/Java.Interop/JavaProxyObject.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -16,13 +17,25 @@ sealed class JavaProxyObject : JavaObject, IEquatable static readonly ConditionalWeakTable CachedValues = new ConditionalWeakTable (); [JniAddNativeMethodRegistrationAttribute] - internal static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args) + static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args) { args.Registrations.Add (new JniNativeMethodRegistration ("equals", "(Ljava/lang/Object;)Z", new EqualsMarshalMethod (Equals))); args.Registrations.Add (new JniNativeMethodRegistration ("hashCode", "()I", new GetHashCodeMarshalMethod (GetHashCode))); args.Registrations.Add (new JniNativeMethodRegistration ("toString", "()Ljava/lang/String;", new ToStringMarshalMethod (ToString))); } + // Reflection-free registration entry point for the built-in proxy type, used by + // JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (). Keeping the + // [JniAddNativeMethodRegistrationAttribute] method private ensures it is stripped + // from the reference assembly so the trimmable typemap scanner does not flag it. + internal static void RegisterBuiltInNativeMembers (JniType nativeClass) + { + var registrations = new List (); + RegisterNativeMembers (new JniNativeMethodRegistrationArguments (registrations, null)); + if (registrations.Count > 0) + nativeClass.RegisterNativeMethods (registrations.ToArray ()); + } + public override JniPeerMembers JniPeerMembers { get { return _members; diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs index 379b70847..0954d5c4d 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs @@ -259,10 +259,7 @@ protected static bool TryRegisterBuiltInNativeMembers ( ReadOnlySpan methods) { if (jniSimpleReference == JavaProxyObject.JniTypeName) { - var registrations = new List (); - JavaProxyObject.RegisterNativeMembers (new JniNativeMethodRegistrationArguments (registrations, null)); - if (registrations.Count > 0) - nativeClass.RegisterNativeMethods (registrations.ToArray ()); + JavaProxyObject.RegisterBuiltInNativeMembers (nativeClass); return true; } From 48b16f585385ab7c51601001da2d25032d46fec7 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 15 Jun 2026 19:07:15 +0200 Subject: [PATCH 2/9] [Java.Interop] Restore pure-reflection registration for JavaProxyObject #1441 added `JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers ()` -- a reflection-free direct call to `JavaProxyObject.RegisterNativeMembers` -- and bumped that method to `internal` so the call would compile. That method is dead code: nothing in Java.Interop (or dotnet/android) ever calls it, and the base `JniTypeManager.RegisterNativeMembers` is a no-op. `JavaProxyObject`'s native members are -- and were before #1441 -- registered purely via reflection in `ReflectionJniTypeManager` (`FindAndCallRegisterMethod` discovering the `[JniAddNativeMethodRegistrationAttribute]`-annotated method), which finds private methods just fine via `GetRuntimeMethods ()`. Remove the unused `TryRegisterBuiltInNativeMembers` and restore `RegisterNativeMembers` to `private` (its state before #1441). Making it private again also strips the attribute-bearing method from the reference assembly, which fixes dotnet/android's trimmable typemap scanner falsely rejecting the built-in `JavaProxyObject` with XA4251 (dotnet/android#11622). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Java.Interop/Java.Interop/JavaProxyObject.cs | 13 ------------- .../Java.Interop/JniRuntime.JniTypeManager.cs | 13 ------------- src/Java.Interop/PublicAPI.Unshipped.txt | 1 - 3 files changed, 27 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JavaProxyObject.cs b/src/Java.Interop/Java.Interop/JavaProxyObject.cs index ba52ab85a..909629b18 100644 --- a/src/Java.Interop/Java.Interop/JavaProxyObject.cs +++ b/src/Java.Interop/Java.Interop/JavaProxyObject.cs @@ -1,7 +1,6 @@ #nullable enable using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -24,18 +23,6 @@ static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args) args.Registrations.Add (new JniNativeMethodRegistration ("toString", "()Ljava/lang/String;", new ToStringMarshalMethod (ToString))); } - // Reflection-free registration entry point for the built-in proxy type, used by - // JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (). Keeping the - // [JniAddNativeMethodRegistrationAttribute] method private ensures it is stripped - // from the reference assembly so the trimmable typemap scanner does not flag it. - internal static void RegisterBuiltInNativeMembers (JniType nativeClass) - { - var registrations = new List (); - RegisterNativeMembers (new JniNativeMethodRegistrationArguments (registrations, null)); - if (registrations.Count > 0) - nativeClass.RegisterNativeMethods (registrations.ToArray ()); - } - public override JniPeerMembers JniPeerMembers { get { return _members; diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs index 0954d5c4d..ce7bcc563 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs @@ -253,19 +253,6 @@ static JniTypeSignature GetBuiltInTypeSignature (Type type) }; } - protected static bool TryRegisterBuiltInNativeMembers ( - JniType nativeClass, - string jniSimpleReference, - ReadOnlySpan methods) - { - if (jniSimpleReference == JavaProxyObject.JniTypeName) { - JavaProxyObject.RegisterBuiltInNativeMembers (nativeClass); - return true; - } - - return jniSimpleReference == JavaProxyThrowable.JniTypeName && methods.IsEmpty; - } - /// [return: DynamicallyAccessedMembers (Constructors)] public Type? GetInvokerType ( diff --git a/src/Java.Interop/PublicAPI.Unshipped.txt b/src/Java.Interop/PublicAPI.Unshipped.txt index 2051fd1e8..15c6329c0 100644 --- a/src/Java.Interop/PublicAPI.Unshipped.txt +++ b/src/Java.Interop/PublicAPI.Unshipped.txt @@ -8,7 +8,6 @@ virtual Java.Interop.JniRuntime.JniTypeManager.GetReflectionConstructibleTypes(J virtual Java.Interop.JniRuntime.JniTypeManager.GetTypeForSimpleReference(string! jniSimpleReference) -> System.Type? virtual Java.Interop.JniRuntime.JniTypeManager.GetTypeSignatureCore(System.Type! type) -> Java.Interop.JniTypeSignature virtual Java.Interop.JniRuntime.JniTypeManager.GetTypeSignaturesCore(System.Type! type) -> System.Collections.Generic.IEnumerable! -static Java.Interop.JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers(Java.Interop.JniType! nativeClass, string! jniSimpleReference, System.ReadOnlySpan methods) -> bool Java.Interop.JavaException.JavaException(ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions transfer, Java.Interop.JniObjectReference throwableOverride) -> void Java.Interop.JavaException.SetJavaStackTrace(Java.Interop.JniObjectReference peerReferenceOverride = default(Java.Interop.JniObjectReference)) -> void Java.Interop.JniRuntime.ReflectionJniTypeManager From b2a2a1599249c777489c6ea8243a65d897d90ad9 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 15 Jun 2026 20:46:44 +0200 Subject: [PATCH 3/9] Address review: restore TryRegisterBuiltInNativeMembers (not dead code) The previous commit deleted JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers claiming it was unused. That was wrong: it has two live callers in the NativeAOT samples (samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs and samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs), both JniTypeManager subclasses. Deleting it broke them (CS0103) and removed the only path that registers JavaProxyObject's equals/hashCode/toString natives for NativeAOT type managers (which don't use the reflection-based FindAndCallRegisterMethod path). The breakage was hidden in CI only because the NativeAOT sample steps use continueOnError: true. Keep the XA4251 fix (RegisterNativeMembers stays private, so the [JniAddNativeMethodRegistrationAttribute] is stripped from the reference assembly) and restore TryRegisterBuiltInNativeMembers by extracting the registration logic into a new attribute-free internal helper, JavaProxyObject.AddBuiltInRegistrations. Both the private attributed RegisterNativeMembers (reflection path) and TryRegisterBuiltInNativeMembers (NativeAOT path) call it. Restore the PublicAPI.Unshipped.txt entry as well. Verified: Java.Interop.dll builds clean; the reference assembly contains no method carrying [JniAddNativeMethodRegistration]; the sample's call to TryRegisterBuiltInNativeMembers resolves (no CS0103). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/Java.Interop/Java.Interop/JavaProxyObject.cs | 15 ++++++++++++--- .../Java.Interop/JniRuntime.JniTypeManager.cs | 16 ++++++++++++++++ src/Java.Interop/PublicAPI.Unshipped.txt | 1 + 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/Java.Interop/Java.Interop/JavaProxyObject.cs b/src/Java.Interop/Java.Interop/JavaProxyObject.cs index 909629b18..1ad94d985 100644 --- a/src/Java.Interop/Java.Interop/JavaProxyObject.cs +++ b/src/Java.Interop/Java.Interop/JavaProxyObject.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -18,9 +19,17 @@ sealed class JavaProxyObject : JavaObject, IEquatable [JniAddNativeMethodRegistrationAttribute] static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args) { - args.Registrations.Add (new JniNativeMethodRegistration ("equals", "(Ljava/lang/Object;)Z", new EqualsMarshalMethod (Equals))); - args.Registrations.Add (new JniNativeMethodRegistration ("hashCode", "()I", new GetHashCodeMarshalMethod (GetHashCode))); - args.Registrations.Add (new JniNativeMethodRegistration ("toString", "()Ljava/lang/String;", new ToStringMarshalMethod (ToString))); + AddBuiltInRegistrations (args.Registrations); + } + + // Attribute-free entry point so the built-in registrations can be reused by the + // reflection-free NativeAOT path (JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers) + // without exposing [JniAddNativeMethodRegistrationAttribute] in the reference assembly. + internal static void AddBuiltInRegistrations (ICollection registrations) + { + registrations.Add (new JniNativeMethodRegistration ("equals", "(Ljava/lang/Object;)Z", new EqualsMarshalMethod (Equals))); + registrations.Add (new JniNativeMethodRegistration ("hashCode", "()I", new GetHashCodeMarshalMethod (GetHashCode))); + registrations.Add (new JniNativeMethodRegistration ("toString", "()Ljava/lang/String;", new ToStringMarshalMethod (ToString))); } public override JniPeerMembers JniPeerMembers { diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs index ce7bcc563..78f7a0dc1 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs @@ -253,6 +253,22 @@ static JniTypeSignature GetBuiltInTypeSignature (Type type) }; } + protected static bool TryRegisterBuiltInNativeMembers ( + JniType nativeClass, + string jniSimpleReference, + ReadOnlySpan methods) + { + if (jniSimpleReference == JavaProxyObject.JniTypeName) { + var registrations = new List (); + JavaProxyObject.AddBuiltInRegistrations (registrations); + if (registrations.Count > 0) + nativeClass.RegisterNativeMethods (registrations.ToArray ()); + return true; + } + + return jniSimpleReference == JavaProxyThrowable.JniTypeName && methods.IsEmpty; + } + /// [return: DynamicallyAccessedMembers (Constructors)] public Type? GetInvokerType ( diff --git a/src/Java.Interop/PublicAPI.Unshipped.txt b/src/Java.Interop/PublicAPI.Unshipped.txt index 15c6329c0..2051fd1e8 100644 --- a/src/Java.Interop/PublicAPI.Unshipped.txt +++ b/src/Java.Interop/PublicAPI.Unshipped.txt @@ -8,6 +8,7 @@ virtual Java.Interop.JniRuntime.JniTypeManager.GetReflectionConstructibleTypes(J virtual Java.Interop.JniRuntime.JniTypeManager.GetTypeForSimpleReference(string! jniSimpleReference) -> System.Type? virtual Java.Interop.JniRuntime.JniTypeManager.GetTypeSignatureCore(System.Type! type) -> Java.Interop.JniTypeSignature virtual Java.Interop.JniRuntime.JniTypeManager.GetTypeSignaturesCore(System.Type! type) -> System.Collections.Generic.IEnumerable! +static Java.Interop.JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers(Java.Interop.JniType! nativeClass, string! jniSimpleReference, System.ReadOnlySpan methods) -> bool Java.Interop.JavaException.JavaException(ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions transfer, Java.Interop.JniObjectReference throwableOverride) -> void Java.Interop.JavaException.SetJavaStackTrace(Java.Interop.JniObjectReference peerReferenceOverride = default(Java.Interop.JniObjectReference)) -> void Java.Interop.JniRuntime.ReflectionJniTypeManager From 8580bd9d1a4ddee1928fb4c99de4de184408f092 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 15 Jun 2026 23:51:01 +0200 Subject: [PATCH 4/9] Keep built-in registration in the samples via ReflectionJniTypeManager Replaces the production TryRegisterBuiltInNativeMembers helper with a samples-only solution, restoring the pre-#1441 behavior for the NativeAOT type managers. Background: making JavaProxyObject.RegisterNativeMembers private (the XA4251 fix) is sufficient for the default Android runtime, which discovers and invokes it via reflection (FindAndCallRegisterMethod -> GetRuntimeMethods(), which returns private methods). The only thing that needed JavaProxyObject's registration via a non-reflection path was the two NativeAOT samples, which #1441 had rewritten to derive from the reflection-free JniRuntime.JniTypeManager base. That is why #1441 introduced TryRegisterBuiltInNativeMembers. Instead of carrying that helper in production, have the sample type managers derive from JniRuntime.ReflectionJniTypeManager again - exactly what the base JniRuntime.JniTypeManager provided before #1441. Built-in types such as JavaProxyObject/JavaProxyThrowable are then registered automatically via reflection, and the samples no longer need any custom registration code. Changes: - JavaProxyObject.RegisterNativeMembers: internal -> private. The marshalers stay (the default reflection runtime registers them by reflecting over this type); only the visibility changes, which strips the attribute from the reference assembly and fixes the dotnet/android XA4251 scanner failure. - Delete JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers (+ its PublicAPI.Unshipped.txt entry); no longer needed. - Hello-NativeAOTFromJNI / Hello-NativeAOTFromAndroid: derive from JniRuntime.ReflectionJniTypeManager and drop the reflection-free overrides and hand-written JavaProxyObject registration. ReflectionJniTypeManager is [RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor suppresses IL2026/IL3050 with [UnconditionalSuppressMessage]. A #pragma is insufficient here: it silences the Roslyn analyzer but not the ILLink/ILC publish passes, which only honor the attribute (verified with a trim-publish of the real type manager against Java.Interop.dll). Net: production loses code (1-line visibility change + deletions) and the samples shrink substantially. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../NativeAotTypeManager.cs | 132 ++------------- .../NativeAotTypeManager.cs | 157 ++++-------------- .../Java.Interop/JavaProxyObject.cs | 15 +- .../Java.Interop/JniRuntime.JniTypeManager.cs | 16 -- src/Java.Interop/PublicAPI.Unshipped.txt | 1 - 5 files changed, 49 insertions(+), 272 deletions(-) diff --git a/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs index caa5187a7..6b6de785e 100644 --- a/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs @@ -4,11 +4,14 @@ namespace Java.Interop.Samples.NativeAotFromAndroid; -partial class NativeAotTypeManager : JniRuntime.JniTypeManager { - - internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; - internal const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; - internal const DynamicallyAccessedMemberTypes MethodsConstructors = MethodsAndPrivateNested | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; +// See the comment in Hello-NativeAOTFromJNI/NativeAotTypeManager.cs: this derives from the +// reflection-based JniRuntime.ReflectionJniTypeManager (the pre-dotnet/java-interop#1441 base +// behavior) so built-in runtime types such as JavaProxyObject get their native members registered +// automatically. ReflectionJniTypeManager is annotated [RequiresDynamicCode]/[RequiresUnreferencedCode]; +// the reflection paths this sample exercises do not require runtime code generation, so the +// constructor suppresses the resulting trimming/AOT warnings via [UnconditionalSuppressMessage] +// (a #pragma would not survive the ILLink/ILC publish passes). +partial class NativeAotTypeManager : JniRuntime.ReflectionJniTypeManager { Dictionary typeMappings = new () { ["android/app/Activity"] = typeof (Android.App.Activity), @@ -20,75 +23,24 @@ partial class NativeAotTypeManager : JniRuntime.JniTypeManager { ["my/MainActivity"] = typeof (MainActivity), }; - public override void RegisterNativeMembers ( - JniType nativeClass, - [DynamicallyAccessedMembers (MethodsAndPrivateNested)] - Type type, - ReadOnlySpan methods) + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Reflection-based registration used by this NativeAOT sample does not require unreferenced code; see class comment.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Reflection-based registration used by this NativeAOT sample does not require runtime code generation; see class comment.")] + public NativeAotTypeManager () { - if (TryRegisterBuiltInNativeMembers (nativeClass, nativeClass.Name, methods)) - return; - if (!methods.IsEmpty) - throw new NotSupportedException ($"Could not register native members for type '{type.FullName}'."); - } - - [Obsolete ("Use RegisterNativeMembers(JniType, Type, ReadOnlySpan)")] - public override void RegisterNativeMembers ( - JniType nativeClass, - [DynamicallyAccessedMembers (MethodsAndPrivateNested)] - Type type, - string? methods) - { - RegisterNativeMembers (nativeClass, type, methods.AsSpan ()); } protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { - var target = GetTypeForSimpleReference (jniSimpleReference); - if (target != null) + if (typeMappings.TryGetValue (jniSimpleReference, out var target)) yield return target; - } - - protected override string? GetSimpleReference (Type type) - { - return GetSimpleReferences (type).FirstOrDefault (); - } - - [return: DynamicallyAccessedMembers (MethodsConstructors)] - protected override Type? GetTypeForSimpleReference (string jniSimpleReference) - { - return jniSimpleReference switch { - "V" => typeof (void), - "Z" => typeof (bool), - "java/lang/Boolean" => typeof (bool?), - "B" => typeof (sbyte), - "java/lang/Byte" => typeof (sbyte?), - "C" => typeof (char), - "java/lang/Character" => typeof (char?), - "S" => typeof (short), - "java/lang/Short" => typeof (short?), - "I" => typeof (int), - "java/lang/Integer" => typeof (int?), - "J" => typeof (long), - "java/lang/Long" => typeof (long?), - "F" => typeof (float), - "java/lang/Float" => typeof (float?), - "D" => typeof (double), - "java/lang/Double" => typeof (double?), - "android/app/Activity" => typeof (Android.App.Activity), - "android/content/Context" => typeof (Android.Content.Context), - "android/content/ContextWrapper" => typeof (Android.Content.ContextWrapper), - "android/os/BaseBundle" => typeof (Android.OS.BaseBundle), - "android/os/Bundle" => typeof (Android.OS.Bundle), - "android/view/ContextThemeWrapper" => typeof (Android.View.ContextThemeWrapper), - "my/MainActivity" => typeof (MainActivity), - _ => null, - }; + foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference)) + yield return t; } protected override IEnumerable GetSimpleReferences (Type type) { - return CreateSimpleReferencesEnumerator (type); + return base.GetSimpleReferences (type) + .Concat (CreateSimpleReferencesEnumerator (type)); } IEnumerable CreateSimpleReferencesEnumerator (Type type) @@ -100,56 +52,4 @@ IEnumerable CreateSimpleReferencesEnumerator (Type type) yield return e.Key; } } - - public override IEnumerable GetTypes (JniTypeSignature typeSignature) - { - if (!typeSignature.IsValid || typeSignature.ArrayRank != 0 || typeSignature.SimpleReference == null) - return []; - return GetTypesForSimpleReference (typeSignature.SimpleReference); - } - - public override IEnumerable GetReflectionConstructibleTypes (JniTypeSignature typeSignature) - { - if (!typeSignature.IsValid || typeSignature.ArrayRank != 0 || typeSignature.SimpleReference == null) - yield break; - var target = GetTypeForSimpleReference (typeSignature.SimpleReference); - if (target != null) - yield return new JniRuntime.JniTypeManager.ReflectionConstructibleType (target); - } - - protected override JniTypeSignature GetTypeSignatureCore (Type type) - { - var simpleReference = GetSimpleReferences (type).FirstOrDefault (); - return simpleReference == null ? default : new JniTypeSignature (simpleReference, 0, false); - } - - protected override IEnumerable GetTypeSignaturesCore (Type type) - { - var signature = GetTypeSignatureCore (type); - if (signature.IsValid) - yield return signature; - } - - [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - protected override Type? GetInvokerTypeCore ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - Type type) - { - return null; - } - - protected override IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimpleReference) - { - return null; - } - - protected override string? GetReplacementTypeCore (string jniSimpleReference) - { - return null; - } - - protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) - { - return null; - } } diff --git a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs index e1a3150b3..42baa1fa4 100644 --- a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs @@ -1,148 +1,51 @@ -using Java.Interop; using System.Diagnostics.CodeAnalysis; +using Java.Interop; + namespace Hello_NativeAOTFromJNI; -class NativeAotTypeManager : JniRuntime.JniTypeManager { - internal const DynamicallyAccessedMemberTypes Methods = DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods; - internal const DynamicallyAccessedMemberTypes MethodsAndPrivateNested = Methods | DynamicallyAccessedMemberTypes.NonPublicNestedTypes; - internal const DynamicallyAccessedMemberTypes MethodsConstructors = MethodsAndPrivateNested | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; +// This NativeAOT sample derives from the reflection-based JniRuntime.ReflectionJniTypeManager, +// which is the behavior the base JniRuntime.JniTypeManager provided before dotnet/java-interop#1441. +// Deriving from it means built-in runtime types such as JavaProxyObject/JavaProxyThrowable get +// their native members registered automatically (via reflection over their +// [JniAddNativeMethodRegistration] methods), so the sample does not need to register them by hand. +// ReflectionJniTypeManager is annotated [RequiresDynamicCode]/[RequiresUnreferencedCode]; the +// reflection paths actually exercised by this sample do not require runtime code generation, so the +// constructor suppresses the resulting trimming/AOT warnings (a #pragma would not survive the +// ILLink/ILC publish passes, so an [UnconditionalSuppressMessage] is required). +class NativeAotTypeManager : JniRuntime.ReflectionJniTypeManager { - protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) - { - var target = GetTypeForSimpleReference (jniSimpleReference); - if (target != null) - yield return target; - } + Dictionary typeMappings = new () { + [Example.ManagedType.JniTypeName] = typeof (Example.ManagedType), + }; - [return: DynamicallyAccessedMembers (MethodsConstructors)] - protected override Type? GetTypeForSimpleReference (string jniSimpleReference) + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Reflection-based registration used by this NativeAOT sample does not require unreferenced code; see class comment.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Reflection-based registration used by this NativeAOT sample does not require runtime code generation; see class comment.")] + public NativeAotTypeManager () { - return jniSimpleReference switch { - "V" => typeof (void), - "Z" => typeof (bool), - "java/lang/Boolean" => typeof (bool?), - "B" => typeof (sbyte), - "java/lang/Byte" => typeof (sbyte?), - "C" => typeof (char), - "java/lang/Character" => typeof (char?), - "S" => typeof (short), - "java/lang/Short" => typeof (short?), - "I" => typeof (int), - "java/lang/Integer" => typeof (int?), - "J" => typeof (long), - "java/lang/Long" => typeof (long?), - "F" => typeof (float), - "java/lang/Float" => typeof (float?), - "D" => typeof (double), - "java/lang/Double" => typeof (double?), - Example.ManagedType.JniTypeName => typeof (Example.ManagedType), - "java/lang/Object" => typeof (Java.Lang.Object), - "java/lang/String" => typeof (Java.Lang.String), - _ => null, - }; } - public override IEnumerable GetTypes (JniTypeSignature typeSignature) - { - if (!typeSignature.IsValid || typeSignature.ArrayRank != 0 || typeSignature.SimpleReference == null) - return []; - return GetTypesForSimpleReference (typeSignature.SimpleReference); - } - - public override IEnumerable GetReflectionConstructibleTypes (JniTypeSignature typeSignature) + protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { - if (!typeSignature.IsValid || typeSignature.ArrayRank != 0 || typeSignature.SimpleReference == null) - yield break; - var target = GetTypeForSimpleReference (typeSignature.SimpleReference); - if (target != null) - yield return new JniRuntime.JniTypeManager.ReflectionConstructibleType (target); + if (typeMappings.TryGetValue (jniSimpleReference, out var target)) + yield return target; + foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference)) + yield return t; } protected override IEnumerable GetSimpleReferences (Type type) { - return CreateSimpleReferencesEnumerator (type); + return base.GetSimpleReferences (type) + .Concat (CreateSimpleReferencesEnumerator (type)); } IEnumerable CreateSimpleReferencesEnumerator (Type type) { - if (type == typeof (Example.ManagedType)) - yield return Example.ManagedType.JniTypeName; - else if (type == typeof (Java.Lang.Object)) - yield return "java/lang/Object"; - else if (type == typeof (Java.Lang.String)) - yield return "java/lang/String"; - } - - protected override string? GetSimpleReference (Type type) - { - return GetSimpleReferences (type).FirstOrDefault (); - } - - protected override JniTypeSignature GetTypeSignatureCore (Type type) - { - var simpleReference = GetSimpleReference (type); - return simpleReference == null ? default : new JniTypeSignature (simpleReference, 0, false); - } - - protected override IEnumerable GetTypeSignaturesCore (Type type) - { - var signature = GetTypeSignatureCore (type); - if (signature.IsValid) - yield return signature; - } - - [return: DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - protected override Type? GetInvokerTypeCore ( - [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] - Type type) - { - return null; - } - - protected override IReadOnlyList? GetStaticMethodFallbackTypesCore (string jniSimpleReference) - { - return null; - } - - protected override string? GetReplacementTypeCore (string jniSimpleReference) - { - return null; - } - - protected override JniRuntime.ReplacementMethodInfo? GetReplacementMethodInfoCore (string jniSourceType, string jniMethodName, string jniMethodSignature) - { - return null; - } - - public override void RegisterNativeMembers ( - JniType nativeClass, - [DynamicallyAccessedMembers (MethodsAndPrivateNested)] - Type type, - ReadOnlySpan methods) - { - if (TryRegisterBuiltInNativeMembers (nativeClass, nativeClass.Name, methods)) - return; - - if (type != typeof (Example.ManagedType)) { - if (!methods.IsEmpty) - throw new NotSupportedException ($"Could not register native members for type '{type.FullName}'."); - return; + if (typeMappings == null) + yield break; + foreach (var e in typeMappings) { + if (e.Value == type) + yield return e.Key; } - - var registrations = new List (); - Example.ManagedType.RegisterNativeMembers (new JniNativeMethodRegistrationArguments (registrations, null)); - if (registrations.Count > 0) - nativeClass.RegisterNativeMethods (registrations.ToArray ()); - } - - [Obsolete ("Use RegisterNativeMembers(JniType, Type, ReadOnlySpan)")] - public override void RegisterNativeMembers ( - JniType nativeClass, - [DynamicallyAccessedMembers (MethodsAndPrivateNested)] - Type type, - string? methods) - { - RegisterNativeMembers (nativeClass, type, methods.AsSpan ()); } } diff --git a/src/Java.Interop/Java.Interop/JavaProxyObject.cs b/src/Java.Interop/Java.Interop/JavaProxyObject.cs index 1ad94d985..909629b18 100644 --- a/src/Java.Interop/Java.Interop/JavaProxyObject.cs +++ b/src/Java.Interop/Java.Interop/JavaProxyObject.cs @@ -1,7 +1,6 @@ #nullable enable using System; -using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -19,17 +18,9 @@ sealed class JavaProxyObject : JavaObject, IEquatable [JniAddNativeMethodRegistrationAttribute] static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args) { - AddBuiltInRegistrations (args.Registrations); - } - - // Attribute-free entry point so the built-in registrations can be reused by the - // reflection-free NativeAOT path (JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers) - // without exposing [JniAddNativeMethodRegistrationAttribute] in the reference assembly. - internal static void AddBuiltInRegistrations (ICollection registrations) - { - registrations.Add (new JniNativeMethodRegistration ("equals", "(Ljava/lang/Object;)Z", new EqualsMarshalMethod (Equals))); - registrations.Add (new JniNativeMethodRegistration ("hashCode", "()I", new GetHashCodeMarshalMethod (GetHashCode))); - registrations.Add (new JniNativeMethodRegistration ("toString", "()Ljava/lang/String;", new ToStringMarshalMethod (ToString))); + args.Registrations.Add (new JniNativeMethodRegistration ("equals", "(Ljava/lang/Object;)Z", new EqualsMarshalMethod (Equals))); + args.Registrations.Add (new JniNativeMethodRegistration ("hashCode", "()I", new GetHashCodeMarshalMethod (GetHashCode))); + args.Registrations.Add (new JniNativeMethodRegistration ("toString", "()Ljava/lang/String;", new ToStringMarshalMethod (ToString))); } public override JniPeerMembers JniPeerMembers { diff --git a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs index 78f7a0dc1..ce7bcc563 100644 --- a/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs +++ b/src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs @@ -253,22 +253,6 @@ static JniTypeSignature GetBuiltInTypeSignature (Type type) }; } - protected static bool TryRegisterBuiltInNativeMembers ( - JniType nativeClass, - string jniSimpleReference, - ReadOnlySpan methods) - { - if (jniSimpleReference == JavaProxyObject.JniTypeName) { - var registrations = new List (); - JavaProxyObject.AddBuiltInRegistrations (registrations); - if (registrations.Count > 0) - nativeClass.RegisterNativeMethods (registrations.ToArray ()); - return true; - } - - return jniSimpleReference == JavaProxyThrowable.JniTypeName && methods.IsEmpty; - } - /// [return: DynamicallyAccessedMembers (Constructors)] public Type? GetInvokerType ( diff --git a/src/Java.Interop/PublicAPI.Unshipped.txt b/src/Java.Interop/PublicAPI.Unshipped.txt index 2051fd1e8..15c6329c0 100644 --- a/src/Java.Interop/PublicAPI.Unshipped.txt +++ b/src/Java.Interop/PublicAPI.Unshipped.txt @@ -8,7 +8,6 @@ virtual Java.Interop.JniRuntime.JniTypeManager.GetReflectionConstructibleTypes(J virtual Java.Interop.JniRuntime.JniTypeManager.GetTypeForSimpleReference(string! jniSimpleReference) -> System.Type? virtual Java.Interop.JniRuntime.JniTypeManager.GetTypeSignatureCore(System.Type! type) -> Java.Interop.JniTypeSignature virtual Java.Interop.JniRuntime.JniTypeManager.GetTypeSignaturesCore(System.Type! type) -> System.Collections.Generic.IEnumerable! -static Java.Interop.JniRuntime.JniTypeManager.TryRegisterBuiltInNativeMembers(Java.Interop.JniType! nativeClass, string! jniSimpleReference, System.ReadOnlySpan methods) -> bool Java.Interop.JavaException.JavaException(ref Java.Interop.JniObjectReference reference, Java.Interop.JniObjectReferenceOptions transfer, Java.Interop.JniObjectReference throwableOverride) -> void Java.Interop.JavaException.SetJavaStackTrace(Java.Interop.JniObjectReference peerReferenceOverride = default(Java.Interop.JniObjectReference)) -> void Java.Interop.JniRuntime.ReflectionJniTypeManager From 9178c6f1a849143a3030799df2057af191bb9346 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Mon, 15 Jun 2026 23:56:06 +0200 Subject: [PATCH 5/9] Drop verbose explanatory comments from the NativeAOT sample type managers The [UnconditionalSuppressMessage] Justification strings already convey the necessary context. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../NativeAotTypeManager.cs | 11 ++--------- .../Hello-NativeAOTFromJNI/NativeAotTypeManager.cs | 13 ++----------- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs index 6b6de785e..131925579 100644 --- a/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs @@ -4,13 +4,6 @@ namespace Java.Interop.Samples.NativeAotFromAndroid; -// See the comment in Hello-NativeAOTFromJNI/NativeAotTypeManager.cs: this derives from the -// reflection-based JniRuntime.ReflectionJniTypeManager (the pre-dotnet/java-interop#1441 base -// behavior) so built-in runtime types such as JavaProxyObject get their native members registered -// automatically. ReflectionJniTypeManager is annotated [RequiresDynamicCode]/[RequiresUnreferencedCode]; -// the reflection paths this sample exercises do not require runtime code generation, so the -// constructor suppresses the resulting trimming/AOT warnings via [UnconditionalSuppressMessage] -// (a #pragma would not survive the ILLink/ILC publish passes). partial class NativeAotTypeManager : JniRuntime.ReflectionJniTypeManager { Dictionary typeMappings = new () { @@ -23,8 +16,8 @@ partial class NativeAotTypeManager : JniRuntime.ReflectionJniTypeManager { ["my/MainActivity"] = typeof (MainActivity), }; - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Reflection-based registration used by this NativeAOT sample does not require unreferenced code; see class comment.")] - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Reflection-based registration used by this NativeAOT sample does not require runtime code generation; see class comment.")] + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Reflection-based registration used by this NativeAOT sample does not require unreferenced code.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Reflection-based registration used by this NativeAOT sample does not require runtime code generation.")] public NativeAotTypeManager () { } diff --git a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs index 42baa1fa4..a8309f500 100644 --- a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs @@ -4,23 +4,14 @@ namespace Hello_NativeAOTFromJNI; -// This NativeAOT sample derives from the reflection-based JniRuntime.ReflectionJniTypeManager, -// which is the behavior the base JniRuntime.JniTypeManager provided before dotnet/java-interop#1441. -// Deriving from it means built-in runtime types such as JavaProxyObject/JavaProxyThrowable get -// their native members registered automatically (via reflection over their -// [JniAddNativeMethodRegistration] methods), so the sample does not need to register them by hand. -// ReflectionJniTypeManager is annotated [RequiresDynamicCode]/[RequiresUnreferencedCode]; the -// reflection paths actually exercised by this sample do not require runtime code generation, so the -// constructor suppresses the resulting trimming/AOT warnings (a #pragma would not survive the -// ILLink/ILC publish passes, so an [UnconditionalSuppressMessage] is required). class NativeAotTypeManager : JniRuntime.ReflectionJniTypeManager { Dictionary typeMappings = new () { [Example.ManagedType.JniTypeName] = typeof (Example.ManagedType), }; - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Reflection-based registration used by this NativeAOT sample does not require unreferenced code; see class comment.")] - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Reflection-based registration used by this NativeAOT sample does not require runtime code generation; see class comment.")] + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Reflection-based registration used by this NativeAOT sample does not require unreferenced code.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Reflection-based registration used by this NativeAOT sample does not require runtime code generation.")] public NativeAotTypeManager () { } From a8f3f2b4cf396dd58d55e794bd65ac114816148c Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 16 Jun 2026 07:10:48 +0200 Subject: [PATCH 6/9] Fix NativeAOT sample type resolution: override GetTypeForSimpleReference Build 1465402's "run Hello-NativeAOTFromJNI" step failed at runtime: System.NotSupportedException: Could not find System.Type corresponding to Java type JniTypeSignature(TypeName=example/ManagedType ...) at Java.Interop.ManagedPeer.RegisterNativeMembers(...) at example.ManagedType.(ManagedType.java:15) Root cause: after #1441, JniRuntime.JniTypeManager.GetType() dispatches through GetTypeForSimpleReference (singular), not GetTypesForSimpleReference (plural). The previous sample rewrite only overrode the plural method, so app types like example/ManagedType were never resolved (the base ReflectionJniTypeManager's GetTypeForSimpleReference only knows built-in types), and ManagedPeer registration threw before it could register the type. Fix: override GetTypeForSimpleReference (singular) in both NativeAOT sample type managers to resolve the sample's own managed types, falling back to the base for built-ins. Registration and the reverse Type->JNI mapping continue to be handled by the reflection base (the pre-#1441 behavior). The override carries the same [return: DynamicallyAccessedMembers(...)] as the base to satisfy IL2093. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../NativeAotTypeManager.cs | 15 +++++++ .../NativeAotTypeManager.cs | 39 +++++++++---------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs index 131925579..830f04658 100644 --- a/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs @@ -6,6 +6,11 @@ namespace Java.Interop.Samples.NativeAotFromAndroid; partial class NativeAotTypeManager : JniRuntime.ReflectionJniTypeManager { + const DynamicallyAccessedMemberTypes MethodsConstructors = + DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | + DynamicallyAccessedMemberTypes.NonPublicNestedTypes | + DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; + Dictionary typeMappings = new () { ["android/app/Activity"] = typeof (Android.App.Activity), ["android/content/Context"] = typeof (Android.Content.Context), @@ -22,6 +27,16 @@ public NativeAotTypeManager () { } + // GetType() dispatches through GetTypeForSimpleReference (singular), so the sample's own type + // map has to be applied here; the base ReflectionJniTypeManager only knows the built-in types. + [return: DynamicallyAccessedMembers (MethodsConstructors)] + protected override Type? GetTypeForSimpleReference (string jniSimpleReference) + { + if (typeMappings.TryGetValue (jniSimpleReference, out var target)) + return target; + return base.GetTypeForSimpleReference (jniSimpleReference); + } + protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { if (typeMappings.TryGetValue (jniSimpleReference, out var target)) diff --git a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs index a8309f500..6eca6ec7a 100644 --- a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs @@ -6,9 +6,10 @@ namespace Hello_NativeAOTFromJNI; class NativeAotTypeManager : JniRuntime.ReflectionJniTypeManager { - Dictionary typeMappings = new () { - [Example.ManagedType.JniTypeName] = typeof (Example.ManagedType), - }; + const DynamicallyAccessedMemberTypes MethodsConstructors = + DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods | + DynamicallyAccessedMemberTypes.NonPublicNestedTypes | + DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Reflection-based registration used by this NativeAOT sample does not require unreferenced code.")] [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Reflection-based registration used by this NativeAOT sample does not require runtime code generation.")] @@ -16,27 +17,23 @@ public NativeAotTypeManager () { } - protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) + // The base ReflectionJniTypeManager resolves built-in types (primitives, java/lang/String, + // JavaProxyObject, ...) and handles registration and the reverse Type->JNI mapping (via the + // [JniTypeSignature] attribute) for us. We only need to teach it about this sample's own + // managed types. + [return: DynamicallyAccessedMembers (MethodsConstructors)] + protected override Type? GetTypeForSimpleReference (string jniSimpleReference) { - if (typeMappings.TryGetValue (jniSimpleReference, out var target)) - yield return target; - foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference)) - yield return t; + if (jniSimpleReference == Example.ManagedType.JniTypeName) + return typeof (Example.ManagedType); + return base.GetTypeForSimpleReference (jniSimpleReference); } - protected override IEnumerable GetSimpleReferences (Type type) - { - return base.GetSimpleReferences (type) - .Concat (CreateSimpleReferencesEnumerator (type)); - } - - IEnumerable CreateSimpleReferencesEnumerator (Type type) + protected override IEnumerable GetTypesForSimpleReference (string jniSimpleReference) { - if (typeMappings == null) - yield break; - foreach (var e in typeMappings) { - if (e.Value == type) - yield return e.Key; - } + if (jniSimpleReference == Example.ManagedType.JniTypeName) + yield return typeof (Example.ManagedType); + foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference)) + yield return t; } } From 1fcc5ec21e39d6a790511dad28affec8c35ceb4a Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 16 Jun 2026 07:21:54 +0200 Subject: [PATCH 7/9] Address review: drop unreachable null guard in FromAndroid sample `typeMappings` is assigned in its field initializer and never set to null, so the `if (typeMappings == null) yield break;` guard in CreateSimpleReferencesEnumerator was unreachable dead code. Remove it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs index 830f04658..1f4f8178a 100644 --- a/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs @@ -53,8 +53,6 @@ protected override IEnumerable GetSimpleReferences (Type type) IEnumerable CreateSimpleReferencesEnumerator (Type type) { - if (typeMappings == null) - yield break; foreach (var e in typeMappings) { if (e.Value == type) yield return e.Key; From 4de18b3460d64f53b0fadf8934bde7ef4d17df92 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 16 Jun 2026 08:55:03 +0200 Subject: [PATCH 8/9] Document why the NativeAOT sample trim/AOT suppressions are acceptable Add a class-level comment to both NativeAOT sample type managers explaining the rationale for the [UnconditionalSuppressMessage] IL2026/IL3050 suppressions: - These are *samples*, not product code. .NET for Android (what we ship) does not pair ReflectionJniTypeManager with NativeAOT, so it isn't worth the effort to make these samples fully trim/AOT-clean right now. - The reflection paths were always trim/AOT-unsafe. Before #1441 the equivalent suppressions lived inside JniTypeManager itself (justified "NotUsedInAndroid"); #1441 simply moved that responsibility to callers via [RequiresDynamicCode]/[RequiresUnreferencedCode]. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs | 10 ++++++++++ samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs index 1f4f8178a..6f00e215c 100644 --- a/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs @@ -4,6 +4,16 @@ namespace Java.Interop.Samples.NativeAotFromAndroid; +// This sample derives from the reflection-based JniRuntime.ReflectionJniTypeManager, which is +// annotated [RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor below suppresses +// the resulting IL2026/IL3050 trim/AOT warnings. +// +// Suppressing here is intentional and good enough: these NativeAOT projects are *samples*, not +// product code. .NET for Android (what we actually ship) does not pair ReflectionJniTypeManager +// with NativeAOT, so it isn't worth the effort to make these samples fully trim/AOT-clean right now. +// The reflection paths were always trim/AOT-unsafe: before dotnet/java-interop#1441 the equivalent +// suppressions lived (buried) inside JniTypeManager itself, justified "NotUsedInAndroid"; #1441 just +// moved that responsibility to callers via [RequiresDynamicCode]/[RequiresUnreferencedCode]. partial class NativeAotTypeManager : JniRuntime.ReflectionJniTypeManager { const DynamicallyAccessedMemberTypes MethodsConstructors = diff --git a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs index 6eca6ec7a..f5d6bacdb 100644 --- a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs @@ -4,6 +4,16 @@ namespace Hello_NativeAOTFromJNI; +// This sample derives from the reflection-based JniRuntime.ReflectionJniTypeManager, which is +// annotated [RequiresDynamicCode]/[RequiresUnreferencedCode], so the constructor below suppresses +// the resulting IL2026/IL3050 trim/AOT warnings. +// +// Suppressing here is intentional and good enough: these NativeAOT projects are *samples*, not +// product code. .NET for Android (what we actually ship) does not pair ReflectionJniTypeManager +// with NativeAOT, so it isn't worth the effort to make these samples fully trim/AOT-clean right now. +// The reflection paths were always trim/AOT-unsafe: before dotnet/java-interop#1441 the equivalent +// suppressions lived (buried) inside JniTypeManager itself, justified "NotUsedInAndroid"; #1441 just +// moved that responsibility to callers via [RequiresDynamicCode]/[RequiresUnreferencedCode]. class NativeAotTypeManager : JniRuntime.ReflectionJniTypeManager { const DynamicallyAccessedMemberTypes MethodsConstructors = From 40d44cbf1aa43ec4b04c67307f7ac5ee9d21a8d3 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Tue, 16 Jun 2026 09:01:11 +0200 Subject: [PATCH 9/9] Reword NativeAOT sample suppression justifications to describe why it's safe The previous IL2026/IL3050 justifications ("does not require unreferenced code / runtime code generation") read as the opposite of reality, since ReflectionJniTypeManager is exactly [RequiresUnreferencedCode]/[RequiresDynamicCode]. Reword to describe why the suppression is correct for this sample: - IL2026: the assembly is rooted via TrimmerRootAssembly and the reflected registration members are preserved by the [DynamicallyAccessedMembers] annotations on the RegisterNativeMembers(Type) -> FindAndCallRegisterMethod path. - IL3050: registration uses CreateDelegate on compile-time-known static methods (no MakeGenericType / expression compilation), so no runtime codegen is required. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs | 4 ++-- samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs index 6f00e215c..42010c449 100644 --- a/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs @@ -31,8 +31,8 @@ partial class NativeAotTypeManager : JniRuntime.ReflectionJniTypeManager { ["my/MainActivity"] = typeof (MainActivity), }; - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Reflection-based registration used by this NativeAOT sample does not require unreferenced code.")] - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Reflection-based registration used by this NativeAOT sample does not require runtime code generation.")] + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Sample only (see class comment): this assembly is rooted via TrimmerRootAssembly and the members reflected over during registration are preserved by the [DynamicallyAccessedMembers] annotations on the RegisterNativeMembers(Type) -> FindAndCallRegisterMethod path, so trimming does not remove what reflection needs.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Sample only (see class comment): built-in member registration calls CreateDelegate on compile-time-known static methods (no MakeGenericType / expression compilation), so no runtime code generation is required.")] public NativeAotTypeManager () { } diff --git a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs index f5d6bacdb..3dbea075f 100644 --- a/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs +++ b/samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs @@ -21,8 +21,8 @@ class NativeAotTypeManager : JniRuntime.ReflectionJniTypeManager { DynamicallyAccessedMemberTypes.NonPublicNestedTypes | DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors; - [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Reflection-based registration used by this NativeAOT sample does not require unreferenced code.")] - [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Reflection-based registration used by this NativeAOT sample does not require runtime code generation.")] + [UnconditionalSuppressMessage ("Trimming", "IL2026", Justification = "Sample only (see class comment): this assembly is rooted via TrimmerRootAssembly and the members reflected over during registration are preserved by the [DynamicallyAccessedMembers] annotations on the RegisterNativeMembers(Type) -> FindAndCallRegisterMethod path, so trimming does not remove what reflection needs.")] + [UnconditionalSuppressMessage ("AOT", "IL3050", Justification = "Sample only (see class comment): built-in member registration calls CreateDelegate on compile-time-known static methods (no MakeGenericType / expression compilation), so no runtime code generation is required.")] public NativeAotTypeManager () { }