Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 31 additions & 115 deletions samples/Hello-NativeAOTFromAndroid/NativeAotTypeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,22 @@

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;
// 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 =
DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;

Dictionary<string, Type> typeMappings = new () {
["android/app/Activity"] = typeof (Android.App.Activity),
Expand All @@ -20,136 +31,41 @@ partial class NativeAotTypeManager : JniRuntime.JniTypeManager {
["my/MainActivity"] = typeof (MainActivity),
};

public override void RegisterNativeMembers (
JniType nativeClass,
[DynamicallyAccessedMembers (MethodsAndPrivateNested)]
Type type,
ReadOnlySpan<char> methods)
[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 ()
{
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<char>)")]
public override void RegisterNativeMembers (
JniType nativeClass,
[DynamicallyAccessedMembers (MethodsAndPrivateNested)]
Type type,
string? methods)
// 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)
{
RegisterNativeMembers (nativeClass, type, methods.AsSpan ());
if (typeMappings.TryGetValue (jniSimpleReference, out var target))
return target;
return base.GetTypeForSimpleReference (jniSimpleReference);
}

protected override IEnumerable<Type> 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<string> GetSimpleReferences (Type type)
{
return CreateSimpleReferencesEnumerator (type);
return base.GetSimpleReferences (type)
Comment thread
simonrozsival marked this conversation as resolved.
.Concat (CreateSimpleReferencesEnumerator (type));
}

IEnumerable<string> CreateSimpleReferencesEnumerator (Type type)
{
if (typeMappings == null)
yield break;
foreach (var e in typeMappings) {
if (e.Value == type)
yield return e.Key;
}
}

public override IEnumerable<Type> GetTypes (JniTypeSignature typeSignature)
{
if (!typeSignature.IsValid || typeSignature.ArrayRank != 0 || typeSignature.SimpleReference == null)
return [];
return GetTypesForSimpleReference (typeSignature.SimpleReference);
}

public override IEnumerable<JniRuntime.JniTypeManager.ReflectionConstructibleType> 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<JniTypeSignature> 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<string>? 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;
}
}
173 changes: 37 additions & 136 deletions samples/Hello-NativeAOTFromJNI/NativeAotTypeManager.cs
Original file line number Diff line number Diff line change
@@ -1,148 +1,49 @@
using Java.Interop;
using System.Diagnostics.CodeAnalysis;

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;
using Java.Interop;

protected override IEnumerable<Type> GetTypesForSimpleReference (string jniSimpleReference)
{
var target = GetTypeForSimpleReference (jniSimpleReference);
if (target != null)
yield return target;
}
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 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 ⚠️ Trimmer/AOT — This PublishAot=true sample now derives from ReflectionJniTypeManager, which is explicitly [RequiresDynamicCode]/[RequiresUnreferencedCode] ("not compatible with Native AOT"). It works here only because the assembly is rooted via TrimmerRootAssembly and the [DynamicallyAccessedMembers] annotations on the RegisterNativeMembers(Type)FindAndCallRegisterMethod path preserve the reflected members — but it inverts what these samples exist to demonstrate (a reflection-free AOT type manager) by depending on a base the runtime declares AOT-incompatible.

The safety net is also thin: Hello-NativeAOTFromAndroid isn't built or run in CI at all, and Hello-NativeAOTFromJNI's publish + RunJavaSample steps are continueOnError: true (build-tools/automation/templates/core-tests.yaml:83,90), so an AOT regression in either sample won't fail the build. Please confirm the FromAndroid sample was actually published/run (not just "structurally identical"), and consider keeping at least one sample on the reflection-free path so the AOT pattern stays demonstrated.

Rule: Consider trimmer/NativeAOT impact

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — and this is a deliberate trade-off, now documented in a class-level comment (4de18b34).

