diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index f16e61eb2cc..b1a5df81766 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -324,12 +324,14 @@ internal RegisterInfo ParseJniTypeSignatureAttribute (CustomAttribute ca) } var isArrayType = TryGetNamedArgument (value, "ArrayRank", out var rank) && rank > 0; + TryGetNamedArgument (value, "InvokerType", out var invokerTypeName); return new RegisterInfo { JniName = jniName.Replace ('.', '/'), DoNotGenerateAcw = doNotGenerateAcw, IsFromJniTypeSignature = true, IsArrayType = isArrayType, + InvokerTypeName = invokerTypeName, }; } @@ -669,6 +671,7 @@ sealed record RegisterInfo public bool DoNotGenerateAcw { get; init; } public bool IsFromJniTypeSignature { get; init; } public bool IsArrayType { get; init; } + public string? InvokerTypeName { get; init; } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index a9b28686132..e246b49443c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -372,7 +372,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A // Keep ActivationCtor scoped to the target/base hierarchy for legacy parity, // and store the invoker ctor style separately for CreateInstance emission. if (invokerTypeName is not null) { - invokerActivationCtorStyle = TryResolveActivationCtorOnInvoker (invokerTypeName)?.Style; + invokerActivationCtorStyle = TryResolveActivationCtorOnInvoker (invokerTypeName, index)?.Style; } var peer = new JavaPeerInfo { @@ -2082,19 +2082,20 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI string? TryFindInvokerTypeName (string typeName, TypeDefinitionHandle typeHandle, AssemblyIndex index) { + if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo)) { + var explicitInvokerTypeName = registerInfo.InvokerTypeName; + if (explicitInvokerTypeName is { Length: > 0 }) { + return TryGetSameAssemblyTypeName (explicitInvokerTypeName, index.AssemblyName); + } + } + // First, check the [Register] attribute's connector arg (3rd arg). // In real Mono.Android, interfaces have [Register("jni/name", "", "InvokerTypeName, Assembly")] // where the connector contains the assembly-qualified invoker type name. - if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo) && registerInfo.Connector is not null) { + if (registerInfo is not null && registerInfo.Connector is not null) { var connector = registerInfo.Connector; - // The connector may be "TypeName" or "TypeName, Assembly, Version=..., Culture=..., PublicKeyToken=..." - // We want just the type name (before the first comma, if any) - var commaIndex = connector.IndexOf (','); - if (commaIndex > 0) { - return NormalizeConnectorManagedTypeName (connector.Substring (0, commaIndex)); - } if (connector.Length > 0) { - return NormalizeConnectorManagedTypeName (connector); + return TryGetSameAssemblyTypeName (connector, index.AssemblyName); } } @@ -2106,27 +2107,36 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI return null; } + static string? TryGetSameAssemblyTypeName (string value, string defaultAssemblyName) + { + var commaIndex = value.IndexOf (','); + if (commaIndex < 0) { + return NormalizeConnectorManagedTypeName (value); + } + + var typeName = NormalizeConnectorManagedTypeName (value.Substring (0, commaIndex)); + var remainder = value.Substring (commaIndex + 1).Trim (); + var nextCommaIndex = remainder.IndexOf (','); + var assemblyName = nextCommaIndex < 0 ? remainder : remainder.Substring (0, nextCommaIndex).Trim (); + return string.Equals (assemblyName, defaultAssemblyName, StringComparison.Ordinal) ? typeName : null; + } + static string NormalizeConnectorManagedTypeName (string managedTypeName) { return managedTypeName.Trim ().Replace ('/', '+'); } /// - /// Resolve the activation ctor on a known invoker type (search all loaded assemblies). + /// Resolve the activation ctor on a known invoker type in its target assembly. /// Used for interface peers, whose own type definition has no constructors. - /// The assemblyCache typically contains 10–30 entries (app + framework assemblies), - /// and each lookup is an O(1) dictionary probe, so the linear scan is cheap. /// - ActivationCtorInfo? TryResolveActivationCtorOnInvoker (string invokerTypeName) + ActivationCtorInfo? TryResolveActivationCtorOnInvoker (string invokerTypeName, AssemblyIndex index) { - foreach (var assembly in assemblyCache.Values) { - if (!assembly.TypesByFullName.TryGetValue (invokerTypeName, out var invokerHandle)) { - continue; - } - var invokerDef = assembly.Reader.GetTypeDefinition (invokerHandle); - return ResolveActivationCtor (invokerTypeName, invokerDef, assembly); + if (!index.TypesByFullName.TryGetValue (invokerTypeName, out var invokerHandle)) { + return null; } - return null; + var invokerDef = index.Reader.GetTypeDefinition (invokerHandle); + return ResolveActivationCtor (invokerTypeName, invokerDef, index); } public void Dispose () diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index f887629cab6..6a5e6698ef4 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -195,6 +195,11 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) return null; } + if (targetType is { IsInterface: true } || targetType is { IsAbstract: true }) { + return TryGetProxyFromTargetType (handle, targetType) ?? + TryGetProxyFromHierarchy (handle, targetType); + } + return TryGetProxyFromHierarchy (handle, targetType) ?? TryGetProxyFromTargetType (handle, targetType); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index f8c9192146b..8422081cfbd 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -92,6 +92,27 @@ public void Scan_InvokerAndInterface_ShareJavaName () Assert.Contains (clickListenerPeers, p => p.DoNotGenerateAcw); } + [Theory] + [InlineData ("MyApp.IExplicitJavaInteropCollection", "MyApp.ExplicitJavaInteropCollectionProxy")] + [InlineData ("MyApp.IInheritedJavaInteropList", "MyApp.InheritedJavaInteropListProxy")] + [InlineData ("MyApp.AbstractJavaInteropList", "MyApp.AbstractJavaInteropListProxy")] + public void Scan_JniTypeSignatureExplicitInvoker_UsesJavaInteropConstructor (string targetType, string invokerType) + { + var peer = FindFixtureByManagedName (targetType); + + Assert.Equal (invokerType, peer.InvokerTypeName); + Assert.Equal (ActivationCtorStyle.JavaInterop, peer.InvokerActivationCtorStyle); + } + + [Fact] + public void Scan_JniTypeSignatureCrossAssemblyInvoker_IsIgnored () + { + var peer = FindFixtureByManagedName ("MyApp.IUnsupportedExternalInvoker"); + + Assert.Null (peer.InvokerTypeName); + Assert.Null (peer.InvokerActivationCtorStyle); + } + [Fact] public void Scan_AllTypes_HaveAssemblyName () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/InvokerFixtures.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/InvokerFixtures.cs new file mode 100644 index 00000000000..c53169f3396 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/InvokerFixtures.cs @@ -0,0 +1,51 @@ +using Java.Interop; + +namespace MyApp; + +[JniTypeSignature ("java/util/Collection", GenerateJavaPeer = false, InvokerType = typeof (ExplicitJavaInteropCollectionProxy))] +public interface IExplicitJavaInteropCollection +{ +} + +public sealed class ExplicitJavaInteropCollectionProxy : Java.Lang.Object +{ + public ExplicitJavaInteropCollectionProxy (ref JniObjectReference reference, JniObjectReferenceOptions options) + : base ((System.IntPtr) 0, Android.Runtime.JniHandleOwnership.DoNotTransfer) + { + } +} + +[JniTypeSignature ("java/util/List", GenerateJavaPeer = false, InvokerType = typeof (InheritedJavaInteropListProxy))] +public interface IInheritedJavaInteropList : IExplicitJavaInteropCollection +{ +} + +public sealed class InheritedJavaInteropListProxy : Java.Lang.Object +{ + public InheritedJavaInteropListProxy (ref JniObjectReference reference, JniObjectReferenceOptions options) + : base ((System.IntPtr) 0, Android.Runtime.JniHandleOwnership.DoNotTransfer) + { + } +} + +[JniTypeSignature ("java/util/AbstractList", GenerateJavaPeer = false, InvokerType = typeof (AbstractJavaInteropListProxy))] +public abstract class AbstractJavaInteropList : Java.Lang.Object +{ + protected AbstractJavaInteropList (ref JniObjectReference reference, JniObjectReferenceOptions options) + : base ((System.IntPtr) 0, Android.Runtime.JniHandleOwnership.DoNotTransfer) + { + } +} + +public sealed class AbstractJavaInteropListProxy : AbstractJavaInteropList +{ + public AbstractJavaInteropListProxy (ref JniObjectReference reference, JniObjectReferenceOptions options) + : base (ref reference, options) + { + } +} + +[JniTypeSignature ("java/lang/UnsupportedExternalInvoker", GenerateJavaPeer = false, InvokerType = typeof (string))] +public interface IUnsupportedExternalInvoker +{ +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 050741c3e20..1263b2b823d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -224,13 +224,14 @@ public sealed class JniConstructorSignatureAttribute : Attribute namespace Java.Interop { - [AttributeUsage (AttributeTargets.Class, AllowMultiple = false)] + [AttributeUsage (AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false)] public sealed class JniTypeSignatureAttribute : Attribute { public string SimpleReference { get; } public bool GenerateJavaPeer { get; set; } = true; public bool IsKeyword { get; set; } public int ArrayRank { get; set; } + public Type? InvokerType { get; set; } public JniTypeSignatureAttribute (string simpleReference) => SimpleReference = simpleReference; } diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InvokerActivationTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InvokerActivationTests.cs new file mode 100644 index 00000000000..61176c221cf --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InvokerActivationTests.cs @@ -0,0 +1,189 @@ +using System; + +using Android.Runtime; + +using Java.Interop; + +using NUnit.Framework; + +namespace Java.InteropTests +{ + [TestFixture] + [Category ("InvokerActivation")] + public class InvokerActivationTests + { + [Test] + public void XamarinAndroidInterfaceInvoker_ActivatesOnceAndPreservesIdentity () + { + IXamarinListInvoker.ConstructorInvocations = 0; + + var handle = CreateArrayListHandle (); + try { + var first = Java.Lang.Object.GetObject (handle, JniHandleOwnership.DoNotTransfer); + var second = Java.Lang.Object.GetObject (handle, JniHandleOwnership.DoNotTransfer); + + Assert.IsNotNull (first); + Assert.AreSame (first, second); + Assert.AreEqual (typeof (IXamarinListInvoker), first.GetType ()); + Assert.AreEqual (1, IXamarinListInvoker.ConstructorInvocations); + first.Dispose (); + } finally { + JNIEnv.DeleteGlobalRef (handle); + } + } + + [Test] + public void JavaInteropInterfaceInvoker_ActivatesOnceAndPreservesIdentity () + { + JavaInteropCollectionInvoker.ConstructorInvocations = 0; + + var handle = CreateArrayListHandle (); + try { + var first = GetValue (handle); + var second = GetValue (handle); + + Assert.IsNotNull (first); + Assert.AreSame (first, second); + Assert.AreEqual (typeof (JavaInteropCollectionInvoker), first.GetType ()); + Assert.AreEqual (1, JavaInteropCollectionInvoker.ConstructorInvocations); + first.Dispose (); + } finally { + JNIEnv.DeleteGlobalRef (handle); + } + } + + [Test] + public void InheritedJavaInteropInterface_UsesExplicitInvoker () + { + InheritedJavaInteropListInvoker.ConstructorInvocations = 0; + + var handle = CreateArrayListHandle (); + try { + var peer = GetValue (handle); + + Assert.IsNotNull (peer); + Assert.IsInstanceOf (peer); + Assert.AreEqual (typeof (InheritedJavaInteropListInvoker), peer.GetType ()); + Assert.AreEqual (1, InheritedJavaInteropListInvoker.ConstructorInvocations); + peer.Dispose (); + } finally { + JNIEnv.DeleteGlobalRef (handle); + } + } + + [Test] + public void AbstractJavaInteropType_UsesExplicitInvoker () + { + JavaInteropAbstractListInvoker.ConstructorInvocations = 0; + + var handle = CreateArrayListHandle (); + try { + var peer = GetValue (handle); + + Assert.IsNotNull (peer); + Assert.AreEqual (typeof (JavaInteropAbstractListInvoker), peer.GetType ()); + Assert.AreEqual (1, JavaInteropAbstractListInvoker.ConstructorInvocations); + peer.Dispose (); + } finally { + JNIEnv.DeleteGlobalRef (handle); + } + } + + static IntPtr CreateArrayListHandle () + { + using var list = new Java.Util.ArrayList (); + return JNIEnv.NewGlobalRef (list.Handle); + } + + static T GetValue (IntPtr handle) + { + var reference = new JniObjectReference (handle, JniObjectReferenceType.Global); + return JniEnvironment.Runtime.ValueManager.GetValue (ref reference, JniObjectReferenceOptions.Copy); + } + } + + [Register ("java/util/List", "", "Java.InteropTests.IXamarinListInvoker")] + interface IXamarinList : IJavaObject, IJavaPeerable, IDisposable + { + } + + [Register ("java/util/List", DoNotGenerateAcw = true)] + sealed class IXamarinListInvoker : Java.Lang.Object, IXamarinList + { + public static int ConstructorInvocations; + + public IXamarinListInvoker (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + ConstructorInvocations++; + } + } + + [JniTypeSignature ("java/util/Collection", GenerateJavaPeer = false, InvokerType = typeof (JavaInteropCollectionInvoker))] + interface IJavaInteropCollection : IJavaPeerable + { + } + + [JniTypeSignature ("java/util/Collection", GenerateJavaPeer = false)] + sealed class JavaInteropCollectionInvoker : global::Java.Interop.JavaObject, IJavaInteropCollection + { + static readonly JniPeerMembers members = new JniPeerMembers ("java/util/Collection", typeof (JavaInteropCollectionInvoker)); + + public static int ConstructorInvocations; + + public override JniPeerMembers JniPeerMembers => members; + + public JavaInteropCollectionInvoker (ref JniObjectReference reference, JniObjectReferenceOptions options) + : base (ref reference, options) + { + ConstructorInvocations++; + } + } + + [JniTypeSignature ("java/util/List", GenerateJavaPeer = false, InvokerType = typeof (InheritedJavaInteropListInvoker))] + interface IInheritedJavaInteropList : IJavaInteropCollection + { + } + + [JniTypeSignature ("java/util/List", GenerateJavaPeer = false)] + sealed class InheritedJavaInteropListInvoker : global::Java.Interop.JavaObject, IInheritedJavaInteropList + { + static readonly JniPeerMembers members = new JniPeerMembers ("java/util/List", typeof (InheritedJavaInteropListInvoker)); + + public static int ConstructorInvocations; + + public override JniPeerMembers JniPeerMembers => members; + + public InheritedJavaInteropListInvoker (ref JniObjectReference reference, JniObjectReferenceOptions options) + : base (ref reference, options) + { + ConstructorInvocations++; + } + } + + [JniTypeSignature ("java/util/AbstractList", GenerateJavaPeer = false, InvokerType = typeof (JavaInteropAbstractListInvoker))] + abstract class JavaInteropAbstractList : global::Java.Interop.JavaObject + { + static readonly JniPeerMembers members = new JniPeerMembers ("java/util/AbstractList", typeof (JavaInteropAbstractList)); + + public override JniPeerMembers JniPeerMembers => members; + + protected JavaInteropAbstractList (ref JniObjectReference reference, JniObjectReferenceOptions options) + : base (ref reference, options) + { + } + } + + [JniTypeSignature ("java/util/AbstractList", GenerateJavaPeer = false)] + sealed class JavaInteropAbstractListInvoker : JavaInteropAbstractList + { + public static int ConstructorInvocations; + + public JavaInteropAbstractListInvoker (ref JniObjectReference reference, JniObjectReferenceOptions options) + : base (ref reference, options) + { + ConstructorInvocations++; + } + } + +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj index 6405287d880..399be0d8401 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj @@ -138,6 +138,7 @@ +