diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs index 636b22e25b0..1d947153cef 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/ITrimmableTypeMapLogger.cs @@ -21,4 +21,6 @@ void LogUnresolvableJavaPeerSkippedWarning ( string unresolvedAssemblyName, string unresolvedAssemblyPath); void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName); + void LogCustomJavaObjectError (string managedTypeName); + void LogCustomJavaObjectWarning (string managedTypeName); } diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs index ec471cceef4..a9b28686132 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/Scanner/JavaPeerScanner.cs @@ -32,12 +32,14 @@ enum HashedPackageNamingPolicy { readonly ITrimmableTypeMapLogger? logger; readonly HashedPackageNamingPolicy packageNamingPolicy; readonly HashSet frameworkAssemblyNames; + readonly bool errorOnCustomJavaObject; - public JavaPeerScanner (string? packageNamingPolicy = null, ITrimmableTypeMapLogger? logger = null, HashSet? frameworkAssemblyNames = null) + public JavaPeerScanner (string? packageNamingPolicy = null, ITrimmableTypeMapLogger? logger = null, HashSet? frameworkAssemblyNames = null, bool errorOnCustomJavaObject = true) { this.packageNamingPolicy = ParsePackageNamingPolicy (packageNamingPolicy); this.logger = logger; this.frameworkAssemblyNames = frameworkAssemblyNames ?? new HashSet (StringComparer.OrdinalIgnoreCase); + this.errorOnCustomJavaObject = errorOnCustomJavaObject; } /// @@ -314,6 +316,17 @@ void ScanAssembly (AssemblyIndex index, Dictionary<(string ManagedName, string A if (ExtendsJavaPeer (typeDef, index)) { (jniName, compatJniName) = ComputeAutoJniNames (typeDef, index); } else { + // A managed class that implements Android.Runtime.IJavaObject but does not + // derive from a Java peer (Java.Lang.Object / Java.Lang.Throwable) cannot be + // marshaled to Java. Mirror the legacy XAJavaTypeScanner XA4212 diagnostic, + // which the managed/llvm-ir typemap paths raise via GenerateJavaStubs. + if (IsCustomJavaObject (typeDef, index)) { + if (errorOnCustomJavaObject) { + logger?.LogCustomJavaObjectError (fullName); + } else { + logger?.LogCustomJavaObjectWarning (fullName); + } + } continue; } } @@ -2126,6 +2139,103 @@ public void Dispose () readonly Dictionary extendsJavaPeerCache = new (StringComparer.Ordinal); + const string IJavaObjectFullName = "Android.Runtime.IJavaObject"; + + readonly Dictionary implementsIJavaObjectCache = new (StringComparer.Ordinal); + + /// + /// Determines whether a type is a "custom" Java object: a managed class that implements + /// Android.Runtime.IJavaObject but does not derive from a Java peer (Java.Lang.Object / + /// Java.Lang.Throwable). Such types cannot be marshaled and produce XA4212. Interfaces and + /// System.Exception subclasses are excluded, matching the legacy XAJavaTypeScanner. + /// + bool IsCustomJavaObject (TypeDefinition typeDef, AssemblyIndex index) + { + if ((typeDef.Attributes & TypeAttributes.Interface) != 0) { + return false; + } + if (!ImplementsIJavaObject (typeDef, index)) { + return false; + } + if (IsSubclassOfSystemException (typeDef, index)) { + return false; + } + return true; + } + + /// + /// Check whether a type implements Android.Runtime.IJavaObject, directly or through an + /// interface that extends it, or via a base class. Results are cached; false-before-recurse + /// prevents cycles. + /// + bool ImplementsIJavaObject (TypeDefinition typeDef, AssemblyIndex index) + { + var fullName = MetadataTypeNameResolver.GetFullName (typeDef, index.Reader); + var key = $"{index.AssemblyName}:{fullName}"; + + if (implementsIJavaObjectCache.TryGetValue (key, out var cached)) { + return cached; + } + + // Mark as false to prevent cycles, then compute + implementsIJavaObjectCache [key] = false; + + foreach (var implHandle in typeDef.GetInterfaceImplementations ()) { + var impl = index.Reader.GetInterfaceImplementation (implHandle); + var resolved = ResolveEntityHandle (impl.Interface, index); + if (resolved is null) { + continue; + } + + if (resolved.ManagedTypeName == IJavaObjectFullName) { + implementsIJavaObjectCache [key] = true; + return true; + } + + // Recurse into the interface's own base interfaces + if (TryResolveType (resolved.ManagedTypeName, resolved.AssemblyName, out var ifaceHandle, out var ifaceIndex)) { + var ifaceDef = ifaceIndex.Reader.GetTypeDefinition (ifaceHandle); + if (ImplementsIJavaObject (ifaceDef, ifaceIndex)) { + implementsIJavaObjectCache [key] = true; + return true; + } + } + } + + // Walk the base class chain + var baseInfo = GetBaseTypeInfo (typeDef, index); + if (baseInfo is not null && + TryResolveType (baseInfo.ManagedTypeName, baseInfo.AssemblyName, out var baseHandle, out var baseIndex)) { + var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle); + if (ImplementsIJavaObject (baseDef, baseIndex)) { + implementsIJavaObjectCache [key] = true; + return true; + } + } + + return false; + } + + /// + /// Walk the base type chain to determine whether the type derives from System.Exception. + /// + bool IsSubclassOfSystemException (TypeDefinition typeDef, AssemblyIndex index) + { + var baseInfo = GetBaseTypeInfo (typeDef, index); + int guard = 0; + while (baseInfo is not null && guard++ < 256) { + if (baseInfo.ManagedTypeName == "System.Exception") { + return true; + } + if (!TryResolveType (baseInfo.ManagedTypeName, baseInfo.AssemblyName, out var baseHandle, out var baseIndex)) { + return false; + } + var baseDef = baseIndex.Reader.GetTypeDefinition (baseHandle); + baseInfo = GetBaseTypeInfo (baseDef, baseIndex); + } + return false; + } + /// /// Check if a type extends a known Java peer (has [Register] or component attribute) /// by walking the base type chain. Results are cached; false-before-recurse prevents cycles. diff --git a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs index 143060577e4..45c65a91ef8 100644 --- a/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs +++ b/src/Microsoft.Android.Sdk.TrimmableTypeMap/TrimmableTypeMapGenerator.cs @@ -34,12 +34,13 @@ public TrimmableTypeMapResult Execute ( ManifestConfig? manifestConfig = null, XDocument? manifestTemplate = null, string? packageNamingPolicy = null, - bool generateTypeMapAssemblies = true) + bool generateTypeMapAssemblies = true, + bool errorOnCustomJavaObject = true) { _ = assemblies ?? throw new ArgumentNullException (nameof (assemblies)); _ = systemRuntimeVersion ?? throw new ArgumentNullException (nameof (systemRuntimeVersion)); _ = frameworkAssemblyNames ?? throw new ArgumentNullException (nameof (frameworkAssemblyNames)); - var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy, frameworkAssemblyNames); + var (allPeers, assemblyManifestInfo) = ScanAssemblies (assemblies, packageNamingPolicy, frameworkAssemblyNames, errorOnCustomJavaObject); if (allPeers.Count == 0) { logger.LogNoJavaPeerTypesFound (); return new TrimmableTypeMapResult ([], [], allPeers); @@ -161,9 +162,10 @@ GeneratedManifest GenerateManifest (List allPeers, AssemblyManifes (List peers, AssemblyManifestInfo manifestInfo) ScanAssemblies ( IReadOnlyList assemblies, string? packageNamingPolicy, - HashSet frameworkAssemblyNames) + HashSet frameworkAssemblyNames, + bool errorOnCustomJavaObject = true) { - using var scanner = new JavaPeerScanner (packageNamingPolicy, logger, frameworkAssemblyNames); + using var scanner = new JavaPeerScanner (packageNamingPolicy, logger, frameworkAssemblyNames, errorOnCustomJavaObject); var peers = scanner.Scan (assemblies); var manifestInfo = scanner.ScanAssemblyManifestInfo (); logger.LogJavaPeerScanInfo (assemblies.Count, peers.Count); 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 e6904adbd52..ac0ff5bbb1b 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 @@ -157,6 +157,7 @@ ManifestPlaceholders="$(AndroidManifestPlaceholders)" CheckedBuild="$(_AndroidCheckedBuild)" ApplicationJavaClass="$(AndroidApplicationJavaClass)" + ErrorOnCustomJavaObject="$(AndroidErrorOnCustomJavaObject)" GeneratedAssembliesListFile="$(_TypeMapAssembliesListFile)" AcwMapOutputFile="$(IntermediateOutputPath)acw-map.txt" ApplicationRegistrationOutputFile="$(IntermediateOutputPath)android/src/net/dot/android/ApplicationRegistration.java"> diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs index 0d77d814434..aa05531d40b 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.Designer.cs @@ -1206,6 +1206,15 @@ public static string XA4213 { } } + /// + /// Looks up a localized string similar to Type `{0}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported.. + /// + public static string XA4212 { + get { + return ResourceManager.GetString("XA4212", resourceCulture); + } + } + /// /// Looks up a localized string similar to The managed type `{0}` exists in multiple assemblies: {1}. Please refactor the managed type names in these assemblies so that they are not identical.. /// diff --git a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx index 3acea7e9bd5..d0014227f83 100644 --- a/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx +++ b/src/Xamarin.Android.Build.Tasks/Properties/Resources.resx @@ -603,6 +603,11 @@ Either change the value in the AndroidManifest.xml to match the $(SupportedOSPla The type '{0}' must provide a public default constructor + + Type `{0}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported. + The following are literal names and should not be translated: Android.Runtime.IJavaObject, Java.Lang.Object, Java.Lang.Throwable +{0} - The managed type name + The managed type `{0}` exists in multiple assemblies: {1}. Please refactor the managed type names in these assemblies so that they are not identical. {0} - The managed type name diff --git a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs index c12cba01d5e..ec5dfd550ae 100644 --- a/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs +++ b/src/Xamarin.Android.Build.Tasks/Tasks/GenerateTrimmableTypeMap.cs @@ -57,6 +57,10 @@ public void LogUnresolvableJavaPeerSkippedWarning ( log.LogCodedWarning ("XA4257", Properties.Resources.XA4257, managedTypeName, assemblyName, unresolvedTypeName, unresolvedAssemblyName, unresolvedAssemblyPath); public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) => log.LogCodedError ("XA4251", Properties.Resources.XA4251, managedTypeName); + public void LogCustomJavaObjectError (string managedTypeName) => + log.LogError ("{0}", $"XA4212: {string.Format (Properties.Resources.XA4212, managedTypeName)}"); + public void LogCustomJavaObjectWarning (string managedTypeName) => + log.LogWarning ("{0}", $"XA4212: {string.Format (Properties.Resources.XA4212, managedTypeName)}"); } public override string TaskPrefix => "GTT"; @@ -108,6 +112,13 @@ public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeN public bool GenerateTypeMapAssemblies { get; set; } = true; public bool CleanJavaSourceOutputDirectory { get; set; } + /// + /// When true (the default, from $(AndroidErrorOnCustomJavaObject)), a managed class + /// that implements Android.Runtime.IJavaObject without deriving from a Java peer is + /// reported as the XA4212 error; otherwise it is reported as a warning. + /// + public bool ErrorOnCustomJavaObject { get; set; } = true; + [Output] public ITaskItem [] GeneratedAssemblies { get; set; } = []; [Output] @@ -207,7 +218,8 @@ public override bool RunTask () manifestConfig: manifestConfig, manifestTemplate: manifestTemplate, packageNamingPolicy: PackageNamingPolicy, - generateTypeMapAssemblies: GenerateTypeMapAssemblies); + generateTypeMapAssemblies: GenerateTypeMapAssemblies, + errorOnCustomJavaObject: ErrorOnCustomJavaObject); if (GenerateTypeMapAssemblies) { GeneratedAssemblies = WriteAssembliesToDisk (result.GeneratedAssemblies, assemblyInputs.Select (i => i.Path).ToList ()); diff --git a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs index fdf9b0bbeec..fbc202dce0c 100644 --- a/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs +++ b/src/Xamarin.Android.Build.Tasks/Tests/Xamarin.Android.Build.Tests/BuildTest.cs @@ -1145,12 +1145,6 @@ public void XA4212 ([Values (AndroidRuntime.CoreCLR, AndroidRuntime.NativeAOT)] if (IgnoreUnsupportedConfiguration (runtime, release: isRelease)) { return; } - // TODO: the trimmable typemap generator (the NativeAOT default) does not yet emit the - // XA4212 custom-IJavaObject diagnostic that the managed/llvm-ir typemap paths raise via - // XAJavaTypeScanner. Re-enable once that detection is added to TrimmableTypeMapGenerator. - if (IgnoreOnNativeAot (runtime, "the trimmable typemap does not yet emit the XA4212 custom-IJavaObject diagnostic (tracked as a follow-up).")) { - return; - } var proj = new XamarinAndroidApplicationProject () { IsRelease = isRelease, diff --git a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs index fb9829ca648..5b16bd0d004 100644 --- a/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs +++ b/tests/Microsoft.Android.Sdk.TrimmableTypeMap.Tests/Generator/TrimmableTypeMapGeneratorTests.cs @@ -51,6 +51,10 @@ public void LogUnresolvableJavaPeerSkippedWarning ( $"'{unresolvedTypeName}' from '{unresolvedAssemblyName}' at '{unresolvedAssemblyPath}' could not be resolved."); public void LogJniAddNativeMethodRegistrationAttributeError (string managedTypeName) => logMessages.Add ($"XA4251: Type '{managedTypeName}' uses [JniAddNativeMethodRegistrationAttribute], which is not supported by the trimmable type map."); + public void LogCustomJavaObjectError (string managedTypeName) => + logMessages.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported."); + public void LogCustomJavaObjectWarning (string managedTypeName) => + warnings?.Add ($"XA4212: Type `{managedTypeName}` implements `Android.Runtime.IJavaObject` but does not inherit `Java.Lang.Object` or `Java.Lang.Throwable`. This is not supported."); } [Fact]