You're right that deriving from ReflectionJniTypeManager inverts the "reflection-free AOT type manager" pattern the samples originally demonstrated. The reason we're accepting that here: these are samples, not product. .NET for Android — what we actually ship — does not pair ReflectionJniTypeManager with NativeAOT; our NativeAOT path uses the trimmable typemap (reflection-free registration), which is exactly what making JavaProxyObject.RegisterNativeMembers private unblocks more of. Re-implementing a hand-written reflection-free registration in the java-interop samples (what #1441 originally did) is real work with no product benefit, so for now the suppression is the pragmatic choice. This also isn't a new AOT hole — pre-#1441 the equivalent suppressions lived inside JniTypeManager itself (justified NotUsedInAndroid).

On the CI safety net — you're correct, and I won't overclaim:

  • Hello-NativeAOTFromAndroid is not built or run in CI (not in Java.Interop.sln; needs an Android device). I did not separately publish/run it — it's the same code shape as FromJNI, but I'm not asserting execution coverage for it.
  • Hello-NativeAOTFromJNI is exercised: CI's RunJavaSample publishes + runs it on the JVM, and that step is what caught the real type-resolution bug I then fixed in a8f3f2b4. The latest green build (run Hello-NativeAOTFromJNI succeeded) confirms the published native binary actually runs.

If keeping one sample on the reflection-free path is something you'd like, I'm happy to do it as a follow-up — but I'd rather not expand this PR (its job is to unblock the dotnet/android bump).


const DynamicallyAccessedMemberTypes MethodsConstructors =
DynamicallyAccessedMemberTypes.PublicMethods | DynamicallyAccessedMemberTypes.NonPublicMethods |
DynamicallyAccessedMemberTypes.NonPublicNestedTypes |
DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors;

[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 ()
{
}

// 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)
{
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<Type> GetTypes (JniTypeSignature typeSignature)
{
if (!typeSignature.IsValid || typeSignature.ArrayRank != 0 || typeSignature.SimpleReference == null)
return [];
return GetTypesForSimpleReference (typeSignature.SimpleReference);
}

public override IEnumerable<JniRuntime.JniTypeManager.ReflectionConstructibleType> 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 IEnumerable<string> GetSimpleReferences (Type type)
{
return CreateSimpleReferencesEnumerator (type);
}

IEnumerable<string> 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 ();
if (jniSimpleReference == Example.ManagedType.JniTypeName)
return typeof (Example.ManagedType);
return base.GetTypeForSimpleReference (jniSimpleReference);
}

protected override JniTypeSignature GetTypeSignatureCore (Type type)
{
var simpleReference = GetSimpleReference (type);
return simpleReference == null ? default : new JniTypeSignature (simpleReference, 0, false);
}

protected override IEnumerable<JniTypeSignature> 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<string>? 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<char> 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;
}

var registrations = new List<JniNativeMethodRegistration> ();
Example.ManagedType.RegisterNativeMembers (new JniNativeMethodRegistrationArguments (registrations, null));
if (registrations.Count > 0)
nativeClass.RegisterNativeMethods (registrations.ToArray ());
}

[Obsolete ("Use RegisterNativeMembers(JniType, Type, ReadOnlySpan<char>)")]
public override void RegisterNativeMembers (
JniType nativeClass,
[DynamicallyAccessedMembers (MethodsAndPrivateNested)]
Type type,
string? methods)
protected override IEnumerable<Type> GetTypesForSimpleReference (string jniSimpleReference)
{
RegisterNativeMembers (nativeClass, type, methods.AsSpan ());
if (jniSimpleReference == Example.ManagedType.JniTypeName)
yield return typeof (Example.ManagedType);
foreach (var t in base.GetTypesForSimpleReference (jniSimpleReference))
yield return t;
}
}
2 changes: 1 addition & 1 deletion src/Java.Interop/Java.Interop/JavaProxyObject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ sealed class JavaProxyObject : JavaObject, IEquatable<JavaProxyObject>
static readonly ConditionalWeakTable<object, JavaProxyObject> CachedValues = new ConditionalWeakTable<object, JavaProxyObject> ();

[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)));
Expand Down
16 changes: 0 additions & 16 deletions src/Java.Interop/Java.Interop/JniRuntime.JniTypeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,22 +253,6 @@ static JniTypeSignature GetBuiltInTypeSignature (Type type)
};
}

protected static bool TryRegisterBuiltInNativeMembers (
JniType nativeClass,
string jniSimpleReference,
ReadOnlySpan<char> methods)
{
if (jniSimpleReference == JavaProxyObject.JniTypeName) {
var registrations = new List<JniNativeMethodRegistration> ();
JavaProxyObject.RegisterNativeMembers (new JniNativeMethodRegistrationArguments (registrations, null));
if (registrations.Count > 0)
nativeClass.RegisterNativeMethods (registrations.ToArray ());
return true;
}

return jniSimpleReference == JavaProxyThrowable.JniTypeName && methods.IsEmpty;
}

/// <include file="../Documentation/Java.Interop/JniRuntime.JniTypeManager.xml" path="/docs/member[@name='M:GetInvokerType']/*" />
[return: DynamicallyAccessedMembers (Constructors)]
public Type? GetInvokerType (
Expand Down
Loading