From 48a582ffd0ed38f656cdd1eb2fdb6af76be1fb26 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 28 Aug 2026 12:34:49 +0200 Subject: [PATCH 1/3] [typemap] Support explicit cross-assembly invokers Track Java.Interop InvokerType metadata and its owning assembly through scanner, model, associations, and emitted proxy references. Add constructor activation regression coverage for interface, inherited interface, abstract, identity, and cross-assembly invoker shapes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 7 +- .../Scanner/AssemblyIndex.cs | 3 + .../Scanner/JavaPeerInfo.cs | 5 + .../Scanner/JavaPeerScanner.cs | 52 +++-- .../ExternalInvokerFixtures.csproj | 10 + .../ExternalRandomAccessInvoker.cs | 6 + .../Generator/FixtureTestBase.cs | 17 +- .../TypeMapAssemblyGeneratorTests.cs | 18 ++ .../Generator/TypeMapModelBuilderTests.cs | 30 +++ ....Android.Sdk.TrimmableTypeMap.Tests.csproj | 2 + .../Scanner/JavaPeerScannerTests.cs | 23 ++ .../TestFixtures/InvokerFixtures.cs | 52 +++++ .../TestFixtures/StubAttributes.cs | 3 +- .../TestFixtures/TestFixtures.csproj | 4 + .../InvokerActivationContracts.cs | 13 ++ .../Java.Interop/InvokerActivationTests.cs | 216 ++++++++++++++++++ .../Mono.Android.NET-Tests.csproj | 1 + 17 files changed, 443 insertions(+), 19 deletions(-) create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalInvokerFixtures.csproj create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalRandomAccessInvoker.cs create mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/InvokerFixtures.cs create mode 100644 tests/Mono.Android-Tests/Mono.Android-Test.Library/InvokerActivationContracts.cs create mode 100644 tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InvokerActivationTests.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index ca25d1962d7..2d4ecff5019 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -97,6 +97,7 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri if (proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType) { AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName); } + AddIfCrossAssembly (referencedAssemblies, proxy.InvokerType?.AssemblyName, assemblyName); } // Always include Mono.Android — the emitter calls internal JNIEnv.DeleteRef @@ -188,7 +189,7 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, AliasProxyTypeReference = holderRef, }); if (proxy != null && peer.InvokerTypeName != null) { - AddProxyAssociation (model, peer.InvokerTypeName, peer.AssemblyName, proxy, assemblyName); + AddProxyAssociation (model, peer.InvokerTypeName, peer.InvokerAssemblyName ?? peer.AssemblyName, proxy, assemblyName); } } @@ -212,7 +213,7 @@ static void AddProxyAssociation (TypeMapAssemblyData model, JavaPeerInfo peer, J { AddProxyAssociation (model, peer.ManagedTypeName, peer.AssemblyName, proxy, assemblyName); if (peer.InvokerTypeName != null) { - AddProxyAssociation (model, peer.InvokerTypeName, peer.AssemblyName, proxy, assemblyName); + AddProxyAssociation (model, peer.InvokerTypeName, peer.InvokerAssemblyName ?? peer.AssemblyName, proxy, assemblyName); } } @@ -306,7 +307,7 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, string jniName, Hash if (peer.InvokerTypeName != null) { proxy.InvokerType = new TypeRefData { ManagedTypeName = peer.InvokerTypeName, - AssemblyName = peer.AssemblyName, + AssemblyName = peer.InvokerAssemblyName ?? peer.AssemblyName, }; proxy.InvokerActivationCtorStyle = peer.InvokerActivationCtorStyle ?? ActivationCtorStyle.XamarinAndroid; } 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/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 5f591978de3..49d6cbf809c 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -132,6 +132,11 @@ public sealed record JavaPeerInfo /// public string? InvokerTypeName { get; init; } + /// + /// Assembly containing . + /// + public string? InvokerAssemblyName { get; init; } + /// /// Activation constructor style declared by . /// Kept separate from , which describes the diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index a9b28686132..9e7080dc0ef 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -346,6 +346,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A var isUnconditional = attrInfo is not null; var cannotRegisterInStaticConstructor = attrInfo is ApplicationAttributeInfo or InstrumentationAttributeInfo; string? invokerTypeName = null; + string? invokerAssemblyName = null; ActivationCtorStyle? invokerActivationCtorStyle = null; // Resolve base Java type name @@ -365,14 +366,14 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A // For interfaces/abstract types, try to find invoker type name if (isInterface || isAbstract) { - invokerTypeName = TryFindInvokerTypeName (fullName, typeHandle, index); + (invokerTypeName, invokerAssemblyName) = TryFindInvokerType (fullName, typeHandle, index); } // Interface/abstract peers create their invoker, not the target type. // 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, invokerAssemblyName)?.Style; } var peer = new JavaPeerInfo { @@ -396,6 +397,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A JavaFields = exportFields, ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, + InvokerAssemblyName = invokerAssemblyName, InvokerActivationCtorStyle = invokerActivationCtorStyle, IsGenericDefinition = isGenericDefinition, ComponentAttribute = ToComponentInfo (attrInfo), @@ -2080,30 +2082,45 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI return typeDef.BaseType.IsNil ? null : ResolveEntityHandle (typeDef.BaseType, index); } - string? TryFindInvokerTypeName (string typeName, TypeDefinitionHandle typeHandle, AssemblyIndex index) + (string? TypeName, string? AssemblyName) TryFindInvokerType (string typeName, TypeDefinitionHandle typeHandle, AssemblyIndex index) { + if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo)) { + var explicitInvokerTypeName = registerInfo.InvokerTypeName; + if (explicitInvokerTypeName is { Length: > 0 }) { + return ParseAssemblyQualifiedTypeName (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 ParseAssemblyQualifiedTypeName (connector, index.AssemblyName); } } // Fallback: convention-based lookup — invoker type is TypeName + "Invoker" var invokerName = $"{typeName}Invoker"; if (index.TypesByFullName.ContainsKey (invokerName)) { - return invokerName; + return (invokerName, index.AssemblyName); } - return null; + return (null, null); + } + + static (string TypeName, string AssemblyName) ParseAssemblyQualifiedTypeName (string value, string defaultAssemblyName) + { + var commaIndex = value.IndexOf (','); + if (commaIndex < 0) { + return (NormalizeConnectorManagedTypeName (value), defaultAssemblyName); + } + + 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 (typeName, assemblyName); } static string NormalizeConnectorManagedTypeName (string managedTypeName) @@ -2117,8 +2134,15 @@ static string NormalizeConnectorManagedTypeName (string managedTypeName) /// 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, string? invokerAssemblyName) { + if (invokerAssemblyName is not null && + assemblyCache.TryGetValue (invokerAssemblyName, out var owningAssembly) && + owningAssembly.TypesByFullName.TryGetValue (invokerTypeName, out var owningInvokerHandle)) { + var owningInvokerDef = owningAssembly.Reader.GetTypeDefinition (owningInvokerHandle); + return ResolveActivationCtor (invokerTypeName, owningInvokerDef, owningAssembly); + } + foreach (var assembly in assemblyCache.Values) { if (!assembly.TypesByFullName.TryGetValue (invokerTypeName, out var invokerHandle)) { continue; diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalInvokerFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalInvokerFixtures.csproj new file mode 100644 index 00000000000..b089adb3218 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalInvokerFixtures.csproj @@ -0,0 +1,10 @@ + + + + $(DotNetStableTargetFramework) + latest + enable + false + + + diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalRandomAccessInvoker.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalRandomAccessInvoker.cs new file mode 100644 index 00000000000..b10e2a93498 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalRandomAccessInvoker.cs @@ -0,0 +1,6 @@ +namespace ExternalInvokerFixtures +{ + public sealed class ExternalRandomAccessInvoker + { + } +} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index d824733c2f2..ff888a472c7 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -22,15 +22,30 @@ private protected static string TestFixtureAssemblyPath { } } + private protected static string ExternalInvokerFixtureAssemblyPath { + get { + var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) + ?? throw new InvalidOperationException ("Cannot determine test assembly directory"); + var fixtureAssembly = Path.Combine (testAssemblyDir, "ExternalInvokerFixtures.dll"); + Assert.True (File.Exists (fixtureAssembly), + $"ExternalInvokerFixtures.dll not found at {fixtureAssembly}. Ensure the ExternalInvokerFixtures project builds."); + return fixtureAssembly; + } + } + static readonly Lazy<(List peers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => { using var scanner = new JavaPeerScanner (); var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); + var invokerPeReader = new PEReader (File.OpenRead (ExternalInvokerFixtureAssemblyPath)); var mdReader = peReader.GetMetadataReader (); + var invokerMdReader = invokerPeReader.GetMetadataReader (); var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); - var assemblies = new [] { (assemblyName, peReader) }; + var invokerAssemblyName = invokerMdReader.GetString (invokerMdReader.GetAssemblyDefinition ().Name); + var assemblies = new [] { (assemblyName, peReader), (invokerAssemblyName, invokerPeReader) }; var peers = scanner.Scan (assemblies); var manifestInfo = scanner.ScanAssemblyManifestInfo (); peReader.Dispose (); + invokerPeReader.Dispose (); return (peers, manifestInfo); }); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 48e55b3edc5..8fa301fd07c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -423,6 +423,24 @@ sig.ParameterTypes [0].Contains ("JniObjectReference")) { Assert.True (foundByRefCtor, "Expected to find a JI-style invoker .ctor with byref JniObjectReference parameter"); } + [Fact] + public void Generate_InvokerInAnotherAssembly_ReferencesOwningAssembly () + { + var peer = MakeInterfacePeer ("java/util/RandomAccess", "Test.IExternalRandomAccess", "Contracts", "External.Invoker") with { + InvokerAssemblyName = "ExternalInvokers", + }; + + using var stream = GenerateAssembly (new [] { peer }, "ExternalInvokerAssemblyTest"); + using var pe = new PEReader (stream); + var reader = pe.GetMetadataReader (); + var invokerReference = reader.TypeReferences + .Select (reader.GetTypeReference) + .Single (type => reader.GetString (type.Namespace) == "External" && reader.GetString (type.Name) == "Invoker"); + var assemblyReference = reader.GetAssemblyReference ((AssemblyReferenceHandle) invokerReference.ResolutionScope); + + Assert.Equal ("ExternalInvokers", reader.GetString (assemblyReference.Name)); + } + [Fact] public void Generate_UcoConstructor_InvokerUsesXamarinAndroidActivationCtor () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index f66eb72f608..c2236d73ca4 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -389,6 +389,36 @@ public void Build_PeerWithInvoker_CreatesProxy () Assert.Equal ("Android.Views.View+IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); } + [Fact] + public void Build_InvokerInAnotherAssembly_UsesInvokerAssembly () + { + var peer = MakeInterfacePeer ("java/util/RandomAccess", "MyApp.IExternalRandomAccess", "Contracts", "ExternalInvokerFixtures.ExternalRandomAccessInvoker") with { + InvokerAssemblyName = "ExternalInvokerFixtures", + }; + + var model = BuildModel (new [] { peer }, "Contracts.TypeMap"); + var proxy = Assert.Single (model.ProxyTypes); + Assert.NotNull (proxy.InvokerType); + Assert.Equal ("ExternalInvokerFixtures", proxy.InvokerType!.AssemblyName); + Assert.Contains ("ExternalInvokerFixtures", model.IgnoresAccessChecksTo); + Assert.Contains (model.Associations, + association => association.SourceTypeReference == "ExternalInvokerFixtures.ExternalRandomAccessInvoker, ExternalInvokerFixtures"); + } + + [Fact] + public void Build_AliasWithInvokerInAnotherAssembly_UsesInvokerAssemblyForAssociation () + { + var peer = MakeInterfacePeer ("java/util/RandomAccess", "MyApp.IExternalRandomAccess", "Contracts", "ExternalInvokerFixtures.ExternalRandomAccessInvoker") with { + InvokerAssemblyName = "ExternalInvokerFixtures", + }; + var alias = MakePeerWithActivation ("java/util/RandomAccess", "MyApp.RandomAccessAlias", "Aliases"); + + var model = BuildModel (new [] { peer, alias }, "Contracts.TypeMap"); + + Assert.Contains (model.Associations, + association => association.SourceTypeReference == "ExternalInvokerFixtures.ExternalRandomAccessInvoker, ExternalInvokerFixtures"); + } + [Theory] [InlineData ("MyApp.PlainActivitySubclass")] [InlineData ("MyApp.UnnamedActivity")] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj index 212fa1be735..6525b6b151f 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj @@ -12,6 +12,7 @@ + @@ -32,6 +33,7 @@ + <_TestFixtureFiles Include="ExternalInvokerFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\ExternalInvokerFixtures.dll" /> <_TestFixtureFiles Include="TestFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestFixtures.dll" /> diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index f8c9192146b..9b38fbef4c7 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -92,6 +92,29 @@ public void Scan_InvokerAndInterface_ShareJavaName () Assert.Contains (clickListenerPeers, p => p.DoNotGenerateAcw); } + [Fact] + public void Scan_JniTypeSignatureExplicitInvoker_PreservesAssemblyAndConstructorStyle () + { + var peer = FindFixtureByManagedName ("MyApp.IExternalRandomAccess"); + + Assert.Equal ("ExternalInvokerFixtures.ExternalRandomAccessInvoker", peer.InvokerTypeName); + Assert.Equal ("ExternalInvokerFixtures", peer.InvokerAssemblyName); + Assert.Null (peer.InvokerActivationCtorStyle); + } + + [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 ("TestFixtures", peer.InvokerAssemblyName); + Assert.Equal (ActivationCtorStyle.JavaInterop, 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..a2ea9263ab4 --- /dev/null +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/InvokerFixtures.cs @@ -0,0 +1,52 @@ +using ExternalInvokerFixtures; +using Java.Interop; + +namespace MyApp; + +[JniTypeSignature ("java/util/RandomAccess", GenerateJavaPeer = false, InvokerType = typeof (ExternalRandomAccessInvoker))] +public interface IExternalRandomAccess +{ +} + +[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) + { + } +} 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/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj index f7f4c72139b..0f5cdac9d34 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj @@ -10,4 +10,8 @@ true + + + + diff --git a/tests/Mono.Android-Tests/Mono.Android-Test.Library/InvokerActivationContracts.cs b/tests/Mono.Android-Tests/Mono.Android-Test.Library/InvokerActivationContracts.cs new file mode 100644 index 00000000000..59ad83ffca0 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Test.Library/InvokerActivationContracts.cs @@ -0,0 +1,13 @@ +using System; + +using Android.Runtime; + +using Java.Interop; + +namespace Mono.Android_Test.Library +{ + [Register ("java/util/RandomAccess", "", "Java.InteropTests.ExternalRandomAccessInvoker, Mono.Android.NET-Tests")] + public interface IExternalRandomAccess : IJavaPeerable, IDisposable + { + } +} 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..c2f372f3a46 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InvokerActivationTests.cs @@ -0,0 +1,216 @@ +using System; + +using Android.Runtime; + +using Java.Interop; + +using Mono.Android_Test.Library; + +using NUnit.Framework; + +namespace Java.InteropTests +{ + [TestFixture] + [Category ("InvokerActivation")] + public class InvokerActivationTests + { + [Test] + public void XamarinAndroidInterfaceInvoker_ActivatesOnceAndPreservesIdentity () + { + XamarinListInvoker.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 (XamarinListInvoker), first.GetType ()); + Assert.AreEqual (1, XamarinListInvoker.ConstructorInvocations); + first.Dispose (); + } finally { + JNIEnv.DeleteGlobalRef (handle); + } + } + + [Test] + public void JavaInteropInterfaceInvoker_ActivatesOnceAndPreservesIdentity () + { + JavaInteropCollectionInvoker.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 (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 = Java.Lang.Object.GetObject (handle, JniHandleOwnership.DoNotTransfer); + + 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 = Java.Lang.Object.GetObject (handle, JniHandleOwnership.DoNotTransfer); + + Assert.IsNotNull (peer); + Assert.AreEqual (typeof (JavaInteropAbstractListInvoker), peer.GetType ()); + Assert.AreEqual (1, JavaInteropAbstractListInvoker.ConstructorInvocations); + peer.Dispose (); + } finally { + JNIEnv.DeleteGlobalRef (handle); + } + } + + [Test] + public void InvokerInAnotherAssembly_ActivatesAndPreservesIdentity () + { + ExternalRandomAccessInvoker.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 (ExternalRandomAccessInvoker), first.GetType ()); + Assert.AreEqual (1, ExternalRandomAccessInvoker.ConstructorInvocations); + first.Dispose (); + } finally { + JNIEnv.DeleteGlobalRef (handle); + } + } + + static IntPtr CreateArrayListHandle () + { + using var list = new Java.Util.ArrayList (); + return JNIEnv.NewGlobalRef (list.Handle); + } + } + + [Register ("java/util/List", "", "Java.InteropTests.XamarinListInvoker")] + interface IXamarinList : IJavaPeerable, IDisposable + { + } + + [Register ("java/util/List", DoNotGenerateAcw = true)] + sealed class XamarinListInvoker : Java.Lang.Object, IXamarinList + { + public static int ConstructorInvocations; + + public XamarinListInvoker (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 : 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 : 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 : 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++; + } + } + + [Register ("java/util/RandomAccess", DoNotGenerateAcw = true)] + sealed class ExternalRandomAccessInvoker : Java.Lang.Object, IExternalRandomAccess + { + public static int ConstructorInvocations; + + public ExternalRandomAccessInvoker (IntPtr handle, JniHandleOwnership transfer) + : base (handle, transfer) + { + 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 @@ + From e74c66cf99a66b7e611409a56f7c6fe543d9f2d5 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 28 Aug 2026 13:52:46 +0200 Subject: [PATCH 2/3] [typemap] Limit invoker activation to supported ownership Remove speculative cross-assembly invoker support and retain same-assembly Xamarin.Android and Java.Interop constructor activation coverage. Prefer the explicitly requested interface or abstract proxy before hierarchy fallbacks so trimmable lookup matches reflection behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Generator/ModelBuilder.cs | 7 +- .../Scanner/JavaPeerInfo.cs | 5 -- .../Scanner/JavaPeerScanner.cs | 43 ++++-------- .../TrimmableTypeMap.cs | 5 ++ .../ExternalInvokerFixtures.csproj | 10 --- .../ExternalRandomAccessInvoker.cs | 6 -- .../Generator/FixtureTestBase.cs | 17 +---- .../TypeMapAssemblyGeneratorTests.cs | 18 ----- .../Generator/TypeMapModelBuilderTests.cs | 30 --------- ....Android.Sdk.TrimmableTypeMap.Tests.csproj | 2 - .../Scanner/JavaPeerScannerTests.cs | 11 --- .../TestFixtures/InvokerFixtures.cs | 6 -- .../TestFixtures/TestFixtures.csproj | 4 -- .../InvokerActivationContracts.cs | 13 ---- .../Java.Interop/InvokerActivationTests.cs | 67 ++++++------------- 15 files changed, 41 insertions(+), 203 deletions(-) delete mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalInvokerFixtures.csproj delete mode 100644 tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalRandomAccessInvoker.cs delete mode 100644 tests/Mono.Android-Tests/Mono.Android-Test.Library/InvokerActivationContracts.cs diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 2d4ecff5019..ca25d1962d7 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -97,7 +97,6 @@ public static TypeMapAssemblyData Build (IReadOnlyList peers, stri if (proxy.ActivationCtor != null && !proxy.ActivationCtor.IsOnLeafType) { AddIfCrossAssembly (referencedAssemblies, proxy.ActivationCtor.DeclaringType.AssemblyName, assemblyName); } - AddIfCrossAssembly (referencedAssemblies, proxy.InvokerType?.AssemblyName, assemblyName); } // Always include Mono.Android — the emitter calls internal JNIEnv.DeleteRef @@ -189,7 +188,7 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, AliasProxyTypeReference = holderRef, }); if (proxy != null && peer.InvokerTypeName != null) { - AddProxyAssociation (model, peer.InvokerTypeName, peer.InvokerAssemblyName ?? peer.AssemblyName, proxy, assemblyName); + AddProxyAssociation (model, peer.InvokerTypeName, peer.AssemblyName, proxy, assemblyName); } } @@ -213,7 +212,7 @@ static void AddProxyAssociation (TypeMapAssemblyData model, JavaPeerInfo peer, J { AddProxyAssociation (model, peer.ManagedTypeName, peer.AssemblyName, proxy, assemblyName); if (peer.InvokerTypeName != null) { - AddProxyAssociation (model, peer.InvokerTypeName, peer.InvokerAssemblyName ?? peer.AssemblyName, proxy, assemblyName); + AddProxyAssociation (model, peer.InvokerTypeName, peer.AssemblyName, proxy, assemblyName); } } @@ -307,7 +306,7 @@ static JavaPeerProxyData BuildProxyType (JavaPeerInfo peer, string jniName, Hash if (peer.InvokerTypeName != null) { proxy.InvokerType = new TypeRefData { ManagedTypeName = peer.InvokerTypeName, - AssemblyName = peer.InvokerAssemblyName ?? peer.AssemblyName, + AssemblyName = peer.AssemblyName, }; proxy.InvokerActivationCtorStyle = peer.InvokerActivationCtorStyle ?? ActivationCtorStyle.XamarinAndroid; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index 49d6cbf809c..5f591978de3 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -132,11 +132,6 @@ public sealed record JavaPeerInfo /// public string? InvokerTypeName { get; init; } - /// - /// Assembly containing . - /// - public string? InvokerAssemblyName { get; init; } - /// /// Activation constructor style declared by . /// Kept separate from , which describes the diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 9e7080dc0ef..600562ff102 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -346,7 +346,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A var isUnconditional = attrInfo is not null; var cannotRegisterInStaticConstructor = attrInfo is ApplicationAttributeInfo or InstrumentationAttributeInfo; string? invokerTypeName = null; - string? invokerAssemblyName = null; ActivationCtorStyle? invokerActivationCtorStyle = null; // Resolve base Java type name @@ -366,14 +365,14 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A // For interfaces/abstract types, try to find invoker type name if (isInterface || isAbstract) { - (invokerTypeName, invokerAssemblyName) = TryFindInvokerType (fullName, typeHandle, index); + invokerTypeName = TryFindInvokerTypeName (fullName, typeHandle, index); } // Interface/abstract peers create their invoker, not the target type. // 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, invokerAssemblyName)?.Style; + invokerActivationCtorStyle = TryResolveActivationCtorOnInvoker (invokerTypeName)?.Style; } var peer = new JavaPeerInfo { @@ -397,7 +396,6 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A JavaFields = exportFields, ActivationCtor = activationCtor, InvokerTypeName = invokerTypeName, - InvokerAssemblyName = invokerAssemblyName, InvokerActivationCtorStyle = invokerActivationCtorStyle, IsGenericDefinition = isGenericDefinition, ComponentAttribute = ToComponentInfo (attrInfo), @@ -2082,12 +2080,12 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI return typeDef.BaseType.IsNil ? null : ResolveEntityHandle (typeDef.BaseType, index); } - (string? TypeName, string? AssemblyName) TryFindInvokerType (string typeName, TypeDefinitionHandle typeHandle, AssemblyIndex index) + 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 ParseAssemblyQualifiedTypeName (explicitInvokerTypeName, index.AssemblyName); + return NormalizeConnectorManagedTypeName (explicitInvokerTypeName); } } @@ -2096,31 +2094,21 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI // where the connector contains the assembly-qualified invoker type name. if (registerInfo is not null && registerInfo.Connector is not null) { var connector = registerInfo.Connector; + var commaIndex = connector.IndexOf (','); + if (commaIndex > 0) { + return NormalizeConnectorManagedTypeName (connector.Substring (0, commaIndex)); + } if (connector.Length > 0) { - return ParseAssemblyQualifiedTypeName (connector, index.AssemblyName); + return NormalizeConnectorManagedTypeName (connector); } } // Fallback: convention-based lookup — invoker type is TypeName + "Invoker" var invokerName = $"{typeName}Invoker"; if (index.TypesByFullName.ContainsKey (invokerName)) { - return (invokerName, index.AssemblyName); + return invokerName; } - return (null, null); - } - - static (string TypeName, string AssemblyName) ParseAssemblyQualifiedTypeName (string value, string defaultAssemblyName) - { - var commaIndex = value.IndexOf (','); - if (commaIndex < 0) { - return (NormalizeConnectorManagedTypeName (value), defaultAssemblyName); - } - - 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 (typeName, assemblyName); + return null; } static string NormalizeConnectorManagedTypeName (string managedTypeName) @@ -2134,15 +2122,8 @@ static string NormalizeConnectorManagedTypeName (string managedTypeName) /// 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, string? invokerAssemblyName) + ActivationCtorInfo? TryResolveActivationCtorOnInvoker (string invokerTypeName) { - if (invokerAssemblyName is not null && - assemblyCache.TryGetValue (invokerAssemblyName, out var owningAssembly) && - owningAssembly.TypesByFullName.TryGetValue (invokerTypeName, out var owningInvokerHandle)) { - var owningInvokerDef = owningAssembly.Reader.GetTypeDefinition (owningInvokerHandle); - return ResolveActivationCtor (invokerTypeName, owningInvokerDef, owningAssembly); - } - foreach (var assembly in assemblyCache.Values) { if (!assembly.TypesByFullName.TryGetValue (invokerTypeName, out var invokerHandle)) { continue; 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/ExternalInvokerFixtures/ExternalInvokerFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalInvokerFixtures.csproj deleted file mode 100644 index b089adb3218..00000000000 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalInvokerFixtures.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - $(DotNetStableTargetFramework) - latest - enable - false - - - diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalRandomAccessInvoker.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalRandomAccessInvoker.cs deleted file mode 100644 index b10e2a93498..00000000000 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/ExternalInvokerFixtures/ExternalRandomAccessInvoker.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace ExternalInvokerFixtures -{ - public sealed class ExternalRandomAccessInvoker - { - } -} diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs index ff888a472c7..d824733c2f2 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/FixtureTestBase.cs @@ -22,30 +22,15 @@ private protected static string TestFixtureAssemblyPath { } } - private protected static string ExternalInvokerFixtureAssemblyPath { - get { - var testAssemblyDir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) - ?? throw new InvalidOperationException ("Cannot determine test assembly directory"); - var fixtureAssembly = Path.Combine (testAssemblyDir, "ExternalInvokerFixtures.dll"); - Assert.True (File.Exists (fixtureAssembly), - $"ExternalInvokerFixtures.dll not found at {fixtureAssembly}. Ensure the ExternalInvokerFixtures project builds."); - return fixtureAssembly; - } - } - static readonly Lazy<(List peers, AssemblyManifestInfo manifestInfo)> _cachedScanResult = new (() => { using var scanner = new JavaPeerScanner (); var peReader = new PEReader (File.OpenRead (TestFixtureAssemblyPath)); - var invokerPeReader = new PEReader (File.OpenRead (ExternalInvokerFixtureAssemblyPath)); var mdReader = peReader.GetMetadataReader (); - var invokerMdReader = invokerPeReader.GetMetadataReader (); var assemblyName = mdReader.GetString (mdReader.GetAssemblyDefinition ().Name); - var invokerAssemblyName = invokerMdReader.GetString (invokerMdReader.GetAssemblyDefinition ().Name); - var assemblies = new [] { (assemblyName, peReader), (invokerAssemblyName, invokerPeReader) }; + var assemblies = new [] { (assemblyName, peReader) }; var peers = scanner.Scan (assemblies); var manifestInfo = scanner.ScanAssemblyManifestInfo (); peReader.Dispose (); - invokerPeReader.Dispose (); return (peers, manifestInfo); }); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 8fa301fd07c..48e55b3edc5 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -423,24 +423,6 @@ sig.ParameterTypes [0].Contains ("JniObjectReference")) { Assert.True (foundByRefCtor, "Expected to find a JI-style invoker .ctor with byref JniObjectReference parameter"); } - [Fact] - public void Generate_InvokerInAnotherAssembly_ReferencesOwningAssembly () - { - var peer = MakeInterfacePeer ("java/util/RandomAccess", "Test.IExternalRandomAccess", "Contracts", "External.Invoker") with { - InvokerAssemblyName = "ExternalInvokers", - }; - - using var stream = GenerateAssembly (new [] { peer }, "ExternalInvokerAssemblyTest"); - using var pe = new PEReader (stream); - var reader = pe.GetMetadataReader (); - var invokerReference = reader.TypeReferences - .Select (reader.GetTypeReference) - .Single (type => reader.GetString (type.Namespace) == "External" && reader.GetString (type.Name) == "Invoker"); - var assemblyReference = reader.GetAssemblyReference ((AssemblyReferenceHandle) invokerReference.ResolutionScope); - - Assert.Equal ("ExternalInvokers", reader.GetString (assemblyReference.Name)); - } - [Fact] public void Generate_UcoConstructor_InvokerUsesXamarinAndroidActivationCtor () { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index c2236d73ca4..f66eb72f608 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -389,36 +389,6 @@ public void Build_PeerWithInvoker_CreatesProxy () Assert.Equal ("Android.Views.View+IOnClickListenerInvoker", proxy.InvokerType!.ManagedTypeName); } - [Fact] - public void Build_InvokerInAnotherAssembly_UsesInvokerAssembly () - { - var peer = MakeInterfacePeer ("java/util/RandomAccess", "MyApp.IExternalRandomAccess", "Contracts", "ExternalInvokerFixtures.ExternalRandomAccessInvoker") with { - InvokerAssemblyName = "ExternalInvokerFixtures", - }; - - var model = BuildModel (new [] { peer }, "Contracts.TypeMap"); - var proxy = Assert.Single (model.ProxyTypes); - Assert.NotNull (proxy.InvokerType); - Assert.Equal ("ExternalInvokerFixtures", proxy.InvokerType!.AssemblyName); - Assert.Contains ("ExternalInvokerFixtures", model.IgnoresAccessChecksTo); - Assert.Contains (model.Associations, - association => association.SourceTypeReference == "ExternalInvokerFixtures.ExternalRandomAccessInvoker, ExternalInvokerFixtures"); - } - - [Fact] - public void Build_AliasWithInvokerInAnotherAssembly_UsesInvokerAssemblyForAssociation () - { - var peer = MakeInterfacePeer ("java/util/RandomAccess", "MyApp.IExternalRandomAccess", "Contracts", "ExternalInvokerFixtures.ExternalRandomAccessInvoker") with { - InvokerAssemblyName = "ExternalInvokerFixtures", - }; - var alias = MakePeerWithActivation ("java/util/RandomAccess", "MyApp.RandomAccessAlias", "Aliases"); - - var model = BuildModel (new [] { peer, alias }, "Contracts.TypeMap"); - - Assert.Contains (model.Associations, - association => association.SourceTypeReference == "ExternalInvokerFixtures.ExternalRandomAccessInvoker, ExternalInvokerFixtures"); - } - [Theory] [InlineData ("MyApp.PlainActivitySubclass")] [InlineData ("MyApp.UnnamedActivity")] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj index 6525b6b151f..212fa1be735 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests.csproj @@ -12,7 +12,6 @@ - @@ -33,7 +32,6 @@ - <_TestFixtureFiles Include="ExternalInvokerFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\ExternalInvokerFixtures.dll" /> <_TestFixtureFiles Include="TestFixtures\bin\$(Configuration)\$(DotNetStableTargetFramework)\TestFixtures.dll" /> diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index 9b38fbef4c7..bf1b6a8127d 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -92,16 +92,6 @@ public void Scan_InvokerAndInterface_ShareJavaName () Assert.Contains (clickListenerPeers, p => p.DoNotGenerateAcw); } - [Fact] - public void Scan_JniTypeSignatureExplicitInvoker_PreservesAssemblyAndConstructorStyle () - { - var peer = FindFixtureByManagedName ("MyApp.IExternalRandomAccess"); - - Assert.Equal ("ExternalInvokerFixtures.ExternalRandomAccessInvoker", peer.InvokerTypeName); - Assert.Equal ("ExternalInvokerFixtures", peer.InvokerAssemblyName); - Assert.Null (peer.InvokerActivationCtorStyle); - } - [Theory] [InlineData ("MyApp.IExplicitJavaInteropCollection", "MyApp.ExplicitJavaInteropCollectionProxy")] [InlineData ("MyApp.IInheritedJavaInteropList", "MyApp.InheritedJavaInteropListProxy")] @@ -111,7 +101,6 @@ public void Scan_JniTypeSignatureExplicitInvoker_UsesJavaInteropConstructor (str var peer = FindFixtureByManagedName (targetType); Assert.Equal (invokerType, peer.InvokerTypeName); - Assert.Equal ("TestFixtures", peer.InvokerAssemblyName); Assert.Equal (ActivationCtorStyle.JavaInterop, peer.InvokerActivationCtorStyle); } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/InvokerFixtures.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/InvokerFixtures.cs index a2ea9263ab4..4109c12929e 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/InvokerFixtures.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/InvokerFixtures.cs @@ -1,13 +1,7 @@ -using ExternalInvokerFixtures; using Java.Interop; namespace MyApp; -[JniTypeSignature ("java/util/RandomAccess", GenerateJavaPeer = false, InvokerType = typeof (ExternalRandomAccessInvoker))] -public interface IExternalRandomAccess -{ -} - [JniTypeSignature ("java/util/Collection", GenerateJavaPeer = false, InvokerType = typeof (ExplicitJavaInteropCollectionProxy))] public interface IExplicitJavaInteropCollection { diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj index 0f5cdac9d34..f7f4c72139b 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestFixtures.csproj @@ -10,8 +10,4 @@ true - - - - diff --git a/tests/Mono.Android-Tests/Mono.Android-Test.Library/InvokerActivationContracts.cs b/tests/Mono.Android-Tests/Mono.Android-Test.Library/InvokerActivationContracts.cs deleted file mode 100644 index 59ad83ffca0..00000000000 --- a/tests/Mono.Android-Tests/Mono.Android-Test.Library/InvokerActivationContracts.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -using Android.Runtime; - -using Java.Interop; - -namespace Mono.Android_Test.Library -{ - [Register ("java/util/RandomAccess", "", "Java.InteropTests.ExternalRandomAccessInvoker, Mono.Android.NET-Tests")] - public interface IExternalRandomAccess : IJavaPeerable, IDisposable - { - } -} 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 index c2f372f3a46..61176c221cf 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InvokerActivationTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/InvokerActivationTests.cs @@ -4,8 +4,6 @@ using Java.Interop; -using Mono.Android_Test.Library; - using NUnit.Framework; namespace Java.InteropTests @@ -17,7 +15,7 @@ public class InvokerActivationTests [Test] public void XamarinAndroidInterfaceInvoker_ActivatesOnceAndPreservesIdentity () { - XamarinListInvoker.ConstructorInvocations = 0; + IXamarinListInvoker.ConstructorInvocations = 0; var handle = CreateArrayListHandle (); try { @@ -26,8 +24,8 @@ public void XamarinAndroidInterfaceInvoker_ActivatesOnceAndPreservesIdentity () Assert.IsNotNull (first); Assert.AreSame (first, second); - Assert.AreEqual (typeof (XamarinListInvoker), first.GetType ()); - Assert.AreEqual (1, XamarinListInvoker.ConstructorInvocations); + Assert.AreEqual (typeof (IXamarinListInvoker), first.GetType ()); + Assert.AreEqual (1, IXamarinListInvoker.ConstructorInvocations); first.Dispose (); } finally { JNIEnv.DeleteGlobalRef (handle); @@ -41,8 +39,8 @@ public void JavaInteropInterfaceInvoker_ActivatesOnceAndPreservesIdentity () var handle = CreateArrayListHandle (); try { - var first = Java.Lang.Object.GetObject (handle, JniHandleOwnership.DoNotTransfer); - var second = Java.Lang.Object.GetObject (handle, JniHandleOwnership.DoNotTransfer); + var first = GetValue (handle); + var second = GetValue (handle); Assert.IsNotNull (first); Assert.AreSame (first, second); @@ -61,7 +59,7 @@ public void InheritedJavaInteropInterface_UsesExplicitInvoker () var handle = CreateArrayListHandle (); try { - var peer = Java.Lang.Object.GetObject (handle, JniHandleOwnership.DoNotTransfer); + var peer = GetValue (handle); Assert.IsNotNull (peer); Assert.IsInstanceOf (peer); @@ -80,7 +78,7 @@ public void AbstractJavaInteropType_UsesExplicitInvoker () var handle = CreateArrayListHandle (); try { - var peer = Java.Lang.Object.GetObject (handle, JniHandleOwnership.DoNotTransfer); + var peer = GetValue (handle); Assert.IsNotNull (peer); Assert.AreEqual (typeof (JavaInteropAbstractListInvoker), peer.GetType ()); @@ -91,44 +89,30 @@ public void AbstractJavaInteropType_UsesExplicitInvoker () } } - [Test] - public void InvokerInAnotherAssembly_ActivatesAndPreservesIdentity () - { - ExternalRandomAccessInvoker.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 (ExternalRandomAccessInvoker), first.GetType ()); - Assert.AreEqual (1, ExternalRandomAccessInvoker.ConstructorInvocations); - first.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.XamarinListInvoker")] - interface IXamarinList : IJavaPeerable, IDisposable + [Register ("java/util/List", "", "Java.InteropTests.IXamarinListInvoker")] + interface IXamarinList : IJavaObject, IJavaPeerable, IDisposable { } [Register ("java/util/List", DoNotGenerateAcw = true)] - sealed class XamarinListInvoker : Java.Lang.Object, IXamarinList + sealed class IXamarinListInvoker : Java.Lang.Object, IXamarinList { public static int ConstructorInvocations; - public XamarinListInvoker (IntPtr handle, JniHandleOwnership transfer) + public IXamarinListInvoker (IntPtr handle, JniHandleOwnership transfer) : base (handle, transfer) { ConstructorInvocations++; @@ -141,7 +125,7 @@ interface IJavaInteropCollection : IJavaPeerable } [JniTypeSignature ("java/util/Collection", GenerateJavaPeer = false)] - sealed class JavaInteropCollectionInvoker : JavaObject, IJavaInteropCollection + sealed class JavaInteropCollectionInvoker : global::Java.Interop.JavaObject, IJavaInteropCollection { static readonly JniPeerMembers members = new JniPeerMembers ("java/util/Collection", typeof (JavaInteropCollectionInvoker)); @@ -162,7 +146,7 @@ interface IInheritedJavaInteropList : IJavaInteropCollection } [JniTypeSignature ("java/util/List", GenerateJavaPeer = false)] - sealed class InheritedJavaInteropListInvoker : JavaObject, IInheritedJavaInteropList + sealed class InheritedJavaInteropListInvoker : global::Java.Interop.JavaObject, IInheritedJavaInteropList { static readonly JniPeerMembers members = new JniPeerMembers ("java/util/List", typeof (InheritedJavaInteropListInvoker)); @@ -178,7 +162,7 @@ public InheritedJavaInteropListInvoker (ref JniObjectReference reference, JniObj } [JniTypeSignature ("java/util/AbstractList", GenerateJavaPeer = false, InvokerType = typeof (JavaInteropAbstractListInvoker))] - abstract class JavaInteropAbstractList : JavaObject + abstract class JavaInteropAbstractList : global::Java.Interop.JavaObject { static readonly JniPeerMembers members = new JniPeerMembers ("java/util/AbstractList", typeof (JavaInteropAbstractList)); @@ -202,15 +186,4 @@ public JavaInteropAbstractListInvoker (ref JniObjectReference reference, JniObje } } - [Register ("java/util/RandomAccess", DoNotGenerateAcw = true)] - sealed class ExternalRandomAccessInvoker : Java.Lang.Object, IExternalRandomAccess - { - public static int ConstructorInvocations; - - public ExternalRandomAccessInvoker (IntPtr handle, JniHandleOwnership transfer) - : base (handle, transfer) - { - ConstructorInvocations++; - } - } } From 91dacf7615fe65e7600f892db066e60f9fbd2a08 Mon Sep 17 00:00:00 2001 From: Simon Rozsival Date: Fri, 28 Aug 2026 14:00:15 +0200 Subject: [PATCH 3/3] [typemap] Restrict invokers to their declaring assembly Parse serialized invoker type names and accept only local/default assembly ownership. Ignore unsupported cross-assembly metadata and resolve activation constructors solely from the target assembly. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Scanner/JavaPeerScanner.cs | 41 +++++++++++-------- .../Scanner/JavaPeerScannerTests.cs | 9 ++++ .../TestFixtures/InvokerFixtures.cs | 5 +++ 3 files changed, 37 insertions(+), 18 deletions(-) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 600562ff102..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 { @@ -2085,7 +2085,7 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI if (index.RegisterInfoByType.TryGetValue (typeHandle, out var registerInfo)) { var explicitInvokerTypeName = registerInfo.InvokerTypeName; if (explicitInvokerTypeName is { Length: > 0 }) { - return NormalizeConnectorManagedTypeName (explicitInvokerTypeName); + return TryGetSameAssemblyTypeName (explicitInvokerTypeName, index.AssemblyName); } } @@ -2094,12 +2094,8 @@ string ManagedTypeToJniDescriptor (TypeRefData managedType, ExportParameterKindI // where the connector contains the assembly-qualified invoker type name. if (registerInfo is not null && registerInfo.Connector is not null) { var connector = registerInfo.Connector; - 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); } } @@ -2111,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/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index bf1b6a8127d..8422081cfbd 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -104,6 +104,15 @@ public void Scan_JniTypeSignatureExplicitInvoker_UsesJavaInteropConstructor (str 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 index 4109c12929e..c53169f3396 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/InvokerFixtures.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/InvokerFixtures.cs @@ -44,3 +44,8 @@ public AbstractJavaInteropListProxy (ref JniObjectReference reference, JniObject { } } + +[JniTypeSignature ("java/lang/UnsupportedExternalInvoker", GenerateJavaPeer = false, InvokerType = typeof (string))] +public interface IUnsupportedExternalInvoker +{ +}