diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
index 1baa3d23e92..fa7b684c472 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs
@@ -121,8 +121,9 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,
if (!isAliasGroup) {
// Single peer — no aliases needed, emit directly with the base JNI name
var peer = peersForName [0];
- bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null;
bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0;
+ isAcw |= peer.DoNotGenerateAcw && peer.HasJniAddNativeMethodRegistrationAttribute && !peer.IsInterface && peer.MarshalMethods.Count > 0;
+ bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null || isAcw;
JavaPeerProxyData? proxy = null;
if (hasProxy) {
@@ -156,8 +157,9 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName,
string entryJniName = $"{jniName}[{i}]";
aliasKeys.Add (entryJniName);
- bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null;
bool isAcw = !peer.DoNotGenerateAcw && !peer.IsInterface && peer.MarshalMethods.Count > 0;
+ isAcw |= peer.DoNotGenerateAcw && peer.HasJniAddNativeMethodRegistrationAttribute && !peer.IsInterface && peer.MarshalMethods.Count > 0;
+ bool hasProxy = peer.ActivationCtor != null || peer.InvokerTypeName != null || isAcw;
JavaPeerProxyData? proxy = null;
if (hasProxy) {
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
index ee285b42798..ceb6979c8f7 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs
@@ -65,6 +65,12 @@ public sealed record JavaPeerInfo
///
public bool DoNotGenerateAcw { get; init; }
+ ///
+ /// True when the managed type uses JniAddNativeMethodRegistrationAttribute
+ /// to provide native methods for a hand-written Java peer.
+ ///
+ public bool HasJniAddNativeMethodRegistrationAttribute { get; init; }
+
///
/// True when the type was discovered via [JniTypeSignatureAttribute]
/// rather than [RegisterAttribute]. Used to resolve cross-assembly
diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
index dda7460271c..2f03eab3b11 100644
--- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
+++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs
@@ -213,6 +213,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
var isInterface = (typeDef.Attributes & TypeAttributes.Interface) != 0;
var isAbstract = (typeDef.Attributes & TypeAttributes.Abstract) != 0;
var isGenericDefinition = typeDef.GetGenericParameters ().Count > 0;
+ var hasJniAddNativeMethodRegistrationAttribute = HasJniAddNativeMethodRegistrationAttribute (typeDef, index);
var isUnconditional = attrInfo is not null;
var cannotRegisterInStaticConstructor = attrInfo is ApplicationAttributeInfo or InstrumentationAttributeInfo;
@@ -258,6 +259,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
IsInterface = isInterface,
IsAbstract = isAbstract,
DoNotGenerateAcw = doNotGenerateAcw,
+ HasJniAddNativeMethodRegistrationAttribute = hasJniAddNativeMethodRegistrationAttribute,
IsFromJniTypeSignature = registerInfo?.IsFromJniTypeSignature ?? false,
IsUnconditional = isUnconditional,
CannotRegisterInStaticConstructor = cannotRegisterInStaticConstructor,
@@ -338,6 +340,20 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A
return (methods, fields);
}
+ static bool HasJniAddNativeMethodRegistrationAttribute (TypeDefinition typeDef, AssemblyIndex index)
+ {
+ foreach (var methodHandle in typeDef.GetMethods ()) {
+ var methodDef = index.Reader.GetMethodDefinition (methodHandle);
+ foreach (var attrHandle in methodDef.GetCustomAttributes ()) {
+ var attr = index.Reader.GetCustomAttribute (attrHandle);
+ if (AssemblyIndex.GetCustomAttributeName (attr, index.Reader) == "JniAddNativeMethodRegistrationAttribute") {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
///
/// For each virtual override method on that wasn't already
/// collected (no direct [Register]), walks up the base type hierarchy to find a
diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj
index 11426b511b1..10ccf40ddf7 100644
--- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj
+++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.NET.csproj
@@ -25,6 +25,8 @@
+
+
diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets
index b1fa7783883..220917d0b41 100644
--- a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets
+++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-Tests.targets
@@ -14,8 +14,8 @@
java-trimmable/net/dot/jni/test/GetThis.java is added explicitly via
TestJarEntry below when building for the trimmable typemap; it must be
removed from the implicit AndroidJavaSource glob so the .NET SDK does
- not also try to compile it as a regular AndroidJavaSource (which would
- collide with the desktop GetThis.java in the test JAR).
+ not also try to compile these files as regular AndroidJavaSource items
+ (which would collide with the desktop Java files in the test JAR).
-->
@@ -38,12 +38,16 @@
MSBuild's up-to-date check doesn't track the conditional swap automatically.
-->
+
+
-
\ No newline at end of file
+
diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-trimmable/CallVirtualFromConstructorDerived.cs b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-trimmable/CallVirtualFromConstructorDerived.cs
new file mode 100644
index 00000000000..0a7816b112b
--- /dev/null
+++ b/tests/Mono.Android-Tests/Java.Interop-Tests/Java.Interop-trimmable/CallVirtualFromConstructorDerived.cs
@@ -0,0 +1,121 @@
+#nullable enable
+
+using System;
+using System.Runtime.CompilerServices;
+
+using Java.Interop;
+
+namespace Java.InteropTests
+{
+ [JniTypeSignature (CallVirtualFromConstructorDerived.JniTypeName, GenerateJavaPeer=false)]
+ public class CallVirtualFromConstructorDerived : CallVirtualFromConstructorBase {
+ new internal const string JniTypeName = "net/dot/jni/test/CallVirtualFromConstructorDerived";
+ static readonly JniPeerMembers _members = new JniPeerMembers (JniTypeName, typeof (CallVirtualFromConstructorDerived));
+
+ [JniAddNativeMethodRegistrationAttribute]
+ static void RegisterNativeMembers (JniNativeMethodRegistrationArguments args)
+ {
+ args.Registrations.Add (new JniNativeMethodRegistration ("calledFromConstructor", "(I)V", (CalledFromConstructorMarshalMethod)CalledFromConstructorHandler));
+ }
+
+ public override JniPeerMembers JniPeerMembers {
+ get {return _members;}
+ }
+
+ int calledValue;
+
+ public bool InvokedConstructor;
+
+ [Register (".ctor", "(I)V", "")]
+ public CallVirtualFromConstructorDerived (int value)
+ : this (value, useNewObject: false)
+ {
+ }
+
+ public CallVirtualFromConstructorDerived (int value, bool useNewObject)
+ : base (value, useNewObject)
+ {
+ InvokedConstructor = true;
+
+ if (useNewObject && calledValue != 0) {
+ // calledValue was set on a *different* instance! So it's 0 here.
+ throw new ArgumentException (
+ string.Format ("value '{0}' doesn't match expected value '{1}'.", value, 0),
+ "value");
+ }
+ if (!useNewObject && value != calledValue)
+ throw new ArgumentException (
+ string.Format ("value '{0}' doesn't match expected value '{1}'.", value, calledValue),
+ "value");
+ }
+
+ public bool InvokedActivationConstructor;
+
+ public static CallVirtualFromConstructorDerived? Intermediate_FromCalledFromConstructor;
+ public static CallVirtualFromConstructorDerived? Intermediate_FromActivationConstructor;
+
+ public CallVirtualFromConstructorDerived (ref JniObjectReference reference, JniObjectReferenceOptions options)
+ : base (ref reference, options)
+ {
+ InvokedActivationConstructor = true;
+
+ Intermediate_FromActivationConstructor = this;
+ }
+
+ public bool Called;
+
+ [Register ("calledFromConstructor", "(I)V", "")]
+ public override void CalledFromConstructor (int value)
+ {
+ Called = true;
+ calledValue = value;
+
+ Intermediate_FromCalledFromConstructor = this;
+ }
+
+ public static unsafe CallVirtualFromConstructorDerived NewInstance (int value)
+ {
+ JniArgumentValue* args = stackalloc JniArgumentValue [1];
+ args [0] = new JniArgumentValue (value);
+ var o = _members.StaticMethods.InvokeObjectMethod ("newInstance.(I)Lnet/dot/jni/test/CallVirtualFromConstructorDerived;", args);
+ var result = JniEnvironment.Runtime.ValueManager.GetValue (ref o, JniObjectReferenceOptions.CopyAndDispose);
+ if (result == null)
+ throw new InvalidOperationException ("newInstance returned null.");
+ return result;
+ }
+
+ delegate void CalledFromConstructorMarshalMethod (IntPtr jnienv, IntPtr n_self, int value);
+ static void CalledFromConstructorHandler (IntPtr jnienv, IntPtr n_self, int value)
+ {
+ n_CalledFromConstructor (jnienv, n_self, value);
+ }
+
+ static void n_CalledFromConstructor (IntPtr jnienv, IntPtr n_self, int value)
+ {
+ var envp = new JniTransition (jnienv);
+ try {
+ var r_self = new JniObjectReference (n_self);
+ var self = JniEnvironment.Runtime.ValueManager.GetValue(ref r_self, JniObjectReferenceOptions.Copy);
+ if (self == null)
+ throw new InvalidOperationException ("calledFromConstructor received null self.");
+ self.CalledFromConstructor (value);
+ self.InvokedConstructor = true;
+ ((IJavaPeerable) self).SetJniManagedPeerState (self.JniManagedPeerState | JniManagedPeerStates.Replaceable);
+ self.DisposeUnlessReferenced ();
+ }
+ catch (Exception e) when (JniEnvironment.Runtime.ExceptionShouldTransitionToJni (e)) {
+ envp.SetPendingException (e);
+ }
+ finally {
+ envp.Dispose ();
+ }
+ }
+ }
+
+ [AttributeUsage (AttributeTargets.Constructor | AttributeTargets.Method)]
+ sealed class RegisterAttribute : Attribute {
+ public RegisterAttribute (string name, string signature, string connector)
+ {
+ }
+ }
+}
diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallVirtualFromConstructorBase.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallVirtualFromConstructorBase.java
new file mode 100644
index 00000000000..05d2d24e284
--- /dev/null
+++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallVirtualFromConstructorBase.java
@@ -0,0 +1,47 @@
+package net.dot.jni.test;
+
+import java.util.ArrayList;
+
+import net.dot.jni.GCUserPeerable;
+
+public class CallVirtualFromConstructorBase implements GCUserPeerable {
+
+ static {
+ registerNatives ();
+ }
+
+ ArrayList