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 managedReferences = new ArrayList(); + + public CallVirtualFromConstructorBase (int value) { + if (CallVirtualFromConstructorBase.class == getClass ()) { + nctor_0 (value); + } + calledFromConstructor (value); + } + + private native void nctor_0 (int value); + + public void calledFromConstructor (int value) { + } + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } + + static void registerNatives () + { + try { + Class runtime = Class.forName ("mono.android.Runtime"); + java.lang.reflect.Method registerNatives = runtime.getMethod ("registerNatives", Class.class); + registerNatives.invoke (null, CallVirtualFromConstructorBase.class); + } catch (Exception e) { + throw new Error (e); + } + } +} diff --git a/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallVirtualFromConstructorDerived.java b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallVirtualFromConstructorDerived.java new file mode 100644 index 00000000000..419eb3c1edb --- /dev/null +++ b/tests/Mono.Android-Tests/Java.Interop-Tests/java-trimmable/net/dot/jni/test/CallVirtualFromConstructorDerived.java @@ -0,0 +1,59 @@ +package net.dot.jni.test; + +import java.util.ArrayList; + +import net.dot.jni.GCUserPeerable; + +public class CallVirtualFromConstructorDerived + extends CallVirtualFromConstructorBase + implements GCUserPeerable +{ + static { + registerNatives (); + } + + ArrayList managedReferences = new ArrayList(); + boolean calledFromConstructorInvoked; + + public CallVirtualFromConstructorDerived (int value) { + super (value); + if (CallVirtualFromConstructorDerived.class == getClass () && !calledFromConstructorInvoked) { + nctor_0 (value); + } + } + + public static CallVirtualFromConstructorDerived newInstance (int value) + { + return new CallVirtualFromConstructorDerived (value); + } + + public void calledFromConstructor (int value) { + calledFromConstructorInvoked = true; + n_CalledFromConstructor (value); + } + + public native void n_CalledFromConstructor (int value); + + private native void nctor_0 (int value); + + public void jiAddManagedReference (java.lang.Object obj) + { + managedReferences.add (obj); + } + + public void jiClearManagedReferences () + { + managedReferences.clear (); + } + + static void registerNatives () + { + try { + Class runtime = Class.forName ("mono.android.Runtime"); + java.lang.reflect.Method registerNatives = runtime.getMethod ("registerNatives", Class.class); + registerNatives.invoke (null, CallVirtualFromConstructorDerived.class); + } catch (Exception e) { + throw new Error (e); + } + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs index f79553258c3..d3783cdee0b 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Xamarin.Android.RuntimeTests/NUnitInstrumentation.cs @@ -31,9 +31,6 @@ protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) // trimmable typemap. These cannot use [Category("TrimmableIgnore")] because // we don't control that assembly — they must be excluded by name here. ExcludedTestNames = new [] { - // net.dot.jni.test.CallVirtualFromConstructorDerived Java class not in APK - "Java.InteropTests.InvokeVirtualFromConstructorTests", - // JNI method remapping not supported in trimmable typemap "Java.InteropTests.JniPeerMembersTests.ReplaceInstanceMethodName", "Java.InteropTests.JniPeerMembersTests.ReplaceInstanceMethodWithStaticMethod",