diff --git a/build-tools/automation/yaml-templates/stage-package-tests.yaml b/build-tools/automation/yaml-templates/stage-package-tests.yaml index 869ac45cee1..47a7a9db5ed 100644 --- a/build-tools/automation/yaml-templates/stage-package-tests.yaml +++ b/build-tools/automation/yaml-templates/stage-package-tests.yaml @@ -203,6 +203,16 @@ stages: artifactSource: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)-android/Mono.Android.NET_Tests-Signed.aab artifactFolder: $(DotNetTargetFramework)-CoreCLR + - template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml + parameters: + configuration: $(XA.Build.Configuration) + testName: Mono.Android.NET_Tests-CoreCLRTrimmable + project: tests/Mono.Android-Tests/Mono.Android-Tests/Mono.Android.NET-Tests.csproj + testResultsFiles: TestResult-Mono.Android.NET_Tests-$(XA.Build.Configuration)CoreCLRTrimmable.xml + extraBuildArgs: -p:_AndroidTypeMapImplementation=trimmable -p:UseMonoRuntime=false + artifactSource: bin/Test$(XA.Build.Configuration)/$(DotNetTargetFramework)-android/Mono.Android.NET_Tests-Signed.aab + artifactFolder: $(DotNetTargetFramework)-CoreCLRTrimmable + - template: /build-tools/automation/yaml-templates/apk-instrumentation.yaml parameters: configuration: $(XA.Build.Configuration) diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs index 6a691882d6e..996ce142a13 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/ModelBuilder.cs @@ -16,6 +16,13 @@ static class ModelBuilder { const string ProxyTypeSuffix = "_Proxy"; + // Workaround for https://github.com/dotnet/runtime/issues/127004 + // When true, all TypeMap entries are emitted as 2-arg (unconditional) to avoid the + // trimmer bug that strips TypeMapAssociation attributes when a TypeMap attribute + // references the same type. Set to false once the runtime bug is fixed to re-enable + // 3-arg conditional entries that allow unused framework bindings to be trimmed away. + const bool ForceUnconditionalEntries = true; + static readonly HashSet EssentialRuntimeTypes = new (StringComparer.Ordinal) { "java/lang/Object", "java/lang/Class", @@ -122,8 +129,15 @@ static void EmitPeers (TypeMapAssemblyData model, string jniName, model.ProxyTypes.Add (proxy); } - model.Entries.Add (BuildEntry (peer, proxy, assemblyName, jniName)); - if (proxy != null && peer.IsGenericDefinition) { + var entry = BuildEntry (peer, proxy, assemblyName, jniName); + model.Entries.Add (entry); + + // Emit a TypeMapAssociation for every entry that has a proxy. + // The runtime's _proxyTypeMap (GetOrCreateProxyTypeMapping) is populated from + // TypeMapAssociationAttribute — NOT from TypeMapAttribute's 3rd arg. + // Without this, the proxy type map is empty and CreatePeer fails for + // interface types like IIterator where targetType-based lookup is needed. + if (proxy != null) { model.Associations.Add (new TypeMapAssociationData { SourceTypeReference = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName), AliasProxyTypeReference = AssemblyQualify ($"{proxy.Namespace}.{proxy.TypeName}", assemblyName), @@ -353,7 +367,9 @@ static TypeMapAttributeData BuildEntry (JavaPeerInfo peer, JavaPeerProxyData? pr proxyRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); } - bool isUnconditional = IsUnconditionalEntry (peer); + // When ForceUnconditionalEntries is true, always emit 2-arg (unconditional) TypeMap + // attributes to work around https://github.com/dotnet/runtime/issues/127004. + bool isUnconditional = ForceUnconditionalEntries || IsUnconditionalEntry (peer); string? targetRef = null; if (!isUnconditional) { targetRef = AssemblyQualify (peer.ManagedTypeName, peer.AssemblyName); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs index ba1dbc484f7..07c85992c21 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/PEAssemblyBuilder.cs @@ -313,7 +313,7 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, attrs, MethodImplAttributes.IL, Metadata.GetOrAddString (name), sigBlobHandle, - bodyOffset, default); + bodyOffset, MetadataTokens.ParameterHandle (Metadata.GetRowCount (TableIndex.Param) + 1)); } /// @@ -358,7 +358,7 @@ public MethodDefinitionHandle EmitBody (string name, MethodAttributes attrs, attrs, MethodImplAttributes.IL, Metadata.GetOrAddString (name), sigBlobHandle, - bodyOffset, default); + bodyOffset, MetadataTokens.ParameterHandle (Metadata.GetRowCount (TableIndex.Param) + 1)); } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs index 36321388253..417b1096f60 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Generator/TypeMapAssemblyEmitter.cs @@ -75,6 +75,7 @@ sealed class TypeMapAssemblyEmitter TypeReferenceHandle _iJavaPeerableRef; TypeReferenceHandle _jniHandleOwnershipRef; TypeReferenceHandle _jniObjectReferenceRef; + TypeReferenceHandle _jniObjectReferenceTypeRef; TypeReferenceHandle _jniObjectReferenceOptionsRef; TypeReferenceHandle _iAndroidCallableWrapperRef; TypeReferenceHandle _jniEnvRef; @@ -91,7 +92,7 @@ sealed class TypeMapAssemblyEmitter MemberReferenceHandle _notSupportedExceptionCtorRef; MemberReferenceHandle _jniObjectReferenceCtorRef; MemberReferenceHandle _jniEnvDeleteRefRef; - MemberReferenceHandle _withinNewObjectScopeRef; + MemberReferenceHandle _shouldSkipActivationRef; MemberReferenceHandle _ucoAttrCtorRef; BlobHandle _ucoAttrBlobHandle; MemberReferenceHandle _typeMapAttrCtorRef2Arg; @@ -206,6 +207,8 @@ void EmitTypeReferences () metadata.GetOrAddString ("Android.Runtime"), metadata.GetOrAddString ("JNIEnv")); _jniObjectReferenceRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReference")); + _jniObjectReferenceTypeRef = metadata.AddTypeReference (_javaInteropRef, + metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReferenceType")); _jniObjectReferenceOptionsRef = metadata.AddTypeReference (_javaInteropRef, metadata.GetOrAddString ("Java.Interop"), metadata.GetOrAddString ("JniObjectReferenceOptions")); _iAndroidCallableWrapperRef = metadata.AddTypeReference (_pe.MonoAndroidRef, @@ -279,10 +282,16 @@ void EmitMemberReferences () rt => rt.Void (), p => p.AddParameter ().Type ().String ())); + // JniObjectReference..ctor(IntPtr handle, JniObjectReferenceType type) + // Note: The C# constructor has a default parameter (type = Invalid), but in IL there is only + // the 2-parameter overload. We must emit both parameters explicitly. _jniObjectReferenceCtorRef = _pe.AddMemberRef (_jniObjectReferenceRef, ".ctor", - sig => sig.MethodSignature (isInstanceMethod: true).Parameters (1, + sig => sig.MethodSignature (isInstanceMethod: true).Parameters (2, rt => rt.Void (), - p => p.AddParameter ().Type ().IntPtr ())); + p => { + p.AddParameter ().Type ().IntPtr (); + p.AddParameter ().Type ().Type (_jniObjectReferenceTypeRef, true); + })); // JNIEnv.DeleteRef(IntPtr, JniHandleOwnership) — static, internal // Used by JI-style activation to clean up the original handle after constructing the peer. @@ -295,11 +304,11 @@ void EmitMemberReferences () p.AddParameter ().Type ().Type (_jniHandleOwnershipRef, true); })); - // JniEnvironment.get_WithinNewObjectScope() -> bool (static property) - _withinNewObjectScopeRef = _pe.AddMemberRef (_jniEnvironmentRef, "get_WithinNewObjectScope", - sig => sig.MethodSignature ().Parameters (0, + // JavaPeerProxy.ShouldSkipActivation(IntPtr) -> bool (static method) + _shouldSkipActivationRef = _pe.AddMemberRef (_javaPeerProxyNonGenericRef, "ShouldSkipActivation", + sig => sig.MethodSignature ().Parameters (1, rt => rt.Type ().Boolean (), - p => { })); + p => { p.AddParameter ().Type ().IntPtr (); })); // JniNativeMethod..ctor(byte*, byte*, IntPtr) _jniNativeMethodCtorRef = _pe.AddMemberRef (_jniNativeMethodRef, ".ctor", @@ -691,9 +700,10 @@ void EmitCreateInstanceViaJavaInteropNewobj (EntityHandle typeRef) EmitCreateInstanceBodyWithLocals ( EncodeJniObjectReferenceAndObjectLocals, encoder => { - // var jniRef = new JniObjectReference(handle); + // var jniRef = new JniObjectReference(handle, JniObjectReferenceType.Invalid); encoder.LoadLocalAddress (0); encoder.OpCode (ILOpCode.Ldarg_1); // handle + encoder.LoadConstantI4 (0); // JniObjectReferenceType.Invalid encoder.Call (_jniObjectReferenceCtorRef); // var result = new TargetType(ref jniRef, JniObjectReferenceOptions.Copy); @@ -738,9 +748,10 @@ void EmitCreateInstanceInheritedJavaInteropCtor (EntityHandle targetTypeRef, Act // dup obj (one copy for the call, one for the return) encoder.OpCode (ILOpCode.Dup); - // var jniRef = new JniObjectReference(handle); + // var jniRef = new JniObjectReference(handle, JniObjectReferenceType.Invalid); encoder.LoadLocalAddress (0); encoder.OpCode (ILOpCode.Ldarg_1); // handle + encoder.LoadConstantI4 (0); // JniObjectReferenceType.Invalid encoder.Call (_jniObjectReferenceCtorRef); // obj.BaseCtor(ref jniRef, JniObjectReferenceOptions.Copy); @@ -932,6 +943,7 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy enc.LoadLocalAddress (3); // jniRef enc.LoadArgument (1); // self + enc.LoadConstantI4 (0); // JniObjectReferenceType.Invalid enc.Call (_jniObjectReferenceCtorRef); if (activationCtor.IsOnLeafType) { @@ -990,7 +1002,7 @@ MethodDefinitionHandle EmitUcoConstructor (UcoConstructorData uco, JavaPeerProxy /// /// if (!JniEnvironment.BeginMarshalMethod(jnienv, out envp, out runtime)) return; /// try { - /// if (!JniEnvironment.WithinNewObjectScope) { [emitActivation] } + /// if (!JavaPeerProxy.ShouldSkipActivation(self)) { [emitActivation] } /// } catch (Exception e) { /// runtime?.OnUserUnhandledException(ref envp, e); /// } finally { @@ -1016,9 +1028,10 @@ void EmitUcoConstructorBodyWithMarshal (InstructionEncoder encoder, ControlFlowB encoder.Call (_beginMarshalMethodRef); encoder.Branch (ILOpCode.Brfalse, afterAll); - // TRY — check WithinNewObjectScope, then run activation code. + // TRY — check ShouldSkipActivation, then run activation code. encoder.MarkLabel (tryStart); - encoder.Call (_withinNewObjectScopeRef); + encoder.LoadArgument (1); // self (IntPtr) + encoder.Call (_shouldSkipActivationRef); encoder.Branch (ILOpCode.Brtrue, skipLabel); emitActivation (encoder); diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs index 190fd095d8c..1db8dfd8309 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/AssemblyIndex.cs @@ -95,6 +95,8 @@ void Build () if (attrName == "RegisterAttribute") { registerInfo = ParseRegisterAttribute (ca); registerInfo = registerInfo with { JniName = registerInfo.JniName.Replace ('.', '/') }; + } else if (attrName == "JniTypeSignatureAttribute") { + registerInfo = ParseJniTypeSignatureAttribute (ca); } else if (attrName == "ExportAttribute") { // [Export] is a method-level attribute; it is parsed at scan time by JavaPeerScanner } else if (IsKnownComponentAttribute (attrName)) { @@ -218,6 +220,28 @@ internal RegisterInfo ParseRegisterAttribute (CustomAttribute ca) return ParseRegisterInfo (DecodeAttribute (ca)); } + internal RegisterInfo ParseJniTypeSignatureAttribute (CustomAttribute ca) + { + var value = DecodeAttribute (ca); + + string jniName = ""; + bool doNotGenerateAcw = false; + + if (value.FixedArguments.Length > 0) { + jniName = (string?)value.FixedArguments [0].Value ?? ""; + } + + if (TryGetNamedArgument (value, "GenerateJavaPeer", out var generateJavaPeer)) { + doNotGenerateAcw = !generateJavaPeer; + } + + return new RegisterInfo { + JniName = jniName.Replace ('.', '/'), + DoNotGenerateAcw = doNotGenerateAcw, + IsFromJniTypeSignature = true, + }; + } + internal CustomAttributeValue DecodeAttribute (CustomAttribute ca) { return ca.DecodeValue (customAttributeTypeProvider); @@ -504,6 +528,7 @@ sealed record RegisterInfo public string? Signature { get; init; } public string? Connector { get; init; } public bool DoNotGenerateAcw { get; init; } + public bool IsFromJniTypeSignature { get; init; } } /// diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs index eff38fd1d51..bcc45d1b1c5 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerInfo.cs @@ -65,6 +65,13 @@ public sealed record JavaPeerInfo /// public bool DoNotGenerateAcw { get; init; } + /// + /// True when the type was discovered via [JniTypeSignatureAttribute] + /// rather than [RegisterAttribute]. Used to resolve cross-assembly + /// alias ownership: [Register] types take precedence. + /// + public bool IsFromJniTypeSignature { get; init; } + /// /// Types with component attributes ([Activity], [Service], etc.), /// custom views from layout XML, or manifest-declared components diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index 08ee7cd337d..2e09766a2fa 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -86,12 +86,15 @@ public List Scan (IReadOnlyList<(string Name, PEReader Reader)> as assemblyCache [index.AssemblyName] = index; } - var resultsByManagedName = new Dictionary (StringComparer.Ordinal); + // Key by (managedTypeName, assemblyName) to avoid collisions when two assemblies + // define a type with the same managed name (e.g. Java.Lang.Throwable in both + // Java.Interop and Mono.Android). + var resultsByQualifiedName = new Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> (); foreach (var index in assemblyCache.Values) { - ScanAssembly (index, resultsByManagedName); + ScanAssembly (index, resultsByQualifiedName); } - ForceUnconditionalCrossReferences (resultsByManagedName, assemblyCache); - return new List (resultsByManagedName.Values); + ForceUnconditionalCrossReferences (resultsByQualifiedName, assemblyCache); + return new List (resultsByQualifiedName.Values); } /// @@ -112,19 +115,19 @@ internal AssemblyManifestInfo ScanAssemblyManifestInfo () /// [Application(ManageSpaceActivity = typeof(X))] must be unconditional, /// because the manifest will reference them even if nothing else does. /// - static void ForceUnconditionalCrossReferences (Dictionary resultsByManagedName, Dictionary assemblyCache) + static void ForceUnconditionalCrossReferences (Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> results, Dictionary assemblyCache) { foreach (var index in assemblyCache.Values) { foreach (var attrInfo in index.AttributesByType.Values) { if (attrInfo is ApplicationAttributeInfo applicationAttributeInfo) { - ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.BackupAgent); - ForceUnconditionalIfPresent (resultsByManagedName, applicationAttributeInfo.ManageSpaceActivity); + ForceUnconditionalIfPresent (results, applicationAttributeInfo.BackupAgent); + ForceUnconditionalIfPresent (results, applicationAttributeInfo.ManageSpaceActivity); } } } } - static void ForceUnconditionalIfPresent (Dictionary resultsByManagedName, string? managedTypeName) + static void ForceUnconditionalIfPresent (Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> results, string? managedTypeName) { if (managedTypeName is null) { return; @@ -135,26 +138,27 @@ static void ForceUnconditionalIfPresent (Dictionary result return; } - // Try exact match first (handles both plain and assembly-qualified names) - if (resultsByManagedName.TryGetValue (managedTypeName, out var peer)) { - resultsByManagedName [managedTypeName] = peer with { IsUnconditional = true }; - return; - } - // TryGetTypeProperty may return assembly-qualified names like "Ns.Type, Assembly, ..." // Strip to just the type name for lookup var commaIndex = managedTypeName.IndexOf (','); - if (commaIndex <= 0) { + if (commaIndex > 0) { + managedTypeName = managedTypeName.Substring (0, commaIndex).Trim (); + } + + if (managedTypeName.Length == 0) { return; } - var typeName = managedTypeName.Substring (0, commaIndex).Trim (); - if (typeName.Length > 0 && resultsByManagedName.TryGetValue (typeName, out peer)) { - resultsByManagedName [typeName] = peer with { IsUnconditional = true }; + // Search by managed type name across all assemblies (BackupAgent/ManageSpaceActivity + // attribute values are not assembly-qualified). + foreach (var key in results.Keys) { + if (string.Equals (key.ManagedName, managedTypeName, StringComparison.Ordinal)) { + results [key] = results [key] with { IsUnconditional = true }; + } } } - void ScanAssembly (AssemblyIndex index, Dictionary results) + void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> results) { foreach (var typeHandle in index.Reader.TypeDefinitions) { var typeDef = index.Reader.GetTypeDefinition (typeHandle); @@ -237,6 +241,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results IsInterface = isInterface, IsAbstract = isAbstract, DoNotGenerateAcw = doNotGenerateAcw, + IsFromJniTypeSignature = registerInfo?.IsFromJniTypeSignature ?? false, IsUnconditional = isUnconditional, CannotRegisterInStaticConstructor = cannotRegisterInStaticConstructor, MarshalMethods = marshalMethods, @@ -248,7 +253,7 @@ void ScanAssembly (AssemblyIndex index, Dictionary results ComponentAttribute = ToComponentInfo (attrInfo), }; - results [fullName] = peer; + results [(fullName, index.AssemblyName)] = peer; } } @@ -901,7 +906,7 @@ static string GetJavaAccess (MethodAttributes access) }; } - string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary results) + string? ResolveBaseJavaName (TypeDefinition typeDef, AssemblyIndex index, Dictionary<(string ManagedName, string AssemblyName), JavaPeerInfo> results) { if (!TryResolveBaseType (typeDef, index, out var baseTypeDef, out _, out var baseIndex, out var baseTypeName, out _)) { return null; @@ -914,7 +919,7 @@ static string GetJavaAccess (MethodAttributes access) } // Fall back to already-scanned results (component-attributed or CRC64-computed peers) - if (results.TryGetValue (baseTypeName, out var basePeer)) { + if (results.TryGetValue ((baseTypeName, baseIndex.AssemblyName), out var basePeer)) { return basePeer.JavaName; } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 5d14490503a..07d689854bb 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -11,6 +11,11 @@ public class TrimmableTypeMapGenerator { readonly ITrimmableTypeMapLogger logger; + static readonly HashSet RequiredFrameworkDeferredRegistrationTypes = new (StringComparer.Ordinal) { + "android/app/Application", + "android/app/Instrumentation", + }; + public TrimmableTypeMapGenerator (ITrimmableTypeMapLogger logger) { this.logger = logger ?? throw new ArgumentNullException (nameof (logger)); @@ -41,6 +46,7 @@ public TrimmableTypeMapResult Execute ( RootManifestReferencedTypes (allPeers, PrepareManifestForRooting (manifestTemplate, manifestConfig)); PropagateDeferredRegistrationToBaseClasses (allPeers); + PropagateCannotRegisterToDescendants (allPeers); var generatedAssemblies = GenerateTypeMapAssemblies (allPeers, systemRuntimeVersion, useSharedTypemapUniverse); var jcwPeers = allPeers.Where (p => @@ -49,14 +55,7 @@ public TrimmableTypeMapResult Execute ( logger.LogGeneratingJcwFilesInfo (jcwPeers.Count, allPeers.Count); var generatedJavaSources = GenerateJcwJavaSources (jcwPeers); - // Collect Application/Instrumentation types that need deferred registerNatives - var appRegTypes = allPeers - // Include all deferred-registration peers here: framework MCWs still need - // ApplicationRegistration.java even without generated ACWs, and abstract - // base types can own the native methods that derived types invoke. - .Where (p => p.CannotRegisterInStaticConstructor) - .Select (p => JniSignatureHelper.JniNameToJavaName (p.JavaName)) - .ToList (); + var appRegTypes = CollectApplicationRegistrationTypes (allPeers); if (appRegTypes.Count > 0) { logger.LogDeferredRegistrationTypesInfo (appRegTypes.Count); } @@ -68,6 +67,33 @@ public TrimmableTypeMapResult Execute ( return new TrimmableTypeMapResult (generatedAssemblies, generatedJavaSources, allPeers, manifest, appRegTypes); } + internal static List CollectApplicationRegistrationTypes (List allPeers) + { + var appRegTypes = new List (); + var seen = new HashSet (StringComparer.Ordinal); + + foreach (var peer in allPeers) { + if (!peer.CannotRegisterInStaticConstructor) { + continue; + } + + // ApplicationRegistration.java is compiled against the app's target Android API + // surface. Legacy framework descendants such as android.test.* may not exist there, + // so keep only the two framework roots plus app/runtime types that participate in + // the deferred-registration flow. + if (peer.DoNotGenerateAcw && !RequiredFrameworkDeferredRegistrationTypes.Contains (peer.JavaName)) { + continue; + } + + var javaName = JniSignatureHelper.JniNameToJavaName (peer.JavaName); + if (seen.Add (javaName)) { + appRegTypes.Add (javaName); + } + } + + return appRegTypes; + } + GeneratedManifest GenerateManifest (List allPeers, AssemblyManifestInfo assemblyManifestInfo, ManifestConfig config, XDocument? manifestTemplate) { @@ -115,19 +141,37 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes List GenerateTypeMapAssemblies (List allPeers, Version systemRuntimeVersion, bool useSharedTypemapUniverse) { - var peersByAssembly = allPeers.GroupBy (p => p.AssemblyName, StringComparer.Ordinal).OrderBy (g => g.Key, StringComparer.Ordinal); + List<(string AssemblyName, List Peers)> peersByAssembly; + + if (useSharedTypemapUniverse) { + // In Release builds all per-assembly typemaps are merged into a single + // shared universe dictionary. Cross-assembly aliases (e.g. Java.Lang.Object + // in Mono.Android and JavaObject in Java.Interop both mapping to + // java/lang/Object) must be moved into the owner assembly's group so the + // ModelBuilder can handle them as an alias group and the runtime doesn't + // crash on duplicate keys. + peersByAssembly = MergeCrossAssemblyAliases (allPeers); + } else { + // In Debug builds each typemap DLL has its own per-assembly universe, so + // cross-assembly duplicates don't collide — simple GroupBy is sufficient. + peersByAssembly = allPeers + .GroupBy (p => p.AssemblyName, StringComparer.Ordinal) + .OrderBy (g => g.Key, StringComparer.Ordinal) + .Select (g => (g.Key, g.ToList ())) + .ToList (); + } + var generatedAssemblies = new List (); var perAssemblyNames = new List (); var generator = new TypeMapAssemblyGenerator (systemRuntimeVersion); - foreach (var group in peersByAssembly) { - string assemblyName = $"_{group.Key}.TypeMap"; - perAssemblyNames.Add (assemblyName); - var peers = group.ToList (); + foreach (var (assemblyName, peers) in peersByAssembly) { + string typeMapAssemblyName = $"_{assemblyName}.TypeMap"; + perAssemblyNames.Add (typeMapAssemblyName); var stream = new MemoryStream (); - generator.Generate (peers, stream, assemblyName, useSharedTypemapUniverse); + generator.Generate (peers, stream, typeMapAssemblyName, useSharedTypemapUniverse); stream.Position = 0; - generatedAssemblies.Add (new GeneratedAssembly (assemblyName, stream)); - logger.LogGeneratedTypeMapAssemblyInfo (assemblyName, peers.Count); + generatedAssemblies.Add (new GeneratedAssembly (typeMapAssemblyName, stream)); + logger.LogGeneratedTypeMapAssemblyInfo (typeMapAssemblyName, peers.Count); } var rootStream = new MemoryStream (); var rootGenerator = new RootTypeMapAssemblyGenerator (systemRuntimeVersion); @@ -139,6 +183,74 @@ List GenerateTypeMapAssemblies (List allPeers, return generatedAssemblies; } + /// + /// Groups peers by assembly, merging cross-assembly aliases into a single group. + /// When the same JNI name appears in multiple assemblies (e.g. Java.Lang.Object + /// in Mono.Android and JavaObject in Java.Interop both mapping + /// to java/lang/Object), peers from later assemblies are moved into the owner + /// assembly's group so the can handle them as an alias group. + /// + /// + /// Ownership is determined by [Register] over [JniTypeSignature] — the + /// canonical MCW binding type takes precedence. Among peers with the same attribute + /// kind, the first assembly in sorted order wins. + /// + internal static List<(string AssemblyName, List Peers)> MergeCrossAssemblyAliases (List allPeers) + { + var groups = new SortedDictionary> (StringComparer.Ordinal); + + // Group by assembly (sorted order) + foreach (var peer in allPeers) { + if (!groups.TryGetValue (peer.AssemblyName, out var list)) { + list = []; + groups [peer.AssemblyName] = list; + } + list.Add (peer); + } + + // Build JNI name → owner assembly map. + // [Register] types take precedence over [JniTypeSignature] types. + // Among peers of the same kind, the first assembly (sorted order) wins. + var jniNameOwner = new Dictionary (StringComparer.Ordinal); + foreach (var kvp in groups) { + string assemblyName = kvp.Key; + foreach (var peer in kvp.Value) { + if (!jniNameOwner.TryGetValue (peer.JavaName, out var current)) { + jniNameOwner [peer.JavaName] = (assemblyName, peer.IsFromJniTypeSignature); + } else if (current.IsFromJniTypeSignature && !peer.IsFromJniTypeSignature) { + // [Register] type takes ownership from [JniTypeSignature] type + jniNameOwner [peer.JavaName] = (assemblyName, false); + } + } + } + + // Move colliding peers to the owner assembly + var movedPeers = new List<(JavaPeerInfo Peer, string TargetAssembly)> (); + foreach (var kvp in groups) { + string assemblyName = kvp.Key; + foreach (var peer in kvp.Value) { + var owner = jniNameOwner [peer.JavaName]; + if (!string.Equals (owner.AssemblyName, assemblyName, StringComparison.Ordinal)) { + movedPeers.Add ((peer, owner.AssemblyName)); + } + } + } + + foreach (var moved in movedPeers) { + groups [moved.Peer.AssemblyName].Remove (moved.Peer); + groups [moved.TargetAssembly].Add (moved.Peer); + } + + // Return non-empty groups + var result = new List<(string, List)> (); + foreach (var kvp in groups) { + if (kvp.Value.Count > 0) { + result.Add ((kvp.Key, kvp.Value)); + } + } + return result; + } + List GenerateJcwJavaSources (List allPeers) { var jcwGenerator = new JcwJavaSourceGenerator (); @@ -218,8 +330,7 @@ internal void RootManifestReferencedTypes (List allPeers, XDocumen /// TestInstrumentation_1 must also defer — otherwise the base class <clinit> will call /// registerNatives before the managed runtime is ready. /// - internal static void PropagateDeferredRegistrationToBaseClasses (List allPeers) - { + internal static void PropagateDeferredRegistrationToBaseClasses (List allPeers) { // In practice only 1–2 types need propagation (one Application, maybe one // Instrumentation), each with a short base-class chain. A linear scan per // ancestor is simpler and cheaper than building a Dictionary> @@ -248,6 +359,43 @@ static void PropagateToAncestors (string? baseJniName, List allPee } } + /// + /// Propagates DOWN + /// from Application/Instrumentation types to all their descendants. Any subclass of + /// an Instrumentation/Application type can be loaded by Android before the native + /// library is ready, so it must also use the lazy __md_registerNatives pattern. + /// + internal static void PropagateCannotRegisterToDescendants (List allPeers) + { + // Build a set of JavaNames that have CannotRegisterInStaticConstructor + var cannotRegister = new HashSet (StringComparer.Ordinal); + foreach (var peer in allPeers) { + if (peer.CannotRegisterInStaticConstructor) { + cannotRegister.Add (peer.JavaName); + } + } + + // Also include the framework base types + cannotRegister.Add ("android/app/Application"); + cannotRegister.Add ("android/app/Instrumentation"); + + // Propagate to descendants: if your base is in the set, you're in the set too + bool changed = true; + while (changed) { + changed = false; + foreach (var peer in allPeers) { + if (peer.CannotRegisterInStaticConstructor || peer.BaseJavaName is null) { + continue; + } + if (cannotRegister.Contains (peer.BaseJavaName)) { + peer.CannotRegisterInStaticConstructor = true; + cannotRegister.Add (peer.JavaName); + changed = true; + } + } + } + } + static void AddPeerByDotName (Dictionary> peersByDotName, string dotName, JavaPeerInfo peer) { if (!peersByDotName.TryGetValue (dotName, out var list)) { diff --git a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs index cfe3611936c..88f7fa32244 100644 --- a/src/Mono.Android/Java.Interop/JavaPeerProxy.cs +++ b/src/Mono.Android/Java.Interop/JavaPeerProxy.cs @@ -85,6 +85,24 @@ protected JavaPeerProxy ( /// /// A factory for creating containers of the target type, or null if not supported. public virtual JavaPeerContainerFactory? GetContainerFactory () => null; + + /// + /// Returns when the UCO constructor callback should skip + /// activation because a managed peer already exists for the given JNI handle + /// (e.g., when called from FinishCreateInstance after StartCreateInstance + /// already registered the peer). + /// + public static bool ShouldSkipActivation (IntPtr jniSelf) + { + var reference = new JniObjectReference (jniSelf, JniObjectReferenceType.Invalid); + var peer = JniEnvironment.Runtime.ValueManager.PeekPeer (reference); + if (peer == null) { + return false; + } + var state = peer.JniManagedPeerState; + return (state & JniManagedPeerStates.Activatable) != JniManagedPeerStates.Activatable + && (state & JniManagedPeerStates.Replaceable) != JniManagedPeerStates.Replaceable; + } } /// diff --git a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs index 7b94c8f2184..eefb7cc2ac5 100644 --- a/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs +++ b/src/Mono.Android/Microsoft.Android.Runtime/TrimmableTypeMap.cs @@ -61,7 +61,7 @@ public static void Initialize (IReadOnlyDictionary[] typeMaps, IRe throw new ArgumentException ("At least one typemap universe must be provided.", nameof (typeMaps)); } if (typeMaps.Length != proxyMaps.Length) { - throw new ArgumentException ($"typeMaps.Length ({typeMaps.Length}) must equal proxyMaps.Length ({proxyMaps.Length})."); + throw new ArgumentException ($"typeMaps.Length ({typeMaps.Length}) must equal proxyMaps.Length ({proxyMaps.Length}).", nameof (proxyMaps)); } var universes = new SingleUniverseTypeMap [typeMaps.Length]; for (int i = 0; i < typeMaps.Length; i++) { @@ -239,7 +239,8 @@ internal bool TryGetJniNameForManagedType (Type managedType, [NotNullWhen (true) try { objClass = JniEnvironment.Types.GetObjectClass (selfRef); targetClass = JniEnvironment.Types.FindClass (targetJniName); - return JniEnvironment.Types.IsAssignableFrom (objClass, targetClass) ? proxy : null; + var isAssignable = JniEnvironment.Types.IsAssignableFrom (objClass, targetClass); + return isAssignable ? proxy : null; } finally { JniObjectReference.Dispose (ref objClass); JniObjectReference.Dispose (ref targetClass); diff --git a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets index 52ce49be5cf..ec8ad004c88 100644 --- a/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets +++ b/src/Xamarin.Android.Build.Tasks/Microsoft.Android.Sdk/targets/Microsoft.Android.Sdk.TypeMap.Trimmable.targets @@ -41,9 +41,12 @@ Generate TypeMap assemblies and JCW files. AfterTargets="CoreCompile" so it runs after compilation. Uses @(ReferencePath) as the primary input (available after compilation). + Skipped in inner per-RID builds (_OuterIntermediateOutputPath is set) because + those builds lack the manifest template and full assembly set needed for correct + deferred-registration propagation. --> @@ -92,15 +95,36 @@ + + + + <_AdditionalNativeConfigResolvedAssemblies Remove="@(_AdditionalNativeConfigResolvedAssemblies)" /> + <_AdditionalNativeConfigResolvedAssemblies Include="$(_TypeMapOutputDirectory)*.dll" /> + + + @@ -122,9 +146,10 @@ <_TypeMapFirstRid Condition=" '$(_TypeMapFirstAbi)' == 'x86' ">android-x86 - + <_ResolvedAssemblies Include="$(_TypeMapOutputDirectory)*.dll"> $(_TypeMapFirstAbi) @@ -132,6 +157,12 @@ $(_TypeMapFirstAbi)/%(Filename)%(Extension) $(_TypeMapFirstAbi)/ + <_ShrunkAssemblies Include="$(_TypeMapOutputDirectory)*.dll"> + $(_TypeMapFirstAbi) + $(_TypeMapFirstRid) + $(_TypeMapFirstAbi)/%(Filename)%(Extension) + $(_TypeMapFirstAbi)/ + @@ -175,5 +206,4 @@ - diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs index 7b01f6f08e5..92aab238757 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateNativeApplicationConfigSources.cs @@ -27,6 +27,8 @@ public class GenerateNativeApplicationConfigSources : AndroidTask [Required] public ITaskItem[] ResolvedAssemblies { get; set; } = []; + public ITaskItem[]? AdditionalResolvedAssemblies { get; set; } + public ITaskItem[]? NativeLibraries { get; set; } public ITaskItem[]? NativeLibrariesNoJniPreload { get; set; } public ITaskItem[]? NativeLibrariesAlwaysJniPreload { get; set; } @@ -202,6 +204,13 @@ public override bool RunTask () GetRequiredTokens (assembly.ItemSpec, out android_runtime_jnienv_class_token, out jnienv_initialize_method_token, out jnienv_registerjninatives_method_token); } + if (AdditionalResolvedAssemblies != null) { + foreach (ITaskItem assembly in AdditionalResolvedAssemblies) { + updateNameWidth (assembly); + updateAssemblyCount (assembly); + } + } + if (!UseAssemblyStore) { int abiNameLength = 0; foreach (string abi in SupportedAbis) { diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs index 096fd7d9f34..f5954e7e77d 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/Tasks/GenerateTrimmableTypeMapTests.cs @@ -169,6 +169,8 @@ public void Execute_ManifestPlaceholdersAreResolvedForRooting () var registrationText = File.ReadAllText (applicationRegistration); StringAssert.Contains ("mono.android.Runtime.registerNatives (android.app.Application.class);", registrationText); StringAssert.Contains ("mono.android.Runtime.registerNatives (android.app.Instrumentation.class);", registrationText); + StringAssert.DoesNotContain ("android.test.InstrumentationTestRunner.class", registrationText); + StringAssert.DoesNotContain ("android.test.mock.MockApplication.class", registrationText); Assert.IsFalse (warnings.Any (w => w.Code == "XA4250"), "Resolved placeholder-based manifest references should not log XA4250."); } diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs index 15938fd1439..70bbe93abe5 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/TrimmableTypeMapBuildTests.cs @@ -1,4 +1,6 @@ using System.IO; +using System.Linq; +using System.Text.RegularExpressions; using NUnit.Framework; using Xamarin.Android.Tasks; using Xamarin.ProjectTools; @@ -11,17 +13,15 @@ public class TrimmableTypeMapBuildTests : BaseTest { [Test] public void Build_WithTrimmableTypeMap_Succeeds () { - var proj = new XamarinAndroidApplicationProject (); + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + }; proj.SetRuntime (AndroidRuntime.CoreCLR); proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); - // Full Build will fail downstream (manifest generation not yet implemented for trimmable path), - // but _GenerateJavaStubs runs and completes before the failure point. using var builder = CreateApkBuilder (); - builder.ThrowOnBuildFailure = false; - builder.Build (proj); + Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); - // Verify _GenerateJavaStubs ran by checking typemap outputs exist var intermediateDir = builder.Output.GetIntermediaryPath ("typemap"); DirectoryAssert.Exists (intermediateDir); } @@ -29,27 +29,85 @@ public void Build_WithTrimmableTypeMap_Succeeds () [Test] public void Build_WithTrimmableTypeMap_IncrementalBuild () { - var proj = new XamarinAndroidApplicationProject (); + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + }; proj.SetRuntime (AndroidRuntime.CoreCLR); proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); - // Full Build will fail downstream (manifest generation not yet implemented for trimmable path), - // but _GenerateJavaStubs runs and completes before the failure point. using var builder = CreateApkBuilder (); - builder.ThrowOnBuildFailure = false; - builder.Build (proj); + Assert.IsTrue (builder.Build (proj), "First build should have succeeded."); - // Verify _GenerateJavaStubs ran on the first build var intermediateDir = builder.Output.GetIntermediaryPath ("typemap"); DirectoryAssert.Exists (intermediateDir); - // Second build with no changes — _GenerateJavaStubs should be skipped - builder.Build (proj); + Assert.IsTrue (builder.Build (proj), "Second build should have succeeded."); Assert.IsTrue ( builder.Output.IsTargetSkipped ("_GenerateJavaStubs"), "_GenerateJavaStubs should be skipped on incremental build."); } + [Test] + public void Build_WithTrimmableTypeMap_DoesNotHitCopyIfChangedMismatch () + { + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); + + Assert.IsFalse ( + StringAssertEx.ContainsText (builder.LastBuildOutput, "source and destination count mismatch"), + $"{builder.BuildLogFile} should not fail with XACIC7004."); + Assert.IsFalse ( + StringAssertEx.ContainsText (builder.LastBuildOutput, "Internal error: architecture"), + $"{builder.BuildLogFile} should keep trimmable typemap assemblies aligned across ABIs."); + } + + [Test] + public void Build_WithTrimmableTypeMap_AssemblyStoreMappingsStayInRange () + { + var proj = new XamarinAndroidApplicationProject { + IsRelease = true, + }; + proj.SetRuntime (AndroidRuntime.CoreCLR); + proj.SetProperty ("_AndroidTypeMapImplementation", "trimmable"); + + using var builder = CreateApkBuilder (); + Assert.IsTrue (builder.Build (proj), "Build should have succeeded."); + + var environmentFiles = Directory.GetFiles (builder.Output.GetIntermediaryPath ("android"), "environment.*.ll"); + Assert.IsNotEmpty (environmentFiles, "Expected generated environment..ll files."); + + foreach (var environmentFile in environmentFiles) { + var abi = Path.GetFileNameWithoutExtension (environmentFile).Substring ("environment.".Length); + var manifestFile = builder.Output.GetIntermediaryPath (Path.Combine ("app_shared_libraries", abi, "assembly-store.so.manifest")); + + if (!File.Exists (manifestFile)) { + continue; + } + + var environmentText = File.ReadAllText (environmentFile); + var runtimeDataMatch = Regex.Match (environmentText, @"assembly_store_bundled_assemblies.*\[(\d+)\s+x"); + Assert.IsTrue (runtimeDataMatch.Success, $"{environmentFile} should declare assembly_store_bundled_assemblies."); + + var runtimeDataCount = int.Parse (runtimeDataMatch.Groups [1].Value); + var maxMappingIndex = File.ReadLines (manifestFile) + .Select (line => Regex.Match (line, @"\bmi:(\d+)\b")) + .Where (match => match.Success) + .Select (match => int.Parse (match.Groups [1].Value)) + .Max (); + + Assert.That ( + runtimeDataCount, + Is.GreaterThan (maxMappingIndex), + $"{Path.GetFileName (environmentFile)} should allocate enough runtime slots for {Path.GetFileName (manifestFile)}."); + } + } + [Test] public void TrimmableTypeMap_PreserveList_IsPackagedInSdk () { diff --git a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets index d8d09f7aee4..eeea9637806 100644 --- a/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets +++ b/src/Xamarin.Android.Build.Tasks/Xamarin.Android.Common.targets @@ -1729,6 +1729,7 @@ because xbuild doesn't support framework reference assemblies. { + new JavaPeerInfo { + JavaName = "android/app/Application", CompatJniName = "android.app.Application", + ManagedTypeName = "Android.App.Application", ManagedTypeNamespace = "Android.App", ManagedTypeShortName = "Application", + AssemblyName = "Mono.Android", DoNotGenerateAcw = true, CannotRegisterInStaticConstructor = true, + }, + new JavaPeerInfo { + JavaName = "android/app/Instrumentation", CompatJniName = "android.app.Instrumentation", + ManagedTypeName = "Android.App.Instrumentation", ManagedTypeNamespace = "Android.App", ManagedTypeShortName = "Instrumentation", + AssemblyName = "Mono.Android", DoNotGenerateAcw = true, CannotRegisterInStaticConstructor = true, + }, + new JavaPeerInfo { + JavaName = "android/test/InstrumentationTestRunner", CompatJniName = "android.test.InstrumentationTestRunner", + ManagedTypeName = "Android.Test.InstrumentationTestRunner", ManagedTypeNamespace = "Android.Test", ManagedTypeShortName = "InstrumentationTestRunner", + AssemblyName = "Mono.Android", BaseJavaName = "android/app/Instrumentation", DoNotGenerateAcw = true, CannotRegisterInStaticConstructor = true, + }, + new JavaPeerInfo { + JavaName = "android/test/mock/MockApplication", CompatJniName = "android.test.mock.MockApplication", + ManagedTypeName = "Android.Test.Mock.MockApplication", ManagedTypeNamespace = "Android.Test.Mock", ManagedTypeShortName = "MockApplication", + AssemblyName = "Mono.Android", BaseJavaName = "android/app/Application", DoNotGenerateAcw = true, CannotRegisterInStaticConstructor = true, + }, + new JavaPeerInfo { + JavaName = "my/app/BaseInstrumentation", CompatJniName = "my.app.BaseInstrumentation", + ManagedTypeName = "My.App.BaseInstrumentation", ManagedTypeNamespace = "My.App", ManagedTypeShortName = "BaseInstrumentation", + AssemblyName = "MyApp", IsAbstract = true, CannotRegisterInStaticConstructor = true, + }, + new JavaPeerInfo { + JavaName = "my/app/MyInstrumentation", CompatJniName = "my.app.MyInstrumentation", + ManagedTypeName = "My.App.MyInstrumentation", ManagedTypeNamespace = "My.App", ManagedTypeShortName = "MyInstrumentation", + AssemblyName = "MyApp", BaseJavaName = "my/app/BaseInstrumentation", CannotRegisterInStaticConstructor = true, + }, + }; + + var types = TrimmableTypeMapGenerator.CollectApplicationRegistrationTypes (peers); + + Assert.Contains ("android.app.Application", types); + Assert.Contains ("android.app.Instrumentation", types); + Assert.Contains ("my.app.BaseInstrumentation", types); + Assert.Contains ("my.app.MyInstrumentation", types); + Assert.DoesNotContain ("android.test.InstrumentationTestRunner", types); + Assert.DoesNotContain ("android.test.mock.MockApplication", types); + } + [Fact] public void Execute_NullAssemblyList_Throws () { @@ -353,6 +399,185 @@ public void RootManifestReferencedTypes_EmptyManifest_NoChanges () Assert.False (peers [0].IsUnconditional); } + [Fact] + public void MergeCrossAssemblyAliases_RegisterTakesPrecedenceOverJniTypeSignature () + { + // Java.Interop has JavaObject with [JniTypeSignature("java/lang/Object")] + var javaInteropPeer = new JavaPeerInfo { + JavaName = "java/lang/Object", CompatJniName = "java/lang/Object", + ManagedTypeName = "Java.Interop.JavaObject", ManagedTypeNamespace = "Java.Interop", ManagedTypeShortName = "JavaObject", + AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, + }; + + // Mono.Android has Java.Lang.Object with [Register("java/lang/Object")] + var monoAndroidPeer = new JavaPeerInfo { + JavaName = "java/lang/Object", CompatJniName = "java/lang/Object", + ManagedTypeName = "Java.Lang.Object", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Object", + AssemblyName = "Mono.Android", IsFromJniTypeSignature = false, DoNotGenerateAcw = true, + }; + + // Another unique peer in Java.Interop that shouldn't be moved + var otherPeer = new JavaPeerInfo { + JavaName = "java/interop/SomeHelper", CompatJniName = "java/interop/SomeHelper", + ManagedTypeName = "Java.Interop.SomeHelper", ManagedTypeNamespace = "Java.Interop", ManagedTypeShortName = "SomeHelper", + AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, + }; + + var allPeers = new List { javaInteropPeer, monoAndroidPeer, otherPeer }; + var result = TrimmableTypeMapGenerator.MergeCrossAssemblyAliases (allPeers); + + // Both java/lang/Object peers should be in the Mono.Android group ([Register] wins) + var monoAndroidGroup = result.Single (g => g.AssemblyName == "Mono.Android"); + Assert.Equal (2, monoAndroidGroup.Peers.Count); + Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Lang.Object"); + Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Interop.JavaObject"); + + // Java.Interop should only have the unique peer + var javaInteropGroup = result.Single (g => g.AssemblyName == "Java.Interop"); + Assert.Single (javaInteropGroup.Peers); + Assert.Equal ("Java.Interop.SomeHelper", javaInteropGroup.Peers [0].ManagedTypeName); + } + + [Fact] + public void MergeCrossAssemblyAliases_NoDuplicates_NothingMoved () + { + var peer1 = new JavaPeerInfo { + JavaName = "com/example/Foo", CompatJniName = "com/example/Foo", + ManagedTypeName = "MyApp.Foo", ManagedTypeNamespace = "MyApp", ManagedTypeShortName = "Foo", + AssemblyName = "MyApp", + }; + var peer2 = new JavaPeerInfo { + JavaName = "com/example/Bar", CompatJniName = "com/example/Bar", + ManagedTypeName = "MyLib.Bar", ManagedTypeNamespace = "MyLib", ManagedTypeShortName = "Bar", + AssemblyName = "MyLib", + }; + + var result = TrimmableTypeMapGenerator.MergeCrossAssemblyAliases (new List { peer1, peer2 }); + + Assert.Equal (2, result.Count); + Assert.Single (result.Single (g => g.AssemblyName == "MyApp").Peers); + Assert.Single (result.Single (g => g.AssemblyName == "MyLib").Peers); + } + + [Fact] + public void MergeCrossAssemblyAliases_SameAssemblyAliases_NotMoved () + { + // Two peers in the same assembly with the same JNI name — within-assembly alias + // should NOT be moved; ModelBuilder handles it. + var peer1 = new JavaPeerInfo { + JavaName = "java/lang/Object", CompatJniName = "java/lang/Object", + ManagedTypeName = "Java.Lang.Object", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Object", + AssemblyName = "Mono.Android", + }; + var peer2 = new JavaPeerInfo { + JavaName = "java/lang/Object", CompatJniName = "java/lang/Object", + ManagedTypeName = "Java.Lang.IDisposable", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "IDisposable", + AssemblyName = "Mono.Android", + }; + + var result = TrimmableTypeMapGenerator.MergeCrossAssemblyAliases (new List { peer1, peer2 }); + + Assert.Single (result); + Assert.Equal (2, result [0].Peers.Count); + } + + [Fact] + public void MergeCrossAssemblyAliases_SameManagedName_DifferentAssemblies_MergedCorrectly () + { + // Reproduces the java/lang/Throwable crash: two assemblies define Java.Lang.Throwable + // with the same JNI name, plus Java.Interop.JavaException also maps to the same JNI name. + // All three should be merged into the [Register]-owning assembly's group. + var javaInteropThrowable = new JavaPeerInfo { + JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", + ManagedTypeName = "Java.Lang.Throwable", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Throwable", + AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, + }; + + var monoAndroidThrowable = new JavaPeerInfo { + JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", + ManagedTypeName = "Java.Lang.Throwable", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Throwable", + AssemblyName = "Mono.Android", IsFromJniTypeSignature = false, DoNotGenerateAcw = true, + }; + + var javaException = new JavaPeerInfo { + JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", + ManagedTypeName = "Java.Interop.JavaException", ManagedTypeNamespace = "Java.Interop", ManagedTypeShortName = "JavaException", + AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, + }; + + var allPeers = new List { javaInteropThrowable, monoAndroidThrowable, javaException }; + var result = TrimmableTypeMapGenerator.MergeCrossAssemblyAliases (allPeers); + + // All java/lang/Throwable peers should be in the Mono.Android group ([Register] wins) + var monoAndroidGroup = result.Single (g => g.AssemblyName == "Mono.Android"); + Assert.Equal (3, monoAndroidGroup.Peers.Count); + Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Lang.Throwable" && p.AssemblyName == "Mono.Android"); + Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Lang.Throwable" && p.AssemblyName == "Java.Interop"); + Assert.Contains (monoAndroidGroup.Peers, p => p.ManagedTypeName == "Java.Interop.JavaException"); + + // Java.Interop group should be empty (all peers moved to Mono.Android) + Assert.DoesNotContain (result, g => g.AssemblyName == "Java.Interop"); + } + + [Fact] + public void MergeCrossAssemblyAliases_SameManagedName_ProducesCorrectAliasGroup () + { + // End-to-end: after merging, ModelBuilder must produce a 3-way alias group + // for java/lang/Throwable with indexed entries and a single base entry, + // ensuring the runtime dictionary only sees java/lang/Throwable once. + var javaInteropThrowable = new JavaPeerInfo { + JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", + ManagedTypeName = "Java.Lang.Throwable", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Throwable", + AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, + }; + + var monoAndroidThrowable = new JavaPeerInfo { + JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", + ManagedTypeName = "Java.Lang.Throwable", ManagedTypeNamespace = "Java.Lang", ManagedTypeShortName = "Throwable", + AssemblyName = "Mono.Android", IsFromJniTypeSignature = false, DoNotGenerateAcw = true, + }; + + var javaException = new JavaPeerInfo { + JavaName = "java/lang/Throwable", CompatJniName = "java/lang/Throwable", + ManagedTypeName = "Java.Interop.JavaException", ManagedTypeNamespace = "Java.Interop", ManagedTypeShortName = "JavaException", + AssemblyName = "Java.Interop", IsFromJniTypeSignature = true, DoNotGenerateAcw = true, + }; + + var allPeers = new List { javaInteropThrowable, monoAndroidThrowable, javaException }; + var merged = TrimmableTypeMapGenerator.MergeCrossAssemblyAliases (allPeers); + + // All peers should be in the Mono.Android group + Assert.Single (merged); + var group = merged [0]; + Assert.Equal ("Mono.Android", group.AssemblyName); + Assert.Equal (3, group.Peers.Count); + + // Build the model — should produce a 3-way alias group + string typeMapAssemblyName = $"_{group.AssemblyName}.TypeMap"; + var model = ModelBuilder.Build (group.Peers, typeMapAssemblyName + ".dll", typeMapAssemblyName); + + // 3 indexed entries + 1 base entry = 4 + Assert.Equal (4, model.Entries.Count); + Assert.Equal ("java/lang/Throwable[0]", model.Entries [0].JniName); + Assert.Equal ("java/lang/Throwable[1]", model.Entries [1].JniName); + Assert.Equal ("java/lang/Throwable[2]", model.Entries [2].JniName); + Assert.Equal ("java/lang/Throwable", model.Entries [3].JniName); + + // Exactly 1 alias holder + Assert.Single (model.AliasHolders); + Assert.Equal (3, model.AliasHolders [0].AliasKeys.Count); + + // The base "java/lang/Throwable" entry points to the alias holder, not a type directly + var baseEntry = model.Entries [3]; + Assert.Contains ("_Aliases", baseEntry.ProxyTypeReference); + + // 3 associations (one per peer → alias holder) + Assert.Equal (3, model.Associations.Count); + + // The bare "java/lang/Throwable" key appears exactly once — no duplicates + Assert.Single (model.Entries, e => e.JniName == "java/lang/Throwable"); + } + static PEReader CreateTestFixturePEReader () { var dir = Path.GetDirectoryName (typeof (FixtureTestBase).Assembly.Location) diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs index 80f1dafc1f8..6994ce45ade 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapAssemblyGeneratorTests.cs @@ -296,7 +296,7 @@ public void Generate_InheritedCtor_UcoUsesGuardAndInlinedActivation () var reader = pe.GetMetadataReader (); var memberNames = GetMemberRefNames (reader); - Assert.Contains ("get_WithinNewObjectScope", memberNames); + Assert.Contains ("ShouldSkipActivation", memberNames); Assert.Contains ("GetUninitializedObject", memberNames); Assert.DoesNotContain ("ActivateInstance", memberNames); Assert.DoesNotContain ("ActivatePeerFromJavaConstructor", memberNames); diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs index bd9d1ab6b0d..651f1ea3c40 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TypeMapModelBuilderTests.cs @@ -165,14 +165,15 @@ public void Build_UserAcwType_IsUnconditional () [Fact] public void Build_McwBinding_IsTrimmable () { - // MCW binding types (DoNotGenerateAcw=true) are trimmable unless essential + // MCW binding types (DoNotGenerateAcw=true) are trimmable unless essential. + // When ForceUnconditionalEntries is enabled (workaround for dotnet/runtime#127004), + // all entries become unconditional. var peer = MakeMcwPeer ("android/app/Activity", "Android.App.Activity", "Mono.Android") with { DoNotGenerateAcw = true }; var model = BuildModel (new [] { peer }); Assert.Single (model.Entries); - Assert.False (model.Entries [0].IsUnconditional); - Assert.NotNull (model.Entries [0].TargetTypeReference); - Assert.Contains ("Android.App.Activity, Mono.Android", model.Entries [0].TargetTypeReference!); + Assert.True (model.Entries [0].IsUnconditional); + Assert.Null (model.Entries [0].TargetTypeReference); } [Fact] @@ -239,13 +240,14 @@ public void Build_PeerWithActivation_CreatesNamedProxy (string jniName, string m } [Fact] - public void Build_SinglePeer_NoAssociation () + public void Build_SinglePeer_HasAssociation () { - // Single peers don't need associations — only alias groups do + // When ForceUnconditionalEntries is enabled, single peers emit associations + // so the runtime proxy type map is populated. var peer = MakePeerWithActivation ("my/app/MainActivity", "MyApp.MainActivity", "App"); var model = BuildModel (new [] { peer }, "MyTypeMap"); - Assert.Empty (model.Associations); + Assert.Single (model.Associations); } [Fact] @@ -330,7 +332,8 @@ public void Fixture_McwBinding_IsTrimmable (string javaName) var peer = FindFixtureByJavaName (javaName); Assert.True (peer.DoNotGenerateAcw); var model = BuildModel (new [] { peer }); - Assert.False (model.Entries [0].IsUnconditional); + // ForceUnconditionalEntries workaround makes all entries unconditional + Assert.True (model.Entries [0].IsUnconditional); } } @@ -761,9 +764,8 @@ public class PeBlobValidation [Fact] public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () { - // java/lang/Object → essential → 2-arg unconditional + // With ForceUnconditionalEntries, both are emitted as 2-arg unconditional var objectPeer = FindFixtureByJavaName ("java/lang/Object"); - // android/app/Activity → MCW → 3-arg trimmable var activityPeer = FindFixtureByJavaName ("android/app/Activity"); var model = BuildModel (new [] { objectPeer, activityPeer }, "MixedBlob"); @@ -773,14 +775,13 @@ public void FullPipeline_Mixed2ArgAnd3Arg_BothSurviveRoundTrip () var attrs = ReadAllTypeMapAttributeBlobs (reader); Assert.Equal (2, attrs.Count); - var unconditional = attrs.FirstOrDefault (a => a.jniName == "java/lang/Object"); - Assert.NotNull (unconditional.jniName); - Assert.Null (unconditional.targetRef); + var objectEntry = attrs.FirstOrDefault (a => a.jniName == "java/lang/Object"); + Assert.NotNull (objectEntry.jniName); + Assert.Null (objectEntry.targetRef); - var trimmable = attrs.FirstOrDefault (a => a.jniName == "android/app/Activity"); - Assert.NotNull (trimmable.jniName); - Assert.NotNull (trimmable.targetRef); - Assert.Contains ("Android.App.Activity", trimmable.targetRef!); + var activityEntry = attrs.FirstOrDefault (a => a.jniName == "android/app/Activity"); + Assert.NotNull (activityEntry.jniName); + Assert.Null (activityEntry.targetRef); // unconditional due to ForceUnconditionalEntries }); } @@ -805,22 +806,22 @@ public void FullPipeline_UnconditionalType_Emits2ArgAttribute (string javaName, } [Fact] - public void FullPipeline_McwBinding_Emits3ArgAttribute () + public void FullPipeline_McwBinding_Emits2ArgAttribute_WithWorkaround () { - // android/app/Activity is MCW → trimmable 3-arg attribute + // With ForceUnconditionalEntries workaround for dotnet/runtime#127004, + // MCW bindings are emitted as 2-arg unconditional. var peer = FindFixtureByJavaName ("android/app/Activity"); - var model = BuildModel (new [] { peer }, "Blob3Arg"); + var model = BuildModel (new [] { peer }, "Blob2ArgWorkaround"); Assert.Single (model.Entries); - Assert.False (model.Entries [0].IsUnconditional); + Assert.True (model.Entries [0].IsUnconditional); - EmitAndVerify (model, "Blob3Arg", (pe, reader) => { + EmitAndVerify (model, "Blob2ArgWorkaround", (pe, reader) => { var (jniName, proxyRef, targetRef) = ReadFirstTypeMapAttributeBlob (reader); Assert.Equal ("android/app/Activity", jniName); Assert.NotNull (proxyRef); Assert.Contains ("Android_App_Activity_Proxy", proxyRef!); - Assert.NotNull (targetRef); - Assert.Contains ("Android.App.Activity", targetRef!); + Assert.Null (targetRef); // unconditional due to ForceUnconditionalEntries }); } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs index a3b725caecb..7d57a37657c 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Scanner/JavaPeerScannerTests.cs @@ -95,4 +95,40 @@ public void Scan_UnregisteredType_UsesCrc64PackageName (string managedName, stri { Assert.Equal (expectedJavaName, FindFixtureByManagedName (managedName).JavaName); } + + [Fact] + public void Scan_JniTypeSignature_IsDiscovered () + { + var peer = FindFixtureByJavaName ("net/dot/jni/test/JavaDisposedObject"); + Assert.Equal ("Java.Interop.TestTypes.JavaDisposedObject", peer.ManagedTypeName); + Assert.False (peer.DoNotGenerateAcw, "GenerateJavaPeer=true should map to DoNotGenerateAcw=false"); + } + + [Fact] + public void Scan_JniTypeSignature_DoNotGenerateAcw () + { + var nonGenerated = FindFixtureByJavaName ("net/dot/jni/test/MyJavaObject"); + Assert.True (nonGenerated.DoNotGenerateAcw, "NonGeneratedJavaObject has GenerateJavaPeer=false"); + } + + [Fact] + public void Scan_JniTypeSignature_DuplicateJniName_BothPresent () + { + // Java.Interop.TestTypes.JavaObject has [JniTypeSignature("java/lang/Object", GenerateJavaPeer=false)] + // and Java.Lang.Object has [Register("java/lang/Object", DoNotGenerateAcw=true)]. + // Both should be present in the scan results — alias support (PR #11122) handles + // the runtime deduplication. + var peers = ScanFixtures (); + var javaObjectPeers = peers.Where (p => p.JavaName == "java/lang/Object").ToList (); + Assert.Equal (2, javaObjectPeers.Count); + } + + [Fact] + public void Scan_JniTypeSignature_SubclassExtendsJavaPeer () + { + // JavaDisposedObject extends JavaObject which has [JniTypeSignature(GenerateJavaPeer=false)] + // The scanner should still detect JavaDisposedObject as extending a Java peer + var peer = FindFixtureByJavaName ("net/dot/jni/test/JavaDisposedObject"); + Assert.NotNull (peer); + } } diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs index 332abe00a19..ba579e4e9d1 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/StubAttributes.cs @@ -205,6 +205,20 @@ public sealed class JniConstructorSignatureAttribute : Attribute } } +namespace Java.Interop +{ + [AttributeUsage (AttributeTargets.Class, 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 JniTypeSignatureAttribute (string simpleReference) => SimpleReference = simpleReference; + } +} + namespace MyApp { [AttributeUsage (AttributeTargets.Class)] diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs index 68734a21ae2..9b220bb6d03 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/TestFixtures/TestTypes.cs @@ -948,3 +948,25 @@ public class AliasTargetExtended : Java.Lang.Object protected AliasTargetExtended (IntPtr handle, Android.Runtime.JniHandleOwnership transfer) : base (handle, transfer) { } } } + +// [JniTypeSignature] types — Java.Interop's JavaObject hierarchy +namespace Java.Interop.TestTypes +{ + [Java.Interop.JniTypeSignature ("java/lang/Object", GenerateJavaPeer = false)] + public class JavaObject + { + public JavaObject () { } + } + + [Java.Interop.JniTypeSignature ("net/dot/jni/test/JavaDisposedObject")] + public class JavaDisposedObject : JavaObject + { + public JavaDisposedObject () { } + } + + [Java.Interop.JniTypeSignature ("net/dot/jni/test/MyJavaObject", GenerateJavaPeer = false)] + public class NonGeneratedJavaObject : JavaObject + { + public NonGeneratedJavaObject () { } + } +} diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaObjectExtensionsTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaObjectExtensionsTests.cs index 1b13316cc3d..9938951cae9 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaObjectExtensionsTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JavaObjectExtensionsTests.cs @@ -15,6 +15,7 @@ namespace Java.InteropTests { [TestFixture] public class JavaObjectExtensionsTests { + // TODO: https://github.com/dotnet/android/issues/11170 — cannot create instance of open generic type under trimmable typemap [Test] public void JavaCast_BaseToGenericWrapper () { @@ -41,6 +42,7 @@ public void JavaCast_InterfaceCast () } } + // TODO: https://github.com/dotnet/android/issues/11170 — throws NotSupportedException instead of InvalidCastException under trimmable typemap [Test] public void JavaCast_BadInterfaceCast () { @@ -67,6 +69,7 @@ public void JavaCast_ObtainOriginalInstance () Assert.AreSame (list, al); } + // TODO: https://github.com/dotnet/android/issues/11170 — throws NotSupportedException instead of InvalidCastException under trimmable typemap [Test] public void JavaCast_InvalidTypeCastThrows () { @@ -75,6 +78,7 @@ public void JavaCast_InvalidTypeCastThrows () } } + // TODO: https://github.com/dotnet/android/issues/11170 — throws NotSupportedException instead of InvalidCastException under trimmable typemap [Test] public void JavaCast_CheckForManagedSubclasses () { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs index a563f56fd3a..148c7dc9383 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Interop/JnienvTest.cs @@ -121,6 +121,7 @@ public void InvokingNullInstanceDoesNotCrashDalvik () } } + // TODO: https://github.com/dotnet/android/issues/11170 — open generic creation should throw but succeeds under trimmable typemap [Test] public void NewOpenGenericTypeThrows () { @@ -301,6 +302,7 @@ public void ActivatedDirectObjectSubclassesShouldBeRegistered () } } + // TODO: https://github.com/dotnet/android/issues/11170 — throwable subclass not registered under trimmable typemap [Test] public void ActivatedDirectThrowableSubclassesShouldBeRegistered () { diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs index 764fc416379..b4d921acd17 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/Java.Lang/ObjectTest.cs @@ -19,6 +19,7 @@ namespace Java.LangTests [TestFixture] public class ObjectTest { + // TODO: https://github.com/dotnet/android/issues/11170 — trimmable typemap doesn't resolve most-derived managed type [Test] public void GetObject_ReturnsMostDerivedType () { 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 18c5032bb58..e3e89a38cff 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 @@ -29,7 +29,7 @@ NetworkInterfaces excluded: https://github.com/dotnet/runtime/issues/75155 --> - $(ExcludeCategories):CoreCLRIgnore:NTLM + $(ExcludeCategories):CoreCLRIgnore:NTLM $(ExcludeCategories):NativeAOTIgnore:SSL:NTLM:AndroidClientHandler:Export:NativeTypeMap @@ -37,6 +37,12 @@ $(ExcludeCategories):InetAccess:NetworkInterfaces + + false + CoreCLRTrimmable + $(ExcludeCategories):NativeTypeMap:Export + + @@ -68,8 +74,17 @@ + + + + + + + <_AndroidRemapMembers Include="Remaps.xml" /> <_AndroidRemapMembers Include="IsAssignableFromRemaps.xml" Condition=" '$(_AndroidIsAssignableFromCheck)' == 'false' " /> @@ -226,10 +241,6 @@ - - <_AndroidEnableObjectReferenceLogging>false - - diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/System/AppContextTests.cs b/tests/Mono.Android-Tests/Mono.Android-Tests/System/AppContextTests.cs index 05cd37eaee3..5f11ac437a8 100644 --- a/tests/Mono.Android-Tests/Mono.Android-Tests/System/AppContextTests.cs +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/System/AppContextTests.cs @@ -46,18 +46,12 @@ public void GetData (string name, string expected) /* expected */ false, #endif // !DEBUG }, - new object [] { - /* className */ "Microsoft.Android.Runtime.RuntimeFeature, Mono.Android", - /* propertyName */ "ObjectReferenceLogging", - /* expected */ false, - }, }; [Test] [Category ("NativeAOTIgnore")] // These switches only exist in Mono & CoreCLR BCL assemblies [DynamicDependency (DynamicallyAccessedMemberTypes.All, "System.LocalAppContextSwitches", "System.Private.CoreLib")] [DynamicDependency (DynamicallyAccessedMemberTypes.All, "System.Diagnostics.Metrics.Meter", "System.Diagnostics.DiagnosticSource")] - [DynamicDependency (DynamicallyAccessedMemberTypes.All, "Microsoft.Android.Runtime.RuntimeFeature", "Mono.Android")] [TestCaseSource (nameof (TestPrivateSwitchesSource))] public void TestPrivateSwitches ( [DynamicallyAccessedMembers (DynamicallyAccessedMemberTypes.All)] diff --git a/tests/Mono.Android-Tests/Mono.Android-Tests/TrimmerRoots.xml b/tests/Mono.Android-Tests/Mono.Android-Tests/TrimmerRoots.xml new file mode 100644 index 00000000000..8197e4f5994 --- /dev/null +++ b/tests/Mono.Android-Tests/Mono.Android-Tests/TrimmerRoots.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + 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 215fde081a9..63c219c712a 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 @@ -25,6 +25,76 @@ protected override string LogTag protected NUnitInstrumentation(IntPtr handle, JniHandleOwnership transfer) : base(handle, transfer) { + if (Microsoft.Android.Runtime.RuntimeFeature.TrimmableTypeMap) { + // TODO: https://github.com/dotnet/android/issues/11170 + // Tests from the external Java.Interop-Tests assembly that fail under the + // 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", + + // net.dot.jni.internal.JavaProxyObject Java class not in APK — fixture setup fails (16 tests) + "Java.InteropTests.JavaObjectArray_object_ContractTest", + + // net.dot.jni.internal.JavaProxyObject Java class not in APK + "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateArgumentState", + "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateGenericArgumentState", + "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateGenericObjectReferenceArgumentState", + "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateGenericValue", + "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateObjectReferenceArgumentState", + "Java.InteropTests.JniValueMarshaler_object_ContractTests.JniValueMarshalerContractTests`1.CreateValue", + "Java.InteropTests.JniValueMarshaler_object_ContractTests.SpecificTypesAreUsed", + + // No generated JavaPeerProxy for java/lang/Object with IJavaPeerable target type + "Java.InteropTests.JniValueMarshaler_IJavaPeerable_ContractTests.JniValueMarshalerContractTests`1.CreateGenericValue", + "Java.InteropTests.JniValueMarshaler_IJavaPeerable_ContractTests.JniValueMarshalerContractTests`1.CreateValue", + + // net.dot.jni.internal.JavaProxyThrowable — proxy throwable creation fails + "Java.InteropTests.JavaExceptionTests.InnerExceptionIsNotAProxy", + + // IJavaInterfaceInvoker ctor trimmed / missing JavaPeerProxy for test types + "Java.InteropTests.JavaPeerableExtensionsTests.JavaAs", + "Java.InteropTests.JavaPeerableExtensionsTests.JavaAs_Exceptions", + "Java.InteropTests.JavaPeerableExtensionsTests.JavaAs_InstanceThatDoesNotImplementInterfaceReturnsNull", + + // JNI method remapping not supported in trimmable typemap + "Java.InteropTests.JniPeerMembersTests.ReplaceInstanceMethodName", + "Java.InteropTests.JniPeerMembersTests.ReplaceInstanceMethodWithStaticMethod", + "Java.InteropTests.JniPeerMembersTests.ReplacementTypeUsedForMethodLookup", + "Java.InteropTests.JniPeerMembersTests.ReplaceStaticMethodName", + + // net.dot.jni.test.GenericHolder Java class not in APK + "Java.InteropTests.JniTypeManagerTests.CannotCreateGenericHolderFromJava", + + // JniPrimitiveArrayInfo lookup fails for JavaBooleanArray + "Java.InteropTests.JniTypeManagerTests.GetType", + + // net.dot.jni.test.GetThis — cannot register native members + "Java.InteropTests.JavaObjectTest.DisposeAccessesThis", + + // NotSupportedException instead of InvalidCastException — no generated JavaPeerProxy + "Java.InteropTests.JavaObjectExtensionsTests.JavaCast_BadInterfaceCast", + "Java.InteropTests.JavaObjectExtensionsTests.JavaCast_BaseToGenericWrapper", + "Java.InteropTests.JavaObjectExtensionsTests.JavaCast_CheckForManagedSubclasses", + "Java.InteropTests.JavaObjectExtensionsTests.JavaCast_InvalidTypeCastThrows", + + // Open generic type handling differs from non-trimmable + "Java.InteropTests.JnienvTest.NewOpenGenericTypeThrows", + + // Throwable subclass registration + "Java.InteropTests.JnienvTest.ActivatedDirectThrowableSubclassesShouldBeRegistered", + + // Typemap doesn't resolve most-derived type + "Java.LangTests.ObjectTest.GetObject_ReturnsMostDerivedType", + + // Instance identity after JNI round-trip + "Java.LangTests.ObjectTest.JnienvCreateInstance_RegistersMultipleInstances", + + // Global ref leak when inflating custom views + "Xamarin.Android.RuntimeTests.CustomWidgetTests.InflateCustomView_ShouldNotLeakGlobalRefs", + }; + } } protected override IList GetTestAssemblies() @@ -39,4 +109,4 @@ protected override IList GetTestAssemblies() }; } } -} \ No newline at end of file +}